Refactor the 'Offset Path' node to use Kurbo entirely (#2946)
* impl function for segment intersections * fix and improve segment intersections * copy and refactor related segment intersection methods * copy and refactor tests for segment intersection from bezier-rs * impl intersection with bezpaths * copy and refactor tests * rename few variables in the tests module * rename position_on_bezpath to evaluate_bezpath * copy and refactor function to clip two intersecting simple bezpaths * refactor comments * copy and refactor functions for milter join * copy and refactor milter and round join functions from bezier-rs * it worked! refactor offset path node impl * fix few bugs * improve vars names and add comments * Code review * fmt --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
f15023ef58
commit
2306e9866e
|
|
@ -1,18 +1,20 @@
|
|||
use super::intersection::bezpath_intersections;
|
||||
use super::poisson_disk::poisson_disk_sample;
|
||||
use super::util::segment_tangent;
|
||||
use crate::vector::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
|
||||
use crate::vector::misc::{PointSpacingType, dvec2_to_point};
|
||||
use glam::DVec2;
|
||||
use crate::vector::misc::{PointSpacingType, dvec2_to_point, point_to_dvec2};
|
||||
use glam::{DMat2, DVec2};
|
||||
use kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, QuadBez, Rect, Shape};
|
||||
use std::f64::consts::{FRAC_PI_2, PI};
|
||||
|
||||
/// Splits the [`BezPath`] at `t` value which lie in the range of [0, 1].
|
||||
/// Splits the [`BezPath`] at segment index at `t` value which lie in the range of [0, 1].
|
||||
/// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1.
|
||||
pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezPath, BezPath)> {
|
||||
pub fn split_bezpath_at_segment(bezpath: &BezPath, segment_index: usize, t: f64) -> Option<(BezPath, BezPath)> {
|
||||
if t <= f64::EPSILON || (1. - t) <= f64::EPSILON || bezpath.segments().count() == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Get the segment which lies at the split.
|
||||
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, None);
|
||||
let segment = bezpath.get_seg(segment_index + 1).unwrap();
|
||||
|
||||
// Divide the segment.
|
||||
|
|
@ -53,7 +55,19 @@ pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezP
|
|||
Some((first_bezpath, second_bezpath))
|
||||
}
|
||||
|
||||
pub fn position_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point {
|
||||
/// Splits the [`BezPath`] at a `t` value which lies in the range of [0, 1].
|
||||
/// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1.
|
||||
pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezPath, BezPath)> {
|
||||
if t <= f64::EPSILON || (1. - t) <= f64::EPSILON || bezpath.segments().count() == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Get the segment which lies at the split.
|
||||
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, None);
|
||||
split_bezpath_at_segment(bezpath, segment_index, t)
|
||||
}
|
||||
|
||||
pub fn evaluate_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point {
|
||||
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length);
|
||||
bezpath.get_seg(segment_index + 1).unwrap().eval(t)
|
||||
}
|
||||
|
|
@ -328,3 +342,117 @@ pub fn is_linear(segment: &PathSeg) -> bool {
|
|||
PathSeg::Cubic(CubicBez { p0, p1, p2, p3 }) => is_colinear(p0, p1, p3) && is_colinear(p0, p2, p3),
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// TODO: 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 BezPaths. Returns an Option because intersections may not exist for certain arrangements and distances.
|
||||
/// Assumes that the BezPaths represents simple Bezier segments, and clips the BezPaths at the last intersection of the first BezPath, and first intersection of the last BezPath.
|
||||
pub fn clip_simple_bezpaths(bezpath1: &BezPath, bezpath2: &BezPath) -> Option<(BezPath, BezPath)> {
|
||||
// Split the first subpath at its last intersection
|
||||
let subpath_1_intersections = bezpath_intersections(bezpath1, bezpath2, None, None);
|
||||
if subpath_1_intersections.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let (segment_index, t) = *subpath_1_intersections.last()?;
|
||||
let (clipped_subpath1, _) = split_bezpath_at_segment(bezpath1, segment_index, t)?;
|
||||
|
||||
// Split the second subpath at its first intersection
|
||||
let subpath_2_intersections = bezpath_intersections(bezpath2, bezpath1, None, None);
|
||||
if subpath_2_intersections.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let (segment_index, t) = subpath_2_intersections[0];
|
||||
let (_, clipped_subpath2) = split_bezpath_at_segment(bezpath2, segment_index, t)?;
|
||||
|
||||
Some((clipped_subpath1, clipped_subpath2))
|
||||
}
|
||||
|
||||
/// Returns the [`PathEl`] that is needed for a miter join if it is possible.
|
||||
///
|
||||
/// `miter_limit` defines a limit for the ratio between the miter length and the stroke width.
|
||||
/// Alternatively, this can be interpreted as limiting the angle that the miter can form.
|
||||
/// When the limit is exceeded, no [`PathEl`] will be returned.
|
||||
/// This value should be greater than 0. If not, the default of 4 will be used.
|
||||
pub fn miter_line_join(bezpath1: &BezPath, bezpath2: &BezPath, miter_limit: Option<f64>) -> Option<[PathEl; 2]> {
|
||||
let miter_limit = match miter_limit {
|
||||
Some(miter_limit) if miter_limit > f64::EPSILON => miter_limit,
|
||||
_ => 4.,
|
||||
};
|
||||
// TODO: Besides returning None using the `?` operator, is there a more appropriate way to handle a `None` result from `get_segment`?
|
||||
let in_segment = bezpath1.segments().last()?;
|
||||
let out_segment = bezpath2.segments().next()?;
|
||||
|
||||
let in_tangent = segment_tangent(in_segment, 1.);
|
||||
let out_tangent = segment_tangent(out_segment, 0.);
|
||||
|
||||
if in_tangent == DVec2::ZERO || out_tangent == DVec2::ZERO {
|
||||
// Avoid panic from normalizing zero vectors
|
||||
// TODO: Besides returning None, is there a more appropriate way to handle this?
|
||||
return None;
|
||||
}
|
||||
|
||||
let angle = (in_tangent * -1.).angle_to(out_tangent).abs();
|
||||
|
||||
if angle.to_degrees() < miter_limit {
|
||||
return None;
|
||||
}
|
||||
|
||||
let p1 = in_segment.end();
|
||||
let p2 = point_to_dvec2(p1) + in_tangent.normalize();
|
||||
let line1 = Line::new(p1, dvec2_to_point(p2));
|
||||
|
||||
let p1 = out_segment.start();
|
||||
let p2 = point_to_dvec2(p1) + out_tangent.normalize();
|
||||
let line2 = Line::new(p1, dvec2_to_point(p2));
|
||||
|
||||
// If we don't find the intersection point to draw the miter join, we instead default to a bevel join.
|
||||
// Otherwise, we return the element to create the join.
|
||||
let intersection = line1.crossing_point(line2)?;
|
||||
|
||||
Some([PathEl::LineTo(intersection), PathEl::LineTo(out_segment.start())])
|
||||
}
|
||||
|
||||
/// Computes the [`PathEl`] to form a circular join from `left` to `right`, along a circle around `center`.
|
||||
/// By default, the angle is assumed to be 180 degrees.
|
||||
pub fn compute_circular_subpath_details(left: DVec2, arc_point: DVec2, right: DVec2, center: DVec2, angle: Option<f64>) -> [PathEl; 2] {
|
||||
let center_to_arc_point = arc_point - center;
|
||||
|
||||
// Based on https://pomax.github.io/bezierinfo/#circles_cubic
|
||||
let handle_offset_factor = if let Some(angle) = angle { 4. / 3. * (angle / 4.).tan() } else { 0.551784777779014 };
|
||||
|
||||
let p1 = dvec2_to_point(left - (left - center).perp() * handle_offset_factor);
|
||||
let p2 = dvec2_to_point(arc_point + center_to_arc_point.perp() * handle_offset_factor);
|
||||
let p3 = dvec2_to_point(arc_point);
|
||||
|
||||
let first_half = PathEl::CurveTo(p1, p2, p3);
|
||||
|
||||
let p1 = dvec2_to_point(arc_point - center_to_arc_point.perp() * handle_offset_factor);
|
||||
let p2 = dvec2_to_point(right + (right - center).perp() * handle_offset_factor);
|
||||
let p3 = dvec2_to_point(right);
|
||||
|
||||
let second_half = PathEl::CurveTo(p1, p2, p3);
|
||||
|
||||
[first_half, second_half]
|
||||
}
|
||||
|
||||
/// Returns two [`PathEl`] to create a round join with the provided center.
|
||||
pub fn round_line_join(bezpath1: &BezPath, bezpath2: &BezPath, center: DVec2) -> [PathEl; 2] {
|
||||
let left = point_to_dvec2(bezpath1.segments().last().unwrap().end());
|
||||
let right = point_to_dvec2(bezpath2.segments().next().unwrap().start());
|
||||
|
||||
let center_to_right = right - center;
|
||||
let center_to_left = left - center;
|
||||
|
||||
let in_segment = bezpath1.segments().last();
|
||||
let in_tangent = in_segment.map(|in_segment| segment_tangent(in_segment, 1.));
|
||||
|
||||
let mut angle = center_to_right.angle_to(center_to_left) / 2.;
|
||||
let mut arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right);
|
||||
|
||||
if in_tangent.map(|in_tangent| (arc_point - left).angle_to(in_tangent).abs()).unwrap_or_default() > FRAC_PI_2 {
|
||||
angle = angle - PI * (if angle < 0. { -1. } else { 1. });
|
||||
arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right);
|
||||
}
|
||||
|
||||
compute_circular_subpath_details(left, arc_point, right, center, Some(angle))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
/// Minimum allowable separation between adjacent `t` values when calculating curve intersections
|
||||
pub const MIN_SEPARATION_VALUE: f64 = 5. * 1e-3;
|
||||
|
||||
/// Constant used to determine if `f64`s are equivalent.
|
||||
#[cfg(test)]
|
||||
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
|
||||
|
|
@ -0,0 +1,365 @@
|
|||
use super::contants::MIN_SEPARATION_VALUE;
|
||||
use kurbo::{BezPath, DEFAULT_ACCURACY, ParamCurve, PathSeg, Shape};
|
||||
|
||||
/// Calculates the intersection points the bezpath has with a given segment and returns a list of `(usize, f64)` tuples,
|
||||
/// where the `usize` represents the index of the segment in the bezpath, and the `f64` represents the `t`-value local to
|
||||
/// that segment where the intersection occurred.
|
||||
///
|
||||
/// `minimum_separation` is the minimum difference that two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
|
||||
pub fn bezpath_and_segment_intersections(bezpath: &BezPath, segment: PathSeg, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
|
||||
bezpath
|
||||
.segments()
|
||||
.enumerate()
|
||||
.flat_map(|(index, this_segment)| {
|
||||
filtered_segment_intersections(this_segment, segment, accuracy, minimum_separation)
|
||||
.into_iter()
|
||||
.map(|t| (index, t))
|
||||
.collect::<Vec<(usize, f64)>>()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Calculates the intersection points the bezpath has with another given bezpath and returns a list of parametric `t`-values.
|
||||
pub fn bezpath_intersections(bezpath1: &BezPath, bezpath2: &BezPath, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
|
||||
let mut intersection_t_values: Vec<(usize, f64)> = bezpath2
|
||||
.segments()
|
||||
.flat_map(|bezier| bezpath_and_segment_intersections(bezpath1, bezier, accuracy, minimum_separation))
|
||||
.collect();
|
||||
|
||||
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
intersection_t_values
|
||||
}
|
||||
|
||||
/// Calculates the intersection points the segment has with another given segment and returns a list of parametric `t`-values with given accuracy.
|
||||
pub fn segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Option<f64>) -> Vec<(f64, f64)> {
|
||||
let accuracy = accuracy.unwrap_or(DEFAULT_ACCURACY);
|
||||
|
||||
match (segment1, segment2) {
|
||||
(PathSeg::Line(line), segment2) => segment2.intersect_line(line).iter().map(|i| (i.line_t, i.segment_t)).collect(),
|
||||
(segment1, PathSeg::Line(line)) => segment1.intersect_line(line).iter().map(|i| (i.segment_t, i.line_t)).collect(),
|
||||
(segment1, segment2) => {
|
||||
let mut intersections = Vec::new();
|
||||
segment_intersections_inner(segment1, 0., 1., segment2, 0., 1., accuracy, &mut intersections);
|
||||
intersections
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements [https://pomax.github.io/bezierinfo/#curveintersection] to find intersection between two Bezier segments
|
||||
/// by splitting the segment recursively until the size of the subsegment's bounding box is smaller than the accuracy.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn segment_intersections_inner(segment1: PathSeg, min_t1: f64, max_t1: f64, segment2: PathSeg, min_t2: f64, max_t2: f64, accuracy: f64, intersections: &mut Vec<(f64, f64)>) {
|
||||
let bbox1 = segment1.bounding_box();
|
||||
let bbox2 = segment2.bounding_box();
|
||||
|
||||
let mid_t1 = (min_t1 + max_t1) / 2.;
|
||||
let mid_t2 = (min_t2 + max_t2) / 2.;
|
||||
|
||||
// Check if the bounding boxes overlap
|
||||
if bbox1.overlaps(bbox2) {
|
||||
// If bounding boxes overlap and they are small enough, we have found an intersection
|
||||
if bbox1.width() < accuracy && bbox1.height() < accuracy && bbox2.width() < accuracy && bbox2.height() < accuracy {
|
||||
// Use the middle `t` value, append the corresponding `t` value
|
||||
intersections.push((mid_t1, mid_t2));
|
||||
return;
|
||||
}
|
||||
|
||||
// Split curves in half
|
||||
let (seg11, seg12) = segment1.subdivide();
|
||||
let (seg21, seg22) = segment2.subdivide();
|
||||
|
||||
// Repeat checking the intersection with the combinations of the two halves of each curve
|
||||
segment_intersections_inner(seg11, min_t1, mid_t1, seg21, min_t2, mid_t2, accuracy, intersections);
|
||||
segment_intersections_inner(seg11, min_t1, mid_t1, seg22, mid_t2, max_t2, accuracy, intersections);
|
||||
segment_intersections_inner(seg12, mid_t1, max_t1, seg21, min_t2, mid_t2, accuracy, intersections);
|
||||
segment_intersections_inner(seg12, mid_t1, max_t1, seg22, mid_t2, max_t2, accuracy, intersections);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 segment and the provided one
|
||||
/// 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 segment, not the provided parameter.
|
||||
/// If the provided segment is linear, then zero intersection points will be returned along colinear segments.
|
||||
///
|
||||
/// `accuracy` defines, for intersections where the provided bezier segment is non-linear, the maximum size of the bounding boxes to be considered an intersection point.
|
||||
///
|
||||
/// `minimum_separation` is the minimum difference between adjacent `t` values in sorted order.
|
||||
pub fn filtered_segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<f64> {
|
||||
let mut intersection_t_values = segment_intersections(segment1, segment2, accuracy);
|
||||
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
|
||||
intersection_t_values.iter().map(|x| x.0).fold(Vec::new(), |mut accumulator, t| {
|
||||
if !accumulator.is_empty() && (accumulator.last().unwrap() - t).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE) {
|
||||
accumulator.pop();
|
||||
}
|
||||
accumulator.push(t);
|
||||
accumulator
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Returns a list of pairs 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 separation value. If the difference between
|
||||
/// two adjacent `t` values is less than the minimum difference, the filtering takes the larger `t` value and discards the smaller `t` value.
|
||||
/// The first value in pair is with respect to the current bezier and the second value in pair is with respect to 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, defines the threshold for bounding boxes to be considered an intersection point.
|
||||
///
|
||||
/// `minimum_separation` is the minimum difference between adjacent `t` values in sorted order
|
||||
pub fn filtered_all_segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<(f64, f64)> {
|
||||
let mut intersection_t_values = segment_intersections(segment1, segment2, accuracy);
|
||||
intersection_t_values.sort_by(|a, b| (a.0 + a.1).partial_cmp(&(b.0 + b.1)).unwrap());
|
||||
|
||||
intersection_t_values.iter().fold(Vec::new(), |mut accumulator, t| {
|
||||
if !accumulator.is_empty()
|
||||
&& (accumulator.last().unwrap().0 - t.0).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE)
|
||||
&& (accumulator.last().unwrap().1 - t.1).abs() < minimum_separation.unwrap_or(MIN_SEPARATION_VALUE)
|
||||
{
|
||||
accumulator.pop();
|
||||
}
|
||||
accumulator.push(*t);
|
||||
accumulator
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{bezpath_and_segment_intersections, filtered_segment_intersections};
|
||||
use crate::vector::algorithms::{
|
||||
contants::MAX_ABSOLUTE_DIFFERENCE,
|
||||
util::{compare_points, compare_vec_of_points, dvec2_compare},
|
||||
};
|
||||
|
||||
use kurbo::{BezPath, CubicBez, Line, ParamCurve, PathEl, PathSeg, Point, QuadBez};
|
||||
|
||||
#[test]
|
||||
fn test_intersect_line_segment_quadratic() {
|
||||
let p1 = Point::new(30., 50.);
|
||||
let p2 = Point::new(140., 30.);
|
||||
let p3 = Point::new(160., 170.);
|
||||
|
||||
// Intersection at edge of curve
|
||||
let bezier = PathSeg::Quad(QuadBez::new(p1, p2, p3));
|
||||
let line1 = PathSeg::Line(Line::new(Point::new(20., 50.), Point::new(40., 50.)));
|
||||
let intersections1 = filtered_segment_intersections(bezier, line1, None, None);
|
||||
assert!(intersections1.len() == 1);
|
||||
assert!(compare_points(bezier.eval(intersections1[0]), p1));
|
||||
|
||||
// Intersection in the middle of curve
|
||||
let line2 = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(30., 30.)));
|
||||
let intersections2 = filtered_segment_intersections(bezier, line2, None, None);
|
||||
assert!(compare_points(bezier.eval(intersections2[0]), Point::new(47.77355, 47.77354)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_intersect_curve_cubic_edge_case() {
|
||||
// M34 107 C40 40 120 120 102 29
|
||||
|
||||
let p1 = Point::new(34., 107.);
|
||||
let p2 = Point::new(40., 40.);
|
||||
let p3 = Point::new(120., 120.);
|
||||
let p4 = Point::new(102., 29.);
|
||||
let cubic_segment = PathSeg::Cubic(CubicBez::new(p1, p2, p3, p4));
|
||||
|
||||
let linear_segment = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.)));
|
||||
let intersections = filtered_segment_intersections(cubic_segment, linear_segment, None, None);
|
||||
|
||||
assert_eq!(intersections.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_intersect_curve() {
|
||||
let p0 = Point::new(30., 30.);
|
||||
let p1 = Point::new(60., 140.);
|
||||
let p2 = Point::new(150., 30.);
|
||||
let p3 = Point::new(160., 160.);
|
||||
|
||||
let cubic_segment = PathSeg::Cubic(CubicBez::new(p0, p1, p2, p3));
|
||||
|
||||
let p0 = Point::new(175., 140.);
|
||||
let p1 = Point::new(20., 20.);
|
||||
let p2 = Point::new(120., 20.);
|
||||
|
||||
let quadratic_segment = PathSeg::Quad(QuadBez::new(p0, p1, p2));
|
||||
|
||||
let intersections1 = filtered_segment_intersections(cubic_segment, quadratic_segment, None, None);
|
||||
let intersections2 = filtered_segment_intersections(quadratic_segment, cubic_segment, None, None);
|
||||
|
||||
let intersections1_points: Vec<Point> = intersections1.iter().map(|&t| cubic_segment.eval(t)).collect();
|
||||
let intersections2_points: Vec<Point> = intersections2.iter().map(|&t| quadratic_segment.eval(t)).rev().collect();
|
||||
|
||||
assert!(compare_vec_of_points(intersections1_points, intersections2_points, 2.));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_linear_multiple_subpath_curves_test_one() {
|
||||
// M 35 125 C 40 40 120 120 43 43 Q 175 90 145 150 Q 70 185 35 125 Z
|
||||
|
||||
let cubic_start = Point::new(35., 125.);
|
||||
let cubic_handle_1 = Point::new(40., 40.);
|
||||
let cubic_handle_2 = Point::new(120., 120.);
|
||||
let cubic_end = Point::new(43., 43.);
|
||||
|
||||
let quadratic_1_handle = Point::new(175., 90.);
|
||||
let quadratic_end = Point::new(145., 150.);
|
||||
|
||||
let quadratic_2_handle = Point::new(70., 185.);
|
||||
|
||||
let cubic_segment = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end));
|
||||
let quadratic_segment = PathSeg::Quad(QuadBez::new(cubic_end, quadratic_1_handle, quadratic_end));
|
||||
|
||||
let bezpath = BezPath::from_vec(vec![
|
||||
PathEl::MoveTo(cubic_start),
|
||||
PathEl::CurveTo(cubic_handle_1, cubic_handle_2, cubic_end),
|
||||
PathEl::QuadTo(quadratic_1_handle, quadratic_end),
|
||||
PathEl::QuadTo(quadratic_2_handle, cubic_start),
|
||||
PathEl::ClosePath,
|
||||
]);
|
||||
|
||||
let linear_segment = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.)));
|
||||
|
||||
let cubic_intersections = filtered_segment_intersections(cubic_segment, linear_segment, None, None);
|
||||
let quadratic_1_intersections = filtered_segment_intersections(quadratic_segment, linear_segment, None, None);
|
||||
let bezpath_intersections = bezpath_and_segment_intersections(&bezpath, linear_segment, None, None);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
cubic_segment.eval(cubic_intersections[0]),
|
||||
bezpath.segments().nth(bezpath_intersections[0].0).unwrap().eval(bezpath_intersections[0].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
quadratic_segment.eval(quadratic_1_intersections[0]),
|
||||
bezpath.segments().nth(bezpath_intersections[1].0).unwrap().eval(bezpath_intersections[1].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
quadratic_segment.eval(quadratic_1_intersections[1]),
|
||||
bezpath.segments().nth(bezpath_intersections[2].0).unwrap().eval(bezpath_intersections[2].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_linear_multiple_subpath_curves_test_two() {
|
||||
// M34 107 C40 40 120 120 102 29 Q175 90 129 171 Q70 185 34 107 Z
|
||||
// M150 150 L 20 20
|
||||
|
||||
let cubic_start = Point::new(34., 107.);
|
||||
let cubic_handle_1 = Point::new(40., 40.);
|
||||
let cubic_handle_2 = Point::new(120., 120.);
|
||||
let cubic_end = Point::new(102., 29.);
|
||||
|
||||
let quadratic_1_handle = Point::new(175., 90.);
|
||||
let quadratic_end = Point::new(129., 171.);
|
||||
|
||||
let quadratic_2_handle = Point::new(70., 185.);
|
||||
|
||||
let cubic_segment = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end));
|
||||
let quadratic_segment = PathSeg::Quad(QuadBez::new(cubic_end, quadratic_1_handle, quadratic_end));
|
||||
|
||||
let bezpath = BezPath::from_vec(vec![
|
||||
PathEl::MoveTo(cubic_start),
|
||||
PathEl::CurveTo(cubic_handle_1, cubic_handle_2, cubic_end),
|
||||
PathEl::QuadTo(quadratic_1_handle, quadratic_end),
|
||||
PathEl::QuadTo(quadratic_2_handle, cubic_start),
|
||||
PathEl::ClosePath,
|
||||
]);
|
||||
|
||||
let line = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.)));
|
||||
|
||||
let cubic_intersections = filtered_segment_intersections(cubic_segment, line, None, None);
|
||||
let quadratic_1_intersections = filtered_segment_intersections(quadratic_segment, line, None, None);
|
||||
let bezpath_intersections = bezpath_and_segment_intersections(&bezpath, line, None, None);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
cubic_segment.eval(cubic_intersections[0]),
|
||||
bezpath.segments().nth(bezpath_intersections[0].0).unwrap().eval(bezpath_intersections[0].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
quadratic_segment.eval(quadratic_1_intersections[0]),
|
||||
bezpath.segments().nth(bezpath_intersections[1].0).unwrap().eval(bezpath_intersections[1].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_linear_multiple_subpath_curves_test_three() {
|
||||
// M35 125 C40 40 120 120 44 44 Q175 90 145 150 Q70 185 35 125 Z
|
||||
|
||||
let cubic_start = Point::new(35., 125.);
|
||||
let cubic_handle_1 = Point::new(40., 40.);
|
||||
let cubic_handle_2 = Point::new(120., 120.);
|
||||
let cubic_end = Point::new(44., 44.);
|
||||
|
||||
let quadratic_1_handle = Point::new(175., 90.);
|
||||
let quadratic_end = Point::new(145., 150.);
|
||||
|
||||
let quadratic_2_handle = Point::new(70., 185.);
|
||||
|
||||
let cubic_segment = PathSeg::Cubic(CubicBez::new(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end));
|
||||
let quadratic_segment = PathSeg::Quad(QuadBez::new(cubic_end, quadratic_1_handle, quadratic_end));
|
||||
|
||||
let bezpath = BezPath::from_vec(vec![
|
||||
PathEl::MoveTo(cubic_start),
|
||||
PathEl::CurveTo(cubic_handle_1, cubic_handle_2, cubic_end),
|
||||
PathEl::QuadTo(quadratic_1_handle, quadratic_end),
|
||||
PathEl::QuadTo(quadratic_2_handle, cubic_start),
|
||||
PathEl::ClosePath,
|
||||
]);
|
||||
|
||||
let line = PathSeg::Line(Line::new(Point::new(150., 150.), Point::new(20., 20.)));
|
||||
|
||||
let cubic_intersections = filtered_segment_intersections(cubic_segment, line, None, None);
|
||||
let quadratic_1_intersections = filtered_segment_intersections(quadratic_segment, line, None, None);
|
||||
let bezpath_intersections = bezpath_and_segment_intersections(&bezpath, line, None, None);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
cubic_segment.eval(cubic_intersections[0]),
|
||||
bezpath.segments().nth(bezpath_intersections[0].0).unwrap().eval(bezpath_intersections[0].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
quadratic_segment.eval(quadratic_1_intersections[0]),
|
||||
bezpath.segments().nth(bezpath_intersections[1].0).unwrap().eval(bezpath_intersections[1].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
|
||||
assert!(
|
||||
dvec2_compare(
|
||||
quadratic_segment.eval(quadratic_1_intersections[1]),
|
||||
bezpath.segments().nth(bezpath_intersections[2].0).unwrap().eval(bezpath_intersections[2].1),
|
||||
MAX_ABSOLUTE_DIFFERENCE
|
||||
)
|
||||
.all()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
pub mod bezpath_algorithms;
|
||||
mod contants;
|
||||
pub mod instance;
|
||||
pub mod intersection;
|
||||
pub mod merge_by_distance;
|
||||
pub mod offset_subpath;
|
||||
pub mod poisson_disk;
|
||||
pub mod spline;
|
||||
pub mod util;
|
||||
|
|
|
|||
|
|
@ -1,173 +1,137 @@
|
|||
use crate::vector::PointId;
|
||||
use bezier_rs::{Bezier, BezierHandles, Join, Subpath, TValue};
|
||||
use super::bezpath_algorithms::{clip_simple_bezpaths, miter_line_join, round_line_join};
|
||||
use crate::vector::misc::point_to_dvec2;
|
||||
use kurbo::{BezPath, Join, ParamCurve, PathEl, PathSeg};
|
||||
|
||||
/// Value to control smoothness and mathematical accuracy to offset a cubic Bezier.
|
||||
const CUBIC_REGULARIZATION_ACCURACY: f64 = 0.5;
|
||||
/// Accuracy of fitting offset curve to Bezier paths.
|
||||
const CUBIC_TO_BEZPATH_ACCURACY: f64 = 1e-3;
|
||||
/// Constant used to determine if `f64`s are equivalent.
|
||||
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
|
||||
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-7;
|
||||
|
||||
fn segment_to_bezier(seg: kurbo::PathSeg) -> Bezier {
|
||||
match seg {
|
||||
kurbo::PathSeg::Line(line) => Bezier::from_linear_coordinates(line.p0.x, line.p0.y, line.p1.x, line.p1.y),
|
||||
kurbo::PathSeg::Quad(quad_bez) => Bezier::from_quadratic_coordinates(quad_bez.p0.x, quad_bez.p0.y, quad_bez.p1.x, quad_bez.p1.y, quad_bez.p1.x, quad_bez.p1.y),
|
||||
kurbo::PathSeg::Cubic(cubic_bez) => Bezier::from_cubic_coordinates(
|
||||
cubic_bez.p0.x,
|
||||
cubic_bez.p0.y,
|
||||
cubic_bez.p1.x,
|
||||
cubic_bez.p1.y,
|
||||
cubic_bez.p2.x,
|
||||
cubic_bez.p2.y,
|
||||
cubic_bez.p3.x,
|
||||
cubic_bez.p3.y,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Replace the implementation to use only Kurbo API.
|
||||
/// Reduces the segments of the subpath into simple subcurves, then offset each subcurve a set `distance` away.
|
||||
/// Reduces the segments of the bezpath into simple subcurves, then offset each subcurve a set `distance` away.
|
||||
/// The intersections of segments of the subpath are joined using the method specified by the `join` argument.
|
||||
pub fn offset_subpath(subpath: &Subpath<PointId>, distance: f64, join: Join) -> Subpath<PointId> {
|
||||
pub fn offset_bezpath(bezpath: &BezPath, distance: f64, join: Join, miter_limit: Option<f64>) -> BezPath {
|
||||
// An offset at a distance 0 from the curve is simply the same curve.
|
||||
// An offset of a single point is not defined.
|
||||
if distance == 0. || subpath.len() <= 1 || subpath.len_segments() < 1 {
|
||||
return subpath.clone();
|
||||
if distance == 0. || bezpath.get_seg(1).is_none() {
|
||||
return bezpath.clone();
|
||||
}
|
||||
|
||||
let mut subpaths = subpath
|
||||
.iter()
|
||||
.filter(|bezier| !bezier.is_point())
|
||||
let mut bezpaths = bezpath
|
||||
.segments()
|
||||
.map(|bezier| bezier.to_cubic())
|
||||
.map(|cubic| {
|
||||
let Bezier { start, end, handles } = cubic;
|
||||
let BezierHandles::Cubic { handle_start, handle_end } = handles else { unreachable!()};
|
||||
|
||||
let cubic_bez = kurbo::CubicBez::new((start.x, start.y), (handle_start.x, handle_start.y), (handle_end.x, handle_end.y), (end.x, end.y));
|
||||
.map(|cubic_bez| {
|
||||
let cubic_offset = kurbo::offset::CubicOffset::new_regularized(cubic_bez, distance, CUBIC_REGULARIZATION_ACCURACY);
|
||||
let offset_bezpath = kurbo::fit_to_bezpath(&cubic_offset, CUBIC_TO_BEZPATH_ACCURACY);
|
||||
|
||||
let beziers = offset_bezpath.segments().fold(Vec::new(), |mut acc, seg| {
|
||||
acc.push(segment_to_bezier(seg));
|
||||
acc
|
||||
});
|
||||
|
||||
Subpath::from_beziers(&beziers, false)
|
||||
kurbo::fit_to_bezpath(&cubic_offset, CUBIC_TO_BEZPATH_ACCURACY)
|
||||
})
|
||||
.filter(|subpath| subpath.len() >= 2) // In some cases the reduced and scaled bézier is marked by is_point (so the subpath is empty).
|
||||
.collect::<Vec<Subpath<PointId>>>();
|
||||
|
||||
let mut drop_common_point = vec![true; subpath.len()];
|
||||
.filter(|bezpath| bezpath.get_seg(1).is_some()) // In some cases the reduced and scaled bézier is marked by is_point (so the subpath is empty).
|
||||
.collect::<Vec<BezPath>>();
|
||||
|
||||
// Clip or join consecutive Subpaths
|
||||
for i in 0..subpaths.len() - 1 {
|
||||
for i in 0..bezpaths.len() - 1 {
|
||||
let j = i + 1;
|
||||
let subpath1 = &subpaths[i];
|
||||
let subpath2 = &subpaths[j];
|
||||
let bezpath1 = &bezpaths[i];
|
||||
let bezpath2 = &bezpaths[j];
|
||||
|
||||
let last_segment = subpath1.get_segment(subpath1.len_segments() - 1).unwrap();
|
||||
let first_segment = subpath2.get_segment(0).unwrap();
|
||||
let last_segment_end = point_to_dvec2(bezpath1.segments().last().unwrap().end());
|
||||
let first_segment_start = point_to_dvec2(bezpath2.segments().next().unwrap().start());
|
||||
|
||||
// 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) {
|
||||
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 = subpath.get_segment(i).unwrap().tangent(TValue::Parametric(1.));
|
||||
let in_tangent = subpath.get_segment(j).unwrap().tangent(TValue::Parametric(0.));
|
||||
let angle = out_tangent.angle_to(in_tangent);
|
||||
|
||||
// The angle is concave. The Subpath overlap and must be clipped
|
||||
let mut apply_join = 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_join = false;
|
||||
}
|
||||
|
||||
if let Some((clipped_subpath1, clipped_subpath2)) = clip_simple_bezpaths(bezpath1, bezpath2) {
|
||||
bezpaths[i] = clipped_subpath1;
|
||||
bezpaths[j] = clipped_subpath2;
|
||||
apply_join = false;
|
||||
}
|
||||
// The angle is convex. The Subpath must be joined using the specified join type
|
||||
if apply_join {
|
||||
drop_common_point[j] = false;
|
||||
match join {
|
||||
Join::Bevel => {}
|
||||
Join::Miter(miter_limit) => {
|
||||
let miter_manipulator_group = subpaths[i].miter_line_join(&subpaths[j], miter_limit);
|
||||
if let Some(miter_manipulator_group) = miter_manipulator_group {
|
||||
subpaths[i].manipulator_groups_mut().push(miter_manipulator_group);
|
||||
Join::Bevel => {
|
||||
let element = PathEl::LineTo(bezpaths[j].segments().next().unwrap().start());
|
||||
bezpaths[i].push(element);
|
||||
}
|
||||
Join::Miter => {
|
||||
let element = miter_line_join(&bezpaths[i], &bezpaths[j], miter_limit);
|
||||
if let Some(element) = element {
|
||||
bezpaths[i].push(element[0]);
|
||||
bezpaths[i].push(element[1]);
|
||||
} else {
|
||||
let element = PathEl::LineTo(bezpaths[j].segments().next().unwrap().start());
|
||||
bezpaths[i].push(element);
|
||||
}
|
||||
}
|
||||
Join::Round => {
|
||||
let (out_handle, round_point, in_handle) = subpaths[i].round_line_join(&subpaths[j], subpath.manipulator_groups()[j].anchor);
|
||||
let last_index = subpaths[i].manipulator_groups().len() - 1;
|
||||
subpaths[i].manipulator_groups_mut()[last_index].out_handle = Some(out_handle);
|
||||
subpaths[i].manipulator_groups_mut().push(round_point);
|
||||
subpaths[j].manipulator_groups_mut()[0].in_handle = Some(in_handle);
|
||||
let center = point_to_dvec2(bezpath.get_seg(i + 1).unwrap().end());
|
||||
let elements = round_line_join(&bezpaths[i], &bezpaths[j], center);
|
||||
bezpaths[i].push(elements[0]);
|
||||
bezpaths[i].push(elements[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clip any overlap in the last segment
|
||||
if subpath.closed {
|
||||
let out_tangent = subpath.get_segment(subpath.len_segments() - 1).unwrap().tangent(TValue::Parametric(1.));
|
||||
let in_tangent = subpath.get_segment(0).unwrap().tangent(TValue::Parametric(0.));
|
||||
let angle = out_tangent.angle_to(in_tangent);
|
||||
|
||||
let is_bezpath_closed = bezpath.elements().last().is_some_and(|element| *element == PathEl::ClosePath);
|
||||
if is_bezpath_closed {
|
||||
let mut apply_join = 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_join = false;
|
||||
}
|
||||
if let Some((clipped_subpath1, clipped_subpath2)) = clip_simple_bezpaths(&bezpaths[bezpaths.len() - 1], &bezpaths[0]) {
|
||||
// Merge the clipped subpaths
|
||||
let last_index = bezpaths.len() - 1;
|
||||
bezpaths[last_index] = clipped_subpath1;
|
||||
bezpaths[0] = clipped_subpath2;
|
||||
apply_join = false;
|
||||
}
|
||||
|
||||
if apply_join {
|
||||
drop_common_point[0] = false;
|
||||
match join {
|
||||
Join::Bevel => {}
|
||||
Join::Miter(miter_limit) => {
|
||||
let last_subpath_index = subpaths.len() - 1;
|
||||
let miter_manipulator_group = subpaths[last_subpath_index].miter_line_join(&subpaths[0], miter_limit);
|
||||
if let Some(miter_manipulator_group) = miter_manipulator_group {
|
||||
subpaths[last_subpath_index].manipulator_groups_mut().push(miter_manipulator_group);
|
||||
Join::Bevel => {
|
||||
let last_subpath_index = bezpaths.len() - 1;
|
||||
let element = PathEl::LineTo(bezpaths[0].segments().next().unwrap().start());
|
||||
bezpaths[last_subpath_index].push(element);
|
||||
}
|
||||
Join::Miter => {
|
||||
let last_subpath_index = bezpaths.len() - 1;
|
||||
let element = miter_line_join(&bezpaths[last_subpath_index], &bezpaths[0], miter_limit);
|
||||
if let Some(element) = element {
|
||||
bezpaths[last_subpath_index].push(element[0]);
|
||||
bezpaths[last_subpath_index].push(element[1]);
|
||||
} else {
|
||||
let element = PathEl::LineTo(bezpaths[0].segments().next().unwrap().start());
|
||||
bezpaths[last_subpath_index].push(element);
|
||||
}
|
||||
}
|
||||
Join::Round => {
|
||||
let last_subpath_index = subpaths.len() - 1;
|
||||
let (out_handle, round_point, in_handle) = subpaths[last_subpath_index].round_line_join(&subpaths[0], subpath.manipulator_groups()[0].anchor);
|
||||
let last_index = subpaths[last_subpath_index].manipulator_groups().len() - 1;
|
||||
subpaths[last_subpath_index].manipulator_groups_mut()[last_index].out_handle = Some(out_handle);
|
||||
subpaths[last_subpath_index].manipulator_groups_mut().push(round_point);
|
||||
subpaths[0].manipulator_groups_mut()[0].in_handle = Some(in_handle);
|
||||
let last_subpath_index = bezpaths.len() - 1;
|
||||
let center = point_to_dvec2(bezpath.get_seg(1).unwrap().start());
|
||||
let elements = round_line_join(&bezpaths[last_subpath_index], &bezpaths[0], center);
|
||||
bezpaths[last_subpath_index].push(elements[0]);
|
||||
bezpaths[last_subpath_index].push(elements[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge the subpaths. Drop points which overlap with one another.
|
||||
let mut manipulator_groups = subpaths[0].manipulator_groups().to_vec();
|
||||
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().to_vec();
|
||||
manipulators_copy[0].in_handle = last_group.in_handle;
|
||||
|
||||
manipulator_groups.append(&mut manipulators_copy);
|
||||
} else {
|
||||
manipulator_groups.append(&mut subpaths[i].manipulator_groups().to_vec());
|
||||
// Merge the bezpaths and its segments. Drop points which overlap with one another.
|
||||
let segments = bezpaths.iter().flat_map(|bezpath| bezpath.segments().collect::<Vec<PathSeg>>()).collect::<Vec<PathSeg>>();
|
||||
let mut offset_bezpath = segments.iter().fold(BezPath::new(), |mut acc, segment| {
|
||||
if acc.elements().is_empty() {
|
||||
acc.move_to(segment.start());
|
||||
}
|
||||
}
|
||||
if subpath.closed && drop_common_point[0] {
|
||||
let last_group = manipulator_groups.pop().unwrap();
|
||||
manipulator_groups[0].in_handle = last_group.in_handle;
|
||||
acc.push(segment.as_path_el());
|
||||
acc
|
||||
});
|
||||
|
||||
if is_bezpath_closed {
|
||||
offset_bezpath.close_path();
|
||||
}
|
||||
|
||||
Subpath::new(manipulator_groups, subpath.closed)
|
||||
offset_bezpath
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
use glam::DVec2;
|
||||
use kurbo::{ParamCurve, ParamCurveDeriv, PathSeg};
|
||||
|
||||
pub fn segment_tangent(segment: PathSeg, t: f64) -> DVec2 {
|
||||
// NOTE: .deriv() method gives inaccurate result when it is 1.
|
||||
let t = if t == 1. { 1. - f64::EPSILON } else { t };
|
||||
|
||||
let tangent = match segment {
|
||||
PathSeg::Line(line) => line.deriv().eval(t),
|
||||
PathSeg::Quad(quad_bez) => quad_bez.deriv().eval(t),
|
||||
PathSeg::Cubic(cubic_bez) => cubic_bez.deriv().eval(t),
|
||||
};
|
||||
|
||||
DVec2::new(tangent.x, tangent.y)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
(f1 - f2).abs() < super::contants::MAX_ABSOLUTE_DIFFERENCE
|
||||
}
|
||||
|
||||
/// Compare points by allowing some maximum absolute difference to account for floating point errors
|
||||
#[cfg(test)]
|
||||
pub fn compare_points(p1: kurbo::Point, p2: kurbo::Point) -> bool {
|
||||
let (p1, p2) = (crate::vector::misc::point_to_dvec2(p1), crate::vector::misc::point_to_dvec2(p2));
|
||||
p1.abs_diff_eq(p2, super::contants::MAX_ABSOLUTE_DIFFERENCE)
|
||||
}
|
||||
|
||||
/// 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<kurbo::Point>, b: Vec<kurbo::Point>, max_absolute_difference: f64) -> bool {
|
||||
a.len() == b.len()
|
||||
&& a.into_iter()
|
||||
.zip(b)
|
||||
.map(|(p1, p2)| (crate::vector::misc::point_to_dvec2(p1), crate::vector::misc::point_to_dvec2(p2)))
|
||||
.all(|(p1, p2)| p1.abs_diff_eq(p2, max_absolute_difference))
|
||||
}
|
||||
|
||||
/// Compare the two values in a `DVec2` independently with a provided max absolute value difference.
|
||||
#[cfg(test)]
|
||||
pub fn dvec2_compare(a: kurbo::Point, b: kurbo::Point, max_abs_diff: f64) -> glam::BVec2 {
|
||||
glam::BVec2::new((a.x - b.x).abs() < max_abs_diff, (a.y - b.y).abs() < max_abs_diff)
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
use super::algorithms::bezpath_algorithms::{self, position_on_bezpath, sample_polyline_on_bezpath, split_bezpath, tangent_on_bezpath};
|
||||
use super::algorithms::offset_subpath::offset_subpath;
|
||||
use super::algorithms::bezpath_algorithms::{self, evaluate_bezpath, sample_polyline_on_bezpath, split_bezpath, tangent_on_bezpath};
|
||||
use super::algorithms::offset_subpath::offset_bezpath;
|
||||
use super::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
|
||||
use super::misc::{CentroidType, point_to_dvec2};
|
||||
use super::style::{Fill, Gradient, GradientStops, Stroke};
|
||||
|
|
@ -17,8 +17,7 @@ use crate::vector::misc::{handles_to_segment, segment_to_handles};
|
|||
use crate::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use crate::vector::{FillId, RegionId};
|
||||
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl};
|
||||
|
||||
use bezier_rs::{BezierHandles, Join, ManipulatorGroup, Subpath};
|
||||
use bezier_rs::{BezierHandles, ManipulatorGroup, Subpath};
|
||||
use core::f64::consts::PI;
|
||||
use core::hash::{Hash, Hasher};
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
|
@ -979,10 +978,10 @@ async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, j
|
|||
vector_data
|
||||
.instance_iter()
|
||||
.map(|mut vector_data_instance| {
|
||||
let vector_data_transform = vector_data_instance.transform;
|
||||
let vector_data_transform = Affine::new(vector_data_instance.transform.to_cols_array());
|
||||
let vector_data = vector_data_instance.instance;
|
||||
|
||||
let subpaths = vector_data.stroke_bezier_paths();
|
||||
let bezpaths = vector_data.stroke_bezpath_iter();
|
||||
let mut result = VectorData {
|
||||
style: vector_data.style.clone(),
|
||||
..Default::default()
|
||||
|
|
@ -990,24 +989,25 @@ async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, j
|
|||
result.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
|
||||
// Perform operation on all subpaths in this shape.
|
||||
for mut subpath in subpaths {
|
||||
subpath.apply_transform(vector_data_transform);
|
||||
for mut bezpath in bezpaths {
|
||||
bezpath.apply_affine(vector_data_transform);
|
||||
|
||||
// Taking the existing stroke data and passing it to Bezier-rs to generate new paths.
|
||||
let mut subpath_out = offset_subpath(
|
||||
&subpath,
|
||||
let mut bezpath_out = offset_bezpath(
|
||||
&bezpath,
|
||||
-distance,
|
||||
match join {
|
||||
StrokeJoin::Miter => Join::Miter(Some(miter_limit)),
|
||||
StrokeJoin::Bevel => Join::Bevel,
|
||||
StrokeJoin::Round => Join::Round,
|
||||
StrokeJoin::Miter => kurbo::Join::Miter,
|
||||
StrokeJoin::Bevel => kurbo::Join::Bevel,
|
||||
StrokeJoin::Round => kurbo::Join::Round,
|
||||
},
|
||||
Some(miter_limit),
|
||||
);
|
||||
|
||||
subpath_out.apply_transform(vector_data_transform.inverse());
|
||||
bezpath_out.apply_affine(vector_data_transform.inverse());
|
||||
|
||||
// One closed subpath, open path.
|
||||
result.append_subpath(subpath_out, false);
|
||||
result.append_bezpath(bezpath_out);
|
||||
}
|
||||
|
||||
vector_data_instance.instance = result;
|
||||
|
|
@ -1325,7 +1325,7 @@ async fn position_on_path(
|
|||
let t = if progress == bezpath_count { 1. } else { progress.fract() };
|
||||
bezpath.apply_affine(Affine::new(transform.to_cols_array()));
|
||||
|
||||
point_to_dvec2(position_on_bezpath(bezpath, t, euclidian, None))
|
||||
point_to_dvec2(evaluate_bezpath(bezpath, t, euclidian, None))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue