use super::*;
use crate::compare::compare_points;
use crate::utils::{f64_compare, Cap, TValue};
use crate::{AppendType, ManipulatorGroup, Subpath};
use glam::DMat2;
use std::f64::consts::PI;
impl Bezier {
pub fn to_linear(&self) -> Bezier {
Bezier::from_linear_dvec2(self.start(), self.end())
}
pub fn to_quadratic(&self) -> Bezier {
let handle = match self.handles {
BezierHandles::Linear => self.start,
BezierHandles::Quadratic { handle } => handle,
BezierHandles::Cubic { handle_start, handle_end } => (handle_start + handle_end) / 2.,
};
Bezier::from_quadratic_dvec2(self.start, handle, self.end)
}
pub fn to_cubic(&self) -> Bezier {
let (handle_start, handle_end) = match self.handles {
BezierHandles::Linear => (self.start, self.end),
BezierHandles::Quadratic { handle } => (self.start + (2. / 3.) * (handle - self.start), self.end + (2. / 3.) * (handle - self.end)),
BezierHandles::Cubic { handle_start: _, handle_end: _ } => return *self,
};
Bezier::from_cubic_dvec2(self.start, handle_start, handle_end, self.end)
}
pub fn split(&self, t: TValue) -> [Bezier; 2] {
let t = self.t_value_to_parametric(t);
let split_point = self.evaluate(TValue::Parametric(t));
match self.handles {
BezierHandles::Linear => [Bezier::from_linear_dvec2(self.start, split_point), Bezier::from_linear_dvec2(split_point, self.end)],
BezierHandles::Quadratic { handle } => {
let t_minus_one = t - 1.;
[
Bezier::from_quadratic_dvec2(self.start, t * handle - t_minus_one * self.start, split_point),
Bezier::from_quadratic_dvec2(split_point, t * self.end - t_minus_one * handle, self.end),
]
}
BezierHandles::Cubic { handle_start, handle_end } => {
let t_minus_one = t - 1.;
[
Bezier::from_cubic_dvec2(
self.start,
t * handle_start - t_minus_one * self.start,
(t * t) * handle_end - 2. * t * t_minus_one * handle_start + (t_minus_one * t_minus_one) * self.start,
split_point,
),
Bezier::from_cubic_dvec2(
split_point,
(t * t) * self.end - 2. * t * t_minus_one * handle_end + (t_minus_one * t_minus_one) * handle_start,
t * self.end - t_minus_one * handle_end,
self.end,
),
]
}
}
}
pub fn reverse(&self) -> Bezier {
match self.handles {
BezierHandles::Linear => Bezier::from_linear_dvec2(self.end, self.start),
BezierHandles::Quadratic { handle } => Bezier::from_quadratic_dvec2(self.end, handle, self.start),
BezierHandles::Cubic { handle_start, handle_end } => Bezier::from_cubic_dvec2(self.end, handle_end, handle_start, self.start),
}
}
pub fn trim(&self, t1: TValue, t2: TValue) -> Bezier {
let (mut t1, mut t2) = (self.t_value_to_parametric(t1), self.t_value_to_parametric(t2));
if f64_compare(t1, t2, MAX_ABSOLUTE_DIFFERENCE) {
let point = self.evaluate(TValue::Parametric(t1));
return match self.handles {
BezierHandles::Linear => Bezier::from_linear_dvec2(point, point),
BezierHandles::Quadratic { handle: _ } => Bezier::from_quadratic_dvec2(point, point, point),
BezierHandles::Cubic { handle_start: _, handle_end: _ } => Bezier::from_cubic_dvec2(point, point, point, point),
};
} else if t1 > t2 {
(t1, t2) = (t2, t1)
}
let bezier_ending_at_t2 = self.split(TValue::Parametric(t2))[0];
let adjusted_t1 = t1 / t2;
bezier_ending_at_t2.split(TValue::Parametric(adjusted_t1))[1]
}
pub fn apply_transformation(&self, transformation_function: impl Fn(DVec2) -> DVec2) -> Bezier {
let transformed_start = transformation_function(self.start);
let transformed_end = transformation_function(self.end);
match self.handles {
BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end),
BezierHandles::Quadratic { handle } => {
let transformed_handle = transformation_function(handle);
Bezier::from_quadratic_dvec2(transformed_start, transformed_handle, transformed_end)
}
BezierHandles::Cubic { handle_start, handle_end } => {
let transformed_handle_start = transformation_function(handle_start);
let transformed_handle_end = transformation_function(handle_end);
Bezier::from_cubic_dvec2(transformed_start, transformed_handle_start, transformed_handle_end, transformed_end)
}
}
}
pub fn rotate(&self, angle: f64) -> Bezier {
let rotation_matrix = DMat2::from_angle(angle);
self.apply_transformation(|point| rotation_matrix.mul_vec2(point))
}
pub fn rotate_about_point(&self, angle: f64, pivot: DVec2) -> Bezier {
let rotation_matrix = DMat2::from_angle(angle);
self.apply_transformation(|point| rotation_matrix.mul_vec2(point - pivot) + pivot)
}
pub fn translate(&self, translation: DVec2) -> Bezier {
self.apply_transformation(|point| point + translation)
}
fn is_scalable(&self) -> bool {
if self.handles == BezierHandles::Linear {
return true;
}
if let BezierHandles::Cubic { handle_start, handle_end } = self.handles {
let angle_1 = (self.end - self.start).angle_between(handle_start - self.start);
let angle_2 = (self.end - self.start).angle_between(handle_end - self.start);
if (angle_1 > 0. && angle_2 < 0.) || (angle_1 < 0. && angle_2 > 0.) {
return false;
}
}
let normal_0 = self.normal(TValue::Parametric(0.));
let normal_1 = self.normal(TValue::Parametric(1.));
let endpoint_normal_angle = (normal_0.x * normal_1.x + normal_0.y * normal_1.y).min(1.).acos();
endpoint_normal_angle < SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE
}
pub(crate) fn get_extrema_t_list(&self) -> Vec<f64> {
let mut extrema = self.local_extrema().into_iter().flatten().collect::<Vec<f64>>();
extrema.append(&mut vec![0., 1.]);
extrema.sort_by(|ex1, ex2| ex1.partial_cmp(ex2).unwrap());
extrema.dedup();
extrema
}
pub(crate) fn reduced_curves_and_t_values(&self, step_size: Option<f64>) -> (Vec<Bezier>, Vec<[f64; 2]>) {
if let BezierHandles::Linear = self.handles {
return (vec![*self], vec![[0., 1.]]);
}
let step_size = step_size.unwrap_or(DEFAULT_REDUCE_STEP_SIZE);
let mut extrema = self.get_extrema_t_list();
if let BezierHandles::Cubic { handle_start: _, handle_end: _ } = self.handles {
extrema.append(&mut self.inflections());
extrema.sort_by(|ex1, ex2| ex1.partial_cmp(ex2).unwrap());
}
let mut result_beziers: Vec<Bezier> = Vec::new();
let mut result_t_values: Vec<[f64; 2]> = vec![];
extrema.windows(2).for_each(|t_pair| {
let t_subcurve_start = t_pair[0];
let t_subcurve_end = t_pair[1];
let subcurve = self.trim(TValue::Parametric(t_subcurve_start), TValue::Parametric(t_subcurve_end));
if subcurve.is_scalable() {
result_beziers.push(subcurve);
result_t_values.push([t_subcurve_start, t_subcurve_end]);
return;
}
let mut segment: Bezier;
let mut t1 = 0.;
let mut t2 = step_size;
let mut is_prev_valid = false;
while t2 <= 1. + step_size {
segment = subcurve.trim(TValue::Parametric(t1), TValue::Parametric(f64::min(t2, 1.)));
if !segment.is_scalable() {
t2 -= step_size;
if is_prev_valid {
segment = subcurve.trim(TValue::Parametric(t1), TValue::Parametric(t2));
if segment.is_scalable() {
result_beziers.push(segment);
result_t_values.push([t_subcurve_start + t1 * (t_subcurve_end - t_subcurve_start), t_subcurve_start + t2 * (t_subcurve_end - t_subcurve_start)]);
} else {
t2 = t1 + step_size;
}
} else {
t2 = t1 + step_size;
}
t1 = t2;
is_prev_valid = false;
} else {
is_prev_valid = true;
}
t2 += step_size;
}
if t1 < 1. {
segment = subcurve.trim(TValue::Parametric(t1), TValue::Parametric(1.));
if segment.is_scalable() {
result_beziers.push(segment);
result_t_values.push([t_subcurve_start + t1 * (t_subcurve_end - t_subcurve_start), t_subcurve_end]);
}
}
});
(result_beziers, result_t_values)
}
pub fn reduce(&self, step_size: Option<f64>) -> Vec<Bezier> {
self.reduced_curves_and_t_values(step_size).0
}
fn scale(&self, distance: f64) -> Bezier {
assert!(self.is_scalable(), "The curve provided to scale is not scalable. Reduce the curve first.");
let normal_start = self.normal(TValue::Parametric(0.));
let normal_end = self.normal(TValue::Parametric(1.));
if normal_start.abs_diff_eq(normal_end, MAX_ABSOLUTE_DIFFERENCE) {
return self.translate(distance * normal_start);
}
let intersection = utils::line_intersection(self.start, normal_start, self.end, normal_end);
let intermediate = match self.handles {
BezierHandles::Quadratic { handle: _ } => self.to_cubic(),
_ => *self,
};
let should_flip_direction = (self.start - intersection).normalize().abs_diff_eq(normal_start, MAX_ABSOLUTE_DIFFERENCE);
intermediate.apply_transformation(|point| {
let mut direction_unit_vector = (intersection - point).normalize();
if should_flip_direction {
direction_unit_vector *= -1.;
}
point + distance * direction_unit_vector
})
}
pub fn graduated_scale(&self, start_distance: f64, end_distance: f64) -> Bezier {
assert!(self.is_scalable(), "The curve provided to scale is not scalable. Reduce the curve first.");
let intermediate = match self.handles {
BezierHandles::Quadratic { handle: _ } => self.to_cubic(),
_ => *self,
};
let normal_start = intermediate.normal(TValue::Parametric(0.));
let normal_end = intermediate.normal(TValue::Parametric(1.));
if normal_start.abs_diff_eq(normal_end, MAX_ABSOLUTE_DIFFERENCE) {
let transformed_start = utils::scale_point_from_direction_vector(intermediate.start, intermediate.normal(TValue::Parametric(0.)), false, start_distance);
let transformed_end = utils::scale_point_from_direction_vector(intermediate.end, intermediate.normal(TValue::Parametric(1.)), false, end_distance);
return match intermediate.handles {
BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end),
BezierHandles::Quadratic { handle: _ } => unreachable!(),
BezierHandles::Cubic { handle_start, handle_end } => {
let handle_start_closest_t = intermediate.project(handle_start, None);
let handle_start_scale_distance = (1. - handle_start_closest_t) * start_distance + handle_start_closest_t * end_distance;
let transformed_handle_start =
utils::scale_point_from_direction_vector(handle_start, intermediate.normal(TValue::Parametric(handle_start_closest_t)), false, handle_start_scale_distance);
let handle_end_closest_t = intermediate.project(handle_start, None);
let handle_end_scale_distance = (1. - handle_end_closest_t) * start_distance + handle_end_closest_t * end_distance;
let transformed_handle_end = utils::scale_point_from_direction_vector(handle_end, intermediate.normal(TValue::Parametric(handle_end_closest_t)), false, handle_end_scale_distance);
Bezier::from_cubic_dvec2(transformed_start, transformed_handle_start, transformed_handle_end, transformed_end)
}
};
}
let intersection = utils::line_intersection(intermediate.start, normal_start, intermediate.end, normal_end);
let should_flip_direction = (intermediate.start - intersection).normalize().abs_diff_eq(normal_start, MAX_ABSOLUTE_DIFFERENCE);
let transformed_start = utils::scale_point_from_origin(intermediate.start, intersection, should_flip_direction, start_distance);
let transformed_end = utils::scale_point_from_origin(intermediate.end, intersection, should_flip_direction, end_distance);
match intermediate.handles {
BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end),
BezierHandles::Quadratic { handle: _ } => unreachable!(),
BezierHandles::Cubic { handle_start, handle_end } => {
let handle_start_scale_distance = (start_distance * 2. + end_distance) / 3.;
let transformed_handle_start = utils::scale_point_from_origin(handle_start, intersection, should_flip_direction, handle_start_scale_distance);
let handle_end_scale_distance = (start_distance + end_distance * 2.) / 3.;
let transformed_handle_end = utils::scale_point_from_origin(handle_end, intersection, should_flip_direction, handle_end_scale_distance);
Bezier::from_cubic_dvec2(transformed_start, transformed_handle_start, transformed_handle_end, transformed_end)
}
}
}
pub fn offset<ManipulatorGroupId: crate::Identifier>(&self, distance: f64) -> Subpath<ManipulatorGroupId> {
if self.is_point() {
return Subpath::from_bezier(self);
}
let reduced = self.reduce(None);
let mut scaled = Subpath::new(vec![], false);
reduced.iter().enumerate().for_each(|(index, bezier)| {
let scaled_bezier = bezier.scale(distance);
if !bezier.is_point() {
if index > 0 && !compare_points(bezier.start(), reduced[index - 1].end()) {
scaled.append_bezier(&scaled_bezier, AppendType::SmoothJoin(MAX_ABSOLUTE_DIFFERENCE));
} else {
scaled.append_bezier(&scaled_bezier, AppendType::IgnoreStart);
}
}
});
if self.handles != BezierHandles::Linear {
scaled.smooth_open_subpath();
}
scaled
}
pub fn graduated_offset<ManipulatorGroupId: crate::Identifier>(&self, start_distance: f64, end_distance: f64) -> Subpath<ManipulatorGroupId> {
let reduced = self.reduce(None);
let mut next_start_distance = start_distance;
let distance_difference = end_distance - start_distance;
let total_length = self.length(None);
if total_length < MAX_ABSOLUTE_DIFFERENCE {
return Subpath::new(vec![], false);
}
let mut result = Subpath::new(vec![], false);
reduced.iter().enumerate().for_each(|(index, bezier)| {
if !bezier.is_point() {
let current_length = bezier.length(None);
let next_end_distance = next_start_distance + (current_length / total_length) * distance_difference;
let scaled_bezier = bezier.graduated_scale(next_start_distance, next_end_distance);
if index > 0 && !compare_points(bezier.start(), reduced[index - 1].end()) {
result.append_bezier(&scaled_bezier, AppendType::SmoothJoin(MAX_ABSOLUTE_DIFFERENCE));
} else {
result.append_bezier(&scaled_bezier, AppendType::IgnoreStart);
}
next_start_distance = next_end_distance;
}
});
if self.handles != BezierHandles::Linear {
result.smooth_open_subpath();
}
result
}
pub fn outline<ManipulatorGroupId: crate::Identifier>(&self, distance: f64, cap: Cap) -> Subpath<ManipulatorGroupId> {
let (pos_offset, neg_offset) = if self.is_point() {
(
Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::NEG_Y * distance)], false),
Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::Y * distance)], false),
)
} else {
(self.offset(distance), self.reverse().offset(distance))
};
if pos_offset.is_empty() || neg_offset.is_empty() {
return Subpath::new(vec![], false);
}
pos_offset.combine_outline(&neg_offset, cap)
}
pub fn graduated_outline<ManipulatorGroupId: crate::Identifier>(&self, start_distance: f64, end_distance: f64, cap: Cap) -> Subpath<ManipulatorGroupId> {
self.skewed_outline(start_distance, end_distance, end_distance, start_distance, cap)
}
pub fn skewed_outline<ManipulatorGroupId: crate::Identifier>(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64, cap: Cap) -> Subpath<ManipulatorGroupId> {
let (pos_offset, neg_offset) = if self.is_point() {
(
Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::NEG_Y * distance1)], false),
Subpath::new(vec![ManipulatorGroup::new_anchor(self.start() + DVec2::Y * distance1)], false),
)
} else {
(self.graduated_offset(distance1, distance2), self.reverse().graduated_offset(distance3, distance4))
};
if pos_offset.is_empty() || neg_offset.is_empty() {
return Subpath::new(vec![], false);
}
pos_offset.combine_outline(&neg_offset, cap)
}
pub fn arcs(&self, arcs_options: ArcsOptions) -> Vec<CircleArc> {
let ArcsOptions {
strategy: maximize_arcs,
error,
max_iterations,
} = arcs_options;
match maximize_arcs {
ArcStrategy::Automatic => {
let (auto_arcs, final_low_t) = self.approximate_curve_with_arcs(0., 1., error, max_iterations, true);
let arc_approximations = self.split(TValue::Parametric(final_low_t))[1].arcs(ArcsOptions {
strategy: ArcStrategy::FavorCorrectness,
error,
max_iterations,
});
if final_low_t != 1. {
[auto_arcs, arc_approximations].concat()
} else {
auto_arcs
}
}
ArcStrategy::FavorLargerArcs => self.approximate_curve_with_arcs(0., 1., error, max_iterations, false).0,
ArcStrategy::FavorCorrectness => self
.get_extrema_t_list()
.windows(2)
.flat_map(|t_pair| self.approximate_curve_with_arcs(t_pair[0], t_pair[1], error, max_iterations, false).0)
.collect::<Vec<CircleArc>>(),
}
}
fn approximate_curve_with_arcs(&self, local_low: f64, local_high: f64, error: f64, max_iterations: usize, stop_when_invalid: bool) -> (Vec<CircleArc>, f64) {
let mut low = local_low;
let mut middle = (local_low + local_high) / 2.;
let mut high = local_high;
let mut previous_high = local_high;
let mut iterations = 0;
let mut previous_arc = CircleArc::default();
let mut was_previous_good = false;
let mut arcs = Vec::new();
while low < local_high {
while iterations <= max_iterations {
iterations += 1;
let p1 = self.evaluate(TValue::Parametric(low));
let p2 = self.evaluate(TValue::Parametric(middle));
let p3 = self.evaluate(TValue::Parametric(high));
let wrapped_center = utils::compute_circle_center_from_points(p1, p2, p3);
if wrapped_center.is_none() {
previous_high = high;
low = high;
high = 1.;
middle = (low + high) / 2.;
was_previous_good = false;
break;
}
let center = wrapped_center.unwrap();
let radius = center.distance(p1);
let angle_p1 = DVec2::new(1., 0.).angle_between(p1 - center);
let angle_p2 = DVec2::new(1., 0.).angle_between(p2 - center);
let angle_p3 = DVec2::new(1., 0.).angle_between(p3 - center);
let mut start_angle = angle_p1;
let mut end_angle = angle_p3;
if angle_p1 < angle_p3 {
if angle_p2 < angle_p1 || angle_p3 < angle_p2 {
std::mem::swap(&mut start_angle, &mut end_angle);
}
} else if angle_p2 < angle_p1 && angle_p3 < angle_p2 {
std::mem::swap(&mut start_angle, &mut end_angle);
}
let new_arc = CircleArc {
center,
radius,
start_angle,
end_angle,
};
let e1 = self.evaluate(TValue::Parametric((low + middle) / 2.));
let e2 = self.evaluate(TValue::Parametric((middle + high) / 2.));
if utils::f64_compare(radius, e1.distance(center), error) && utils::f64_compare(radius, e2.distance(center), error) {
let mut sector_angle = end_angle - start_angle;
if sector_angle < 0. {
sector_angle += 2. * PI;
}
if stop_when_invalid && sector_angle > PI {
return (arcs, low);
}
if high == local_high {
arcs.push(new_arc);
low = high;
break;
}
previous_high = high;
high = (high + (high - low) / 2.).min(local_high);
middle = (low + high) / 2.;
previous_arc = new_arc;
was_previous_good = true;
} else if was_previous_good {
arcs.push(previous_arc);
low = previous_high;
high = local_high;
middle = low + (high - low) / 2.;
was_previous_good = false;
break;
} else {
previous_high = high;
high = middle;
middle = low + (high - low) / 2.;
previous_arc = new_arc;
}
}
}
(arcs, low)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compare::{compare_arcs, compare_points};
use crate::utils::{Cap, TValue};
use crate::EmptyId;
#[test]
fn test_split() {
let line = Bezier::from_linear_coordinates(25., 25., 75., 75.);
let [part1, part2] = line.split(TValue::Parametric(0.5));
assert_eq!(part1.start(), line.start());
assert_eq!(part1.end(), line.evaluate(TValue::Parametric(0.5)));
assert_eq!(part1.evaluate(TValue::Parametric(0.5)), line.evaluate(TValue::Parametric(0.25)));
assert_eq!(part2.start(), line.evaluate(TValue::Parametric(0.5)));
assert_eq!(part2.end(), line.end());
assert_eq!(part2.evaluate(TValue::Parametric(0.5)), line.evaluate(TValue::Parametric(0.75)));
let quad_bezier = Bezier::from_quadratic_coordinates(10., 10., 50., 50., 90., 10.);
let [part3, part4] = quad_bezier.split(TValue::Parametric(0.5));
assert_eq!(part3.start(), quad_bezier.start());
assert_eq!(part3.end(), quad_bezier.evaluate(TValue::Parametric(0.5)));
assert_eq!(part3.evaluate(TValue::Parametric(0.5)), quad_bezier.evaluate(TValue::Parametric(0.25)));
assert_eq!(part4.start(), quad_bezier.evaluate(TValue::Parametric(0.5)));
assert_eq!(part4.end(), quad_bezier.end());
assert_eq!(part4.evaluate(TValue::Parametric(0.5)), quad_bezier.evaluate(TValue::Parametric(0.75)));
let cubic_bezier = Bezier::from_cubic_coordinates(10., 10., 50., 50., 90., 10., 40., 50.);
let [part5, part6] = cubic_bezier.split(TValue::Parametric(0.5));
assert_eq!(part5.start(), cubic_bezier.start());
assert_eq!(part5.end(), cubic_bezier.evaluate(TValue::Parametric(0.5)));
assert_eq!(part5.evaluate(TValue::Parametric(0.5)), cubic_bezier.evaluate(TValue::Parametric(0.25)));
assert_eq!(part6.start(), cubic_bezier.evaluate(TValue::Parametric(0.5)));
assert_eq!(part6.end(), cubic_bezier.end());
assert_eq!(part6.evaluate(TValue::Parametric(0.5)), cubic_bezier.evaluate(TValue::Parametric(0.75)));
}
#[test]
fn test_split_at_anchors() {
let start = DVec2::new(30., 50.);
let end = DVec2::new(160., 170.);
let bezier_quadratic = Bezier::from_quadratic_dvec2(start, DVec2::new(140., 30.), end);
let [point_bezier1, remainder1] = bezier_quadratic.split(TValue::Parametric(0.));
assert_eq!(point_bezier1, Bezier::from_quadratic_dvec2(start, start, start));
assert!(remainder1.abs_diff_eq(&bezier_quadratic, MAX_ABSOLUTE_DIFFERENCE));
let [remainder2, point_bezier2] = bezier_quadratic.split(TValue::Parametric(1.));
assert_eq!(point_bezier2, Bezier::from_quadratic_dvec2(end, end, end));
assert!(remainder2.abs_diff_eq(&bezier_quadratic, MAX_ABSOLUTE_DIFFERENCE));
let bezier_cubic = Bezier::from_cubic_dvec2(start, DVec2::new(60., 140.), DVec2::new(150., 30.), end);
let [point_bezier3, remainder3] = bezier_cubic.split(TValue::Parametric(0.));
assert_eq!(point_bezier3, Bezier::from_cubic_dvec2(start, start, start, start));
assert!(remainder3.abs_diff_eq(&bezier_cubic, MAX_ABSOLUTE_DIFFERENCE));
let [remainder4, point_bezier4] = bezier_cubic.split(TValue::Parametric(1.));
assert_eq!(point_bezier4, Bezier::from_cubic_dvec2(end, end, end, end));
assert!(remainder4.abs_diff_eq(&bezier_cubic, MAX_ABSOLUTE_DIFFERENCE));
}
#[test]
fn test_trim() {
let line = Bezier::from_linear_coordinates(80., 80., 40., 40.);
let trimmed1 = line.trim(TValue::Parametric(0.25), TValue::Parametric(0.75));
assert_eq!(trimmed1.start(), line.evaluate(TValue::Parametric(0.25)));
assert_eq!(trimmed1.end(), line.evaluate(TValue::Parametric(0.75)));
assert_eq!(trimmed1.evaluate(TValue::Parametric(0.5)), line.evaluate(TValue::Parametric(0.5)));
let quadratic_bezier = Bezier::from_quadratic_coordinates(80., 80., 40., 40., 70., 70.);
let trimmed2 = quadratic_bezier.trim(TValue::Parametric(0.25), TValue::Parametric(0.75));
assert_eq!(trimmed2.start(), quadratic_bezier.evaluate(TValue::Parametric(0.25)));
assert_eq!(trimmed2.end(), quadratic_bezier.evaluate(TValue::Parametric(0.75)));
assert_eq!(trimmed2.evaluate(TValue::Parametric(0.5)), quadratic_bezier.evaluate(TValue::Parametric(0.5)));
let cubic_bezier = Bezier::from_cubic_coordinates(80., 80., 40., 40., 70., 70., 150., 150.);
let trimmed3 = cubic_bezier.trim(TValue::Parametric(0.25), TValue::Parametric(0.75));
assert!(trimmed3.start().abs_diff_eq(cubic_bezier.evaluate(TValue::Parametric(0.25)), MAX_ABSOLUTE_DIFFERENCE));
assert_eq!(trimmed3.end(), cubic_bezier.evaluate(TValue::Parametric(0.75)));
assert_eq!(trimmed3.evaluate(TValue::Parametric(0.5)), cubic_bezier.evaluate(TValue::Parametric(0.5)));
}
#[test]
fn test_trim_t2_greater_than_t1() {
let bezier_quadratic = Bezier::from_quadratic_coordinates(30., 50., 140., 30., 160., 170.);
let trim1 = bezier_quadratic.trim(TValue::Parametric(0.25), TValue::Parametric(0.75));
let trim2 = bezier_quadratic.trim(TValue::Parametric(0.75), TValue::Parametric(0.25));
assert!(trim1.abs_diff_eq(&trim2, MAX_ABSOLUTE_DIFFERENCE));
let bezier_cubic = Bezier::from_cubic_coordinates(30., 30., 60., 140., 150., 30., 160., 160.);
let trim3 = bezier_cubic.trim(TValue::Parametric(0.25), TValue::Parametric(0.75));
let trim4 = bezier_cubic.trim(TValue::Parametric(0.75), TValue::Parametric(0.25));
assert!(trim3.abs_diff_eq(&trim4, MAX_ABSOLUTE_DIFFERENCE));
}
#[test]
fn test_rotate() {
let bezier_linear = Bezier::from_linear_coordinates(30., 60., 140., 120.);
let rotated_bezier_linear = bezier_linear.rotate(-PI / 2.);
let expected_bezier_linear = Bezier::from_linear_coordinates(60., -30., 120., -140.);
assert!(rotated_bezier_linear.abs_diff_eq(&expected_bezier_linear, MAX_ABSOLUTE_DIFFERENCE));
let bezier_quadratic = Bezier::from_quadratic_coordinates(30., 50., 140., 30., 160., 170.);
let rotated_bezier_quadratic = bezier_quadratic.rotate(PI);
let expected_bezier_quadratic = Bezier::from_quadratic_coordinates(-30., -50., -140., -30., -160., -170.);
assert!(rotated_bezier_quadratic.abs_diff_eq(&expected_bezier_quadratic, MAX_ABSOLUTE_DIFFERENCE));
let bezier = Bezier::from_cubic_coordinates(30., 30., 60., 140., 150., 30., 160., 160.);
let rotated_bezier = bezier.rotate(PI / 2.);
let expected_bezier = Bezier::from_cubic_coordinates(-30., 30., -140., 60., -30., 150., -160., 160.);
assert!(rotated_bezier.abs_diff_eq(&expected_bezier, MAX_ABSOLUTE_DIFFERENCE));
}
#[test]
fn test_translate() {
let bezier_linear = Bezier::from_linear_coordinates(30., 60., 140., 120.);
let rotated_bezier_linear = bezier_linear.translate(DVec2::new(10., 10.));
let expected_bezier_linear = Bezier::from_linear_coordinates(40., 70., 150., 130.);
assert!(rotated_bezier_linear.abs_diff_eq(&expected_bezier_linear, MAX_ABSOLUTE_DIFFERENCE));
let bezier_quadratic = Bezier::from_quadratic_coordinates(30., 50., 140., 30., 160., 170.);
let rotated_bezier_quadratic = bezier_quadratic.translate(DVec2::new(-10., 10.));
let expected_bezier_quadratic = Bezier::from_quadratic_coordinates(20., 60., 130., 40., 150., 180.);
assert!(rotated_bezier_quadratic.abs_diff_eq(&expected_bezier_quadratic, MAX_ABSOLUTE_DIFFERENCE));
let bezier = Bezier::from_cubic_coordinates(30., 30., 60., 140., 150., 30., 160., 160.);
let translated_bezier = bezier.translate(DVec2::new(10., -10.));
let expected_bezier = Bezier::from_cubic_coordinates(40., 20., 70., 130., 160., 20., 170., 150.);
assert!(translated_bezier.abs_diff_eq(&expected_bezier, MAX_ABSOLUTE_DIFFERENCE));
}
#[test]
fn test_reduce() {
let p1 = DVec2::new(0., 0.);
let p2 = DVec2::new(50., 50.);
let p3 = DVec2::new(0., 0.);
let bezier = Bezier::from_quadratic_dvec2(p1, p2, p3);
let reduced_curves = bezier.reduce(None);
assert!(reduced_curves.iter().all(|bezier| bezier.is_scalable()));
let (helper_curves, helper_t_values) = bezier.reduced_curves_and_t_values(None);
assert!(reduced_curves
.iter()
.zip(helper_curves.iter())
.all(|(bezier1, bezier2)| bezier1.abs_diff_eq(bezier2, MAX_ABSOLUTE_DIFFERENCE)));
assert!(reduced_curves
.iter()
.zip(helper_t_values.iter())
.all(|(curve, t_pair)| curve.abs_diff_eq(&bezier.trim(TValue::Parametric(t_pair[0]), TValue::Parametric(t_pair[1])), MAX_ABSOLUTE_DIFFERENCE)))
}
fn assert_valid_offset<ManipulatorGroupId: crate::Identifier>(bezier: &Bezier, offset: &Subpath<ManipulatorGroupId>, expected_distance: f64) {
if offset.len() > 1 {
offset.iter().take(offset.len() - 2).zip(offset.iter().skip(1)).for_each(|beziers_pair| {
assert!(compare_points(beziers_pair.0.end, beziers_pair.1.start));
assert!(compare_points(beziers_pair.0.normal(TValue::Parametric(1.)), beziers_pair.1.normal(TValue::Parametric(0.))));
});
}
let start_distance = bezier.evaluate(TValue::Parametric(0.)).distance(offset.iter().next().unwrap().evaluate(TValue::Parametric(0.)));
assert!(f64_compare(start_distance, expected_distance, MAX_ABSOLUTE_DIFFERENCE));
let end_distance = bezier.evaluate(TValue::Parametric(1.)).distance(offset.iter().last().unwrap().evaluate(TValue::Parametric(1.)));
assert!(f64_compare(end_distance, expected_distance, MAX_ABSOLUTE_DIFFERENCE));
let err_threshold = expected_distance / 10.;
let t_values: Vec<f64> = offset
.iter()
.flat_map(|offset_segment| {
[0.1, 0.25, 0.5, 0.75, 0.9]
.iter()
.map(|t| {
let offset_point = offset_segment.evaluate(TValue::Parametric(*t));
let closest_point_t = bezier.project(offset_point, None);
let closest_point = bezier.evaluate(TValue::Parametric(closest_point_t));
let actual_distance = offset_point.distance(closest_point);
assert!(f64_compare(actual_distance, expected_distance, err_threshold));
closest_point_t
})
.collect::<Vec<f64>>()
})
.collect();
for i in 1..t_values.len() {
assert!(t_values[i - 1] < t_values[i]);
}
}
#[test]
fn test_offset_linear() {
let start = DVec2::new(30., 60.);
let end = DVec2::new(140., 120.);
let bezier = Bezier::from_linear_dvec2(start, end);
for distance in [-20., -10., 10., 20.] {
let offset = bezier.offset::<EmptyId>(distance);
assert_valid_offset(&bezier, &offset, distance.abs());
}
}
#[test]
fn test_offset_quadratic() {
let start = DVec2::new(30., 50.);
let handle = DVec2::new(140., 30.);
let end = DVec2::new(160., 170.);
let bezier = Bezier::from_quadratic_dvec2(start, handle, end);
for distance in [-20., -10., 10., 20.] {
let offset = bezier.offset::<EmptyId>(distance);
assert_valid_offset(&bezier, &offset, distance.abs());
}
}
#[test]
fn test_offset_cubic() {
let start = DVec2::new(30., 30.);
let handle1 = DVec2::new(60., 140.);
let handle2 = DVec2::new(150., 30.);
let end = DVec2::new(160., 160.);
let bezier = Bezier::from_cubic_dvec2(start, handle1, handle2, end);
for distance in [-20., -10., 10., 20.] {
let offset = bezier.offset::<EmptyId>(distance);
assert_valid_offset(&bezier, &offset, distance.abs());
}
}
#[test]
fn test_offset_curve_that_has_a_single_point_after_reduce() {
let p1 = DVec2::new(30., 30.);
let p2 = DVec2::new(150., 29.);
let p3 = DVec2::new(150., 30.);
let p4 = DVec2::new(160., 160.);
let bezier = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
let reduce = bezier.reduce(None);
let offset = bezier.offset::<EmptyId>(15.);
assert!(reduce.last().is_some());
assert!(reduce.last().unwrap().is_point());
assert_eq!(reduce.len(), offset.len_segments() + 1);
}
#[test]
fn test_outline() {
let p1 = DVec2::new(30., 50.);
let p2 = DVec2::new(140., 30.);
let line = Bezier::from_linear_dvec2(p1, p2);
let outline = line.outline::<EmptyId>(10., Cap::Butt);
assert_eq!(outline.len(), 4);
assert!(f64_compare(
outline.iter().next().unwrap().evaluate(TValue::Parametric(0.25)).distance(line.evaluate(TValue::Parametric(0.25))),
10.,
MAX_ABSOLUTE_DIFFERENCE
)); assert!(outline.iter().nth(1).unwrap().evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.end(), MAX_ABSOLUTE_DIFFERENCE));
assert!(f64_compare(
outline.iter().nth(2).unwrap().evaluate(TValue::Parametric(0.25)).distance(line.evaluate(TValue::Parametric(0.75))),
10.,
MAX_ABSOLUTE_DIFFERENCE
)); assert!(outline.iter().nth(3).unwrap().evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.start(), MAX_ABSOLUTE_DIFFERENCE));
}
#[test]
fn test_outline_single_point_circle() {
let ellipse: Subpath<EmptyId> = Subpath::new_ellipse(DVec2::new(0., 0.), DVec2::new(50., 50.)).reverse();
let p = DVec2::new(25., 25.);
let line = Bezier::from_linear_dvec2(p, p);
let outline = line.outline::<EmptyId>(25., Cap::Round);
assert_eq!(outline, ellipse);
let cubic = Bezier::from_cubic_dvec2(p, p, p, p);
let outline_cubic = cubic.outline::<EmptyId>(25., Cap::Round);
assert_eq!(outline_cubic, ellipse);
}
#[test]
fn test_outline_single_point_square() {
let square: Subpath<EmptyId> = Subpath::from_anchors(
[
DVec2::new(25., 0.),
DVec2::new(0., 0.),
DVec2::new(0., 50.),
DVec2::new(25., 50.),
DVec2::new(50., 50.),
DVec2::new(50., 0.),
],
true,
);
let p = DVec2::new(25., 25.);
let line = Bezier::from_linear_dvec2(p, p);
let outline = line.outline::<EmptyId>(25., Cap::Square);
assert_eq!(outline, square);
let cubic = Bezier::from_cubic_dvec2(p, p, p, p);
let outline_cubic = cubic.outline::<EmptyId>(25., Cap::Square);
assert_eq!(outline_cubic, square);
}
#[test]
fn test_graduated_scale() {
let bezier = Bezier::from_linear_coordinates(30., 60., 140., 120.);
bezier.graduated_scale(10., 20.);
}
#[test]
fn test_graduated_scale_quadratic() {
let bezier = Bezier::from_quadratic_coordinates(30., 50., 82., 98., 160., 170.);
let scaled_bezier = bezier.graduated_scale(30., 30.);
dbg!(scaled_bezier);
assert!(f64_compare(
scaled_bezier.evaluate(TValue::Parametric(0.)).distance(bezier.evaluate(TValue::Parametric(0.))),
30.,
MAX_ABSOLUTE_DIFFERENCE
));
assert!(f64_compare(
scaled_bezier.evaluate(TValue::Parametric(1.)).distance(bezier.evaluate(TValue::Parametric(1.))),
30.,
MAX_ABSOLUTE_DIFFERENCE
));
assert!(f64_compare(
scaled_bezier.evaluate(TValue::Parametric(0.5)).distance(bezier.evaluate(TValue::Parametric(0.5))),
30.,
MAX_ABSOLUTE_DIFFERENCE
));
}
#[test]
fn test_arcs_linear() {
let bezier = Bezier::from_linear_coordinates(30., 60., 140., 120.);
let linear_arcs = bezier.arcs(ArcsOptions::default());
assert!(linear_arcs.is_empty());
}
#[test]
fn test_arcs_quadratic() {
let bezier1 = Bezier::from_quadratic_coordinates(30., 30., 50., 50., 100., 100.);
assert!(bezier1.arcs(ArcsOptions::default()).is_empty());
let bezier2 = Bezier::from_quadratic_coordinates(50., 50., 85., 65., 100., 100.);
let actual_arcs = bezier2.arcs(ArcsOptions::default());
let expected_arc = CircleArc {
center: DVec2::new(15., 135.),
radius: 91.92388,
start_angle: -1.18019,
end_angle: -0.39061,
};
assert_eq!(actual_arcs.len(), 1);
assert!(compare_arcs(actual_arcs[0], expected_arc));
}
#[test]
fn test_arcs_cubic() {
let bezier = Bezier::from_cubic_coordinates(30., 30., 30., 80., 60., 80., 60., 140.);
let actual_arcs = bezier.arcs(ArcsOptions::default());
let expected_arcs = [
CircleArc {
center: DVec2::new(122.394877, 30.7777189),
radius: 92.39815,
start_angle: 2.5637146,
end_angle: -3.1331755,
},
CircleArc {
center: DVec2::new(-47.54881, 136.169378),
radius: 107.61701,
start_angle: -0.53556,
end_angle: 0.0356025,
},
];
assert_eq!(actual_arcs.len(), 2);
assert!(compare_arcs(actual_arcs[0], expected_arcs[0]));
assert!(compare_arcs(actual_arcs[1], expected_arcs[1]));
let bezier2 = Bezier::from_cubic_coordinates(48., 176., 170., 10., 30., 90., 180., 160.);
let auto_arcs = bezier2.arcs(ArcsOptions::default());
let extrema_arcs = bezier2.arcs(ArcsOptions {
strategy: ArcStrategy::FavorCorrectness,
..ArcsOptions::default()
});
let maximal_arcs = bezier2.arcs(ArcsOptions {
strategy: ArcStrategy::FavorLargerArcs,
..ArcsOptions::default()
});
assert_eq!(auto_arcs[0], maximal_arcs[0]);
assert_ne!(auto_arcs[0], extrema_arcs[0]);
assert!(auto_arcs.iter().skip(2).zip(extrema_arcs.iter().skip(2)).all(|(arc1, arc2)| compare_arcs(*arc1, *arc2)));
}
}