mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-03 23:08:12 +08:00
Bezier-rs: Subpath offset and bezier offset improvements (#1039)
* Added subpath offset * Enhanced offset to produce smooth curves * Lots of outline bugfixes * Fixed failing unit tests * Added subpath outline * Refactor bezier offset and outline to return Subpaths * Fix outline bug due to smooth joining and removed reduce optimization that causes jumping approximations * Bugfix when subpath angle is acute but doesn't intersect * Stylistic changes per review * Stylistic changes per review and updated doc comments --------- Co-authored-by: Hannah Li <hannahli2010@gmail.com> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Hannah Li
Keavon Chambers
parent
0512cb249f
commit
2fd5c26450
@@ -1,11 +1,41 @@
|
||||
use super::*;
|
||||
|
||||
use crate::compare::compare_points;
|
||||
use crate::utils::{f64_compare, TValue};
|
||||
use crate::{AppendType, ManipulatorGroup, Subpath};
|
||||
|
||||
use glam::DMat2;
|
||||
use std::f64::consts::PI;
|
||||
|
||||
/// Functionality that transform Beziers, such as split, reduce, offset, etc.
|
||||
impl Bezier {
|
||||
/// Returns a linear approximation of the given [Bezier]. For higher order [Bezier], this means simply dropping the handles.
|
||||
pub fn to_linear(&self) -> Bezier {
|
||||
Bezier::from_linear_dvec2(self.start(), self.end())
|
||||
}
|
||||
|
||||
/// Returns a quadratic approximation of the given [Bezier]. For cubic Bezier, which typically cannot be represented by a single
|
||||
/// quadratic segment, this function simply takes the average of the cubic handles to be the new quadratic handle.
|
||||
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)
|
||||
}
|
||||
|
||||
/// Returns a cubic approximation of the given [Bezier].
|
||||
pub fn to_cubic(&self) -> Bezier {
|
||||
let (handle_start, handle_end) = match self.handles {
|
||||
BezierHandles::Linear => (self.start, self.end),
|
||||
// Conversion reference source: https://stackoverflow.com/a/63059651/775283
|
||||
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)
|
||||
}
|
||||
|
||||
/// Returns the pair of Bezier curves that result from splitting the original curve at the point `t` along the curve.
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#bezier/split/solo" title="Split Demo"></iframe>
|
||||
pub fn split(&self, t: TValue) -> [Bezier; 2] {
|
||||
@@ -154,7 +184,11 @@ impl Bezier {
|
||||
|
||||
let step_size = step_size.unwrap_or(DEFAULT_REDUCE_STEP_SIZE);
|
||||
|
||||
let extrema = self.get_extrema_t_list();
|
||||
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());
|
||||
}
|
||||
|
||||
// Split each subcurve such that each resulting segment is scalable.
|
||||
let mut result_beziers: Vec<Bezier> = Vec::new();
|
||||
@@ -170,15 +204,6 @@ impl Bezier {
|
||||
result_t_values.push(t_subcurve_end);
|
||||
return;
|
||||
}
|
||||
// According to <https://pomax.github.io/bezierinfo/#offsetting>, it is generally sufficient to split subcurves with no local extrema at `t = 0.5` to generate two scalable segments.
|
||||
let [first_half, second_half] = subcurve.split(TValue::Parametric(0.5));
|
||||
if first_half.is_scalable() && second_half.is_scalable() {
|
||||
result_beziers.push(first_half);
|
||||
result_beziers.push(second_half);
|
||||
result_t_values.push(t_subcurve_start + (t_subcurve_end - t_subcurve_start) / 2.);
|
||||
result_t_values.push(t_subcurve_end);
|
||||
return;
|
||||
}
|
||||
|
||||
// Greedily iterate across the subcurve at intervals of size `step_size` to break up the curve into maximally large segments
|
||||
let mut segment: Bezier;
|
||||
@@ -242,8 +267,14 @@ impl Bezier {
|
||||
// Find the intersection point of the endpoint normals
|
||||
let intersection = utils::line_intersection(self.start, normal_start, self.end, normal_end);
|
||||
|
||||
// If the Bezier is a quadratic, convert it to a cubic to increase expressiveness
|
||||
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);
|
||||
self.apply_transformation(&|point| {
|
||||
intermediate.apply_transformation(&|point| {
|
||||
let mut direction_unit_vector = (intersection - point).normalize();
|
||||
if should_flip_direction {
|
||||
direction_unit_vector *= -1.;
|
||||
@@ -258,49 +289,47 @@ impl Bezier {
|
||||
pub fn graduated_scale(&self, start_distance: f64, end_distance: f64) -> Bezier {
|
||||
assert!(self.is_scalable(), "The curve provided to scale is not scalable. Reduce the curve first.");
|
||||
|
||||
let normal_start = self.normal(TValue::Parametric(0.));
|
||||
let normal_end = self.normal(TValue::Parametric(1.));
|
||||
// If the Bezier is a quadratic, convert it to a cubic to increase expressiveness
|
||||
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 unit vectors are equal, then the lines are parallel
|
||||
if normal_start.abs_diff_eq(normal_end, MAX_ABSOLUTE_DIFFERENCE) {
|
||||
let transformed_start = utils::scale_point_from_direction_vector(self.start, self.normal(TValue::Parametric(0.)), false, start_distance);
|
||||
let transformed_end = utils::scale_point_from_direction_vector(self.end, self.normal(TValue::Parametric(1.)), false, end_distance);
|
||||
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 self.handles {
|
||||
return match intermediate.handles {
|
||||
BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end),
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
let handle_closest_t = self.project(handle, ProjectionOptions::default());
|
||||
let handle_scale_distance = (1. - handle_closest_t) * start_distance + handle_closest_t * end_distance;
|
||||
let transformed_handle = utils::scale_point_from_direction_vector(handle, self.normal(TValue::Parametric(handle_closest_t)), false, handle_scale_distance);
|
||||
Bezier::from_quadratic_dvec2(transformed_start, transformed_handle, transformed_end)
|
||||
}
|
||||
BezierHandles::Quadratic { handle: _ } => unreachable!(),
|
||||
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||
let handle_start_closest_t = self.project(handle_start, ProjectionOptions::default());
|
||||
let handle_start_closest_t = intermediate.project(handle_start, ProjectionOptions::default());
|
||||
let handle_start_scale_distance = (1. - handle_start_closest_t) * start_distance + handle_start_closest_t * end_distance;
|
||||
let transformed_handle_start = utils::scale_point_from_direction_vector(handle_start, self.normal(TValue::Parametric(handle_start_closest_t)), false, handle_start_scale_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 = self.project(handle_start, ProjectionOptions::default());
|
||||
let handle_end_closest_t = intermediate.project(handle_start, ProjectionOptions::default());
|
||||
let handle_end_scale_distance = (1. - handle_end_closest_t) * start_distance + handle_end_closest_t * end_distance;
|
||||
let transformed_handle_end = utils::scale_point_from_direction_vector(handle_end, self.normal(TValue::Parametric(handle_end_closest_t)), false, handle_end_scale_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)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Find the intersection point of the endpoint normals
|
||||
let intersection = utils::line_intersection(self.start, normal_start, self.end, normal_end);
|
||||
let should_flip_direction = (self.start - intersection).normalize().abs_diff_eq(normal_start, MAX_ABSOLUTE_DIFFERENCE);
|
||||
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(self.start, intersection, should_flip_direction, start_distance);
|
||||
let transformed_end = utils::scale_point_from_origin(self.end, intersection, should_flip_direction, end_distance);
|
||||
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 self.handles {
|
||||
match intermediate.handles {
|
||||
BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end),
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
let handle_scale_distance = (start_distance + end_distance) / 2.;
|
||||
let transformed_handle = utils::scale_point_from_origin(handle, intersection, should_flip_direction, handle_scale_distance);
|
||||
Bezier::from_quadratic_dvec2(transformed_start, transformed_handle, 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);
|
||||
@@ -312,78 +341,107 @@ impl Bezier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Offset will get all the reduceable subcurves, and for each subcurve, it will scale the subcurve a set distance away from the original curve.
|
||||
/// Offset will break down the Bezier into reducible subcurves, and scale each subcurve a set distance from the original curve.
|
||||
/// Note that not all bezier curves are possible to offset, so this function first reduces the curve to scalable segments and then offsets those segments.
|
||||
/// A proof for why this is true can be found in the [Curve offsetting section](https://pomax.github.io/bezierinfo/#offsetting) of Pomax's bezier curve primer.
|
||||
/// Offset takes the following parameter:
|
||||
/// - `distance` - The offset's distance from the curve. Positive values will offset the curve in the same direction as the endpoint normals,
|
||||
/// while negative values will offset in the opposite direction.
|
||||
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#bezier/offset/solo" title="Offset Demo"></iframe>
|
||||
pub fn offset(&self, distance: f64) -> Vec<Bezier> {
|
||||
let mut reduced = self.reduce(None);
|
||||
reduced.iter_mut().for_each(|bezier| *bezier = bezier.scale(distance));
|
||||
reduced
|
||||
pub fn offset<ManipulatorGroupId: crate::Identifier>(&self, distance: f64) -> Subpath<ManipulatorGroupId> {
|
||||
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 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 the curve is not linear, smooth the handles. All segments produced by bezier::scale will be cubic.
|
||||
if self.handles != BezierHandles::Linear {
|
||||
scaled.smooth_open_subpath();
|
||||
}
|
||||
|
||||
scaled
|
||||
}
|
||||
|
||||
/// Version of the `offset` function which scales the offset such that the start of the offset is `start_distance` from the original curve, while the end of
|
||||
/// of the offset is `end_distance` from the original curve. The curve transitions from `start_distance` to `end_distance` gradually, proportional to the
|
||||
/// distance along the equation (`t`-value) of the curve. Similarily to the `offset` function, the returned result is an approximation.
|
||||
pub fn graduated_offset(&self, start_distance: f64, end_distance: f64) -> Vec<Bezier> {
|
||||
/// distance along the equation (`t`-value) of the curve. Similarly to the `offset` function, the returned result is an approximation.
|
||||
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);
|
||||
|
||||
let mut result = vec![];
|
||||
reduced.iter().for_each(|bezier| {
|
||||
let mut result = Subpath::new(vec![], false);
|
||||
reduced.iter().enumerate().for_each(|(index, bezier)| {
|
||||
let current_length = bezier.length(None);
|
||||
let next_end_distance = next_start_distance + (current_length / total_length) * distance_difference;
|
||||
result.push(bezier.graduated_scale(next_start_distance, next_end_distance));
|
||||
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 the curve is not linear, smooth the handles. All segments produced by bezier::scale will be cubic.
|
||||
if self.handles != BezierHandles::Linear {
|
||||
result.smooth_open_subpath();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Outline will return a vector of Beziers that creates an outline around the curve at the designated distance away from the curve.
|
||||
/// It makes use of the `offset` function, thus restrictions applicable to `offset` are relevant to this function as well.
|
||||
/// The 'caps', the linear segments at opposite ends of the outline, intersect the original curve at the midpoint of the cap.
|
||||
///
|
||||
/// Outline takes the following parameter:
|
||||
/// - `distance` - The outline's distance from the curve.
|
||||
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#bezier/outline/solo" title="Outline Demo"></iframe>
|
||||
pub fn outline(&self, distance: f64) -> Vec<Bezier> {
|
||||
pub fn outline<ManipulatorGroupId: crate::Identifier>(&self, distance: f64) -> Subpath<ManipulatorGroupId> {
|
||||
let first_segment = self.offset(distance);
|
||||
let third_segment = self.reverse().offset(distance);
|
||||
|
||||
if first_segment.is_empty() || third_segment.is_empty() {
|
||||
return vec![];
|
||||
return Subpath::new(vec![], false);
|
||||
}
|
||||
|
||||
let second_segment = Bezier::from_linear_dvec2(first_segment.last().unwrap().end, third_segment.first().unwrap().start);
|
||||
let fourth_segment = Bezier::from_linear_dvec2(third_segment.last().unwrap().end, first_segment.first().unwrap().start);
|
||||
[first_segment, vec![second_segment], third_segment, vec![fourth_segment]].concat()
|
||||
let mut result_manipulator_groups: Vec<ManipulatorGroup<ManipulatorGroupId>> = vec![];
|
||||
result_manipulator_groups.extend_from_slice(first_segment.manipulator_groups());
|
||||
// TODO: Handle other caps here
|
||||
result_manipulator_groups.extend_from_slice(third_segment.manipulator_groups());
|
||||
Subpath::new(result_manipulator_groups, true)
|
||||
}
|
||||
|
||||
/// Version of the `outline` function which draws the outline at the specified distances away from the curve.
|
||||
/// The outline begins `start_distance` away, and gradually move to being `end_distance` away.
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#bezier/graduated-outline/solo" title="Graduated Outline Demo"></iframe>
|
||||
pub fn graduated_outline(&self, start_distance: f64, end_distance: f64) -> Vec<Bezier> {
|
||||
pub fn graduated_outline<ManipulatorGroupId: crate::Identifier>(&self, start_distance: f64, end_distance: f64) -> Subpath<ManipulatorGroupId> {
|
||||
self.skewed_outline(start_distance, end_distance, end_distance, start_distance)
|
||||
}
|
||||
|
||||
/// Version of the `graduated_outline` function that allows for the 4 corners of the outline to be different distances away from the curve.
|
||||
/// <iframe frameBorder="0" width="100%" height="475px" src="https://graphite.rs/bezier-rs-demos#bezier/skewed-outline/solo" title="Skewed Outline Demo"></iframe>
|
||||
pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64) -> Vec<Bezier> {
|
||||
pub fn skewed_outline<ManipulatorGroupId: crate::Identifier>(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64) -> Subpath<ManipulatorGroupId> {
|
||||
let first_segment = self.graduated_offset(distance1, distance2);
|
||||
let third_segment = self.reverse().graduated_offset(distance3, distance4);
|
||||
|
||||
if first_segment.is_empty() || third_segment.is_empty() {
|
||||
return vec![];
|
||||
return Subpath::new(vec![], false);
|
||||
}
|
||||
|
||||
let second_segment = Bezier::from_linear_dvec2(first_segment.last().unwrap().end, third_segment.first().unwrap().start);
|
||||
let fourth_segment = Bezier::from_linear_dvec2(third_segment.last().unwrap().end, first_segment.first().unwrap().start);
|
||||
[first_segment, vec![second_segment], third_segment, vec![fourth_segment]].concat()
|
||||
let mut result_manipulator_groups: Vec<ManipulatorGroup<ManipulatorGroupId>> = vec![];
|
||||
result_manipulator_groups.extend_from_slice(first_segment.manipulator_groups());
|
||||
// TODO: Handle other caps here
|
||||
result_manipulator_groups.extend_from_slice(third_segment.manipulator_groups());
|
||||
Subpath::new(result_manipulator_groups, true)
|
||||
}
|
||||
|
||||
/// Approximate a bezier curve with circular arcs.
|
||||
@@ -538,8 +596,9 @@ impl Bezier {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::compare::{compare_arcs, compare_vector_of_beziers};
|
||||
use crate::compare::{compare_arcs, compare_points, compare_vec_of_points};
|
||||
use crate::utils::TValue;
|
||||
use crate::EmptyId;
|
||||
|
||||
#[test]
|
||||
fn test_split() {
|
||||
@@ -695,41 +754,103 @@ mod tests {
|
||||
vec![DVec2::new(4.2975, 4.2975), DVec2::new(5.6625, 5.6625), DVec2::new(6.9375, 6.9375)],
|
||||
];
|
||||
let reduced_curves = bezier.reduce(None);
|
||||
assert!(compare_vector_of_beziers(&reduced_curves, expected_bezier_points));
|
||||
assert!(reduced_curves.iter().zip(expected_bezier_points.into_iter()).all(|(bezier, points)| compare_vec_of_points(
|
||||
bezier.get_points().collect::<Vec<DVec2>>(),
|
||||
points,
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)));
|
||||
|
||||
// Check that the reduce helper is correct
|
||||
let (helper_curves, helper_t_values) = bezier.reduced_curves_and_t_values(None);
|
||||
assert_eq!(&reduced_curves, &helper_curves);
|
||||
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.windows(2))
|
||||
.all(|(curve, t_pair)| curve.abs_diff_eq(&bezier.trim(TValue::Parametric(t_pair[0]), TValue::Parametric(t_pair[1])), MAX_ABSOLUTE_DIFFERENCE)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_offset() {
|
||||
let p1 = DVec2::new(30., 50.);
|
||||
let p2 = DVec2::new(140., 30.);
|
||||
let p3 = DVec2::new(160., 170.);
|
||||
let bezier1 = Bezier::from_quadratic_dvec2(p1, p2, p3);
|
||||
let expected_bezier_points1 = vec![
|
||||
vec![DVec2::new(31.7888, 59.8387), DVec2::new(44.5924, 57.46446), DVec2::new(56.09375, 57.5)],
|
||||
vec![DVec2::new(56.09375, 57.5), DVec2::new(94.94197, 56.5019), DVec2::new(117.6473, 84.5936)],
|
||||
vec![DVec2::new(117.6473, 84.5936), DVec2::new(142.3985, 113.403), DVec2::new(150.1005, 171.4142)],
|
||||
];
|
||||
assert!(compare_vector_of_beziers(&bezier1.offset(10.), expected_bezier_points1));
|
||||
fn assert_valid_offset<ManipulatorGroupId: crate::Identifier>(bezier: &Bezier, offset: &Subpath<ManipulatorGroupId>, expected_distance: f64) {
|
||||
// Verify that the offset is smooth
|
||||
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 p4 = DVec2::new(32., 77.);
|
||||
let p5 = DVec2::new(169., 25.);
|
||||
let p6 = DVec2::new(164., 157.);
|
||||
let bezier2 = Bezier::from_quadratic_dvec2(p4, p5, p6);
|
||||
let expected_bezier_points2 = vec![
|
||||
vec![DVec2::new(42.6458, 105.04758), DVec2::new(75.0218, 91.9939), DVec2::new(98.09357, 92.3043)],
|
||||
vec![DVec2::new(98.09357, 92.3043), DVec2::new(116.5995, 88.5479), DVec2::new(123.9055, 102.0401)],
|
||||
vec![DVec2::new(123.9055, 102.0401), DVec2::new(136.6087, 116.9522), DVec2::new(134.1761, 147.9324)],
|
||||
vec![DVec2::new(134.1761, 147.9324), DVec2::new(134.1812, 151.7987), DVec2::new(134.0215, 155.86445)],
|
||||
];
|
||||
assert!(compare_vector_of_beziers(&bezier2.offset(30.), expected_bezier_points2));
|
||||
// Verify that the offset spans the length of the curve
|
||||
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.;
|
||||
// Sample the curve and verify that the offset lies at the correct distance from the curve.
|
||||
// Collect the t-value associated with the point on the bezier closest to the sample.
|
||||
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, ProjectionOptions::default());
|
||||
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();
|
||||
|
||||
// Verify that the curve segments are in the correct order by asserting that t_values is sorted
|
||||
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]
|
||||
@@ -737,29 +858,29 @@ mod tests {
|
||||
let p1 = DVec2::new(30., 50.);
|
||||
let p2 = DVec2::new(140., 30.);
|
||||
let line = Bezier::from_linear_dvec2(p1, p2);
|
||||
let outline = line.outline(10.);
|
||||
let outline = line.outline::<EmptyId>(10.);
|
||||
|
||||
assert_eq!(outline.len(), 4);
|
||||
|
||||
// Assert the first length-wise piece of the outline is 10 units from the line
|
||||
assert!(f64_compare(
|
||||
outline[0].evaluate(TValue::Parametric(0.25)).distance(line.evaluate(TValue::Parametric(0.25))),
|
||||
outline.iter().next().unwrap().evaluate(TValue::Parametric(0.25)).distance(line.evaluate(TValue::Parametric(0.25))),
|
||||
10.,
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)); // f64
|
||||
|
||||
// Assert the first cap touches the line end point at the halfway point
|
||||
assert!(outline[1].evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.end(), MAX_ABSOLUTE_DIFFERENCE));
|
||||
assert!(outline.iter().nth(1).unwrap().evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.end(), MAX_ABSOLUTE_DIFFERENCE));
|
||||
|
||||
// Assert the second length-wise piece of the outline is 10 units from the line
|
||||
assert!(f64_compare(
|
||||
outline[2].evaluate(TValue::Parametric(0.25)).distance(line.evaluate(TValue::Parametric(0.75))),
|
||||
outline.iter().nth(2).unwrap().evaluate(TValue::Parametric(0.25)).distance(line.evaluate(TValue::Parametric(0.75))),
|
||||
10.,
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)); // f64
|
||||
|
||||
// Assert the second cap touches the line start point at the halfway point
|
||||
assert!(outline[3].evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.start(), MAX_ABSOLUTE_DIFFERENCE));
|
||||
assert!(outline.iter().nth(3).unwrap().evaluate(TValue::Parametric(0.5)).abs_diff_eq(line.start(), MAX_ABSOLUTE_DIFFERENCE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user