Replace the legacy Bezier struct with kurbo::PathSeg throughout (#4455)

This commit is contained in:
Keavon Chambers
2026-08-18 13:08:28 -07:00
committed by GitHub
parent 20af96c1d9
commit c111308c67
14 changed files with 127 additions and 324 deletions

View File

@@ -1,7 +1,6 @@
use crate::vector::algorithms::intersection::filtered_segment_intersections;
use crate::vector::misc::{dvec2_to_point, handles_to_segment};
use crate::vector::misc::dvec2_to_point;
use glam::{DAffine2, DVec2};
use kurbo::{CubicBez, Line, PathSeg, QuadBez, Shape};
use kurbo::{CubicBez, Line, PathSeg, QuadBez};
use std::fmt::{Debug, Formatter, Result};
use std::hash::Hash;
@@ -163,187 +162,3 @@ impl BezierHandles {
}
}
}
/// Representation of a bezier curve with 2D points.
#[derive(Copy, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Bezier {
/// Start point of the bezier curve.
pub start: DVec2,
/// End point of the bezier curve.
pub end: DVec2,
/// Handles of the bezier curve.
pub handles: BezierHandles,
}
impl Debug for Bezier {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let mut debug_struct = f.debug_struct("Bezier");
let mut debug_struct_ref = debug_struct.field("start", &self.start);
debug_struct_ref = match self.handles {
BezierHandles::Linear => debug_struct_ref,
BezierHandles::Quadratic { handle } => debug_struct_ref.field("handle", &handle),
BezierHandles::Cubic { handle_start, handle_end } => debug_struct_ref.field("handle_start", &handle_start).field("handle_end", &handle_end),
};
debug_struct_ref.field("end", &self.end).finish()
}
}
/// Functionality for the getters and setters of the various points in a Bezier
impl Bezier {
/// Set the coordinates of the start point.
pub fn set_start(&mut self, s: DVec2) {
self.start = s;
}
/// Set the coordinates of the end point.
pub fn set_end(&mut self, e: DVec2) {
self.end = e;
}
/// Set the coordinates of the first handle point. This represents the only handle in a quadratic segment. If used on a linear segment, it will be changed to a quadratic.
pub fn set_handle_start(&mut self, h1: DVec2) {
match self.handles {
BezierHandles::Linear => {
self.handles = BezierHandles::Quadratic { handle: h1 };
}
BezierHandles::Quadratic { ref mut handle } => {
*handle = h1;
}
BezierHandles::Cubic { ref mut handle_start, .. } => {
*handle_start = h1;
}
};
}
/// Set the coordinates of the second handle point. This will convert both linear and quadratic segments into cubic ones. For a linear segment, the first handle will be set to the start point.
pub fn set_handle_end(&mut self, h2: DVec2) {
match self.handles {
BezierHandles::Linear => {
self.handles = BezierHandles::Cubic {
handle_start: self.start,
handle_end: h2,
};
}
BezierHandles::Quadratic { handle } => {
self.handles = BezierHandles::Cubic { handle_start: handle, handle_end: h2 };
}
BezierHandles::Cubic { ref mut handle_end, .. } => {
*handle_end = h2;
}
};
}
/// Get the coordinates of the bezier segment's start point.
pub fn start(&self) -> DVec2 {
self.start
}
/// Get the coordinates of the bezier segment's end point.
pub fn end(&self) -> DVec2 {
self.end
}
/// Get the coordinates of the bezier segment's first handle point. This represents the only handle in a quadratic segment.
pub fn handle_start(&self) -> Option<DVec2> {
self.handles.start()
}
/// Get the coordinates of the second handle point. This will return `None` for a quadratic segment.
pub fn handle_end(&self) -> Option<DVec2> {
self.handles.end()
}
/// Get an iterator over the coordinates of all points in a vector.
/// - For a linear segment, the order of the points will be: `start`, `end`.
/// - For a quadratic segment, the order of the points will be: `start`, `handle`, `end`.
/// - For a cubic segment, the order of the points will be: `start`, `handle_start`, `handle_end`, `end`.
pub fn get_points(&self) -> impl Iterator<Item = DVec2> + use<> {
match self.handles {
BezierHandles::Linear => [self.start, self.end, DVec2::ZERO, DVec2::ZERO].into_iter().take(2),
BezierHandles::Quadratic { handle } => [self.start, handle, self.end, DVec2::ZERO].into_iter().take(3),
BezierHandles::Cubic { handle_start, handle_end } => [self.start, handle_start, handle_end, self.end].into_iter().take(4),
}
}
// TODO: Consider removing this function
/// Create a linear bezier using the provided coordinates as the start and end points.
pub fn from_linear_coordinates(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
Bezier {
start: DVec2::new(x1, y1),
handles: BezierHandles::Linear,
end: DVec2::new(x2, y2),
}
}
/// Create a linear bezier using the provided DVec2s as the start and end points.
pub fn from_linear_dvec2(p1: DVec2, p2: DVec2) -> Self {
Bezier {
start: p1,
handles: BezierHandles::Linear,
end: p2,
}
}
// TODO: Consider removing this function
/// Create a quadratic bezier using the provided coordinates as the start, handle, and end points.
pub fn from_quadratic_coordinates(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> Self {
Bezier {
start: DVec2::new(x1, y1),
handles: BezierHandles::Quadratic { handle: DVec2::new(x2, y2) },
end: DVec2::new(x3, y3),
}
}
/// Create a quadratic bezier using the provided DVec2s as the start, handle, and end points.
pub fn from_quadratic_dvec2(p1: DVec2, p2: DVec2, p3: DVec2) -> Self {
Bezier {
start: p1,
handles: BezierHandles::Quadratic { handle: p2 },
end: p3,
}
}
// TODO: Consider removing this function
/// Create a cubic bezier using the provided coordinates as the start, handles, and end points.
#[allow(clippy::too_many_arguments)]
pub fn from_cubic_coordinates(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) -> Self {
Bezier {
start: DVec2::new(x1, y1),
handles: BezierHandles::Cubic {
handle_start: DVec2::new(x2, y2),
handle_end: DVec2::new(x3, y3),
},
end: DVec2::new(x4, y4),
}
}
/// Create a cubic bezier using the provided DVec2s as the start, handles, and end points.
pub fn from_cubic_dvec2(p1: DVec2, p2: DVec2, p3: DVec2, p4: DVec2) -> Self {
Bezier {
start: p1,
handles: BezierHandles::Cubic { handle_start: p2, handle_end: p3 },
end: p4,
}
}
/// Returns a Bezier curve that results from applying the transformation function to each point in the Bezier.
pub fn apply_transformation(&self, transformation_function: impl Fn(DVec2) -> DVec2) -> Bezier {
Self {
start: transformation_function(self.start),
end: transformation_function(self.end),
handles: self.handles.apply_transformation(transformation_function),
}
}
pub fn intersections(&self, other: &Bezier, accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<f64> {
let this = handles_to_segment(self.start, self.handles, self.end);
let other = handles_to_segment(other.start, other.handles, other.end);
filtered_segment_intersections(this, other, accuracy, minimum_separation)
}
pub fn winding(&self, point: DVec2) -> i32 {
let this = handles_to_segment(self.start, self.handles, self.end);
this.winding(dvec2_to_point(point))
}
}

View File

@@ -449,8 +449,8 @@ impl ManipulatorPointId {
pub fn get_position(&self, vector: &Vector) -> Option<DVec2> {
match self {
ManipulatorPointId::Anchor(id) => vector.point_domain.position_from_id(*id),
ManipulatorPointId::PrimaryHandle(id) => vector.segment_from_id(*id).and_then(|bezier| bezier.handle_start()),
ManipulatorPointId::EndHandle(id) => vector.segment_from_id(*id).and_then(|bezier| bezier.handle_end()),
ManipulatorPointId::PrimaryHandle(id) => vector.segment_from_id(*id).and_then(|segment| segment_to_handles(&segment).start()),
ManipulatorPointId::EndHandle(id) => vector.segment_from_id(*id).and_then(|segment| segment_to_handles(&segment).end()),
}
}

View File

@@ -1,4 +1,4 @@
use crate::subpath::{Bezier, BezierHandles, Identifier, ManipulatorGroup, Subpath};
use crate::subpath::{BezierHandles, Identifier, ManipulatorGroup, Subpath};
use crate::vector::misc::{HandleId, Tangent, dvec2_to_point};
use crate::vector::vector_types::Vector;
use dyn_any::DynAny;
@@ -881,33 +881,26 @@ impl Vector {
}
}
/// Construct a [`Bezier`] curve spanning from the resolved position of the start and end points with the specified handles.
fn segment_to_bezier_with_index(&self, start: usize, end: usize, handles: BezierHandles) -> Bezier {
let start = self.point_domain.positions()[start];
let end = self.point_domain.positions()[end];
Bezier { start, end, handles }
/// Tries to convert a segment with the specified id to a [`PathSeg`], returning None if the id is invalid.
pub fn segment_from_id(&self, id: SegmentId) -> Option<PathSeg> {
self.segment_points_from_id(id).map(|(_, _, segment)| segment)
}
/// Tries to convert a segment with the specified id to a [`Bezier`], returning None if the id is invalid.
pub fn segment_from_id(&self, id: SegmentId) -> Option<Bezier> {
self.segment_points_from_id(id).map(|(_, _, bezier)| bezier)
}
/// Tries to convert a segment with the specified id to the start and end points and a [`Bezier`], returning None if the id is invalid.
pub fn segment_points_from_id(&self, id: SegmentId) -> Option<(PointId, PointId, Bezier)> {
/// Tries to convert a segment with the specified id to the start and end points and a [`PathSeg`], returning None if the id is invalid.
pub fn segment_points_from_id(&self, id: SegmentId) -> Option<(PointId, PointId, PathSeg)> {
Some(self.segment_points_from_index(self.segment_domain.id_to_index(id)?))
}
/// Tries to convert a segment with the specified index to the start and end points and a [`Bezier`].
pub fn segment_points_from_index(&self, index: usize) -> (PointId, PointId, Bezier) {
/// Converts a segment with the specified index to the start and end points and a [`PathSeg`].
pub fn segment_points_from_index(&self, index: usize) -> (PointId, PointId, PathSeg) {
let start = self.segment_domain.start_point[index];
let end = self.segment_domain.end_point[index];
let start_id = self.point_domain.ids()[start];
let end_id = self.point_domain.ids()[end];
(start_id, end_id, self.segment_to_bezier_with_index(start, end, self.segment_domain.handles[index]))
(start_id, end_id, self.path_segment_from_index(start, end, self.segment_domain.handles[index]))
}
/// Iterator over all of the [`Bezier`] following the order that they are stored in the segment domain, skipping invalid segments.
/// Iterator over all of the [`PathSeg`]s following the order that they are stored in the segment domain, skipping invalid segments.
pub fn segment_iter(&self) -> impl Iterator<Item = (SegmentId, PathSeg, PointId, PointId)> {
let to_segment = |(((&handles, &id), &start), &end)| (id, self.path_segment_from_index(start, end, handles), self.point_domain.ids()[start], self.point_domain.ids()[end]);
@@ -920,18 +913,6 @@ impl Vector {
.map(to_segment)
}
/// Iterator over all of the [`Bezier`] following the order that they are stored in the segment domain, skipping invalid segments.
pub fn segment_bezier_iter(&self) -> impl Iterator<Item = (SegmentId, Bezier, PointId, PointId)> + '_ {
let to_bezier = |(((&handles, &id), &start), &end)| (id, self.segment_to_bezier_with_index(start, end, handles), self.point_domain.ids()[start], self.point_domain.ids()[end]);
self.segment_domain
.handles
.iter()
.zip(&self.segment_domain.id)
.zip(self.segment_domain.start_point())
.zip(self.segment_domain.end_point())
.map(to_bezier)
}
pub fn auto_join_paths(&self) -> Vec<FoundSubpath> {
let segments = self.segment_domain.iter().map(|(id, start, end, _)| HalfEdge::new(id, start, end, false));
@@ -1003,7 +984,7 @@ impl Vector {
}
}
/// Construct a [`Bezier`] curve from an iterator of segments with (handles, start point, end point) independently of discontinuities.
/// Construct a [`Subpath`] from an iterator of segments with (handles, start point, end point) independently of discontinuities.
pub fn subpath_from_segments_ignore_discontinuities(&self, segments: impl Iterator<Item = (BezierHandles, usize, usize)>) -> Option<Subpath<PointId>> {
let mut first_point = None;
let mut manipulators_list = Vec::new();
@@ -1055,7 +1036,7 @@ impl Vector {
}
}
/// Construct a [`Bezier`] curve for stroke.
/// Construct a [`Subpath`] for each stroke path.
pub fn stroke_bezier_paths(&self) -> impl Iterator<Item = Subpath<PointId>> {
self.build_stroke_path_iter().map(|(manipulators_list, closed)| Subpath::new(manipulators_list, closed))
}

View File

@@ -1,10 +1,10 @@
use super::*;
use crate::subpath::BezierHandles;
use crate::vector::misc::{HandleId, HandleType, point_to_dvec2};
use crate::vector::misc::{HandleId, HandleType, point_to_dvec2, segment_to_handles};
use core_types::uuid::generate_uuid;
use dyn_any::DynAny;
use glam::DVec2;
use kurbo::{BezPath, PathEl, Point};
use kurbo::{BezPath, ParamCurve, PathEl, Point};
use serde::de::{SeqAccess, Visitor};
use serde::ser::SerializeSeq;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
@@ -225,8 +225,14 @@ impl SegmentModification {
remove: HashSet::new(),
start_point: vector.segment_domain.ids().iter().zip(vector.segment_domain.start_point()).map(point_id).collect(),
end_point: vector.segment_domain.ids().iter().zip(vector.segment_domain.end_point()).map(point_id).collect(),
handle_primary: vector.segment_bezier_iter().map(|(id, b, _, _)| (id, b.handle_start().map(|handle| handle - b.start))).collect(),
handle_end: vector.segment_bezier_iter().map(|(id, b, _, _)| (id, b.handle_end().map(|handle| handle - b.end))).collect(),
handle_primary: vector
.segment_iter()
.map(|(id, segment, _, _)| (id, segment_to_handles(&segment).start().map(|handle| handle - point_to_dvec2(segment.start()))))
.collect(),
handle_end: vector
.segment_iter()
.map(|(id, segment, _, _)| (id, segment_to_handles(&segment).end().map(|handle| handle - point_to_dvec2(segment.end()))))
.collect(),
stroke: vector.segment_domain.ids().iter().copied().zip(vector.segment_domain.stroke().iter().cloned()).collect(),
}
}
@@ -808,7 +814,8 @@ impl HandleExt for HandleId {
mod tests {
use super::*;
use crate::subpath::{Bezier, ManipulatorGroup, Subpath};
use crate::subpath::{ManipulatorGroup, Subpath};
use kurbo::{PathSeg, QuadBez};
#[test]
fn modify_new() {
@@ -856,12 +863,12 @@ mod tests {
assert_eq!(vector.point_domain.positions()[0], DVec2::X);
assert_eq!(vector.point_domain.positions()[9], DVec2::new(11., 0.));
assert_eq!(
vector.segment_bezier_iter().nth(8).unwrap().1,
Bezier::from_quadratic_dvec2(DVec2::new(0., 0.), DVec2::new(5., 10.), DVec2::new(11., 0.))
vector.segment_iter().nth(8).unwrap().1,
PathSeg::Quad(QuadBez::new(Point::new(0., 0.), Point::new(5., 10.), Point::new(11., 0.)))
);
assert_eq!(
vector.segment_bezier_iter().nth(9).unwrap().1,
Bezier::from_quadratic_dvec2(DVec2::new(11., 0.), DVec2::new(16., 10.), DVec2::new(20., 0.))
vector.segment_iter().nth(9).unwrap().1,
PathSeg::Quad(QuadBez::new(Point::new(11., 0.), Point::new(16., 10.), Point::new(20., 0.)))
);
}
}

View File

@@ -338,8 +338,8 @@ impl Vector {
/// Returns the number of linear segments connected to the given point.
pub fn connected_linear_segments(&self, point_id: PointId) -> usize {
self.segment_bezier_iter()
.filter(|(_, bez, start, end)| (*start == point_id || *end == point_id) && matches!(bez.handles, BezierHandles::Linear))
self.segment_iter()
.filter(|(_, segment, start, end)| (*start == point_id || *end == point_id) && matches!(segment, kurbo::PathSeg::Line(_)))
.count()
}

View File

@@ -399,6 +399,8 @@ fn grid<T: GridSpacing>(
#[cfg(test)]
mod tests {
use super::*;
use kurbo::ParamCurve;
use vector_types::vector::misc::point_to_dvec2;
fn item<T>(value: T) -> Item<T> {
Item::new_from_element(value)
@@ -413,14 +415,11 @@ mod tests {
// Works properly
let grid = grid((), (), item(GridType::Isometric), item(10.), item(5_u32), item(5_u32), item((30., 30.).into()), item(true));
assert_eq!(grid.element().point_domain.ids().len(), 5 * 5);
assert_eq!(grid.element().segment_bezier_iter().count(), 4 * 5 + 4 * 9);
for (_, bezier, _, _) in grid.element().segment_bezier_iter() {
assert_eq!(bezier.handles, subpath::BezierHandles::Linear);
assert!(
((bezier.start - bezier.end).length() - 10.).abs() < 1e-5,
"Length of {} should be 10",
(bezier.start - bezier.end).length()
);
assert_eq!(grid.element().segment_iter().count(), 4 * 5 + 4 * 9);
for (_, segment, _, _) in grid.element().segment_iter() {
assert!(matches!(segment, kurbo::PathSeg::Line(_)));
let span = point_to_dvec2(segment.start()) - point_to_dvec2(segment.end());
assert!((span.length() - 10.).abs() < 1e-5, "Length of {} should be 10", span.length());
}
}
@@ -428,10 +427,10 @@ mod tests {
fn skew_isometric_grid_test() {
let grid = grid((), (), item(GridType::Isometric), item(10.), item(5_u32), item(5_u32), item((40., 30.).into()), item(true));
assert_eq!(grid.element().point_domain.ids().len(), 5 * 5);
assert_eq!(grid.element().segment_bezier_iter().count(), 4 * 5 + 4 * 9);
for (_, bezier, _, _) in grid.element().segment_bezier_iter() {
assert_eq!(bezier.handles, subpath::BezierHandles::Linear);
let vector = bezier.start - bezier.end;
assert_eq!(grid.element().segment_iter().count(), 4 * 5 + 4 * 9);
for (_, segment, _, _) in grid.element().segment_iter() {
assert!(matches!(segment, kurbo::PathSeg::Line(_)));
let vector = point_to_dvec2(segment.start()) - point_to_dvec2(segment.end());
let angle = (vector.angle_to(DVec2::X).to_degrees() + 180.) % 180.;
assert!([90., 150., 40.].into_iter().any(|target| (target - angle).abs() < 1e-10), "unexpected angle of {angle}")
}

View File

@@ -696,16 +696,6 @@ pub mod extrude_algorithms {
use vector_types::vector::StrokeId;
use vector_types::vector::misc::ExtrudeJoiningAlgorithm;
/// Convert [`vector_types::subpath::Bezier`] to [`kurbo::PathSeg`].
fn bezier_to_path_seg(bezier: vector_types::subpath::Bezier) -> kurbo::PathSeg {
let [start, end] = [(bezier.start().x, bezier.start().y), (bezier.end().x, bezier.end().y)];
match bezier.handles {
BezierHandles::Linear => kurbo::Line::new(start, end).into(),
BezierHandles::Quadratic { handle } => kurbo::QuadBez::new(start, (handle.x, handle.y), end).into(),
BezierHandles::Cubic { handle_start, handle_end } => kurbo::CubicBez::new(start, (handle_start.x, handle_start.y), (handle_end.x, handle_end.y), end).into(),
}
}
/// Convert [`kurbo::CubicBez`] to [`vector_types::subpath::BezierHandles`].
fn cubic_to_handles(cubic_bez: kurbo::CubicBez) -> BezierHandles {
BezierHandles::Cubic {
@@ -736,9 +726,9 @@ pub mod extrude_algorithms {
let mut next_segment = vector.segment_domain.next_id();
for segment_index in 0..segment_count {
let (_, _, bezier) = vector.segment_points_from_index(segment_index);
let (_, _, segment) = vector.segment_points_from_index(segment_index);
let mut start_index = vector.segment_domain.start_point()[segment_index];
let pathseg = bezier_to_path_seg(bezier).to_cubic();
let pathseg = segment.to_cubic();
let mut start_t = 0.;
for split_t in find_splits(pathseg, direction) {