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:
parent
531438161e
commit
ccb698ffa8
|
|
@ -53,7 +53,7 @@ impl Bezier {
|
|||
}
|
||||
|
||||
/// Returns the non-normalized vector representing the tangent at the point `t` along the curve.
|
||||
fn non_normalized_tangent(&self, t: f64) -> DVec2 {
|
||||
pub(crate) fn non_normalized_tangent(&self, t: f64) -> DVec2 {
|
||||
match self.handles {
|
||||
BezierHandles::Linear => self.end - self.start,
|
||||
_ => self.derivative().unwrap().evaluate(TValue::Parametric(t)),
|
||||
|
|
@ -202,7 +202,7 @@ impl Bezier {
|
|||
/// Implementation of the algorithm to find curve intersections by iterating on bounding boxes.
|
||||
/// - `self_original_t_interval` - Used to identify the `t` values of the original parent of `self` that the current iteration is representing.
|
||||
/// - `other_original_t_interval` - Used to identify the `t` values of the original parent of `other` that the current iteration is representing.
|
||||
fn intersections_between_subcurves(&self, self_original_t_interval: Range<f64>, other: &Bezier, other_original_t_interval: Range<f64>, error: f64) -> Vec<[f64; 2]> {
|
||||
pub(crate) fn intersections_between_subcurves(&self, self_original_t_interval: Range<f64>, other: &Bezier, other_original_t_interval: Range<f64>, error: f64) -> Vec<[f64; 2]> {
|
||||
let bounding_box1 = self.bounding_box();
|
||||
let bounding_box2 = other.bounding_box();
|
||||
|
||||
|
|
@ -245,8 +245,8 @@ impl Bezier {
|
|||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Returns a list of filtered parametric `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.
|
||||
/// such that the difference between adjacent `t` values in sorted order is greater than some minimum separation value. If the difference
|
||||
/// between 2 adjacent `t` values is less 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.
|
||||
|
|
@ -259,7 +259,7 @@ impl Bezier {
|
|||
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
|
||||
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) {
|
||||
if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_seperation.unwrap_or(MIN_SEPARATION_VALUE) {
|
||||
accumulator.pop();
|
||||
}
|
||||
accumulator.push(*t);
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
/// Comparison functions used for tests in the bezier module
|
||||
use super::{Bezier, CircleArc, Subpath};
|
||||
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
|
||||
#[cfg(test)]
|
||||
use super::{CircleArc, Subpath};
|
||||
#[cfg(test)]
|
||||
use crate::utils::f64_compare;
|
||||
|
||||
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
|
||||
|
||||
use glam::DVec2;
|
||||
|
||||
// Compare two f64s with some maximum absolute difference to account for floating point errors
|
||||
#[cfg(test)]
|
||||
pub fn compare_f64s(f1: f64, f2: f64) -> bool {
|
||||
f64_compare(f1, f2, MAX_ABSOLUTE_DIFFERENCE)
|
||||
}
|
||||
|
|
@ -16,19 +20,13 @@ pub fn compare_points(p1: DVec2, p2: DVec2) -> bool {
|
|||
}
|
||||
|
||||
/// Compare vectors of points by allowing some maximum absolute difference to account for floating point errors
|
||||
#[cfg(test)]
|
||||
pub fn compare_vec_of_points(a: Vec<DVec2>, b: Vec<DVec2>, max_absolute_difference: f64) -> bool {
|
||||
a.len() == b.len() && a.into_iter().zip(b.into_iter()).all(|(p1, p2)| p1.abs_diff_eq(p2, max_absolute_difference))
|
||||
}
|
||||
|
||||
/// Compare vectors of beziers by allowing some maximum absolute difference between points to account for floating point errors
|
||||
pub fn compare_vector_of_beziers(beziers: &[Bezier], expected_bezier_points: Vec<Vec<DVec2>>) -> bool {
|
||||
beziers
|
||||
.iter()
|
||||
.zip(expected_bezier_points.iter())
|
||||
.all(|(&a, b)| compare_vec_of_points(a.get_points().collect::<Vec<DVec2>>(), b.to_vec(), MAX_ABSOLUTE_DIFFERENCE))
|
||||
}
|
||||
|
||||
/// Compare circle arcs by allowing some maximum absolute difference between values to account for floating point errors
|
||||
#[cfg(test)]
|
||||
pub fn compare_arcs(arc1: CircleArc, arc2: CircleArc) -> bool {
|
||||
compare_points(arc1.center, arc2.center)
|
||||
&& f64_compare(arc1.radius, arc1.radius, MAX_ABSOLUTE_DIFFERENCE)
|
||||
|
|
@ -38,6 +36,7 @@ pub fn compare_arcs(arc1: CircleArc, arc2: CircleArc) -> bool {
|
|||
|
||||
/// Compare Subpath by verifying that their bezier segments match.
|
||||
/// In this way, matching quadratic segments where the handles are on opposite manipulator groups will be considered equal.
|
||||
#[cfg(test)]
|
||||
pub fn compare_subpaths<ManipulatorGroupId: crate::Identifier>(subpath1: &Subpath<ManipulatorGroupId>, subpath2: &Subpath<ManipulatorGroupId>) -> bool {
|
||||
subpath1.len() == subpath2.len() && subpath1.closed() == subpath2.closed() && subpath1.iter().eq(subpath2.iter())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ 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;
|
||||
pub const MIN_SEPARATION_VALUE: f64 = 5. * 1e-3;
|
||||
/// Default error bound for `t_value_to_parametric` function when TValue argument is Euclidean
|
||||
pub const DEFAULT_EUCLIDEAN_ERROR_BOUND: f64 = 0.001;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
//! Bezier-rs: A Bezier Math Library for Rust
|
||||
#[cfg(test)]
|
||||
pub(crate) mod compare;
|
||||
|
||||
mod bezier;
|
||||
|
|
@ -9,4 +8,4 @@ mod utils;
|
|||
|
||||
pub use bezier::*;
|
||||
pub use subpath::*;
|
||||
pub use utils::{SubpathTValue, TValue};
|
||||
pub use utils::{Joint, SubpathTValue, TValue};
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
}
|
||||
|
||||
/// Create a `Subpath` consisting of 2 manipulator groups from a `Bezier`.
|
||||
pub fn from_bezier(bezier: Bezier) -> Self {
|
||||
pub fn from_bezier(bezier: &Bezier) -> Self {
|
||||
Subpath::new(
|
||||
vec![
|
||||
ManipulatorGroup {
|
||||
|
|
@ -34,6 +34,47 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
)
|
||||
}
|
||||
|
||||
/// Creates a subpath from a slice of [Bezier]. When two consecutive Beziers do not share an end and start point, this function
|
||||
/// resolves the discrepancy by simply taking the start-point of the second Bezier as the anchor of the Manipulator Group.
|
||||
pub fn from_beziers(beziers: &[Bezier], closed: bool) -> Self {
|
||||
assert!(!closed || beziers.len() > 1, "A closed Subpath must contain at least 1 Bezier.");
|
||||
if beziers.is_empty() {
|
||||
return Subpath::new(vec![], closed);
|
||||
}
|
||||
|
||||
let first = beziers.first().unwrap();
|
||||
let mut manipulator_groups = vec![ManipulatorGroup {
|
||||
anchor: first.start(),
|
||||
in_handle: None,
|
||||
out_handle: first.handle_start(),
|
||||
id: ManipulatorGroupId::new(),
|
||||
}];
|
||||
let mut inner_groups: Vec<ManipulatorGroup<ManipulatorGroupId>> = beziers
|
||||
.windows(2)
|
||||
.map(|bezier_pair| ManipulatorGroup {
|
||||
anchor: bezier_pair[1].start(),
|
||||
in_handle: bezier_pair[0].handle_end(),
|
||||
out_handle: bezier_pair[1].handle_start(),
|
||||
id: ManipulatorGroupId::new(),
|
||||
})
|
||||
.collect::<Vec<ManipulatorGroup<ManipulatorGroupId>>>();
|
||||
manipulator_groups.append(&mut inner_groups);
|
||||
|
||||
let last = beziers.last().unwrap();
|
||||
if !closed {
|
||||
manipulator_groups.push(ManipulatorGroup {
|
||||
anchor: last.end(),
|
||||
in_handle: last.handle_end(),
|
||||
out_handle: None,
|
||||
id: ManipulatorGroupId::new(),
|
||||
});
|
||||
return Subpath::new(manipulator_groups, false);
|
||||
}
|
||||
|
||||
manipulator_groups[0].in_handle = last.handle_end();
|
||||
Subpath::new(manipulator_groups, true)
|
||||
}
|
||||
|
||||
/// Returns true if the `Subpath` contains no [ManipulatorGroup].
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.manipulator_groups.is_empty()
|
||||
|
|
|
|||
|
|
@ -34,6 +34,47 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
self.manipulator_groups[segment_index % number_of_groups].out_handle = first.handle_start();
|
||||
self.manipulator_groups[(segment_index + 2) % number_of_groups].in_handle = second.handle_end();
|
||||
}
|
||||
|
||||
/// Append a [Bezier] to the end of a subpath from a vector of [Bezier].
|
||||
/// The `append_type` parameter determines how the function behaves when the subpath's last anchor is not equal to the Bezier's start point.
|
||||
/// - `IgnoreStart`: drops the bezier's start point in favor of the subpath's last anchor
|
||||
/// - `SmoothJoin(f64)`: joins the subpath's endpoint with the bezier's start with a another Bezier segment that is continuous up to the second derivative
|
||||
/// if the difference between the subpath's end point and Bezier's start point exceeds the wrapped integer value.
|
||||
/// This function assumes that the position of the [Bezier]'s starting point is equal to that of the Subpath's last manipulator group.
|
||||
pub fn append_bezier(&mut self, bezier: &Bezier, append_type: AppendType) {
|
||||
if self.manipulator_groups.is_empty() {
|
||||
self.manipulator_groups = vec![ManipulatorGroup {
|
||||
anchor: bezier.start(),
|
||||
in_handle: None,
|
||||
out_handle: None,
|
||||
id: ManipulatorGroupId::new(),
|
||||
}];
|
||||
}
|
||||
let mut last_index = self.manipulator_groups.len() - 1;
|
||||
let last_anchor = self.manipulator_groups[last_index].anchor;
|
||||
|
||||
if let AppendType::SmoothJoin(max_absolute_difference) = append_type {
|
||||
// If the provided Bezier does not start at a location similar to the end of the Subpath,
|
||||
// add an additional manipulator group to represent a smooth join with a new bezier in between
|
||||
if !last_anchor.abs_diff_eq(bezier.start(), max_absolute_difference) {
|
||||
let last_bezier = if self.manipulator_groups.len() > 1 {
|
||||
self.manipulator_groups[last_index - 1].to_bezier(&self.manipulator_groups[last_index])
|
||||
} else {
|
||||
Bezier::from_linear_dvec2(last_anchor, last_anchor)
|
||||
};
|
||||
let join_bezier = last_bezier.join(bezier);
|
||||
self.append_bezier(&join_bezier, AppendType::IgnoreStart);
|
||||
last_index = self.manipulator_groups.len() - 1;
|
||||
}
|
||||
}
|
||||
self.manipulator_groups[last_index].out_handle = bezier.handle_start();
|
||||
self.manipulator_groups.push(ManipulatorGroup {
|
||||
anchor: bezier.end(),
|
||||
in_handle: bezier.handle_end(),
|
||||
out_handle: None,
|
||||
id: ManipulatorGroupId::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -17,20 +17,27 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
/// Calculates the intersection points the subpath has with a given curve and returns a list of `(usize, f64)` tuples,
|
||||
/// where the `usize` represents the index of the curve in the subpath, and the `f64` represents the `t`-value local to
|
||||
/// that curve where the intersection occured.
|
||||
/// This function expects the following:
|
||||
/// Expects the following:
|
||||
/// - `other`: a [Bezier] curve to check intersections against
|
||||
/// - `error`: an optional f64 value to provide an error bound
|
||||
/// - `minimum_seperation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
|
||||
/// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two.
|
||||
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/intersect-cubic/solo" title="Intersection Demo"></iframe>
|
||||
pub fn intersections(&self, other: &Bezier, error: Option<f64>, minimum_seperation: Option<f64>) -> Vec<(usize, f64)> {
|
||||
// TODO: account for either euclidean or parametric type
|
||||
let intersection_t_values: Vec<(usize, f64)> = self
|
||||
.iter()
|
||||
pub fn intersections(&self, other: &Bezier, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
|
||||
self.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(index, bezier)| bezier.intersections(other, error, minimum_seperation).into_iter().map(|t| (index, t)).collect::<Vec<(usize, f64)>>())
|
||||
.collect();
|
||||
.flat_map(|(index, bezier)| bezier.intersections(other, error, minimum_separation).into_iter().map(|t| (index, t)).collect::<Vec<(usize, f64)>>())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Calculates the intersection points the subpath has with another given subpath and returns a list of global parametric `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
|
||||
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/intersect-cubic/solo" title="Intersection Demo"></iframe>
|
||||
pub fn subpath_intersections(&self, other: &Subpath<ManipulatorGroupId>, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
|
||||
let mut intersection_t_values: Vec<(usize, f64)> = other.iter().flat_map(|bezier| self.intersections(&bezier, error, minimum_separation)).collect();
|
||||
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
intersection_t_values
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -93,3 +93,9 @@ impl<ManipulatorGroupId: crate::Identifier> ManipulatorGroup<ManipulatorGroupId>
|
|||
self.out_handle = self.out_handle.map(|out_handle| affine_transform.transform_point2(out_handle));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum AppendType {
|
||||
IgnoreStart,
|
||||
SmoothJoin(f64),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
use std::vec;
|
||||
|
||||
use super::*;
|
||||
use crate::utils::SubpathTValue;
|
||||
use crate::utils::TValue;
|
||||
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
|
||||
use crate::utils::{Joint, SubpathTValue, TValue};
|
||||
|
||||
use glam::DAffine2;
|
||||
|
||||
|
|
@ -235,7 +237,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
// If the target curve_indices are the same, then the trim must be happening within one bezier
|
||||
// This means curve1 == curve2 must be true, and we can simply call the Bezier trim.
|
||||
if t1_curve_index == t2_curve_index {
|
||||
return Subpath::from_bezier(curve1.trim(TValue::Parametric(t1_curve_t), TValue::Parametric(t2_curve_t)));
|
||||
return Subpath::from_bezier(&curve1.trim(TValue::Parametric(t1_curve_t), TValue::Parametric(t2_curve_t)));
|
||||
}
|
||||
|
||||
// Split the bezier's with the according t value and keep the correct half
|
||||
|
|
@ -272,6 +274,183 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
manipulator_group.apply_transform(affine_transform);
|
||||
}
|
||||
}
|
||||
|
||||
/// Smooths a Subpath up to the first derivative, using a weighted averaged based on segment length.
|
||||
/// The Subpath must be open, and contain no quadratic segments.
|
||||
pub(crate) fn smooth_open_subpath(&mut self) {
|
||||
for i in 1..self.len() - 1 {
|
||||
let first_bezier = self.manipulator_groups[i - 1].to_bezier(&self.manipulator_groups[i]);
|
||||
let second_bezier = self.manipulator_groups[i].to_bezier(&self.manipulator_groups[i + 1]);
|
||||
if first_bezier.handle_end().is_none() || second_bezier.handle_end().is_none() {
|
||||
continue;
|
||||
}
|
||||
let end_tangent = first_bezier.non_normalized_tangent(1.);
|
||||
let start_tangent = second_bezier.non_normalized_tangent(0.);
|
||||
|
||||
// Compute an average unit vector, weighing the segments by a rough estimation of their relative size.
|
||||
let segment1_len = first_bezier.length(Some(5));
|
||||
let segment2_len = second_bezier.length(Some(5));
|
||||
let average_unit_tangent = (end_tangent.normalize() * segment1_len + start_tangent.normalize() * segment2_len) / (segment1_len + segment2_len);
|
||||
|
||||
// Adjust start and end handles to fit the average tangent
|
||||
let end_point = first_bezier.end();
|
||||
self.manipulator_groups[i].in_handle = Some((average_unit_tangent / 3. * -1.) * end_tangent.length() + end_point);
|
||||
|
||||
let start_point = second_bezier.start();
|
||||
self.manipulator_groups[i].out_handle = Some((average_unit_tangent / 3.) * start_tangent.length() + start_point);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: If a segment curls back on itself tightly enough it could intersect again at the portion that should be trimmed. This could cause the Subpaths to be clipped
|
||||
// at the incorrect location. This can be avoided by first trimming the two Subpaths at any extrema, effectively ignoring loopbacks.
|
||||
/// Helper function to clip overlap of two intersecting open Subpaths. Returns an optional, as intersections may not exist for certain arrangements and distances.
|
||||
/// Assumes that the Subpaths represents simple Bezier segments, and clips the Subpaths at the last intersection of the first Subpath, and first intersection of the last Subpath.
|
||||
fn clip_simple_subpaths(subpath1: &Subpath<ManipulatorGroupId>, subpath2: &Subpath<ManipulatorGroupId>) -> Option<(Subpath<ManipulatorGroupId>, Subpath<ManipulatorGroupId>)> {
|
||||
// Split the first subpath at its last intersection
|
||||
let intersections1 = subpath1.subpath_intersections(subpath2, None, None);
|
||||
if intersections1.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let (segment_index, t) = *intersections1.last().unwrap();
|
||||
let (clipped_subpath1, _) = subpath1.split(SubpathTValue::Parametric { segment_index, t });
|
||||
|
||||
// Split the second subpath at its first intersection
|
||||
let intersections2 = subpath2.subpath_intersections(subpath1, None, None);
|
||||
if intersections2.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let (segment_index, t) = intersections2[0];
|
||||
let (_, clipped_subpath2) = subpath2.split(SubpathTValue::Parametric { segment_index, t });
|
||||
|
||||
Some((clipped_subpath1, clipped_subpath2.unwrap()))
|
||||
}
|
||||
|
||||
/// Reduces the segments of the subpath into simple subcurves, then scales each subcurve a set `distance` away.
|
||||
/// The intersections of segments of the subpath are joined using the method specified by the `joint` argument.
|
||||
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#subpath/offset/solo" title="Offset Demo"></iframe>
|
||||
pub fn offset(&self, distance: f64, joint: Joint) -> Subpath<ManipulatorGroupId> {
|
||||
assert!(self.len_segments() > 1, "Cannot offset an empty Subpath.");
|
||||
|
||||
// An offset at a distance 0 from the curve is simply the same curve
|
||||
if distance == 0. {
|
||||
return self.clone();
|
||||
}
|
||||
|
||||
let mut subpaths = self.iter().map(|bezier| bezier.offset(distance)).collect::<Vec<Subpath<ManipulatorGroupId>>>();
|
||||
let mut drop_common_point = vec![true; self.len()];
|
||||
|
||||
// Clip or join consecutive Subpaths
|
||||
for i in 0..subpaths.len() - 1 {
|
||||
let j = i + 1;
|
||||
let subpath1 = &subpaths[i];
|
||||
let subpath2 = &subpaths[j];
|
||||
|
||||
let last_segment = subpath1.get_segment(subpath1.len_segments() - 1).unwrap();
|
||||
let first_segment = subpath2.get_segment(0).unwrap();
|
||||
|
||||
// If the anchors are approximately equal, there is no need to clip / join the segments
|
||||
if last_segment.end().abs_diff_eq(first_segment.start(), MAX_ABSOLUTE_DIFFERENCE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate the angle formed between two consecutive Subpaths
|
||||
let out_tangent = self.get_segment(i).unwrap().tangent(TValue::Parametric(1.));
|
||||
let in_tangent = self.get_segment(j).unwrap().tangent(TValue::Parametric(0.));
|
||||
let angle = out_tangent.angle_between(in_tangent);
|
||||
|
||||
// The angle is concave. The Subpath overlap and must be clipped
|
||||
let mut apply_joint = true;
|
||||
if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) {
|
||||
// If the distance is large enough, there may still be no intersections. Also, if the angle is close enough to zero,
|
||||
// subpath intersections may find no intersections. In this case, the points are likely close enough that we can approximate
|
||||
// the points as being on top of one another.
|
||||
if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(subpath1, subpath2) {
|
||||
subpaths[i] = clipped_subpath1;
|
||||
subpaths[j] = clipped_subpath2;
|
||||
apply_joint = false;
|
||||
}
|
||||
}
|
||||
// The angle is convex. The Subpath must be joined using the specified Joint type
|
||||
if apply_joint {
|
||||
match joint {
|
||||
Joint::Bevel => {
|
||||
drop_common_point[j] = false;
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clip any overlap in the last segment
|
||||
if self.closed {
|
||||
let out_tangent = self.get_segment(self.len_segments() - 1).unwrap().tangent(TValue::Parametric(1.));
|
||||
let in_tangent = self.get_segment(0).unwrap().tangent(TValue::Parametric(0.));
|
||||
let angle = out_tangent.angle_between(in_tangent);
|
||||
|
||||
let mut apply_joint = true;
|
||||
if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) {
|
||||
if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(&subpaths[subpaths.len() - 1], &subpaths[0]) {
|
||||
// Merge the clipped subpaths
|
||||
let last_index = subpaths.len() - 1;
|
||||
subpaths[last_index] = clipped_subpath1;
|
||||
subpaths[0] = clipped_subpath2;
|
||||
apply_joint = false;
|
||||
}
|
||||
}
|
||||
if apply_joint {
|
||||
match joint {
|
||||
Joint::Bevel => {
|
||||
drop_common_point[0] = false;
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge the subpaths. Drop points which overlap with one another.
|
||||
let mut manipulator_groups = subpaths[0].manipulator_groups.clone();
|
||||
for i in 1..subpaths.len() {
|
||||
if drop_common_point[i] {
|
||||
let last_group = manipulator_groups.pop().unwrap();
|
||||
let mut manipulators_copy = subpaths[i].manipulator_groups.clone();
|
||||
manipulators_copy[0].in_handle = last_group.in_handle;
|
||||
|
||||
manipulator_groups.append(&mut manipulators_copy);
|
||||
} else {
|
||||
manipulator_groups.append(&mut subpaths[i].manipulator_groups.clone());
|
||||
}
|
||||
}
|
||||
if self.closed && drop_common_point[0] {
|
||||
let last_group = manipulator_groups.pop().unwrap();
|
||||
manipulator_groups[0].in_handle = last_group.in_handle;
|
||||
}
|
||||
|
||||
Subpath::new(manipulator_groups, self.closed)
|
||||
}
|
||||
|
||||
// TODO: Replace this return type with `Path`, once the `Path` data type has been created.
|
||||
/// Outline returns a single closed subpath (if the original subpath was open) or two closed subpaths (if the original subpath was closed) that forms
|
||||
/// an approximate outline around the subpath at a specified distance from the curve. Outline takes the following parameters:
|
||||
/// - `distance` - The outline's distance from the curve.
|
||||
/// - `joint` - The joint type used to cap the endpoints of open bezier curves, and join successive subpath segments.
|
||||
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#subpath/outline/solo" title="Outline Demo"></iframe>
|
||||
pub fn outline(&self, distance: f64, joint: Joint) -> (Subpath<ManipulatorGroupId>, Option<Subpath<ManipulatorGroupId>>) {
|
||||
let mut pos_offset = self.offset(distance, joint);
|
||||
let mut neg_offset = self.reverse().offset(distance, joint);
|
||||
|
||||
if self.closed {
|
||||
return (pos_offset, Some(neg_offset));
|
||||
}
|
||||
|
||||
match joint {
|
||||
Joint::Bevel => {
|
||||
pos_offset.manipulator_groups.append(&mut neg_offset.manipulator_groups);
|
||||
pos_offset.closed = true;
|
||||
(pos_offset, None)
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -515,7 +694,7 @@ mod tests {
|
|||
let result1 = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(0.2));
|
||||
let result2 = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.8));
|
||||
|
||||
assert!(compare_subpaths(&result1, &result2));
|
||||
assert!(compare_subpaths::<EmptyId>(&result1, &result2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -544,7 +723,7 @@ mod tests {
|
|||
let result = subpath.trim(SubpathTValue::GlobalParametric(0.), SubpathTValue::GlobalParametric(1.));
|
||||
|
||||
// Assume that resulting subpath would no longer have the any meaningless handles
|
||||
let mut expected_subpath = subpath.clone();
|
||||
let mut expected_subpath = subpath;
|
||||
expected_subpath[3].out_handle = None;
|
||||
|
||||
assert_eq!(result.manipulator_groups[0].anchor, location_front);
|
||||
|
|
@ -561,7 +740,7 @@ mod tests {
|
|||
|
||||
assert_eq!(result.manipulator_groups[0].anchor, location_front);
|
||||
assert_eq!(result.manipulator_groups[3].anchor, location_back);
|
||||
assert!(compare_subpaths(&subpath, &result));
|
||||
assert!(compare_subpaths::<EmptyId>(&subpath, &result));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -728,10 +907,4 @@ mod tests {
|
|||
assert!(result.manipulator_groups[0].out_handle.is_none());
|
||||
assert_eq!(result.manipulator_groups.len(), 1);
|
||||
}
|
||||
|
||||
fn transform_subpath() {
|
||||
let mut subpath = set_up_open_subpath();
|
||||
subpath.apply_transform(glam::DAffine2::IDENTITY);
|
||||
assert_eq!(subpath, set_up_open_subpath());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, MIN_SEPERATION_VALUE, STRICT_MAX_ABSOLUTE_DIFFERENCE};
|
||||
use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, MIN_SEPARATION_VALUE, STRICT_MAX_ABSOLUTE_DIFFERENCE};
|
||||
|
||||
use glam::{BVec2, DMat2, DVec2};
|
||||
use std::f64::consts::PI;
|
||||
|
|
@ -28,6 +28,13 @@ pub enum SubpathTValue {
|
|||
GlobalEuclideanWithinError { t: f64, error: f64 },
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum Joint {
|
||||
Miter,
|
||||
Bevel,
|
||||
Round,
|
||||
}
|
||||
|
||||
/// Helper to perform the computation of a and c, where b is the provided point on the curve.
|
||||
/// Given the correct power of `t` and `(1-t)`, the computation is the same for quadratic and cubic cases.
|
||||
/// Relevant derivation and the definitions of a, b, and c can be found in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer.
|
||||
|
|
@ -115,7 +122,7 @@ pub fn solve_reformatted_cubic(discriminant: f64, a: f64, p: f64, q: f64) -> Vec
|
|||
let a_divided_by_3 = a / 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 {
|
||||
if (root_1 - root_2).abs() > MIN_SEPARATION_VALUE {
|
||||
roots.push(root_1);
|
||||
}
|
||||
roots.push(root_2);
|
||||
|
|
|
|||
|
|
@ -239,10 +239,10 @@ const bezierFeatures = {
|
|||
sliderOptions: [
|
||||
{
|
||||
variable: "distance",
|
||||
min: -50,
|
||||
max: 50,
|
||||
min: -30,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 20,
|
||||
default: 15,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -257,9 +257,9 @@ const bezierFeatures = {
|
|||
{
|
||||
variable: "distance",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 20,
|
||||
default: 15,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -274,16 +274,16 @@ const bezierFeatures = {
|
|||
{
|
||||
variable: "start_distance",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 30,
|
||||
default: 5,
|
||||
},
|
||||
{
|
||||
variable: "end_distance",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 30,
|
||||
default: 15,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -306,28 +306,28 @@ const bezierFeatures = {
|
|||
{
|
||||
variable: "distance1",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 20,
|
||||
},
|
||||
{
|
||||
variable: "distance2",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 10,
|
||||
},
|
||||
{
|
||||
variable: "distance3",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 30,
|
||||
},
|
||||
{
|
||||
variable: "distance4",
|
||||
min: 0,
|
||||
max: 50,
|
||||
max: 30,
|
||||
step: 1,
|
||||
default: 5,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -114,6 +114,32 @@ const subpathFeatures = {
|
|||
],
|
||||
chooseTVariant: true,
|
||||
},
|
||||
offset: {
|
||||
name: "Offset",
|
||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.offset(options.distance),
|
||||
sliderOptions: [
|
||||
{
|
||||
variable: "distance",
|
||||
min: -25,
|
||||
max: 25,
|
||||
step: 1,
|
||||
default: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
outline: {
|
||||
name: "Outline",
|
||||
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string => subpath.outline(options.distance),
|
||||
sliderOptions: [
|
||||
{
|
||||
variable: "distance",
|
||||
min: 0,
|
||||
max: 25,
|
||||
step: 1,
|
||||
default: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export type SubpathFeatureKey = keyof typeof subpathFeatures;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::svg_drawing::*;
|
||||
use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, ProjectionOptions, TValue};
|
||||
use bezier_rs::{ArcStrategy, ArcsOptions, Bezier, Identifier, ProjectionOptions, TValue};
|
||||
use glam::DVec2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
|
@ -49,6 +49,16 @@ fn parse_t_variant(t_variant: &String, t: f64) -> TValue {
|
|||
}
|
||||
}
|
||||
|
||||
/// An empty id type for use in tests
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct EmptyId;
|
||||
|
||||
impl Identifier for EmptyId {
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmBezier {
|
||||
/// Expect js_points to be a list of 2 pairs.
|
||||
|
|
@ -542,7 +552,7 @@ impl WasmBezier {
|
|||
let original_curve_svg = self.get_bezier_path();
|
||||
let bezier_curves_svg = self
|
||||
.0
|
||||
.offset(distance)
|
||||
.offset::<EmptyId>(distance)
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, bezier_curve)| {
|
||||
|
|
@ -561,36 +571,39 @@ impl WasmBezier {
|
|||
}
|
||||
|
||||
pub fn outline(&self, distance: f64) -> String {
|
||||
let outline_beziers = self.0.outline(distance);
|
||||
if outline_beziers.is_empty() {
|
||||
let outline_subpath = self.0.outline::<EmptyId>(distance);
|
||||
if outline_subpath.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let outline_svg = draw_beziers(outline_beziers, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED));
|
||||
let mut outline_svg = String::new();
|
||||
outline_subpath.to_svg(&mut outline_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
|
||||
let bezier_svg = self.get_bezier_path();
|
||||
|
||||
wrap_svg_tag(format!("{bezier_svg}{outline_svg}"))
|
||||
}
|
||||
|
||||
pub fn graduated_outline(&self, start_distance: f64, end_distance: f64) -> String {
|
||||
let outline_beziers = self.0.graduated_outline(start_distance, end_distance);
|
||||
if outline_beziers.is_empty() {
|
||||
let outline_subpath = self.0.graduated_outline::<EmptyId>(start_distance, end_distance);
|
||||
if outline_subpath.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let outline_svg = draw_beziers(outline_beziers, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED));
|
||||
let mut outline_svg = String::new();
|
||||
outline_subpath.to_svg(&mut outline_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
|
||||
let bezier_svg = self.get_bezier_path();
|
||||
|
||||
wrap_svg_tag(format!("{bezier_svg}{outline_svg}"))
|
||||
}
|
||||
|
||||
pub fn skewed_outline(&self, distance1: f64, distance2: f64, distance3: f64, distance4: f64) -> String {
|
||||
let outline_beziers = self.0.skewed_outline(distance1, distance2, distance3, distance4);
|
||||
if outline_beziers.is_empty() {
|
||||
let outline_subpath = self.0.skewed_outline::<EmptyId>(distance1, distance2, distance3, distance4);
|
||||
if outline_subpath.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let outline_svg = draw_beziers(outline_beziers, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED));
|
||||
let mut outline_svg = String::new();
|
||||
outline_subpath.to_svg(&mut outline_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
|
||||
let bezier_svg = self.get_bezier_path();
|
||||
|
||||
wrap_svg_tag(format!("{bezier_svg}{outline_svg}"))
|
||||
|
|
|
|||
|
|
@ -376,4 +376,29 @@ impl WasmSubpath {
|
|||
|
||||
wrap_svg_tag(format!("{}{}", self.to_default_svg(), trimmed_subpath_svg))
|
||||
}
|
||||
|
||||
pub fn offset(&self, distance: f64) -> String {
|
||||
let offset_subpath = self.0.offset(distance, bezier_rs::Joint::Bevel);
|
||||
|
||||
let mut offset_svg = String::new();
|
||||
offset_subpath.to_svg(&mut offset_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
|
||||
|
||||
wrap_svg_tag(format!("{}{offset_svg}", self.to_default_svg()))
|
||||
}
|
||||
|
||||
pub fn outline(&self, distance: f64) -> String {
|
||||
let (outline_piece1, outline_piece2) = self.0.outline(distance, bezier_rs::Joint::Bevel);
|
||||
|
||||
let mut outline_piece1_svg = String::new();
|
||||
outline_piece1.to_svg(&mut outline_piece1_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
|
||||
|
||||
let mut outline_piece2_svg = String::new();
|
||||
if outline_piece2.is_some() {
|
||||
outline_piece2
|
||||
.unwrap()
|
||||
.to_svg(&mut outline_piece2_svg, CURVE_ATTRIBUTES.to_string().replace(BLACK, RED), String::new(), String::new(), String::new());
|
||||
}
|
||||
|
||||
wrap_svg_tag(format!("{}{outline_piece1_svg}{outline_piece2_svg}", self.to_default_svg()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
use bezier_rs::Bezier;
|
||||
use glam::DVec2;
|
||||
use std::fmt::Write;
|
||||
|
||||
// SVG drawing constants
|
||||
pub const SVG_OPEN_TAG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="200px" height="200px">"#;
|
||||
|
|
@ -48,19 +46,6 @@ pub fn draw_line(start_x: f64, start_y: f64, end_x: f64, end_y: f64, stroke: &st
|
|||
format!(r#"<line x1="{start_x}" y1="{start_y}" x2="{end_x}" y2="{end_y}" stroke="{stroke}" stroke-width="{stroke_width}"/>"#)
|
||||
}
|
||||
|
||||
/// Helper function to draw a list of beziers.
|
||||
pub fn draw_beziers(beziers: Vec<Bezier>, options: String) -> String {
|
||||
let start_point = beziers.first().unwrap().start();
|
||||
let mut svg = format!("<path d=\"M {} {}", start_point.x, start_point.y);
|
||||
|
||||
beziers.iter().for_each(|bezier| {
|
||||
let _ = write!(svg, " {}", bezier.svg_curve_argument());
|
||||
});
|
||||
|
||||
let _ = write!(svg, " Z\" {}/>", options);
|
||||
svg
|
||||
}
|
||||
|
||||
// Helper function to convert polar to cartesian coordinates
|
||||
fn polar_to_cartesian(center_x: f64, center_y: f64, radius: f64, angle_in_rad: f64) -> [f64; 2] {
|
||||
let x = center_x + radius * angle_in_rad.cos();
|
||||
|
|
|
|||
Loading…
Reference in New Issue