From 153c3a8558e514ac328ddcbfdd6f64474e5cd631 Mon Sep 17 00:00:00 2001 From: indierusty Date: Fri, 8 Aug 2025 13:16:51 +0530 Subject: [PATCH 01/10] define Subpath struct in gcore and refactor node-graph --- Cargo.lock | 7 +- node-graph/gcore/Cargo.toml | 1 - node-graph/gcore/src/lib.rs | 1 + node-graph/gcore/src/math/math_ext.rs | 10 +- node-graph/gcore/src/subpath/consts.rs | 4 + node-graph/gcore/src/subpath/core.rs | 358 ++++++++ node-graph/gcore/src/subpath/lookup.rs | 271 ++++++ node-graph/gcore/src/subpath/manipulators.rs | 52 ++ node-graph/gcore/src/subpath/mod.rs | 77 ++ node-graph/gcore/src/subpath/solvers.rs | 864 ++++++++++++++++++ node-graph/gcore/src/subpath/structs.rs | 444 +++++++++ node-graph/gcore/src/subpath/transform.rs | 642 +++++++++++++ node-graph/gcore/src/subpath/utils.rs | 429 +++++++++ node-graph/gcore/src/text/to_path.rs | 2 +- .../gcore/src/vector/algorithms/instance.rs | 2 +- node-graph/gcore/src/vector/click_target.rs | 21 +- .../gcore/src/vector/generator_nodes.rs | 32 +- node-graph/gcore/src/vector/misc.rs | 17 +- node-graph/gcore/src/vector/mod.rs | 1 - .../gcore/src/vector/vector_attributes.rs | 50 +- .../gcore/src/vector/vector_modification.rs | 16 +- node-graph/gcore/src/vector/vector_nodes.rs | 16 +- node-graph/gcore/src/vector/vector_types.rs | 44 +- node-graph/gpath-bool/Cargo.toml | 2 +- node-graph/gpath-bool/src/lib.rs | 22 +- node-graph/gsvg-renderer/Cargo.toml | 5 +- .../gsvg-renderer/src/convert_usvg_path.rs | 2 +- node-graph/gsvg-renderer/src/renderer.rs | 15 +- 28 files changed, 3285 insertions(+), 122 deletions(-) create mode 100644 node-graph/gcore/src/subpath/consts.rs create mode 100644 node-graph/gcore/src/subpath/core.rs create mode 100644 node-graph/gcore/src/subpath/lookup.rs create mode 100644 node-graph/gcore/src/subpath/manipulators.rs create mode 100644 node-graph/gcore/src/subpath/mod.rs create mode 100644 node-graph/gcore/src/subpath/solvers.rs create mode 100644 node-graph/gcore/src/subpath/structs.rs create mode 100644 node-graph/gcore/src/subpath/transform.rs create mode 100644 node-graph/gcore/src/subpath/utils.rs diff --git a/Cargo.lock b/Cargo.lock index b9a131eba5..9a919030dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1961,7 +1961,6 @@ name = "graphene-core" version = "0.1.0" dependencies = [ "base64 0.22.1", - "bezier-rs", "bytemuck", "ctor", "dyn-any", @@ -2018,10 +2017,10 @@ dependencies = [ name = "graphene-path-bool" version = "0.1.0" dependencies = [ - "bezier-rs", "dyn-any", "glam", "graphene-core", + "kurbo", "log", "node-macro", "path-bool", @@ -2089,10 +2088,10 @@ name = "graphene-svg-renderer" version = "0.1.0" dependencies = [ "base64 0.22.1", - "bezier-rs", "dyn-any", "glam", "graphene-core", + "kurbo", "log", "num-traits", "serde", @@ -2130,7 +2129,6 @@ dependencies = [ name = "graphite-editor" version = "0.0.0" dependencies = [ - "bezier-rs", "bitflags 2.9.1", "bytemuck", "derivative", @@ -2139,6 +2137,7 @@ dependencies = [ "futures", "glam", "graph-craft", + "graphene-core", "graphene-std", "graphite-proc-macros", "interpreted-executor", diff --git a/node-graph/gcore/Cargo.toml b/node-graph/gcore/Cargo.toml index 9f462187be..360b8b31ee 100644 --- a/node-graph/gcore/Cargo.toml +++ b/node-graph/gcore/Cargo.toml @@ -29,7 +29,6 @@ rustc-hash = { workspace = true } dyn-any = { workspace = true } ctor = { workspace = true } rand_chacha = { workspace = true } -bezier-rs = { workspace = true } specta = { workspace = true } image = { workspace = true } tinyvec = { workspace = true } diff --git a/node-graph/gcore/src/lib.rs b/node-graph/gcore/src/lib.rs index 6e2bc65530..47916cdcc8 100644 --- a/node-graph/gcore/src/lib.rs +++ b/node-graph/gcore/src/lib.rs @@ -22,6 +22,7 @@ pub mod raster_types; pub mod registry; pub mod render_complexity; pub mod structural; +pub mod subpath; pub mod table; pub mod text; pub mod transform; diff --git a/node-graph/gcore/src/math/math_ext.rs b/node-graph/gcore/src/math/math_ext.rs index bab9c19bf9..ba6d1e2d90 100644 --- a/node-graph/gcore/src/math/math_ext.rs +++ b/node-graph/gcore/src/math/math_ext.rs @@ -1,16 +1,24 @@ +use kurbo::{Line, PathSeg}; + use crate::math::quad::Quad; use crate::math::rect::Rect; -use bezier_rs::Bezier; +use crate::subpath::Bezier; +use crate::vector::misc::dvec2_to_point; pub trait QuadExt { /// Get all the edges in the rect as linear bezier curves fn bezier_lines(&self) -> impl Iterator + '_; + fn to_lines(&self) -> impl Iterator; } impl QuadExt for Quad { fn bezier_lines(&self) -> impl Iterator + '_ { self.all_edges().into_iter().map(|[start, end]| Bezier::from_linear_dvec2(start, end)) } + + fn to_lines(&self) -> impl Iterator { + self.all_edges().into_iter().map(|[start, end]| PathSeg::Line(Line::new(dvec2_to_point(start), dvec2_to_point(end)))) + } } pub trait RectExt { diff --git a/node-graph/gcore/src/subpath/consts.rs b/node-graph/gcore/src/subpath/consts.rs new file mode 100644 index 0000000000..300407f526 --- /dev/null +++ b/node-graph/gcore/src/subpath/consts.rs @@ -0,0 +1,4 @@ +// Implementation constants + +/// Constant used to determine if `f64`s are equivalent. +pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3; diff --git a/node-graph/gcore/src/subpath/core.rs b/node-graph/gcore/src/subpath/core.rs new file mode 100644 index 0000000000..c5606e5486 --- /dev/null +++ b/node-graph/gcore/src/subpath/core.rs @@ -0,0 +1,358 @@ +use crate::vector::misc::point_to_dvec2; + +use super::consts::*; +use super::*; +use glam::DVec2; +use kurbo::PathSeg; + +pub fn pathseg_points(segment: PathSeg) -> (DVec2, Option, Option, DVec2) { + match segment { + PathSeg::Line(line) => (point_to_dvec2(line.p0), None, None, point_to_dvec2(line.p1)), + PathSeg::Quad(quad) => (point_to_dvec2(quad.p0), None, Some(point_to_dvec2(quad.p1)), point_to_dvec2(quad.p1)), + PathSeg::Cubic(cube) => (point_to_dvec2(cube.p0), Some(point_to_dvec2(cube.p1)), Some(point_to_dvec2(cube.p2)), point_to_dvec2(cube.p1)), + } +} + +/// Functionality relating to core `Subpath` operations, such as constructors and `iter`. +impl Subpath { + /// Create a new `Subpath` using a list of [ManipulatorGroup]s. + /// A `Subpath` with less than 2 [ManipulatorGroup]s may not be closed. + #[track_caller] + pub fn new(manipulator_groups: Vec>, closed: bool) -> Self { + assert!(!closed || !manipulator_groups.is_empty(), "A closed Subpath must contain more than 0 ManipulatorGroups."); + Self { manipulator_groups, closed } + } + + /// Create a `Subpath` consisting of 2 manipulator groups from a `Bezier`. + pub fn from_bezier(segment: PathSeg) -> Self { + let (p1, h1, h2, p2) = pathseg_points(segment); + Subpath::new(vec![ManipulatorGroup::new(p1, None, h1), ManipulatorGroup::new(p2, h2, None)], false) + } + + /// 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: &[PathSeg], 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 beziers: Vec<_> = beziers.iter().map(|b| pathseg_points(*b)).collect(); + + let first = beziers.first().unwrap(); + let mut manipulator_groups = vec![ManipulatorGroup { + anchor: first.0, + in_handle: None, + out_handle: first.1, + id: PointId::new(), + }]; + let mut inner_groups: Vec> = beziers + .windows(2) + .map(|bezier_pair| ManipulatorGroup { + anchor: bezier_pair[1].0, + in_handle: bezier_pair[0].2, + out_handle: bezier_pair[1].1, + id: PointId::new(), + }) + .collect::>>(); + manipulator_groups.append(&mut inner_groups); + + let last = beziers.last().unwrap(); + if !closed { + manipulator_groups.push(ManipulatorGroup { + anchor: last.3, + in_handle: last.2, + out_handle: None, + id: PointId::new(), + }); + return Subpath::new(manipulator_groups, false); + } + + manipulator_groups[0].in_handle = last.2; + Subpath::new(manipulator_groups, true) + } + + /// Returns true if the `Subpath` contains no [ManipulatorGroup]. + pub fn is_empty(&self) -> bool { + self.manipulator_groups.is_empty() + } + + /// Returns the number of [ManipulatorGroup]s contained within the `Subpath`. + pub fn len(&self) -> usize { + self.manipulator_groups.len() + } + + /// Returns the number of segments contained within the `Subpath`. + pub fn len_segments(&self) -> usize { + let mut number_of_curves = self.len(); + if !self.closed && number_of_curves > 0 { + number_of_curves -= 1 + } + number_of_curves + } + + /// Returns a copy of the bezier segment at the given segment index, if this segment exists. + pub fn get_segment(&self, segment_index: usize) -> Option { + if segment_index >= self.len_segments() { + return None; + } + Some(self[segment_index].to_bezier(&self[(segment_index + 1) % self.len()])) + } + + /// Returns an iterator of the [Bezier]s along the `Subpath`. + pub fn iter(&self) -> SubpathIter<'_, PointId> { + SubpathIter { + subpath: self, + index: 0, + is_always_closed: false, + } + } + + /// Returns an iterator of the [Bezier]s along the `Subpath` always considering it as a closed subpath. + pub fn iter_closed(&self) -> SubpathIter<'_, PointId> { + SubpathIter { + subpath: self, + index: 0, + is_always_closed: true, + } + } + + /// Returns a slice of the [ManipulatorGroup]s in the `Subpath`. + pub fn manipulator_groups(&self) -> &[ManipulatorGroup] { + &self.manipulator_groups + } + + /// Returns a mutable reference to the [ManipulatorGroup]s in the `Subpath`. + pub fn manipulator_groups_mut(&mut self) -> &mut Vec> { + &mut self.manipulator_groups + } + + /// Returns a vector of all the anchors (DVec2) for this `Subpath`. + pub fn anchors(&self) -> Vec { + self.manipulator_groups().iter().map(|group| group.anchor).collect() + } + + /// Returns if the Subpath is equivalent to a single point. + pub fn is_point(&self) -> bool { + if self.is_empty() { + return false; + } + let point = self.manipulator_groups[0].anchor; + self.manipulator_groups + .iter() + .all(|manipulator_group| manipulator_group.anchor.abs_diff_eq(point, MAX_ABSOLUTE_DIFFERENCE)) + } + + /// Construct a [Subpath] from an iter of anchor positions. + pub fn from_anchors(anchor_positions: impl IntoIterator, closed: bool) -> Self { + Self::new(anchor_positions.into_iter().map(|anchor| ManipulatorGroup::new_anchor(anchor)).collect(), closed) + } + + pub fn from_anchors_linear(anchor_positions: impl IntoIterator, closed: bool) -> Self { + Self::new(anchor_positions.into_iter().map(|anchor| ManipulatorGroup::new_anchor_linear(anchor)).collect(), closed) + } + + /// Constructs a rectangle with `corner1` and `corner2` as the two corners. + pub fn new_rect(corner1: DVec2, corner2: DVec2) -> Self { + Self::from_anchors_linear([corner1, DVec2::new(corner2.x, corner1.y), corner2, DVec2::new(corner1.x, corner2.y)], true) + } + + /// Constructs a rounded rectangle with `corner1` and `corner2` as the two corners and `corner_radii` as the radii of the corners: `[top_left, top_right, bottom_right, bottom_left]`. + pub fn new_rounded_rect(corner1: DVec2, corner2: DVec2, corner_radii: [f64; 4]) -> Self { + if corner_radii.iter().all(|radii| radii.abs() < f64::EPSILON * 100.) { + return Self::new_rect(corner1, corner2); + } + + use std::f64::consts::{FRAC_1_SQRT_2, PI}; + + let new_arc = |center: DVec2, corner: DVec2, radius: f64| -> Vec> { + let point1 = center + DVec2::from_angle(-PI * 0.25).rotate(corner - center) * FRAC_1_SQRT_2; + let point2 = center + DVec2::from_angle(PI * 0.25).rotate(corner - center) * FRAC_1_SQRT_2; + if radius == 0. { + return vec![ManipulatorGroup::new_anchor(point1), ManipulatorGroup::new_anchor(point2)]; + } + + // Based on https://pomax.github.io/bezierinfo/#circles_cubic + const HANDLE_OFFSET_FACTOR: f64 = 0.551784777779014; + let handle_offset = radius * HANDLE_OFFSET_FACTOR; + vec![ + ManipulatorGroup::new(point1, None, Some(point1 + handle_offset * (corner - point1).normalize())), + ManipulatorGroup::new(point2, Some(point2 + handle_offset * (corner - point2).normalize()), None), + ] + }; + Self::new( + [ + new_arc(DVec2::new(corner1.x + corner_radii[0], corner1.y + corner_radii[0]), DVec2::new(corner1.x, corner1.y), corner_radii[0]), + new_arc(DVec2::new(corner2.x - corner_radii[1], corner1.y + corner_radii[1]), DVec2::new(corner2.x, corner1.y), corner_radii[1]), + new_arc(DVec2::new(corner2.x - corner_radii[2], corner2.y - corner_radii[2]), DVec2::new(corner2.x, corner2.y), corner_radii[2]), + new_arc(DVec2::new(corner1.x + corner_radii[3], corner2.y - corner_radii[3]), DVec2::new(corner1.x, corner2.y), corner_radii[3]), + ] + .concat(), + true, + ) + } + + /// Constructs an ellipse with `corner1` and `corner2` as the two corners of the bounding box. + pub fn new_ellipse(corner1: DVec2, corner2: DVec2) -> Self { + let size = (corner1 - corner2).abs(); + let center = (corner1 + corner2) / 2.; + let top = DVec2::new(center.x, corner1.y); + let bottom = DVec2::new(center.x, corner2.y); + let left = DVec2::new(corner1.x, center.y); + let right = DVec2::new(corner2.x, center.y); + + // Based on https://pomax.github.io/bezierinfo/#circles_cubic + const HANDLE_OFFSET_FACTOR: f64 = 0.551784777779014; + let handle_offset = size * HANDLE_OFFSET_FACTOR * 0.5; + + let manipulator_groups = vec![ + ManipulatorGroup::new(top, Some(top - handle_offset * DVec2::X), Some(top + handle_offset * DVec2::X)), + ManipulatorGroup::new(right, Some(right - handle_offset * DVec2::Y), Some(right + handle_offset * DVec2::Y)), + ManipulatorGroup::new(bottom, Some(bottom + handle_offset * DVec2::X), Some(bottom - handle_offset * DVec2::X)), + ManipulatorGroup::new(left, Some(left + handle_offset * DVec2::Y), Some(left - handle_offset * DVec2::Y)), + ]; + Self::new(manipulator_groups, true) + } + + /// Constructs an arc by a `radius`, `angle_start` and `angle_size`. Angles must be in radians. Slice option makes it look like pie or pacman. + pub fn new_arc(radius: f64, start_angle: f64, sweep_angle: f64, arc_type: ArcType) -> Self { + // Prevents glitches from numerical imprecision that have been observed during animation playback after about a minute + let start_angle = start_angle % (std::f64::consts::TAU * 2.); + let sweep_angle = sweep_angle % (std::f64::consts::TAU * 2.); + + let original_start_angle = start_angle; + let sweep_angle_sign = sweep_angle.signum(); + + let mut start_angle = 0.; + let mut sweep_angle = sweep_angle.abs(); + + if (sweep_angle / std::f64::consts::TAU).floor() as u32 % 2 == 0 { + sweep_angle %= std::f64::consts::TAU; + } else { + start_angle = sweep_angle % std::f64::consts::TAU; + sweep_angle = std::f64::consts::TAU - start_angle; + } + + sweep_angle *= sweep_angle_sign; + start_angle *= sweep_angle_sign; + start_angle += original_start_angle; + + let closed = arc_type == ArcType::Closed; + let slice = arc_type == ArcType::PieSlice; + + let center = DVec2::new(0., 0.); + let segments = (sweep_angle.abs() / (std::f64::consts::PI / 4.)).ceil().max(1.) as usize; + let step = sweep_angle / segments as f64; + let factor = 4. / 3. * (step / 2.).sin() / (1. + (step / 2.).cos()); + + let mut manipulator_groups = Vec::with_capacity(segments); + let mut prev_in_handle = None; + let mut prev_end = DVec2::new(0., 0.); + + for i in 0..segments { + let start_angle = start_angle + step * i as f64; + let end_angle = start_angle + step; + let start_vec = DVec2::from_angle(start_angle); + let end_vec = DVec2::from_angle(end_angle); + + let start = center + radius * start_vec; + let end = center + radius * end_vec; + + let handle_start = start + start_vec.perp() * radius * factor; + let handle_end = end - end_vec.perp() * radius * factor; + + manipulator_groups.push(ManipulatorGroup::new(start, prev_in_handle, Some(handle_start))); + prev_in_handle = Some(handle_end); + prev_end = end; + } + manipulator_groups.push(ManipulatorGroup::new(prev_end, prev_in_handle, None)); + + if slice { + manipulator_groups.push(ManipulatorGroup::new(center, None, None)); + } + + Self::new(manipulator_groups, closed || slice) + } + + /// Constructs a regular polygon (ngon). Based on `sides` and `radius`, which is the distance from the center to any vertex. + pub fn new_regular_polygon(center: DVec2, sides: u64, radius: f64) -> Self { + let sides = sides.max(3); + let angle_increment = std::f64::consts::TAU / (sides as f64); + let anchor_positions = (0..sides).map(|i| { + let angle = (i as f64) * angle_increment - std::f64::consts::FRAC_PI_2; + let center = center + DVec2::ONE * radius; + DVec2::new(center.x + radius * f64::cos(angle), center.y + radius * f64::sin(angle)) * 0.5 + }); + Self::from_anchors(anchor_positions, true) + } + + /// Constructs a star polygon (n-star). See [new_regular_polygon], but with interspersed vertices at an `inner_radius`. + pub fn new_star_polygon(center: DVec2, sides: u64, radius: f64, inner_radius: f64) -> Self { + let sides = sides.max(2); + let angle_increment = 0.5 * std::f64::consts::TAU / (sides as f64); + let anchor_positions = (0..sides * 2).map(|i| { + let angle = (i as f64) * angle_increment - std::f64::consts::FRAC_PI_2; + let center = center + DVec2::ONE * radius; + let r = if i % 2 == 0 { radius } else { inner_radius }; + DVec2::new(center.x + r * f64::cos(angle), center.y + r * f64::sin(angle)) * 0.5 + }); + Self::from_anchors(anchor_positions, true) + } + + /// Constructs a line from `p1` to `p2` + pub fn new_line(p1: DVec2, p2: DVec2) -> Self { + Self::from_anchors([p1, p2], false) + } + + #[cfg(feature = "kurbo")] + pub fn to_vello_path(&self, transform: glam::DAffine2, path: &mut kurbo::BezPath) { + use crate::BezierHandles; + + let to_point = |p: DVec2| { + let p = transform.transform_point2(p); + kurbo::Point::new(p.x, p.y) + }; + path.move_to(to_point(self.iter().next().unwrap().start)); + for segment in self.iter() { + match segment.handles { + BezierHandles::Linear => path.line_to(to_point(segment.end)), + BezierHandles::Quadratic { handle } => path.quad_to(to_point(handle), to_point(segment.end)), + BezierHandles::Cubic { handle_start, handle_end } => path.curve_to(to_point(handle_start), to_point(handle_end), to_point(segment.end)), + } + } + if self.closed { + path.close_path(); + } + } +} + +// #[cfg(test)] +// mod tests { +// use super::*; + +// #[test] +// fn closed_spline() { +// // These points are just chosen arbitrary +// let points = [DVec2::new(0., 0.), DVec2::new(0., 0.), DVec2::new(6., 5.), DVec2::new(7., 9.), DVec2::new(2., 3.)]; + +// let out_handles = solve_spline_first_handle_closed(&points); + +// // Construct the Subpath +// let mut manipulator_groups = Vec::new(); +// for i in 0..out_handles.len() { +// manipulator_groups.push(ManipulatorGroup::::new(points[i], Some(2. * points[i] - out_handles[i]), Some(out_handles[i]))); +// } +// let subpath = Subpath::new(manipulator_groups, true); + +// // For each pair of bézier curves, ensure that the second derivative is continuous +// for (bézier_a, bézier_b) in subpath.iter().zip(subpath.iter().skip(1).chain(subpath.iter().take(1))) { +// let derivative2_end_a = bézier_a.derivative().unwrap().derivative().unwrap().evaluate(super::utils::TValue::Parametric(1.)); +// let derivative2_start_b = bézier_b.derivative().unwrap().derivative().unwrap().evaluate(super::utils::TValue::Parametric(0.)); + +// assert!( +// derivative2_end_a.abs_diff_eq(derivative2_start_b, 1e-10), +// "second derivative at the end of a {derivative2_end_a} is equal to the second derivative at the start of b {derivative2_start_b}" +// ); +// } +// } +// } diff --git a/node-graph/gcore/src/subpath/lookup.rs b/node-graph/gcore/src/subpath/lookup.rs new file mode 100644 index 0000000000..b20a56e593 --- /dev/null +++ b/node-graph/gcore/src/subpath/lookup.rs @@ -0,0 +1,271 @@ +// // use super::consts::DEFAULT_LUT_STEP_SIZE; +// use super::utils::{SubpathTValue, TValueType}; +// use super::*; +// use glam::DVec2; + +// /// Functionality relating to looking up properties of the `Subpath` or points along the `Subpath`. +// impl Subpath { +// /// Return a selection of equidistant points on the bezier curve. +// /// If no value is provided for `steps`, then the function will default `steps` to be 10. +// /// +// pub fn compute_lookup_table(&self, steps: Option, tvalue_type: Option) -> Vec { +// let steps = steps.unwrap_or(DEFAULT_LUT_STEP_SIZE); +// let tvalue_type = tvalue_type.unwrap_or(TValueType::Parametric); + +// (0..=steps) +// .map(|t| { +// let tvalue = match tvalue_type { +// TValueType::Parametric => SubpathTValue::GlobalParametric(t as f64 / steps as f64), +// TValueType::Euclidean => SubpathTValue::GlobalEuclidean(t as f64 / steps as f64), +// }; +// self.evaluate(tvalue) +// }) +// .collect() +// } +// } + +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::consts::MAX_ABSOLUTE_DIFFERENCE; +// use crate::utils::f64_compare; + +// #[test] +// fn length_quadratic() { +// let start = DVec2::new(20., 30.); +// let middle = DVec2::new(80., 90.); +// let end = DVec2::new(60., 45.); +// let handle1 = DVec2::new(75., 85.); +// let handle2 = DVec2::new(40., 30.); +// let handle3 = DVec2::new(10., 10.); + +// let bezier1 = Bezier::from_quadratic_dvec2(start, handle1, middle); +// let bezier2 = Bezier::from_quadratic_dvec2(middle, handle2, end); +// let bezier3 = Bezier::from_quadratic_dvec2(end, handle3, start); + +// let mut subpath = Subpath::new( +// vec![ +// ManipulatorGroup { +// anchor: start, +// in_handle: None, +// out_handle: Some(handle1), +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: middle, +// in_handle: None, +// out_handle: Some(handle2), +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: end, +// in_handle: None, +// out_handle: Some(handle3), +// id: EmptyId, +// }, +// ], +// false, +// ); +// assert_eq!(subpath.length(None), bezier1.length(None) + bezier2.length(None)); + +// subpath.closed = true; +// assert_eq!(subpath.length(None), bezier1.length(None) + bezier2.length(None) + bezier3.length(None)); +// } + +// #[test] +// fn length_mixed() { +// let start = DVec2::new(20., 30.); +// let middle = DVec2::new(70., 70.); +// let end = DVec2::new(60., 45.); +// let handle1 = DVec2::new(75., 85.); +// let handle2 = DVec2::new(40., 30.); +// let handle3 = DVec2::new(10., 10.); + +// let linear_bezier = Bezier::from_linear_dvec2(start, middle); +// let quadratic_bezier = Bezier::from_quadratic_dvec2(middle, handle1, end); +// let cubic_bezier = Bezier::from_cubic_dvec2(end, handle2, handle3, start); + +// let mut subpath = Subpath::new( +// vec![ +// ManipulatorGroup { +// anchor: start, +// in_handle: Some(handle3), +// out_handle: None, +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: middle, +// in_handle: None, +// out_handle: Some(handle1), +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: end, +// in_handle: None, +// out_handle: Some(handle2), +// id: EmptyId, +// }, +// ], +// false, +// ); +// assert_eq!(subpath.length(None), linear_bezier.length(None) + quadratic_bezier.length(None)); + +// subpath.closed = true; +// assert_eq!(subpath.length(None), linear_bezier.length(None) + quadratic_bezier.length(None) + cubic_bezier.length(None)); +// } + +// #[test] +// fn length_centroid() { +// let start = DVec2::new(0., 0.); +// let end = DVec2::new(1., 1.); +// let handle = DVec2::new(0., 1.); + +// let mut subpath = Subpath::new( +// vec![ +// ManipulatorGroup { +// anchor: start, +// in_handle: None, +// out_handle: Some(handle), +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: end, +// in_handle: None, +// out_handle: None, +// id: EmptyId, +// }, +// ], +// false, +// ); + +// let expected_centroid = DVec2::new(0.4153039799983826, 0.5846960200016174); +// let epsilon = 0.00001; + +// assert!(subpath.length_centroid_and_length(None, true).unwrap().0.abs_diff_eq(expected_centroid, epsilon)); + +// subpath.closed = true; +// assert!(subpath.length_centroid_and_length(None, true).unwrap().0.abs_diff_eq(expected_centroid, epsilon)); +// } + +// #[test] +// fn area() { +// let start = DVec2::new(0., 0.); +// let end = DVec2::new(1., 1.); +// let handle = DVec2::new(0., 1.); + +// let mut subpath = Subpath::new( +// vec![ +// ManipulatorGroup { +// anchor: start, +// in_handle: None, +// out_handle: Some(handle), +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: end, +// in_handle: None, +// out_handle: None, +// id: EmptyId, +// }, +// ], +// false, +// ); + +// let expected_area = 1. / 3.; +// let epsilon = 0.00001; + +// assert!((subpath.area(Some(0.001), Some(0.001)) - expected_area).abs() < epsilon); + +// subpath.closed = true; +// assert!((subpath.area(Some(0.001), Some(0.001)) - expected_area).abs() < epsilon); +// } + +// #[test] +// fn area_centroid() { +// let start = DVec2::new(0., 0.); +// let end = DVec2::new(1., 1.); +// let handle = DVec2::new(0., 1.); + +// let mut subpath = Subpath::new( +// vec![ +// ManipulatorGroup { +// anchor: start, +// in_handle: None, +// out_handle: Some(handle), +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: end, +// in_handle: None, +// out_handle: None, +// id: EmptyId, +// }, +// ], +// false, +// ); + +// let expected_centroid = DVec2::new(0.4, 0.6); +// let epsilon = 0.00001; + +// assert!(subpath.area_centroid(Some(0.001), Some(0.001), None).unwrap().abs_diff_eq(expected_centroid, epsilon)); + +// subpath.closed = true; +// assert!(subpath.area_centroid(Some(0.001), Some(0.001), None).unwrap().abs_diff_eq(expected_centroid, epsilon)); +// } + +// #[test] +// fn t_value_to_parametric_global_parametric_open_subpath() { +// let mock_manipulator_group = ManipulatorGroup { +// anchor: DVec2::new(0., 0.), +// in_handle: None, +// out_handle: None, +// id: EmptyId, +// }; +// let open_subpath = Subpath { +// manipulator_groups: vec![mock_manipulator_group; 5], +// closed: false, +// }; + +// let (segment_index, t) = open_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.7)); +// assert_eq!(segment_index, 2); +// assert!(f64_compare(t, 0.8, MAX_ABSOLUTE_DIFFERENCE)); + +// // The start and end points of an open subpath are NOT equivalent +// assert_eq!(open_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.)), (0, 0.)); +// assert_eq!(open_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(1.)), (3, 1.)); +// } + +// #[test] +// fn t_value_to_parametric_global_parametric_closed_subpath() { +// let mock_manipulator_group = ManipulatorGroup { +// anchor: DVec2::new(0., 0.), +// in_handle: None, +// out_handle: None, +// id: EmptyId, +// }; +// let closed_subpath = Subpath { +// manipulator_groups: vec![mock_manipulator_group; 5], +// closed: true, +// }; + +// let (segment_index, t) = closed_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.7)); +// assert_eq!(segment_index, 3); +// assert!(f64_compare(t, 0.5, MAX_ABSOLUTE_DIFFERENCE)); + +// // The start and end points of a closed subpath are equivalent +// assert_eq!(closed_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.)), (0, 0.)); +// assert_eq!(closed_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(1.)), (4, 1.)); +// } + +// #[test] +// fn exact_start_end() { +// let start = DVec2::new(20., 30.); +// let end = DVec2::new(60., 45.); +// let handle = DVec2::new(75., 85.); + +// let subpath: Subpath = Subpath::from_bezier(&Bezier::from_quadratic_dvec2(start, handle, end)); + +// assert_eq!(subpath.evaluate(SubpathTValue::GlobalEuclidean(0.)), start); +// assert_eq!(subpath.evaluate(SubpathTValue::GlobalEuclidean(1.)), end); +// } +// } diff --git a/node-graph/gcore/src/subpath/manipulators.rs b/node-graph/gcore/src/subpath/manipulators.rs new file mode 100644 index 0000000000..6dad21d6d1 --- /dev/null +++ b/node-graph/gcore/src/subpath/manipulators.rs @@ -0,0 +1,52 @@ +// use super::consts::MAX_ABSOLUTE_DIFFERENCE; +// use super::utils::{SubpathTValue}; +use super::*; + +impl Subpath { + /// Get whether the subpath is closed. + pub fn closed(&self) -> bool { + self.closed + } + + /// Set whether the subpath is closed. + pub fn set_closed(&mut self, new_closed: bool) { + self.closed = new_closed; + } + + /// Access a [ManipulatorGroup] from a PointId. + pub fn manipulator_from_id(&self, id: PointId) -> Option<&ManipulatorGroup> { + self.manipulator_groups.iter().find(|manipulator_group| manipulator_group.id == id) + } + + /// Access a mutable [ManipulatorGroup] from a PointId. + pub fn manipulator_mut_from_id(&mut self, id: PointId) -> Option<&mut ManipulatorGroup> { + self.manipulator_groups.iter_mut().find(|manipulator_group| manipulator_group.id == id) + } + + /// Access the index of a [ManipulatorGroup] from a PointId. + pub fn manipulator_index_from_id(&self, id: PointId) -> Option { + self.manipulator_groups.iter().position(|manipulator_group| manipulator_group.id == id) + } + + /// Insert a manipulator group at an index. + pub fn insert_manipulator_group(&mut self, index: usize, group: ManipulatorGroup) { + assert!(group.is_finite(), "Inserting non finite manipulator group"); + self.manipulator_groups.insert(index, group) + } + + /// Push a manipulator group to the end. + pub fn push_manipulator_group(&mut self, group: ManipulatorGroup) { + assert!(group.is_finite(), "Pushing non finite manipulator group"); + self.manipulator_groups.push(group) + } + + /// Get a mutable reference to the last manipulator + pub fn last_manipulator_group_mut(&mut self) -> Option<&mut ManipulatorGroup> { + self.manipulator_groups.last_mut() + } + + /// Remove a manipulator group at an index. + pub fn remove_manipulator_group(&mut self, index: usize) -> ManipulatorGroup { + self.manipulator_groups.remove(index) + } +} diff --git a/node-graph/gcore/src/subpath/mod.rs b/node-graph/gcore/src/subpath/mod.rs new file mode 100644 index 0000000000..99c3cdb7f4 --- /dev/null +++ b/node-graph/gcore/src/subpath/mod.rs @@ -0,0 +1,77 @@ +mod consts; +mod core; +mod lookup; +mod manipulators; +mod solvers; +mod structs; +mod transform; +mod utils; + +pub use core::*; +use kurbo::PathSeg; +use std::fmt::{Debug, Formatter, Result}; +use std::ops::{Index, IndexMut}; +pub use structs::*; + +/// Structure used to represent a path composed of [Bezier] curves. +#[derive(Clone, PartialEq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Subpath { + manipulator_groups: Vec>, + pub closed: bool, +} + +#[cfg(feature = "dyn-any")] +unsafe impl dyn_any::StaticType for Subpath { + type Static = Subpath; +} + +/// Iteration structure for iterating across each curve of a `Subpath`, using an intermediate `Bezier` representation. +pub struct SubpathIter<'a, PointId: Identifier> { + index: usize, + subpath: &'a Subpath, + is_always_closed: bool, +} + +impl Index for Subpath { + type Output = ManipulatorGroup; + + fn index(&self, index: usize) -> &Self::Output { + assert!(index < self.len(), "Index out of bounds in trait Index of SubPath."); + &self.manipulator_groups[index] + } +} + +impl IndexMut for Subpath { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + assert!(index < self.len(), "Index out of bounds in trait IndexMut of SubPath."); + &mut self.manipulator_groups[index] + } +} + +impl Iterator for SubpathIter<'_, PointId> { + type Item = PathSeg; + + // Returns the Bezier representation of each `Subpath` segment, defined between a pair of adjacent manipulator points. + fn next(&mut self) -> Option { + if self.subpath.is_empty() { + return None; + } + let closed = if self.is_always_closed { true } else { self.subpath.closed }; + let len = self.subpath.len() - 1 + if closed { 1 } else { 0 }; + if self.index >= len { + return None; + } + let start_index = self.index; + let end_index = (self.index + 1) % self.subpath.len(); + self.index += 1; + + Some(self.subpath[start_index].to_bezier(&self.subpath[end_index])) + } +} + +impl Debug for Subpath { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + f.debug_struct("Subpath").field("closed", &self.closed).field("manipulator_groups", &self.manipulator_groups).finish() + } +} diff --git a/node-graph/gcore/src/subpath/solvers.rs b/node-graph/gcore/src/subpath/solvers.rs new file mode 100644 index 0000000000..4e4199cdcc --- /dev/null +++ b/node-graph/gcore/src/subpath/solvers.rs @@ -0,0 +1,864 @@ +use crate::vector::misc::dvec2_to_point; + +use super::utils::SubpathTValue; +use super::*; +use glam::{DMat2, DVec2}; +use kurbo::{Affine, BezPath, Shape}; + +impl Subpath { + /// Calculate the point on the subpath based on the parametric `t`-value provided. + /// Expects `t` to be within the inclusive range `[0, 1]`. + /// + pub fn evaluate(&self, t: SubpathTValue) -> DVec2 { + todo!(); + // let (segment_index, t) = self.t_value_to_parametric(t); + // self.get_segment(segment_index).unwrap().evaluate(TValue::Parametric(t)) + } + + pub fn contains_point(&self, point: DVec2) -> bool { + self.to_bezpath().contains(dvec2_to_point(point)) + } + + pub fn to_bezpath(&self) -> BezPath { + let mut bezpath = kurbo::BezPath::new(); + let mut out_handle; + + let Some(first) = self.manipulator_groups.first() else { return bezpath }; + bezpath.move_to(dvec2_to_point(first.anchor)); + out_handle = first.out_handle; + + for manipulator in self.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; + } + + if self.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 + } + + /// 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 occurred. + /// Expects the following: + /// - `other`: a [Bezier] curve to check intersections against + /// - `error`: an optional f64 value to provide an error bound + /// - `minimum_separation`: 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. + /// + /// + /// + /// + /// + pub fn intersections(&self, other: PathSeg, error: Option, minimum_separation: Option) -> Vec<(usize, f64)> { + todo!() + // self.iter() + // .enumerate() + // .flat_map(|(index, bezier)| bezier.intersections(other, error, minimum_separation).into_iter().map(|t| (index, t)).collect::>()) + // .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 + pub fn subpath_intersections(&self, other: &Subpath, error: Option, minimum_separation: Option) -> Vec<(usize, f64)> { + todo!(); + // 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 + } + + /// Returns how many times a given ray intersects with this subpath. (`ray_direction` does not need to be normalized.) + /// If this needs to be called frequently with a ray of the same rotation angle, consider instead using [`ray_test_crossings_count_prerotated`]. + pub fn ray_test_crossings_count(&self, ray_start: DVec2, ray_direction: DVec2) -> usize { + todo!() + // self.iter().map(|bezier| bezier.ray_test_crossings(ray_start, ray_direction).count()).sum() + } + + /// Returns how many times a given ray intersects with this subpath. (`ray_direction` does not need to be normalized.) + /// This version of the function is for better performance when calling it frequently without needing to change the rotation between each call. + /// If that isn't important, use [`ray_test_crossings_count`] which provides an easier interface by taking a ray direction vector. + /// Instead, this version requires a rotation matrix for the ray's rotation and a prerotated version of this subpath that has had its rotation applied. + pub fn ray_test_crossings_count_prerotated(&self, ray_start: DVec2, rotation_matrix: DMat2, rotated_subpath: &Self) -> usize { + todo!() + // self.iter() + // .zip(rotated_subpath.iter()) + // .map(|(bezier, rotated_bezier)| bezier.ray_test_crossings_prerotated(ray_start, rotation_matrix, rotated_bezier).count()) + // .sum() + } + + /// Returns true if the given point is inside this subpath. Open paths are NOT automatically closed so you'll need to call `set_closed(true)` before calling this. + /// Self-intersecting subpaths use the `evenodd` fill rule for checking in/outside-ness: . + /// If this needs to be called frequently, consider instead using [`point_inside_prerotated`] and moving this function's setup code into your own logic before the repeated call. + pub fn point_inside(&self, point: DVec2) -> bool { + // The directions use prime numbers to reduce the likelihood of running across two anchor points simultaneously + const SIN_13DEG: f64 = 0.22495105434; + const COS_13DEG: f64 = 0.97437006478; + const DIRECTION1: DVec2 = DVec2::new(SIN_13DEG, COS_13DEG); + const DIRECTION2: DVec2 = DVec2::new(-COS_13DEG, -SIN_13DEG); + + // We (inefficiently) check for odd crossings in two directions and make sure they agree to reduce how often anchor points cause a double-increment + let test1 = self.ray_test_crossings_count(point, DIRECTION1) % 2 == 1; + let test2 = self.ray_test_crossings_count(point, DIRECTION2) % 2 == 1; + + test1 && test2 + } + + /// Returns true if the given point is inside this subpath. Open paths are NOT automatically closed so you'll need to call `set_closed(true)` before calling this. + /// Self-intersecting subpaths use the `evenodd` fill rule for checking in/outside-ness: . + /// This version of the function is for better performance when calling it frequently because it lets the caller precompute the rotations once instead of every call. + /// If that isn't important, use [`point_inside`] which provides an easier interface. + /// Instead, this version requires a pair of rotation matrices for the ray's rotation and a pair of prerotated versions of this subpath. + /// They should face in different directions that are unlikely to align in the real world. Consider using the following rotations: + /// ```rs + /// const SIN_13DEG: f64 = 0.22495105434; + /// const COS_13DEG: f64 = 0.97437006478; + /// const DIRECTION1: DVec2 = DVec2::new(SIN_13DEG, COS_13DEG); + /// const DIRECTION2: DVec2 = DVec2::new(-COS_13DEG, -SIN_13DEG); + /// ``` + pub fn point_inside_prerotated(&self, point: DVec2, rotation_matrix1: DMat2, rotation_matrix2: DMat2, rotated_subpath1: &Self, rotated_subpath2: &Self) -> bool { + // We (inefficiently) check for odd crossings in two directions and make sure they agree to reduce how often anchor points cause a double-increment + let test1 = self.ray_test_crossings_count_prerotated(point, rotation_matrix1, rotated_subpath1) % 2 == 1; + let test2 = self.ray_test_crossings_count_prerotated(point, rotation_matrix2, rotated_subpath2) % 2 == 1; + + test1 && test2 + } + + /// Returns a list of `t` values that correspond to the self intersection points of the subpath. For each intersection point, the returned `t` value is the smaller of the two that correspond to the point. + /// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point. + /// - `minimum_separation`: 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 + /// + /// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out. + /// + pub fn self_intersections(&self, error: Option, minimum_separation: Option) -> Vec<(usize, f64)> { + // let mut intersections_vec = Vec::new(); + // let err = error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE); + // // TODO: optimization opportunity - this for-loop currently compares all intersections with all curve-segments in the subpath collection + // self.iter().enumerate().for_each(|(i, other)| { + // intersections_vec.extend(other.self_intersections(error, minimum_separation).iter().map(|value| (i, value[0]))); + // self.iter().enumerate().skip(i + 1).for_each(|(j, curve)| { + // intersections_vec.extend( + // curve + // .intersections(&other, error, minimum_separation) + // .iter() + // .filter(|&value| value > &err && (1. - value) > err) + // .map(|value| (j, *value)), + // ); + // }); + // }); + // intersections_vec + todo!() + } + + /// Returns a list of `t` values that correspond to all the self intersection points of the subpath always considering it as a closed subpath. The index and `t` value of both will be returned that corresponds to a point. + /// The points will be sorted based on their index and `t` repsectively. + /// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point. + /// - `minimum_separation`: 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 + /// + /// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out. + pub fn all_self_intersections(&self, error: Option, minimum_separation: Option) -> Vec<(usize, f64)> { + // let mut intersections_vec = Vec::new(); + // let err = error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE); + // let num_curves = self.len(); + // // TODO: optimization opportunity - this for-loop currently compares all intersections with all curve-segments in the subpath collection + // self.iter_closed().enumerate().for_each(|(i, other)| { + // intersections_vec.extend(other.self_intersections(error, minimum_separation).iter().flat_map(|value| [(i, value[0]), (i, value[1])])); + // self.iter_closed().enumerate().skip(i + 1).for_each(|(j, curve)| { + // intersections_vec.extend( + // curve + // .all_intersections(&other, error, minimum_separation) + // .iter() + // .filter(|&value| (j != i + 1 || value[0] > err || (1. - value[1]) > err) && (j != num_curves - 1 || i != 0 || value[1] > err || (1. - value[0]) > err)) + // .flat_map(|value| [(j, value[0]), (i, value[1])]), + // ); + // }); + // }); + + // intersections_vec.sort_by(|a, b| a.partial_cmp(b).unwrap()); + + // intersections_vec + todo!() + } + + /// Returns `true` if this subpath is completely inside the `other` subpath. + /// + pub fn is_inside_subpath(&self, other: &Subpath, error: Option, minimum_separation: Option) -> bool { + // Eliminate any possibility of one being inside the other, if either of them is empty + // if self.is_empty() || other.is_empty() { + // return false; + // } + + // // Safe to unwrap because the subpath is not empty + // let inner_bbox = self.bounding_box().unwrap(); + // let outer_bbox = other.bounding_box().unwrap(); + + // // Eliminate this subpath if its bounding box is not completely inside the other subpath's bounding box. + // // Reasoning: + // // If the (min x, min y) of the inner subpath is less than or equal to the (min x, min y) of the outer subpath, + // // or if the (min x, min y) of the inner subpath is greater than or equal to the (max x, max y) of the outer subpath, + // // then the inner subpath is intersecting with or outside the outer subpath. The same logic applies for (max x, max y). + // if !is_rectangle_inside_other(inner_bbox, outer_bbox) { + // return false; + // } + + // // Eliminate this subpath if any of its anchors are outside the other subpath. + // for anchors in self.anchors() { + // if !other.contains_point(anchors) { + // return false; + // } + // } + + // // Eliminate this subpath if it intersects with the other subpath. + // if !self.subpath_intersections(other, error, minimum_separation).is_empty() { + // return false; + // } + + // // At this point: + // // (1) This subpath's bounding box is inside the other subpath's bounding box, + // // (2) Its anchors are inside the other subpath, and + // // (3) It is not intersecting with the other subpath. + // // Hence, this subpath is completely inside the given other subpath. + // true + todo!() + } + + /// Returns a normalized unit vector representing the tangent on the subpath based on the parametric `t`-value provided. + /// + pub fn tangent(&self, t: SubpathTValue) -> DVec2 { + todo!() + // let (segment_index, t) = self.t_value_to_parametric(t); + // self.get_segment(segment_index).unwrap().tangent(TValue::Parametric(t)) + } + + /// Returns a normalized unit vector representing the direction of the normal on the subpath based on the parametric `t`-value provided. + /// + pub fn normal(&self, t: SubpathTValue) -> DVec2 { + todo!() + // let (segment_index, t) = self.t_value_to_parametric(t); + // self.get_segment(segment_index).unwrap().normal(TValue::Parametric(t)) + } + + /// Returns two lists of `t`-values representing the local extrema of the `x` and `y` parametric subpaths respectively. + /// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`. + /// + pub fn local_extrema(&self) -> [Vec; 2] { + todo!() + // let number_of_curves = self.len_segments() as f64; + + // // TODO: Consider the shared point between adjacent beziers. + // self.iter().enumerate().fold([Vec::new(), Vec::new()], |mut acc, elem| { + // let [x, y] = elem.1.local_extrema(); + // // Convert t-values of bezier curve to t-values of subpath + // acc[0].extend(x.map(|t| ((elem.0 as f64) + t) / number_of_curves).collect::>()); + // acc[1].extend(y.map(|t| ((elem.0 as f64) + t) / number_of_curves).collect::>()); + // acc + // }) + } + + /// Return the min and max corners that represent the bounding box of the subpath. Return `None` if the subpath is empty. + /// + pub fn bounding_box(&self) -> Option<[DVec2; 2]> { + self.iter() + .map(|bezier| bezier.bounding_box()) + .map(|bbox| [DVec2::new(bbox.min_x(), bbox.min_y()), DVec2::new(bbox.max_x(), bbox.max_y())]) + .reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])]) + } + + /// Return the min and max corners that represent the bounding box of the subpath, after a given affine transform. + pub fn bounding_box_with_transform(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> { + self.iter() + .map(|bezier| (Affine::new(transform.to_cols_array()) * bezier).bounding_box()) + .map(|bbox| [DVec2::new(bbox.min_x(), bbox.min_y()), DVec2::new(bbox.max_x(), bbox.max_y())]) + .reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])]) + } + + /// Return the min and max corners that represent the loose bounding box of the subpath (bounding box of all handles and anchors). + pub fn loose_bounding_box(&self) -> Option<[DVec2; 2]> { + self.manipulator_groups + .iter() + .flat_map(|group| [group.in_handle, group.out_handle, Some(group.anchor)]) + .flatten() + .map(|pos| [pos, pos]) + .reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])]) + } + + /// Return the min and max corners that represent the loose bounding box of the subpath, after a given affine transform. + pub fn loose_bounding_box_with_transform(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> { + self.manipulator_groups + .iter() + .flat_map(|group| [group.in_handle, group.out_handle, Some(group.anchor)]) + .flatten() + .map(|pos| transform.transform_point2(pos)) + .map(|pos| [pos, pos]) + .reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])]) + } + + /// Returns list of `t`-values representing the inflection points of the subpath. + /// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`. + /// + pub fn inflections(&self) -> Vec { + let number_of_curves = self.len_segments() as f64; + let inflection_t_values: Vec = self + .iter() + .enumerate() + .flat_map(|(index, bezier)| { + bezier.to_cubic().inflections() + .into_iter() + // Convert t-values of bezier curve to t-values of subpath + .map(move |t| ((index as f64) + t) / number_of_curves) + }) + .collect(); + + // TODO: Consider the shared point between adjacent beziers. + inflection_t_values + } + + /// Returns the curvature, a scalar value for the derivative at the point `t` along the subpath. + /// Curvature is 1 over the radius of a circle with an equivalent derivative. + /// + pub fn curvature(&self, t: SubpathTValue) -> f64 { + // let (segment_index, t) = self.t_value_to_parametric(t); + // self.get_segment(segment_index).unwrap().curvature(TValue::Parametric(t)) + todo!() + } + pub fn area_centroid_and_area(&self, _: Option, _: Option) -> Option<(DVec2, f64)> { + todo!(); + } + + pub fn length_centroid_and_length(&self, _: Option, _: bool) -> Option<(DVec2, f64)> { + todo!() + } +} + +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::Bezier; +// use crate::consts::MAX_ABSOLUTE_DIFFERENCE; +// use crate::utils; +// use glam::DVec2; + +// fn normalize_t(n: i64, t: f64) -> f64 { +// t * (n as f64) % 1. +// } + +// #[test] +// fn evaluate_one_subpath_curve() { +// let start = DVec2::new(20., 30.); +// let end = DVec2::new(60., 45.); +// let handle = DVec2::new(75., 85.); + +// let bezier = Bezier::from_quadratic_dvec2(start, handle, end); +// let subpath = Subpath::new( +// vec![ +// ManipulatorGroup { +// anchor: start, +// in_handle: None, +// out_handle: Some(handle), +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: end, +// in_handle: None, +// out_handle: Some(handle), +// id: EmptyId, +// }, +// ], +// false, +// ); + +// let t0 = 0.; +// assert_eq!(subpath.evaluate(SubpathTValue::GlobalParametric(t0)), bezier.evaluate(TValue::Parametric(t0))); + +// let t1 = 0.25; +// assert_eq!(subpath.evaluate(SubpathTValue::GlobalParametric(t1)), bezier.evaluate(TValue::Parametric(t1))); + +// let t2 = 0.50; +// assert_eq!(subpath.evaluate(SubpathTValue::GlobalParametric(t2)), bezier.evaluate(TValue::Parametric(t2))); + +// let t3 = 1.; +// assert_eq!(subpath.evaluate(SubpathTValue::GlobalParametric(t3)), bezier.evaluate(TValue::Parametric(t3))); +// } + +// #[test] +// fn evaluate_multiple_subpath_curves() { +// let start = DVec2::new(20., 30.); +// let middle = DVec2::new(70., 70.); +// let end = DVec2::new(60., 45.); +// let handle1 = DVec2::new(75., 85.); +// let handle2 = DVec2::new(40., 30.); +// let handle3 = DVec2::new(10., 10.); + +// let linear_bezier = Bezier::from_linear_dvec2(start, middle); +// let quadratic_bezier = Bezier::from_quadratic_dvec2(middle, handle1, end); +// let cubic_bezier = Bezier::from_cubic_dvec2(end, handle2, handle3, start); + +// let mut subpath = Subpath::new( +// vec![ +// ManipulatorGroup { +// anchor: start, +// in_handle: Some(handle3), +// out_handle: None, +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: middle, +// in_handle: None, +// out_handle: Some(handle1), +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: end, +// in_handle: None, +// out_handle: Some(handle2), +// id: EmptyId, +// }, +// ], +// false, +// ); + +// // Test open subpath + +// let mut n = (subpath.len() as i64) - 1; + +// let t0 = 0.; +// assert!( +// utils::dvec2_compare( +// subpath.evaluate(SubpathTValue::GlobalParametric(t0)), +// linear_bezier.evaluate(TValue::Parametric(normalize_t(n, t0))), +// MAX_ABSOLUTE_DIFFERENCE +// ) +// .all() +// ); + +// let t1 = 0.25; +// assert!( +// utils::dvec2_compare( +// subpath.evaluate(SubpathTValue::GlobalParametric(t1)), +// linear_bezier.evaluate(TValue::Parametric(normalize_t(n, t1))), +// MAX_ABSOLUTE_DIFFERENCE +// ) +// .all() +// ); + +// let t2 = 0.50; +// assert!( +// utils::dvec2_compare( +// subpath.evaluate(SubpathTValue::GlobalParametric(t2)), +// quadratic_bezier.evaluate(TValue::Parametric(normalize_t(n, t2))), +// MAX_ABSOLUTE_DIFFERENCE +// ) +// .all() +// ); + +// let t3 = 0.75; +// assert!( +// utils::dvec2_compare( +// subpath.evaluate(SubpathTValue::GlobalParametric(t3)), +// quadratic_bezier.evaluate(TValue::Parametric(normalize_t(n, t3))), +// MAX_ABSOLUTE_DIFFERENCE +// ) +// .all() +// ); + +// let t4 = 1.; +// assert!( +// utils::dvec2_compare( +// subpath.evaluate(SubpathTValue::GlobalParametric(t4)), +// quadratic_bezier.evaluate(TValue::Parametric(1.)), +// MAX_ABSOLUTE_DIFFERENCE +// ) +// .all() +// ); + +// // Test closed subpath + +// subpath.closed = true; +// n = subpath.len() as i64; + +// let t5 = 2. / 3.; +// assert!( +// utils::dvec2_compare( +// subpath.evaluate(SubpathTValue::GlobalParametric(t5)), +// cubic_bezier.evaluate(TValue::Parametric(normalize_t(n, t5))), +// MAX_ABSOLUTE_DIFFERENCE +// ) +// .all() +// ); + +// let t6 = 1.; +// assert!( +// utils::dvec2_compare( +// subpath.evaluate(SubpathTValue::GlobalParametric(t6)), +// cubic_bezier.evaluate(TValue::Parametric(1.)), +// MAX_ABSOLUTE_DIFFERENCE +// ) +// .all() +// ); +// } + +// #[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 = DVec2::new(35., 125.); +// let cubic_handle_1 = DVec2::new(40., 40.); +// let cubic_handle_2 = DVec2::new(120., 120.); +// let cubic_end = DVec2::new(43., 43.); + +// let quadratic_1_handle = DVec2::new(175., 90.); +// let quadratic_end = DVec2::new(145., 150.); + +// let quadratic_2_handle = DVec2::new(70., 185.); + +// let cubic_bezier = Bezier::from_cubic_dvec2(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end); +// let quadratic_bezier_1 = Bezier::from_quadratic_dvec2(cubic_end, quadratic_1_handle, quadratic_end); + +// let subpath = Subpath::new( +// vec![ +// ManipulatorGroup { +// anchor: cubic_start, +// in_handle: None, +// out_handle: Some(cubic_handle_1), +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: cubic_end, +// in_handle: Some(cubic_handle_2), +// out_handle: None, +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: quadratic_end, +// in_handle: Some(quadratic_1_handle), +// out_handle: Some(quadratic_2_handle), +// id: EmptyId, +// }, +// ], +// true, +// ); + +// let line = Bezier::from_linear_coordinates(150., 150., 20., 20.); + +// let cubic_intersections = cubic_bezier.intersections(&line, None, None); +// let quadratic_1_intersections = quadratic_bezier_1.intersections(&line, None, None); +// let subpath_intersections = subpath.intersections(&line, None, None); + +// assert!( +// utils::dvec2_compare( +// cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])), +// subpath.evaluate(SubpathTValue::Parametric { +// segment_index: subpath_intersections[0].0, +// t: subpath_intersections[0].1 +// }), +// MAX_ABSOLUTE_DIFFERENCE +// ) +// .all() +// ); + +// assert!( +// utils::dvec2_compare( +// quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])), +// subpath.evaluate(SubpathTValue::Parametric { +// segment_index: subpath_intersections[1].0, +// t: subpath_intersections[1].1 +// }), +// MAX_ABSOLUTE_DIFFERENCE +// ) +// .all() +// ); + +// assert!( +// utils::dvec2_compare( +// quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[1])), +// subpath.evaluate(SubpathTValue::Parametric { +// segment_index: subpath_intersections[2].0, +// t: subpath_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 = DVec2::new(34., 107.); +// let cubic_handle_1 = DVec2::new(40., 40.); +// let cubic_handle_2 = DVec2::new(120., 120.); +// let cubic_end = DVec2::new(102., 29.); + +// let quadratic_1_handle = DVec2::new(175., 90.); +// let quadratic_end = DVec2::new(129., 171.); + +// let quadratic_2_handle = DVec2::new(70., 185.); + +// let cubic_bezier = Bezier::from_cubic_dvec2(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end); +// let quadratic_bezier_1 = Bezier::from_quadratic_dvec2(cubic_end, quadratic_1_handle, quadratic_end); + +// let subpath = Subpath::new( +// vec![ +// ManipulatorGroup { +// anchor: cubic_start, +// in_handle: None, +// out_handle: Some(cubic_handle_1), +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: cubic_end, +// in_handle: Some(cubic_handle_2), +// out_handle: None, +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: quadratic_end, +// in_handle: Some(quadratic_1_handle), +// out_handle: Some(quadratic_2_handle), +// id: EmptyId, +// }, +// ], +// true, +// ); + +// let line = Bezier::from_linear_coordinates(150., 150., 20., 20.); + +// let cubic_intersections = cubic_bezier.intersections(&line, None, None); +// let quadratic_1_intersections = quadratic_bezier_1.intersections(&line, None, None); +// let subpath_intersections = subpath.intersections(&line, None, None); + +// assert!( +// utils::dvec2_compare( +// cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])), +// subpath.evaluate(SubpathTValue::Parametric { +// segment_index: subpath_intersections[0].0, +// t: subpath_intersections[0].1 +// }), +// MAX_ABSOLUTE_DIFFERENCE +// ) +// .all() +// ); + +// assert!( +// utils::dvec2_compare( +// quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])), +// subpath.evaluate(SubpathTValue::Parametric { +// segment_index: subpath_intersections[1].0, +// t: subpath_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 = DVec2::new(35., 125.); +// let cubic_handle_1 = DVec2::new(40., 40.); +// let cubic_handle_2 = DVec2::new(120., 120.); +// let cubic_end = DVec2::new(44., 44.); + +// let quadratic_1_handle = DVec2::new(175., 90.); +// let quadratic_end = DVec2::new(145., 150.); + +// let quadratic_2_handle = DVec2::new(70., 185.); + +// let cubic_bezier = Bezier::from_cubic_dvec2(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end); +// let quadratic_bezier_1 = Bezier::from_quadratic_dvec2(cubic_end, quadratic_1_handle, quadratic_end); + +// let subpath = Subpath::new( +// vec![ +// ManipulatorGroup { +// anchor: cubic_start, +// in_handle: None, +// out_handle: Some(cubic_handle_1), +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: cubic_end, +// in_handle: Some(cubic_handle_2), +// out_handle: None, +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: quadratic_end, +// in_handle: Some(quadratic_1_handle), +// out_handle: Some(quadratic_2_handle), +// id: EmptyId, +// }, +// ], +// true, +// ); + +// let line = Bezier::from_linear_coordinates(150., 150., 20., 20.); + +// let cubic_intersections = cubic_bezier.intersections(&line, None, None); +// let quadratic_1_intersections = quadratic_bezier_1.intersections(&line, None, None); +// let subpath_intersections = subpath.intersections(&line, None, None); + +// assert!( +// utils::dvec2_compare( +// cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])), +// subpath.evaluate(SubpathTValue::Parametric { +// segment_index: subpath_intersections[0].0, +// t: subpath_intersections[0].1 +// }), +// MAX_ABSOLUTE_DIFFERENCE +// ) +// .all() +// ); + +// assert!( +// utils::dvec2_compare( +// quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])), +// subpath.evaluate(SubpathTValue::Parametric { +// segment_index: subpath_intersections[1].0, +// t: subpath_intersections[1].1 +// }), +// MAX_ABSOLUTE_DIFFERENCE +// ) +// .all() +// ); + +// assert!( +// utils::dvec2_compare( +// quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[1])), +// subpath.evaluate(SubpathTValue::Parametric { +// segment_index: subpath_intersections[2].0, +// t: subpath_intersections[2].1 +// }), +// MAX_ABSOLUTE_DIFFERENCE +// ) +// .all() +// ); +// } + +// // TODO: add more intersection tests + +// #[test] +// fn is_inside_subpath() { +// let boundary_polygon = [DVec2::new(100., 100.), DVec2::new(500., 100.), DVec2::new(500., 500.), DVec2::new(100., 500.)].to_vec(); +// let boundary_polygon = Subpath::from_anchors_linear(boundary_polygon, true); + +// let curve = Bezier::from_quadratic_dvec2(DVec2::new(189., 289.), DVec2::new(9., 286.), DVec2::new(45., 410.)); +// let curve_intersecting = Subpath::::from_bezier(&curve); +// assert!(!curve_intersecting.is_inside_subpath(&boundary_polygon, None, None)); + +// let curve = Bezier::from_quadratic_dvec2(DVec2::new(115., 37.), DVec2::new(51.4, 91.8), DVec2::new(76.5, 242.)); +// let curve_outside = Subpath::::from_bezier(&curve); +// assert!(!curve_outside.is_inside_subpath(&boundary_polygon, None, None)); + +// let curve = Bezier::from_cubic_dvec2(DVec2::new(210.1, 133.5), DVec2::new(150.2, 436.9), DVec2::new(436., 285.), DVec2::new(247.6, 240.7)); +// let curve_inside = Subpath::::from_bezier(&curve); +// assert!(curve_inside.is_inside_subpath(&boundary_polygon, None, None)); + +// let line = Bezier::from_linear_dvec2(DVec2::new(101., 101.5), DVec2::new(150.2, 499.)); +// let line_inside = Subpath::::from_bezier(&line); +// assert!(line_inside.is_inside_subpath(&boundary_polygon, None, None)); +// } + +// #[test] +// fn round_join_counter_clockwise_rotation() { +// // Test case where the round join is drawn in the counter clockwise direction between two consecutive offsets +// let subpath = Subpath::new( +// vec![ +// ManipulatorGroup { +// anchor: DVec2::new(20., 20.), +// out_handle: Some(DVec2::new(10., 90.)), +// in_handle: None, +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: DVec2::new(114., 159.), +// out_handle: None, +// in_handle: Some(DVec2::new(60., 40.)), +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: DVec2::new(148., 155.), +// out_handle: None, +// in_handle: None, +// id: EmptyId, +// }, +// ], +// false, +// ); + +// let offset = subpath.offset(10., utils::Join::Round); +// let offset_len = offset.len(); + +// let manipulator_groups = offset.manipulator_groups(); +// let round_start = manipulator_groups[offset_len - 4].anchor; +// let round_point = manipulator_groups[offset_len - 3].anchor; +// let round_end = manipulator_groups[offset_len - 2].anchor; + +// let middle = (round_start + round_end) / 2.; + +// assert!((round_point - middle).angle_to(round_start - middle) > 0.); +// assert!((round_end - middle).angle_to(round_point - middle) > 0.); +// } + +// #[test] +// fn round_join_clockwise_rotation() { +// // Test case where the round join is drawn in the clockwise direction between two consecutive offsets +// let subpath = Subpath::new( +// vec![ +// ManipulatorGroup { +// anchor: DVec2::new(20., 20.), +// out_handle: Some(DVec2::new(10., 90.)), +// in_handle: None, +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: DVec2::new(150., 40.), +// out_handle: None, +// in_handle: Some(DVec2::new(60., 40.)), +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: DVec2::new(78., 36.), +// out_handle: None, +// in_handle: None, +// id: EmptyId, +// }, +// ], +// false, +// ); + +// let offset = subpath.offset(-15., utils::Join::Round); +// let offset_len = offset.len(); + +// let manipulator_groups = offset.manipulator_groups(); +// let round_start = manipulator_groups[offset_len - 4].anchor; +// let round_point = manipulator_groups[offset_len - 3].anchor; +// let round_end = manipulator_groups[offset_len - 2].anchor; + +// let middle = (round_start + round_end) / 2.; + +// assert!((round_point - middle).angle_to(round_start - middle) < 0.); +// assert!((round_end - middle).angle_to(round_point - middle) < 0.); +// } +// } diff --git a/node-graph/gcore/src/subpath/structs.rs b/node-graph/gcore/src/subpath/structs.rs new file mode 100644 index 0000000000..e5191ff19d --- /dev/null +++ b/node-graph/gcore/src/subpath/structs.rs @@ -0,0 +1,444 @@ +use crate::vector::algorithms::intersection::filtered_segment_intersections; +use crate::vector::misc::{dvec2_to_point, handles_to_segment}; + +use glam::{DAffine2, DVec2}; +use kurbo::{CubicBez, Line, PathSeg, QuadBez, Shape}; +use std::fmt::{Debug, Formatter, Result}; +use std::hash::Hash; + +/// An id type used for each [ManipulatorGroup]. +pub trait Identifier: Sized + Clone + PartialEq + Hash + 'static { + fn new() -> Self; +} + +/// An empty id type for use in tests +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] +#[cfg(test)] +pub(crate) struct EmptyId; + +#[cfg(test)] +impl Identifier for EmptyId { + fn new() -> Self { + Self + } +} + +/// Structure used to represent a single anchor with up to two optional associated handles along a `Subpath` +#[derive(Copy, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ManipulatorGroup { + pub anchor: DVec2, + pub in_handle: Option, + pub out_handle: Option, + pub id: PointId, +} + +// TODO: Remove once we no longer need to hash floats in Graphite +impl Hash for ManipulatorGroup { + fn hash(&self, state: &mut H) { + self.anchor.to_array().iter().for_each(|x| x.to_bits().hash(state)); + self.in_handle.is_some().hash(state); + if let Some(in_handle) = self.in_handle { + in_handle.to_array().iter().for_each(|x| x.to_bits().hash(state)); + } + self.out_handle.is_some().hash(state); + if let Some(out_handle) = self.out_handle { + out_handle.to_array().iter().for_each(|x| x.to_bits().hash(state)); + } + self.id.hash(state); + } +} + +#[cfg(feature = "dyn-any")] +unsafe impl dyn_any::StaticType for ManipulatorGroup { + type Static = ManipulatorGroup; +} + +impl Debug for ManipulatorGroup { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + f.debug_struct("ManipulatorGroup") + .field("anchor", &self.anchor) + .field("in_handle", &self.in_handle) + .field("out_handle", &self.out_handle) + .finish() + } +} + +impl ManipulatorGroup { + /// Construct a new manipulator group from an anchor, in handle and out handle + pub fn new(anchor: DVec2, in_handle: Option, out_handle: Option) -> Self { + let id = PointId::new(); + Self { anchor, in_handle, out_handle, id } + } + + /// Construct a new manipulator point with just an anchor position + pub fn new_anchor(anchor: DVec2) -> Self { + Self::new(anchor, Some(anchor), Some(anchor)) + } + + pub fn new_anchor_linear(anchor: DVec2) -> Self { + Self::new(anchor, None, None) + } + + /// Construct a new manipulator group from an anchor, in handle, out handle and an id + pub fn new_with_id(anchor: DVec2, in_handle: Option, out_handle: Option, id: PointId) -> Self { + Self { anchor, in_handle, out_handle, id } + } + + /// Construct a new manipulator point with just an anchor position and an id + pub fn new_anchor_with_id(anchor: DVec2, id: PointId) -> Self { + Self::new_with_id(anchor, Some(anchor), Some(anchor), id) + } + + /// Create a bezier curve that starts at the current manipulator group and finishes in the `end_group` manipulator group. + pub fn to_bezier(&self, end_group: &ManipulatorGroup) -> PathSeg { + let start = self.anchor; + let end = end_group.anchor; + let out_handle = self.out_handle; + let in_handle = end_group.in_handle; + + match (out_handle, in_handle) { + (Some(handle1), Some(handle2)) => PathSeg::Cubic(CubicBez::new(dvec2_to_point(start), dvec2_to_point(handle1), dvec2_to_point(handle2), dvec2_to_point(end))), + (Some(handle), None) | (None, Some(handle)) => PathSeg::Quad(QuadBez::new(dvec2_to_point(start), dvec2_to_point(handle), dvec2_to_point(end))), + (None, None) => PathSeg::Line(Line::new(dvec2_to_point(start), dvec2_to_point(end))), + } + } + + /// Apply a transformation to all of the [ManipulatorGroup] points + pub fn apply_transform(&mut self, affine_transform: DAffine2) { + self.anchor = affine_transform.transform_point2(self.anchor); + self.in_handle = self.in_handle.map(|in_handle| affine_transform.transform_point2(in_handle)); + self.out_handle = self.out_handle.map(|out_handle| affine_transform.transform_point2(out_handle)); + } + + /// Are all handles at finite positions + pub fn is_finite(&self) -> bool { + self.anchor.is_finite() && self.in_handle.is_none_or(|handle| handle.is_finite()) && self.out_handle.is_none_or(|handle| handle.is_finite()) + } + + /// Reverse directions of handles + pub fn flip(mut self) -> Self { + std::mem::swap(&mut self.in_handle, &mut self.out_handle); + self + } + + pub fn has_in_handle(&self) -> bool { + self.in_handle.map(|handle| Self::has_handle(self.anchor, handle)).unwrap_or(false) + } + + pub fn has_out_handle(&self) -> bool { + self.out_handle.map(|handle| Self::has_handle(self.anchor, handle)).unwrap_or(false) + } + + fn has_handle(anchor: DVec2, handle: DVec2) -> bool { + !((handle.x - anchor.x).abs() < f64::EPSILON && (handle.y - anchor.y).abs() < f64::EPSILON) + } +} + +#[derive(Copy, Clone)] +pub enum AppendType { + IgnoreStart, + SmoothJoin(f64), +} + +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub enum ArcType { + Open, + Closed, + PieSlice, +} + +/// Representation of the handle point(s) in a bezier segment. +#[derive(Copy, Clone, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum BezierHandles { + Linear, + /// Handles for a quadratic curve. + Quadratic { + /// Point representing the location of the single handle. + handle: DVec2, + }, + /// Handles for a cubic curve. + Cubic { + /// Point representing the location of the handle associated to the start point. + handle_start: DVec2, + /// Point representing the location of the handle associated to the end point. + handle_end: DVec2, + }, +} + +impl std::hash::Hash for BezierHandles { + fn hash(&self, state: &mut H) { + std::mem::discriminant(self).hash(state); + match self { + BezierHandles::Linear => {} + BezierHandles::Quadratic { handle } => handle.to_array().map(|v| v.to_bits()).hash(state), + BezierHandles::Cubic { handle_start, handle_end } => [handle_start, handle_end].map(|handle| handle.to_array().map(|v| v.to_bits())).hash(state), + } + } +} + +impl BezierHandles { + pub fn is_cubic(&self) -> bool { + matches!(self, Self::Cubic { .. }) + } + + pub fn is_finite(&self) -> bool { + match self { + BezierHandles::Linear => true, + BezierHandles::Quadratic { handle } => handle.is_finite(), + BezierHandles::Cubic { handle_start, handle_end } => handle_start.is_finite() && handle_end.is_finite(), + } + } + + /// Get the coordinates of the bezier segment's first handle point. This represents the only handle in a quadratic segment. + pub fn start(&self) -> Option { + match *self { + BezierHandles::Cubic { handle_start, .. } | BezierHandles::Quadratic { handle: handle_start } => Some(handle_start), + _ => None, + } + } + + /// Get the coordinates of the second handle point. This will return `None` for a quadratic segment. + pub fn end(&self) -> Option { + match *self { + BezierHandles::Cubic { handle_end, .. } => Some(handle_end), + _ => None, + } + } + + pub fn move_start(&mut self, delta: DVec2) { + if let BezierHandles::Cubic { handle_start, .. } | BezierHandles::Quadratic { handle: handle_start } = self { + *handle_start += delta + } + } + + pub fn move_end(&mut self, delta: DVec2) { + if let BezierHandles::Cubic { handle_end, .. } = self { + *handle_end += delta + } + } + + /// Returns a Bezier curve that results from applying the transformation function to each handle point in the Bezier. + #[must_use] + pub fn apply_transformation(&self, transformation_function: impl Fn(DVec2) -> DVec2) -> Self { + match *self { + BezierHandles::Linear => Self::Linear, + BezierHandles::Quadratic { handle } => { + let handle = transformation_function(handle); + Self::Quadratic { handle } + } + BezierHandles::Cubic { handle_start, handle_end } => { + let handle_start = transformation_function(handle_start); + let handle_end = transformation_function(handle_end); + Self::Cubic { handle_start, handle_end } + } + } + } + + #[must_use] + pub fn reversed(self) -> Self { + match self { + BezierHandles::Cubic { handle_start, handle_end } => Self::Cubic { + handle_start: handle_end, + handle_end: handle_start, + }, + _ => self, + } + } +} + +#[cfg(feature = "dyn-any")] +unsafe impl dyn_any::StaticType for BezierHandles { + type Static = 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() + } +} + +#[cfg(feature = "dyn-any")] +unsafe impl dyn_any::StaticType for Bezier { + type Static = Bezier; +} + +/// 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 { + 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 { + 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 + 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, minimum_separation: Option) -> Vec { + 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)) + } +} diff --git a/node-graph/gcore/src/subpath/transform.rs b/node-graph/gcore/src/subpath/transform.rs new file mode 100644 index 0000000000..72a1e96464 --- /dev/null +++ b/node-graph/gcore/src/subpath/transform.rs @@ -0,0 +1,642 @@ +use super::structs::Identifier; +use super::*; +use glam::{DAffine2, DVec2}; + +/// Helper function to ensure the index and t value pair is mapped within a maximum index value. +/// Allows for the point to be fetched without needing to handle an additional edge case. +/// - Ex. Via `subpath.iter().nth(index).evaluate(t);` +fn map_index_within_range(index: usize, t: f64, max_size: usize) -> (usize, f64) { + if max_size > 0 && index == max_size && t == 0. { (index - 1, 1.) } else { (index, t) } +} + +/// Functionality that transforms Subpaths, such as split, reduce, offset, etc. +impl Subpath { + /// Returns [ManipulatorGroup]s with a reversed winding order. + fn reverse_manipulator_groups(manipulator_groups: &[ManipulatorGroup]) -> Vec> { + manipulator_groups + .iter() + .rev() + .map(|group| ManipulatorGroup { + anchor: group.anchor, + in_handle: group.out_handle, + out_handle: group.in_handle, + id: PointId::new(), + }) + .collect::>>() + } + + /// Returns a [Subpath] with a reversed winding order. + /// Note that a reversed closed subpath will start on the same manipulator group and simply wind the other direction + pub fn reverse(&self) -> Subpath { + let mut reversed = Subpath::reverse_manipulator_groups(self.manipulator_groups()); + if self.closed { + reversed.rotate_right(1); + }; + Subpath { + manipulator_groups: reversed, + closed: self.closed, + } + } + + /// Apply a transformation to all of the [ManipulatorGroup]s in the [Subpath]. + pub fn apply_transform(&mut self, affine_transform: DAffine2) { + for manipulator_group in &mut self.manipulator_groups { + manipulator_group.apply_transform(affine_transform); + } + } + + /// Returns a subpath that results from rotating this subpath around the origin by the given angle (in radians). + /// + pub fn rotate(&self, angle: f64) -> Subpath { + let mut rotated_subpath = self.clone(); + + let affine_transform: DAffine2 = DAffine2::from_angle(angle); + rotated_subpath.apply_transform(affine_transform); + + rotated_subpath + } + + /// Returns a subpath that results from rotating this subpath around the provided point by the given angle (in radians). + pub fn rotate_about_point(&self, angle: f64, pivot: DVec2) -> Subpath { + // Translate before and after the rotation to account for the pivot + let translate: DAffine2 = DAffine2::from_translation(pivot); + let rotate: DAffine2 = DAffine2::from_angle(angle); + let translate_inverse = translate.inverse(); + + let mut rotated_subpath = self.clone(); + rotated_subpath.apply_transform(translate * rotate * translate_inverse); + rotated_subpath + } +} + +// #[cfg(test)] +// mod tests { +// use super::{Cap, Join, ManipulatorGroup, Subpath}; +// use crate::EmptyId; +// use crate::compare::{compare_points, compare_subpaths, compare_vec_of_points}; +// use crate::consts::MAX_ABSOLUTE_DIFFERENCE; +// use crate::utils::{SubpathTValue, TValue}; +// use glam::DVec2; + +// fn set_up_open_subpath() -> Subpath { +// let start = DVec2::new(20., 30.); +// let middle1 = DVec2::new(80., 90.); +// let middle2 = DVec2::new(100., 100.); +// let end = DVec2::new(60., 45.); + +// let handle1 = DVec2::new(75., 85.); +// let handle2 = DVec2::new(40., 30.); +// let handle3 = DVec2::new(10., 10.); + +// Subpath::new( +// vec![ +// ManipulatorGroup { +// anchor: start, +// in_handle: None, +// out_handle: Some(handle1), +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: middle1, +// in_handle: None, +// out_handle: Some(handle2), +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: middle2, +// in_handle: None, +// out_handle: None, +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: end, +// in_handle: None, +// out_handle: Some(handle3), +// id: EmptyId, +// }, +// ], +// false, +// ) +// } + +// fn set_up_closed_subpath() -> Subpath { +// let mut subpath = set_up_open_subpath(); +// subpath.closed = true; +// subpath +// } + +// #[test] +// fn outline_with_single_point_segment() { +// let subpath = Subpath::new( +// vec![ +// ManipulatorGroup { +// anchor: DVec2::new(20., 20.), +// out_handle: Some(DVec2::new(10., 90.)), +// in_handle: None, +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: DVec2::new(150., 40.), +// out_handle: None, +// in_handle: Some(DVec2::new(60., 40.)), +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: DVec2::new(150., 40.), +// out_handle: Some(DVec2::new(40., 120.)), +// in_handle: None, +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: DVec2::new(100., 100.), +// out_handle: None, +// in_handle: None, +// id: EmptyId, +// }, +// ], +// false, +// ); + +// let outline = subpath.outline(10., crate::Join::Round, crate::Cap::Round).0; +// assert!(outline.manipulator_groups.windows(2).all(|pair| !pair[0].anchor.abs_diff_eq(pair[1].anchor, MAX_ABSOLUTE_DIFFERENCE))); +// assert!(outline.closed()); +// } + +// #[test] +// /// Even though the bézier here is not marked as a point, the offset and scaled version is. +// fn outline_with_point_offset() { +// let subpath = Subpath::new( +// vec![ +// ManipulatorGroup { +// anchor: DVec2::new(1122.6253015182049, 610.9441551227939), +// out_handle: Some(DVec2::new(1122.6253015182049, 610.9445412168651)), +// in_handle: None, +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: DVec2::new(1122.6258671405062, 610.9453107605276), +// out_handle: None, +// in_handle: Some(DVec2::new(1122.6254904904154, 610.9449255479497)), +// id: EmptyId, +// }, +// ManipulatorGroup { +// anchor: DVec2::new(0., 0.), +// out_handle: None, +// in_handle: None, +// id: EmptyId, +// }, +// ], +// false, +// ); +// let outline = subpath.outline(4.4, crate::Join::Round, crate::Cap::Round).0; +// assert!(outline.manipulator_groups.windows(2).all(|pair| !pair[0].anchor.abs_diff_eq(pair[1].anchor, MAX_ABSOLUTE_DIFFERENCE))); +// assert!(outline.closed()); +// } + +// #[test] +// fn split_an_open_subpath() { +// let subpath = set_up_open_subpath(); +// let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); +// let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 3.) % 1.)); +// let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.2)); +// assert!(second.is_some()); +// let second = second.unwrap(); +// assert_eq!(first.manipulator_groups[1].anchor, location); +// assert_eq!(second.manipulator_groups[0].anchor, location); +// assert_eq!(split_pair[0], first.iter().last().unwrap()); +// assert_eq!(split_pair[1], second.iter().next().unwrap()); +// } + +// #[test] +// fn split_at_start_of_an_open_subpath() { +// let subpath = set_up_open_subpath(); +// let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); +// let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric(0.)); +// let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.)); +// assert!(second.is_some()); +// let second = second.unwrap(); +// assert_eq!( +// first.manipulator_groups[0], +// ManipulatorGroup { +// anchor: location, +// in_handle: None, +// out_handle: None, +// id: EmptyId, +// } +// ); +// assert_eq!(first.manipulator_groups.len(), 1); +// assert_eq!(second.manipulator_groups[0].anchor, location); +// assert_eq!(split_pair[1], second.iter().next().unwrap()); +// } + +// #[test] +// fn split_at_end_of_an_open_subpath() { +// let subpath = set_up_open_subpath(); +// let location = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); +// let split_pair = subpath.iter().last().unwrap().split(TValue::Parametric(1.)); +// let (first, second) = subpath.split(SubpathTValue::GlobalParametric(1.)); +// assert!(second.is_some()); +// let second = second.unwrap(); +// assert_eq!(first.manipulator_groups[3].anchor, location); +// assert_eq!(split_pair[0], first.iter().last().unwrap()); +// assert_eq!( +// second.manipulator_groups[0], +// ManipulatorGroup { +// anchor: location, +// in_handle: None, +// out_handle: None, +// id: EmptyId, +// } +// ); +// assert_eq!(second.manipulator_groups.len(), 1); +// } + +// #[test] +// fn split_a_closed_subpath() { +// let subpath = set_up_closed_subpath(); +// let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); +// let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 4.) % 1.)); +// let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.2)); +// assert!(second.is_none()); +// assert_eq!(first.manipulator_groups[0].anchor, location); +// assert_eq!(first.manipulator_groups[5].anchor, location); +// assert_eq!(first.manipulator_groups.len(), 6); +// assert_eq!(split_pair[0], first.iter().last().unwrap()); +// assert_eq!(split_pair[1], first.iter().next().unwrap()); +// } + +// #[test] +// fn split_at_start_of_a_closed_subpath() { +// let subpath = set_up_closed_subpath(); +// let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); +// let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.)); +// assert!(second.is_none()); +// assert_eq!(first.manipulator_groups[0].anchor, location); +// assert_eq!(first.manipulator_groups[4].anchor, location); +// assert_eq!(subpath.manipulator_groups[0..], first.manipulator_groups[..4]); +// assert!(!first.closed); +// assert_eq!(first.iter().last().unwrap(), subpath.iter().last().unwrap()); +// assert_eq!(first.iter().next().unwrap(), subpath.iter().next().unwrap()); +// } + +// #[test] +// fn split_at_end_of_a_closed_subpath() { +// let subpath = set_up_closed_subpath(); +// let location = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); +// let (first, second) = subpath.split(SubpathTValue::GlobalParametric(1.)); +// assert!(second.is_none()); +// assert_eq!(first.manipulator_groups[0].anchor, location); +// assert_eq!(first.manipulator_groups[4].anchor, location); +// assert_eq!(subpath.manipulator_groups[0..], first.manipulator_groups[..4]); +// assert!(!first.closed); +// assert_eq!(first.iter().last().unwrap(), subpath.iter().last().unwrap()); +// assert_eq!(first.iter().next().unwrap(), subpath.iter().next().unwrap()); +// } + +// #[test] +// fn reverse_an_open_subpath() { +// let subpath = set_up_open_subpath(); +// let temporary = subpath.reverse(); +// let result = temporary.reverse(); +// let end = result.len(); + +// assert_eq!(temporary.manipulator_groups[0].anchor, result.manipulator_groups[end - 1].anchor); +// assert_eq!(temporary.manipulator_groups[0].out_handle, result.manipulator_groups[end - 1].in_handle); +// assert_eq!(subpath, result); +// } + +// #[test] +// fn reverse_a_closed_subpath() { +// let subpath = set_up_closed_subpath(); +// let temporary = subpath.reverse(); +// let result = temporary.reverse(); +// let end = result.len(); + +// // Second manipulator group on the temporary subpath should be the reflected version of the last in the result +// assert_eq!(temporary.manipulator_groups[1].anchor, result.manipulator_groups[end - 1].anchor); +// assert_eq!(temporary.manipulator_groups[1].in_handle, result.manipulator_groups[end - 1].out_handle); +// assert_eq!(temporary.manipulator_groups[1].out_handle, result.manipulator_groups[end - 1].in_handle); + +// // The first manipulator group in both should be the reflected versions of each other +// assert_eq!(temporary.manipulator_groups[0].anchor, result.manipulator_groups[0].anchor); +// assert_eq!(temporary.manipulator_groups[0].in_handle, result.manipulator_groups[0].out_handle); +// assert_eq!(temporary.manipulator_groups[0].out_handle, result.manipulator_groups[0].in_handle); +// assert_eq!(subpath, result); +// } + +// #[test] +// fn trim_an_open_subpath() { +// let subpath = set_up_open_subpath(); +// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); +// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); +// let [_, trim_front] = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 3.) % 1.)); +// let [trim_back, _] = subpath.iter().last().unwrap().split(TValue::Parametric((0.8 * 3.) % 1.)); +// let result = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.8)); + +// assert_eq!(result.manipulator_groups[0].anchor, location_front); +// assert_eq!(result.manipulator_groups[3].anchor, location_back); +// assert_eq!(trim_front, result.iter().next().unwrap()); +// assert_eq!(trim_back, result.iter().last().unwrap()); +// } + +// #[test] +// fn trim_within_a_bezier() { +// let subpath = set_up_open_subpath(); +// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.1)); +// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); +// let trimmed = subpath.iter().next().unwrap().trim(TValue::Parametric((0.1 * 3.) % 1.), TValue::Parametric((0.2 * 3.) % 1.)); +// let result = subpath.trim(SubpathTValue::GlobalParametric(0.1), SubpathTValue::GlobalParametric(0.2)); +// assert!(compare_points(result.manipulator_groups[0].anchor, location_front)); +// assert!(compare_points(result.manipulator_groups[1].anchor, location_back)); +// assert_eq!(trimmed, result.iter().next().unwrap()); +// assert_eq!(result.len(), 2); +// } + +// #[test] +// fn trim_first_segment_of_an_open_subpath() { +// let subpath = set_up_closed_subpath(); +// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); +// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.25)); +// let trimmed = subpath.iter().next().unwrap().trim(TValue::Parametric(0.), TValue::Parametric(1.)); +// let result = subpath.trim(SubpathTValue::GlobalParametric(0.), SubpathTValue::GlobalParametric(0.25)); + +// assert_eq!(result.manipulator_groups[0].anchor, location_front); +// assert_eq!(result.manipulator_groups[1].anchor, location_back); +// assert_eq!(trimmed, result.iter().next().unwrap()); +// } + +// #[test] +// fn trim_second_segment_of_an_open_subpath() { +// let subpath = set_up_closed_subpath(); +// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.25)); +// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.5)); +// let trimmed = subpath.iter().nth(1).unwrap().trim(TValue::Parametric(0.), TValue::Parametric(1.)); +// let result = subpath.trim(SubpathTValue::GlobalParametric(0.25), SubpathTValue::GlobalParametric(0.5)); + +// assert_eq!(result.manipulator_groups[0].anchor, location_front); +// assert_eq!(result.manipulator_groups[1].anchor, location_back); +// assert_eq!(trimmed, result.iter().next().unwrap()); +// } + +// #[test] +// fn trim_reverse_in_open_subpath() { +// let subpath = set_up_open_subpath(); +// 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)); +// } + +// #[test] +// fn trim_reverse_within_a_bezier() { +// let subpath = set_up_open_subpath(); +// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.1)); +// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); +// let trimmed = subpath.iter().next().unwrap().trim(TValue::Parametric((0.2 * 3.) % 1.), TValue::Parametric((0.1 * 3.) % 1.)); +// let result = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.1)); + +// assert!(compare_points(result.manipulator_groups[0].anchor, location_front)); +// assert!(compare_points(result.manipulator_groups[1].anchor, location_back)); +// assert!(compare_vec_of_points( +// trimmed.get_points().collect(), +// result.iter().next().unwrap().get_points().collect(), +// MAX_ABSOLUTE_DIFFERENCE +// )); +// assert_eq!(result.len(), 2); +// } + +// #[test] +// fn trim_a_duplicate_subpath() { +// let subpath = set_up_open_subpath(); +// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); +// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); +// 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; +// expected_subpath[3].out_handle = None; + +// assert_eq!(result.manipulator_groups[0].anchor, location_front); +// assert!(compare_points(result.manipulator_groups[3].anchor, location_back)); +// assert_eq!(expected_subpath, result); +// } + +// #[test] +// fn trim_a_reversed_duplicate_subpath() { +// let subpath = set_up_open_subpath(); +// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); +// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); +// let result = subpath.trim(SubpathTValue::GlobalParametric(1.), SubpathTValue::GlobalParametric(0.)); + +// assert_eq!(result.manipulator_groups[0].anchor, location_front); +// assert_eq!(result.manipulator_groups[3].anchor, location_back); +// assert!(compare_subpaths::(&subpath, &result)); +// } + +// #[test] +// fn trim_to_end_of_subpath() { +// let subpath = set_up_open_subpath(); +// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); +// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); +// let trimmed = subpath.iter().last().unwrap().trim(TValue::Parametric((0.8 * 3.) % 1.), TValue::Parametric(1.)); +// let result = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(1.)); + +// assert_eq!(result.manipulator_groups[0].anchor, location_front); +// assert!(compare_points(result.manipulator_groups[1].anchor, location_back)); +// assert_eq!(trimmed, result.iter().next().unwrap()); +// } + +// #[test] +// fn trim_reversed_to_end_of_subpath() { +// let subpath = set_up_open_subpath(); +// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); +// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); +// let trimmed = subpath.iter().next().unwrap().trim(TValue::Parametric((0.2 * 3.) % 1.), TValue::Parametric(0.)); +// let result = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.)); + +// assert!(compare_points(result.manipulator_groups[0].anchor, location_front)); +// assert!(compare_points(result.manipulator_groups[1].anchor, location_back)); +// assert!(compare_vec_of_points( +// trimmed.get_points().collect(), +// result.iter().next().unwrap().get_points().collect(), +// MAX_ABSOLUTE_DIFFERENCE +// )); +// } + +// #[test] +// fn trim_start_point() { +// let subpath = set_up_open_subpath(); +// let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); +// let result = subpath.trim(SubpathTValue::GlobalParametric(0.), SubpathTValue::GlobalParametric(0.)); + +// assert!(compare_points(result.manipulator_groups[0].anchor, location)); +// assert!(result.manipulator_groups[0].in_handle.is_none()); +// assert!(result.manipulator_groups[0].out_handle.is_none()); +// assert_eq!(result.len(), 1); +// } + +// #[test] +// fn trim_middle_point() { +// let subpath = set_up_closed_subpath(); +// let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.25)); +// let result = subpath.trim(SubpathTValue::GlobalParametric(0.25), SubpathTValue::GlobalParametric(0.25)); + +// assert!(compare_points(result.manipulator_groups[0].anchor, location)); +// assert!(result.manipulator_groups[0].in_handle.is_none()); +// assert!(result.manipulator_groups[0].out_handle.is_none()); +// assert_eq!(result.len(), 1); +// } + +// #[test] +// fn trim_a_closed_subpath() { +// let subpath = set_up_closed_subpath(); +// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); +// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); +// let [_, trim_front] = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 4.) % 1.)); +// let [trim_back, _] = subpath.iter().last().unwrap().split(TValue::Parametric((0.8 * 4.) % 1.)); +// let result = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.8)); + +// assert_eq!(result.manipulator_groups[0].anchor, location_front); +// assert_eq!(result.manipulator_groups[4].anchor, location_back); +// assert_eq!(trim_front, result.iter().next().unwrap()); +// assert_eq!(trim_back, result.iter().last().unwrap()); +// } + +// #[test] +// fn trim_to_end_of_closed_subpath() { +// let subpath = set_up_closed_subpath(); +// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); +// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); +// let trimmed = subpath.iter().last().unwrap().trim(TValue::Parametric((0.8 * 4.) % 1.), TValue::Parametric(1.)); +// let result = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(1.)); + +// assert_eq!(result.manipulator_groups[0].anchor, location_front); +// assert!(compare_points(result.manipulator_groups[1].anchor, location_back)); +// assert_eq!(trimmed, result.iter().next().unwrap()); +// } + +// #[test] +// fn trim_across_break_in_a_closed_subpath() { +// let subpath = set_up_closed_subpath(); +// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); +// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); +// let [_, trim_front] = subpath.iter().last().unwrap().split(TValue::Parametric((0.8 * 4.) % 1.)); +// let [trim_back, _] = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 4.) % 1.)); +// let result = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(0.2)); + +// assert_eq!(result.manipulator_groups[0].anchor, location_front); +// assert_eq!(result.manipulator_groups[2].anchor, location_back); +// assert_eq!(trim_front, result.iter().next().unwrap()); +// assert_eq!(trim_back, result.iter().last().unwrap()); +// } + +// #[test] +// fn trim_across_break_in_a_closed_subpath_where_result_is_multiple_segments() { +// let subpath = set_up_closed_subpath(); +// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.6)); +// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.4)); +// let [_, trim_front] = subpath.iter().nth(2).unwrap().split(TValue::Parametric((0.6 * 4.) % 1.)); +// let [trim_back, _] = subpath.iter().nth(1).unwrap().split(TValue::Parametric((0.4 * 4.) % 1.)); +// let result = subpath.trim(SubpathTValue::GlobalParametric(0.6), SubpathTValue::GlobalParametric(0.4)); + +// assert_eq!(result.manipulator_groups[0].anchor, location_front); +// assert_eq!(result.manipulator_groups[4].anchor, location_back); +// assert_eq!(trim_front, result.iter().next().unwrap()); +// assert_eq!(trim_back, result.iter().last().unwrap()); +// } + +// #[test] +// fn trim_across_break_in_a_closed_subpath_where_ends_are_in_same_segment() { +// let subpath = set_up_closed_subpath(); +// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.45)); +// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.4)); +// let [_, trim_front] = subpath.iter().nth(1).unwrap().split(TValue::Parametric((0.45 * 4.) % 1.)); +// let [trim_back, _] = subpath.iter().nth(1).unwrap().split(TValue::Parametric((0.4 * 4.) % 1.)); +// let result = subpath.trim(SubpathTValue::GlobalParametric(0.45), SubpathTValue::GlobalParametric(0.4)); + +// assert_eq!(result.manipulator_groups[0].anchor, location_front); +// assert_eq!(result.manipulator_groups[5].anchor, location_back); +// assert_eq!(trim_front, result.iter().next().unwrap()); +// assert_eq!(trim_back, result.iter().last().unwrap()); +// } + +// #[test] +// fn trim_at_break_in_closed_subpath_where_end_is_0() { +// let subpath = set_up_closed_subpath(); +// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); +// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); +// let trimmed = subpath.iter().last().unwrap().trim(TValue::Parametric((0.8 * 4.) % 1.), TValue::Parametric(1.)); +// let result = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(0.)); + +// assert_eq!(result.manipulator_groups[0].anchor, location_front); +// assert_eq!(result.manipulator_groups[1].anchor, location_back); +// assert_eq!(trimmed, result.iter().next().unwrap()); +// } + +// #[test] +// fn trim_at_break_in_closed_subpath_where_start_is_1() { +// let subpath = set_up_closed_subpath(); +// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); +// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); +// let trimmed = subpath.iter().next().unwrap().trim(TValue::Parametric(0.), TValue::Parametric((0.2 * 4.) % 1.)); +// let result = subpath.trim(SubpathTValue::GlobalParametric(1.), SubpathTValue::GlobalParametric(0.2)); + +// assert_eq!(result.manipulator_groups[0].anchor, location_front); +// assert_eq!(result.manipulator_groups[1].anchor, location_back); +// assert_eq!(trimmed, result.iter().next().unwrap()); +// } + +// #[test] +// fn trim_at_break_in_closed_subpath_from_1_to_0() { +// let subpath = set_up_closed_subpath(); +// let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); +// let result = subpath.trim(SubpathTValue::GlobalParametric(1.), SubpathTValue::GlobalParametric(0.)); + +// assert_eq!(result.manipulator_groups[0].anchor, location); +// assert!(result.manipulator_groups[0].in_handle.is_none()); +// assert!(result.manipulator_groups[0].out_handle.is_none()); +// assert_eq!(result.manipulator_groups.len(), 1); +// } + +// #[test] +// fn outline_single_point_circle() { +// let ellipse: Subpath = Subpath::new_ellipse(DVec2::new(0., 0.), DVec2::new(50., 50.)).reverse(); +// let p = DVec2::new(25., 25.); + +// let subpath: Subpath = Subpath::from_anchors([p, p, p], false); +// let outline_open = subpath.outline(25., Join::Bevel, Cap::Round); +// assert_eq!(outline_open.0, ellipse); +// assert_eq!(outline_open.1, None); + +// let subpath_closed: Subpath = Subpath::from_anchors([p, p, p], true); +// let outline_closed = subpath_closed.outline(25., Join::Bevel, Cap::Round); +// assert_eq!(outline_closed.0, ellipse); +// assert_eq!(outline_closed.1, None); +// } + +// #[test] +// fn outline_single_point_square() { +// let square: Subpath = Subpath::from_anchors( +// [ +// DVec2::new(25., 0.), +// DVec2::new(0., 0.), +// DVec2::new(0., 50.), +// DVec2::new(25., 50.), +// DVec2::new(50., 50.), +// DVec2::new(50., 0.), +// ], +// true, +// ); +// let p = DVec2::new(25., 25.); + +// let subpath: Subpath = Subpath::from_anchors([p, p, p], false); +// let outline_open = subpath.outline(25., Join::Bevel, Cap::Square); +// assert_eq!(outline_open.0, square); +// assert_eq!(outline_open.1, None); + +// let subpath_closed: Subpath = Subpath::from_anchors([p, p, p], true); +// let outline_closed = subpath_closed.outline(25., Join::Bevel, Cap::Square); +// assert_eq!(outline_closed.0, square); +// assert_eq!(outline_closed.1, None); +// } +// } diff --git a/node-graph/gcore/src/subpath/utils.rs b/node-graph/gcore/src/subpath/utils.rs new file mode 100644 index 0000000000..c28c89d567 --- /dev/null +++ b/node-graph/gcore/src/subpath/utils.rs @@ -0,0 +1,429 @@ +// use super::consts::{MAX_ABSOLUTE_DIFFERENCE, STRICT_MAX_ABSOLUTE_DIFFERENCE}; +// use super::*; +// use glam::{BVec2, DMat2, DVec2}; +// use std::fmt::Write; + +// #[derive(Copy, Clone, PartialEq)] +// /// A structure which can be used to reference a particular point along a `Bezier`. +// /// Assuming a 2-dimensional Bezier is represented as a parametric curve defined by components `(x(f(t), y(f(t))))`, this structure defines variants for `f(t)`. +// /// - The `Parametric` variant represents the point calculated using the parametric equation of the curve at argument `t`. That is, `f(t) = t`. Speed along the curve's parametric form is not constant. `t` must lie in the range `[0, 1]`. +// /// - The `Euclidean` variant represents the point calculated at a distance ratio `t` along the arc length of the curve in the range `[0, 1]`. Speed is constant along the curve's arc length. +// /// - E.g. If `d` is the distance from the start point of a `Bezier` to a certain point along the curve, and `l` is the total arc length of the curve, that certain point lies at a distance ratio `t = d / l`. +// /// - All `Bezier` functions will implicitly convert a Euclidean [TValue] argument to a parametric `t`-value using binary search, computed within a particular error. That is, a point at distance ratio `t*`, +// /// satisfying `|t* - t| <= error`. The default error is `0.001`. Given this requires a lengthier calculation, it is not recommended to use the `Euclidean` or `EuclideanWithinError` variants frequently in computationally intensive tasks. +// /// - The `EuclideanWithinError` variant functions exactly as the `Euclidean` variant, but allows the `error` to be customized when computing `t` internally. +// pub enum TValue { +// Parametric(f64), +// Euclidean(f64), +// EuclideanWithinError { t: f64, error: f64 }, +// } + +#[derive(Copy, Clone, PartialEq)] +pub enum TValueType { + Parametric, + Euclidean, +} + +#[derive(Copy, Clone, PartialEq)] +pub enum SubpathTValue { + Parametric { segment_index: usize, t: f64 }, + GlobalParametric(f64), + Euclidean { segment_index: usize, t: f64 }, + GlobalEuclidean(f64), + EuclideanWithinError { segment_index: usize, t: f64, error: f64 }, + GlobalEuclideanWithinError { t: f64, error: f64 }, +} + +// #[derive(Copy, Clone)] +// /// Represents the shape of the join between two segments of a path which meet at an angle. +// /// Bevel provides a flat connection, Miter provides a sharp connection, and Round provides a rounded connection. +// /// As defined in SVG: . +// pub enum Join { +// /// The join is a straight line between the end points of the offset path sides from the two connecting segments. +// Bevel, +// /// Optional f64 is the miter limit, which defaults to 4 if `None` or a value less than 1 is provided. +// /// The miter limit is used to prevent highly sharp angles from resulting in excessively long miter joins. +// /// If the miter limit is exceeded, the join will be converted to a bevel join. +// /// The value is the ratio of the miter length to the stroke width. +// /// When that ratio is greater than the miter limit, a bevel join is used instead. +// Miter(Option), +// /// The join is a circular arc between the end points of the offset path sides from the two connecting segments. +// Round, +// } + +// #[derive(Copy, Clone)] +// /// Enum to represent the cap type at the ends of an outline +// /// As defined in SVG: . +// pub enum Cap { +// Butt, +// Round, +// Square, +// } + +// /// 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. +// fn compute_abc_through_points(start_point: DVec2, point_on_curve: DVec2, end_point: DVec2, t_to_nth_power: f64, nth_power_of_one_minus_t: f64) -> [DVec2; 3] { +// let point_c_ratio = nth_power_of_one_minus_t / (t_to_nth_power + nth_power_of_one_minus_t); +// let c = point_c_ratio * start_point + (1. - point_c_ratio) * end_point; +// let ab_bc_ratio = (t_to_nth_power + nth_power_of_one_minus_t - 1.).abs() / (t_to_nth_power + nth_power_of_one_minus_t); +// let a = point_on_curve + (point_on_curve - c) / ab_bc_ratio; +// [a, point_on_curve, c] +// } + +// /// Compute `a`, `b`, and `c` for a quadratic curve that fits the start, end and point on curve at `t`. +// /// The definition for the `a`, `b`, `c` points are defined in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer. +// pub fn compute_abc_for_quadratic_through_points(start_point: DVec2, point_on_curve: DVec2, end_point: DVec2, t: f64) -> [DVec2; 3] { +// let t_squared = t * t; +// let one_minus_t = 1. - t; +// let squared_one_minus_t = one_minus_t * one_minus_t; +// compute_abc_through_points(start_point, point_on_curve, end_point, t_squared, squared_one_minus_t) +// } + +// /// Compute `a`, `b`, and `c` for a cubic curve that fits the start, end and point on curve at `t`. +// /// The definition for the `a`, `b`, `c` points are defined in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer. +// pub fn compute_abc_for_cubic_through_points(start_point: DVec2, point_on_curve: DVec2, end_point: DVec2, t: f64) -> [DVec2; 3] { +// let t_cubed = t * t * t; +// let one_minus_t = 1. - t; +// let cubed_one_minus_t = one_minus_t * one_minus_t * one_minus_t; + +// compute_abc_through_points(start_point, point_on_curve, end_point, t_cubed, cubed_one_minus_t) +// } + +// /// Find the roots of the linear equation `ax + b`. +// pub fn solve_linear(a: f64, b: f64) -> [Option; 3] { +// // There exist roots when `a` is not 0 +// if a.abs() > MAX_ABSOLUTE_DIFFERENCE { [Some(-b / a), None, None] } else { [None; 3] } +// } + +// /// Find the roots of the linear equation `ax^2 + bx + c`. +// /// Precompute the `discriminant` (`b^2 - 4ac`) and `two_times_a` arguments prior to calling this function for efficiency purposes. +// pub fn solve_quadratic(discriminant: f64, two_times_a: f64, b: f64, c: f64) -> [Option; 3] { +// let mut roots = [None; 3]; +// if two_times_a.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE { +// roots = solve_linear(b, c); +// } else if discriminant.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE { +// roots[0] = Some(-b / (two_times_a)); +// } else if discriminant > 0. { +// let root_discriminant = discriminant.sqrt(); +// roots[0] = Some((-b + root_discriminant) / (two_times_a)); +// roots[1] = Some((-b - root_discriminant) / (two_times_a)); +// } +// roots +// } + +// // TODO: Use an `impl Iterator` return type instead of a `Vec` +// /// Solve a cubic of the form `ax^3 + bx^2 + ct + d`. +// pub fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> [Option; 3] { +// if a.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE { +// if b.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE { +// // If both a and b are approximately 0, treat as a linear problem +// solve_linear(c, d) +// } else { +// // If a is approximately 0, treat as a quadratic problem +// let discriminant = c * c - 4. * b * d; +// solve_quadratic(discriminant, 2. * b, c, d) +// } +// } else { +// // https://momentsingraphics.de/CubicRoots.html +// let d_recip = a.recip(); +// const ONETHIRD: f64 = 1. / 3.; +// let scaled_c2 = b * (ONETHIRD * d_recip); +// let scaled_c1 = c * (ONETHIRD * d_recip); +// let scaled_c0 = d * d_recip; +// if !(scaled_c0.is_finite() && scaled_c1.is_finite() && scaled_c2.is_finite()) { +// // cubic coefficient is zero or nearly so. +// return solve_quadratic(c * c - 4. * b * d, 2. * b, c, d); +// } +// let (c0, c1, c2) = (scaled_c0, scaled_c1, scaled_c2); +// // (d0, d1, d2) is called "Delta" in article +// let d0 = (-c2).mul_add(c2, c1); +// let d1 = (-c1).mul_add(c2, c0); +// let d2 = c2 * c0 - c1 * c1; +// // d is called "Discriminant" +// let d = 4. * d0 * d2 - d1 * d1; +// // de is called "Depressed.x", Depressed.y = d0 +// let de = (-2. * c2).mul_add(d0, d1); +// if d < 0. { +// let sq = (-0.25 * d).sqrt(); +// let r = -0.5 * de; +// let t1 = (r + sq).cbrt() + (r - sq).cbrt(); +// [Some(t1 - c2), None, None] +// } else if d == 0. { +// let t1 = (-d0).sqrt().copysign(de); +// [Some(t1 - c2), Some(-2. * t1 - c2).filter(|&a| a != t1 - c2), None] +// } else { +// let th = d.sqrt().atan2(-de) * ONETHIRD; +// // (th_cos, th_sin) is called "CubicRoot" +// let (th_sin, th_cos) = th.sin_cos(); +// // (r0, r1, r2) is called "Root" +// let r0 = th_cos; +// let ss3 = th_sin * 3_f64.sqrt(); +// let r1 = 0.5 * (-th_cos + ss3); +// let r2 = 0.5 * (-th_cos - ss3); +// let t = 2. * (-d0).sqrt(); +// [Some(t.mul_add(r0, -c2)), Some(t.mul_add(r1, -c2)), Some(t.mul_add(r2, -c2))] +// } +// } +// } + +// /// Determines if two rectangles have any overlap. The rectangles are represented by a pair of coordinates that designate the top left and bottom right corners (in a graphical coordinate system). +// pub fn do_rectangles_overlap(rectangle1: [DVec2; 2], rectangle2: [DVec2; 2]) -> bool { +// let [bottom_left1, top_right1] = rectangle1; +// let [bottom_left2, top_right2] = rectangle2; + +// top_right1.x >= bottom_left2.x && top_right2.x >= bottom_left1.x && top_right2.y >= bottom_left1.y && top_right1.y >= bottom_left2.y +// } + +// /// Determines if a point is completely inside a rectangle, which is represented as a pair of coordinates [top-left, bottom-right]. +// pub fn is_point_inside_rectangle(rect: [DVec2; 2], point: DVec2) -> bool { +// let [top_left, bottom_right] = rect; +// point.x > top_left.x && point.x < bottom_right.x && point.y > top_left.y && point.y < bottom_right.y +// } + +// /// Determines if the inner rectangle is completely inside the outer rectangle. The rectangles are represented as pairs of coordinates [top-left, bottom-right]. +// pub fn is_rectangle_inside_other(inner: [DVec2; 2], outer: [DVec2; 2]) -> bool { +// is_point_inside_rectangle(outer, inner[0]) && is_point_inside_rectangle(outer, inner[1]) +// } + +// /// Returns the intersection of two lines. The lines are given by a point on the line and its slope (represented by a vector). +// pub fn line_intersection(point1: DVec2, point1_slope_vector: DVec2, point2: DVec2, point2_slope_vector: DVec2) -> DVec2 { +// assert!(point1_slope_vector.normalize() != point2_slope_vector.normalize()); + +// // Find the intersection when the first line is vertical +// if f64_compare(point1_slope_vector.x, 0., MAX_ABSOLUTE_DIFFERENCE) { +// let m2 = point2_slope_vector.y / point2_slope_vector.x; +// let b2 = point2.y - m2 * point2.x; +// DVec2::new(point1.x, point1.x * m2 + b2) +// } +// // Find the intersection when the second line is vertical +// else if f64_compare(point2_slope_vector.x, 0., MAX_ABSOLUTE_DIFFERENCE) { +// let m1 = point1_slope_vector.y / point1_slope_vector.x; +// let b1 = point1.y - m1 * point1.x; +// DVec2::new(point2.x, point2.x * m1 + b1) +// } +// // Find the intersection where neither line is vertical +// else { +// let m1 = point1_slope_vector.y / point1_slope_vector.x; +// let b1 = point1.y - m1 * point1.x; +// let m2 = point2_slope_vector.y / point2_slope_vector.x; +// let b2 = point2.y - m2 * point2.x; +// let intersection_x = (b2 - b1) / (m1 - m2); +// DVec2::new(intersection_x, intersection_x * m1 + b1) +// } +// } + +// /// Check if 3 points are collinear. +// pub fn are_points_collinear(p1: DVec2, p2: DVec2, p3: DVec2) -> bool { +// let matrix = DMat2::from_cols(p1 - p2, p2 - p3); +// f64_compare(matrix.determinant() / 2., 0., MAX_ABSOLUTE_DIFFERENCE) +// } + +// /// Compute the center of the circle that passes through all three provided points. The provided points cannot be collinear. +// pub fn compute_circle_center_from_points(p1: DVec2, p2: DVec2, p3: DVec2) -> Option { +// if are_points_collinear(p1, p2, p3) { +// return None; +// } + +// let midpoint_a = p1.lerp(p2, 0.5); +// let midpoint_b = p2.lerp(p3, 0.5); +// let midpoint_c = p3.lerp(p1, 0.5); + +// let tangent_a = (p1 - p2).perp(); +// let tangent_b = (p2 - p3).perp(); +// let tangent_c = (p3 - p1).perp(); + +// let intersect_a_b = line_intersection(midpoint_a, tangent_a, midpoint_b, tangent_b); +// let intersect_b_c = line_intersection(midpoint_b, tangent_b, midpoint_c, tangent_c); +// let intersect_c_a = line_intersection(midpoint_c, tangent_c, midpoint_a, tangent_a); + +// Some((intersect_a_b + intersect_b_c + intersect_c_a) / 3.) +// } + +// /// Compare two `f64` numbers with a provided max absolute value difference. +// pub fn f64_compare(a: f64, b: f64, max_abs_diff: f64) -> bool { +// (a - b).abs() < max_abs_diff +// } + +// /// Determine if an `f64` number is within a given range by using a max absolute value difference comparison. +// pub fn f64_approximately_in_range(value: f64, min: f64, max: f64, max_abs_diff: f64) -> bool { +// (min..=max).contains(&value) || f64_compare(value, min, max_abs_diff) || f64_compare(value, max, max_abs_diff) +// } + +// /// Compare the two values in a `DVec2` independently with a provided max absolute value difference. +// pub fn dvec2_compare(a: DVec2, b: DVec2, max_abs_diff: f64) -> BVec2 { +// BVec2::new((a.x - b.x).abs() < max_abs_diff, (a.y - b.y).abs() < max_abs_diff) +// } + +// /// Determine if the values in a `DVec2` are within a given range independently by using a max absolute value difference comparison. +// pub fn dvec2_approximately_in_range(point: DVec2, min_corner: DVec2, max_corner: DVec2, max_abs_diff: f64) -> BVec2 { +// (point.cmpge(min_corner) & point.cmple(max_corner)) | dvec2_compare(point, min_corner, max_abs_diff) | dvec2_compare(point, max_corner, max_abs_diff) +// } + +// /// Calculate a new position for a point given its original position, a unit vector in the desired direction, and a distance to move it by. +// pub fn scale_point_from_direction_vector(point: DVec2, direction_unit_vector: DVec2, should_flip_direction: bool, distance: f64) -> DVec2 { +// let should_reverse_factor = if should_flip_direction { -1. } else { 1. }; +// point + distance * direction_unit_vector * should_reverse_factor +// } + +// /// Scale a point by a given distance with respect to the provided origin. +// pub fn scale_point_from_origin(point: DVec2, origin: DVec2, should_flip_direction: bool, distance: f64) -> DVec2 { +// scale_point_from_direction_vector(point, (origin - point).normalize(), should_flip_direction, distance) +// } + +// /// Computes the necessary details 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, +// ) -> (DVec2, ManipulatorGroup, DVec2) { +// 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 }; + +// ( +// left - (left - center).perp() * handle_offset_factor, +// ManipulatorGroup::new( +// arc_point, +// Some(arc_point + center_to_arc_point.perp() * handle_offset_factor), +// Some(arc_point - center_to_arc_point.perp() * handle_offset_factor), +// ), +// right + (right - center).perp() * handle_offset_factor, +// ) +// } + +// pub fn format_point(svg: &mut String, prefix: &str, x: f64, y: f64) -> std::fmt::Result { +// write!(svg, "{prefix}{:.6}", x)?; +// let trimmed_length = svg.trim_end_matches('0').trim_end_matches('.').len(); +// svg.truncate(trimmed_length); + +// write!(svg, ",{:.6}", y)?; +// let trimmed_length = svg.trim_end_matches('0').trim_end_matches('.').len(); +// svg.truncate(trimmed_length); + +// Ok(()) +// } + +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::consts::MAX_ABSOLUTE_DIFFERENCE; +// use crate::{Bezier, EmptyId}; + +// /// Compare vectors of `f64`s with a provided max absolute value difference. +// fn f64_compare_vector(a: Vec, b: Vec, max_abs_diff: f64) -> bool { +// a.len() == b.len() && a.into_iter().zip(b).all(|(a, b)| f64_compare(a, b, max_abs_diff)) +// } + +// fn collect_roots(mut roots: [Option; 3]) -> Vec { +// roots.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap()); +// roots.into_iter().flatten().collect() +// } + +// #[test] +// fn test_solve_linear() { +// // Line that is on the x-axis +// assert!(collect_roots(solve_linear(0., 0.)).is_empty()); +// // Line that is parallel to but not on the x-axis +// assert!(collect_roots(solve_linear(0., 1.)).is_empty()); +// // Line with a non-zero slope +// assert!(collect_roots(solve_linear(2., -8.)) == vec![4.]); +// } + +// #[test] +// fn test_solve_cubic() { +// // discriminant == 0 +// let roots1 = collect_roots(solve_cubic(1., 0., 0., 0.)); +// assert!(roots1 == vec![0.]); + +// let roots2 = collect_roots(solve_cubic(1., 3., 0., -4.)); +// assert!(roots2 == vec![-2., 1.]); + +// // p == 0 +// let roots3 = collect_roots(solve_cubic(1., 0., 0., -1.)); +// assert!(roots3 == vec![1.]); + +// // discriminant > 0 +// let roots4 = collect_roots(solve_cubic(1., 3., 0., 2.)); +// assert!(f64_compare_vector(roots4, vec![-3.196], MAX_ABSOLUTE_DIFFERENCE)); + +// // discriminant < 0 +// let roots5 = collect_roots(solve_cubic(1., 3., 0., -1.)); +// assert!(f64_compare_vector(roots5, vec![-2.879, -0.653, 0.532], MAX_ABSOLUTE_DIFFERENCE)); + +// // quadratic +// let roots6 = collect_roots(solve_cubic(0., 3., 0., -3.)); +// assert!(roots6 == vec![-1., 1.]); + +// // linear +// let roots7 = collect_roots(solve_cubic(0., 0., 1., -1.)); +// assert!(roots7 == vec![1.]); +// } + +// #[test] +// fn test_do_rectangles_overlap() { +// // Rectangles overlap +// assert!(do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(20., 20.)], [DVec2::new(10., 10.), DVec2::new(30., 20.)])); +// // Rectangles share a side +// assert!(do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(10., 10.), DVec2::new(30., 30.)])); +// // Rectangle inside the other +// assert!(do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(2., 2.), DVec2::new(6., 4.)])); +// // No overlap, rectangles are beside each other +// assert!(!do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(20., 0.), DVec2::new(30., 10.)])); +// // No overlap, rectangles are above and below each other +// assert!(!do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(0., 20.), DVec2::new(20., 30.)])); +// } + +// #[test] +// fn test_is_rectangle_inside_other() { +// assert!(!is_rectangle_inside_other([DVec2::new(10., 10.), DVec2::new(50., 50.)], [DVec2::new(10., 10.), DVec2::new(50., 50.)])); +// assert!(is_rectangle_inside_other( +// [DVec2::new(10.01, 10.01), DVec2::new(49., 49.)], +// [DVec2::new(10., 10.), DVec2::new(50., 50.)] +// )); +// assert!(!is_rectangle_inside_other([DVec2::new(5., 5.), DVec2::new(50., 9.99)], [DVec2::new(10., 10.), DVec2::new(50., 50.)])); +// } + +// #[test] +// fn test_find_intersection() { +// // y = 2x + 10 +// // y = 5x + 4 +// // intersect at (2, 14) + +// let start1 = DVec2::new(0., 10.); +// let end1 = DVec2::new(0., 4.); +// let start_direction1 = DVec2::new(1., 2.); +// let end_direction1 = DVec2::new(1., 5.); +// assert!(line_intersection(start1, start_direction1, end1, end_direction1) == DVec2::new(2., 14.)); + +// // y = x +// // y = -x + 8 +// // intersect at (4, 4) + +// let start2 = DVec2::new(0., 0.); +// let end2 = DVec2::new(8., 0.); +// let start_direction2 = DVec2::new(1., 1.); +// let end_direction2 = DVec2::new(1., -1.); +// assert!(line_intersection(start2, start_direction2, end2, end_direction2) == DVec2::new(4., 4.)); +// } + +// #[test] +// fn test_are_points_collinear() { +// assert!(are_points_collinear(DVec2::new(2., 4.), DVec2::new(6., 8.), DVec2::new(4., 6.))); +// assert!(!are_points_collinear(DVec2::new(1., 4.), DVec2::new(6., 8.), DVec2::new(4., 6.))); +// } + +// #[test] +// fn test_compute_circle_center_from_points() { +// // 3/4 of unit circle +// let center1 = compute_circle_center_from_points(DVec2::new(0., 1.), DVec2::new(-1., 0.), DVec2::new(1., 0.)); +// assert_eq!(center1.unwrap(), DVec2::new(0., 0.)); +// // 1/4 of unit circle +// let center2 = compute_circle_center_from_points(DVec2::new(-1., 0.), DVec2::new(0., 1.), DVec2::new(1., 0.)); +// assert_eq!(center2.unwrap(), DVec2::new(0., 0.)); +// } +// } diff --git a/node-graph/gcore/src/text/to_path.rs b/node-graph/gcore/src/text/to_path.rs index f1fee2b432..d1b9b6f188 100644 --- a/node-graph/gcore/src/text/to_path.rs +++ b/node-graph/gcore/src/text/to_path.rs @@ -1,7 +1,7 @@ use super::TextAlign; +use crate::subpath::{ManipulatorGroup, Subpath}; use crate::table::{Table, TableRow}; use crate::vector::{PointId, Vector}; -use bezier_rs::{ManipulatorGroup, Subpath}; use core::cell::RefCell; use glam::{DAffine2, DVec2}; use parley::fontique::Blob; diff --git a/node-graph/gcore/src/vector/algorithms/instance.rs b/node-graph/gcore/src/vector/algorithms/instance.rs index cf2e4b7399..7d06a82ab3 100644 --- a/node-graph/gcore/src/vector/algorithms/instance.rs +++ b/node-graph/gcore/src/vector/algorithms/instance.rs @@ -99,8 +99,8 @@ mod test { use super::*; use crate::Node; use crate::extract_xy::{ExtractXyNode, XY}; + use crate::subpath::Subpath; use crate::vector::Vector; - use bezier_rs::Subpath; use glam::DVec2; use std::pin::Pin; diff --git a/node-graph/gcore/src/vector/click_target.rs b/node-graph/gcore/src/vector/click_target.rs index 4fd9faa905..31fd020988 100644 --- a/node-graph/gcore/src/vector/click_target.rs +++ b/node-graph/gcore/src/vector/click_target.rs @@ -1,8 +1,13 @@ use crate::math::math_ext::QuadExt; use crate::math::quad::Quad; +use crate::subpath::Subpath; use crate::vector::PointId; -use bezier_rs::Subpath; use glam::{DAffine2, DMat2, DVec2}; +use kurbo::{Affine, ParamCurve, PathSeg, Point, Shape}; + +use super::algorithms::intersection::filtered_segment_intersections; +use super::misc::dvec2_to_point; +use crate::vector::misc::point_to_dvec2; #[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct FreePoint { @@ -99,7 +104,7 @@ impl ClickTarget { } /// Does the click target intersect the path - pub fn intersect_path>(&self, mut bezier_iter: impl FnMut() -> It, layer_transform: DAffine2) -> bool { + pub fn intersect_path>(&self, mut bezier_iter: impl FnMut() -> It, layer_transform: DAffine2) -> bool { // Check if the matrix is not invertible let mut layer_transform = layer_transform; if layer_transform.matrix2.determinant().abs() <= f64::EPSILON { @@ -107,25 +112,25 @@ impl ClickTarget { } let inverse = layer_transform.inverse(); - let mut bezier_iter = || bezier_iter().map(|bezier| bezier.apply_transformation(|point| inverse.transform_point2(point))); + let mut bezier_iter = || bezier_iter().map(|bezier| Affine::new(inverse.to_cols_array()) * bezier); match self.target_type() { ClickTargetType::Subpath(subpath) => { // Check if outlines intersect - let outline_intersects = |path_segment: bezier_rs::Bezier| bezier_iter().any(|line| !path_segment.intersections(&line, None, None).is_empty()); + let outline_intersects = |path_segment: PathSeg| bezier_iter().any(|line| !filtered_segment_intersections(path_segment, line, None, None).is_empty()); if subpath.iter().any(outline_intersects) { return true; } // Check if selection is entirely within the shape - if subpath.closed() && bezier_iter().next().is_some_and(|bezier| subpath.contains_point(bezier.start)) { + if subpath.closed() && bezier_iter().next().is_some_and(|bezier| subpath.contains_point(point_to_dvec2(bezier.start()))) { return true; } // Check if shape is entirely within selection let any_point_from_subpath = subpath.manipulator_groups().first().map(|manipulators| manipulators.anchor); - any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(shape_point)).sum::() != 0) + any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(Point::new(shape_point.x, shape_point.y))).sum::() != 0) } - ClickTargetType::FreePoint(point) => bezier_iter().map(|bezier: bezier_rs::Bezier| bezier.winding(point.position)).sum::() != 0, + ClickTargetType::FreePoint(point) => bezier_iter().map(|bezier: PathSeg| bezier.winding(dvec2_to_point(point.position))).sum::() != 0, } } @@ -144,7 +149,7 @@ impl ClickTarget { // Allows for selecting lines // TODO: actual intersection of stroke let inflated_quad = Quad::from_box(target_bounds); - self.intersect_path(|| inflated_quad.bezier_lines(), layer_transform) + self.intersect_path(|| inflated_quad.to_lines(), layer_transform) } /// Does the click target intersect the point (not accounting for stroke size) diff --git a/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs index dee36668e5..1e49607b21 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/generator_nodes.rs @@ -2,10 +2,10 @@ use super::misc::{ArcType, AsU64, GridType}; use super::{PointId, SegmentId, StrokeId}; use crate::Ctx; use crate::registry::types::{Angle, PixelSize}; +use crate::subpath; use crate::table::Table; use crate::vector::Vector; use crate::vector::misc::HandleId; -use bezier_rs::Subpath; use glam::DVec2; trait CornerRadius { @@ -14,7 +14,7 @@ trait CornerRadius { impl CornerRadius for f64 { fn generate(self, size: DVec2, clamped: bool) -> Table { let clamped_radius = if clamped { self.clamp(0., size.x.min(size.y).max(0.) / 2.) } else { self }; - Table::new_from_element(Vector::from_subpath(Subpath::new_rounded_rect(size / -2., size / 2., [clamped_radius; 4]))) + Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_rounded_rect(size / -2., size / 2., [clamped_radius; 4]))) } } impl CornerRadius for [f64; 4] { @@ -34,7 +34,7 @@ impl CornerRadius for [f64; 4] { } else { self }; - Table::new_from_element(Vector::from_subpath(Subpath::new_rounded_rect(size / -2., size / 2., clamped_radius))) + Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_rounded_rect(size / -2., size / 2., clamped_radius))) } } @@ -47,7 +47,7 @@ fn circle( radius: f64, ) -> Table { let radius = radius.abs(); - Table::new_from_element(Vector::from_subpath(Subpath::new_ellipse(DVec2::splat(-radius), DVec2::splat(radius)))) + Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_ellipse(DVec2::splat(-radius), DVec2::splat(radius)))) } #[node_macro::node(category("Vector: Shape"))] @@ -63,14 +63,14 @@ fn arc( sweep_angle: Angle, arc_type: ArcType, ) -> Table { - Table::new_from_element(Vector::from_subpath(Subpath::new_arc( + Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_arc( radius, start_angle / 360. * std::f64::consts::TAU, sweep_angle / 360. * std::f64::consts::TAU, match arc_type { - ArcType::Open => bezier_rs::ArcType::Open, - ArcType::Closed => bezier_rs::ArcType::Closed, - ArcType::PieSlice => bezier_rs::ArcType::PieSlice, + ArcType::Open => subpath::ArcType::Open, + ArcType::Closed => subpath::ArcType::Closed, + ArcType::PieSlice => subpath::ArcType::PieSlice, }, ))) } @@ -90,7 +90,7 @@ fn ellipse( let corner1 = -radius; let corner2 = radius; - let mut ellipse = Vector::from_subpath(Subpath::new_ellipse(corner1, corner2)); + let mut ellipse = Vector::from_subpath(subpath::Subpath::new_ellipse(corner1, corner2)); let len = ellipse.segment_domain.ids().len(); for i in 0..len { @@ -133,7 +133,7 @@ fn regular_polygon( ) -> Table { let points = sides.as_u64(); let radius: f64 = radius * 2.; - Table::new_from_element(Vector::from_subpath(Subpath::new_regular_polygon(DVec2::splat(-radius), points, radius))) + Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_regular_polygon(DVec2::splat(-radius), points, radius))) } #[node_macro::node(category("Vector: Shape"))] @@ -155,12 +155,12 @@ fn star( let diameter: f64 = radius_1 * 2.; let inner_diameter = radius_2 * 2.; - Table::new_from_element(Vector::from_subpath(Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter))) + Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter))) } #[node_macro::node(category("Vector: Shape"))] fn line(_: impl Ctx, _primary: (), #[default(0., 0.)] start: PixelSize, #[default(100., 100.)] end: PixelSize) -> Table { - Table::new_from_element(Vector::from_subpath(Subpath::new_line(start, end))) + Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_line(start, end))) } trait GridSpacing { @@ -212,7 +212,7 @@ fn grid( if let Some(other_index) = to_index { vector .segment_domain - .push(segment_id.next_id(), other_index, current_index, bezier_rs::BezierHandles::Linear, StrokeId::ZERO); + .push(segment_id.next_id(), other_index, current_index, subpath::BezierHandles::Linear, StrokeId::ZERO); } }; @@ -249,7 +249,7 @@ fn grid( if let Some(other_index) = to_index { vector .segment_domain - .push(segment_id.next_id(), other_index, current_index, bezier_rs::BezierHandles::Linear, StrokeId::ZERO); + .push(segment_id.next_id(), other_index, current_index, subpath::BezierHandles::Linear, StrokeId::ZERO); } }; @@ -289,7 +289,7 @@ mod tests { assert_eq!(grid.iter().next().unwrap().element.point_domain.ids().len(), 5 * 5); assert_eq!(grid.iter().next().unwrap().element.segment_bezier_iter().count(), 4 * 5 + 4 * 9); for (_, bezier, _, _) in grid.iter().next().unwrap().element.segment_bezier_iter() { - assert_eq!(bezier.handles, bezier_rs::BezierHandles::Linear); + assert_eq!(bezier.handles, subpath::BezierHandles::Linear); assert!( ((bezier.start - bezier.end).length() - 10.).abs() < 1e-5, "Length of {} should be 10", @@ -304,7 +304,7 @@ mod tests { assert_eq!(grid.iter().next().unwrap().element.point_domain.ids().len(), 5 * 5); assert_eq!(grid.iter().next().unwrap().element.segment_bezier_iter().count(), 4 * 5 + 4 * 9); for (_, bezier, _, _) in grid.iter().next().unwrap().element.segment_bezier_iter() { - assert_eq!(bezier.handles, bezier_rs::BezierHandles::Linear); + assert_eq!(bezier.handles, subpath::BezierHandles::Linear); let vector = bezier.start - bezier.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) diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index 8c60ae9354..eea0c81777 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -1,7 +1,7 @@ use super::PointId; use super::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE; +use crate::subpath::{BezierHandles, ManipulatorGroup}; use crate::vector::{SegmentId, Vector}; -use bezier_rs::{BezierHandles, ManipulatorGroup}; use dyn_any::DynAny; use glam::DVec2; use kurbo::{BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez}; @@ -115,18 +115,18 @@ pub fn segment_to_handles(segment: &PathSeg) -> BezierHandles { pub fn handles_to_segment(start: DVec2, handles: BezierHandles, end: DVec2) -> PathSeg { match handles { - bezier_rs::BezierHandles::Linear => { + BezierHandles::Linear => { let p0 = dvec2_to_point(start); let p1 = dvec2_to_point(end); PathSeg::Line(Line::new(p0, p1)) } - bezier_rs::BezierHandles::Quadratic { handle } => { + BezierHandles::Quadratic { handle } => { let p0 = dvec2_to_point(start); let p1 = dvec2_to_point(handle); let p2 = dvec2_to_point(end); PathSeg::Quad(QuadBez::new(p0, p1, p2)) } - bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => { + BezierHandles::Cubic { handle_start, handle_end } => { let p0 = dvec2_to_point(start); let p1 = dvec2_to_point(handle_start); let p2 = dvec2_to_point(handle_end); @@ -220,6 +220,15 @@ pub fn get_segment_points(segment: PathSeg) -> Vec { } } +pub fn transform_pathseg(segment: PathSeg, transform: impl Fn(DVec2) -> DVec2) -> PathSeg { + let transform = |point: Point| dvec2_to_point(transform(point_to_dvec2(point))); + match segment { + PathSeg::Line(line) => PathSeg::Line(Line::new(transform(line.p0), transform(line.p1))), + PathSeg::Quad(quad_bez) => PathSeg::Quad(QuadBez::new(transform(quad_bez.p0), transform(quad_bez.p1), transform(quad_bez.p2))), + PathSeg::Cubic(cubic_bez) => PathSeg::Cubic(CubicBez::new(transform(cubic_bez.p0), transform(cubic_bez.p0), transform(cubic_bez.p0), transform(cubic_bez.p0))), + } +} + /// Returns true if the corresponding points of the two [`PathSeg`]s are within the provided absolute value difference from each other. pub fn pathseg_abs_diff_eq(seg1: PathSeg, seg2: PathSeg, max_abs_diff: f64) -> bool { let seg1 = if is_linear(seg1) { PathSeg::Line(Line::new(seg1.start(), seg1.end())) } else { seg1 }; diff --git a/node-graph/gcore/src/vector/mod.rs b/node-graph/gcore/src/vector/mod.rs index c27bb9c9c2..1b920c94a5 100644 --- a/node-graph/gcore/src/vector/mod.rs +++ b/node-graph/gcore/src/vector/mod.rs @@ -9,7 +9,6 @@ mod vector_modification; mod vector_nodes; mod vector_types; -pub use bezier_rs; pub use reference_point::*; pub use style::PathStyle; pub use vector_nodes::*; diff --git a/node-graph/gcore/src/vector/vector_attributes.rs b/node-graph/gcore/src/vector/vector_attributes.rs index 8edc97d766..461e3a1abf 100644 --- a/node-graph/gcore/src/vector/vector_attributes.rs +++ b/node-graph/gcore/src/vector/vector_attributes.rs @@ -1,6 +1,6 @@ +use crate::subpath::{Bezier, BezierHandles, Identifier, ManipulatorGroup, Subpath}; use crate::vector::misc::{HandleId, dvec2_to_point}; use crate::vector::vector_types::Vector; -use bezier_rs::{BezierHandles, ManipulatorGroup}; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; use kurbo::{CubicBez, Line, PathSeg, QuadBez}; @@ -442,11 +442,7 @@ impl SegmentDomain { zip(ids, zip(start_point, zip(end_point, handles))).map(|(id, (start_point, (end_point, handles)))| (id, start_point, end_point, handles)) } - pub(crate) fn pair_handles_and_points_mut_by_index( - &mut self, - index1: usize, - index2: usize, - ) -> (&mut bezier_rs::BezierHandles, &mut usize, &mut usize, &mut bezier_rs::BezierHandles, &mut usize, &mut usize) { + pub(crate) fn pair_handles_and_points_mut_by_index(&mut self, index1: usize, index2: usize) -> (&mut BezierHandles, &mut usize, &mut usize, &mut BezierHandles, &mut usize, &mut usize) { // Use split_at_mut to avoid multiple mutable borrows of the same slice let (handles_first, handles_second) = self.handles.split_at_mut(index2.max(index1)); let (start_first, start_second) = self.start_point.split_at_mut(index2.max(index1)); @@ -686,25 +682,25 @@ impl Vector { } } - /// Construct a [`bezier_rs::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_rs::Bezier { + /// 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_rs::Bezier { start, end, handles } + Bezier { start, end, handles } } - /// Tries to convert a segment with the specified id to a [`bezier_rs::Bezier`], returning None if the id is invalid. - pub fn segment_from_id(&self, id: SegmentId) -> Option { + /// 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 { 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_rs::Bezier`], returning None if the id is invalid. - pub fn segment_points_from_id(&self, id: SegmentId) -> Option<(PointId, PointId, bezier_rs::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)> { 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_rs::Bezier`]. - pub fn segment_points_from_index(&self, index: usize) -> (PointId, PointId, bezier_rs::Bezier) { + /// 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) { let start = self.segment_domain.start_point[index]; let end = self.segment_domain.end_point[index]; let start_id = self.point_domain.ids()[start]; @@ -712,7 +708,7 @@ impl Vector { (start_id, end_id, self.segment_to_bezier_with_index(start, end, self.segment_domain.handles[index])) } - /// Iterator over all of the [`bezier_rs::Bezier`] following the order that they are stored in the segment domain, skipping invalid segments. + /// Iterator over all of the [`Bezier`] following the order that they are stored in the segment domain, skipping invalid segments. pub fn segment_iter(&self) -> impl Iterator { 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]); @@ -725,8 +721,8 @@ impl Vector { .map(to_segment) } - /// Iterator over all of the [`bezier_rs::Bezier`] following the order that they are stored in the segment domain, skipping invalid segments. - pub fn segment_bezier_iter(&self) -> impl Iterator + '_ { + /// 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 + '_ { 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 @@ -808,8 +804,8 @@ impl Vector { } } - /// Construct a [`bezier_rs::Bezier`] curve 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) -> Option> { + /// Construct a [`Bezier`] curve 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) -> Option> { let mut first_point = None; let mut manipulators_list = Vec::new(); let mut last: Option<(usize, BezierHandles)> = None; @@ -842,10 +838,10 @@ impl Vector { } } - Some(bezier_rs::Subpath::new(manipulators_list, closed)) + Some(Subpath::new(manipulators_list, closed)) } - /// Construct a [`bezier_rs::Bezier`] curve for each region, skipping invalid regions. + /// Construct a [`Bezier`] curve for each region, skipping invalid regions. pub fn region_manipulator_groups(&self) -> impl Iterator>)> + '_ { self.region_domain .id @@ -903,12 +899,12 @@ impl Vector { } } - /// Construct a [`bezier_rs::Bezier`] curve for stroke. - pub fn stroke_bezier_paths(&self) -> impl Iterator> { - self.build_stroke_path_iter().map(|(manipulators_list, closed)| bezier_rs::Subpath::new(manipulators_list, closed)) + /// Construct a [`Bezier`] curve for stroke. + pub fn stroke_bezier_paths(&self) -> impl Iterator> { + self.build_stroke_path_iter().map(|(manipulators_list, closed)| Subpath::new(manipulators_list, closed)) } - /// Construct and return an iterator of Vec of `(bezier_rs::ManipulatorGroup], bool)` for stroke. + /// Construct and return an iterator of Vec of `(ManipulatorGroup], bool)` for stroke. /// The boolean in the tuple indicates if the path is closed. pub fn stroke_manipulator_groups(&self) -> impl Iterator>, bool)> { self.build_stroke_path_iter() @@ -1094,7 +1090,7 @@ impl Iterator for StrokePathIter<'_> { } } -impl bezier_rs::Identifier for PointId { +impl Identifier for PointId { fn new() -> Self { Self::generate() } diff --git a/node-graph/gcore/src/vector/vector_modification.rs b/node-graph/gcore/src/vector/vector_modification.rs index a841f9695a..6a10588dd4 100644 --- a/node-graph/gcore/src/vector/vector_modification.rs +++ b/node-graph/gcore/src/vector/vector_modification.rs @@ -1,9 +1,9 @@ use super::*; use crate::Ctx; +use crate::subpath::BezierHandles; use crate::table::{Table, TableRow}; use crate::uuid::{NodeId, generate_uuid}; use crate::vector::misc::{HandleId, HandleType, point_to_dvec2}; -use bezier_rs::BezierHandles; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; use kurbo::{BezPath, PathEl, Point}; @@ -684,14 +684,15 @@ impl HandleExt for HandleId { #[cfg(test)] mod tests { + use kurbo::{PathSeg, QuadBez}; + use super::*; + use crate::subpath::{Bezier, Subpath}; + #[test] fn modify_new() { - let vector = Vector::from_subpaths( - [bezier_rs::Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE), bezier_rs::Subpath::new_rect(DVec2::NEG_ONE, DVec2::ZERO)], - false, - ); + let vector = Vector::from_subpaths([Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE), Subpath::new_rect(DVec2::NEG_ONE, DVec2::ZERO)], false); let modify = VectorModification::create_from_vector(&vector); @@ -702,14 +703,13 @@ mod tests { #[test] fn modify_existing() { - use bezier_rs::{Bezier, Subpath}; let subpaths = [ Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE), Subpath::new_rect(DVec2::NEG_ONE, DVec2::ZERO), Subpath::from_beziers( &[ - Bezier::from_quadratic_dvec2(DVec2::new(0., 0.), DVec2::new(5., 10.), DVec2::new(10., 0.)), - Bezier::from_quadratic_dvec2(DVec2::new(10., 0.), DVec2::new(15., 10.), DVec2::new(20., 0.)), + PathSeg::Quad(QuadBez::new(Point::new(0., 0.), Point::new(5., 10.), Point::new(10., 0.))), + PathSeg::Quad(QuadBez::new(Point::new(10., 0.), Point::new(15., 10.), Point::new(20., 0.))), ], false, ), diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index e130d7db25..81d1e63b44 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -7,6 +7,7 @@ use super::{PointId, SegmentDomain, SegmentId, StrokeId, Vector, VectorExt}; use crate::bounds::BoundingBox; use crate::raster_types::{CPU, GPU, Raster}; use crate::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier, Percentage, PixelLength, PixelSize, SeedValue}; +use crate::subpath::{BezierHandles, ManipulatorGroup}; use crate::table::{Table, TableRow, TableRowMut}; use crate::transform::{Footprint, ReferencePoint, Transform}; use crate::vector::PointDomain; @@ -17,7 +18,6 @@ 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, Graphic, OwnedContextImpl}; -use bezier_rs::ManipulatorGroup; use core::f64::consts::PI; use core::hash::{Hash, Hasher}; use glam::{DAffine2, DVec2}; @@ -840,11 +840,11 @@ async fn points_to_polyline(_: impl Ctx, mut points: Table, #[default(tr if points_count > 2 { (0..points_count - 1).for_each(|i| { - segment_domain.push(SegmentId::generate(), i, i + 1, bezier_rs::BezierHandles::Linear, StrokeId::generate()); + segment_domain.push(SegmentId::generate(), i, i + 1, BezierHandles::Linear, StrokeId::generate()); }); if closed { - segment_domain.push(SegmentId::generate(), points_count - 1, 0, bezier_rs::BezierHandles::Linear, StrokeId::generate()); + segment_domain.push(SegmentId::generate(), points_count - 1, 0, BezierHandles::Linear, StrokeId::generate()); row.element .region_domain @@ -1364,7 +1364,7 @@ async fn spline(_: impl Ctx, content: Table) -> Table { let handle_start = first_handles[i]; let handle_end = positions[next_index] * 2. - first_handles[next_index]; - let handles = bezier_rs::BezierHandles::Cubic { handle_start, handle_end }; + let handles = BezierHandles::Cubic { handle_start, handle_end }; segment_domain.push(SegmentId::generate(), start_index, end_index, handles, stroke_id); } @@ -1418,14 +1418,14 @@ async fn jitter_points( } match handles { - bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => { + BezierHandles::Cubic { handle_start, handle_end } => { *handle_start += start_delta; *handle_end += end_delta; } - bezier_rs::BezierHandles::Quadratic { handle } => { + BezierHandles::Quadratic { handle } => { *handle = row.transform.transform_point2(*handle) + (start_delta + end_delta) / 2.; } - bezier_rs::BezierHandles::Linear => {} + BezierHandles::Linear => {} } } @@ -1859,7 +1859,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve let mut next_id = vector.segment_domain.next_id(); for &[start, end] in new_segments { - let handles = bezier_rs::BezierHandles::Linear; + let handles = BezierHandles::Linear; vector.segment_domain.push(next_id.next_id(), start, end, handles, StrokeId::ZERO); } } diff --git a/node-graph/gcore/src/vector/vector_types.rs b/node-graph/gcore/src/vector/vector_types.rs index bd63d00af2..ffdd2173bd 100644 --- a/node-graph/gcore/src/vector/vector_types.rs +++ b/node-graph/gcore/src/vector/vector_types.rs @@ -4,12 +4,12 @@ pub use super::vector_attributes::*; pub use super::vector_modification::*; use crate::bounds::BoundingBox; use crate::math::quad::Quad; +use crate::subpath::{BezierHandles, ManipulatorGroup, Subpath}; use crate::table::Table; use crate::transform::Transform; use crate::vector::click_target::{ClickTargetType, FreePoint}; use crate::vector::misc::{HandleId, ManipulatorPointId}; use crate::{AlphaBlending, Color, Graphic}; -use bezier_rs::{BezierHandles, ManipulatorGroup}; use core::borrow::Borrow; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; @@ -60,15 +60,15 @@ impl std::hash::Hash for Vector { impl Vector { /// Add a Bezier-rs subpath to this path. - pub fn append_subpath(&mut self, subpath: impl Borrow>, preserve_id: bool) { - let subpath: &bezier_rs::Subpath = subpath.borrow(); + pub fn append_subpath(&mut self, subpath: impl Borrow>, preserve_id: bool) { + let subpath: &Subpath = subpath.borrow(); let stroke_id = StrokeId::ZERO; let mut point_id = self.point_domain.next_id(); let handles = |a: &ManipulatorGroup<_>, b: &ManipulatorGroup<_>| match (a.out_handle, b.in_handle) { - (None, None) => bezier_rs::BezierHandles::Linear, - (Some(handle), None) | (None, Some(handle)) => bezier_rs::BezierHandles::Quadratic { handle }, - (Some(handle_start), Some(handle_end)) => bezier_rs::BezierHandles::Cubic { handle_start, handle_end }, + (None, None) => BezierHandles::Linear, + (Some(handle), None) | (None, Some(handle)) => BezierHandles::Quadratic { handle }, + (Some(handle_start), Some(handle_end)) => BezierHandles::Cubic { handle_start, handle_end }, }; let [mut first_seg, mut last_seg] = [None, None]; let mut segment_id = self.segment_domain.next_id(); @@ -132,7 +132,7 @@ impl Vector { } /// Construct some new vector path from a single Bezier-rs subpath with an identity transform and black fill. - pub fn from_subpath(subpath: impl Borrow>) -> Self { + pub fn from_subpath(subpath: impl Borrow>) -> Self { Self::from_subpaths([subpath], false) } @@ -144,7 +144,7 @@ impl Vector { } /// Construct some new vector path from Bezier-rs subpaths with an identity transform and black fill. - pub fn from_subpaths(subpaths: impl IntoIterator>>, preserve_id: bool) -> Self { + pub fn from_subpaths(subpaths: impl IntoIterator>>, preserve_id: bool) -> Self { let mut vector = Self::default(); for subpath in subpaths.into_iter() { @@ -185,7 +185,7 @@ impl Vector { for (start, end) in segments_to_add { let segment_id = self.segment_domain.next_id().next_id(); - self.segment_domain.push(segment_id, start, end, bezier_rs::BezierHandles::Linear, StrokeId::ZERO); + self.segment_domain.push(segment_id, start, end, BezierHandles::Linear, StrokeId::ZERO); } } @@ -244,14 +244,14 @@ impl Vector { self.segment_domain.end_point().iter().map(|&index| self.point_domain.ids()[index]) } - pub fn push(&mut self, id: SegmentId, start: PointId, end: PointId, handles: bezier_rs::BezierHandles, stroke: StrokeId) { + pub fn push(&mut self, id: SegmentId, start: PointId, end: PointId, handles: BezierHandles, stroke: StrokeId) { let [Some(start), Some(end)] = [start, end].map(|id| self.point_domain.resolve_id(id)) else { return; }; self.segment_domain.push(id, start, end, handles, stroke) } - pub fn handles_mut(&mut self) -> impl Iterator { + pub fn handles_mut(&mut self) -> impl Iterator { self.segment_domain .handles_mut() .map(|(id, handles, start, end)| (id, handles, self.point_domain.ids()[start], self.point_domain.ids()[end])) @@ -509,9 +509,11 @@ pub fn migrate_vector<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Resu #[cfg(test)] mod tests { + use kurbo::{CubicBez, PathSeg, Point}; + use super::*; - fn assert_subpath_eq(generated: &[bezier_rs::Subpath], expected: &[bezier_rs::Subpath]) { + fn assert_subpath_eq(generated: &[Subpath], expected: &[Subpath]) { assert_eq!(generated.len(), expected.len()); for (generated, expected) in generated.iter().zip(expected) { assert_eq!(generated.manipulator_groups().len(), expected.manipulator_groups().len()); @@ -526,10 +528,10 @@ mod tests { #[test] fn construct_closed_subpath() { - let circle = bezier_rs::Subpath::new_ellipse(DVec2::NEG_ONE, DVec2::ONE); + let circle = Subpath::new_ellipse(DVec2::NEG_ONE, DVec2::ONE); let vector = Vector::from_subpath(&circle); assert_eq!(vector.point_domain.ids().len(), 4); - let bezier_paths = vector.segment_bezier_iter().map(|(_, bezier, _, _)| bezier).collect::>(); + let bezier_paths = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::>(); assert_eq!(bezier_paths.len(), 4); assert!(bezier_paths.iter().all(|&bezier| circle.iter().any(|original_bezier| original_bezier == bezier))); @@ -539,11 +541,11 @@ mod tests { #[test] fn construct_open_subpath() { - let bezier = bezier_rs::Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::NEG_ONE, DVec2::ONE, DVec2::X); - let subpath = bezier_rs::Subpath::from_bezier(&bezier); + let bezier = PathSeg::Cubic(CubicBez::new(Point::ZERO, Point::new(-1., -1.), Point::new(1., 1.), Point::new(1., 0.))); + let subpath = Subpath::from_bezier(bezier); let vector = Vector::from_subpath(&subpath); assert_eq!(vector.point_domain.ids().len(), 2); - let bezier_paths = vector.segment_bezier_iter().map(|(_, bezier, _, _)| bezier).collect::>(); + let bezier_paths = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::>(); assert_eq!(bezier_paths, vec![bezier]); let generated = vector.stroke_bezier_paths().collect::>(); @@ -552,14 +554,14 @@ mod tests { #[test] fn construct_many_subpath() { - let curve = bezier_rs::Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::NEG_ONE, DVec2::ONE, DVec2::X); - let curve = bezier_rs::Subpath::from_bezier(&curve); - let circle = bezier_rs::Subpath::new_ellipse(DVec2::NEG_ONE, DVec2::ONE); + let curve = PathSeg::Cubic(CubicBez::new(Point::ZERO, Point::new(-1., -1.), Point::new(1., 1.), Point::new(1., 0.))); + let curve = Subpath::from_bezier(curve); + let circle = Subpath::new_ellipse(DVec2::NEG_ONE, DVec2::ONE); let vector = Vector::from_subpaths([&curve, &circle], false); assert_eq!(vector.point_domain.ids().len(), 6); - let bezier_paths = vector.segment_bezier_iter().map(|(_, bezier, _, _)| bezier).collect::>(); + let bezier_paths = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::>(); assert_eq!(bezier_paths.len(), 5); assert!(bezier_paths.iter().all(|&bezier| circle.iter().chain(curve.iter()).any(|original_bezier| original_bezier == bezier))); diff --git a/node-graph/gpath-bool/Cargo.toml b/node-graph/gpath-bool/Cargo.toml index db472571a4..b4c5f06f5a 100644 --- a/node-graph/gpath-bool/Cargo.toml +++ b/node-graph/gpath-bool/Cargo.toml @@ -9,7 +9,6 @@ license = "MIT OR Apache-2.0" [dependencies] # Local dependencies dyn-any = { workspace = true } -bezier-rs = { workspace = true } graphene-core = { workspace = true } node-macro = { workspace = true } glam = { workspace = true } @@ -17,3 +16,4 @@ specta = { workspace = true } log = { workspace = true } path-bool = { workspace = true } serde = { workspace = true } +kurbo.workspace = true diff --git a/node-graph/gpath-bool/src/lib.rs b/node-graph/gpath-bool/src/lib.rs index f16c1b7553..9e609ef3f4 100644 --- a/node-graph/gpath-bool/src/lib.rs +++ b/node-graph/gpath-bool/src/lib.rs @@ -1,11 +1,13 @@ -use bezier_rs::{ManipulatorGroup, Subpath}; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; +use graphene_core::subpath::{ManipulatorGroup, Subpath}; use graphene_core::table::{Table, TableRow, TableRowRef}; use graphene_core::vector::algorithms::merge_by_distance::MergeByDistanceExt; +use graphene_core::vector::misc::{point_to_dvec2, transform_pathseg}; use graphene_core::vector::style::Fill; use graphene_core::vector::{PointId, Vector}; use graphene_core::{Color, Ctx, Graphic}; +use kurbo::{ParamCurve, Point}; pub use path_bool as path_bool_lib; use path_bool::{FillRule, PathBooleanOperation}; use std::ops::Mul; @@ -296,25 +298,25 @@ fn to_path(vector: &Vector, transform: DAffine2) -> Vec fn to_path_segments(path: &mut Vec, subpath: &Subpath, transform: DAffine2) { use path_bool::PathSegment; let mut global_start = None; - let mut global_end = DVec2::ZERO; + let mut global_end = Point::ZERO; for bezier in subpath.iter() { const EPS: f64 = 1e-8; - let transformed = bezier.apply_transformation(|pos| transform.transform_point2(pos).mul(EPS.recip()).round().mul(EPS)); - let start = transformed.start; - let end = transformed.end; + let transformed = transform_pathseg(bezier, |pos| transform.transform_point2(pos).mul(EPS.recip()).round().mul(EPS)); + let start = transformed.start(); + let end = transformed.end(); if global_start.is_none() { global_start = Some(start); } global_end = end; - let segment = match transformed.handles { - bezier_rs::BezierHandles::Linear => PathSegment::Line(start, end), - bezier_rs::BezierHandles::Quadratic { handle } => PathSegment::Quadratic(start, handle, end), - bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => PathSegment::Cubic(start, handle_start, handle_end, end), + let segment = match transformed { + kurbo::PathSeg::Line(line) => PathSegment::Line(point_to_dvec2(line.p0), point_to_dvec2(line.p1)), + kurbo::PathSeg::Quad(quad_bez) => PathSegment::Quadratic(point_to_dvec2(quad_bez.p0), point_to_dvec2(quad_bez.p1), point_to_dvec2(quad_bez.p2)), + kurbo::PathSeg::Cubic(cubic_bez) => PathSegment::Cubic(point_to_dvec2(cubic_bez.p0), point_to_dvec2(cubic_bez.p1), point_to_dvec2(cubic_bez.p2), point_to_dvec2(cubic_bez.p3)), }; path.push(segment); } if let Some(start) = global_start { - path.push(PathSegment::Line(global_end, start)); + path.push(PathSegment::Line(point_to_dvec2(global_end), point_to_dvec2(start))); } } diff --git a/node-graph/gsvg-renderer/Cargo.toml b/node-graph/gsvg-renderer/Cargo.toml index 86b3b14aa2..a6d0e395c5 100644 --- a/node-graph/gsvg-renderer/Cargo.toml +++ b/node-graph/gsvg-renderer/Cargo.toml @@ -6,14 +6,10 @@ description = "graphene svg renderer" authors = ["Graphite Authors "] license = "MIT OR Apache-2.0" -[features] -vello = ["dep:vello", "bezier-rs/kurbo"] - [dependencies] # Local dependencies dyn-any = { workspace = true } graphene-core = { workspace = true } -bezier-rs = { workspace = true } # Workspace dependencies glam = { workspace = true } @@ -22,6 +18,7 @@ base64 = { workspace = true } log = { workspace = true } num-traits = { workspace = true } usvg = { workspace = true } +kurbo = { workspace = true } # Optional workspace dependencies vello = { workspace = true, optional = true } diff --git a/node-graph/gsvg-renderer/src/convert_usvg_path.rs b/node-graph/gsvg-renderer/src/convert_usvg_path.rs index 8c7804a453..a04ca08ccb 100644 --- a/node-graph/gsvg-renderer/src/convert_usvg_path.rs +++ b/node-graph/gsvg-renderer/src/convert_usvg_path.rs @@ -1,5 +1,5 @@ -use bezier_rs::{ManipulatorGroup, Subpath}; use glam::DVec2; +use graphene_core::subpath::{ManipulatorGroup, Subpath}; use graphene_core::vector::PointId; pub fn convert_usvg_path(path: &usvg::Path) -> Vec> { diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index 5bfdbef692..cbef9de3b9 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -1,6 +1,5 @@ use crate::render_ext::RenderExt; use crate::to_peniko::BlendModeExt; -use bezier_rs::Subpath; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; use graphene_core::blending::BlendMode; @@ -10,6 +9,7 @@ use graphene_core::math::quad::Quad; use graphene_core::raster::Image; use graphene_core::raster_types::{CPU, GPU, Raster}; use graphene_core::render_complexity::RenderComplexity; +use graphene_core::subpath::Subpath; use graphene_core::table::{Table, TableRow}; use graphene_core::transform::{Footprint, Transform}; use graphene_core::uuid::{NodeId, generate_uuid}; @@ -17,6 +17,7 @@ use graphene_core::vector::Vector; use graphene_core::vector::click_target::{ClickTarget, FreePoint}; use graphene_core::vector::style::{Fill, Stroke, StrokeAlign, ViewMode}; use graphene_core::{Artboard, Graphic}; +use kurbo::Affine; use num_traits::Zero; use std::collections::{HashMap, HashSet}; use std::fmt::Write; @@ -425,8 +426,9 @@ impl Render for Table { let mut path = String::new(); - for subpath in row.element.stroke_bezier_paths() { - let _ = subpath.subpath_to_svg(&mut path, applied_stroke_transform); + for mut bezpath in row.element.stroke_bezpath_iter() { + bezpath.apply_affine(Affine::new(applied_stroke_transform.to_cols_array())); + path.push_str(bezpath.to_svg().as_str()); } let connected = vector.stroke_bezier_paths().all(|path| path.closed()); @@ -531,8 +533,11 @@ impl Render for Table { let to_point = |p: DVec2| kurbo::Point::new(p.x, p.y); let mut path = kurbo::BezPath::new(); - for subpath in row.element.stroke_bezier_paths() { - subpath.to_vello_path(applied_stroke_transform, &mut path); + for mut bezpath in row.element.stroke_bezpath_iter() { + bezpath.apply_affine(Affine::new(applied_stroke_transform.to_cols_array())); + for element in bezpath { + path.push(element); + } } // If we're using opacity or a blend mode, we need to push a layer From c52957a844aec7764e90936b798b02a18aa89af0 Mon Sep 17 00:00:00 2001 From: indierusty Date: Sat, 9 Aug 2025 11:16:08 +0530 Subject: [PATCH 02/10] Refactor few methods --- node-graph/gcore/src/subpath/core.rs | 41 ++++++++++++++++++---------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/node-graph/gcore/src/subpath/core.rs b/node-graph/gcore/src/subpath/core.rs index c5606e5486..f61eb2dcb9 100644 --- a/node-graph/gcore/src/subpath/core.rs +++ b/node-graph/gcore/src/subpath/core.rs @@ -5,11 +5,24 @@ use super::*; use glam::DVec2; use kurbo::PathSeg; -pub fn pathseg_points(segment: PathSeg) -> (DVec2, Option, Option, DVec2) { +pub struct PathSegPoints { + p0: DVec2, + p1: Option, + p2: Option, + p3: DVec2, +} + +impl PathSegPoints { + pub fn new(p0: DVec2, p1: Option, p2: Option, p3: DVec2) -> Self { + Self { p0, p1, p2, p3 } + } +} + +pub fn pathseg_points(segment: PathSeg) -> PathSegPoints { match segment { - PathSeg::Line(line) => (point_to_dvec2(line.p0), None, None, point_to_dvec2(line.p1)), - PathSeg::Quad(quad) => (point_to_dvec2(quad.p0), None, Some(point_to_dvec2(quad.p1)), point_to_dvec2(quad.p1)), - PathSeg::Cubic(cube) => (point_to_dvec2(cube.p0), Some(point_to_dvec2(cube.p1)), Some(point_to_dvec2(cube.p2)), point_to_dvec2(cube.p1)), + PathSeg::Line(line) => PathSegPoints::new(point_to_dvec2(line.p0), None, None, point_to_dvec2(line.p1)), + PathSeg::Quad(quad) => PathSegPoints::new(point_to_dvec2(quad.p0), None, Some(point_to_dvec2(quad.p1)), point_to_dvec2(quad.p1)), + PathSeg::Cubic(cube) => PathSegPoints::new(point_to_dvec2(cube.p0), Some(point_to_dvec2(cube.p1)), Some(point_to_dvec2(cube.p2)), point_to_dvec2(cube.p1)), } } @@ -25,8 +38,8 @@ impl Subpath { /// Create a `Subpath` consisting of 2 manipulator groups from a `Bezier`. pub fn from_bezier(segment: PathSeg) -> Self { - let (p1, h1, h2, p2) = pathseg_points(segment); - Subpath::new(vec![ManipulatorGroup::new(p1, None, h1), ManipulatorGroup::new(p2, h2, None)], false) + let PathSegPoints { p0, p1, p2, p3 } = pathseg_points(segment); + Subpath::new(vec![ManipulatorGroup::new(p0, None, p1), ManipulatorGroup::new(p3, p2, None)], false) } /// Creates a subpath from a slice of [Bezier]. When two consecutive Beziers do not share an end and start point, this function @@ -41,17 +54,17 @@ impl Subpath { let first = beziers.first().unwrap(); let mut manipulator_groups = vec![ManipulatorGroup { - anchor: first.0, + anchor: first.p0, in_handle: None, - out_handle: first.1, + out_handle: first.p1, id: PointId::new(), }]; let mut inner_groups: Vec> = beziers .windows(2) .map(|bezier_pair| ManipulatorGroup { - anchor: bezier_pair[1].0, - in_handle: bezier_pair[0].2, - out_handle: bezier_pair[1].1, + anchor: bezier_pair[1].p0, + in_handle: bezier_pair[0].p2, + out_handle: bezier_pair[1].p1, id: PointId::new(), }) .collect::>>(); @@ -60,15 +73,15 @@ impl Subpath { let last = beziers.last().unwrap(); if !closed { manipulator_groups.push(ManipulatorGroup { - anchor: last.3, - in_handle: last.2, + anchor: last.p3, + in_handle: last.p2, out_handle: None, id: PointId::new(), }); return Subpath::new(manipulator_groups, false); } - manipulator_groups[0].in_handle = last.2; + manipulator_groups[0].in_handle = last.p2; Subpath::new(manipulator_groups, true) } From 6a1e4bfffc2964e1bcc0ed770955c9a3f5829d82 Mon Sep 17 00:00:00 2001 From: indierusty Date: Sat, 9 Aug 2025 18:38:29 +0530 Subject: [PATCH 03/10] refactoring worked! --- editor/Cargo.toml | 2 +- .../document/document_message_handler.rs | 25 +- .../graph_operation_message.rs | 2 +- .../graph_operation/transform_utils.rs | 2 +- .../document/graph_operation/utility_types.rs | 2 +- .../node_graph/node_graph_message_handler.rs | 2 +- .../document/overlays/utility_functions.rs | 4 +- .../document/overlays/utility_types.rs | 71 +- .../document/overlays/utility_types_vello.rs | 67 +- .../utility_types/document_metadata.rs | 3 +- .../utility_types/network_interface.rs | 36 +- .../messages/portfolio/document_migration.rs | 2 +- .../portfolio/portfolio_message_handler.rs | 2 +- .../graph_modification_utils.rs | 2 +- .../tool/common_functionality/shape_editor.rs | 100 +-- .../shapes/shape_utility.rs | 8 +- .../tool/common_functionality/snapping.rs | 9 +- .../snapping/layer_snapper.rs | 45 +- .../snapping/snap_results.rs | 4 +- .../common_functionality/utility_functions.rs | 50 +- .../messages/tool/tool_messages/path_tool.rs | 98 +-- .../messages/tool/tool_messages/pen_tool.rs | 23 +- .../tool/tool_messages/select_tool.rs | 2 +- node-graph/gcore/src/subpath/core.rs | 8 +- .../vector/algorithms/bezpath_algorithms.rs | 56 +- node-graph/gcore/src/vector/algorithms/mod.rs | 1 + .../vector/algorithms/symmetrical_basis.rs | 613 ++++++++++++++++++ .../gcore/src/vector/algorithms/util.rs | 2 +- node-graph/gcore/src/vector/misc.rs | 8 +- node-graph/gcore/src/vector/vector_types.rs | 7 +- 30 files changed, 960 insertions(+), 296 deletions(-) create mode 100644 node-graph/gcore/src/vector/algorithms/symmetrical_basis.rs diff --git a/editor/Cargo.toml b/editor/Cargo.toml index ed020ef919..7407b10670 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -24,6 +24,7 @@ graphite-proc-macros = { workspace = true } graph-craft = { workspace = true } interpreted-executor = { workspace = true } graphene-std = { workspace = true } +graphene-core = { workspace = true } preprocessor = { workspace = true } # Workspace dependencies @@ -33,7 +34,6 @@ bitflags = { workspace = true } thiserror = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -bezier-rs = { workspace = true } kurbo = { workspace = true } futures = { workspace = true } glam = { workspace = true } diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 5bfa0af7dd..5807724b91 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -25,10 +25,10 @@ use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys; use crate::messages::tool::tool_messages::tool_prelude::Key; use crate::messages::tool::utility_types::ToolType; use crate::node_graph_executor::NodeGraphExecutor; -use bezier_rs::Subpath; use glam::{DAffine2, DVec2, IVec2}; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork}; +use graphene_core::subpath::Subpath; use graphene_std::math::quad::Quad; use graphene_std::path_bool::{boolean_intersect, path_bool_lib}; use graphene_std::raster::BlendMode; @@ -36,7 +36,9 @@ use graphene_std::raster_types::Raster; use graphene_std::table::Table; use graphene_std::vector::PointId; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; +use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2}; use graphene_std::vector::style::ViewMode; +use kurbo::{Affine, CubicBez, Line, ParamCurve, PathSeg, QuadBez}; use std::path::PathBuf; use std::time::Duration; @@ -2949,10 +2951,10 @@ fn quad_to_path_lib_segments(quad: Quad) -> Vec { } fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator, transform: DAffine2) -> Vec { - let segment = |bezier: bezier_rs::Bezier| match bezier.handles { - bezier_rs::BezierHandles::Linear => path_bool_lib::PathSegment::Line(bezier.start, bezier.end), - bezier_rs::BezierHandles::Quadratic { handle } => path_bool_lib::PathSegment::Quadratic(bezier.start, handle, bezier.end), - bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => path_bool_lib::PathSegment::Cubic(bezier.start, handle_start, handle_end, bezier.end), + let segment = |bezier: PathSeg| match bezier { + PathSeg::Line(line) => path_bool_lib::PathSegment::Line(point_to_dvec2(line.p0), point_to_dvec2(line.p1)), + PathSeg::Quad(quad_bez) => path_bool_lib::PathSegment::Quadratic(point_to_dvec2(quad_bez.p0), point_to_dvec2(quad_bez.p1), point_to_dvec2(quad_bez.p2)), + PathSeg::Cubic(cubic_bez) => path_bool_lib::PathSegment::Cubic(point_to_dvec2(cubic_bez.p0), point_to_dvec2(cubic_bez.p1), point_to_dvec2(cubic_bez.p2), point_to_dvec2(cubic_bez.p3)), }; click_targets .filter_map(|target| { @@ -2963,7 +2965,7 @@ fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator ClickXRayIter<'a> { fn check_layer_area_target(&mut self, click_targets: Option<&Vec>, clip: bool, layer: LayerNodeIdentifier, path: Vec, transform: DAffine2) -> XRayResult { // Convert back to Bezier-rs types for intersections let segment = |bezier: &path_bool_lib::PathSegment| match *bezier { - path_bool_lib::PathSegment::Line(start, end) => bezier_rs::Bezier::from_linear_dvec2(start, end), - path_bool_lib::PathSegment::Cubic(start, h1, h2, end) => bezier_rs::Bezier::from_cubic_dvec2(start, h1, h2, end), - path_bool_lib::PathSegment::Quadratic(start, h1, end) => bezier_rs::Bezier::from_quadratic_dvec2(start, h1, end), + path_bool_lib::PathSegment::Line(start, end) => PathSeg::Line(Line::new(dvec2_to_point(start), dvec2_to_point(end))), + path_bool_lib::PathSegment::Cubic(start, h1, h2, end) => PathSeg::Cubic(CubicBez::new(dvec2_to_point(start), dvec2_to_point(h1), dvec2_to_point(h2), dvec2_to_point(end))), + path_bool_lib::PathSegment::Quadratic(start, h1, end) => PathSeg::Quad(QuadBez::new(dvec2_to_point(start), dvec2_to_point(h1), dvec2_to_point(end))), path_bool_lib::PathSegment::Arc(_, _, _, _, _, _, _) => unimplemented!(), }; let get_clip = || path.iter().map(segment); @@ -3039,7 +3041,10 @@ impl<'a> ClickXRayIter<'a> { XRayTarget::Quad(quad) => self.check_layer_area_target(click_targets, clip, layer, quad_to_path_lib_segments(*quad), transform), XRayTarget::Path(path) => self.check_layer_area_target(click_targets, clip, layer, path.clone(), transform), XRayTarget::Polygon(polygon) => { - let polygon = polygon.iter_closed().map(|line| path_bool_lib::PathSegment::Line(line.start, line.end)).collect(); + let polygon = polygon + .iter_closed() + .map(|line| path_bool_lib::PathSegment::Line(point_to_dvec2(line.start()), point_to_dvec2(line.end()))) + .collect(); self.check_layer_area_target(click_targets, clip, layer, polygon, transform) } } diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index afd1c819b0..aa92e07836 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -2,9 +2,9 @@ use super::utility_types::TransformIn; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::NodeTemplate; use crate::messages::prelude::*; -use bezier_rs::Subpath; use glam::{DAffine2, IVec2}; use graph_craft::document::NodeId; +use graphene_core::subpath::Subpath; use graphene_std::Artboard; use graphene_std::brush::brush_stroke::BrushStroke; use graphene_std::raster::BlendMode; diff --git a/editor/src/messages/portfolio/document/graph_operation/transform_utils.rs b/editor/src/messages/portfolio/document/graph_operation/transform_utils.rs index 2b3639516d..f7ce2d85f9 100644 --- a/editor/src/messages/portfolio/document/graph_operation/transform_utils.rs +++ b/editor/src/messages/portfolio/document/graph_operation/transform_utils.rs @@ -1,8 +1,8 @@ use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface}; -use bezier_rs::Subpath; use glam::{DAffine2, DVec2}; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput}; +use graphene_core::subpath::Subpath; use graphene_std::vector::PointId; /// Convert an affine transform into the tuple `(scale, angle, translation, shear)` assuming `shear.y = 0`. diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index a988c6f1f8..9a3904ac89 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -3,11 +3,11 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions: use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::{self, InputConnector, NodeNetworkInterface, OutputConnector}; use crate::messages::prelude::*; -use bezier_rs::Subpath; use glam::{DAffine2, IVec2}; use graph_craft::concrete; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput}; +use graphene_core::subpath::Subpath; use graphene_std::Artboard; use graphene_std::brush::brush_stroke::BrushStroke; use graphene_std::raster::BlendMode; diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 234f04c360..ec3521eb41 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -1833,7 +1833,7 @@ impl<'a> MessageHandler> for NodeG continue; }; let quad = Quad::from_box([box_selection_start, box_selection_end_graph]); - if click_targets.node_click_target.intersect_path(|| quad.bezier_lines(), DAffine2::IDENTITY) { + if click_targets.node_click_target.intersect_path(|| quad.to_lines(), DAffine2::IDENTITY) { nodes.insert(node_id); } } diff --git a/editor/src/messages/portfolio/document/overlays/utility_functions.rs b/editor/src/messages/portfolio/document/overlays/utility_functions.rs index db94184211..965fa4ac5c 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_functions.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_functions.rs @@ -3,8 +3,8 @@ use crate::consts::HIDE_HANDLE_DISTANCE; use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState}; use crate::messages::tool::tool_messages::tool_prelude::{DocumentMessageHandler, PreferencesMessageHandler}; -use bezier_rs::{Bezier, BezierHandles}; use glam::{DAffine2, DVec2}; +use graphene_core::subpath::{Bezier, BezierHandles}; use graphene_std::vector::misc::ManipulatorPointId; use graphene_std::vector::{PointId, SegmentId}; use wasm_bindgen::JsCast; @@ -125,7 +125,7 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle } // Get the selected segments and then add a bold line overlay on them - for (segment_id, bezier, _, _) in vector.segment_bezier_iter() { + for (segment_id, bezier, _, _) in vector.segment_iter() { let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) else { continue; }; diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index c1bd7ccd39..51550547af 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -5,14 +5,16 @@ use crate::consts::{ PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, SEGMENT_SELECTED_THICKNESS, }; use crate::messages::prelude::Message; -use bezier_rs::{Bezier, Subpath}; use core::borrow::Borrow; use core::f64::consts::{FRAC_PI_2, PI, TAU}; use glam::{DAffine2, DVec2}; +use graphene_core::subpath::Subpath; use graphene_std::Color; use graphene_std::math::quad::Quad; use graphene_std::vector::click_target::ClickTargetType; +use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2}; use graphene_std::vector::{PointId, SegmentId, Vector}; +use kurbo::{self, Affine, CubicBez, ParamCurve, PathSeg}; use std::collections::HashMap; use wasm_bindgen::{JsCast, JsValue}; use web_sys::{OffscreenCanvas, OffscreenCanvasRenderingContext2d}; @@ -571,11 +573,7 @@ impl OverlayContext { let handle_start = start + start_vec.perp() * radius * factor; let handle_end = end - end_vec.perp() * radius * factor; - let bezier = Bezier { - start, - end, - handles: bezier_rs::BezierHandles::Cubic { handle_start, handle_end }, - }; + let bezier = PathSeg::Cubic(CubicBez::new(dvec2_to_point(start), dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(end))); self.bezier_command(bezier, DAffine2::IDENTITY, i == 0); } @@ -762,7 +760,7 @@ impl OverlayContext { self.render_context.begin_path(); let mut last_point = None; - for (_, bezier, start_id, end_id) in vector.segment_bezier_iter() { + for (_, bezier, start_id, end_id) in vector.segment_iter() { let move_to = last_point != Some(start_id); last_point = Some(end_id); @@ -776,7 +774,7 @@ impl OverlayContext { } /// Used by the Pen tool in order to show how the bezier curve would look like. - pub fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) { + pub fn outline_bezier(&mut self, bezier: PathSeg, transform: DAffine2) { self.start_dpi_aware_transform(); self.render_context.begin_path(); @@ -788,7 +786,7 @@ impl OverlayContext { } /// Used by the path tool segment mode in order to show the selected segments. - pub fn outline_select_bezier(&mut self, bezier: Bezier, transform: DAffine2) { + pub fn outline_select_bezier(&mut self, bezier: PathSeg, transform: DAffine2) { self.start_dpi_aware_transform(); self.render_context.begin_path(); @@ -802,7 +800,7 @@ impl OverlayContext { self.end_dpi_aware_transform(); } - pub fn outline_overlay_bezier(&mut self, bezier: Bezier, transform: DAffine2) { + pub fn outline_overlay_bezier(&mut self, bezier: PathSeg, transform: DAffine2) { self.start_dpi_aware_transform(); self.render_context.begin_path(); @@ -816,18 +814,18 @@ impl OverlayContext { self.end_dpi_aware_transform(); } - fn bezier_command(&self, bezier: Bezier, transform: DAffine2, move_to: bool) { + fn bezier_command(&self, bezier: PathSeg, transform: DAffine2, move_to: bool) { self.start_dpi_aware_transform(); - let Bezier { start, end, handles } = bezier.apply_transformation(|point| transform.transform_point2(point)); + let bezier = Affine::new(transform.to_cols_array()) * bezier; if move_to { - self.render_context.move_to(start.x, start.y); + self.render_context.move_to(bezier.start().x, bezier.start().y); } - - match handles { - bezier_rs::BezierHandles::Linear => self.render_context.line_to(end.x, end.y), - bezier_rs::BezierHandles::Quadratic { handle } => self.render_context.quadratic_curve_to(handle.x, handle.y, end.x, end.y), - bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => self.render_context.bezier_curve_to(handle_start.x, handle_start.y, handle_end.x, handle_end.y, end.x, end.y), + match bezier.as_path_el() { + kurbo::PathEl::LineTo(point) => self.render_context.line_to(point.x, point.y), + kurbo::PathEl::QuadTo(point, point1) => self.render_context.quadratic_curve_to(point.x, point.y, point1.x, point1.y), + kurbo::PathEl::CurveTo(point, point1, point2) => self.render_context.bezier_curve_to(point.x, point.y, point1.x, point1.y, point2.x, point2.y), + _ => unreachable!(), } self.end_dpi_aware_transform(); @@ -841,36 +839,35 @@ impl OverlayContext { let subpath = subpath.borrow(); let mut curves = subpath.iter().peekable(); - let Some(first) = curves.peek() else { + let Some(&first) = curves.peek() else { continue; }; - self.render_context.move_to(transform.transform_point2(first.start()).x, transform.transform_point2(first.start()).y); + let start_point = transform.transform_point2(point_to_dvec2(first.start())); + self.render_context.move_to(start_point.x, start_point.y); + for curve in curves { - match curve.handles { - bezier_rs::BezierHandles::Linear => { - let a = transform.transform_point2(curve.end()); + match curve { + PathSeg::Line(line) => { + let a = transform.transform_point2(point_to_dvec2(line.p1)); let a = a.round() - DVec2::splat(0.5); - - self.render_context.line_to(a.x, a.y) + self.render_context.line_to(a.x, a.y); } - bezier_rs::BezierHandles::Quadratic { handle } => { - let a = transform.transform_point2(handle); - let b = transform.transform_point2(curve.end()); + PathSeg::Quad(quad_bez) => { + let a = transform.transform_point2(point_to_dvec2(quad_bez.p1)); + let b = transform.transform_point2(point_to_dvec2(quad_bez.p2)); let a = a.round() - DVec2::splat(0.5); let b = b.round() - DVec2::splat(0.5); - - self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y) + self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y); } - bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => { - let a = transform.transform_point2(handle_start); - let b = transform.transform_point2(handle_end); - let c = transform.transform_point2(curve.end()); + PathSeg::Cubic(cubic_bez) => { + let a = transform.transform_point2(point_to_dvec2(cubic_bez.p1)); + let b = transform.transform_point2(point_to_dvec2(cubic_bez.p2)); + let c = transform.transform_point2(point_to_dvec2(cubic_bez.p3)); let a = a.round() - DVec2::splat(0.5); let b = b.round() - DVec2::splat(0.5); let c = c.round() - DVec2::splat(0.5); - - self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y) + self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y); } } } @@ -885,7 +882,7 @@ impl OverlayContext { /// Used by the Select tool to outline a path or a free point when selected or hovered. pub fn outline(&mut self, target_types: impl Iterator>, transform: DAffine2, color: Option<&str>) { - let mut subpaths: Vec> = vec![]; + let mut subpaths: Vec> = vec![]; target_types.for_each(|target_type| match target_type.borrow() { ClickTargetType::FreePoint(point) => { diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs index 59c5b2657f..e6c8b70475 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs @@ -4,20 +4,22 @@ use crate::consts::{ PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, }; use crate::messages::prelude::Message; -use bezier_rs::{Bezier, Subpath}; use core::borrow::Borrow; use core::f64::consts::{FRAC_PI_2, PI, TAU}; use glam::{DAffine2, DVec2}; +use graphene_core::subpath::{self, Subpath}; use graphene_std::Color; use graphene_std::math::quad::Quad; use graphene_std::table::Table; use graphene_std::text::{TextAlign, TypesettingConfig, load_font, to_path}; use graphene_std::vector::click_target::ClickTargetType; +use graphene_std::vector::misc::point_to_dvec2; use graphene_std::vector::{PointId, SegmentId, Vector}; +use kurbo::{self, BezPath, ParamCurve}; +use kurbo::{Affine, PathSeg}; use std::collections::HashMap; use std::sync::{Arc, Mutex, MutexGuard}; use vello::Scene; -use vello::kurbo::{self, BezPath}; use vello::peniko; pub type OverlayProvider = fn(OverlayContext) -> Message; @@ -345,16 +347,16 @@ impl OverlayContext { } /// Used by the Pen tool in order to show how the bezier curve would look like. - pub fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) { + pub fn outline_bezier(&mut self, bezier: PathSeg, transform: DAffine2) { self.internal().outline_bezier(bezier, transform); } /// Used by the path tool segment mode in order to show the selected segments. - pub fn outline_select_bezier(&mut self, bezier: Bezier, transform: DAffine2) { + pub fn outline_select_bezier(&mut self, bezier: PathSeg, transform: DAffine2) { self.internal().outline_select_bezier(bezier, transform); } - pub fn outline_overlay_bezier(&mut self, bezier: Bezier, transform: DAffine2) { + pub fn outline_overlay_bezier(&mut self, bezier: PathSeg, transform: DAffine2) { self.internal().outline_overlay_bezier(bezier, transform); } @@ -842,7 +844,7 @@ impl OverlayContextInternal { let mut path = BezPath::new(); let mut last_point = None; - for (_, bezier, start_id, end_id) in vector.segment_bezier_iter() { + for (_, bezier, start_id, end_id) in vector.segment_iter() { let move_to = last_point != Some(start_id); last_point = Some(end_id); @@ -853,7 +855,7 @@ impl OverlayContextInternal { } /// Used by the Pen tool in order to show how the bezier curve would look like. - fn outline_bezier(&mut self, bezier: Bezier, transform: DAffine2) { + fn outline_bezier(&mut self, bezier: PathSeg, transform: DAffine2) { let vello_transform = self.get_transform(); let mut path = BezPath::new(); self.bezier_to_path(bezier, transform, true, &mut path); @@ -862,7 +864,7 @@ impl OverlayContextInternal { } /// Used by the path tool segment mode in order to show the selected segments. - fn outline_select_bezier(&mut self, bezier: Bezier, transform: DAffine2) { + fn outline_select_bezier(&mut self, bezier: PathSeg, transform: DAffine2) { let vello_transform = self.get_transform(); let mut path = BezPath::new(); self.bezier_to_path(bezier, transform, true, &mut path); @@ -870,7 +872,7 @@ impl OverlayContextInternal { self.scene.stroke(&kurbo::Stroke::new(4.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &path); } - fn outline_overlay_bezier(&mut self, bezier: Bezier, transform: DAffine2) { + fn outline_overlay_bezier(&mut self, bezier: PathSeg, transform: DAffine2) { let vello_transform = self.get_transform(); let mut path = BezPath::new(); self.bezier_to_path(bezier, transform, true, &mut path); @@ -878,21 +880,12 @@ impl OverlayContextInternal { self.scene.stroke(&kurbo::Stroke::new(4.0), vello_transform, Self::parse_color(COLOR_OVERLAY_BLUE_50), None, &path); } - fn bezier_to_path(&self, bezier: Bezier, transform: DAffine2, move_to: bool, path: &mut BezPath) { - let Bezier { start, end, handles } = bezier.apply_transformation(|point| transform.transform_point2(point)); + fn bezier_to_path(&self, bezier: PathSeg, transform: DAffine2, move_to: bool, path: &mut BezPath) { + let bezier = Affine::new(transform.to_cols_array()) * bezier; if move_to { - path.move_to(kurbo::Point::new(start.x, start.y)); - } - - match handles { - bezier_rs::BezierHandles::Linear => path.line_to(kurbo::Point::new(end.x, end.y)), - bezier_rs::BezierHandles::Quadratic { handle } => path.quad_to(kurbo::Point::new(handle.x, handle.y), kurbo::Point::new(end.x, end.y)), - bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => path.curve_to( - kurbo::Point::new(handle_start.x, handle_start.y), - kurbo::Point::new(handle_end.x, handle_end.y), - kurbo::Point::new(end.x, end.y), - ), + path.move_to(bezier.start()); } + path.push(bezier.as_path_el()); } fn push_path(&mut self, subpaths: impl Iterator>>, transform: DAffine2) -> BezPath { @@ -906,27 +899,27 @@ impl OverlayContextInternal { continue; }; - let start_point = transform.transform_point2(first.start()); + let start_point = transform.transform_point2(point_to_dvec2(first.start())); path.move_to(kurbo::Point::new(start_point.x, start_point.y)); for curve in curves { - match curve.handles { - bezier_rs::BezierHandles::Linear => { - let a = transform.transform_point2(curve.end()); + match curve { + PathSeg::Line(line) => { + let a = transform.transform_point2(point_to_dvec2(line.p1)); let a = a.round() - DVec2::splat(0.5); path.line_to(kurbo::Point::new(a.x, a.y)); } - bezier_rs::BezierHandles::Quadratic { handle } => { - let a = transform.transform_point2(handle); - let b = transform.transform_point2(curve.end()); + PathSeg::Quad(quad_bez) => { + let a = transform.transform_point2(point_to_dvec2(quad_bez.p1)); + let b = transform.transform_point2(point_to_dvec2(quad_bez.p2)); let a = a.round() - DVec2::splat(0.5); let b = b.round() - DVec2::splat(0.5); path.quad_to(kurbo::Point::new(a.x, a.y), kurbo::Point::new(b.x, b.y)); } - bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => { - let a = transform.transform_point2(handle_start); - let b = transform.transform_point2(handle_end); - let c = transform.transform_point2(curve.end()); + PathSeg::Cubic(cubic_bez) => { + let a = transform.transform_point2(point_to_dvec2(cubic_bez.p1)); + let b = transform.transform_point2(point_to_dvec2(cubic_bez.p2)); + let c = transform.transform_point2(point_to_dvec2(cubic_bez.p3)); let a = a.round() - DVec2::splat(0.5); let b = b.round() - DVec2::splat(0.5); let c = c.round() - DVec2::splat(0.5); @@ -945,7 +938,7 @@ impl OverlayContextInternal { /// Used by the Select tool to outline a path or a free point when selected or hovered. fn outline(&mut self, target_types: impl Iterator>, transform: DAffine2, color: Option<&str>) { - let mut subpaths: Vec> = vec![]; + let mut subpaths: Vec> = vec![]; for target_type in target_types { match target_type.borrow() { @@ -1118,13 +1111,13 @@ impl OverlayContextInternal { // Add handle points if they exist match transformed_bezier.handles { - bezier_rs::BezierHandles::Quadratic { handle } => { + subpath::BezierHandles::Quadratic { handle } => { min_x = min_x.min(handle.x); min_y = min_y.min(handle.y); max_x = max_x.max(handle.x); max_y = max_y.max(handle.y); } - bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => { + subpath::BezierHandles::Cubic { handle_start, handle_end } => { for handle in [handle_start, handle_end] { min_x = min_x.min(handle.x); min_y = min_y.min(handle.y); @@ -1154,7 +1147,7 @@ impl OverlayContextInternal { let mut path = BezPath::new(); let mut last_point = None; - for (_, bezier, start_id, end_id) in row.element.segment_bezier_iter() { + for (_, bezier, start_id, end_id) in row.element.segment_iter() { let move_to = last_point != Some(start_id); last_point = Some(end_id); diff --git a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index cc7073e8a2..28943e22af 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -5,6 +5,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::Flow use crate::messages::tool::common_functionality::graph_modification_utils; use glam::{DAffine2, DVec2}; use graph_craft::document::NodeId; +use graphene_core::subpath; use graphene_std::math::quad::Quad; use graphene_std::transform::Footprint; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; @@ -196,7 +197,7 @@ impl DocumentMetadata { self.all_layers().filter_map(|layer| self.bounding_box_viewport(layer)).reduce(Quad::combine_bounds) } - pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator> { + pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator> { static EMPTY: Vec = Vec::new(); let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY); click_targets.iter().filter_map(|target| match target.target_type() { diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 938bd24d5a..a13a9fa6e6 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -8,11 +8,11 @@ use crate::messages::portfolio::document::node_graph::utility_types::{Direction, use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire}; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::tool_messages::tool_prelude::NumberInputMode; -use bezier_rs::Subpath; use glam::{DAffine2, DVec2, IVec2}; use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork}; use graph_craft::{Type, concrete}; +use graphene_core::subpath::Subpath; use graphene_std::Artboard; use graphene_std::math::quad::Quad; use graphene_std::table::Table; @@ -2832,7 +2832,7 @@ impl NodeNetworkInterface { let node_click_target_bottom_right = node_click_target_top_left + DVec2::new(width as f64, height as f64); let radius = 3.; - let subpath = bezier_rs::Subpath::new_rounded_rect(node_click_target_top_left, node_click_target_bottom_right, [radius; 4]); + let subpath = Subpath::new_rounded_rect(node_click_target_top_left, node_click_target_bottom_right, [radius; 4]); let node_click_target = ClickTarget::new_with_subpath(subpath, 0.); DocumentNodeClickTargets { @@ -2871,7 +2871,7 @@ impl NodeNetworkInterface { let node_bottom_right = node_top_left + DVec2::new(width as f64, height as f64); let chain_top_left = node_top_left - DVec2::new((chain_width_grid_spaces * crate::consts::GRID_SIZE) as f64, 0.); let radius = 10.; - let subpath = bezier_rs::Subpath::new_rounded_rect(chain_top_left, node_bottom_right, [radius; 4]); + let subpath = Subpath::new_rounded_rect(chain_top_left, node_bottom_right, [radius; 4]); let node_click_target = ClickTarget::new_with_subpath(subpath, 0.); DocumentNodeClickTargets { @@ -3059,27 +3059,21 @@ impl NodeNetworkInterface { let mut node_path = String::new(); if let ClickTargetType::Subpath(subpath) = node_click_targets.node_click_target.target_type() { - let _ = subpath.subpath_to_svg(&mut node_path, DAffine2::IDENTITY); + node_path.push_str(subpath.to_bezpath().to_svg().as_str()) } all_node_click_targets.push((node_id, node_path)); for port in node_click_targets.port_click_targets.click_targets().chain(import_export_click_targets.click_targets()) { if let ClickTargetType::Subpath(subpath) = port.target_type() { - let mut port_path = String::new(); - let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY); - port_click_targets.push(port_path); + port_click_targets.push(subpath.to_bezpath().to_svg()); } } if let NodeTypeClickTargets::Layer(layer_metadata) = &node_click_targets.node_type_metadata { if let ClickTargetType::Subpath(subpath) = layer_metadata.visibility_click_target.target_type() { - let mut port_path = String::new(); - let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY); - icon_click_targets.push(port_path); + icon_click_targets.push(subpath.to_bezpath().to_svg()); } if let ClickTargetType::Subpath(subpath) = layer_metadata.grip_click_target.target_type() { - let mut port_path = String::new(); - let _ = subpath.subpath_to_svg(&mut port_path, DAffine2::IDENTITY); - icon_click_targets.push(port_path); + icon_click_targets.push(subpath.to_bezpath().to_svg()); } } } @@ -3095,9 +3089,8 @@ impl NodeNetworkInterface { }); let bounds = self.all_nodes_bounding_box(network_path).cloned().unwrap_or([DVec2::ZERO, DVec2::ZERO]); - let rect = bezier_rs::Subpath::::new_rect(bounds[0], bounds[1]); - let mut all_nodes_bounding_box = String::new(); - let _ = rect.subpath_to_svg(&mut all_nodes_bounding_box, DAffine2::IDENTITY); + let rect = Subpath::::new_rect(bounds[0], bounds[1]); + let all_nodes_bounding_box = rect.to_bezpath().to_svg(); let Some(rounded_network_edge_distance) = self.rounded_network_edge_distance(network_path).cloned() else { log::error!("Could not get rounded_network_edge_distance in collect_frontend_click_targets"); @@ -3123,9 +3116,8 @@ impl NodeNetworkInterface { .inverse() .transform_point2(import_exports_viewport_bottom_right); - let import_exports_target = bezier_rs::Subpath::::new_rect(node_graph_top_left, node_graph_bottom_right); - let mut import_exports_bounding_box = String::new(); - let _ = import_exports_target.subpath_to_svg(&mut import_exports_bounding_box, DAffine2::IDENTITY); + let import_exports_target = Subpath::::new_rect(node_graph_top_left, node_graph_bottom_right); + let import_exports_bounding_box = import_exports_target.to_bezpath().to_svg(); let mut modify_import_export = Vec::new(); if let Some(modify_import_export_click_targets) = self.modify_import_export(network_path) { @@ -3136,9 +3128,7 @@ impl NodeNetworkInterface { .chain(modify_import_export_click_targets.reorder_imports_exports.click_targets()) { if let ClickTargetType::Subpath(subpath) = click_target.target_type() { - let mut remove_string = String::new(); - let _ = subpath.subpath_to_svg(&mut remove_string, DAffine2::IDENTITY); - modify_import_export.push(remove_string); + modify_import_export.push(subpath.to_bezpath().to_svg()); } } } @@ -3407,7 +3397,7 @@ impl NodeNetworkInterface { return None; }; - let bounding_box_subpath = bezier_rs::Subpath::::new_rect(bounds[0], bounds[1]); + let bounding_box_subpath = Subpath::::new_rect(bounds[0], bounds[1]); bounding_box_subpath.bounding_box_with_transform(network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport) } diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index e182558630..586b934b86 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -5,10 +5,10 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions: use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector}; use crate::messages::prelude::DocumentMessageHandler; -use bezier_rs::Subpath; use glam::IVec2; use graph_craft::document::DocumentNode; use graph_craft::document::{DocumentNodeImplementation, NodeInput, value::TaggedValue}; +use graphene_core::subpath::Subpath; use graphene_std::ProtoNodeIdentifier; use graphene_std::table::Table; use graphene_std::text::{TextAlign, TypesettingConfig}; diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 0a1e1d54ed..86b29fdb89 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -23,9 +23,9 @@ use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::utility_functions::make_path_editable_is_allowed; use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType}; use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor}; -use bezier_rs::BezierHandles; use glam::{DAffine2, DVec2}; use graph_craft::document::NodeId; +use graphene_core::subpath::BezierHandles; use graphene_std::Color; use graphene_std::renderer::Quad; use graphene_std::text::Font; diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index cecc7563dc..39ad06d357 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -3,11 +3,11 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeNetworkInterface, NodeTemplate}; use crate::messages::prelude::*; -use bezier_rs::Subpath; use glam::DVec2; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput}; use graph_craft::{ProtoNodeIdentifier, concrete}; +use graphene_core::subpath::Subpath; use graphene_std::Color; use graphene_std::NodeInputDecleration; use graphene_std::raster::BlendMode; diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 45a1740f91..3080e738c4 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -1,6 +1,6 @@ use super::graph_modification_utils::merge_layers; use super::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint}; -use super::utility_functions::{adjust_handle_colinearity, calculate_bezier_bbox, calculate_segment_angle, restore_g1_continuity, restore_previous_handle_position}; +use super::utility_functions::{adjust_handle_colinearity, calculate_segment_angle, restore_g1_continuity, restore_previous_handle_position}; use crate::consts::HANDLE_LENGTH_FACTOR; use crate::messages::portfolio::document::overlays::utility_functions::selected_segments; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; @@ -9,12 +9,15 @@ use crate::messages::portfolio::document::utility_types::network_interface::Node use crate::messages::preferences::SelectionMode; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration; -use crate::messages::tool::common_functionality::utility_functions::{is_intersecting, is_visible_point}; +use crate::messages::tool::common_functionality::utility_functions::is_visible_point; use crate::messages::tool::tool_messages::path_tool::{PathOverlayMode, PointSelectState}; -use bezier_rs::{Bezier, BezierHandles, Subpath, TValue}; use glam::{DAffine2, DVec2}; -use graphene_std::vector::misc::{HandleId, ManipulatorPointId}; +use graphene_core::subpath::{BezierHandles, Subpath}; +use graphene_std::subpath::{PathSegPoints, pathseg_points}; +use graphene_std::vector::algorithms::bezpath_algorithms::pathseg_compute_lookup_table; +use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point, point_to_dvec2}; use graphene_std::vector::{HandleExt, PointId, SegmentId, Vector, VectorModificationType}; +use kurbo::{Affine, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveNearest, PathSeg, Rect, Shape}; use std::f64::consts::TAU; #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -26,7 +29,7 @@ pub enum SelectionChange { #[derive(Clone, Copy, Debug)] pub enum SelectionShape<'a> { - Box([DVec2; 2]), + Box(Rect), Lasso(&'a Vec), } @@ -185,7 +188,7 @@ pub type OpposingHandleLengths = HashMap; 2], t: f64, @@ -205,12 +208,12 @@ impl ClosestSegment { self.points } - pub fn bezier(&self) -> Bezier { + pub fn pathseg(&self) -> PathSeg { self.bezier } pub fn closest_point_document(&self) -> DVec2 { - self.bezier.evaluate(TValue::Parametric(self.t)) + point_to_dvec2(self.bezier.eval(self.t)) } pub fn closest_point_to_viewport(&self) -> DVec2 { @@ -219,7 +222,7 @@ impl ClosestSegment { pub fn closest_point(&self, document_metadata: &DocumentMetadata, network_interface: &NodeNetworkInterface) -> DVec2 { let transform = document_metadata.transform_to_viewport_if_feeds(self.layer, network_interface); - let bezier_point = self.bezier.evaluate(TValue::Parametric(self.t)); + let bezier_point = point_to_dvec2(self.bezier.eval(self.t)); transform.transform_point2(bezier_point) } @@ -228,10 +231,10 @@ impl ClosestSegment { let transform = document_metadata.transform_to_viewport_if_feeds(self.layer, network_interface); let layer_mouse_pos = transform.inverse().transform_point2(mouse_position); - let t = self.bezier.project(layer_mouse_pos).clamp(0., 1.); + let t = self.bezier.nearest(dvec2_to_point(layer_mouse_pos), DEFAULT_ACCURACY).t.clamp(0., 1.); self.t = t; - let bezier_point = self.bezier.evaluate(TValue::Parametric(t)); + let bezier_point = point_to_dvec2(self.bezier.eval(t)); let bezier_point = transform.transform_point2(bezier_point); self.bezier_point_to_viewport = bezier_point; } @@ -249,22 +252,24 @@ impl ClosestSegment { let transform = document_metadata.transform_to_viewport_if_feeds(self.layer, network_interface); // Split the Bezier at the parameter `t` - let [first, second] = self.bezier.split(TValue::Parametric(self.t)); + let first = self.bezier.subsegment(0f64..self.t); + let second = self.bezier.subsegment(self.t..1.); // Transform the handle positions to viewport space - let first_handle = first.handle_end().map(|handle| transform.transform_point2(handle)); - let second_handle = second.handle_start().map(|handle| transform.transform_point2(handle)); + let first_handle = pathseg_points(first).p2.map(|handle| transform.transform_point2(handle)); + let second_handle = pathseg_points(second).p1.map(|handle| transform.transform_point2(handle)); (first_handle, second_handle) } pub fn adjusted_insert(&self, responses: &mut VecDeque) -> (PointId, [SegmentId; 2]) { let layer = self.layer; - let [first, second] = self.bezier.split(TValue::Parametric(self.t)); + let first = pathseg_points(self.bezier.subsegment(0f64..self.t)); + let second = pathseg_points(self.bezier.subsegment(self.t..1.)); // Point let midpoint = PointId::generate(); - let modification_type = VectorModificationType::InsertPoint { id: midpoint, position: first.end }; + let modification_type = VectorModificationType::InsertPoint { id: midpoint, position: first.p3 }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); // First segment @@ -272,7 +277,7 @@ impl ClosestSegment { let modification_type = VectorModificationType::InsertSegment { id: segment_ids[0], points: [self.points[0], midpoint], - handles: [first.handle_start().map(|handle| handle - first.start), first.handle_end().map(|handle| handle - first.end)], + handles: [first.p1.map(|handle| handle - first.p0), first.p2.map(|handle| handle - first.p3)], }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); @@ -280,12 +285,12 @@ impl ClosestSegment { let modification_type = VectorModificationType::InsertSegment { id: segment_ids[1], points: [midpoint, self.points[1]], - handles: [second.handle_start().map(|handle| handle - second.start), second.handle_end().map(|handle| handle - second.end)], + handles: [second.p1.map(|handle| handle - second.p0), second.p2.map(|handle| handle - second.p3)], }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); // G1 continuous on new handles - if self.bezier.handle_end().is_some() { + if pathseg_points(self.bezier).p2.is_some() { let handles = [HandleId::end(segment_ids[0]), HandleId::primary(segment_ids[1])]; let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: true }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); @@ -353,8 +358,8 @@ impl ClosestSegment { ) -> Option<[Option; 2]> { let transform = document.metadata().transform_to_viewport_if_feeds(self.layer, &document.network_interface); - let start = self.bezier.start; - let end = self.bezier.end; + let start = point_to_dvec2(self.bezier.start()); + let end = point_to_dvec2(self.bezier.end()); // Apply the drag delta to the segment's handles let b = self.bezier_point_to_viewport; @@ -1686,9 +1691,9 @@ impl ShapeState { let vector = network_interface.compute_modified_vector(layer)?; - for (segment, mut bezier, start, end) in vector.segment_bezier_iter() { - let t = bezier.project(layer_pos); - let layerspace = bezier.evaluate(TValue::Parametric(t)); + for (segment_id, mut segment, start, end) in vector.segment_iter() { + let t = segment.nearest(dvec2_to_point(layer_pos), DEFAULT_ACCURACY).t; + let layerspace = point_to_dvec2(segment.eval(t)); let screenspace = transform.transform_point2(layerspace); let distance_squared = screenspace.distance_squared(position); @@ -1697,20 +1702,22 @@ impl ShapeState { closest_distance_squared = distance_squared; // Convert to linear if handes are on top of control points - if let bezier_rs::BezierHandles::Cubic { handle_start, handle_end } = bezier.handles { - if handle_start.abs_diff_eq(bezier.start(), f64::EPSILON * 100.) && handle_end.abs_diff_eq(bezier.end(), f64::EPSILON * 100.) { - bezier = Bezier::from_linear_dvec2(bezier.start, bezier.end); + let PathSegPoints { p0: _, p1, p2, p3: _ } = pathseg_points(segment); + if let (Some(p1), Some(p2)) = (p1, p2) { + let segment_points = pathseg_points(segment); + if p1.abs_diff_eq(segment_points.p0, f64::EPSILON * 100.) && p2.abs_diff_eq(segment_points.p3, f64::EPSILON * 100.) { + segment = PathSeg::Line(Line::new(segment.start(), segment.end())); } } - let primary_handle = vector.colinear_manipulators.iter().find(|handles| handles.contains(&HandleId::primary(segment))); - let end_handle = vector.colinear_manipulators.iter().find(|handles| handles.contains(&HandleId::end(segment))); - let primary_handle = primary_handle.and_then(|&handles| handles.into_iter().find(|handle| handle.segment != segment)); - let end_handle = end_handle.and_then(|&handles| handles.into_iter().find(|handle| handle.segment != segment)); + let primary_handle = vector.colinear_manipulators.iter().find(|handles| handles.contains(&HandleId::primary(segment_id))); + let end_handle = vector.colinear_manipulators.iter().find(|handles| handles.contains(&HandleId::end(segment_id))); + let primary_handle = primary_handle.and_then(|&handles| handles.into_iter().find(|handle| handle.segment != segment_id)); + let end_handle = end_handle.and_then(|&handles| handles.into_iter().find(|handle| handle.segment != segment_id)); closest = Some(ClosestSegment { - segment, - bezier, + segment: segment_id, + bezier: segment, points: [start, end], colinear: [primary_handle, end_handle], t, @@ -2076,21 +2083,24 @@ impl ShapeState { }; // Selection segments - for (id, bezier, _, _) in vector.segment_bezier_iter() { + for (id, segment, _, _) in vector.segment_iter() { if select_segments { // Select segments if they lie inside the bounding box or lasso polygon - let segment_bbox = calculate_bezier_bbox(bezier); - let bottom_left = transform.transform_point2(segment_bbox[0]); - let top_right = transform.transform_point2(segment_bbox[1]); + let transformed_segment = Affine::new(transform.to_cols_array()) * segment; + let segment_bbox = transformed_segment.bounding_box(); let select = match selection_shape { - SelectionShape::Box(quad) => { - let enclosed = quad[0].min(quad[1]).cmple(bottom_left).all() && quad[0].max(quad[1]).cmpge(top_right).all(); + SelectionShape::Box(rect) => { + let enclosed = segment_bbox.contains_rect(rect); match selection_mode { SelectionMode::Enclosed => enclosed, _ => { // Check for intersection with the segment - enclosed || is_intersecting(bezier, quad, transform) + enclosed + || rect + .path_segments(DEFAULT_ACCURACY) + .map(|seg| seg.as_line().unwrap()) + .any(|line| !transformed_segment.intersect_line(line).is_empty()) } } } @@ -2098,7 +2108,7 @@ impl ShapeState { let polygon = polygon_subpath.as_ref().expect("If `selection_shape` is a polygon then subpath is constructed beforehand."); // Sample 10 points on the bezier and check if all or some lie inside the polygon - let points = bezier.compute_lookup_table(Some(10), None); + let points = pathseg_compute_lookup_table(segment, Some(10), false); match selection_mode { SelectionMode::Enclosed => points.map(|p| transform.transform_point2(p)).all(|p| polygon.contains_point(p)), _ => points.map(|p| transform.transform_point2(p)).any(|p| polygon.contains_point(p)), @@ -2111,13 +2121,15 @@ impl ShapeState { } } + let segment_points = pathseg_points(segment); + // Selecting handles - for (position, id) in [(bezier.handle_start(), ManipulatorPointId::PrimaryHandle(id)), (bezier.handle_end(), ManipulatorPointId::EndHandle(id))] { + for (position, id) in [(segment_points.p1, ManipulatorPointId::PrimaryHandle(id)), (segment_points.p2, ManipulatorPointId::EndHandle(id))] { let Some(position) = position else { continue }; let transformed_position = transform.transform_point2(position); let select = match selection_shape { - SelectionShape::Box(quad) => quad[0].min(quad[1]).cmple(transformed_position).all() && quad[0].max(quad[1]).cmpge(transformed_position).all(), + SelectionShape::Box(rect) => rect.contains(dvec2_to_point(transformed_position)), SelectionShape::Lasso(_) => polygon_subpath .as_ref() .expect("If `selection_shape` is a polygon then subpath is constructed beforehand.") @@ -2139,7 +2151,7 @@ impl ShapeState { let transformed_position = transform.transform_point2(position); let select = match selection_shape { - SelectionShape::Box(quad) => quad[0].min(quad[1]).cmple(transformed_position).all() && quad[0].max(quad[1]).cmpge(transformed_position).all(), + SelectionShape::Box(rect) => rect.contains(dvec2_to_point(transformed_position)), SelectionShape::Lasso(_) => polygon_subpath .as_ref() .expect("If `selection_shape` is a polygon then subpath is constructed beforehand.") diff --git a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs index aa43e0cb29..6d6d41a519 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -11,10 +11,10 @@ use crate::messages::tool::common_functionality::shape_editor::ShapeState; use crate::messages::tool::common_functionality::transformation_cage::BoundingBoxManager; use crate::messages::tool::tool_messages::tool_prelude::Key; use crate::messages::tool::utility_types::*; -use bezier_rs::Subpath; use glam::{DAffine2, DMat2, DVec2}; use graph_craft::document::NodeInput; use graph_craft::document::value::TaggedValue; +use graphene_core::subpath::{self, Subpath}; use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::misc::{ArcType, dvec2_to_point}; use kurbo::{BezPath, PathEl, Shape}; @@ -363,9 +363,9 @@ pub fn arc_outline(layer: Option, document: &DocumentMessag start_angle / 360. * std::f64::consts::TAU, sweep_angle / 360. * std::f64::consts::TAU, match arc_type { - ArcType::Open => bezier_rs::ArcType::Open, - ArcType::Closed => bezier_rs::ArcType::Closed, - ArcType::PieSlice => bezier_rs::ArcType::PieSlice, + ArcType::Open => subpath::ArcType::Open, + ArcType::Closed => subpath::ArcType::Closed, + ArcType::PieSlice => subpath::ArcType::PieSlice, }, ))]; let viewport = document.metadata().transform_to_viewport(layer); diff --git a/editor/src/messages/tool/common_functionality/snapping.rs b/editor/src/messages/tool/common_functionality/snapping.rs index c9628e3c99..ef812d34b7 100644 --- a/editor/src/messages/tool/common_functionality/snapping.rs +++ b/editor/src/messages/tool/common_functionality/snapping.rs @@ -10,14 +10,16 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye use crate::messages::portfolio::document::utility_types::misc::{GridSnapTarget, PathSnapTarget, SnapTarget}; use crate::messages::prelude::*; pub use alignment_snapper::*; -use bezier_rs::TValue; pub use distribution_snapper::*; use glam::{DAffine2, DVec2}; use graphene_std::renderer::Quad; use graphene_std::renderer::Rect; use graphene_std::vector::NoHashBuilder; use graphene_std::vector::PointId; +use graphene_std::vector::algorithms::intersection::filtered_segment_intersections; +use graphene_std::vector::misc::point_to_dvec2; pub use grid_snapper::*; +use kurbo::ParamCurve; pub use layer_snapper::*; pub use snap_results::*; use std::cmp::Ordering; @@ -141,8 +143,8 @@ fn get_closest_intersection(snap_to: DVec2, curves: &[SnappedCurve]) -> Option Option Option { let mut best = None; for line_i in lines { diff --git a/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs b/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs index 0c4148e563..5ab9ab050e 100644 --- a/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs +++ b/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs @@ -3,11 +3,16 @@ use crate::consts::HIDE_HANDLE_DISTANCE; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::misc::*; use crate::messages::prelude::*; -use bezier_rs::{Bezier, Identifier, Subpath, TValue}; use glam::{DAffine2, DVec2}; -use graphene_std::math::math_ext::QuadExt; +use graphene_core::subpath::{Identifier, ManipulatorGroup, Subpath}; use graphene_std::renderer::Quad; +use graphene_std::subpath::pathseg_points; use graphene_std::vector::PointId; +use graphene_std::vector::algorithms::bezpath_algorithms::{pathseg_normals_to_point, pathseg_tangents_to_point}; +use graphene_std::vector::algorithms::intersection::filtered_segment_intersections; +use graphene_std::vector::misc::point_to_dvec2; +use graphene_std::{math::math_ext::QuadExt, vector::misc::dvec2_to_point}; +use kurbo::{Affine, DEFAULT_ACCURACY, Nearest, ParamCurve, ParamCurveNearest, PathSeg}; #[derive(Clone, Debug, Default)] pub struct LayerSnapper { @@ -37,7 +42,7 @@ impl LayerSnapper { return; } - for document_curve in bounds.bezier_lines() { + for document_curve in bounds.to_lines() { self.paths_to_snap.push(SnapCandidatePath { document_curve, layer, @@ -70,7 +75,7 @@ impl LayerSnapper { if document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::IntersectionPoint)) || document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AlongPath)) { for subpath in document.metadata().layer_outline(layer) { for (start_index, curve) in subpath.iter().enumerate() { - let document_curve = curve.apply_transformation(|p| transform.transform_point2(p)); + let document_curve = Affine::new(transform.to_cols_array()) * curve; let start = subpath.manipulator_groups()[start_index].id; if snap_data.ignore_manipulator(layer, start) || snap_data.ignore_manipulator(layer, subpath.manipulator_groups()[(start_index + 1) % subpath.len()].id) { continue; @@ -98,13 +103,12 @@ impl LayerSnapper { for path in &self.paths_to_snap { // Skip very short paths - if path.document_curve.start.distance_squared(path.document_curve.end) < tolerance * tolerance * 2. { + if path.document_curve.start().distance_squared(path.document_curve.end()) < tolerance * tolerance * 2. { continue; } - let time = path.document_curve.project(point.document_point); - let snapped_point_document = path.document_curve.evaluate(bezier_rs::TValue::Parametric(time)); - - let distance = snapped_point_document.distance(point.document_point); + let Nearest { distance_sq, t } = path.document_curve.nearest(dvec2_to_point(point.document_point), DEFAULT_ACCURACY); + let snapped_point_document = point_to_dvec2(path.document_curve.eval(t)); + let distance = distance_sq.sqrt(); if distance < tolerance { snap_results.curves.push(SnappedCurve { @@ -144,8 +148,8 @@ impl LayerSnapper { for path in &self.paths_to_snap { for constraint_path in constraint_path.iter() { - for time in path.document_curve.intersections(&constraint_path, None, None) { - let snapped_point_document = path.document_curve.evaluate(bezier_rs::TValue::Parametric(time)); + for time in filtered_segment_intersections(path.document_curve, constraint_path, None, None) { + let snapped_point_document = point_to_dvec2(path.document_curve.eval(time)); let distance = snapped_point_document.distance(point.document_point); @@ -266,8 +270,8 @@ impl LayerSnapper { fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool, point: &SnapCandidatePoint, tolerance: f64, snap_results: &mut SnapResults) { if normals && path.bounds.is_none() { for &neighbor in &point.neighbors { - for t in path.document_curve.normals_to_point(neighbor) { - let normal_point = path.document_curve.evaluate(TValue::Parametric(t)); + for t in pathseg_normals_to_point(path.document_curve, dvec2_to_point(neighbor)) { + let normal_point = point_to_dvec2(path.document_curve.eval(t)); let distance = normal_point.distance(point.document_point); if distance > tolerance { continue; @@ -287,8 +291,8 @@ fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool, } if tangents && path.bounds.is_none() { for &neighbor in &point.neighbors { - for t in path.document_curve.tangents_to_point(neighbor) { - let tangent_point = path.document_curve.evaluate(TValue::Parametric(t)); + for t in pathseg_tangents_to_point(path.document_curve, dvec2_to_point(neighbor)) { + let tangent_point = point_to_dvec2(path.document_curve.eval(t)); let distance = tangent_point.distance(point.document_point); if distance > tolerance { continue; @@ -310,7 +314,7 @@ fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool, #[derive(Clone, Debug)] struct SnapCandidatePath { - document_curve: Bezier, + document_curve: PathSeg, layer: LayerNodeIdentifier, start: PointId, target: SnapTarget, @@ -440,12 +444,13 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath= crate::consts::MAX_LAYER_SNAP_POINTS { return; } + let curve = pathseg_points(curve); - let in_handle = curve.handle_start().map(|handle| handle - curve.start).filter(handle_not_under(to_document)); - let out_handle = curve.handle_end().map(|handle| handle - curve.end).filter(handle_not_under(to_document)); + let in_handle = curve.p1.map(|handle| handle - curve.p0).filter(handle_not_under(to_document)); + let out_handle = curve.p2.map(|handle| handle - curve.p3).filter(handle_not_under(to_document)); if in_handle.is_none() && out_handle.is_none() { points.push(SnapCandidatePoint::new( - to_document.transform_point2(curve.start() * 0.5 + curve.end * 0.5), + to_document.transform_point2(curve.p0 * 0.5 + curve.p3 * 0.5), SnapSource::Path(PathSnapSource::LineMidpoint), SnapTarget::Path(PathSnapTarget::LineMidpoint), Some(layer), @@ -487,7 +492,7 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath, to_document: DAffine2, subpath: &Subpath, index: usize) -> bool { +pub fn are_manipulator_handles_colinear(manipulators: &ManipulatorGroup, to_document: DAffine2, subpath: &Subpath, index: usize) -> bool { let anchor = manipulators.anchor; let handle_in = manipulators.in_handle.map(|handle| anchor - handle).filter(handle_not_under(to_document)); let handle_out = manipulators.out_handle.map(|handle| handle - anchor).filter(handle_not_under(to_document)); diff --git a/editor/src/messages/tool/common_functionality/snapping/snap_results.rs b/editor/src/messages/tool/common_functionality/snapping/snap_results.rs index 3ab53e4dd7..415ead679f 100644 --- a/editor/src/messages/tool/common_functionality/snapping/snap_results.rs +++ b/editor/src/messages/tool/common_functionality/snapping/snap_results.rs @@ -2,11 +2,11 @@ use super::DistributionMatch; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::misc::{DistributionSnapTarget, SnapSource, SnapTarget}; use crate::messages::tool::common_functionality::snapping::SnapCandidatePoint; -use bezier_rs::Bezier; use glam::DVec2; use graphene_std::renderer::Quad; use graphene_std::renderer::Rect; use graphene_std::vector::PointId; +use kurbo::PathSeg; use std::collections::VecDeque; #[derive(Clone, Debug, Default)] @@ -120,5 +120,5 @@ pub struct SnappedCurve { pub layer: LayerNodeIdentifier, pub start: PointId, pub point: SnappedPoint, - pub document_curve: Bezier, + pub document_curve: PathSeg, } diff --git a/editor/src/messages/tool/common_functionality/utility_functions.rs b/editor/src/messages/tool/common_functionality/utility_functions.rs index c425ada6cc..294147419c 100644 --- a/editor/src/messages/tool/common_functionality/utility_functions.rs +++ b/editor/src/messages/tool/common_functionality/utility_functions.rs @@ -9,16 +9,17 @@ use crate::messages::tool::common_functionality::graph_modification_utils::{Node use crate::messages::tool::common_functionality::transformation_cage::SelectedEdges; use crate::messages::tool::tool_messages::path_tool::PathOverlayMode; use crate::messages::tool::utility_types::ToolType; -use bezier_rs::{Bezier, BezierHandles}; use glam::{DAffine2, DVec2}; use graph_craft::concrete; use graph_craft::document::value::TaggedValue; +use graphene_core::subpath::{Bezier, BezierHandles}; use graphene_std::renderer::Quad; use graphene_std::table::Table; use graphene_std::text::{FontCache, load_font}; -use graphene_std::vector::misc::{HandleId, ManipulatorPointId}; +use graphene_std::vector::algorithms::bezpath_algorithms::pathseg_compute_lookup_table; +use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point}; use graphene_std::vector::{HandleExt, PointId, SegmentId, Vector, VectorModification, VectorModificationType}; -use kurbo::{CubicBez, Line, ParamCurveExtrema, PathSeg, Point, QuadBez}; +use kurbo::{CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, PathSeg, Point, QuadBez, Shape}; /// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable. pub fn should_extend( @@ -208,25 +209,6 @@ pub fn is_visible_point( } } -/// Function to find the bounding box of bezier (uses method from kurbo) -pub fn calculate_bezier_bbox(bezier: Bezier) -> [DVec2; 2] { - let start = Point::new(bezier.start.x, bezier.start.y); - let end = Point::new(bezier.end.x, bezier.end.y); - let bbox = match bezier.handles { - BezierHandles::Cubic { handle_start, handle_end } => { - let p1 = Point::new(handle_start.x, handle_start.y); - let p2 = Point::new(handle_end.x, handle_end.y); - CubicBez::new(start, p1, p2, end).bounding_box() - } - BezierHandles::Quadratic { handle } => { - let p1 = Point::new(handle.x, handle.y); - QuadBez::new(start, p1, end).bounding_box() - } - BezierHandles::Linear => Line::new(start, end).bounding_box(), - }; - [DVec2::new(bbox.x0, bbox.y0), DVec2::new(bbox.x1, bbox.y1)] -} - pub fn is_intersecting(bezier: Bezier, quad: [DVec2; 2], transform: DAffine2) -> bool { let to_layerspace = transform.inverse(); let quad = [to_layerspace.transform_point2(quad[0]), to_layerspace.transform_point2(quad[1])]; @@ -496,19 +478,19 @@ pub fn log_optimization(a: f64, b: f64, p1: DVec2, p3: DVec2, d1: DVec2, d2: DVe let c1 = p1 + d1 * start_handle_length; let c2 = p3 + d2 * end_handle_length; - let new_curve = Bezier::from_cubic_coordinates(p1.x, p1.y, c1.x, c1.y, c2.x, c2.y, p3.x, p3.y); + let new_curve = PathSeg::Cubic(CubicBez::new(Point::new(p1.x, p1.y), Point::new(c1.x, c1.y), Point::new(c2.x, c2.y), Point::new(p3.x, p3.y))); // Sample 2*n points from new curve and get the L2 metric between all of points - let points = new_curve.compute_lookup_table(Some(2 * n), None).collect::>(); + let points = pathseg_compute_lookup_table(new_curve, Some(2 * n), false); - let dist = points1.iter().zip(points.iter()).map(|(p1, p2)| (p1.x - p2.x).powi(2) + (p1.y - p2.y).powi(2)).sum::(); + let dist = points1.iter().zip(points).map(|(p1, p2)| (p1.x - p2.x).powi(2) + (p1.y - p2.y).powi(2)).sum::(); dist / (2 * n) as f64 } /// Calculates optimal handle lengths with adam optimization. #[allow(clippy::too_many_arguments)] -pub fn find_two_param_best_approximate(p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, min_len1: f64, min_len2: f64, farther_segment: Bezier, other_segment: Bezier) -> (DVec2, DVec2) { +pub fn find_two_param_best_approximate(p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, min_len1: f64, min_len2: f64, farther_segment: PathSeg, other_segment: PathSeg) -> (DVec2, DVec2) { let h = 1e-6; let tol = 1e-6; let max_iter = 200; @@ -530,21 +512,25 @@ pub fn find_two_param_best_approximate(p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec let n = 20; - let farther_segment = if farther_segment.start.distance(p1) >= f64::EPSILON { + let farther_segment = if farther_segment.start().distance(dvec2_to_point(p1)) >= f64::EPSILON { farther_segment.reverse() } else { farther_segment }; - let other_segment = if other_segment.end.distance(p3) >= f64::EPSILON { other_segment.reverse() } else { other_segment }; + let other_segment = if other_segment.end().distance(dvec2_to_point(p3)) >= f64::EPSILON { + other_segment.reverse() + } else { + other_segment + }; // Now we sample points proportional to the lengths of the beziers - let l1 = farther_segment.length(None); - let l2 = other_segment.length(None); + let l1 = farther_segment.perimeter(DEFAULT_ACCURACY); + let l2 = other_segment.perimeter(DEFAULT_ACCURACY); let ratio = l1 / (l1 + l2); let n_points1 = ((2 * n) as f64 * ratio).floor() as usize; - let mut points1 = farther_segment.compute_lookup_table(Some(n_points1), None).collect::>(); - let mut points2 = other_segment.compute_lookup_table(Some(n), None).collect::>(); + let mut points1 = pathseg_compute_lookup_table(farther_segment, Some(n_points1), false).collect::>(); + let mut points2 = pathseg_compute_lookup_table(other_segment, Some(n), false).collect::>(); points1.append(&mut points2); let f = |a: f64, b: f64| -> f64 { log_optimization(a, b, p1, p3, d1, d2, &points1, n) }; diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 00c99bd975..f76c41bc11 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -21,14 +21,16 @@ use crate::messages::tool::common_functionality::shape_editor::{ }; use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager}; use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, find_two_param_best_approximate, make_path_editable_is_allowed}; -use bezier_rs::{Bezier, BezierHandles, TValue}; use graphene_std::Color; use graphene_std::renderer::Quad; +use graphene_std::subpath::pathseg_points; use graphene_std::transform::ReferencePoint; use graphene_std::uuid::NodeId; +use graphene_std::vector::algorithms::util::pathseg_tangent; use graphene_std::vector::click_target::ClickTargetType; -use graphene_std::vector::misc::{HandleId, ManipulatorPointId}; +use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point, point_to_dvec2}; use graphene_std::vector::{HandleExt, NoHashBuilder, PointId, SegmentId, Vector, VectorModificationType}; +use kurbo::{DEFAULT_ACCURACY, ParamCurve, ParamCurveNearest, PathSeg, Rect}; use std::vec; #[derive(Default, ExtractField)] @@ -494,7 +496,7 @@ pub enum PointSelectState { #[derive(Clone, Copy)] pub struct SlidingSegmentData { segment_id: SegmentId, - bezier: Bezier, + bezier: PathSeg, start: PointId, } @@ -840,13 +842,12 @@ impl PathToolData { responses.add(OverlaysMessage::Draw); PathToolFsmState::Dragging(self.dragging_state) } else { - let start_pos = segment.bezier().start; - let end_pos = segment.bezier().end; + let points = pathseg_points(segment.pathseg()); - let [pos1, pos2] = match segment.bezier().handles { - BezierHandles::Cubic { handle_start, handle_end } => [handle_start, handle_end], - BezierHandles::Quadratic { handle } => [handle, end_pos], - BezierHandles::Linear => [start_pos + (end_pos - start_pos) / 3., end_pos + (start_pos - end_pos) / 3.], + let [pos1, pos2] = match (points.p1, points.p2) { + (Some(p1), Some(p2)) => [p1, p2], + (Some(p1), None) | (None, Some(p1)) => [p1, points.p3], + (None, None) => [points.p0 + (points.p3 - points.p0) / 3., points.p3 + (points.p0 - points.p3) / 3.], }; self.molding_info = Some((pos1, pos2)); PathToolFsmState::Dragging(self.dragging_state) @@ -1215,7 +1216,7 @@ impl PathToolData { let Some(point_id) = anchor.as_anchor() else { return false }; let mut connected_segments = [None, None]; - for (segment, bezier, start, end) in vector.segment_bezier_iter() { + for (segment, bezier, start, end) in vector.segment_iter() { if start == point_id || end == point_id { match (connected_segments[0], connected_segments[1]) { (None, None) => connected_segments[0] = Some(SlidingSegmentData { segment_id: segment, bezier, start }), @@ -1255,11 +1256,11 @@ impl PathToolData { let segments = sliding_point_info.connected_segments; - let t1 = segments[0].bezier.project(layer_pos); - let position1 = segments[0].bezier.evaluate(TValue::Parametric(t1)); + let t1 = segments[0].bezier.nearest(dvec2_to_point(layer_pos), DEFAULT_ACCURACY).t; + let position1 = point_to_dvec2(segments[0].bezier.eval(t1)); - let t2 = segments[1].bezier.project(layer_pos); - let position2 = segments[1].bezier.evaluate(TValue::Parametric(t2)); + let t2 = segments[1].bezier.nearest(dvec2_to_point(layer_pos), DEFAULT_ACCURACY).t; + let position2 = point_to_dvec2(segments[1].bezier.eval(t2)); let (closer_segment, farther_segment, t_value, new_position) = if position2.distance(layer_pos) < position1.distance(layer_pos) { (segments[1], segments[0], t2, position2) @@ -1276,39 +1277,48 @@ impl PathToolData { shape_editor.move_anchor(anchor, &vector, delta, layer, None, responses); // Make a split at the t_value - let [first, second] = closer_segment.bezier.split(TValue::Parametric(t_value)); - let closer_segment_other_point = if anchor == closer_segment.start { closer_segment.bezier.end } else { closer_segment.bezier.start }; + let first = closer_segment.bezier.subsegment(0f64..t_value); + let second = closer_segment.bezier.subsegment(t_value..1.); - let (split_segment, other_segment) = if first.start == closer_segment_other_point { (first, second) } else { (second, first) }; + let closer_segment_other_point = if anchor == closer_segment.start { + closer_segment.bezier.end() + } else { + closer_segment.bezier.start() + }; + + let (split_segment, other_segment) = if first.start() == closer_segment_other_point { (first, second) } else { (second, first) }; + let split_segment_points = pathseg_points(split_segment); // Primary handle maps to primary handle and secondary maps to secondary let closer_primary_handle = HandleId::primary(closer_segment.segment_id); - let Some(handle_position) = split_segment.handle_start() else { return }; - let relative_position1 = handle_position - split_segment.start; + let Some(handle_position) = split_segment_points.p1 else { return }; + let relative_position1 = handle_position - split_segment_points.p0; let modification_type = closer_primary_handle.set_relative_position(relative_position1); responses.add(GraphOperationMessage::Vector { layer, modification_type }); let closer_secondary_handle = HandleId::end(closer_segment.segment_id); - let Some(handle_position) = split_segment.handle_end() else { return }; - let relative_position2 = handle_position - split_segment.end; + let Some(handle_position) = split_segment_points.p2 else { return }; + let relative_position2 = handle_position - split_segment_points.p3; let modification_type = closer_secondary_handle.set_relative_position(relative_position2); responses.add(GraphOperationMessage::Vector { layer, modification_type }); let end_handle_direction = if anchor == closer_segment.start { -relative_position1 } else { -relative_position2 }; + let farther_segment_points = pathseg_points(farther_segment.bezier); + let (farther_other_point, start_handle, end_handle, start_handle_pos) = if anchor == farther_segment.start { ( - farther_segment.bezier.end, + farther_segment_points.p3, HandleId::end(farther_segment.segment_id), HandleId::primary(farther_segment.segment_id), - farther_segment.bezier.handle_end(), + farther_segment_points.p2, ) } else { ( - farther_segment.bezier.start, + farther_segment_points.p0, HandleId::primary(farther_segment.segment_id), HandleId::end(farther_segment.segment_id), - farther_segment.bezier.handle_start(), + farther_segment_points.p1, ) }; let Some(start_handle_position) = start_handle_pos else { return }; @@ -1317,9 +1327,9 @@ impl PathToolData { // Get normalized direction vectors, if cubic handle is zero then we consider corresponding tangent let d1 = start_handle_direction.try_normalize().unwrap_or({ if anchor == farther_segment.start { - -farther_segment.bezier.tangent(TValue::Parametric(0.99)) + -pathseg_tangent(farther_segment.bezier, 1.) } else { - farther_segment.bezier.tangent(TValue::Parametric(0.01)) + pathseg_tangent(farther_segment.bezier, 0.) } }); @@ -1737,13 +1747,13 @@ impl Fsm for PathToolFsmState { if tool_options.path_editing_mode.segment_editing_mode && !tool_data.segment_editing_modifier { let transform = document.metadata().transform_to_viewport_if_feeds(closest_segment.layer(), &document.network_interface); - overlay_context.outline_overlay_bezier(closest_segment.bezier(), transform); + overlay_context.outline_overlay_bezier(closest_segment.pathseg(), transform); // Draw the anchors again let display_anchors = overlay_context.visibility_settings.anchors(); if display_anchors { - let start_pos = transform.transform_point2(closest_segment.bezier().start); - let end_pos = transform.transform_point2(closest_segment.bezier().end); + let start_pos = transform.transform_point2(point_to_dvec2(closest_segment.pathseg().start())); + let end_pos = transform.transform_point2(point_to_dvec2(closest_segment.pathseg().end())); let start_id = closest_segment.points()[0]; let end_id = closest_segment.points()[1]; if let Some(shape_state) = shape_editor.selected_shape_state.get_mut(&closest_segment.layer()) { @@ -1820,7 +1830,7 @@ impl Fsm for PathToolFsmState { let (points_inside, segments_inside) = match selection_shape { SelectionShapeType::Box => { let previous_mouse = document.metadata().document_to_viewport.transform_point2(tool_data.previous_mouse_position); - let bbox = [tool_data.drag_start_pos, previous_mouse]; + let bbox = Rect::new(tool_data.drag_start_pos.x, tool_data.drag_start_pos.y, previous_mouse.x, previous_mouse.y); shape_editor.get_inside_points_and_segments( &document.network_interface, SelectionShape::Box(bbox), @@ -1865,7 +1875,7 @@ impl Fsm for PathToolFsmState { let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface); - for (segment, bezier, _, _) in vector.segment_bezier_iter() { + for (segment, bezier, _, _) in vector.segment_iter() { if segments.contains(&segment) { overlay_context.outline_overlay_bezier(bezier, transform); } @@ -2241,7 +2251,8 @@ impl Fsm for PathToolFsmState { match selection_shape { SelectionShapeType::Box => { - let bbox = [tool_data.drag_start_pos, previous_mouse]; + let bbox = Rect::new(tool_data.drag_start_pos.x, tool_data.drag_start_pos.y, previous_mouse.x, previous_mouse.y); + shape_editor.select_all_in_shape( &document.network_interface, SelectionShape::Box(bbox), @@ -2337,7 +2348,8 @@ impl Fsm for PathToolFsmState { } else { match selection_shape { SelectionShapeType::Box => { - let bbox = [tool_data.drag_start_pos, previous_mouse]; + let bbox = Rect::new(tool_data.drag_start_pos.x, tool_data.drag_start_pos.y, previous_mouse.x, previous_mouse.y); + shape_editor.select_all_in_shape( &document.network_interface, SelectionShape::Box(bbox), @@ -2697,16 +2709,13 @@ impl Fsm for PathToolFsmState { // Create new segment ids and add the segments into the existing vector content let mut segments_map = HashMap::new(); - for (segment_id, bezier, start, end) in new_vector.segment_bezier_iter() { + for (segment_id, bezier, start, end) in new_vector.segment_iter() { let new_segment_id = SegmentId::generate(); segments_map.insert(segment_id, new_segment_id); - let handles = match bezier.handles { - BezierHandles::Linear => [None, None], - BezierHandles::Quadratic { handle } => [Some(handle - bezier.start), None], - BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - bezier.start), Some(handle_end - bezier.end)], - }; + let points = pathseg_points(bezier); + let handles = [points.p1, points.p2]; let points = [points_map[&start], points_map[&end]]; let modification_type = VectorModificationType::InsertSegment { id: new_segment_id, points, handles }; @@ -2802,7 +2811,7 @@ impl Fsm for PathToolFsmState { let mut segments_map = HashMap::new(); - for (segment_id, bezier, start, end) in old_vector.segment_bezier_iter() { + for (segment_id, bezier, start, end) in old_vector.segment_iter() { let both_ends_selected = layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(start)) && layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(end)); let segment_selected = layer_selection_state.is_segment_selected(segment_id); @@ -2811,11 +2820,8 @@ impl Fsm for PathToolFsmState { let new_id = SegmentId::generate(); segments_map.insert(segment_id, new_id); - let handles = match bezier.handles { - BezierHandles::Linear => [None, None], - BezierHandles::Quadratic { handle } => [Some(handle - bezier.start), None], - BezierHandles::Cubic { handle_start, handle_end } => [Some(handle_start - bezier.start), Some(handle_end - bezier.end)], - }; + let points = pathseg_points(bezier); + let handles = [points.p1, points.p2]; let points = [points_map[&start], points_map[&end]]; let modification_type = VectorModificationType::InsertSegment { id: new_id, points, handles }; diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 00ec54db8d..7c95daa5a2 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -11,11 +11,12 @@ use crate::messages::tool::common_functionality::graph_modification_utils::{self use crate::messages::tool::common_functionality::shape_editor::ShapeState; use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration}; use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, closest_point, should_extend}; -use bezier_rs::{Bezier, BezierHandles}; use graph_craft::document::NodeId; use graphene_std::Color; -use graphene_std::vector::misc::{HandleId, ManipulatorPointId}; +use graphene_std::subpath::pathseg_points; +use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point}; use graphene_std::vector::{NoHashBuilder, PointId, SegmentId, StrokeId, Vector, VectorModificationType}; +use kurbo::{CubicBez, PathSeg}; #[derive(Default, ExtractField)] pub struct PenTool { @@ -1257,7 +1258,7 @@ impl PenToolData { let vector = document.network_interface.compute_modified_vector(layer); let (handle_start, in_segment) = if let Some(vector) = &vector { vector - .segment_bezier_iter() + .segment_iter() .find_map(|(segment_id, bezier, start, end)| { let is_end = point == end; let is_start = point == start; @@ -1265,15 +1266,16 @@ impl PenToolData { return None; } - let handle = match bezier.handles { - BezierHandles::Cubic { handle_start, handle_end, .. } => { + let points = pathseg_points(bezier); + let handle = match (points.p1, points.p2) { + (Some(p1), Some(p2)) => { if is_start { - handle_start + p1 } else { - handle_end + p2 } } - BezierHandles::Quadratic { handle } => handle, + (Some(p1), None) | (None, Some(p1)) => p1, _ => return None, }; Some((segment_id, is_end, handle)) @@ -1599,9 +1601,8 @@ impl Fsm for PenToolFsmState { let handle_start = tool_data.latest_point().map(|point| transform.transform_point2(point.handle_start)); if let (Some((start, handle_start)), Some(handle_end)) = (tool_data.latest_point().map(|point| (point.pos, point.handle_start)), tool_data.handle_end) { - let handles = BezierHandles::Cubic { handle_start, handle_end }; let end = tool_data.next_point; - let bezier = Bezier { start, handles, end }; + let bezier = PathSeg::Cubic(CubicBez::new(dvec2_to_point(start), dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(end))); if (end - start).length_squared() > f64::EPSILON { // Draw the curve for the currently-being-placed segment overlay_context.outline_bezier(bezier, transform); @@ -1723,7 +1724,7 @@ impl Fsm for PenToolFsmState { // We have the point. Join the 2 vertices and check if any path is closed. if let Some(end) = closest_point { let segment_id = SegmentId::generate(); - vector.push(segment_id, start, end, BezierHandles::Cubic { handle_start, handle_end }, StrokeId::ZERO); + vector.push(segment_id, start, end, (Some(handle_start), Some(handle_end)), StrokeId::ZERO); let grouped_segments = vector.auto_join_paths(); let closed_paths = grouped_segments.iter().filter(|path| path.is_closed() && path.contains(segment_id)); diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 31bd2c3a4b..2acccad47d 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -20,9 +20,9 @@ use crate::messages::tool::common_functionality::shape_editor::SelectionShapeTyp use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapManager}; use crate::messages::tool::common_functionality::transformation_cage::*; use crate::messages::tool::common_functionality::utility_functions::{resize_bounds, rotate_bounds, skew_bounds, text_bounding_box, transforming_transform_cage}; -use bezier_rs::Subpath; use glam::DMat2; use graph_craft::document::NodeId; +use graphene_core::subpath::Subpath; use graphene_std::path_bool::BooleanOperation; use graphene_std::renderer::Quad; use graphene_std::renderer::Rect; diff --git a/node-graph/gcore/src/subpath/core.rs b/node-graph/gcore/src/subpath/core.rs index f61eb2dcb9..ac436d11e4 100644 --- a/node-graph/gcore/src/subpath/core.rs +++ b/node-graph/gcore/src/subpath/core.rs @@ -6,10 +6,10 @@ use glam::DVec2; use kurbo::PathSeg; pub struct PathSegPoints { - p0: DVec2, - p1: Option, - p2: Option, - p3: DVec2, + pub p0: DVec2, + pub p1: Option, + pub p2: Option, + pub p3: DVec2, } impl PathSegPoints { diff --git a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs index f6dd1b31d9..6f069c615e 100644 --- a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs +++ b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs @@ -1,10 +1,11 @@ use super::intersection::bezpath_intersections; use super::poisson_disk::poisson_disk_sample; -use super::util::segment_tangent; +use super::symmetrical_basis::{SymmetricalBasis, to_symmetrical_basis_pair}; +use super::util::pathseg_tangent; use crate::vector::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE; 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 kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, QuadBez, Rect, Shape, Vec2}; use std::f64::consts::{FRAC_PI_2, PI}; /// Splits the [`BezPath`] at segment index at `t` value which lie in the range of [0, 1]. @@ -187,6 +188,51 @@ pub enum TValue { Euclidean(f64), } +/// Default LUT step size in `compute_lookup_table` function. +pub const DEFAULT_LUT_STEP_SIZE: usize = 10; + +/// Return a selection of equidistant points on the bezier curve. +/// If no value is provided for `steps`, then the function will default `steps` to be 10. +/// +pub fn pathseg_compute_lookup_table(segment: PathSeg, steps: Option, eucliean: bool) -> impl Iterator { + let steps = steps.unwrap_or(DEFAULT_LUT_STEP_SIZE); + + (0..=steps).map(move |t| { + let tvalue = if eucliean { + TValue::Euclidean(t as f64 / steps as f64) + } else { + TValue::Parametric(t as f64 / steps as f64) + }; + let t = eval_pathseg(segment, tvalue); + point_to_dvec2(segment.eval(t)) + }) +} + +/// Find the `t`-value(s) such that the normal(s) at `t` pass through the specified point. +pub fn pathseg_normals_to_point(segment: PathSeg, point: Point) -> Vec { + let point = DVec2::new(point.x, point.y); + + let sbasis = to_symmetrical_basis_pair(segment); + let derivative = sbasis.derivative(); + let cross = (sbasis - point).dot(&derivative); + + SymmetricalBasis::roots(&cross) +} + +/// Find the `t`-value(s) such that the tangent(s) at `t` pass through the given point. +pub fn pathseg_tangents_to_point(segment: PathSeg, point: Point) -> Vec { + segment.to_cubic().tangents_to_point(point).to_vec() +} + +fn parameters(cubic_bez: CubicBez) -> (Vec2, Vec2, Vec2, Vec2) { + let c = (cubic_bez.p1 - cubic_bez.p0) * 3.0; + let b = (cubic_bez.p2 - cubic_bez.p1) * 3.0 - c; + let d = cubic_bez.p0.to_vec2(); + let a = cubic_bez.p3.to_vec2() - d - c - b; + + (a, b, c, d) +} + /// Return the subsegment for the given [TValue] range. Returns None if parametric value of `t1` is greater than `t2`. pub fn trim_pathseg(segment: PathSeg, t1: TValue, t2: TValue) -> Option { let t1 = eval_pathseg(segment, t1); @@ -392,8 +438,8 @@ pub fn miter_line_join(bezpath1: &BezPath, bezpath2: &BezPath, miter_limit: Opti 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.); + let in_tangent = pathseg_tangent(in_segment, 1.); + let out_tangent = pathseg_tangent(out_segment, 0.); if in_tangent == DVec2::ZERO || out_tangent == DVec2::ZERO { // Avoid panic from normalizing zero vectors @@ -454,7 +500,7 @@ pub fn round_line_join(bezpath1: &BezPath, bezpath2: &BezPath, center: DVec2) -> 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 in_tangent = in_segment.map(|in_segment| pathseg_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); diff --git a/node-graph/gcore/src/vector/algorithms/mod.rs b/node-graph/gcore/src/vector/algorithms/mod.rs index b9284f327d..15d0f53d14 100644 --- a/node-graph/gcore/src/vector/algorithms/mod.rs +++ b/node-graph/gcore/src/vector/algorithms/mod.rs @@ -6,4 +6,5 @@ pub mod merge_by_distance; pub mod offset_subpath; pub mod poisson_disk; pub mod spline; +pub(crate) mod symmetrical_basis; pub mod util; diff --git a/node-graph/gcore/src/vector/algorithms/symmetrical_basis.rs b/node-graph/gcore/src/vector/algorithms/symmetrical_basis.rs new file mode 100644 index 0000000000..4a6f3ea847 --- /dev/null +++ b/node-graph/gcore/src/vector/algorithms/symmetrical_basis.rs @@ -0,0 +1,613 @@ +/* + * Modifications and Rust port copyright (C) 2024 by 0Hypercube. + * + * Original version by lib2geom: + * + * The entirety of this file is specially licensed under MPL 1.1 terms: + * + * Original Authors: + * Nathan Hurst + * Michael Sloan + * Marco Cecchetti + * MenTaLguY + * Michael Sloan + * Nathan Hurst + * Krzysztof Kosiński + * And additional authors listed in the version control history of the following files: + * - https://gitlab.com/inkscape/lib2geom/-/blob/master/include/2geom/sbasis.h + * - https://gitlab.com/inkscape/lib2geom/-/blob/master/src/2geom/sbasis.cpp + * - https://gitlab.com/inkscape/lib2geom/-/blob/master/src/2geom/sbasis-to-bezier.cpp + * - https://gitlab.com/inkscape/lib2geom/-/blob/master/src/2geom/bezier.cpp + * - https://gitlab.com/inkscape/lib2geom/-/blob/master/src/2geom/solve-bezier.cpp + * - https://gitlab.com/inkscape/lib2geom/-/blob/master/src/2geom/solve-bezier-one-d.cpp + * + * Copyright (C) 2006-2015 Original Authors + * + * This file is free software; you can redistribute it and/or modify it + * either under the terms of the Mozilla Public License Version 1.1 (the + * "MPL"). + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * https://www.mozilla.org/MPL/1.1/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the MPL for the specific + * language governing rights and limitations. + */ + +use glam::DVec2; +use kurbo::PathSeg; + +use crate::vector::misc::pathseg_points_vec; + +// Note that the built in signum cannot be used as it does not handle 0 the same way as in the C code. +fn sign(x: f64) -> i8 { + if x > 0. { + 1 + } else if x < 0. { + -1 + } else { + 0 + } +} + +// https://gitlab.com/inkscape/lib2geom/-/blob/master/include/2geom/sbasis.h#L70 +#[derive(Debug, Clone)] +pub(crate) struct SymmetricalBasis(pub Vec); + +impl SymmetricalBasis { + // https://gitlab.com/inkscape/lib2geom/-/blob/master/src/2geom/sbasis.cpp#L323 + #[must_use] + fn derivative(&self) -> SymmetricalBasis { + let mut c = SymmetricalBasis(vec![DVec2::ZERO; self.len()]); + if self.iter().all(|x| x.abs_diff_eq(DVec2::ZERO, 1e-5)) { + return c; + } + for k in 0..(self.len() - 1) { + let d = (2. * k as f64 + 1.) * (self[k][1] - self[k][0]); + + c[k][0] = d + (k as f64 + 1.) * self[k + 1][0]; + c[k][1] = d - (k as f64 + 1.) * self[k + 1][1]; + } + let k = self.len() - 1; + let d = (2. * k as f64 + 1.) * (self[k][1] - self[k][0]); + if d == 0. && k > 0 { + c.pop(); + } else { + c[k][0] = d; + c[k][1] = d; + } + c + } + + // https://gitlab.com/inkscape/lib2geom/-/blob/master/src/2geom/sbasis-to-bezier.cpp#L86 + #[must_use] + pub fn to_bezier1d(&self) -> Bezier1d { + let sb = self; + assert!(!sb.is_empty()); + + let n; + let even; + + let mut q = sb.len(); + if sb[q - 1][0] == sb[q - 1][1] { + even = true; + q -= 1; + n = 2 * q; + } else { + even = false; + n = 2 * q - 1; + } + + let mut bz = Bezier1d(vec![0.; n + 1]); + for k in 0..q { + let mut tjk = 1.; + for j in k..(n - k) { + // j <= n-k-1 + bz[j] += tjk * sb[k][0]; + bz[n - j] += tjk * sb[k][1]; // n-k <-> [k][1] + tjk = binomial_increment_k(tjk, n - 2 * k - 1, j - k); + } + } + if even { + bz[q] += sb[q][0]; + } + // the resulting coefficients are with respect to the scaled Bernstein + // basis so we need to divide them by (n, j) binomial coefficient + let mut bcj = n as f64; + for j in 1..n { + bz[j] /= bcj; + bcj = binomial_increment_k(bcj, n, j); + } + bz[0] = sb[0][0]; + bz[n] = sb[0][1]; + bz + } + + fn normalize(&mut self) { + while self.len() > 1 && self.last().is_some_and(|x| x.abs_diff_eq(DVec2::ZERO, 1e-5)) { + self.pop(); + } + } + + #[must_use] + pub(crate) fn roots(&self) -> Vec { + match self.len() { + 0 => Vec::new(), + 1 => { + let mut res = Vec::new(); + let d = self[0].x - self[0].y; + if d != 0. { + let r = self[0].x / d; + if (0. ..=1.).contains(&r) { + res.push(r); + } + } + res + } + _ => { + let mut bz = self.to_bezier1d(); + let mut solutions = Vec::new(); + if bz.len() == 0 || bz.iter().all(|&x| (x - bz[0]).abs() < 1e-5) { + return solutions; + } + while bz[0] == 0. { + bz = bz.deflate(); + solutions.push(0.); + } + // Linear + if bz.len() - 1 == 1 { + if sign(bz[0]) != sign(bz[1]) { + let d = bz[0] - bz[1]; + if d != 0. { + let r = bz[0] / d; + if (0. ..=1.).contains(&r) { + solutions.push(r); + } + } + } + return solutions; + } + bz.find_bernstein_roots(&mut solutions, 0, 0., 1.); + + solutions.sort_by(f64::total_cmp); + + solutions + } + } + } +} + +// https://gitlab.com/inkscape/lib2geom/-/blob/master/src/2geom/sbasis.cpp#L228 +impl std::ops::Mul for &SymmetricalBasis { + type Output = SymmetricalBasis; + fn mul(self, b: Self) -> Self::Output { + let a = self; + if a.iter().all(|x| x.abs_diff_eq(DVec2::ZERO, 1e-5)) || b.iter().all(|x| x.abs_diff_eq(DVec2::ZERO, 1e-5)) { + return SymmetricalBasis(vec![DVec2::ZERO]); + } + let mut c = SymmetricalBasis(vec![DVec2::ZERO; a.len() + b.len()]); + + for j in 0..b.len() { + for i in j..(a.len() + j) { + let tri = (b[j][1] - b[j][0]) * (a[i - j][1] - a[i - j][0]); + c[i + 1] += DVec2::splat(-tri); + } + } + for j in 0..b.len() { + for i in j..(a.len() + j) { + for dim in 0..2 { + c[i][dim] += b[j][dim] * a[i - j][dim]; + } + } + } + c.normalize(); + c + } +} + +// https://gitlab.com/inkscape/lib2geom/-/blob/master/src/2geom/sbasis.cpp#L88 +impl std::ops::Add for SymmetricalBasis { + type Output = SymmetricalBasis; + fn add(self, b: Self) -> Self::Output { + let a = self; + let out_size = a.len().max(b.len()); + let min_size = a.len().min(b.len()); + let mut result = SymmetricalBasis(vec![DVec2::ZERO; out_size]); + for i in 0..min_size { + result[i] = a[i] + b[i]; + } + for i in min_size..a.len() { + result[i] = a[i]; + } + for i in min_size..b.len() { + result[i] = b[i]; + } + result + } +} + +// https://gitlab.com/inkscape/lib2geom/-/blob/master/src/2geom/sbasis.cpp#L110 +impl std::ops::Sub for SymmetricalBasis { + type Output = SymmetricalBasis; + fn sub(self, b: Self) -> Self::Output { + let a = self; + let out_size = a.len().max(b.len()); + let min_size = a.len().min(b.len()); + let mut result = SymmetricalBasis(vec![DVec2::ZERO; out_size]); + for i in 0..min_size { + result[i] = a[i] - b[i]; + } + for i in min_size..a.len() { + result[i] = a[i]; + } + for i in min_size..b.len() { + result[i] = -b[i]; + } + result + } +} + +impl std::ops::Deref for SymmetricalBasis { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl std::ops::DerefMut for SymmetricalBasis { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[derive(Debug, Clone)] +pub(crate) struct SymmetricalBasisPair { + pub x: SymmetricalBasis, + pub y: SymmetricalBasis, +} + +impl SymmetricalBasisPair { + #[must_use] + pub fn derivative(&self) -> Self { + Self { + x: self.x.derivative(), + y: self.y.derivative(), + } + } + + #[must_use] + pub fn dot(&self, other: &Self) -> SymmetricalBasis { + (&self.x * &other.x) + (&self.y * &other.y) + } + + #[must_use] + pub fn cross(&self, rhs: &Self) -> SymmetricalBasis { + (&self.x * &rhs.y) - (&self.y * &rhs.x) + } +} + +// https://gitlab.com/inkscape/lib2geom/-/blob/master/include/2geom/sbasis.h#L337 +impl std::ops::Sub for SymmetricalBasisPair { + type Output = SymmetricalBasisPair; + fn sub(self, rhs: DVec2) -> Self::Output { + let sub = |a: &SymmetricalBasis, b: f64| { + if a.iter().all(|x| x.abs_diff_eq(DVec2::ZERO, 1e-5)) { + return SymmetricalBasis(vec![DVec2::splat(-b)]); + } + let mut result = a.clone(); + result[0] -= DVec2::splat(b); + result + }; + + Self { + x: sub(&self.x, rhs.x), + y: sub(&self.y, rhs.y), + } + } +} + +#[derive(Debug, Clone)] +pub struct Bezier1d(pub Vec); + +impl std::ops::Deref for Bezier1d { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl std::ops::DerefMut for Bezier1d { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Bezier1d { + const MAX_DEPTH: u32 = 53; + + // https://gitlab.com/inkscape/lib2geom/-/blob/master/src/2geom/bezier.cpp#L176 + #[must_use] + fn deflate(&self) -> Self { + let bz = self; + if bz.is_empty() { + return Bezier1d(Vec::new()); + } + let n = bz.len() - 1; + let mut b = Bezier1d(vec![0.; n]); + for i in 0..n { + b[i] = (n as f64 * bz[i + 1]) / (i as f64 + 1.) + } + b + } + + // https://gitlab.com/inkscape/lib2geom/-/blob/master/include/2geom/bezier.h#L55 + /// Compute the value of a Bernstein-Bezier polynomial using a Horner-like fast evaluation scheme. + #[must_use] + fn value_at(&self, t: f64) -> f64 { + let bz = self; + let order = bz.len() - 1; + let u = 1. - t; + let mut bc = 1.; + let mut tn = 1.; + let mut tmp = bz[0] * u; + for i in 1..order { + tn *= t; + bc = bc * (order as f64 - i as f64 + 1.) / i as f64; + tmp = (tmp + tn * bc * bz[i]) * u; + } + tmp + tn * t * bz[bz.len() - 1] + } + + // https://gitlab.com/inkscape/lib2geom/-/blob/master/src/2geom/solve-bezier.cpp#L258 + #[must_use] + fn secant(&self) -> f64 { + let bz = self; + let mut s = 0.; + let mut t = 1.; + let e = 1e-14; + let mut side = 0; + let mut r = 0.; + let mut fs = bz[0]; + let mut ft = bz[bz.len() - 1]; + + for _n in 0..100 { + r = (fs * t - ft * s) / (fs - ft); + if (t - s).abs() < e * (t + s).abs() { + return r; + } + + let fr = self.value_at(r); + + if fr * ft > 0. { + t = r; + ft = fr; + if side == -1 { + fs /= 2.; + } + side = -1; + } else if fs * fr > 0. { + s = r; + fs = fr; + if side == 1 { + ft /= 2.; + } + side = 1; + } else { + break; + } + } + r + } + + // https://gitlab.com/inkscape/lib2geom/-/blob/master/include/2geom/bezier.h#L78 + fn casteljau_subdivision(&self, t: f64) -> [Self; 2] { + let v = self; + let order = v.len() - 1; + let mut left = v.clone(); + let mut right = v.clone(); + + // The Horner-like scheme gives very slightly different results, but we need + // the result of subdivision to match exactly with Bezier's valueAt function. + let val = v.value_at(t); + for i in (1..=order).rev() { + left[i - 1] = right[0]; + for j in i..v.len() { + right[j - 1] = right[j - 1] + ((right[j] - right[j - 1]) * t); + } + } + right[0] = val; + left[order] = right[0]; + [left, right] + } + + // https://gitlab.com/inkscape/lib2geom/-/blob/master/src/2geom/bezier.cpp#L282 + fn derivative(&self) -> Self { + let bz = self; + if bz.len() - 1 == 1 { + return Bezier1d(vec![bz[1] - bz[0]]); + } + let mut der = Bezier1d(vec![0.; bz.len() - 1]); + + for i in 0..(bz.len() - 1) { + der[i] = (bz.len() - 1) as f64 * (bz[i + 1] - bz[i]); + } + der + } + + // https://gitlab.com/inkscape/lib2geom/-/blob/master/src/2geom/solve-bezier-one-d.cpp#L76 + /// given an equation in Bernstein-Bernstein form, find all roots between left_t and right_t + fn find_bernstein_roots(&self, solutions: &mut Vec, depth: u32, left_t: f64, right_t: f64) { + let bz = self; + let mut n_crossings = 0; + + let mut old_sign = sign(bz[0]); + for i in 1..bz.len() { + let sign = sign(bz[i]); + if sign != 0 { + if sign != old_sign && old_sign != 0 { + n_crossings += 1; + } + old_sign = sign; + } + } + // if last control point is zero, that counts as crossing too + if sign(bz[bz.len() - 1]) == 0 { + n_crossings += 1; + } + // no solutions + if n_crossings == 0 { + return; + } + // Unique solution + if n_crossings == 1 { + // Stop recursion when the tree is deep enough - return 1 solution at midpoint + if depth > Self::MAX_DEPTH { + let ax = right_t - left_t; + let ay = bz.last().unwrap() - bz[0]; + + solutions.push(left_t - ax * bz[0] / ay); + return; + } + + let r = bz.secant(); + solutions.push(r * right_t + (1. - r) * left_t); + return; + } + // solve recursively after subdividing control polygon + let o = bz.len() - 1; + let mut left = Bezier1d(vec![0.; o + 1]); + let mut right = bz.clone(); + let mut split_t = (left_t + right_t) * 0.5; + + // If subdivision is working poorly, split around the leftmost root of the derivative + if depth > 2 { + let dbz = bz.derivative(); + + let mut d_solutions = Vec::new(); + dbz.find_bernstein_roots(&mut d_solutions, 0, left_t, right_t); + d_solutions.sort_by(f64::total_cmp); + + let mut d_split_t = 0.5; + if !d_solutions.is_empty() { + d_split_t = d_solutions[0]; + split_t = left_t + (right_t - left_t) * d_split_t; + } + + [left, right] = bz.casteljau_subdivision(d_split_t); + } else { + // split at midpoint, because it is cheap + left[0] = right[0]; + for i in 1..bz.len() { + for j in 0..(bz.len() - i) { + right[j] = (right[j] + right[j + 1]) * 0.5; + } + left[i] = right[0]; + } + } + // Solution is exactly on the subdivision point + left.reverse(); + while right.len() - 1 > 0 && (right[0]).abs() <= 1e-10 { + // Deflate + right = right.deflate(); + left = left.deflate(); + solutions.push(split_t); + } + left.reverse(); + if right.len() - 1 > 0 { + left.find_bernstein_roots(solutions, depth + 1, left_t, split_t); + right.find_bernstein_roots(solutions, depth + 1, split_t, right_t); + } + } +} + +// https://gitlab.com/inkscape/lib2geom/-/blob/master/include/2geom/choose.h#L61 +/// Given a multiple of binomial(n, k), modify it to the same multiple of binomial(n, k + 1). +#[must_use] +fn binomial_increment_k(b: f64, n: usize, k: usize) -> f64 { + b * (n as f64 - k as f64) / (k + 1) as f64 +} + +// https://gitlab.com/inkscape/lib2geom/-/blob/master/include/2geom/choose.h#L52 +/// Given a multiple of binomial(n, k), modify it to the same multiple of binomial(n - 1, k). +#[must_use] +fn binomial_decrement_n(b: f64, n: usize, k: usize) -> f64 { + b * (n as f64 - k as f64) / n as f64 +} + +// https://gitlab.com/inkscape/lib2geom/-/blob/master/src/2geom/sbasis-to-bezier.cpp#L86 +#[must_use] +pub(crate) fn to_symmetrical_basis_pair(bezier: PathSeg) -> SymmetricalBasisPair { + let n = match bezier { + PathSeg::Line(line) => 1, + PathSeg::Quad(quad_bez) => 2, + PathSeg::Cubic(cubic_bez) => 3, + }; + let q = (n + 1) / 2; + let even = n % 2 == 0; + let mut sb = SymmetricalBasisPair { + x: SymmetricalBasis(vec![DVec2::ZERO; q + even as usize]), + y: SymmetricalBasis(vec![DVec2::ZERO; q + even as usize]), + }; + + let mut nck = 1.; + for k in 0..q { + let mut tjk = nck; + for j in k..q { + sb.x[j][0] += tjk * pathseg_points_vec(bezier)[k].x; + sb.x[j][1] += tjk * pathseg_points_vec(bezier)[n - k].x; + sb.y[j][0] += tjk * pathseg_points_vec(bezier)[k].y; + sb.y[j][1] += tjk * pathseg_points_vec(bezier)[n - k].y; + tjk = binomial_increment_k(tjk, n - j - k, j - k); + tjk = binomial_decrement_n(tjk, n - j - k, j - k + 1); + tjk = -tjk; + } + tjk = -nck; + for j in (k + 1)..q { + sb.x[j][0] += tjk * pathseg_points_vec(bezier)[n - k].x; + sb.x[j][1] += tjk * pathseg_points_vec(bezier)[k].x; + sb.y[j][0] += tjk * pathseg_points_vec(bezier)[n - k].y; + sb.y[j][1] += tjk * pathseg_points_vec(bezier)[k].y; + tjk = binomial_increment_k(tjk, n - j - k - 1, j - k - 1); + tjk = binomial_decrement_n(tjk, n - j - k - 1, j - k); + tjk = -tjk; + } + nck = binomial_increment_k(nck, n, k); + } + if even { + let mut tjk = if q % 2 == 1 { -1. } else { 1. }; + for k in 0..q { + sb.x[q][0] += tjk * (pathseg_points_vec(bezier)[k].x + pathseg_points_vec(bezier)[n - k].x); + sb.y[q][0] += tjk * (pathseg_points_vec(bezier)[k].y + pathseg_points_vec(bezier)[n - k].y); + tjk = binomial_increment_k(tjk, n, k); + tjk = -tjk; + } + sb.x[q][0] += tjk * pathseg_points_vec(bezier)[q].x; + sb.x[q][1] = sb.x[q][0]; + sb.y[q][0] += tjk * pathseg_points_vec(bezier)[q].y; + sb.y[q][1] = sb.y[q][0]; + } + sb.x[0][0] = pathseg_points_vec(bezier)[0].x; + sb.x[0][1] = pathseg_points_vec(bezier)[n].x; + sb.y[0][0] = pathseg_points_vec(bezier)[0].y; + sb.y[0][1] = pathseg_points_vec(bezier)[n].y; + + sb +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn find_bernstein_roots() { + let bz = Bezier1d(vec![50., -100., 170.]); + let mut solutions = Vec::new(); + bz.find_bernstein_roots(&mut solutions, 0, 0., 1.); + + solutions.sort_by(f64::total_cmp); + for &t in &solutions { + assert!(bz.value_at(t,).abs() < 1e-5, "roots should be roots {} {}", t, bz.value_at(t,)); + } + } +} diff --git a/node-graph/gcore/src/vector/algorithms/util.rs b/node-graph/gcore/src/vector/algorithms/util.rs index 5f70428abf..0c2eacf7ce 100644 --- a/node-graph/gcore/src/vector/algorithms/util.rs +++ b/node-graph/gcore/src/vector/algorithms/util.rs @@ -1,7 +1,7 @@ use glam::DVec2; use kurbo::{ParamCurve, ParamCurveDeriv, PathSeg}; -pub fn segment_tangent(segment: PathSeg, t: f64) -> DVec2 { +pub fn pathseg_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 }; diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index eea0c81777..2a3ea467ad 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -211,8 +211,8 @@ pub fn is_linear(segment: PathSeg) -> bool { } } -/// Get an iterator over the coordinates of all points in a path segment. -pub fn get_segment_points(segment: PathSeg) -> Vec { +/// Get an vec of all the points in a path segment. +pub fn pathseg_points_vec(segment: PathSeg) -> Vec { match segment { PathSeg::Line(line) => [line.p0, line.p1].to_vec(), PathSeg::Quad(quad_bez) => [quad_bez.p0, quad_bez.p1, quad_bez.p2].to_vec(), @@ -234,8 +234,8 @@ pub fn pathseg_abs_diff_eq(seg1: PathSeg, seg2: PathSeg, max_abs_diff: f64) -> b let seg1 = if is_linear(seg1) { PathSeg::Line(Line::new(seg1.start(), seg1.end())) } else { seg1 }; let seg2 = if is_linear(seg2) { PathSeg::Line(Line::new(seg2.start(), seg2.end())) } else { seg2 }; - let seg1_points = get_segment_points(seg1); - let seg2_points = get_segment_points(seg2); + let seg1_points = pathseg_points_vec(seg1); + let seg2_points = pathseg_points_vec(seg2); let cmp = |a: f64, b: f64| a.sub(b).abs() < max_abs_diff; diff --git a/node-graph/gcore/src/vector/vector_types.rs b/node-graph/gcore/src/vector/vector_types.rs index ffdd2173bd..d99dbdae5d 100644 --- a/node-graph/gcore/src/vector/vector_types.rs +++ b/node-graph/gcore/src/vector/vector_types.rs @@ -244,10 +244,15 @@ impl Vector { self.segment_domain.end_point().iter().map(|&index| self.point_domain.ids()[index]) } - pub fn push(&mut self, id: SegmentId, start: PointId, end: PointId, handles: BezierHandles, stroke: StrokeId) { + pub fn push(&mut self, id: SegmentId, start: PointId, end: PointId, handles: (Option, Option), stroke: StrokeId) { let [Some(start), Some(end)] = [start, end].map(|id| self.point_domain.resolve_id(id)) else { return; }; + let handles = match handles { + (None, None) => BezierHandles::Linear, + (None, Some(handle)) | (Some(handle), None) => BezierHandles::Quadratic { handle }, + (Some(handle_start), Some(handle_end)) => BezierHandles::Cubic { handle_start, handle_end }, + }; self.segment_domain.push(id, start, end, handles, stroke) } From 1476a4ab00a610ec2c665fee63744b80594e5768 Mon Sep 17 00:00:00 2001 From: indierusty Date: Sun, 10 Aug 2025 10:37:32 +0530 Subject: [PATCH 04/10] refactor centoid area and length --- node-graph/gcore/src/math/mod.rs | 1 + node-graph/gcore/src/math/polynomial.rs | 293 +++++++ node-graph/gcore/src/subpath/core.rs | 31 - node-graph/gcore/src/subpath/lookup.rs | 387 +++------ node-graph/gcore/src/subpath/mod.rs | 1 - node-graph/gcore/src/subpath/solvers.rs | 789 +----------------- node-graph/gcore/src/subpath/transform.rs | 579 ------------- node-graph/gcore/src/subpath/utils.rs | 429 ---------- .../vector/algorithms/bezpath_algorithms.rs | 62 ++ .../src/vector/algorithms/intersection.rs | 74 ++ 10 files changed, 551 insertions(+), 2095 deletions(-) create mode 100644 node-graph/gcore/src/math/polynomial.rs delete mode 100644 node-graph/gcore/src/subpath/utils.rs diff --git a/node-graph/gcore/src/math/mod.rs b/node-graph/gcore/src/math/mod.rs index 06a1c21bc4..34e2d22684 100644 --- a/node-graph/gcore/src/math/mod.rs +++ b/node-graph/gcore/src/math/mod.rs @@ -1,4 +1,5 @@ pub mod bbox; pub mod math_ext; +pub mod polynomial; pub mod quad; pub mod rect; diff --git a/node-graph/gcore/src/math/polynomial.rs b/node-graph/gcore/src/math/polynomial.rs new file mode 100644 index 0000000000..a72b3d634b --- /dev/null +++ b/node-graph/gcore/src/math/polynomial.rs @@ -0,0 +1,293 @@ +use std::fmt::{self, Display, Formatter}; +use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}; + +use kurbo::PathSeg; + +/// 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. +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct Polynomial { + coefficients: [f64; N], +} + +impl Polynomial { + /// Create a new polynomial from the coefficients given in the array. + /// + /// 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 new(coefficients: [f64; N]) -> Polynomial { + Polynomial { coefficients } + } + + /// Create a polynomial where all its coefficients are zero. + pub fn zero() -> Polynomial { + 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. + pub fn coefficients_mut(&mut self) -> &mut [f64; N] { + &mut self.coefficients + } + + /// Evaluate the polynomial at `value`. + pub fn eval(&self, value: f64) -> f64 { + self.coefficients.iter().rev().copied().reduce(|acc, x| acc * value + x).unwrap() + } + + /// Return the same polynomial but with a different maximum degree of `M-1`.\ + /// + /// Returns `None` if the polynomial cannot fit in the specified size. + pub fn as_size(&self) -> Option> { + let mut coefficients = [0.; M]; + + if M >= N { + coefficients[..N].copy_from_slice(&self.coefficients); + } else if self.coefficients.iter().rev().take(N - M).all(|&x| x == 0.) { + coefficients.copy_from_slice(&self.coefficients[..M]) + } else { + return None; + } + + Some(Polynomial { coefficients }) + } + + /// Computes the derivative in place. + pub fn derivative_mut(&mut self) { + self.coefficients.iter_mut().enumerate().for_each(|(index, x)| *x *= index as f64); + self.coefficients.rotate_left(1); + } + + /// Computes the antiderivative at `C = 0` in place. + /// + /// Returns `None` if the polynomial is not big enough to accommodate the extra degree. + pub fn antiderivative_mut(&mut self) -> Option<()> { + if self.coefficients[N - 1] != 0. { + return None; + } + self.coefficients.rotate_right(1); + self.coefficients.iter_mut().enumerate().skip(1).for_each(|(index, x)| *x /= index as f64); + Some(()) + } + + /// Computes the polynomial's derivative. + pub fn derivative(&self) -> Polynomial { + let mut ans = *self; + 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> { + let mut ans = *self; + ans.antiderivative_mut()?; + Some(ans) + } +} + +impl Default for Polynomial { + fn default() -> Self { + Self::zero() + } +} + +impl Display for Polynomial { + 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 AddAssign<&Polynomial> for Polynomial { + fn add_assign(&mut self, rhs: &Polynomial) { + self.coefficients.iter_mut().zip(rhs.coefficients.iter()).for_each(|(a, b)| *a += b); + } +} + +impl Add for &Polynomial { + type Output = Polynomial; + + fn add(self, other: &Polynomial) -> Polynomial { + let mut output = *self; + output += other; + output + } +} + +impl Neg for &Polynomial { + type Output = Polynomial; + + fn neg(self) -> Polynomial { + let mut output = *self; + output.coefficients.iter_mut().for_each(|x| *x = -*x); + output + } +} + +impl Neg for Polynomial { + type Output = Polynomial; + + fn neg(mut self) -> Polynomial { + self.coefficients.iter_mut().for_each(|x| *x = -*x); + self + } +} + +impl SubAssign<&Polynomial> for Polynomial { + fn sub_assign(&mut self, rhs: &Polynomial) { + self.coefficients.iter_mut().zip(rhs.coefficients.iter()).for_each(|(a, b)| *a -= b); + } +} + +impl Sub for &Polynomial { + type Output = Polynomial; + + fn sub(self, other: &Polynomial) -> Polynomial { + let mut output = *self; + output -= other; + output + } +} + +impl MulAssign<&Polynomial> for Polynomial { + fn mul_assign(&mut self, rhs: &Polynomial) { + for i in (0..N).rev() { + self.coefficients[i] = self.coefficients[i] * rhs.coefficients[0]; + for j in 0..i { + self.coefficients[i] += self.coefficients[j] * rhs.coefficients[i - j]; + } + } + } +} + +impl Mul for &Polynomial { + type Output = Polynomial; + + fn mul(self, other: &Polynomial) -> Polynomial { + let mut output = *self; + output *= other; + output + } +} + +/// Returns two [`Polynomial`]s representing the parametric equations for x and y coordinates of the bezier curve respectively. +/// The domain of both the equations are from t=0.0 representing the start and t=1.0 representing the end of the bezier curve. +pub fn pathseg_to_parametric_polynomial(segment: PathSeg) -> (Polynomial<4>, Polynomial<4>) { + match segment { + PathSeg::Line(line) => { + let term1 = line.p0 - line.p1; + (Polynomial::new([line.p0.x, term1.x, 0., 0.]), Polynomial::new([line.p0.y, term1.y, 0., 0.])) + } + PathSeg::Quad(quad_bez) => { + let term1 = 2. * (quad_bez.p1 - quad_bez.p0); + let term2 = quad_bez.p0 - 2. * quad_bez.p1.to_vec2() + quad_bez.p2.to_vec2(); + + (Polynomial::new([quad_bez.p0.x, term1.x, term2.x, 0.]), Polynomial::new([quad_bez.p0.y, term1.y, term2.y, 0.])) + } + PathSeg::Cubic(cubic_bez) => { + let term1 = 3. * (cubic_bez.p1 - cubic_bez.p0); + let term2 = 3. * (cubic_bez.p2 - cubic_bez.p1) - term1; + let term3 = cubic_bez.p3 - cubic_bez.p0 - term2 - term1; + + ( + Polynomial::new([cubic_bez.p0.x, term1.x, term2.x, term3.x]), + Polynomial::new([cubic_bez.p0.y, term1.y, term2.y, term3.y]), + ) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn evaluation() { + let p = Polynomial::new([1., 2., 3.]); + + assert_eq!(p.eval(1.), 6.); + assert_eq!(p.eval(2.), 17.); + } + + #[test] + fn size_change() { + let p1 = Polynomial::new([1., 2., 3.]); + let p2 = Polynomial::new([1., 2., 3., 0.]); + + assert_eq!(p1.as_size(), Some(p2)); + assert_eq!(p2.as_size(), Some(p1)); + + 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(); + let p2 = Polynomial::new([4., 5., 6.]).as_size().unwrap(); + + let multiplication = Polynomial::new([4., 13., 28., 27., 18.]); + + assert_eq!(&p1 * &p2, multiplication); + } + + #[test] + fn derivative_and_antiderivative() { + let mut p = Polynomial::new([1., 2., 3.]); + let p_deriv = Polynomial::new([2., 6., 0.]); + + assert_eq!(p.derivative(), p_deriv); + + p.coefficients_mut()[0] = 0.; + assert_eq!(p_deriv.antiderivative().unwrap(), p); + + assert_eq!(p.antiderivative(), None); + } + + #[test] + fn display() { + let p = Polynomial::new([1., 2., 0., 3.]); + + assert_eq!(format!("{:.2}", p), "3.00x^3 + 2.00x + 1.00"); + } +} diff --git a/node-graph/gcore/src/subpath/core.rs b/node-graph/gcore/src/subpath/core.rs index ac436d11e4..fee23209be 100644 --- a/node-graph/gcore/src/subpath/core.rs +++ b/node-graph/gcore/src/subpath/core.rs @@ -338,34 +338,3 @@ impl Subpath { } } } - -// #[cfg(test)] -// mod tests { -// use super::*; - -// #[test] -// fn closed_spline() { -// // These points are just chosen arbitrary -// let points = [DVec2::new(0., 0.), DVec2::new(0., 0.), DVec2::new(6., 5.), DVec2::new(7., 9.), DVec2::new(2., 3.)]; - -// let out_handles = solve_spline_first_handle_closed(&points); - -// // Construct the Subpath -// let mut manipulator_groups = Vec::new(); -// for i in 0..out_handles.len() { -// manipulator_groups.push(ManipulatorGroup::::new(points[i], Some(2. * points[i] - out_handles[i]), Some(out_handles[i]))); -// } -// let subpath = Subpath::new(manipulator_groups, true); - -// // For each pair of bézier curves, ensure that the second derivative is continuous -// for (bézier_a, bézier_b) in subpath.iter().zip(subpath.iter().skip(1).chain(subpath.iter().take(1))) { -// let derivative2_end_a = bézier_a.derivative().unwrap().derivative().unwrap().evaluate(super::utils::TValue::Parametric(1.)); -// let derivative2_start_b = bézier_b.derivative().unwrap().derivative().unwrap().evaluate(super::utils::TValue::Parametric(0.)); - -// assert!( -// derivative2_end_a.abs_diff_eq(derivative2_start_b, 1e-10), -// "second derivative at the end of a {derivative2_end_a} is equal to the second derivative at the start of b {derivative2_start_b}" -// ); -// } -// } -// } diff --git a/node-graph/gcore/src/subpath/lookup.rs b/node-graph/gcore/src/subpath/lookup.rs index b20a56e593..37608b0f7c 100644 --- a/node-graph/gcore/src/subpath/lookup.rs +++ b/node-graph/gcore/src/subpath/lookup.rs @@ -1,271 +1,116 @@ -// // use super::consts::DEFAULT_LUT_STEP_SIZE; -// use super::utils::{SubpathTValue, TValueType}; -// use super::*; -// use glam::DVec2; - -// /// Functionality relating to looking up properties of the `Subpath` or points along the `Subpath`. -// impl Subpath { -// /// Return a selection of equidistant points on the bezier curve. -// /// If no value is provided for `steps`, then the function will default `steps` to be 10. -// /// -// pub fn compute_lookup_table(&self, steps: Option, tvalue_type: Option) -> Vec { -// let steps = steps.unwrap_or(DEFAULT_LUT_STEP_SIZE); -// let tvalue_type = tvalue_type.unwrap_or(TValueType::Parametric); - -// (0..=steps) -// .map(|t| { -// let tvalue = match tvalue_type { -// TValueType::Parametric => SubpathTValue::GlobalParametric(t as f64 / steps as f64), -// TValueType::Euclidean => SubpathTValue::GlobalEuclidean(t as f64 / steps as f64), -// }; -// self.evaluate(tvalue) -// }) -// .collect() -// } -// } - -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::consts::MAX_ABSOLUTE_DIFFERENCE; -// use crate::utils::f64_compare; - -// #[test] -// fn length_quadratic() { -// let start = DVec2::new(20., 30.); -// let middle = DVec2::new(80., 90.); -// let end = DVec2::new(60., 45.); -// let handle1 = DVec2::new(75., 85.); -// let handle2 = DVec2::new(40., 30.); -// let handle3 = DVec2::new(10., 10.); - -// let bezier1 = Bezier::from_quadratic_dvec2(start, handle1, middle); -// let bezier2 = Bezier::from_quadratic_dvec2(middle, handle2, end); -// let bezier3 = Bezier::from_quadratic_dvec2(end, handle3, start); - -// let mut subpath = Subpath::new( -// vec![ -// ManipulatorGroup { -// anchor: start, -// in_handle: None, -// out_handle: Some(handle1), -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: middle, -// in_handle: None, -// out_handle: Some(handle2), -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: end, -// in_handle: None, -// out_handle: Some(handle3), -// id: EmptyId, -// }, -// ], -// false, -// ); -// assert_eq!(subpath.length(None), bezier1.length(None) + bezier2.length(None)); - -// subpath.closed = true; -// assert_eq!(subpath.length(None), bezier1.length(None) + bezier2.length(None) + bezier3.length(None)); -// } - -// #[test] -// fn length_mixed() { -// let start = DVec2::new(20., 30.); -// let middle = DVec2::new(70., 70.); -// let end = DVec2::new(60., 45.); -// let handle1 = DVec2::new(75., 85.); -// let handle2 = DVec2::new(40., 30.); -// let handle3 = DVec2::new(10., 10.); - -// let linear_bezier = Bezier::from_linear_dvec2(start, middle); -// let quadratic_bezier = Bezier::from_quadratic_dvec2(middle, handle1, end); -// let cubic_bezier = Bezier::from_cubic_dvec2(end, handle2, handle3, start); - -// let mut subpath = Subpath::new( -// vec![ -// ManipulatorGroup { -// anchor: start, -// in_handle: Some(handle3), -// out_handle: None, -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: middle, -// in_handle: None, -// out_handle: Some(handle1), -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: end, -// in_handle: None, -// out_handle: Some(handle2), -// id: EmptyId, -// }, -// ], -// false, -// ); -// assert_eq!(subpath.length(None), linear_bezier.length(None) + quadratic_bezier.length(None)); - -// subpath.closed = true; -// assert_eq!(subpath.length(None), linear_bezier.length(None) + quadratic_bezier.length(None) + cubic_bezier.length(None)); -// } - -// #[test] -// fn length_centroid() { -// let start = DVec2::new(0., 0.); -// let end = DVec2::new(1., 1.); -// let handle = DVec2::new(0., 1.); - -// let mut subpath = Subpath::new( -// vec![ -// ManipulatorGroup { -// anchor: start, -// in_handle: None, -// out_handle: Some(handle), -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: end, -// in_handle: None, -// out_handle: None, -// id: EmptyId, -// }, -// ], -// false, -// ); - -// let expected_centroid = DVec2::new(0.4153039799983826, 0.5846960200016174); -// let epsilon = 0.00001; - -// assert!(subpath.length_centroid_and_length(None, true).unwrap().0.abs_diff_eq(expected_centroid, epsilon)); - -// subpath.closed = true; -// assert!(subpath.length_centroid_and_length(None, true).unwrap().0.abs_diff_eq(expected_centroid, epsilon)); -// } - -// #[test] -// fn area() { -// let start = DVec2::new(0., 0.); -// let end = DVec2::new(1., 1.); -// let handle = DVec2::new(0., 1.); - -// let mut subpath = Subpath::new( -// vec![ -// ManipulatorGroup { -// anchor: start, -// in_handle: None, -// out_handle: Some(handle), -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: end, -// in_handle: None, -// out_handle: None, -// id: EmptyId, -// }, -// ], -// false, -// ); - -// let expected_area = 1. / 3.; -// let epsilon = 0.00001; - -// assert!((subpath.area(Some(0.001), Some(0.001)) - expected_area).abs() < epsilon); - -// subpath.closed = true; -// assert!((subpath.area(Some(0.001), Some(0.001)) - expected_area).abs() < epsilon); -// } - -// #[test] -// fn area_centroid() { -// let start = DVec2::new(0., 0.); -// let end = DVec2::new(1., 1.); -// let handle = DVec2::new(0., 1.); - -// let mut subpath = Subpath::new( -// vec![ -// ManipulatorGroup { -// anchor: start, -// in_handle: None, -// out_handle: Some(handle), -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: end, -// in_handle: None, -// out_handle: None, -// id: EmptyId, -// }, -// ], -// false, -// ); - -// let expected_centroid = DVec2::new(0.4, 0.6); -// let epsilon = 0.00001; - -// assert!(subpath.area_centroid(Some(0.001), Some(0.001), None).unwrap().abs_diff_eq(expected_centroid, epsilon)); - -// subpath.closed = true; -// assert!(subpath.area_centroid(Some(0.001), Some(0.001), None).unwrap().abs_diff_eq(expected_centroid, epsilon)); -// } - -// #[test] -// fn t_value_to_parametric_global_parametric_open_subpath() { -// let mock_manipulator_group = ManipulatorGroup { -// anchor: DVec2::new(0., 0.), -// in_handle: None, -// out_handle: None, -// id: EmptyId, -// }; -// let open_subpath = Subpath { -// manipulator_groups: vec![mock_manipulator_group; 5], -// closed: false, -// }; - -// let (segment_index, t) = open_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.7)); -// assert_eq!(segment_index, 2); -// assert!(f64_compare(t, 0.8, MAX_ABSOLUTE_DIFFERENCE)); - -// // The start and end points of an open subpath are NOT equivalent -// assert_eq!(open_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.)), (0, 0.)); -// assert_eq!(open_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(1.)), (3, 1.)); -// } - -// #[test] -// fn t_value_to_parametric_global_parametric_closed_subpath() { -// let mock_manipulator_group = ManipulatorGroup { -// anchor: DVec2::new(0., 0.), -// in_handle: None, -// out_handle: None, -// id: EmptyId, -// }; -// let closed_subpath = Subpath { -// manipulator_groups: vec![mock_manipulator_group; 5], -// closed: true, -// }; - -// let (segment_index, t) = closed_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.7)); -// assert_eq!(segment_index, 3); -// assert!(f64_compare(t, 0.5, MAX_ABSOLUTE_DIFFERENCE)); - -// // The start and end points of a closed subpath are equivalent -// assert_eq!(closed_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(0.)), (0, 0.)); -// assert_eq!(closed_subpath.t_value_to_parametric(SubpathTValue::GlobalParametric(1.)), (4, 1.)); -// } - -// #[test] -// fn exact_start_end() { -// let start = DVec2::new(20., 30.); -// let end = DVec2::new(60., 45.); -// let handle = DVec2::new(75., 85.); - -// let subpath: Subpath = Subpath::from_bezier(&Bezier::from_quadratic_dvec2(start, handle, end)); - -// assert_eq!(subpath.evaluate(SubpathTValue::GlobalEuclidean(0.)), start); -// assert_eq!(subpath.evaluate(SubpathTValue::GlobalEuclidean(1.)), end); -// } -// } +use crate::{ + math::polynomial::pathseg_to_parametric_polynomial, + vector::algorithms::bezpath_algorithms::pathseg_length_centroid_and_length, + vector::algorithms::intersection::{filtered_all_segment_intersections, pathseg_self_intersections}, +}; + +use super::{consts::MAX_ABSOLUTE_DIFFERENCE, *}; +use glam::DVec2; + +impl Subpath { + /// Returns a list of `t` values that correspond to all the self intersection points of the subpath always considering it as a closed subpath. The index and `t` value of both will be returned that corresponds to a point. + /// The points will be sorted based on their index and `t` repsectively. + /// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point. + /// - `minimum_separation`: 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 + /// + /// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out. + pub fn all_self_intersections(&self, accuracy: Option, minimum_separation: Option) -> Vec<(usize, f64)> { + let mut intersections_vec = Vec::new(); + let err = accuracy.unwrap_or(MAX_ABSOLUTE_DIFFERENCE); + let num_curves = self.len(); + // TODO: optimization opportunity - this for-loop currently compares all intersections with all curve-segments in the subpath collection + self.iter_closed().enumerate().for_each(|(i, other)| { + intersections_vec.extend(pathseg_self_intersections(other, accuracy, minimum_separation).iter().flat_map(|value| [(i, value.0), (i, value.1)])); + self.iter_closed().enumerate().skip(i + 1).for_each(|(j, curve)| { + intersections_vec.extend( + filtered_all_segment_intersections(curve, other, accuracy, minimum_separation) + .iter() + .filter(|&value| (j != i + 1 || value.0 > err || (1. - value.1) > err) && (j != num_curves - 1 || i != 0 || value.1 > err || (1. - value.0) > err)) + .flat_map(|value| [(j, value.0), (i, value.1)]), + ); + }); + }); + + intersections_vec.sort_by(|a, b| a.partial_cmp(b).unwrap()); + + intersections_vec + } + + /// Return the area centroid, together with the area, of the `Subpath` always considering it as a closed subpath. The area will always be a positive value. + /// + /// The area centroid is the center of mass for the area of a solid shape's interior. + /// An infinitely flat material forming the subpath's closed shape would balance at this point. + /// + /// It will return `None` if no manipulator is present. If the area is less than `error`, it will return `Some((DVec2::NAN, 0.))`. + /// + /// Because the calculation of area and centroid for self-intersecting path requires finding the intersections, the following parameters are used: + /// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point. + /// - `minimum_separation` - 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. + /// + /// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out. + pub fn area_centroid_and_area(&self, error: Option, minimum_separation: Option) -> Option<(DVec2, f64)> { + let all_intersections = self.all_self_intersections(error, minimum_separation); + let mut current_sign: f64 = 1.; + + let (x_sum, y_sum, area) = self + .iter_closed() + .enumerate() + .map(|(index, bezier)| { + let (f_x, f_y) = pathseg_to_parametric_polynomial(bezier); + let (f_x, f_y) = (f_x.as_size::<10>().unwrap(), f_y.as_size::<10>().unwrap()); + let f_y_prime = f_y.derivative(); + let f_x_prime = f_x.derivative(); + let f_xy = &f_x * &f_y; + + let mut x_part = &f_xy * &f_x_prime; + let mut y_part = &f_xy * &f_y_prime; + let mut area_part = &f_x * &f_y_prime; + x_part.antiderivative_mut(); + y_part.antiderivative_mut(); + area_part.antiderivative_mut(); + + let mut curve_sum_x = -current_sign * x_part.eval(0.); + let mut curve_sum_y = -current_sign * y_part.eval(0.); + let mut curve_sum_area = -current_sign * area_part.eval(0.); + for (_, t) in all_intersections.iter().filter(|(i, _)| *i == index) { + curve_sum_x += 2. * current_sign * x_part.eval(*t); + curve_sum_y += 2. * current_sign * y_part.eval(*t); + curve_sum_area += 2. * current_sign * area_part.eval(*t); + current_sign *= -1.; + } + curve_sum_x += current_sign * x_part.eval(1.); + curve_sum_y += current_sign * y_part.eval(1.); + curve_sum_area += current_sign * area_part.eval(1.); + + (-curve_sum_x, curve_sum_y, curve_sum_area) + }) + .reduce(|(x1, y1, area1), (x2, y2, area2)| (x1 + x2, y1 + y2, area1 + area2))?; + + if area.abs() < error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE) { + return Some((DVec2::NAN, 0.)); + } + + Some((DVec2::new(x_sum / area, y_sum / area), area.abs())) + } + + /// Return the approximation of the length centroid, together with the length, of the `Subpath`. + /// + /// The length centroid is the center of mass for the arc length of the solid shape's perimeter. + /// An infinitely thin wire forming the subpath's closed shape would balance at this point. + /// + /// It will return `None` if no manipulator is present. + /// - `accuracy` is used to approximate the curve. + /// - `always_closed` is to consider the subpath as closed always. + pub fn length_centroid_and_length(&self, accuracy: Option, always_closed: bool) -> Option<(DVec2, f64)> { + if always_closed { self.iter_closed() } else { self.iter() } + .map(|bezier| pathseg_length_centroid_and_length(bezier, accuracy)) + .map(|(centroid, length)| (centroid * length, length)) + .reduce(|(centroid_part1, length1), (centroid_part2, length2)| (centroid_part1 + centroid_part2, length1 + length2)) + .map(|(centroid_part, length)| (centroid_part / length, length)) + .map(|(centroid_part, length)| (DVec2::new(centroid_part.x, centroid_part.y), length)) + } +} diff --git a/node-graph/gcore/src/subpath/mod.rs b/node-graph/gcore/src/subpath/mod.rs index 99c3cdb7f4..b6e9ea1143 100644 --- a/node-graph/gcore/src/subpath/mod.rs +++ b/node-graph/gcore/src/subpath/mod.rs @@ -5,7 +5,6 @@ mod manipulators; mod solvers; mod structs; mod transform; -mod utils; pub use core::*; use kurbo::PathSeg; diff --git a/node-graph/gcore/src/subpath/solvers.rs b/node-graph/gcore/src/subpath/solvers.rs index 4e4199cdcc..3fda325d38 100644 --- a/node-graph/gcore/src/subpath/solvers.rs +++ b/node-graph/gcore/src/subpath/solvers.rs @@ -1,20 +1,10 @@ -use crate::vector::misc::dvec2_to_point; +use crate::subpath::{Identifier, Subpath}; +use crate::vector::{algorithms::bezpath_algorithms::bezpath_is_inside_bezpath, misc::dvec2_to_point}; -use super::utils::SubpathTValue; -use super::*; -use glam::{DMat2, DVec2}; +use glam::DVec2; use kurbo::{Affine, BezPath, Shape}; impl Subpath { - /// Calculate the point on the subpath based on the parametric `t`-value provided. - /// Expects `t` to be within the inclusive range `[0, 1]`. - /// - pub fn evaluate(&self, t: SubpathTValue) -> DVec2 { - todo!(); - // let (segment_index, t) = self.t_value_to_parametric(t); - // self.get_segment(segment_index).unwrap().evaluate(TValue::Parametric(t)) - } - pub fn contains_point(&self, point: DVec2) -> bool { self.to_bezpath().contains(dvec2_to_point(point)) } @@ -49,228 +39,10 @@ impl Subpath { bezpath } - /// 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 occurred. - /// Expects the following: - /// - `other`: a [Bezier] curve to check intersections against - /// - `error`: an optional f64 value to provide an error bound - /// - `minimum_separation`: 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. - /// - /// - /// - /// - /// - pub fn intersections(&self, other: PathSeg, error: Option, minimum_separation: Option) -> Vec<(usize, f64)> { - todo!() - // self.iter() - // .enumerate() - // .flat_map(|(index, bezier)| bezier.intersections(other, error, minimum_separation).into_iter().map(|t| (index, t)).collect::>()) - // .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 - pub fn subpath_intersections(&self, other: &Subpath, error: Option, minimum_separation: Option) -> Vec<(usize, f64)> { - todo!(); - // 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 - } - - /// Returns how many times a given ray intersects with this subpath. (`ray_direction` does not need to be normalized.) - /// If this needs to be called frequently with a ray of the same rotation angle, consider instead using [`ray_test_crossings_count_prerotated`]. - pub fn ray_test_crossings_count(&self, ray_start: DVec2, ray_direction: DVec2) -> usize { - todo!() - // self.iter().map(|bezier| bezier.ray_test_crossings(ray_start, ray_direction).count()).sum() - } - - /// Returns how many times a given ray intersects with this subpath. (`ray_direction` does not need to be normalized.) - /// This version of the function is for better performance when calling it frequently without needing to change the rotation between each call. - /// If that isn't important, use [`ray_test_crossings_count`] which provides an easier interface by taking a ray direction vector. - /// Instead, this version requires a rotation matrix for the ray's rotation and a prerotated version of this subpath that has had its rotation applied. - pub fn ray_test_crossings_count_prerotated(&self, ray_start: DVec2, rotation_matrix: DMat2, rotated_subpath: &Self) -> usize { - todo!() - // self.iter() - // .zip(rotated_subpath.iter()) - // .map(|(bezier, rotated_bezier)| bezier.ray_test_crossings_prerotated(ray_start, rotation_matrix, rotated_bezier).count()) - // .sum() - } - - /// Returns true if the given point is inside this subpath. Open paths are NOT automatically closed so you'll need to call `set_closed(true)` before calling this. - /// Self-intersecting subpaths use the `evenodd` fill rule for checking in/outside-ness: . - /// If this needs to be called frequently, consider instead using [`point_inside_prerotated`] and moving this function's setup code into your own logic before the repeated call. - pub fn point_inside(&self, point: DVec2) -> bool { - // The directions use prime numbers to reduce the likelihood of running across two anchor points simultaneously - const SIN_13DEG: f64 = 0.22495105434; - const COS_13DEG: f64 = 0.97437006478; - const DIRECTION1: DVec2 = DVec2::new(SIN_13DEG, COS_13DEG); - const DIRECTION2: DVec2 = DVec2::new(-COS_13DEG, -SIN_13DEG); - - // We (inefficiently) check for odd crossings in two directions and make sure they agree to reduce how often anchor points cause a double-increment - let test1 = self.ray_test_crossings_count(point, DIRECTION1) % 2 == 1; - let test2 = self.ray_test_crossings_count(point, DIRECTION2) % 2 == 1; - - test1 && test2 - } - - /// Returns true if the given point is inside this subpath. Open paths are NOT automatically closed so you'll need to call `set_closed(true)` before calling this. - /// Self-intersecting subpaths use the `evenodd` fill rule for checking in/outside-ness: . - /// This version of the function is for better performance when calling it frequently because it lets the caller precompute the rotations once instead of every call. - /// If that isn't important, use [`point_inside`] which provides an easier interface. - /// Instead, this version requires a pair of rotation matrices for the ray's rotation and a pair of prerotated versions of this subpath. - /// They should face in different directions that are unlikely to align in the real world. Consider using the following rotations: - /// ```rs - /// const SIN_13DEG: f64 = 0.22495105434; - /// const COS_13DEG: f64 = 0.97437006478; - /// const DIRECTION1: DVec2 = DVec2::new(SIN_13DEG, COS_13DEG); - /// const DIRECTION2: DVec2 = DVec2::new(-COS_13DEG, -SIN_13DEG); - /// ``` - pub fn point_inside_prerotated(&self, point: DVec2, rotation_matrix1: DMat2, rotation_matrix2: DMat2, rotated_subpath1: &Self, rotated_subpath2: &Self) -> bool { - // We (inefficiently) check for odd crossings in two directions and make sure they agree to reduce how often anchor points cause a double-increment - let test1 = self.ray_test_crossings_count_prerotated(point, rotation_matrix1, rotated_subpath1) % 2 == 1; - let test2 = self.ray_test_crossings_count_prerotated(point, rotation_matrix2, rotated_subpath2) % 2 == 1; - - test1 && test2 - } - - /// Returns a list of `t` values that correspond to the self intersection points of the subpath. For each intersection point, the returned `t` value is the smaller of the two that correspond to the point. - /// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point. - /// - `minimum_separation`: 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 - /// - /// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out. - /// - pub fn self_intersections(&self, error: Option, minimum_separation: Option) -> Vec<(usize, f64)> { - // let mut intersections_vec = Vec::new(); - // let err = error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE); - // // TODO: optimization opportunity - this for-loop currently compares all intersections with all curve-segments in the subpath collection - // self.iter().enumerate().for_each(|(i, other)| { - // intersections_vec.extend(other.self_intersections(error, minimum_separation).iter().map(|value| (i, value[0]))); - // self.iter().enumerate().skip(i + 1).for_each(|(j, curve)| { - // intersections_vec.extend( - // curve - // .intersections(&other, error, minimum_separation) - // .iter() - // .filter(|&value| value > &err && (1. - value) > err) - // .map(|value| (j, *value)), - // ); - // }); - // }); - // intersections_vec - todo!() - } - - /// Returns a list of `t` values that correspond to all the self intersection points of the subpath always considering it as a closed subpath. The index and `t` value of both will be returned that corresponds to a point. - /// The points will be sorted based on their index and `t` repsectively. - /// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point. - /// - `minimum_separation`: 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 - /// - /// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out. - pub fn all_self_intersections(&self, error: Option, minimum_separation: Option) -> Vec<(usize, f64)> { - // let mut intersections_vec = Vec::new(); - // let err = error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE); - // let num_curves = self.len(); - // // TODO: optimization opportunity - this for-loop currently compares all intersections with all curve-segments in the subpath collection - // self.iter_closed().enumerate().for_each(|(i, other)| { - // intersections_vec.extend(other.self_intersections(error, minimum_separation).iter().flat_map(|value| [(i, value[0]), (i, value[1])])); - // self.iter_closed().enumerate().skip(i + 1).for_each(|(j, curve)| { - // intersections_vec.extend( - // curve - // .all_intersections(&other, error, minimum_separation) - // .iter() - // .filter(|&value| (j != i + 1 || value[0] > err || (1. - value[1]) > err) && (j != num_curves - 1 || i != 0 || value[1] > err || (1. - value[0]) > err)) - // .flat_map(|value| [(j, value[0]), (i, value[1])]), - // ); - // }); - // }); - - // intersections_vec.sort_by(|a, b| a.partial_cmp(b).unwrap()); - - // intersections_vec - todo!() - } - /// Returns `true` if this subpath is completely inside the `other` subpath. /// - pub fn is_inside_subpath(&self, other: &Subpath, error: Option, minimum_separation: Option) -> bool { - // Eliminate any possibility of one being inside the other, if either of them is empty - // if self.is_empty() || other.is_empty() { - // return false; - // } - - // // Safe to unwrap because the subpath is not empty - // let inner_bbox = self.bounding_box().unwrap(); - // let outer_bbox = other.bounding_box().unwrap(); - - // // Eliminate this subpath if its bounding box is not completely inside the other subpath's bounding box. - // // Reasoning: - // // If the (min x, min y) of the inner subpath is less than or equal to the (min x, min y) of the outer subpath, - // // or if the (min x, min y) of the inner subpath is greater than or equal to the (max x, max y) of the outer subpath, - // // then the inner subpath is intersecting with or outside the outer subpath. The same logic applies for (max x, max y). - // if !is_rectangle_inside_other(inner_bbox, outer_bbox) { - // return false; - // } - - // // Eliminate this subpath if any of its anchors are outside the other subpath. - // for anchors in self.anchors() { - // if !other.contains_point(anchors) { - // return false; - // } - // } - - // // Eliminate this subpath if it intersects with the other subpath. - // if !self.subpath_intersections(other, error, minimum_separation).is_empty() { - // return false; - // } - - // // At this point: - // // (1) This subpath's bounding box is inside the other subpath's bounding box, - // // (2) Its anchors are inside the other subpath, and - // // (3) It is not intersecting with the other subpath. - // // Hence, this subpath is completely inside the given other subpath. - // true - todo!() - } - - /// Returns a normalized unit vector representing the tangent on the subpath based on the parametric `t`-value provided. - /// - pub fn tangent(&self, t: SubpathTValue) -> DVec2 { - todo!() - // let (segment_index, t) = self.t_value_to_parametric(t); - // self.get_segment(segment_index).unwrap().tangent(TValue::Parametric(t)) - } - - /// Returns a normalized unit vector representing the direction of the normal on the subpath based on the parametric `t`-value provided. - /// - pub fn normal(&self, t: SubpathTValue) -> DVec2 { - todo!() - // let (segment_index, t) = self.t_value_to_parametric(t); - // self.get_segment(segment_index).unwrap().normal(TValue::Parametric(t)) - } - - /// Returns two lists of `t`-values representing the local extrema of the `x` and `y` parametric subpaths respectively. - /// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`. - /// - pub fn local_extrema(&self) -> [Vec; 2] { - todo!() - // let number_of_curves = self.len_segments() as f64; - - // // TODO: Consider the shared point between adjacent beziers. - // self.iter().enumerate().fold([Vec::new(), Vec::new()], |mut acc, elem| { - // let [x, y] = elem.1.local_extrema(); - // // Convert t-values of bezier curve to t-values of subpath - // acc[0].extend(x.map(|t| ((elem.0 as f64) + t) / number_of_curves).collect::>()); - // acc[1].extend(y.map(|t| ((elem.0 as f64) + t) / number_of_curves).collect::>()); - // acc - // }) + pub fn is_inside_subpath(&self, other: &Subpath, accuracy: Option, minimum_separation: Option) -> bool { + bezpath_is_inside_bezpath(&self.to_bezpath(), &other.to_bezpath(), accuracy, minimum_separation) } /// Return the min and max corners that represent the bounding box of the subpath. Return `None` if the subpath is empty. @@ -310,555 +82,4 @@ impl Subpath { .map(|pos| [pos, pos]) .reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])]) } - - /// Returns list of `t`-values representing the inflection points of the subpath. - /// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`. - /// - pub fn inflections(&self) -> Vec { - let number_of_curves = self.len_segments() as f64; - let inflection_t_values: Vec = self - .iter() - .enumerate() - .flat_map(|(index, bezier)| { - bezier.to_cubic().inflections() - .into_iter() - // Convert t-values of bezier curve to t-values of subpath - .map(move |t| ((index as f64) + t) / number_of_curves) - }) - .collect(); - - // TODO: Consider the shared point between adjacent beziers. - inflection_t_values - } - - /// Returns the curvature, a scalar value for the derivative at the point `t` along the subpath. - /// Curvature is 1 over the radius of a circle with an equivalent derivative. - /// - pub fn curvature(&self, t: SubpathTValue) -> f64 { - // let (segment_index, t) = self.t_value_to_parametric(t); - // self.get_segment(segment_index).unwrap().curvature(TValue::Parametric(t)) - todo!() - } - pub fn area_centroid_and_area(&self, _: Option, _: Option) -> Option<(DVec2, f64)> { - todo!(); - } - - pub fn length_centroid_and_length(&self, _: Option, _: bool) -> Option<(DVec2, f64)> { - todo!() - } } - -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::Bezier; -// use crate::consts::MAX_ABSOLUTE_DIFFERENCE; -// use crate::utils; -// use glam::DVec2; - -// fn normalize_t(n: i64, t: f64) -> f64 { -// t * (n as f64) % 1. -// } - -// #[test] -// fn evaluate_one_subpath_curve() { -// let start = DVec2::new(20., 30.); -// let end = DVec2::new(60., 45.); -// let handle = DVec2::new(75., 85.); - -// let bezier = Bezier::from_quadratic_dvec2(start, handle, end); -// let subpath = Subpath::new( -// vec![ -// ManipulatorGroup { -// anchor: start, -// in_handle: None, -// out_handle: Some(handle), -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: end, -// in_handle: None, -// out_handle: Some(handle), -// id: EmptyId, -// }, -// ], -// false, -// ); - -// let t0 = 0.; -// assert_eq!(subpath.evaluate(SubpathTValue::GlobalParametric(t0)), bezier.evaluate(TValue::Parametric(t0))); - -// let t1 = 0.25; -// assert_eq!(subpath.evaluate(SubpathTValue::GlobalParametric(t1)), bezier.evaluate(TValue::Parametric(t1))); - -// let t2 = 0.50; -// assert_eq!(subpath.evaluate(SubpathTValue::GlobalParametric(t2)), bezier.evaluate(TValue::Parametric(t2))); - -// let t3 = 1.; -// assert_eq!(subpath.evaluate(SubpathTValue::GlobalParametric(t3)), bezier.evaluate(TValue::Parametric(t3))); -// } - -// #[test] -// fn evaluate_multiple_subpath_curves() { -// let start = DVec2::new(20., 30.); -// let middle = DVec2::new(70., 70.); -// let end = DVec2::new(60., 45.); -// let handle1 = DVec2::new(75., 85.); -// let handle2 = DVec2::new(40., 30.); -// let handle3 = DVec2::new(10., 10.); - -// let linear_bezier = Bezier::from_linear_dvec2(start, middle); -// let quadratic_bezier = Bezier::from_quadratic_dvec2(middle, handle1, end); -// let cubic_bezier = Bezier::from_cubic_dvec2(end, handle2, handle3, start); - -// let mut subpath = Subpath::new( -// vec![ -// ManipulatorGroup { -// anchor: start, -// in_handle: Some(handle3), -// out_handle: None, -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: middle, -// in_handle: None, -// out_handle: Some(handle1), -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: end, -// in_handle: None, -// out_handle: Some(handle2), -// id: EmptyId, -// }, -// ], -// false, -// ); - -// // Test open subpath - -// let mut n = (subpath.len() as i64) - 1; - -// let t0 = 0.; -// assert!( -// utils::dvec2_compare( -// subpath.evaluate(SubpathTValue::GlobalParametric(t0)), -// linear_bezier.evaluate(TValue::Parametric(normalize_t(n, t0))), -// MAX_ABSOLUTE_DIFFERENCE -// ) -// .all() -// ); - -// let t1 = 0.25; -// assert!( -// utils::dvec2_compare( -// subpath.evaluate(SubpathTValue::GlobalParametric(t1)), -// linear_bezier.evaluate(TValue::Parametric(normalize_t(n, t1))), -// MAX_ABSOLUTE_DIFFERENCE -// ) -// .all() -// ); - -// let t2 = 0.50; -// assert!( -// utils::dvec2_compare( -// subpath.evaluate(SubpathTValue::GlobalParametric(t2)), -// quadratic_bezier.evaluate(TValue::Parametric(normalize_t(n, t2))), -// MAX_ABSOLUTE_DIFFERENCE -// ) -// .all() -// ); - -// let t3 = 0.75; -// assert!( -// utils::dvec2_compare( -// subpath.evaluate(SubpathTValue::GlobalParametric(t3)), -// quadratic_bezier.evaluate(TValue::Parametric(normalize_t(n, t3))), -// MAX_ABSOLUTE_DIFFERENCE -// ) -// .all() -// ); - -// let t4 = 1.; -// assert!( -// utils::dvec2_compare( -// subpath.evaluate(SubpathTValue::GlobalParametric(t4)), -// quadratic_bezier.evaluate(TValue::Parametric(1.)), -// MAX_ABSOLUTE_DIFFERENCE -// ) -// .all() -// ); - -// // Test closed subpath - -// subpath.closed = true; -// n = subpath.len() as i64; - -// let t5 = 2. / 3.; -// assert!( -// utils::dvec2_compare( -// subpath.evaluate(SubpathTValue::GlobalParametric(t5)), -// cubic_bezier.evaluate(TValue::Parametric(normalize_t(n, t5))), -// MAX_ABSOLUTE_DIFFERENCE -// ) -// .all() -// ); - -// let t6 = 1.; -// assert!( -// utils::dvec2_compare( -// subpath.evaluate(SubpathTValue::GlobalParametric(t6)), -// cubic_bezier.evaluate(TValue::Parametric(1.)), -// MAX_ABSOLUTE_DIFFERENCE -// ) -// .all() -// ); -// } - -// #[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 = DVec2::new(35., 125.); -// let cubic_handle_1 = DVec2::new(40., 40.); -// let cubic_handle_2 = DVec2::new(120., 120.); -// let cubic_end = DVec2::new(43., 43.); - -// let quadratic_1_handle = DVec2::new(175., 90.); -// let quadratic_end = DVec2::new(145., 150.); - -// let quadratic_2_handle = DVec2::new(70., 185.); - -// let cubic_bezier = Bezier::from_cubic_dvec2(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end); -// let quadratic_bezier_1 = Bezier::from_quadratic_dvec2(cubic_end, quadratic_1_handle, quadratic_end); - -// let subpath = Subpath::new( -// vec![ -// ManipulatorGroup { -// anchor: cubic_start, -// in_handle: None, -// out_handle: Some(cubic_handle_1), -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: cubic_end, -// in_handle: Some(cubic_handle_2), -// out_handle: None, -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: quadratic_end, -// in_handle: Some(quadratic_1_handle), -// out_handle: Some(quadratic_2_handle), -// id: EmptyId, -// }, -// ], -// true, -// ); - -// let line = Bezier::from_linear_coordinates(150., 150., 20., 20.); - -// let cubic_intersections = cubic_bezier.intersections(&line, None, None); -// let quadratic_1_intersections = quadratic_bezier_1.intersections(&line, None, None); -// let subpath_intersections = subpath.intersections(&line, None, None); - -// assert!( -// utils::dvec2_compare( -// cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])), -// subpath.evaluate(SubpathTValue::Parametric { -// segment_index: subpath_intersections[0].0, -// t: subpath_intersections[0].1 -// }), -// MAX_ABSOLUTE_DIFFERENCE -// ) -// .all() -// ); - -// assert!( -// utils::dvec2_compare( -// quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])), -// subpath.evaluate(SubpathTValue::Parametric { -// segment_index: subpath_intersections[1].0, -// t: subpath_intersections[1].1 -// }), -// MAX_ABSOLUTE_DIFFERENCE -// ) -// .all() -// ); - -// assert!( -// utils::dvec2_compare( -// quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[1])), -// subpath.evaluate(SubpathTValue::Parametric { -// segment_index: subpath_intersections[2].0, -// t: subpath_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 = DVec2::new(34., 107.); -// let cubic_handle_1 = DVec2::new(40., 40.); -// let cubic_handle_2 = DVec2::new(120., 120.); -// let cubic_end = DVec2::new(102., 29.); - -// let quadratic_1_handle = DVec2::new(175., 90.); -// let quadratic_end = DVec2::new(129., 171.); - -// let quadratic_2_handle = DVec2::new(70., 185.); - -// let cubic_bezier = Bezier::from_cubic_dvec2(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end); -// let quadratic_bezier_1 = Bezier::from_quadratic_dvec2(cubic_end, quadratic_1_handle, quadratic_end); - -// let subpath = Subpath::new( -// vec![ -// ManipulatorGroup { -// anchor: cubic_start, -// in_handle: None, -// out_handle: Some(cubic_handle_1), -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: cubic_end, -// in_handle: Some(cubic_handle_2), -// out_handle: None, -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: quadratic_end, -// in_handle: Some(quadratic_1_handle), -// out_handle: Some(quadratic_2_handle), -// id: EmptyId, -// }, -// ], -// true, -// ); - -// let line = Bezier::from_linear_coordinates(150., 150., 20., 20.); - -// let cubic_intersections = cubic_bezier.intersections(&line, None, None); -// let quadratic_1_intersections = quadratic_bezier_1.intersections(&line, None, None); -// let subpath_intersections = subpath.intersections(&line, None, None); - -// assert!( -// utils::dvec2_compare( -// cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])), -// subpath.evaluate(SubpathTValue::Parametric { -// segment_index: subpath_intersections[0].0, -// t: subpath_intersections[0].1 -// }), -// MAX_ABSOLUTE_DIFFERENCE -// ) -// .all() -// ); - -// assert!( -// utils::dvec2_compare( -// quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])), -// subpath.evaluate(SubpathTValue::Parametric { -// segment_index: subpath_intersections[1].0, -// t: subpath_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 = DVec2::new(35., 125.); -// let cubic_handle_1 = DVec2::new(40., 40.); -// let cubic_handle_2 = DVec2::new(120., 120.); -// let cubic_end = DVec2::new(44., 44.); - -// let quadratic_1_handle = DVec2::new(175., 90.); -// let quadratic_end = DVec2::new(145., 150.); - -// let quadratic_2_handle = DVec2::new(70., 185.); - -// let cubic_bezier = Bezier::from_cubic_dvec2(cubic_start, cubic_handle_1, cubic_handle_2, cubic_end); -// let quadratic_bezier_1 = Bezier::from_quadratic_dvec2(cubic_end, quadratic_1_handle, quadratic_end); - -// let subpath = Subpath::new( -// vec![ -// ManipulatorGroup { -// anchor: cubic_start, -// in_handle: None, -// out_handle: Some(cubic_handle_1), -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: cubic_end, -// in_handle: Some(cubic_handle_2), -// out_handle: None, -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: quadratic_end, -// in_handle: Some(quadratic_1_handle), -// out_handle: Some(quadratic_2_handle), -// id: EmptyId, -// }, -// ], -// true, -// ); - -// let line = Bezier::from_linear_coordinates(150., 150., 20., 20.); - -// let cubic_intersections = cubic_bezier.intersections(&line, None, None); -// let quadratic_1_intersections = quadratic_bezier_1.intersections(&line, None, None); -// let subpath_intersections = subpath.intersections(&line, None, None); - -// assert!( -// utils::dvec2_compare( -// cubic_bezier.evaluate(TValue::Parametric(cubic_intersections[0])), -// subpath.evaluate(SubpathTValue::Parametric { -// segment_index: subpath_intersections[0].0, -// t: subpath_intersections[0].1 -// }), -// MAX_ABSOLUTE_DIFFERENCE -// ) -// .all() -// ); - -// assert!( -// utils::dvec2_compare( -// quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[0])), -// subpath.evaluate(SubpathTValue::Parametric { -// segment_index: subpath_intersections[1].0, -// t: subpath_intersections[1].1 -// }), -// MAX_ABSOLUTE_DIFFERENCE -// ) -// .all() -// ); - -// assert!( -// utils::dvec2_compare( -// quadratic_bezier_1.evaluate(TValue::Parametric(quadratic_1_intersections[1])), -// subpath.evaluate(SubpathTValue::Parametric { -// segment_index: subpath_intersections[2].0, -// t: subpath_intersections[2].1 -// }), -// MAX_ABSOLUTE_DIFFERENCE -// ) -// .all() -// ); -// } - -// // TODO: add more intersection tests - -// #[test] -// fn is_inside_subpath() { -// let boundary_polygon = [DVec2::new(100., 100.), DVec2::new(500., 100.), DVec2::new(500., 500.), DVec2::new(100., 500.)].to_vec(); -// let boundary_polygon = Subpath::from_anchors_linear(boundary_polygon, true); - -// let curve = Bezier::from_quadratic_dvec2(DVec2::new(189., 289.), DVec2::new(9., 286.), DVec2::new(45., 410.)); -// let curve_intersecting = Subpath::::from_bezier(&curve); -// assert!(!curve_intersecting.is_inside_subpath(&boundary_polygon, None, None)); - -// let curve = Bezier::from_quadratic_dvec2(DVec2::new(115., 37.), DVec2::new(51.4, 91.8), DVec2::new(76.5, 242.)); -// let curve_outside = Subpath::::from_bezier(&curve); -// assert!(!curve_outside.is_inside_subpath(&boundary_polygon, None, None)); - -// let curve = Bezier::from_cubic_dvec2(DVec2::new(210.1, 133.5), DVec2::new(150.2, 436.9), DVec2::new(436., 285.), DVec2::new(247.6, 240.7)); -// let curve_inside = Subpath::::from_bezier(&curve); -// assert!(curve_inside.is_inside_subpath(&boundary_polygon, None, None)); - -// let line = Bezier::from_linear_dvec2(DVec2::new(101., 101.5), DVec2::new(150.2, 499.)); -// let line_inside = Subpath::::from_bezier(&line); -// assert!(line_inside.is_inside_subpath(&boundary_polygon, None, None)); -// } - -// #[test] -// fn round_join_counter_clockwise_rotation() { -// // Test case where the round join is drawn in the counter clockwise direction between two consecutive offsets -// let subpath = Subpath::new( -// vec![ -// ManipulatorGroup { -// anchor: DVec2::new(20., 20.), -// out_handle: Some(DVec2::new(10., 90.)), -// in_handle: None, -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: DVec2::new(114., 159.), -// out_handle: None, -// in_handle: Some(DVec2::new(60., 40.)), -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: DVec2::new(148., 155.), -// out_handle: None, -// in_handle: None, -// id: EmptyId, -// }, -// ], -// false, -// ); - -// let offset = subpath.offset(10., utils::Join::Round); -// let offset_len = offset.len(); - -// let manipulator_groups = offset.manipulator_groups(); -// let round_start = manipulator_groups[offset_len - 4].anchor; -// let round_point = manipulator_groups[offset_len - 3].anchor; -// let round_end = manipulator_groups[offset_len - 2].anchor; - -// let middle = (round_start + round_end) / 2.; - -// assert!((round_point - middle).angle_to(round_start - middle) > 0.); -// assert!((round_end - middle).angle_to(round_point - middle) > 0.); -// } - -// #[test] -// fn round_join_clockwise_rotation() { -// // Test case where the round join is drawn in the clockwise direction between two consecutive offsets -// let subpath = Subpath::new( -// vec![ -// ManipulatorGroup { -// anchor: DVec2::new(20., 20.), -// out_handle: Some(DVec2::new(10., 90.)), -// in_handle: None, -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: DVec2::new(150., 40.), -// out_handle: None, -// in_handle: Some(DVec2::new(60., 40.)), -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: DVec2::new(78., 36.), -// out_handle: None, -// in_handle: None, -// id: EmptyId, -// }, -// ], -// false, -// ); - -// let offset = subpath.offset(-15., utils::Join::Round); -// let offset_len = offset.len(); - -// let manipulator_groups = offset.manipulator_groups(); -// let round_start = manipulator_groups[offset_len - 4].anchor; -// let round_point = manipulator_groups[offset_len - 3].anchor; -// let round_end = manipulator_groups[offset_len - 2].anchor; - -// let middle = (round_start + round_end) / 2.; - -// assert!((round_point - middle).angle_to(round_start - middle) < 0.); -// assert!((round_end - middle).angle_to(round_point - middle) < 0.); -// } -// } diff --git a/node-graph/gcore/src/subpath/transform.rs b/node-graph/gcore/src/subpath/transform.rs index 72a1e96464..5e98872903 100644 --- a/node-graph/gcore/src/subpath/transform.rs +++ b/node-graph/gcore/src/subpath/transform.rs @@ -2,13 +2,6 @@ use super::structs::Identifier; use super::*; use glam::{DAffine2, DVec2}; -/// Helper function to ensure the index and t value pair is mapped within a maximum index value. -/// Allows for the point to be fetched without needing to handle an additional edge case. -/// - Ex. Via `subpath.iter().nth(index).evaluate(t);` -fn map_index_within_range(index: usize, t: f64, max_size: usize) -> (usize, f64) { - if max_size > 0 && index == max_size && t == 0. { (index - 1, 1.) } else { (index, t) } -} - /// Functionality that transforms Subpaths, such as split, reduce, offset, etc. impl Subpath { /// Returns [ManipulatorGroup]s with a reversed winding order. @@ -68,575 +61,3 @@ impl Subpath { rotated_subpath } } - -// #[cfg(test)] -// mod tests { -// use super::{Cap, Join, ManipulatorGroup, Subpath}; -// use crate::EmptyId; -// use crate::compare::{compare_points, compare_subpaths, compare_vec_of_points}; -// use crate::consts::MAX_ABSOLUTE_DIFFERENCE; -// use crate::utils::{SubpathTValue, TValue}; -// use glam::DVec2; - -// fn set_up_open_subpath() -> Subpath { -// let start = DVec2::new(20., 30.); -// let middle1 = DVec2::new(80., 90.); -// let middle2 = DVec2::new(100., 100.); -// let end = DVec2::new(60., 45.); - -// let handle1 = DVec2::new(75., 85.); -// let handle2 = DVec2::new(40., 30.); -// let handle3 = DVec2::new(10., 10.); - -// Subpath::new( -// vec![ -// ManipulatorGroup { -// anchor: start, -// in_handle: None, -// out_handle: Some(handle1), -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: middle1, -// in_handle: None, -// out_handle: Some(handle2), -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: middle2, -// in_handle: None, -// out_handle: None, -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: end, -// in_handle: None, -// out_handle: Some(handle3), -// id: EmptyId, -// }, -// ], -// false, -// ) -// } - -// fn set_up_closed_subpath() -> Subpath { -// let mut subpath = set_up_open_subpath(); -// subpath.closed = true; -// subpath -// } - -// #[test] -// fn outline_with_single_point_segment() { -// let subpath = Subpath::new( -// vec![ -// ManipulatorGroup { -// anchor: DVec2::new(20., 20.), -// out_handle: Some(DVec2::new(10., 90.)), -// in_handle: None, -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: DVec2::new(150., 40.), -// out_handle: None, -// in_handle: Some(DVec2::new(60., 40.)), -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: DVec2::new(150., 40.), -// out_handle: Some(DVec2::new(40., 120.)), -// in_handle: None, -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: DVec2::new(100., 100.), -// out_handle: None, -// in_handle: None, -// id: EmptyId, -// }, -// ], -// false, -// ); - -// let outline = subpath.outline(10., crate::Join::Round, crate::Cap::Round).0; -// assert!(outline.manipulator_groups.windows(2).all(|pair| !pair[0].anchor.abs_diff_eq(pair[1].anchor, MAX_ABSOLUTE_DIFFERENCE))); -// assert!(outline.closed()); -// } - -// #[test] -// /// Even though the bézier here is not marked as a point, the offset and scaled version is. -// fn outline_with_point_offset() { -// let subpath = Subpath::new( -// vec![ -// ManipulatorGroup { -// anchor: DVec2::new(1122.6253015182049, 610.9441551227939), -// out_handle: Some(DVec2::new(1122.6253015182049, 610.9445412168651)), -// in_handle: None, -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: DVec2::new(1122.6258671405062, 610.9453107605276), -// out_handle: None, -// in_handle: Some(DVec2::new(1122.6254904904154, 610.9449255479497)), -// id: EmptyId, -// }, -// ManipulatorGroup { -// anchor: DVec2::new(0., 0.), -// out_handle: None, -// in_handle: None, -// id: EmptyId, -// }, -// ], -// false, -// ); -// let outline = subpath.outline(4.4, crate::Join::Round, crate::Cap::Round).0; -// assert!(outline.manipulator_groups.windows(2).all(|pair| !pair[0].anchor.abs_diff_eq(pair[1].anchor, MAX_ABSOLUTE_DIFFERENCE))); -// assert!(outline.closed()); -// } - -// #[test] -// fn split_an_open_subpath() { -// let subpath = set_up_open_subpath(); -// let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); -// let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 3.) % 1.)); -// let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.2)); -// assert!(second.is_some()); -// let second = second.unwrap(); -// assert_eq!(first.manipulator_groups[1].anchor, location); -// assert_eq!(second.manipulator_groups[0].anchor, location); -// assert_eq!(split_pair[0], first.iter().last().unwrap()); -// assert_eq!(split_pair[1], second.iter().next().unwrap()); -// } - -// #[test] -// fn split_at_start_of_an_open_subpath() { -// let subpath = set_up_open_subpath(); -// let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); -// let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric(0.)); -// let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.)); -// assert!(second.is_some()); -// let second = second.unwrap(); -// assert_eq!( -// first.manipulator_groups[0], -// ManipulatorGroup { -// anchor: location, -// in_handle: None, -// out_handle: None, -// id: EmptyId, -// } -// ); -// assert_eq!(first.manipulator_groups.len(), 1); -// assert_eq!(second.manipulator_groups[0].anchor, location); -// assert_eq!(split_pair[1], second.iter().next().unwrap()); -// } - -// #[test] -// fn split_at_end_of_an_open_subpath() { -// let subpath = set_up_open_subpath(); -// let location = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); -// let split_pair = subpath.iter().last().unwrap().split(TValue::Parametric(1.)); -// let (first, second) = subpath.split(SubpathTValue::GlobalParametric(1.)); -// assert!(second.is_some()); -// let second = second.unwrap(); -// assert_eq!(first.manipulator_groups[3].anchor, location); -// assert_eq!(split_pair[0], first.iter().last().unwrap()); -// assert_eq!( -// second.manipulator_groups[0], -// ManipulatorGroup { -// anchor: location, -// in_handle: None, -// out_handle: None, -// id: EmptyId, -// } -// ); -// assert_eq!(second.manipulator_groups.len(), 1); -// } - -// #[test] -// fn split_a_closed_subpath() { -// let subpath = set_up_closed_subpath(); -// let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); -// let split_pair = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 4.) % 1.)); -// let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.2)); -// assert!(second.is_none()); -// assert_eq!(first.manipulator_groups[0].anchor, location); -// assert_eq!(first.manipulator_groups[5].anchor, location); -// assert_eq!(first.manipulator_groups.len(), 6); -// assert_eq!(split_pair[0], first.iter().last().unwrap()); -// assert_eq!(split_pair[1], first.iter().next().unwrap()); -// } - -// #[test] -// fn split_at_start_of_a_closed_subpath() { -// let subpath = set_up_closed_subpath(); -// let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); -// let (first, second) = subpath.split(SubpathTValue::GlobalParametric(0.)); -// assert!(second.is_none()); -// assert_eq!(first.manipulator_groups[0].anchor, location); -// assert_eq!(first.manipulator_groups[4].anchor, location); -// assert_eq!(subpath.manipulator_groups[0..], first.manipulator_groups[..4]); -// assert!(!first.closed); -// assert_eq!(first.iter().last().unwrap(), subpath.iter().last().unwrap()); -// assert_eq!(first.iter().next().unwrap(), subpath.iter().next().unwrap()); -// } - -// #[test] -// fn split_at_end_of_a_closed_subpath() { -// let subpath = set_up_closed_subpath(); -// let location = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); -// let (first, second) = subpath.split(SubpathTValue::GlobalParametric(1.)); -// assert!(second.is_none()); -// assert_eq!(first.manipulator_groups[0].anchor, location); -// assert_eq!(first.manipulator_groups[4].anchor, location); -// assert_eq!(subpath.manipulator_groups[0..], first.manipulator_groups[..4]); -// assert!(!first.closed); -// assert_eq!(first.iter().last().unwrap(), subpath.iter().last().unwrap()); -// assert_eq!(first.iter().next().unwrap(), subpath.iter().next().unwrap()); -// } - -// #[test] -// fn reverse_an_open_subpath() { -// let subpath = set_up_open_subpath(); -// let temporary = subpath.reverse(); -// let result = temporary.reverse(); -// let end = result.len(); - -// assert_eq!(temporary.manipulator_groups[0].anchor, result.manipulator_groups[end - 1].anchor); -// assert_eq!(temporary.manipulator_groups[0].out_handle, result.manipulator_groups[end - 1].in_handle); -// assert_eq!(subpath, result); -// } - -// #[test] -// fn reverse_a_closed_subpath() { -// let subpath = set_up_closed_subpath(); -// let temporary = subpath.reverse(); -// let result = temporary.reverse(); -// let end = result.len(); - -// // Second manipulator group on the temporary subpath should be the reflected version of the last in the result -// assert_eq!(temporary.manipulator_groups[1].anchor, result.manipulator_groups[end - 1].anchor); -// assert_eq!(temporary.manipulator_groups[1].in_handle, result.manipulator_groups[end - 1].out_handle); -// assert_eq!(temporary.manipulator_groups[1].out_handle, result.manipulator_groups[end - 1].in_handle); - -// // The first manipulator group in both should be the reflected versions of each other -// assert_eq!(temporary.manipulator_groups[0].anchor, result.manipulator_groups[0].anchor); -// assert_eq!(temporary.manipulator_groups[0].in_handle, result.manipulator_groups[0].out_handle); -// assert_eq!(temporary.manipulator_groups[0].out_handle, result.manipulator_groups[0].in_handle); -// assert_eq!(subpath, result); -// } - -// #[test] -// fn trim_an_open_subpath() { -// let subpath = set_up_open_subpath(); -// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); -// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); -// let [_, trim_front] = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 3.) % 1.)); -// let [trim_back, _] = subpath.iter().last().unwrap().split(TValue::Parametric((0.8 * 3.) % 1.)); -// let result = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.8)); - -// assert_eq!(result.manipulator_groups[0].anchor, location_front); -// assert_eq!(result.manipulator_groups[3].anchor, location_back); -// assert_eq!(trim_front, result.iter().next().unwrap()); -// assert_eq!(trim_back, result.iter().last().unwrap()); -// } - -// #[test] -// fn trim_within_a_bezier() { -// let subpath = set_up_open_subpath(); -// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.1)); -// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); -// let trimmed = subpath.iter().next().unwrap().trim(TValue::Parametric((0.1 * 3.) % 1.), TValue::Parametric((0.2 * 3.) % 1.)); -// let result = subpath.trim(SubpathTValue::GlobalParametric(0.1), SubpathTValue::GlobalParametric(0.2)); -// assert!(compare_points(result.manipulator_groups[0].anchor, location_front)); -// assert!(compare_points(result.manipulator_groups[1].anchor, location_back)); -// assert_eq!(trimmed, result.iter().next().unwrap()); -// assert_eq!(result.len(), 2); -// } - -// #[test] -// fn trim_first_segment_of_an_open_subpath() { -// let subpath = set_up_closed_subpath(); -// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); -// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.25)); -// let trimmed = subpath.iter().next().unwrap().trim(TValue::Parametric(0.), TValue::Parametric(1.)); -// let result = subpath.trim(SubpathTValue::GlobalParametric(0.), SubpathTValue::GlobalParametric(0.25)); - -// assert_eq!(result.manipulator_groups[0].anchor, location_front); -// assert_eq!(result.manipulator_groups[1].anchor, location_back); -// assert_eq!(trimmed, result.iter().next().unwrap()); -// } - -// #[test] -// fn trim_second_segment_of_an_open_subpath() { -// let subpath = set_up_closed_subpath(); -// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.25)); -// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.5)); -// let trimmed = subpath.iter().nth(1).unwrap().trim(TValue::Parametric(0.), TValue::Parametric(1.)); -// let result = subpath.trim(SubpathTValue::GlobalParametric(0.25), SubpathTValue::GlobalParametric(0.5)); - -// assert_eq!(result.manipulator_groups[0].anchor, location_front); -// assert_eq!(result.manipulator_groups[1].anchor, location_back); -// assert_eq!(trimmed, result.iter().next().unwrap()); -// } - -// #[test] -// fn trim_reverse_in_open_subpath() { -// let subpath = set_up_open_subpath(); -// 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)); -// } - -// #[test] -// fn trim_reverse_within_a_bezier() { -// let subpath = set_up_open_subpath(); -// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.1)); -// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); -// let trimmed = subpath.iter().next().unwrap().trim(TValue::Parametric((0.2 * 3.) % 1.), TValue::Parametric((0.1 * 3.) % 1.)); -// let result = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.1)); - -// assert!(compare_points(result.manipulator_groups[0].anchor, location_front)); -// assert!(compare_points(result.manipulator_groups[1].anchor, location_back)); -// assert!(compare_vec_of_points( -// trimmed.get_points().collect(), -// result.iter().next().unwrap().get_points().collect(), -// MAX_ABSOLUTE_DIFFERENCE -// )); -// assert_eq!(result.len(), 2); -// } - -// #[test] -// fn trim_a_duplicate_subpath() { -// let subpath = set_up_open_subpath(); -// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); -// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); -// 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; -// expected_subpath[3].out_handle = None; - -// assert_eq!(result.manipulator_groups[0].anchor, location_front); -// assert!(compare_points(result.manipulator_groups[3].anchor, location_back)); -// assert_eq!(expected_subpath, result); -// } - -// #[test] -// fn trim_a_reversed_duplicate_subpath() { -// let subpath = set_up_open_subpath(); -// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); -// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); -// let result = subpath.trim(SubpathTValue::GlobalParametric(1.), SubpathTValue::GlobalParametric(0.)); - -// assert_eq!(result.manipulator_groups[0].anchor, location_front); -// assert_eq!(result.manipulator_groups[3].anchor, location_back); -// assert!(compare_subpaths::(&subpath, &result)); -// } - -// #[test] -// fn trim_to_end_of_subpath() { -// let subpath = set_up_open_subpath(); -// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); -// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); -// let trimmed = subpath.iter().last().unwrap().trim(TValue::Parametric((0.8 * 3.) % 1.), TValue::Parametric(1.)); -// let result = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(1.)); - -// assert_eq!(result.manipulator_groups[0].anchor, location_front); -// assert!(compare_points(result.manipulator_groups[1].anchor, location_back)); -// assert_eq!(trimmed, result.iter().next().unwrap()); -// } - -// #[test] -// fn trim_reversed_to_end_of_subpath() { -// let subpath = set_up_open_subpath(); -// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); -// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); -// let trimmed = subpath.iter().next().unwrap().trim(TValue::Parametric((0.2 * 3.) % 1.), TValue::Parametric(0.)); -// let result = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.)); - -// assert!(compare_points(result.manipulator_groups[0].anchor, location_front)); -// assert!(compare_points(result.manipulator_groups[1].anchor, location_back)); -// assert!(compare_vec_of_points( -// trimmed.get_points().collect(), -// result.iter().next().unwrap().get_points().collect(), -// MAX_ABSOLUTE_DIFFERENCE -// )); -// } - -// #[test] -// fn trim_start_point() { -// let subpath = set_up_open_subpath(); -// let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); -// let result = subpath.trim(SubpathTValue::GlobalParametric(0.), SubpathTValue::GlobalParametric(0.)); - -// assert!(compare_points(result.manipulator_groups[0].anchor, location)); -// assert!(result.manipulator_groups[0].in_handle.is_none()); -// assert!(result.manipulator_groups[0].out_handle.is_none()); -// assert_eq!(result.len(), 1); -// } - -// #[test] -// fn trim_middle_point() { -// let subpath = set_up_closed_subpath(); -// let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.25)); -// let result = subpath.trim(SubpathTValue::GlobalParametric(0.25), SubpathTValue::GlobalParametric(0.25)); - -// assert!(compare_points(result.manipulator_groups[0].anchor, location)); -// assert!(result.manipulator_groups[0].in_handle.is_none()); -// assert!(result.manipulator_groups[0].out_handle.is_none()); -// assert_eq!(result.len(), 1); -// } - -// #[test] -// fn trim_a_closed_subpath() { -// let subpath = set_up_closed_subpath(); -// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); -// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); -// let [_, trim_front] = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 4.) % 1.)); -// let [trim_back, _] = subpath.iter().last().unwrap().split(TValue::Parametric((0.8 * 4.) % 1.)); -// let result = subpath.trim(SubpathTValue::GlobalParametric(0.2), SubpathTValue::GlobalParametric(0.8)); - -// assert_eq!(result.manipulator_groups[0].anchor, location_front); -// assert_eq!(result.manipulator_groups[4].anchor, location_back); -// assert_eq!(trim_front, result.iter().next().unwrap()); -// assert_eq!(trim_back, result.iter().last().unwrap()); -// } - -// #[test] -// fn trim_to_end_of_closed_subpath() { -// let subpath = set_up_closed_subpath(); -// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); -// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); -// let trimmed = subpath.iter().last().unwrap().trim(TValue::Parametric((0.8 * 4.) % 1.), TValue::Parametric(1.)); -// let result = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(1.)); - -// assert_eq!(result.manipulator_groups[0].anchor, location_front); -// assert!(compare_points(result.manipulator_groups[1].anchor, location_back)); -// assert_eq!(trimmed, result.iter().next().unwrap()); -// } - -// #[test] -// fn trim_across_break_in_a_closed_subpath() { -// let subpath = set_up_closed_subpath(); -// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); -// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); -// let [_, trim_front] = subpath.iter().last().unwrap().split(TValue::Parametric((0.8 * 4.) % 1.)); -// let [trim_back, _] = subpath.iter().next().unwrap().split(TValue::Parametric((0.2 * 4.) % 1.)); -// let result = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(0.2)); - -// assert_eq!(result.manipulator_groups[0].anchor, location_front); -// assert_eq!(result.manipulator_groups[2].anchor, location_back); -// assert_eq!(trim_front, result.iter().next().unwrap()); -// assert_eq!(trim_back, result.iter().last().unwrap()); -// } - -// #[test] -// fn trim_across_break_in_a_closed_subpath_where_result_is_multiple_segments() { -// let subpath = set_up_closed_subpath(); -// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.6)); -// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.4)); -// let [_, trim_front] = subpath.iter().nth(2).unwrap().split(TValue::Parametric((0.6 * 4.) % 1.)); -// let [trim_back, _] = subpath.iter().nth(1).unwrap().split(TValue::Parametric((0.4 * 4.) % 1.)); -// let result = subpath.trim(SubpathTValue::GlobalParametric(0.6), SubpathTValue::GlobalParametric(0.4)); - -// assert_eq!(result.manipulator_groups[0].anchor, location_front); -// assert_eq!(result.manipulator_groups[4].anchor, location_back); -// assert_eq!(trim_front, result.iter().next().unwrap()); -// assert_eq!(trim_back, result.iter().last().unwrap()); -// } - -// #[test] -// fn trim_across_break_in_a_closed_subpath_where_ends_are_in_same_segment() { -// let subpath = set_up_closed_subpath(); -// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.45)); -// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.4)); -// let [_, trim_front] = subpath.iter().nth(1).unwrap().split(TValue::Parametric((0.45 * 4.) % 1.)); -// let [trim_back, _] = subpath.iter().nth(1).unwrap().split(TValue::Parametric((0.4 * 4.) % 1.)); -// let result = subpath.trim(SubpathTValue::GlobalParametric(0.45), SubpathTValue::GlobalParametric(0.4)); - -// assert_eq!(result.manipulator_groups[0].anchor, location_front); -// assert_eq!(result.manipulator_groups[5].anchor, location_back); -// assert_eq!(trim_front, result.iter().next().unwrap()); -// assert_eq!(trim_back, result.iter().last().unwrap()); -// } - -// #[test] -// fn trim_at_break_in_closed_subpath_where_end_is_0() { -// let subpath = set_up_closed_subpath(); -// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(0.8)); -// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); -// let trimmed = subpath.iter().last().unwrap().trim(TValue::Parametric((0.8 * 4.) % 1.), TValue::Parametric(1.)); -// let result = subpath.trim(SubpathTValue::GlobalParametric(0.8), SubpathTValue::GlobalParametric(0.)); - -// assert_eq!(result.manipulator_groups[0].anchor, location_front); -// assert_eq!(result.manipulator_groups[1].anchor, location_back); -// assert_eq!(trimmed, result.iter().next().unwrap()); -// } - -// #[test] -// fn trim_at_break_in_closed_subpath_where_start_is_1() { -// let subpath = set_up_closed_subpath(); -// let location_front = subpath.evaluate(SubpathTValue::GlobalParametric(1.)); -// let location_back = subpath.evaluate(SubpathTValue::GlobalParametric(0.2)); -// let trimmed = subpath.iter().next().unwrap().trim(TValue::Parametric(0.), TValue::Parametric((0.2 * 4.) % 1.)); -// let result = subpath.trim(SubpathTValue::GlobalParametric(1.), SubpathTValue::GlobalParametric(0.2)); - -// assert_eq!(result.manipulator_groups[0].anchor, location_front); -// assert_eq!(result.manipulator_groups[1].anchor, location_back); -// assert_eq!(trimmed, result.iter().next().unwrap()); -// } - -// #[test] -// fn trim_at_break_in_closed_subpath_from_1_to_0() { -// let subpath = set_up_closed_subpath(); -// let location = subpath.evaluate(SubpathTValue::GlobalParametric(0.)); -// let result = subpath.trim(SubpathTValue::GlobalParametric(1.), SubpathTValue::GlobalParametric(0.)); - -// assert_eq!(result.manipulator_groups[0].anchor, location); -// assert!(result.manipulator_groups[0].in_handle.is_none()); -// assert!(result.manipulator_groups[0].out_handle.is_none()); -// assert_eq!(result.manipulator_groups.len(), 1); -// } - -// #[test] -// fn outline_single_point_circle() { -// let ellipse: Subpath = Subpath::new_ellipse(DVec2::new(0., 0.), DVec2::new(50., 50.)).reverse(); -// let p = DVec2::new(25., 25.); - -// let subpath: Subpath = Subpath::from_anchors([p, p, p], false); -// let outline_open = subpath.outline(25., Join::Bevel, Cap::Round); -// assert_eq!(outline_open.0, ellipse); -// assert_eq!(outline_open.1, None); - -// let subpath_closed: Subpath = Subpath::from_anchors([p, p, p], true); -// let outline_closed = subpath_closed.outline(25., Join::Bevel, Cap::Round); -// assert_eq!(outline_closed.0, ellipse); -// assert_eq!(outline_closed.1, None); -// } - -// #[test] -// fn outline_single_point_square() { -// let square: Subpath = Subpath::from_anchors( -// [ -// DVec2::new(25., 0.), -// DVec2::new(0., 0.), -// DVec2::new(0., 50.), -// DVec2::new(25., 50.), -// DVec2::new(50., 50.), -// DVec2::new(50., 0.), -// ], -// true, -// ); -// let p = DVec2::new(25., 25.); - -// let subpath: Subpath = Subpath::from_anchors([p, p, p], false); -// let outline_open = subpath.outline(25., Join::Bevel, Cap::Square); -// assert_eq!(outline_open.0, square); -// assert_eq!(outline_open.1, None); - -// let subpath_closed: Subpath = Subpath::from_anchors([p, p, p], true); -// let outline_closed = subpath_closed.outline(25., Join::Bevel, Cap::Square); -// assert_eq!(outline_closed.0, square); -// assert_eq!(outline_closed.1, None); -// } -// } diff --git a/node-graph/gcore/src/subpath/utils.rs b/node-graph/gcore/src/subpath/utils.rs deleted file mode 100644 index c28c89d567..0000000000 --- a/node-graph/gcore/src/subpath/utils.rs +++ /dev/null @@ -1,429 +0,0 @@ -// use super::consts::{MAX_ABSOLUTE_DIFFERENCE, STRICT_MAX_ABSOLUTE_DIFFERENCE}; -// use super::*; -// use glam::{BVec2, DMat2, DVec2}; -// use std::fmt::Write; - -// #[derive(Copy, Clone, PartialEq)] -// /// A structure which can be used to reference a particular point along a `Bezier`. -// /// Assuming a 2-dimensional Bezier is represented as a parametric curve defined by components `(x(f(t), y(f(t))))`, this structure defines variants for `f(t)`. -// /// - The `Parametric` variant represents the point calculated using the parametric equation of the curve at argument `t`. That is, `f(t) = t`. Speed along the curve's parametric form is not constant. `t` must lie in the range `[0, 1]`. -// /// - The `Euclidean` variant represents the point calculated at a distance ratio `t` along the arc length of the curve in the range `[0, 1]`. Speed is constant along the curve's arc length. -// /// - E.g. If `d` is the distance from the start point of a `Bezier` to a certain point along the curve, and `l` is the total arc length of the curve, that certain point lies at a distance ratio `t = d / l`. -// /// - All `Bezier` functions will implicitly convert a Euclidean [TValue] argument to a parametric `t`-value using binary search, computed within a particular error. That is, a point at distance ratio `t*`, -// /// satisfying `|t* - t| <= error`. The default error is `0.001`. Given this requires a lengthier calculation, it is not recommended to use the `Euclidean` or `EuclideanWithinError` variants frequently in computationally intensive tasks. -// /// - The `EuclideanWithinError` variant functions exactly as the `Euclidean` variant, but allows the `error` to be customized when computing `t` internally. -// pub enum TValue { -// Parametric(f64), -// Euclidean(f64), -// EuclideanWithinError { t: f64, error: f64 }, -// } - -#[derive(Copy, Clone, PartialEq)] -pub enum TValueType { - Parametric, - Euclidean, -} - -#[derive(Copy, Clone, PartialEq)] -pub enum SubpathTValue { - Parametric { segment_index: usize, t: f64 }, - GlobalParametric(f64), - Euclidean { segment_index: usize, t: f64 }, - GlobalEuclidean(f64), - EuclideanWithinError { segment_index: usize, t: f64, error: f64 }, - GlobalEuclideanWithinError { t: f64, error: f64 }, -} - -// #[derive(Copy, Clone)] -// /// Represents the shape of the join between two segments of a path which meet at an angle. -// /// Bevel provides a flat connection, Miter provides a sharp connection, and Round provides a rounded connection. -// /// As defined in SVG: . -// pub enum Join { -// /// The join is a straight line between the end points of the offset path sides from the two connecting segments. -// Bevel, -// /// Optional f64 is the miter limit, which defaults to 4 if `None` or a value less than 1 is provided. -// /// The miter limit is used to prevent highly sharp angles from resulting in excessively long miter joins. -// /// If the miter limit is exceeded, the join will be converted to a bevel join. -// /// The value is the ratio of the miter length to the stroke width. -// /// When that ratio is greater than the miter limit, a bevel join is used instead. -// Miter(Option), -// /// The join is a circular arc between the end points of the offset path sides from the two connecting segments. -// Round, -// } - -// #[derive(Copy, Clone)] -// /// Enum to represent the cap type at the ends of an outline -// /// As defined in SVG: . -// pub enum Cap { -// Butt, -// Round, -// Square, -// } - -// /// 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. -// fn compute_abc_through_points(start_point: DVec2, point_on_curve: DVec2, end_point: DVec2, t_to_nth_power: f64, nth_power_of_one_minus_t: f64) -> [DVec2; 3] { -// let point_c_ratio = nth_power_of_one_minus_t / (t_to_nth_power + nth_power_of_one_minus_t); -// let c = point_c_ratio * start_point + (1. - point_c_ratio) * end_point; -// let ab_bc_ratio = (t_to_nth_power + nth_power_of_one_minus_t - 1.).abs() / (t_to_nth_power + nth_power_of_one_minus_t); -// let a = point_on_curve + (point_on_curve - c) / ab_bc_ratio; -// [a, point_on_curve, c] -// } - -// /// Compute `a`, `b`, and `c` for a quadratic curve that fits the start, end and point on curve at `t`. -// /// The definition for the `a`, `b`, `c` points are defined in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer. -// pub fn compute_abc_for_quadratic_through_points(start_point: DVec2, point_on_curve: DVec2, end_point: DVec2, t: f64) -> [DVec2; 3] { -// let t_squared = t * t; -// let one_minus_t = 1. - t; -// let squared_one_minus_t = one_minus_t * one_minus_t; -// compute_abc_through_points(start_point, point_on_curve, end_point, t_squared, squared_one_minus_t) -// } - -// /// Compute `a`, `b`, and `c` for a cubic curve that fits the start, end and point on curve at `t`. -// /// The definition for the `a`, `b`, `c` points are defined in [the projection identity section](https://pomax.github.io/bezierinfo/#abc) of Pomax's bezier curve primer. -// pub fn compute_abc_for_cubic_through_points(start_point: DVec2, point_on_curve: DVec2, end_point: DVec2, t: f64) -> [DVec2; 3] { -// let t_cubed = t * t * t; -// let one_minus_t = 1. - t; -// let cubed_one_minus_t = one_minus_t * one_minus_t * one_minus_t; - -// compute_abc_through_points(start_point, point_on_curve, end_point, t_cubed, cubed_one_minus_t) -// } - -// /// Find the roots of the linear equation `ax + b`. -// pub fn solve_linear(a: f64, b: f64) -> [Option; 3] { -// // There exist roots when `a` is not 0 -// if a.abs() > MAX_ABSOLUTE_DIFFERENCE { [Some(-b / a), None, None] } else { [None; 3] } -// } - -// /// Find the roots of the linear equation `ax^2 + bx + c`. -// /// Precompute the `discriminant` (`b^2 - 4ac`) and `two_times_a` arguments prior to calling this function for efficiency purposes. -// pub fn solve_quadratic(discriminant: f64, two_times_a: f64, b: f64, c: f64) -> [Option; 3] { -// let mut roots = [None; 3]; -// if two_times_a.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE { -// roots = solve_linear(b, c); -// } else if discriminant.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE { -// roots[0] = Some(-b / (two_times_a)); -// } else if discriminant > 0. { -// let root_discriminant = discriminant.sqrt(); -// roots[0] = Some((-b + root_discriminant) / (two_times_a)); -// roots[1] = Some((-b - root_discriminant) / (two_times_a)); -// } -// roots -// } - -// // TODO: Use an `impl Iterator` return type instead of a `Vec` -// /// Solve a cubic of the form `ax^3 + bx^2 + ct + d`. -// pub fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> [Option; 3] { -// if a.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE { -// if b.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE { -// // If both a and b are approximately 0, treat as a linear problem -// solve_linear(c, d) -// } else { -// // If a is approximately 0, treat as a quadratic problem -// let discriminant = c * c - 4. * b * d; -// solve_quadratic(discriminant, 2. * b, c, d) -// } -// } else { -// // https://momentsingraphics.de/CubicRoots.html -// let d_recip = a.recip(); -// const ONETHIRD: f64 = 1. / 3.; -// let scaled_c2 = b * (ONETHIRD * d_recip); -// let scaled_c1 = c * (ONETHIRD * d_recip); -// let scaled_c0 = d * d_recip; -// if !(scaled_c0.is_finite() && scaled_c1.is_finite() && scaled_c2.is_finite()) { -// // cubic coefficient is zero or nearly so. -// return solve_quadratic(c * c - 4. * b * d, 2. * b, c, d); -// } -// let (c0, c1, c2) = (scaled_c0, scaled_c1, scaled_c2); -// // (d0, d1, d2) is called "Delta" in article -// let d0 = (-c2).mul_add(c2, c1); -// let d1 = (-c1).mul_add(c2, c0); -// let d2 = c2 * c0 - c1 * c1; -// // d is called "Discriminant" -// let d = 4. * d0 * d2 - d1 * d1; -// // de is called "Depressed.x", Depressed.y = d0 -// let de = (-2. * c2).mul_add(d0, d1); -// if d < 0. { -// let sq = (-0.25 * d).sqrt(); -// let r = -0.5 * de; -// let t1 = (r + sq).cbrt() + (r - sq).cbrt(); -// [Some(t1 - c2), None, None] -// } else if d == 0. { -// let t1 = (-d0).sqrt().copysign(de); -// [Some(t1 - c2), Some(-2. * t1 - c2).filter(|&a| a != t1 - c2), None] -// } else { -// let th = d.sqrt().atan2(-de) * ONETHIRD; -// // (th_cos, th_sin) is called "CubicRoot" -// let (th_sin, th_cos) = th.sin_cos(); -// // (r0, r1, r2) is called "Root" -// let r0 = th_cos; -// let ss3 = th_sin * 3_f64.sqrt(); -// let r1 = 0.5 * (-th_cos + ss3); -// let r2 = 0.5 * (-th_cos - ss3); -// let t = 2. * (-d0).sqrt(); -// [Some(t.mul_add(r0, -c2)), Some(t.mul_add(r1, -c2)), Some(t.mul_add(r2, -c2))] -// } -// } -// } - -// /// Determines if two rectangles have any overlap. The rectangles are represented by a pair of coordinates that designate the top left and bottom right corners (in a graphical coordinate system). -// pub fn do_rectangles_overlap(rectangle1: [DVec2; 2], rectangle2: [DVec2; 2]) -> bool { -// let [bottom_left1, top_right1] = rectangle1; -// let [bottom_left2, top_right2] = rectangle2; - -// top_right1.x >= bottom_left2.x && top_right2.x >= bottom_left1.x && top_right2.y >= bottom_left1.y && top_right1.y >= bottom_left2.y -// } - -// /// Determines if a point is completely inside a rectangle, which is represented as a pair of coordinates [top-left, bottom-right]. -// pub fn is_point_inside_rectangle(rect: [DVec2; 2], point: DVec2) -> bool { -// let [top_left, bottom_right] = rect; -// point.x > top_left.x && point.x < bottom_right.x && point.y > top_left.y && point.y < bottom_right.y -// } - -// /// Determines if the inner rectangle is completely inside the outer rectangle. The rectangles are represented as pairs of coordinates [top-left, bottom-right]. -// pub fn is_rectangle_inside_other(inner: [DVec2; 2], outer: [DVec2; 2]) -> bool { -// is_point_inside_rectangle(outer, inner[0]) && is_point_inside_rectangle(outer, inner[1]) -// } - -// /// Returns the intersection of two lines. The lines are given by a point on the line and its slope (represented by a vector). -// pub fn line_intersection(point1: DVec2, point1_slope_vector: DVec2, point2: DVec2, point2_slope_vector: DVec2) -> DVec2 { -// assert!(point1_slope_vector.normalize() != point2_slope_vector.normalize()); - -// // Find the intersection when the first line is vertical -// if f64_compare(point1_slope_vector.x, 0., MAX_ABSOLUTE_DIFFERENCE) { -// let m2 = point2_slope_vector.y / point2_slope_vector.x; -// let b2 = point2.y - m2 * point2.x; -// DVec2::new(point1.x, point1.x * m2 + b2) -// } -// // Find the intersection when the second line is vertical -// else if f64_compare(point2_slope_vector.x, 0., MAX_ABSOLUTE_DIFFERENCE) { -// let m1 = point1_slope_vector.y / point1_slope_vector.x; -// let b1 = point1.y - m1 * point1.x; -// DVec2::new(point2.x, point2.x * m1 + b1) -// } -// // Find the intersection where neither line is vertical -// else { -// let m1 = point1_slope_vector.y / point1_slope_vector.x; -// let b1 = point1.y - m1 * point1.x; -// let m2 = point2_slope_vector.y / point2_slope_vector.x; -// let b2 = point2.y - m2 * point2.x; -// let intersection_x = (b2 - b1) / (m1 - m2); -// DVec2::new(intersection_x, intersection_x * m1 + b1) -// } -// } - -// /// Check if 3 points are collinear. -// pub fn are_points_collinear(p1: DVec2, p2: DVec2, p3: DVec2) -> bool { -// let matrix = DMat2::from_cols(p1 - p2, p2 - p3); -// f64_compare(matrix.determinant() / 2., 0., MAX_ABSOLUTE_DIFFERENCE) -// } - -// /// Compute the center of the circle that passes through all three provided points. The provided points cannot be collinear. -// pub fn compute_circle_center_from_points(p1: DVec2, p2: DVec2, p3: DVec2) -> Option { -// if are_points_collinear(p1, p2, p3) { -// return None; -// } - -// let midpoint_a = p1.lerp(p2, 0.5); -// let midpoint_b = p2.lerp(p3, 0.5); -// let midpoint_c = p3.lerp(p1, 0.5); - -// let tangent_a = (p1 - p2).perp(); -// let tangent_b = (p2 - p3).perp(); -// let tangent_c = (p3 - p1).perp(); - -// let intersect_a_b = line_intersection(midpoint_a, tangent_a, midpoint_b, tangent_b); -// let intersect_b_c = line_intersection(midpoint_b, tangent_b, midpoint_c, tangent_c); -// let intersect_c_a = line_intersection(midpoint_c, tangent_c, midpoint_a, tangent_a); - -// Some((intersect_a_b + intersect_b_c + intersect_c_a) / 3.) -// } - -// /// Compare two `f64` numbers with a provided max absolute value difference. -// pub fn f64_compare(a: f64, b: f64, max_abs_diff: f64) -> bool { -// (a - b).abs() < max_abs_diff -// } - -// /// Determine if an `f64` number is within a given range by using a max absolute value difference comparison. -// pub fn f64_approximately_in_range(value: f64, min: f64, max: f64, max_abs_diff: f64) -> bool { -// (min..=max).contains(&value) || f64_compare(value, min, max_abs_diff) || f64_compare(value, max, max_abs_diff) -// } - -// /// Compare the two values in a `DVec2` independently with a provided max absolute value difference. -// pub fn dvec2_compare(a: DVec2, b: DVec2, max_abs_diff: f64) -> BVec2 { -// BVec2::new((a.x - b.x).abs() < max_abs_diff, (a.y - b.y).abs() < max_abs_diff) -// } - -// /// Determine if the values in a `DVec2` are within a given range independently by using a max absolute value difference comparison. -// pub fn dvec2_approximately_in_range(point: DVec2, min_corner: DVec2, max_corner: DVec2, max_abs_diff: f64) -> BVec2 { -// (point.cmpge(min_corner) & point.cmple(max_corner)) | dvec2_compare(point, min_corner, max_abs_diff) | dvec2_compare(point, max_corner, max_abs_diff) -// } - -// /// Calculate a new position for a point given its original position, a unit vector in the desired direction, and a distance to move it by. -// pub fn scale_point_from_direction_vector(point: DVec2, direction_unit_vector: DVec2, should_flip_direction: bool, distance: f64) -> DVec2 { -// let should_reverse_factor = if should_flip_direction { -1. } else { 1. }; -// point + distance * direction_unit_vector * should_reverse_factor -// } - -// /// Scale a point by a given distance with respect to the provided origin. -// pub fn scale_point_from_origin(point: DVec2, origin: DVec2, should_flip_direction: bool, distance: f64) -> DVec2 { -// scale_point_from_direction_vector(point, (origin - point).normalize(), should_flip_direction, distance) -// } - -// /// Computes the necessary details 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, -// ) -> (DVec2, ManipulatorGroup, DVec2) { -// 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 }; - -// ( -// left - (left - center).perp() * handle_offset_factor, -// ManipulatorGroup::new( -// arc_point, -// Some(arc_point + center_to_arc_point.perp() * handle_offset_factor), -// Some(arc_point - center_to_arc_point.perp() * handle_offset_factor), -// ), -// right + (right - center).perp() * handle_offset_factor, -// ) -// } - -// pub fn format_point(svg: &mut String, prefix: &str, x: f64, y: f64) -> std::fmt::Result { -// write!(svg, "{prefix}{:.6}", x)?; -// let trimmed_length = svg.trim_end_matches('0').trim_end_matches('.').len(); -// svg.truncate(trimmed_length); - -// write!(svg, ",{:.6}", y)?; -// let trimmed_length = svg.trim_end_matches('0').trim_end_matches('.').len(); -// svg.truncate(trimmed_length); - -// Ok(()) -// } - -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::consts::MAX_ABSOLUTE_DIFFERENCE; -// use crate::{Bezier, EmptyId}; - -// /// Compare vectors of `f64`s with a provided max absolute value difference. -// fn f64_compare_vector(a: Vec, b: Vec, max_abs_diff: f64) -> bool { -// a.len() == b.len() && a.into_iter().zip(b).all(|(a, b)| f64_compare(a, b, max_abs_diff)) -// } - -// fn collect_roots(mut roots: [Option; 3]) -> Vec { -// roots.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap()); -// roots.into_iter().flatten().collect() -// } - -// #[test] -// fn test_solve_linear() { -// // Line that is on the x-axis -// assert!(collect_roots(solve_linear(0., 0.)).is_empty()); -// // Line that is parallel to but not on the x-axis -// assert!(collect_roots(solve_linear(0., 1.)).is_empty()); -// // Line with a non-zero slope -// assert!(collect_roots(solve_linear(2., -8.)) == vec![4.]); -// } - -// #[test] -// fn test_solve_cubic() { -// // discriminant == 0 -// let roots1 = collect_roots(solve_cubic(1., 0., 0., 0.)); -// assert!(roots1 == vec![0.]); - -// let roots2 = collect_roots(solve_cubic(1., 3., 0., -4.)); -// assert!(roots2 == vec![-2., 1.]); - -// // p == 0 -// let roots3 = collect_roots(solve_cubic(1., 0., 0., -1.)); -// assert!(roots3 == vec![1.]); - -// // discriminant > 0 -// let roots4 = collect_roots(solve_cubic(1., 3., 0., 2.)); -// assert!(f64_compare_vector(roots4, vec![-3.196], MAX_ABSOLUTE_DIFFERENCE)); - -// // discriminant < 0 -// let roots5 = collect_roots(solve_cubic(1., 3., 0., -1.)); -// assert!(f64_compare_vector(roots5, vec![-2.879, -0.653, 0.532], MAX_ABSOLUTE_DIFFERENCE)); - -// // quadratic -// let roots6 = collect_roots(solve_cubic(0., 3., 0., -3.)); -// assert!(roots6 == vec![-1., 1.]); - -// // linear -// let roots7 = collect_roots(solve_cubic(0., 0., 1., -1.)); -// assert!(roots7 == vec![1.]); -// } - -// #[test] -// fn test_do_rectangles_overlap() { -// // Rectangles overlap -// assert!(do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(20., 20.)], [DVec2::new(10., 10.), DVec2::new(30., 20.)])); -// // Rectangles share a side -// assert!(do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(10., 10.), DVec2::new(30., 30.)])); -// // Rectangle inside the other -// assert!(do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(2., 2.), DVec2::new(6., 4.)])); -// // No overlap, rectangles are beside each other -// assert!(!do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(20., 0.), DVec2::new(30., 10.)])); -// // No overlap, rectangles are above and below each other -// assert!(!do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(0., 20.), DVec2::new(20., 30.)])); -// } - -// #[test] -// fn test_is_rectangle_inside_other() { -// assert!(!is_rectangle_inside_other([DVec2::new(10., 10.), DVec2::new(50., 50.)], [DVec2::new(10., 10.), DVec2::new(50., 50.)])); -// assert!(is_rectangle_inside_other( -// [DVec2::new(10.01, 10.01), DVec2::new(49., 49.)], -// [DVec2::new(10., 10.), DVec2::new(50., 50.)] -// )); -// assert!(!is_rectangle_inside_other([DVec2::new(5., 5.), DVec2::new(50., 9.99)], [DVec2::new(10., 10.), DVec2::new(50., 50.)])); -// } - -// #[test] -// fn test_find_intersection() { -// // y = 2x + 10 -// // y = 5x + 4 -// // intersect at (2, 14) - -// let start1 = DVec2::new(0., 10.); -// let end1 = DVec2::new(0., 4.); -// let start_direction1 = DVec2::new(1., 2.); -// let end_direction1 = DVec2::new(1., 5.); -// assert!(line_intersection(start1, start_direction1, end1, end_direction1) == DVec2::new(2., 14.)); - -// // y = x -// // y = -x + 8 -// // intersect at (4, 4) - -// let start2 = DVec2::new(0., 0.); -// let end2 = DVec2::new(8., 0.); -// let start_direction2 = DVec2::new(1., 1.); -// let end_direction2 = DVec2::new(1., -1.); -// assert!(line_intersection(start2, start_direction2, end2, end_direction2) == DVec2::new(4., 4.)); -// } - -// #[test] -// fn test_are_points_collinear() { -// assert!(are_points_collinear(DVec2::new(2., 4.), DVec2::new(6., 8.), DVec2::new(4., 6.))); -// assert!(!are_points_collinear(DVec2::new(1., 4.), DVec2::new(6., 8.), DVec2::new(4., 6.))); -// } - -// #[test] -// fn test_compute_circle_center_from_points() { -// // 3/4 of unit circle -// let center1 = compute_circle_center_from_points(DVec2::new(0., 1.), DVec2::new(-1., 0.), DVec2::new(1., 0.)); -// assert_eq!(center1.unwrap(), DVec2::new(0., 0.)); -// // 1/4 of unit circle -// let center2 = compute_circle_center_from_points(DVec2::new(-1., 0.), DVec2::new(0., 1.), DVec2::new(1., 0.)); -// assert_eq!(center2.unwrap(), DVec2::new(0., 0.)); -// } -// } diff --git a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs index 6f069c615e..f07a4fb818 100644 --- a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs +++ b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs @@ -248,6 +248,68 @@ pub fn eval_pathseg(segment: PathSeg, t_value: TValue) -> f64 { } } +/// Return an approximation of the length centroid, together with the length, of the bezier curve. +/// +/// The length centroid is the center of mass for the arc length of the Bezier segment. +/// An infinitely thin wire forming the Bezier segment's shape would balance at this point. +/// +/// - `accuracy` is used to approximate the curve. +pub(crate) fn pathseg_length_centroid_and_length(segment: PathSeg, accuracy: Option) -> (Vec2, f64) { + match segment { + PathSeg::Line(line) => ((line.start().to_vec2() + line.end().to_vec2()) / 2., (line.start().to_vec2() - line.end().to_vec2()).length()), + PathSeg::Quad(quad_bez) => { + 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 upper = (a1 - a0).length() + (a2 - a1).length(); + if upper - lower <= 2. * accuracy || level >= 8 { + let length = (lower + upper) / 2.; + return (length, length * (a0 + a1 + a2) / 3.); + } + + let b1 = 0.5 * (a0 + a1); + let c1 = 0.5 * (a1 + a2); + let b2 = 0.5 * (b1 + c1); + + let (length1, centroid_part1) = recurse(a0, b1, b2, 0.5 * accuracy, level + 1); + let (length2, centroid_part2) = recurse(b2, c1, a2, 0.5 * accuracy, level + 1); + (length1 + length2, centroid_part1 + centroid_part2) + } + + let (length, centroid_parts) = recurse(p0.to_vec2(), p1.to_vec2(), p2.to_vec2(), accuracy.unwrap_or_default(), 0); + (centroid_parts / length, length) + } + PathSeg::Cubic(cubic_bez) => { + let CubicBez { p0, p1, p2, p3 } = cubic_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, a3: Vec2, accuracy: f64, level: u8) -> (f64, Vec2) { + let lower = (a3 - a0).length(); + let upper = (a1 - a0).length() + (a2 - a1).length() + (a3 - a2).length(); + if upper - lower <= 2. * accuracy || level >= 8 { + let length = (lower + upper) / 2.; + return (length, length * (a0 + a1 + a2 + a3) / 4.); + } + + let b1 = 0.5 * (a0 + a1); + let t0 = 0.5 * (a1 + a2); + let c1 = 0.5 * (a2 + a3); + let b2 = 0.5 * (b1 + t0); + let c2 = 0.5 * (t0 + c1); + let b3 = 0.5 * (b2 + c2); + + let (length1, centroid_part1) = recurse(a0, b1, b2, b3, 0.5 * accuracy, level + 1); + let (length2, centroid_part2) = recurse(b3, c2, c1, a3, 0.5 * accuracy, level + 1); + (length1 + length2, centroid_part1 + centroid_part2) + } + + let (length, centroid_parts) = recurse(p0.to_vec2(), p1.to_vec2(), p2.to_vec2(), p3.to_vec2(), accuracy.unwrap_or_default(), 0); + (centroid_parts / length, length) + } + } +} + /// Finds the t value of point on the given path segment i.e fractional distance along the segment's total length. /// It uses a binary search to find the value `t` such that the ratio `length_up_to_t / total_length` approximates the input `distance`. pub fn eval_pathseg_euclidean(segment: PathSeg, distance: f64, accuracy: f64) -> f64 { diff --git a/node-graph/gcore/src/vector/algorithms/intersection.rs b/node-graph/gcore/src/vector/algorithms/intersection.rs index de373aa1b8..04ed297b46 100644 --- a/node-graph/gcore/src/vector/algorithms/intersection.rs +++ b/node-graph/gcore/src/vector/algorithms/intersection.rs @@ -45,6 +45,20 @@ pub fn segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Opt } } +pub fn subsegment_intersections(segment1: PathSeg, min_t1: f64, max_t1: f64, segment2: PathSeg, min_t2: f64, max_t2: f64, accuracy: Option) -> 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, min_t1, max_t1, segment2, min_t2, max_t2, 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)] @@ -125,6 +139,66 @@ pub fn filtered_all_segment_intersections(segment1: PathSeg, segment2: PathSeg, }) } +/// Helper function to compute intersections between lists of subcurves. +/// This function uses the algorithm implemented in `intersections_between_subcurves`. +fn intersections_between_vectors_of_path_segments(subcurves1: &[(f64, f64, PathSeg)], subcurves2: &[(f64, f64, PathSeg)], accuracy: Option) -> Vec<(f64, f64)> { + let segment_pairs = subcurves1.iter().flat_map(move |(t11, t12, curve1)| { + subcurves2 + .iter() + .filter_map(move |(t21, t22, curve2)| curve1.bounding_box().overlaps(curve2.bounding_box()).then_some((t11, t12, curve1, t21, t22, curve2))) + }); + + segment_pairs + .flat_map(|(&t11, &t12, &curve1, &t21, &t22, &curve2)| subsegment_intersections(curve1, t11, t12, curve2, t21, t22, accuracy)) + .collect::>() +} + +fn pathseg_self_intersection(segment: PathSeg, accuracy: Option) -> Vec<(f64, f64)> { + let cubic_bez = match segment { + PathSeg::Line(_) | PathSeg::Quad(_) => return vec![], + PathSeg::Cubic(cubic_bez) => cubic_bez, + }; + + // Get 2 copies of the reduced curves + let quads1 = cubic_bez.to_quads(DEFAULT_ACCURACY).map(|(t1, t2, quad_bez)| (t1, t2, PathSeg::Quad(quad_bez))).collect::>(); + let quads2 = quads1.clone(); + + let num_curves = quads1.len(); + + // Adjacent reduced curves cannot intersect + if num_curves <= 2 { + return vec![]; + } + + // For each curve, look for intersections with every curve that is at least 2 indices away + quads1 + .iter() + .take(num_curves - 2) + .enumerate() + .flat_map(|(index, &subsegment)| intersections_between_vectors_of_path_segments(&[subsegment], &quads2[index + 2..], accuracy)) + .collect() +} + +/// Returns a list of parametric `t` values that correspond to the self intersection points of the current bezier curve. For each intersection point, the returned `t` value is the smaller of the two that correspond to the point. +/// 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. +/// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point. +/// - `minimum_separation` - The minimum difference between adjacent `t` values in sorted order +pub fn pathseg_self_intersections(segment: PathSeg, accuracy: Option, minimum_separation: Option) -> Vec<(f64, f64)> { + let mut intersection_t_values = pathseg_self_intersection(segment, 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}; From 240f618520b78b4f23ce4b566e7ec9d11b735f2e Mon Sep 17 00:00:00 2001 From: indierusty Date: Sun, 10 Aug 2025 10:41:21 +0530 Subject: [PATCH 05/10] remove unused --- .../gcore/src/vector/algorithms/bezpath_algorithms.rs | 9 --------- .../gcore/src/vector/algorithms/symmetrical_basis.rs | 8 ++++---- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs index f07a4fb818..5fd0f62d41 100644 --- a/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs +++ b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs @@ -224,15 +224,6 @@ pub fn pathseg_tangents_to_point(segment: PathSeg, point: Point) -> Vec { segment.to_cubic().tangents_to_point(point).to_vec() } -fn parameters(cubic_bez: CubicBez) -> (Vec2, Vec2, Vec2, Vec2) { - let c = (cubic_bez.p1 - cubic_bez.p0) * 3.0; - let b = (cubic_bez.p2 - cubic_bez.p1) * 3.0 - c; - let d = cubic_bez.p0.to_vec2(); - let a = cubic_bez.p3.to_vec2() - d - c - b; - - (a, b, c, d) -} - /// Return the subsegment for the given [TValue] range. Returns None if parametric value of `t1` is greater than `t2`. pub fn trim_pathseg(segment: PathSeg, t1: TValue, t2: TValue) -> Option { let t1 = eval_pathseg(segment, t1); diff --git a/node-graph/gcore/src/vector/algorithms/symmetrical_basis.rs b/node-graph/gcore/src/vector/algorithms/symmetrical_basis.rs index 4a6f3ea847..6f0aaf3339 100644 --- a/node-graph/gcore/src/vector/algorithms/symmetrical_basis.rs +++ b/node-graph/gcore/src/vector/algorithms/symmetrical_basis.rs @@ -282,7 +282,7 @@ impl SymmetricalBasisPair { (&self.x * &other.x) + (&self.y * &other.y) } - #[must_use] + #[allow(unused)] pub fn cross(&self, rhs: &Self) -> SymmetricalBasis { (&self.x * &rhs.y) - (&self.y * &rhs.x) } @@ -539,9 +539,9 @@ fn binomial_decrement_n(b: f64, n: usize, k: usize) -> f64 { #[must_use] pub(crate) fn to_symmetrical_basis_pair(bezier: PathSeg) -> SymmetricalBasisPair { let n = match bezier { - PathSeg::Line(line) => 1, - PathSeg::Quad(quad_bez) => 2, - PathSeg::Cubic(cubic_bez) => 3, + PathSeg::Line(_) => 1, + PathSeg::Quad(_) => 2, + PathSeg::Cubic(_) => 3, }; let q = (n + 1) / 2; let even = n % 2 == 0; From ebf74815f4e4205e24a81c3127e159ee9aba593b Mon Sep 17 00:00:00 2001 From: indierusty Date: Sun, 10 Aug 2025 11:07:51 +0530 Subject: [PATCH 06/10] cleanup --- node-graph/gcore/src/subpath/core.rs | 21 --------------------- node-graph/gcore/src/subpath/mod.rs | 5 ----- node-graph/gcore/src/subpath/structs.rs | 15 --------------- 3 files changed, 41 deletions(-) diff --git a/node-graph/gcore/src/subpath/core.rs b/node-graph/gcore/src/subpath/core.rs index fee23209be..0e2c914846 100644 --- a/node-graph/gcore/src/subpath/core.rs +++ b/node-graph/gcore/src/subpath/core.rs @@ -316,25 +316,4 @@ impl Subpath { pub fn new_line(p1: DVec2, p2: DVec2) -> Self { Self::from_anchors([p1, p2], false) } - - #[cfg(feature = "kurbo")] - pub fn to_vello_path(&self, transform: glam::DAffine2, path: &mut kurbo::BezPath) { - use crate::BezierHandles; - - let to_point = |p: DVec2| { - let p = transform.transform_point2(p); - kurbo::Point::new(p.x, p.y) - }; - path.move_to(to_point(self.iter().next().unwrap().start)); - for segment in self.iter() { - match segment.handles { - BezierHandles::Linear => path.line_to(to_point(segment.end)), - BezierHandles::Quadratic { handle } => path.quad_to(to_point(handle), to_point(segment.end)), - BezierHandles::Cubic { handle_start, handle_end } => path.curve_to(to_point(handle_start), to_point(handle_end), to_point(segment.end)), - } - } - if self.closed { - path.close_path(); - } - } } diff --git a/node-graph/gcore/src/subpath/mod.rs b/node-graph/gcore/src/subpath/mod.rs index b6e9ea1143..1e50e32f40 100644 --- a/node-graph/gcore/src/subpath/mod.rs +++ b/node-graph/gcore/src/subpath/mod.rs @@ -20,11 +20,6 @@ pub struct Subpath { pub closed: bool, } -#[cfg(feature = "dyn-any")] -unsafe impl dyn_any::StaticType for Subpath { - type Static = Subpath; -} - /// Iteration structure for iterating across each curve of a `Subpath`, using an intermediate `Bezier` representation. pub struct SubpathIter<'a, PointId: Identifier> { index: usize, diff --git a/node-graph/gcore/src/subpath/structs.rs b/node-graph/gcore/src/subpath/structs.rs index e5191ff19d..5ac954f58a 100644 --- a/node-graph/gcore/src/subpath/structs.rs +++ b/node-graph/gcore/src/subpath/structs.rs @@ -49,11 +49,6 @@ impl Hash for ManipulatorGroup { } } -#[cfg(feature = "dyn-any")] -unsafe impl dyn_any::StaticType for ManipulatorGroup { - type Static = ManipulatorGroup; -} - impl Debug for ManipulatorGroup { fn fmt(&self, f: &mut Formatter<'_>) -> Result { f.debug_struct("ManipulatorGroup") @@ -248,11 +243,6 @@ impl BezierHandles { } } -#[cfg(feature = "dyn-any")] -unsafe impl dyn_any::StaticType for BezierHandles { - type Static = BezierHandles; -} - /// Representation of a bezier curve with 2D points. #[derive(Copy, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -278,11 +268,6 @@ impl Debug for Bezier { } } -#[cfg(feature = "dyn-any")] -unsafe impl dyn_any::StaticType for Bezier { - type Static = Bezier; -} - /// Functionality for the getters and setters of the various points in a Bezier impl Bezier { /// Set the coordinates of the start point. From eef780aa6613259268f2bc2288c78965dae5ebbb Mon Sep 17 00:00:00 2001 From: indierusty Date: Sun, 10 Aug 2025 11:34:16 +0530 Subject: [PATCH 07/10] fix pathseg_points function --- node-graph/gcore/src/subpath/core.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node-graph/gcore/src/subpath/core.rs b/node-graph/gcore/src/subpath/core.rs index 0e2c914846..93e6b10c86 100644 --- a/node-graph/gcore/src/subpath/core.rs +++ b/node-graph/gcore/src/subpath/core.rs @@ -21,8 +21,8 @@ impl PathSegPoints { pub fn pathseg_points(segment: PathSeg) -> PathSegPoints { match segment { PathSeg::Line(line) => PathSegPoints::new(point_to_dvec2(line.p0), None, None, point_to_dvec2(line.p1)), - PathSeg::Quad(quad) => PathSegPoints::new(point_to_dvec2(quad.p0), None, Some(point_to_dvec2(quad.p1)), point_to_dvec2(quad.p1)), - PathSeg::Cubic(cube) => PathSegPoints::new(point_to_dvec2(cube.p0), Some(point_to_dvec2(cube.p1)), Some(point_to_dvec2(cube.p2)), point_to_dvec2(cube.p1)), + PathSeg::Quad(quad) => PathSegPoints::new(point_to_dvec2(quad.p0), None, Some(point_to_dvec2(quad.p1)), point_to_dvec2(quad.p2)), + PathSeg::Cubic(cube) => PathSegPoints::new(point_to_dvec2(cube.p0), Some(point_to_dvec2(cube.p1)), Some(point_to_dvec2(cube.p2)), point_to_dvec2(cube.p3)), } } From 11314e626470aea0ff9ceae7ec280bfe52a28baa Mon Sep 17 00:00:00 2001 From: indierusty Date: Sun, 10 Aug 2025 12:20:46 +0530 Subject: [PATCH 08/10] fix tranforming segments --- node-graph/gcore/src/vector/misc.rs | 9 --------- node-graph/gpath-bool/src/lib.rs | 8 +++----- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index 2a3ea467ad..3f65ed3deb 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -220,15 +220,6 @@ pub fn pathseg_points_vec(segment: PathSeg) -> Vec { } } -pub fn transform_pathseg(segment: PathSeg, transform: impl Fn(DVec2) -> DVec2) -> PathSeg { - let transform = |point: Point| dvec2_to_point(transform(point_to_dvec2(point))); - match segment { - PathSeg::Line(line) => PathSeg::Line(Line::new(transform(line.p0), transform(line.p1))), - PathSeg::Quad(quad_bez) => PathSeg::Quad(QuadBez::new(transform(quad_bez.p0), transform(quad_bez.p1), transform(quad_bez.p2))), - PathSeg::Cubic(cubic_bez) => PathSeg::Cubic(CubicBez::new(transform(cubic_bez.p0), transform(cubic_bez.p0), transform(cubic_bez.p0), transform(cubic_bez.p0))), - } -} - /// Returns true if the corresponding points of the two [`PathSeg`]s are within the provided absolute value difference from each other. pub fn pathseg_abs_diff_eq(seg1: PathSeg, seg2: PathSeg, max_abs_diff: f64) -> bool { let seg1 = if is_linear(seg1) { PathSeg::Line(Line::new(seg1.start(), seg1.end())) } else { seg1 }; diff --git a/node-graph/gpath-bool/src/lib.rs b/node-graph/gpath-bool/src/lib.rs index 9e609ef3f4..1535893271 100644 --- a/node-graph/gpath-bool/src/lib.rs +++ b/node-graph/gpath-bool/src/lib.rs @@ -3,14 +3,13 @@ use glam::{DAffine2, DVec2}; use graphene_core::subpath::{ManipulatorGroup, Subpath}; use graphene_core::table::{Table, TableRow, TableRowRef}; use graphene_core::vector::algorithms::merge_by_distance::MergeByDistanceExt; -use graphene_core::vector::misc::{point_to_dvec2, transform_pathseg}; +use graphene_core::vector::misc::point_to_dvec2; use graphene_core::vector::style::Fill; use graphene_core::vector::{PointId, Vector}; use graphene_core::{Color, Ctx, Graphic}; -use kurbo::{ParamCurve, Point}; +use kurbo::{Affine, ParamCurve, Point}; pub use path_bool as path_bool_lib; use path_bool::{FillRule, PathBooleanOperation}; -use std::ops::Mul; // TODO: Fix boolean ops to work by removing .transform() and .one_instnace_*() calls, // TODO: since before we used a Vec of single-row tables and now we use a single table @@ -300,8 +299,7 @@ fn to_path_segments(path: &mut Vec, subpath: &Subpath Date: Sun, 10 Aug 2025 14:25:56 +0530 Subject: [PATCH 09/10] fix segment intersection --- node-graph/gcore/src/vector/algorithms/intersection.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/node-graph/gcore/src/vector/algorithms/intersection.rs b/node-graph/gcore/src/vector/algorithms/intersection.rs index 04ed297b46..222033e97f 100644 --- a/node-graph/gcore/src/vector/algorithms/intersection.rs +++ b/node-graph/gcore/src/vector/algorithms/intersection.rs @@ -63,8 +63,8 @@ pub fn subsegment_intersections(segment1: PathSeg, min_t1: f64, max_t1: f64, seg /// 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 bbox1 = segment1.subsegment(min_t1..max_t1).bounding_box(); + let bbox2 = segment2.subsegment(min_t2..max_t2).bounding_box(); let mid_t1 = (min_t1 + max_t1) / 2.; let mid_t2 = (min_t2 + max_t2) / 2.; @@ -72,7 +72,7 @@ fn segment_intersections_inner(segment1: PathSeg, min_t1: f64, max_t1: f64, segm // 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 { + if bbox1.width().abs() < accuracy && bbox1.height().abs() < accuracy && bbox2.width().abs() < accuracy && bbox2.height().abs() < accuracy { // Use the middle `t` value, append the corresponding `t` value intersections.push((mid_t1, mid_t2)); return; From a5901dab772f5fedff3d5cf9e5ac36fc99e69816 Mon Sep 17 00:00:00 2001 From: indierusty Date: Sun, 10 Aug 2025 14:28:22 +0530 Subject: [PATCH 10/10] refactor to_path_segments fn in gpath-bool crate --- node-graph/gpath-bool/src/lib.rs | 37 ++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/node-graph/gpath-bool/src/lib.rs b/node-graph/gpath-bool/src/lib.rs index 1535893271..fbaa767b15 100644 --- a/node-graph/gpath-bool/src/lib.rs +++ b/node-graph/gpath-bool/src/lib.rs @@ -1,15 +1,14 @@ use dyn_any::DynAny; use glam::{DAffine2, DVec2}; -use graphene_core::subpath::{ManipulatorGroup, Subpath}; +use graphene_core::subpath::{ManipulatorGroup, PathSegPoints, Subpath, pathseg_points}; use graphene_core::table::{Table, TableRow, TableRowRef}; use graphene_core::vector::algorithms::merge_by_distance::MergeByDistanceExt; -use graphene_core::vector::misc::point_to_dvec2; use graphene_core::vector::style::Fill; use graphene_core::vector::{PointId, Vector}; use graphene_core::{Color, Ctx, Graphic}; -use kurbo::{Affine, ParamCurve, Point}; pub use path_bool as path_bool_lib; use path_bool::{FillRule, PathBooleanOperation}; +use std::ops::Mul; // TODO: Fix boolean ops to work by removing .transform() and .one_instnace_*() calls, // TODO: since before we used a Vec of single-row tables and now we use a single table @@ -297,24 +296,34 @@ fn to_path(vector: &Vector, transform: DAffine2) -> Vec fn to_path_segments(path: &mut Vec, subpath: &Subpath, transform: DAffine2) { use path_bool::PathSegment; let mut global_start = None; - let mut global_end = Point::ZERO; + let mut global_end = DVec2::ZERO; + for bezier in subpath.iter() { - let transformed = Affine::new(transform.to_cols_array()) * bezier; - let start = transformed.start(); - let end = transformed.end(); + const EPS: f64 = 1e-8; + let transform_point = |pos: DVec2| transform.transform_point2(pos).mul(EPS.recip()).round().mul(EPS); + + let PathSegPoints { p0, p1, p2, p3 } = pathseg_points(bezier); + + let p0 = transform_point(p0); + let p1 = p1.map(|p1| transform_point(p1)); + let p2 = p2.map(|p2| transform_point(p2)); + let p3 = transform_point(p3); + if global_start.is_none() { - global_start = Some(start); + global_start = Some(p0); } - global_end = end; - let segment = match transformed { - kurbo::PathSeg::Line(line) => PathSegment::Line(point_to_dvec2(line.p0), point_to_dvec2(line.p1)), - kurbo::PathSeg::Quad(quad_bez) => PathSegment::Quadratic(point_to_dvec2(quad_bez.p0), point_to_dvec2(quad_bez.p1), point_to_dvec2(quad_bez.p2)), - kurbo::PathSeg::Cubic(cubic_bez) => PathSegment::Cubic(point_to_dvec2(cubic_bez.p0), point_to_dvec2(cubic_bez.p1), point_to_dvec2(cubic_bez.p2), point_to_dvec2(cubic_bez.p3)), + global_end = p3; + + let segment = match (p1, p2) { + (None, None) => PathSegment::Line(p0, p3), + (None, Some(p2)) | (Some(p2), None) => PathSegment::Quadratic(p0, p2, p3), + (Some(p1), Some(p2)) => PathSegment::Cubic(p0, p1, p2, p3), }; + path.push(segment); } if let Some(start) = global_start { - path.push(PathSegment::Line(point_to_dvec2(global_end), point_to_dvec2(start))); + path.push(PathSegment::Line(global_end, start)); } }