Port all remaining Subpath producers to BezPath and delete the legacy subpath module (#4457)

* Port all remaining Subpath producers to BezPath and delete the legacy subpath module

* Reset contour state at each MoveTo so an open contour's segments don't leak into the next region

* Give the polygon and star constructors a true center and radius instead of compensated arguments
This commit is contained in:
Keavon Chambers
2026-08-18 15:28:58 -07:00
committed by GitHub
parent 8f1b2bed5f
commit e3b968f7e2
43 changed files with 961 additions and 1291 deletions

View File

@@ -12,8 +12,7 @@ use graph_craft::application_io::resource::{DataSource, ResourceHash};
use graph_craft::document::NodeId;
use graphene_std::Color;
use graphene_std::raster::Image;
use graphene_std::subpath::BezierHandles;
use graphene_std::vector::misc::{HandleId, point_to_dvec2, segment_to_handles};
use graphene_std::vector::misc::{BezierHandles, HandleId, point_to_dvec2, segment_to_handles};
use graphene_std::vector::{PointId, SegmentId, VectorModificationType};
use graphite_proc_macros::{ExtractField, message_handler_data};
use kurbo::ParamCurve;

View File

@@ -18,12 +18,12 @@ use graphene_std::raster::{
CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, SelectiveColorChoice,
};
use graphene_std::raster_types::{CPU, GPU, Raster};
use graphene_std::subpath::BezierHandles;
use graphene_std::text::TextAlign;
use graphene_std::text_nodes::StringCapitalization;
use graphene_std::transform::{ReferencePoint, ScaleType};
use graphene_std::vector::misc::{
ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
ArcType, BezierHandles, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns,
SpiralType,
};
use graphene_std::vector::style::{
DashPattern, FillChoice, GradientForm, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, StrokeAlign, StrokeCap, StrokeJoin,

View File

@@ -41,13 +41,12 @@ use graphene_std::Cover;
use graphene_std::math::quad::Quad;
use graphene_std::path_bool_nodes::boolean_intersect;
use graphene_std::raster::BlendMode;
use graphene_std::subpath::Subpath;
use graphene_std::vector::algorithms::bezpath_algorithms::bezpath_is_inside_bezpath;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::graphic_types;
use graphene_std::vector::misc::dvec2_to_point;
use graphene_std::vector::style::RenderMode;
use graphene_std::vector::{PointId, graphic_types};
use kurbo::{Affine, BezPath, Line, PathSeg};
use kurbo::{Affine, BezPath, Line, PathSeg, Shape};
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::Arc;
@@ -1832,17 +1831,16 @@ impl DocumentMessageHandler {
self.intersect_quad(viewport_quad, viewport).filter(|layer| !self.network_interface.is_artboard(&layer.to_node(), &[]))
}
/// Runs an intersection test with all layers and a viewport space subpath
pub fn intersect_polygon<'a>(&'a self, mut viewport_polygon: Subpath<PointId>, viewport: &ViewportMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + use<'a> {
/// Runs an intersection test with all layers and a viewport space polygon path
pub fn intersect_polygon<'a>(&'a self, mut viewport_polygon: BezPath, viewport: &ViewportMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + use<'a> {
let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz);
viewport_polygon.apply_transform(document_to_viewport.inverse());
viewport_polygon.apply_affine(Affine::new(document_to_viewport.inverse().to_cols_array()));
let polygon = BezPath::from_path_segments(viewport_polygon.iter_closed());
ClickXRayIter::new(&self.network_interface, XRayTarget::Path(polygon))
ClickXRayIter::new(&self.network_interface, XRayTarget::Path(viewport_polygon))
}
/// Runs an intersection test with all layers and a viewport space subpath; ignoring artboards
pub fn intersect_polygon_no_artboards<'a>(&'a self, viewport_polygon: Subpath<PointId>, viewport: &ViewportMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + use<'a> {
/// Runs an intersection test with all layers and a viewport space polygon path; ignoring artboards
pub fn intersect_polygon_no_artboards<'a>(&'a self, viewport_polygon: BezPath, viewport: &ViewportMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + use<'a> {
self.intersect_polygon(viewport_polygon, viewport)
.filter(|layer| !self.network_interface.is_artboard(&layer.to_node(), &[]))
}
@@ -1870,26 +1868,24 @@ impl DocumentMessageHandler {
layer_left >= quad_left && layer_right <= quad_right && layer_top <= quad_top && layer_bottom >= quad_bottom
}
pub fn is_layer_fully_inside_polygon(&self, layer: &LayerNodeIdentifier, viewport: &ViewportMessageHandler, mut viewport_polygon: Subpath<PointId>) -> bool {
pub fn is_layer_fully_inside_polygon(&self, layer: &LayerNodeIdentifier, viewport: &ViewportMessageHandler, mut viewport_polygon: BezPath) -> bool {
let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz);
viewport_polygon.apply_transform(document_to_viewport.inverse());
viewport_polygon.apply_affine(Affine::new(document_to_viewport.inverse().to_cols_array()));
let layer_click_targets = self.network_interface.document_metadata().click_targets(*layer);
let layer_transform = self.network_interface.document_metadata().transform_to_document(*layer);
let viewport_polygon_bezpath = viewport_polygon.to_bezpath();
layer_click_targets.is_some_and(|targets| {
targets.iter().all(|target| match target.target_type() {
ClickTargetType::Path(path) => {
let mut path = path.clone();
path.apply_affine(Affine::new(layer_transform.to_cols_array()));
bezpath_is_inside_bezpath(&path, &viewport_polygon_bezpath, None, None)
bezpath_is_inside_bezpath(&path, &viewport_polygon, None, None)
}
ClickTargetType::FreePoint(point) => {
let mut point = *point;
point.apply_transform(layer_transform);
viewport_polygon.contains_point(point.position)
viewport_polygon.contains(dvec2_to_point(point.position))
}
})
})

View File

@@ -849,13 +849,13 @@ fn import_usvg_node_inner(
/// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer.
fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, path: &usvg::Path, layer: LayerNodeIdentifier, gradient_info: &SvgGradientInfo) {
let subpaths = convert_usvg_path(path);
let bezpath = convert_usvg_path(path);
// Skip creating a Transform node entirely when the SVG-native transform is identity.
let node_transform = usvg_transform(node.abs_transform());
let has_transform = node_transform != DAffine2::IDENTITY;
modify_inputs.insert_vector(subpaths, layer, has_transform, path.fill().is_some(), path.stroke().is_some());
modify_inputs.insert_vector(bezpath, layer, has_transform, path.fill().is_some(), path.stroke().is_some());
if has_transform && let Some(transform_node_id) = modify_inputs.existing_proto_node_id(graphene_std::transform_nodes::transform::IDENTIFIER, false) {
transform_utils::update_transform(modify_inputs.network_interface, &transform_node_id, node_transform);

View File

@@ -2,9 +2,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{Inp
use glam::{DAffine2, DVec2};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use graphene_std::subpath::Subpath;
use graphene_std::transform::Transform;
use graphene_std::vector::PointId;
/// Update the inputs of the transform node to match a new transform
pub fn update_transform(network_interface: &mut NodeNetworkInterface, node_id: &NodeId, transform: DAffine2) {
@@ -87,32 +85,6 @@ pub fn get_current_normalized_pivot(inputs: &[NodeInput]) -> DVec2 {
if let Some(&TaggedValue::DVec2(pivot)) = inputs[5].as_value() { pivot } else { DVec2::splat(0.5) }
}
/// Expand a bounds to avoid div zero errors
fn clamp_bounds(bounds_min: DVec2, mut bounds_max: DVec2) -> [DVec2; 2] {
let bounds_size = bounds_max - bounds_min;
if bounds_size.x < 1e-10 {
bounds_max.x = bounds_min.x + 1.;
}
if bounds_size.y < 1e-10 {
bounds_max.y = bounds_min.y + 1.;
}
[bounds_min, bounds_max]
}
/// Returns corners of all subpaths
fn subpath_bounds(subpaths: &[Subpath<PointId>]) -> [DVec2; 2] {
subpaths
.iter()
.filter_map(|subpath| subpath.bounding_box())
.reduce(|b1, b2| [b1[0].min(b2[0]), b1[1].max(b2[1])])
.unwrap_or_default()
}
/// Returns corners of all subpaths (but expanded to avoid division-by-zero errors)
pub fn nonzero_subpath_bounds(subpaths: &[Subpath<PointId>]) -> [DVec2; 2] {
let [bounds_min, bounds_max] = subpath_bounds(subpaths);
clamp_bounds(bounds_min, bounds_max)
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -16,11 +16,11 @@ use graph_craft::{ProtoNodeIdentifier, list};
use graphene_std::brush::brush_stroke::BrushStroke;
use graphene_std::raster::BlendMode;
use graphene_std::raster_types::Image;
use graphene_std::subpath::Subpath;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, PaintOrder, Stroke};
use graphene_std::vector::{Gradient, GradientRamp, PointId, Vector, VectorModification, VectorModificationType};
use graphene_std::vector::{Gradient, GradientRamp, Vector, VectorModification, VectorModificationType};
use graphene_std::{Artboard, Color, Graphic};
use kurbo::BezPath;
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
pub enum TransformIn {
@@ -146,9 +146,9 @@ impl<'a> ModifyInputsContext<'a> {
path_id
}
pub fn insert_vector(&mut self, subpaths: Vec<Subpath<PointId>>, layer: LayerNodeIdentifier, include_transform: bool, include_fill: bool, include_stroke: bool) {
pub fn insert_vector(&mut self, bezpath: BezPath, layer: LayerNodeIdentifier, include_transform: bool, include_fill: bool, include_stroke: bool) {
// Build a VectorModification that reproduces the geometry (same format the Pen tool uses)
let vector = Vector::from_subpaths(subpaths, true);
let vector = Vector::from_bezpath(bezpath);
let modification = Box::new(VectorModification::create_from_vector(&vector));
let shape = resolve_network_node_type("Path")

View File

@@ -6,8 +6,7 @@ pub use crate::messages::portfolio::document::utility_types::text_metrics::text_
use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
use crate::messages::tool::tool_messages::tool_prelude::DocumentMessageHandler;
use glam::{DAffine2, DVec2};
use graphene_std::subpath::BezierHandles;
use graphene_std::vector::misc::{ManipulatorPointId, point_to_dvec2, segment_to_handles};
use graphene_std::vector::misc::{BezierHandles, ManipulatorPointId, point_to_dvec2, segment_to_handles};
use graphene_std::vector::{PointId, SegmentId, Vector};
use kurbo::{Affine, ParamCurve, PathSeg};
use std::collections::HashMap;

View File

@@ -39,8 +39,8 @@ impl NodeNetworkInterface {
let mut targets = Vec::new();
let mut combined = BezPath::new();
for subpath in vector.stroke_bezier_paths() {
combined.extend(subpath.to_bezpath().elements().iter().copied());
for bezpath in vector.stroke_bezpath_iter() {
combined.extend(bezpath.elements().iter().copied());
}
if !combined.is_empty() {
targets.push(ClickTargetType::Path(combined));

View File

@@ -12,7 +12,7 @@ use crate::messages::tool::common_functionality::shapes::spiral_shape::calculate
use glam::DVec2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use graphene_std::subpath::{calculate_growth_factor, spiral_point};
use graphene_std::vector::algorithms::shapes::{calculate_growth_factor, spiral_point};
use graphene_std::vector::misc::SpiralType;
use std::collections::VecDeque;
use std::f64::consts::TAU;

View File

@@ -12,10 +12,9 @@ use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration
use crate::messages::tool::common_functionality::utility_functions::is_visible_point;
use crate::messages::tool::tool_messages::path_tool::{PathOverlayMode, PointSelectState};
use glam::{DAffine2, DVec2};
use graphene_std::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, segment_to_handles};
use graphene_std::vector::algorithms::shapes::polyline_bezpath;
use graphene_std::vector::misc::{BezierHandles, HandleId, ManipulatorPointId, PathSegPoints, bezpath_from_manipulator_groups, dvec2_to_point, pathseg_points, point_to_dvec2, segment_to_handles};
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;
@@ -750,9 +749,9 @@ impl ShapeState {
let mut selected_stack = Vec::new();
// Find all subpaths that have been clicked
for stroke in vector.stroke_bezier_paths() {
if stroke.contains_point(layer_mouse)
&& let Some(first) = stroke.manipulator_groups().first()
for (groups, closed) in vector.stroke_manipulator_groups() {
if bezpath_from_manipulator_groups(&groups, closed).contains(dvec2_to_point(layer_mouse))
&& let Some(first) = groups.first()
{
selected_stack.push(first.id);
}
@@ -2223,12 +2222,11 @@ impl ShapeState {
assert!(vector.point_domain.ids().contains(&end));
}
let polygon_subpath = if let SelectionShape::Lasso(polygon) = selection_shape {
let polygon_bezpath = if let SelectionShape::Lasso(polygon) = selection_shape {
if polygon.len() < 2 {
return (points_inside, segments_inside);
}
let polygon: Subpath<PointId> = Subpath::from_anchors(polygon.to_vec(), true);
Some(polygon)
Some(polyline_bezpath(polygon.iter().copied(), true))
} else {
None
};
@@ -2256,13 +2254,13 @@ impl ShapeState {
}
}
SelectionShape::Lasso(_) => {
let polygon = polygon_subpath.as_ref().expect("If `selection_shape` is a polygon then subpath is constructed beforehand.");
let polygon = polygon_bezpath.as_ref().expect("If `selection_shape` is a polygon then its path is constructed beforehand.");
// Sample 10 points on the bezier and check if all or some lie inside the polygon
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)),
SelectionMode::Enclosed => points.map(|p| transform.transform_point2(p)).all(|p| polygon.contains(dvec2_to_point(p))),
_ => points.map(|p| transform.transform_point2(p)).any(|p| polygon.contains(dvec2_to_point(p))),
}
}
};
@@ -2281,10 +2279,10 @@ impl ShapeState {
let select = match selection_shape {
SelectionShape::Box(rect) => rect.contains(dvec2_to_point(transformed_position)),
SelectionShape::Lasso(_) => polygon_subpath
SelectionShape::Lasso(_) => polygon_bezpath
.as_ref()
.expect("If `selection_shape` is a polygon then subpath is constructed beforehand.")
.contains_point(transformed_position),
.expect("If `selection_shape` is a polygon then its path is constructed beforehand.")
.contains(dvec2_to_point(transformed_position)),
};
if select && select_points {
@@ -2306,10 +2304,10 @@ impl ShapeState {
let select = match selection_shape {
SelectionShape::Box(rect) => rect.contains(dvec2_to_point(transformed_position)),
SelectionShape::Lasso(_) => polygon_subpath
SelectionShape::Lasso(_) => polygon_bezpath
.as_ref()
.expect("If `selection_shape` is a polygon then subpath is constructed beforehand.")
.contains_point(transformed_position),
.expect("If `selection_shape` is a polygon then its path is constructed beforehand.")
.contains(dvec2_to_point(transformed_position)),
};
if select && select_points {

View File

@@ -16,8 +16,7 @@ use crate::messages::tool::utility_types::*;
use glam::{DAffine2, DMat2, DVec2};
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use graphene_std::subpath::Subpath;
use graphene_std::vector::PointId;
use graphene_std::vector::algorithms::shapes::{arc_bezpath, regular_polygon_bezpath, star_polygon_bezpath};
use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::{ArcType, GridType, SpiralType, dvec2_to_point};
use kurbo::{BezPath, PathEl, Shape};
@@ -443,12 +442,7 @@ pub fn star_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMessa
let viewport = document.metadata().transform_to_viewport(layer);
let points = sides as u64;
let diameter: f64 = radius1 * 2.;
let inner_diameter = radius2 * 2.;
let targets: Vec<ClickTargetType> = vec![ClickTargetType::Path(
Subpath::<PointId>::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter).to_bezpath(),
)];
let targets: Vec<ClickTargetType> = vec![ClickTargetType::Path(star_polygon_bezpath(DVec2::ZERO, points, radius1, radius2))];
overlay_context.outline(targets.iter(), viewport, None);
}
@@ -463,9 +457,7 @@ pub fn polygon_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMe
let viewport = document.metadata().transform_to_viewport(layer);
let points = sides as u64;
let radius: f64 = radius * 2.;
let targets: Vec<ClickTargetType> = vec![ClickTargetType::Path(Subpath::<PointId>::new_regular_polygon(DVec2::splat(-radius), points, radius).to_bezpath())];
let targets: Vec<ClickTargetType> = vec![ClickTargetType::Path(regular_polygon_bezpath(DVec2::ZERO, points, radius))];
overlay_context.outline(targets.iter(), viewport, None);
}
@@ -478,8 +470,8 @@ pub fn arc_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMessag
return;
};
let arc = Subpath::<PointId>::new_arc(radius, start_angle / 360. * std::f64::consts::TAU, sweep_angle / 360. * std::f64::consts::TAU, arc_type);
let targets: Vec<ClickTargetType> = vec![ClickTargetType::Path(arc.to_bezpath())];
let arc = arc_bezpath(radius, start_angle / 360. * std::f64::consts::TAU, sweep_angle / 360. * std::f64::consts::TAU, arc_type);
let targets: Vec<ClickTargetType> = vec![ClickTargetType::Path(arc)];
let viewport = document.metadata().transform_to_viewport(layer);
overlay_context.outline(targets.iter(), viewport, None);

View File

@@ -14,7 +14,7 @@ use crate::messages::tool::tool_messages::tool_prelude::*;
use glam::DAffine2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use graphene_std::subpath::{calculate_growth_factor, spiral_point};
use graphene_std::vector::algorithms::shapes::{calculate_growth_factor, spiral_point};
use graphene_std::vector::misc::SpiralType;
use std::collections::VecDeque;

View File

@@ -6,13 +6,11 @@ use crate::messages::prelude::*;
use glam::{DAffine2, DVec2, FloatExt};
use graphene_std::math::math_ext::QuadExt;
use graphene_std::renderer::Quad;
use graphene_std::subpath::pathseg_points;
use graphene_std::subpath::{Identifier, ManipulatorGroup, Subpath};
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::dvec2_to_point;
use graphene_std::vector::misc::point_to_dvec2;
use graphene_std::vector::algorithms::shapes::{ellipse_bezpath, line_bezpath};
use graphene_std::vector::misc::{ManipulatorGroup, bezpath_from_manipulator_groups, dvec2_to_point, pathseg_points, point_to_dvec2};
use kurbo::{Affine, BezPath, ParamCurve, PathEl, PathSeg};
#[derive(Clone, Debug, Default)]
@@ -47,7 +45,7 @@ impl LayerSnapper {
self.paths_to_snap.push(SnapCandidatePath {
document_curve,
layer,
start: PointId::new(),
start: PointId::generate(),
target,
bounds: Some(bounds),
});
@@ -82,7 +80,7 @@ impl LayerSnapper {
self.paths_to_snap.push(SnapCandidatePath {
document_curve: Affine::new(transform.to_cols_array()) * curve,
layer,
start: PointId::new(),
start: PointId::generate(),
target: SnapTarget::Path(PathSnapTarget::AlongPath),
bounds: None,
});
@@ -95,10 +93,11 @@ impl LayerSnapper {
if let Some(vector) = document.network_interface.upstream_path_node_vector(layer) {
let path_aware_transform = document.metadata().transform_to_document_if_feeds(layer, &document.network_interface);
if path_aware_transform.is_finite() {
for subpath in vector.stroke_bezier_paths() {
for (start_index, curve) in subpath.iter().enumerate() {
let start = subpath.manipulator_groups()[start_index].id;
let end = subpath.manipulator_groups()[(start_index + 1) % subpath.len()].id;
for (groups, closed) in vector.stroke_manipulator_groups() {
let bezpath = bezpath_from_manipulator_groups(&groups, closed);
for (start_index, curve) in bezpath.segments().enumerate() {
let start = groups[start_index].id;
let end = groups[(start_index + 1) % groups.len()].id;
if snap_data.ignore_manipulator(layer, start) || snap_data.ignore_manipulator(layer, end) {
continue;
}
@@ -163,17 +162,17 @@ impl LayerSnapper {
let tolerance = snap_tolerance(document);
let constraint_path = if let SnapConstraint::Circle { center, radius } = constraint {
Subpath::new_ellipse(center - DVec2::splat(radius), center + DVec2::splat(radius))
ellipse_bezpath(center - DVec2::splat(radius), center + DVec2::splat(radius))
} else {
let constrained_point = constraint.projection(point.document_point);
let direction = constraint.direction().normalize_or_zero();
let start = constrained_point - tolerance * direction;
let end = constrained_point + tolerance * direction;
Subpath::<PointId>::new_line(start, end)
line_bezpath(start, end)
};
for path in &self.paths_to_snap {
for constraint_path in constraint_path.iter() {
for constraint_path in constraint_path.segments() {
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));
@@ -555,13 +554,14 @@ fn handle_not_under(to_document: DAffine2) -> impl Fn(&DVec2) -> bool {
move |&offset: &DVec2| to_document.transform_vector2(offset).length_squared() >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE
}
fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<PointId>, snap_data: &SnapData, points: &mut Vec<SnapCandidatePoint>, to_document: DAffine2) {
fn manipulator_groups_anchor_snap_points(layer: LayerNodeIdentifier, groups: &[ManipulatorGroup], closed: bool, snap_data: &SnapData, points: &mut Vec<SnapCandidatePoint>, to_document: DAffine2) {
let document = snap_data.document;
let bezpath = bezpath_from_manipulator_groups(groups, closed);
// Midpoints of linear segments
if document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::LineMidpoint)) {
for (index, curve) in subpath.iter().enumerate() {
if snap_data.ignore_manipulator(layer, subpath.manipulator_groups()[index].id) || snap_data.ignore_manipulator(layer, subpath.manipulator_groups()[(index + 1) % subpath.len()].id) {
for (index, curve) in bezpath.segments().enumerate() {
if snap_data.ignore_manipulator(layer, groups[index].id) || snap_data.ignore_manipulator(layer, groups[(index + 1) % groups.len()].id) {
continue;
}
if points.len() >= crate::consts::MAX_LAYER_SNAP_POINTS {
@@ -583,7 +583,7 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<Poin
}
// Anchors
for (index, manipulators) in subpath.manipulator_groups().iter().enumerate() {
for (index, manipulators) in groups.iter().enumerate() {
if snap_data.ignore_manipulator(layer, manipulators.id) {
continue;
}
@@ -592,7 +592,7 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<Poin
return;
}
let colinear = are_manipulator_handles_colinear(manipulators, to_document, subpath, index);
let colinear = are_manipulator_handles_colinear(manipulators, to_document, closed, index, groups.len());
// Colinear handles
if colinear && document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AnchorPointWithColinearHandles)) {
@@ -705,11 +705,11 @@ fn bezpath_anchor_snap_points(layer: LayerNodeIdentifier, bezpath: &BezPath, sna
}
}
pub fn are_manipulator_handles_colinear(manipulators: &ManipulatorGroup<PointId>, to_document: DAffine2, subpath: &Subpath<PointId>, index: usize) -> bool {
pub fn are_manipulator_handles_colinear(manipulators: &ManipulatorGroup, to_document: DAffine2, closed: bool, index: usize, count: 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));
let anchor_is_endpoint = !subpath.closed() && (index == 0 || index == subpath.len() - 1);
let anchor_is_endpoint = !closed && (index == 0 || index == count - 1);
// Unless this is an endpoint, check if both handles are colinear (within an angular epsilon)
!anchor_is_endpoint && handle_in.is_some_and(|handle_in| handle_out.is_some_and(|handle_out| handle_in.angle_to(handle_out) < 1e-5))
@@ -743,8 +743,8 @@ pub fn get_layer_snap_points(layer: LayerNodeIdentifier, snap_data: &SnapData, p
// matching what the Path tool's overlay shows
if let Some(vector) = document.network_interface.upstream_path_node_vector(layer) {
let to_document = document.metadata().transform_to_document_if_feeds(layer, &document.network_interface);
for subpath in vector.stroke_bezier_paths() {
subpath_anchor_snap_points(layer, &subpath, snap_data, points, to_document);
for (groups, closed) in vector.stroke_manipulator_groups() {
manipulator_groups_anchor_snap_points(layer, &groups, closed, snap_data, points, to_document);
}
if document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AnchorPointWithFreeHandles)) {

View File

@@ -25,11 +25,11 @@ use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandi
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, find_two_param_best_approximate, make_path_editable_is_allowed};
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::pathseg_points;
use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point, point_to_dvec2, segment_to_handles};
use graphene_std::vector::{HandleExt, NoHashBuilder, PointId, SegmentId, Vector, VectorModificationType};
use kurbo::{DEFAULT_ACCURACY, ParamCurve, ParamCurveNearest, PathSeg, Rect};

View File

@@ -18,7 +18,7 @@ use crate::messages::tool::common_functionality::stroke_options::{StrokeOptionsU
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, closest_point, should_extend};
use graph_craft::document::NodeId;
use graphene_std::Color;
use graphene_std::subpath::pathseg_points;
use graphene_std::vector::misc::pathseg_points;
use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point};
use graphene_std::vector::style::FillChoice;
use graphene_std::vector::{NoHashBuilder, PointId, SegmentId, StrokeId, Vector, VectorModificationType};
@@ -1866,8 +1866,8 @@ impl Fsm for PenToolFsmState {
.find(|(id, _, _, _)| id == &edge.id)
.map(|(_, start, end, bezier)| if start == edge.start { (bezier, start, end) } else { (bezier.reversed(), end, start) })
});
if let Some(subpath) = vector.subpath_from_segments_ignore_discontinuities(segments) {
fill_region.extend(subpath.to_bezpath().elements().iter().copied());
if let Some(bezpath) = vector.bezpath_from_segments_ignore_discontinuities(segments) {
fill_region.extend(bezpath.elements().iter().copied());
}
}

View File

@@ -28,8 +28,8 @@ use graph_craft::document::NodeId;
use graphene_std::Color;
use graphene_std::renderer::Quad;
use graphene_std::renderer::Rect;
use graphene_std::subpath::Subpath;
use graphene_std::transform::ReferencePoint;
use graphene_std::vector::algorithms::shapes::polyline_bezpath;
use graphene_std::vector::misc::BooleanOperation;
use graphene_std::vector::style::FillChoice;
use std::fmt;
@@ -575,7 +575,7 @@ impl SelectToolData {
if self.lasso_polygon.len() < 2 {
return Vec::new();
}
let polygon = Subpath::from_anchors(self.lasso_polygon.clone(), true);
let polygon = polyline_bezpath(self.lasso_polygon.iter().copied(), true);
document.intersect_polygon_no_artboards(polygon, viewport).collect()
}
@@ -583,7 +583,7 @@ impl SelectToolData {
if self.lasso_polygon.len() < 2 {
return false;
}
let polygon = Subpath::from_anchors(self.lasso_polygon.clone(), true);
let polygon = polyline_bezpath(self.lasso_polygon.iter().copied(), true);
document.is_layer_fully_inside_polygon(layer, viewport, polygon)
}