diff --git a/Cargo.lock b/Cargo.lock index b34cf496d9..d063c99efa 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", @@ -2131,7 +2130,6 @@ name = "graphite-editor" version = "0.0.0" dependencies = [ "base64 0.22.1", - "bezier-rs", "bitflags 2.9.1", "bytemuck", "derivative", @@ -2140,6 +2138,7 @@ dependencies = [ "futures", "glam", "graph-craft", + "graphene-core", "graphene-std", "graphite-proc-macros", "interpreted-executor", diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 9287333e15..01cd5ebdc4 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 c36d29da39..405b0d43a3 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -27,10 +27,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; @@ -38,7 +38,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; @@ -2982,10 +2984,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| { @@ -2996,7 +2998,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); @@ -3072,7 +3074,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 5207a90aa0..a02f0b971d 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 @@ -1836,7 +1836,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 9ec886fc9c..2005597e9f 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -22,10 +22,10 @@ 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 derivative::*; 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/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/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/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..93e6b10c86 --- /dev/null +++ b/node-graph/gcore/src/subpath/core.rs @@ -0,0 +1,319 @@ +use crate::vector::misc::point_to_dvec2; + +use super::consts::*; +use super::*; +use glam::DVec2; +use kurbo::PathSeg; + +pub struct PathSegPoints { + pub p0: DVec2, + pub p1: Option, + pub p2: Option, + pub 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) => 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.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)), + } +} + +/// 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 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 + /// 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.p0, + in_handle: None, + out_handle: first.p1, + id: PointId::new(), + }]; + let mut inner_groups: Vec> = beziers + .windows(2) + .map(|bezier_pair| ManipulatorGroup { + anchor: bezier_pair[1].p0, + in_handle: bezier_pair[0].p2, + out_handle: bezier_pair[1].p1, + id: PointId::new(), + }) + .collect::>>(); + manipulator_groups.append(&mut inner_groups); + + let last = beziers.last().unwrap(); + if !closed { + manipulator_groups.push(ManipulatorGroup { + 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.p2; + 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) + } +} diff --git a/node-graph/gcore/src/subpath/lookup.rs b/node-graph/gcore/src/subpath/lookup.rs new file mode 100644 index 0000000000..37608b0f7c --- /dev/null +++ b/node-graph/gcore/src/subpath/lookup.rs @@ -0,0 +1,116 @@ +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/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..1e50e32f40 --- /dev/null +++ b/node-graph/gcore/src/subpath/mod.rs @@ -0,0 +1,71 @@ +mod consts; +mod core; +mod lookup; +mod manipulators; +mod solvers; +mod structs; +mod transform; + +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, +} + +/// 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..3fda325d38 --- /dev/null +++ b/node-graph/gcore/src/subpath/solvers.rs @@ -0,0 +1,85 @@ +use crate::subpath::{Identifier, Subpath}; +use crate::vector::{algorithms::bezpath_algorithms::bezpath_is_inside_bezpath, misc::dvec2_to_point}; + +use glam::DVec2; +use kurbo::{Affine, BezPath, Shape}; + +impl Subpath { + 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 + } + + /// Returns `true` if this subpath is completely inside the `other` subpath. + /// + 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. + /// + 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])]) + } +} diff --git a/node-graph/gcore/src/subpath/structs.rs b/node-graph/gcore/src/subpath/structs.rs new file mode 100644 index 0000000000..5ac954f58a --- /dev/null +++ b/node-graph/gcore/src/subpath/structs.rs @@ -0,0 +1,429 @@ +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); + } +} + +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, + } + } +} + +/// Representation of a bezier curve with 2D points. +#[derive(Copy, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Bezier { + /// Start point of the bezier curve. + pub start: DVec2, + /// End point of the bezier curve. + pub end: DVec2, + /// Handles of the bezier curve. + pub handles: BezierHandles, +} + +impl Debug for Bezier { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + let mut debug_struct = f.debug_struct("Bezier"); + let mut debug_struct_ref = debug_struct.field("start", &self.start); + debug_struct_ref = match self.handles { + BezierHandles::Linear => debug_struct_ref, + BezierHandles::Quadratic { handle } => debug_struct_ref.field("handle", &handle), + BezierHandles::Cubic { handle_start, handle_end } => debug_struct_ref.field("handle_start", &handle_start).field("handle_end", &handle_end), + }; + debug_struct_ref.field("end", &self.end).finish() + } +} + +/// Functionality for the getters and setters of the various points in a Bezier +impl Bezier { + /// Set the coordinates of the start point. + pub fn set_start(&mut self, s: DVec2) { + self.start = s; + } + + /// Set the coordinates of the end point. + pub fn set_end(&mut self, e: DVec2) { + self.end = e; + } + + /// Set the coordinates of the first handle point. This represents the only handle in a quadratic segment. If used on a linear segment, it will be changed to a quadratic. + pub fn set_handle_start(&mut self, h1: DVec2) { + match self.handles { + BezierHandles::Linear => { + self.handles = BezierHandles::Quadratic { handle: h1 }; + } + BezierHandles::Quadratic { ref mut handle } => { + *handle = h1; + } + BezierHandles::Cubic { ref mut handle_start, .. } => { + *handle_start = h1; + } + }; + } + + /// Set the coordinates of the second handle point. This will convert both linear and quadratic segments into cubic ones. For a linear segment, the first handle will be set to the start point. + pub fn set_handle_end(&mut self, h2: DVec2) { + match self.handles { + BezierHandles::Linear => { + self.handles = BezierHandles::Cubic { + handle_start: self.start, + handle_end: h2, + }; + } + BezierHandles::Quadratic { handle } => { + self.handles = BezierHandles::Cubic { handle_start: handle, handle_end: h2 }; + } + BezierHandles::Cubic { ref mut handle_end, .. } => { + *handle_end = h2; + } + }; + } + + /// Get the coordinates of the bezier segment's start point. + pub fn start(&self) -> DVec2 { + self.start + } + + /// Get the coordinates of the bezier segment's end point. + pub fn end(&self) -> DVec2 { + self.end + } + + /// Get the coordinates of the bezier segment's first handle point. This represents the only handle in a quadratic segment. + pub fn handle_start(&self) -> Option { + 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..5e98872903 --- /dev/null +++ b/node-graph/gcore/src/subpath/transform.rs @@ -0,0 +1,63 @@ +use super::structs::Identifier; +use super::*; +use glam::{DAffine2, DVec2}; + +/// 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 + } +} 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/bezpath_algorithms.rs b/node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs index f6dd1b31d9..5fd0f62d41 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,42 @@ 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() +} + /// 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); @@ -202,6 +239,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 { @@ -392,8 +491,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 +553,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/instance.rs b/node-graph/gcore/src/vector/algorithms/instance.rs index f72aefd62b..abb519214b 100644 --- a/node-graph/gcore/src/vector/algorithms/instance.rs +++ b/node-graph/gcore/src/vector/algorithms/instance.rs @@ -102,8 +102,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/algorithms/intersection.rs b/node-graph/gcore/src/vector/algorithms/intersection.rs index de373aa1b8..222033e97f 100644 --- a/node-graph/gcore/src/vector/algorithms/intersection.rs +++ b/node-graph/gcore/src/vector/algorithms/intersection.rs @@ -45,12 +45,26 @@ 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)] 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.; @@ -58,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; @@ -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}; 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..6f0aaf3339 --- /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) + } + + #[allow(unused)] + 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(_) => 1, + PathSeg::Quad(_) => 2, + PathSeg::Cubic(_) => 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/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..3f65ed3deb 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); @@ -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(), @@ -225,8 +225,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/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 9b6a908891..055c78e2e1 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, RenderBoundingBox}; 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}; @@ -837,11 +837,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 @@ -1361,7 +1361,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); } @@ -1415,14 +1415,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 => {} } } @@ -1856,7 +1856,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 e3a7b098f1..442d834c10 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, RenderBoundingBox}; 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,19 @@ 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: (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) } - 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])) @@ -515,9 +520,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()); @@ -532,10 +539,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))); @@ -545,11 +552,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::>(); @@ -558,14 +565,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 f9931b30dc..25d3ef52f0 100644 --- a/node-graph/gpath-bool/src/lib.rs +++ b/node-graph/gpath-bool/src/lib.rs @@ -1,6 +1,6 @@ -use bezier_rs::{ManipulatorGroup, Subpath}; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; +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::style::Fill; @@ -312,20 +312,29 @@ fn to_path_segments(path: &mut Vec, subpath: &Subpath 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), + 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 { 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 c7f19faa25..3214d40db0 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; @@ -12,6 +11,7 @@ use graphene_core::raster::BitmapMut; 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}; @@ -19,6 +19,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; @@ -427,8 +428,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()); @@ -533,8 +535,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