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:
Rob Nadal
2023-03-03 14:21:21 -05:00
committed by GitHub
co-authored by Hannah Li Keavon Chambers
parent 0512cb249f
commit 2fd5c26450
16 changed files with 611 additions and 168 deletions
+210 -89
View File
@@ -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]