Dedupe code for BezPath emission, epsilon constants, and Polynomial internals (#4459)

* Collapse duplicated BezPath emission, constants, and Polynomial surface across the vector algorithms

* Fix the quadratic segment length lower bound using a control leg instead of the chord
This commit is contained in:
Keavon Chambers
2026-08-18 16:24:12 -07:00
committed by GitHub
parent 5c580f9dac
commit c19f79977b
12 changed files with 62 additions and 233 deletions

View File

@@ -1,10 +1,9 @@
use kurbo::PathSeg;
use std::fmt::{self, Display, Formatter};
use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign};
use std::ops::{Mul, MulAssign};
/// A struct that represents a polynomial with a maximum degree of `N-1`.
///
/// It provides basic mathematical operations for polynomials like addition, multiplication, differentiation, integration, etc.
/// It provides basic mathematical operations for polynomials like multiplication, differentiation, integration, etc.
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Polynomial<const N: usize> {
coefficients: [f64; N],
@@ -18,18 +17,6 @@ impl<const N: usize> Polynomial<N> {
Polynomial { coefficients }
}
/// Create a polynomial where all its coefficients are zero.
pub fn zero() -> Polynomial<N> {
Polynomial { coefficients: [0.; N] }
}
/// Return an immutable reference to the coefficients.
///
/// The coefficient for nth degree is at the nth index in array. Therefore the order of coefficients are reversed than the usual order for writing polynomials mathematically.
pub fn coefficients(&self) -> &[f64; N] {
&self.coefficients
}
/// Return a mutable reference to the coefficients.
///
/// The coefficient for nth degree is at the nth index in array. Therefore the order of coefficients are reversed than the usual order for writing polynomials mathematically.
@@ -83,98 +70,6 @@ impl<const N: usize> Polynomial<N> {
ans.derivative_mut();
ans
}
/// Computes the antiderivative at `C = 0`.
///
/// Returns `None` if the polynomial is not big enough to accommodate the extra degree.
pub fn antiderivative(&self) -> Option<Polynomial<N>> {
let mut ans = *self;
ans.antiderivative_mut()?;
Some(ans)
}
}
impl<const N: usize> Default for Polynomial<N> {
fn default() -> Self {
Self::zero()
}
}
impl<const N: usize> Display for Polynomial<N> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut first = true;
for (index, coefficient) in self.coefficients.iter().enumerate().rev().filter(|&(_, &coefficient)| coefficient != 0.) {
if first {
first = false;
} else {
f.write_str(" + ")?
}
coefficient.fmt(f)?;
if index == 0 {
continue;
}
f.write_str("x")?;
if index == 1 {
continue;
}
f.write_str("^")?;
index.fmt(f)?;
}
Ok(())
}
}
impl<const N: usize> AddAssign<&Polynomial<N>> for Polynomial<N> {
fn add_assign(&mut self, rhs: &Polynomial<N>) {
self.coefficients.iter_mut().zip(rhs.coefficients.iter()).for_each(|(a, b)| *a += b);
}
}
impl<const N: usize> Add for &Polynomial<N> {
type Output = Polynomial<N>;
fn add(self, other: &Polynomial<N>) -> Polynomial<N> {
let mut output = *self;
output += other;
output
}
}
impl<const N: usize> Neg for &Polynomial<N> {
type Output = Polynomial<N>;
fn neg(self) -> Polynomial<N> {
let mut output = *self;
output.coefficients.iter_mut().for_each(|x| *x = -*x);
output
}
}
impl<const N: usize> Neg for Polynomial<N> {
type Output = Polynomial<N>;
fn neg(mut self) -> Polynomial<N> {
self.coefficients.iter_mut().for_each(|x| *x = -*x);
self
}
}
impl<const N: usize> SubAssign<&Polynomial<N>> for Polynomial<N> {
fn sub_assign(&mut self, rhs: &Polynomial<N>) {
self.coefficients.iter_mut().zip(rhs.coefficients.iter()).for_each(|(a, b)| *a -= b);
}
}
impl<const N: usize> Sub for &Polynomial<N> {
type Output = Polynomial<N>;
fn sub(self, other: &Polynomial<N>) -> Polynomial<N> {
let mut output = *self;
output -= other;
output
}
}
impl<const N: usize> MulAssign<&Polynomial<N>> for Polynomial<N> {
@@ -248,18 +143,6 @@ mod test {
assert_eq!(p2.as_size::<2>(), None);
}
#[test]
fn addition_and_subtaction() {
let p1 = Polynomial::new([1., 2., 3.]);
let p2 = Polynomial::new([4., 5., 6.]);
let addition = Polynomial::new([5., 7., 9.]);
let subtraction = Polynomial::new([-3., -3., -3.]);
assert_eq!(&p1 + &p2, addition);
assert_eq!(&p1 - &p2, subtraction);
}
#[test]
fn multiplication() {
let p1 = Polynomial::new([1., 2., 3.]).as_size().unwrap();
@@ -278,15 +161,10 @@ mod test {
assert_eq!(p.derivative(), p_deriv);
p.coefficients_mut()[0] = 0.;
assert_eq!(p_deriv.antiderivative().unwrap(), p);
let mut antiderivative = p_deriv;
assert_eq!(antiderivative.antiderivative_mut(), Some(()));
assert_eq!(antiderivative, p);
assert_eq!(p.antiderivative(), None);
}
#[test]
fn display() {
let p = Polynomial::new([1., 2., 0., 3.]);
assert_eq!(format!("{p:.2}"), "3.00x^3 + 2.00x + 1.00");
assert_eq!(p.antiderivative_mut(), None);
}
}

View File

@@ -1,15 +1,13 @@
use super::consts::MAX_ABSOLUTE_DIFFERENCE;
use super::intersection::{bezpath_intersections, filtered_all_segment_intersections, pathseg_self_intersections};
use super::poisson_disk::poisson_disk_sample;
use super::util::pathseg_tangent;
use crate::vector::misc::{PointSpacingType, dvec2_to_point, point_to_dvec2};
use core_types::math::polynomial::pathseg_to_parametric_polynomial;
use glam::{DMat2, DVec2};
use kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArclen, ParamCurveDeriv, PathEl, PathSeg, Point, QuadBez, Rect, Shape, Vec2};
use kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Point, QuadBez, Rect, Shape, Vec2};
use std::f64::consts::{FRAC_PI_2, PI};
/// Default threshold for comparing floating point values in intersection and centroid math.
const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
/// 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.
fn split_bezpath_at_segment(bezpath: &BezPath, segment_index: usize, t: f64) -> Option<(BezPath, BezPath)> {
@@ -79,11 +77,7 @@ pub fn tangent_on_bezpath(bezpath: &BezPath, t_value: TValue, segments_length: O
let (segment_index, t) = eval_bezpath(bezpath, t_value, segments_length);
let segment = bezpath.get_seg(segment_index + 1).unwrap();
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_to_point(pathseg_tangent(segment, t))
}
/// Computes sample locations along a bezpath, returning parametric `(segment_index, t)` pairs and whether the path was closed.
@@ -254,7 +248,7 @@ pub(crate) fn pathseg_length_centroid_and_length(segment: PathSeg, accuracy: Opt
let QuadBez { p0, p1, p2 } = quad_bez;
// Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles
fn recurse(a0: Vec2, a1: Vec2, a2: Vec2, accuracy: f64, level: u8) -> (f64, Vec2) {
let lower = (a2 - a1).length();
let lower = (a2 - a0).length();
let upper = (a1 - a0).length() + (a2 - a1).length();
if upper - lower <= 2. * accuracy || level >= 8 {
let length = (lower + upper) / 2.;

View File

@@ -0,0 +1,8 @@
/// Minimum allowable separation between adjacent `t` values when calculating curve intersections
pub(crate) const MIN_SEPARATION_VALUE: f64 = 5. * 1e-3;
/// Threshold for comparing floating point values in intersection and centroid math.
pub(crate) const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
/// Maximum distance at which two points are treated as one and the same point.
pub(crate) const MAX_COINCIDENT_POINT_DISTANCE: f64 = 1e-7;

View File

@@ -1,6 +0,0 @@
/// Minimum allowable separation between adjacent `t` values when calculating curve intersections
pub(crate) const MIN_SEPARATION_VALUE: f64 = 5. * 1e-3;
/// Constant used to determine if `f64`s are equivalent.
#[cfg(test)]
pub(crate) const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;

View File

@@ -1,4 +1,4 @@
use super::contants::MIN_SEPARATION_VALUE;
use super::consts::MIN_SEPARATION_VALUE;
use kurbo::{BezPath, DEFAULT_ACCURACY, ParamCurve, PathSeg, Shape};
use lyon_geom::{CubicBezierSegment, Point};
@@ -260,7 +260,7 @@ pub(crate) fn pathseg_self_intersections(segment: PathSeg, accuracy: Option<f64>
mod tests {
use super::{bezpath_and_segment_intersections, filtered_segment_intersections};
use crate::vector::algorithms::{
contants::MAX_ABSOLUTE_DIFFERENCE,
consts::MAX_ABSOLUTE_DIFFERENCE,
util::{compare_points, compare_vec_of_points, dvec2_compare},
};

View File

@@ -1,8 +1,8 @@
pub mod bezpath_algorithms;
mod contants;
pub(crate) mod consts;
pub mod intersection;
pub mod merge_by_distance;
pub mod offset_subpath;
pub mod offset_bezpath;
mod poisson_disk;
pub mod shapes;
pub mod spline;

View File

@@ -1,13 +1,12 @@
use super::bezpath_algorithms::{clip_simple_bezpaths, miter_line_join, round_line_join};
use super::consts::MAX_COINCIDENT_POINT_DISTANCE;
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;
/// Constant used to determine if `f64`s are equivalent.
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-7;
/// Squared version to avoid sqrt in distance checks.
const MAX_ABSOLUTE_DIFFERENCE_SQUARED: f64 = MAX_ABSOLUTE_DIFFERENCE * MAX_ABSOLUTE_DIFFERENCE;
const MAX_COINCIDENT_POINT_DISTANCE_SQUARED: f64 = MAX_COINCIDENT_POINT_DISTANCE * MAX_COINCIDENT_POINT_DISTANCE;
const MAX_FITTED_SEGMENTS: usize = 10000;
/// Reduces the segments of the bezpath into simple subcurves, then offset each subcurve a set `distance` away.
@@ -26,9 +25,9 @@ pub fn offset_bezpath(bezpath: &BezPath, distance: f64, join: Join, miter_limit:
// Skip degenerate curves where all control points are at the same location.
// Offsetting a point is undefined and causes infinite recursion in fit_to_bezpath.
let start = cubic_bez.p0;
let is_degenerate = start.distance_squared(cubic_bez.p1) < MAX_ABSOLUTE_DIFFERENCE_SQUARED
&& start.distance_squared(cubic_bez.p2) < MAX_ABSOLUTE_DIFFERENCE_SQUARED
&& start.distance_squared(cubic_bez.p3) < MAX_ABSOLUTE_DIFFERENCE_SQUARED;
let is_degenerate = start.distance_squared(cubic_bez.p1) < MAX_COINCIDENT_POINT_DISTANCE_SQUARED
&& start.distance_squared(cubic_bez.p2) < MAX_COINCIDENT_POINT_DISTANCE_SQUARED
&& start.distance_squared(cubic_bez.p3) < MAX_COINCIDENT_POINT_DISTANCE_SQUARED;
if is_degenerate {
return None;
@@ -59,7 +58,7 @@ pub fn offset_bezpath(bezpath: &BezPath, distance: f64, join: Join, miter_limit:
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_COINCIDENT_POINT_DISTANCE) {
continue;
}

View File

@@ -2,7 +2,7 @@
//!
//! Anchor order and winding direction are load-bearing, since fills rely on every generator agreeing.
use crate::vector::misc::{ArcType, SpiralType, dvec2_to_point};
use crate::vector::misc::{ArcType, SpiralType, bezpath_from_anchors_and_handles};
use glam::DVec2;
use kurbo::BezPath;
use std::f64::consts::TAU;
@@ -28,31 +28,8 @@ impl Anchor {
}
}
/// Stitches anchors into a path, emitting a cubic when both facing handles exist, a quadratic when only one does, and a line otherwise.
fn bezpath_from_anchors(anchors: &[Anchor], closed: bool) -> BezPath {
let mut bezpath = BezPath::new();
let Some(first) = anchors.first() else { return bezpath };
bezpath.move_to(dvec2_to_point(first.position));
let mut out_handle = first.out_handle;
let connect_to = |bezpath: &mut BezPath, out_handle: Option<DVec2>, anchor: &Anchor| match (out_handle, anchor.in_handle) {
(Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(anchor.position)),
(None, None) => bezpath.line_to(dvec2_to_point(anchor.position)),
(None, Some(handle)) | (Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(anchor.position)),
};
for anchor in anchors.iter().skip(1) {
connect_to(&mut bezpath, out_handle, anchor);
out_handle = anchor.out_handle;
}
if closed {
connect_to(&mut bezpath, out_handle, first);
bezpath.close_path();
}
bezpath
bezpath_from_anchors_and_handles(anchors.iter().map(|anchor| (anchor.position, anchor.in_handle, anchor.out_handle)), closed)
}
/// Stitches a sequence of sharp (handleless) anchors into a polyline, or a closed polygon.

View File

@@ -18,7 +18,7 @@ pub fn pathseg_tangent(segment: PathSeg, t: f64) -> DVec2 {
#[cfg(test)]
pub(crate) 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)
p1.abs_diff_eq(p2, super::consts::MAX_ABSOLUTE_DIFFERENCE)
}
/// Compare vectors of points by allowing some maximum absolute difference to account for floating point errors

View File

@@ -1,5 +1,5 @@
use super::PointId;
use super::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
use super::algorithms::consts::MAX_COINCIDENT_POINT_DISTANCE;
use crate::vector::{SegmentId, Vector};
use core_types::list::{Item, List};
use dyn_any::DynAny;
@@ -227,36 +227,42 @@ pub fn handles_to_segment(start: DVec2, handles: BezierHandles, end: DVec2) -> P
}
}
pub fn bezpath_from_manipulator_groups(manipulator_groups: &[ManipulatorGroup], closed: bool) -> BezPath {
let mut bezpath = kurbo::BezPath::new();
let mut out_handle;
/// Stitches anchors into a path, emitting a cubic when both facing handles exist, a quadratic when only one does, and a line otherwise.
///
/// Each item is an anchor position paired with its incoming and outgoing handle positions, in absolute coordinates.
pub fn bezpath_from_anchors_and_handles(anchors: impl IntoIterator<Item = (DVec2, Option<DVec2>, Option<DVec2>)>, closed: bool) -> BezPath {
let mut bezpath = BezPath::new();
let mut anchors = anchors.into_iter();
let Some(first) = manipulator_groups.first() else { return bezpath };
bezpath.move_to(dvec2_to_point(first.anchor));
out_handle = first.out_handle;
let Some((first_anchor, first_in_handle, first_out_handle)) = anchors.next() else {
return bezpath;
};
bezpath.move_to(dvec2_to_point(first_anchor));
let mut out_handle = first_out_handle;
for manipulator in manipulator_groups.iter().skip(1) {
match (out_handle, manipulator.in_handle) {
(Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(manipulator.anchor)),
(None, None) => bezpath.line_to(dvec2_to_point(manipulator.anchor)),
(None, Some(handle)) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(manipulator.anchor)),
(Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(manipulator.anchor)),
}
out_handle = manipulator.out_handle;
let connect_to = |bezpath: &mut BezPath, out_handle: Option<DVec2>, anchor: DVec2, in_handle: Option<DVec2>| match (out_handle, in_handle) {
(Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(anchor)),
(None, None) => bezpath.line_to(dvec2_to_point(anchor)),
(None, Some(handle)) | (Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(anchor)),
};
for (anchor, in_handle, anchor_out_handle) in anchors {
connect_to(&mut bezpath, out_handle, anchor, in_handle);
out_handle = anchor_out_handle;
}
if closed {
match (out_handle, first.in_handle) {
(Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(first.anchor)),
(None, None) => bezpath.line_to(dvec2_to_point(first.anchor)),
(None, Some(handle)) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(first.anchor)),
(Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(first.anchor)),
}
connect_to(&mut bezpath, out_handle, first_anchor, first_in_handle);
bezpath.close_path();
}
bezpath
}
pub fn bezpath_from_manipulator_groups(manipulator_groups: &[ManipulatorGroup], closed: bool) -> BezPath {
bezpath_from_anchors_and_handles(manipulator_groups.iter().map(|group| (group.anchor, group.in_handle, group.out_handle)), closed)
}
pub fn bezpath_to_manipulator_groups(bezpath: &BezPath) -> (Vec<ManipulatorGroup>, bool) {
let mut manipulator_groups = Vec::<ManipulatorGroup>::new();
let mut is_closed = false;
@@ -293,7 +299,7 @@ pub fn bezpath_to_manipulator_groups(bezpath: &BezPath) -> (Vec<ManipulatorGroup
///
/// This is different from simply checking if the segment is [`PathSeg::Line`] or [`PathSeg::Quad`] or [`PathSeg::Cubic`]. Bezier curve can also be a line if the control points are colinear to the start and end points. Therefore if the handles exceed the start and end point, it will still be considered as a line.
pub fn is_linear(segment: PathSeg) -> bool {
let is_colinear = |a: Point, b: Point, c: Point| -> bool { ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)).abs() < MAX_ABSOLUTE_DIFFERENCE };
let is_colinear = |a: Point, b: Point, c: Point| -> bool { ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)).abs() < MAX_COINCIDENT_POINT_DISTANCE };
match segment {
PathSeg::Line(_) => true,

View File

@@ -1043,35 +1043,8 @@ impl Vector {
/// Construct a [`kurbo::BezPath`] curve for stroke.
pub fn stroke_bezpath_iter(&self) -> impl Iterator<Item = kurbo::BezPath> {
self.build_stroke_path_iter().map(|(manipulators_list, closed)| {
let mut bezpath = kurbo::BezPath::new();
let mut out_handle;
let Some(first) = manipulators_list.first() else { return bezpath };
bezpath.move_to(dvec2_to_point(first.anchor));
out_handle = first.out_handle;
for manipulator in manipulators_list.iter().skip(1) {
match (out_handle, manipulator.in_handle) {
(Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(manipulator.anchor)),
(None, None) => bezpath.line_to(dvec2_to_point(manipulator.anchor)),
(None, Some(handle)) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(manipulator.anchor)),
(Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(manipulator.anchor)),
}
out_handle = manipulator.out_handle;
}
if closed {
match (out_handle, first.in_handle) {
(Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(first.anchor)),
(None, None) => bezpath.line_to(dvec2_to_point(first.anchor)),
(None, Some(handle)) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(first.anchor)),
(Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(first.anchor)),
}
bezpath.close_path();
}
bezpath
})
self.build_stroke_path_iter()
.map(|(manipulators_list, closed)| crate::vector::misc::bezpath_from_manipulator_groups(&manipulators_list, closed))
}
pub fn transform(&mut self, transform: DAffine2) {

View File

@@ -27,7 +27,7 @@ use vector_types::vector::algorithms::bezpath_algorithms::{
self, TValue, bezpath_area_centroid_and_area, bezpath_length_centroid_and_length, eval_pathseg_euclidean, evaluate_bezpath, split_bezpath, tangent_on_bezpath,
};
use vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
use vector_types::vector::algorithms::offset_subpath::offset_bezpath;
use vector_types::vector::algorithms::offset_bezpath::offset_bezpath;
use vector_types::vector::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
use vector_types::vector::misc::{
BezierHandles, CentroidType, ExtrudeJoiningAlgorithm, HandleId, InterpolationDistribution, ManipulatorGroup, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns,