mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-28 08:08:13 +08:00
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:
@@ -12,8 +12,7 @@ use graph_craft::application_io::resource::{DataSource, ResourceHash};
|
|||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
use graphene_std::raster::Image;
|
use graphene_std::raster::Image;
|
||||||
use graphene_std::subpath::BezierHandles;
|
use graphene_std::vector::misc::{BezierHandles, HandleId, point_to_dvec2, segment_to_handles};
|
||||||
use graphene_std::vector::misc::{HandleId, point_to_dvec2, segment_to_handles};
|
|
||||||
use graphene_std::vector::{PointId, SegmentId, VectorModificationType};
|
use graphene_std::vector::{PointId, SegmentId, VectorModificationType};
|
||||||
use graphite_proc_macros::{ExtractField, message_handler_data};
|
use graphite_proc_macros::{ExtractField, message_handler_data};
|
||||||
use kurbo::ParamCurve;
|
use kurbo::ParamCurve;
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ use graphene_std::raster::{
|
|||||||
CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, SelectiveColorChoice,
|
CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, SelectiveColorChoice,
|
||||||
};
|
};
|
||||||
use graphene_std::raster_types::{CPU, GPU, Raster};
|
use graphene_std::raster_types::{CPU, GPU, Raster};
|
||||||
use graphene_std::subpath::BezierHandles;
|
|
||||||
use graphene_std::text::TextAlign;
|
use graphene_std::text::TextAlign;
|
||||||
use graphene_std::text_nodes::StringCapitalization;
|
use graphene_std::text_nodes::StringCapitalization;
|
||||||
use graphene_std::transform::{ReferencePoint, ScaleType};
|
use graphene_std::transform::{ReferencePoint, ScaleType};
|
||||||
use graphene_std::vector::misc::{
|
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::{
|
use graphene_std::vector::style::{
|
||||||
DashPattern, FillChoice, GradientForm, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, StrokeAlign, StrokeCap, StrokeJoin,
|
DashPattern, FillChoice, GradientForm, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, StrokeAlign, StrokeCap, StrokeJoin,
|
||||||
|
|||||||
@@ -41,13 +41,12 @@ use graphene_std::Cover;
|
|||||||
use graphene_std::math::quad::Quad;
|
use graphene_std::math::quad::Quad;
|
||||||
use graphene_std::path_bool_nodes::boolean_intersect;
|
use graphene_std::path_bool_nodes::boolean_intersect;
|
||||||
use graphene_std::raster::BlendMode;
|
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::algorithms::bezpath_algorithms::bezpath_is_inside_bezpath;
|
||||||
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
|
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::misc::dvec2_to_point;
|
||||||
use graphene_std::vector::style::RenderMode;
|
use graphene_std::vector::style::RenderMode;
|
||||||
use graphene_std::vector::{PointId, graphic_types};
|
use kurbo::{Affine, BezPath, Line, PathSeg, Shape};
|
||||||
use kurbo::{Affine, BezPath, Line, PathSeg};
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
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(), &[]))
|
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
|
/// Runs an intersection test with all layers and a viewport space polygon path
|
||||||
pub fn intersect_polygon<'a>(&'a self, mut viewport_polygon: Subpath<PointId>, viewport: &ViewportMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + use<'a> {
|
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);
|
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(viewport_polygon))
|
||||||
ClickXRayIter::new(&self.network_interface, XRayTarget::Path(polygon))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs an intersection test with all layers and a viewport space subpath; ignoring artboards
|
/// 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: Subpath<PointId>, viewport: &ViewportMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + use<'a> {
|
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)
|
self.intersect_polygon(viewport_polygon, viewport)
|
||||||
.filter(|layer| !self.network_interface.is_artboard(&layer.to_node(), &[]))
|
.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
|
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);
|
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_click_targets = self.network_interface.document_metadata().click_targets(*layer);
|
||||||
let layer_transform = self.network_interface.document_metadata().transform_to_document(*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| {
|
layer_click_targets.is_some_and(|targets| {
|
||||||
targets.iter().all(|target| match target.target_type() {
|
targets.iter().all(|target| match target.target_type() {
|
||||||
ClickTargetType::Path(path) => {
|
ClickTargetType::Path(path) => {
|
||||||
let mut path = path.clone();
|
let mut path = path.clone();
|
||||||
path.apply_affine(Affine::new(layer_transform.to_cols_array()));
|
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) => {
|
ClickTargetType::FreePoint(point) => {
|
||||||
let mut point = *point;
|
let mut point = *point;
|
||||||
point.apply_transform(layer_transform);
|
point.apply_transform(layer_transform);
|
||||||
viewport_polygon.contains_point(point.position)
|
viewport_polygon.contains(dvec2_to_point(point.position))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+2
-2
@@ -849,13 +849,13 @@ fn import_usvg_node_inner(
|
|||||||
|
|
||||||
/// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer.
|
/// 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) {
|
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.
|
// Skip creating a Transform node entirely when the SVG-native transform is identity.
|
||||||
let node_transform = usvg_transform(node.abs_transform());
|
let node_transform = usvg_transform(node.abs_transform());
|
||||||
let has_transform = node_transform != DAffine2::IDENTITY;
|
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) {
|
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);
|
transform_utils::update_transform(modify_inputs.network_interface, &transform_node_id, node_transform);
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{Inp
|
|||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{NodeId, NodeInput};
|
use graph_craft::document::{NodeId, NodeInput};
|
||||||
use graphene_std::subpath::Subpath;
|
|
||||||
use graphene_std::transform::Transform;
|
use graphene_std::transform::Transform;
|
||||||
use graphene_std::vector::PointId;
|
|
||||||
|
|
||||||
/// Update the inputs of the transform node to match a new transform
|
/// 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) {
|
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) }
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ use graph_craft::{ProtoNodeIdentifier, list};
|
|||||||
use graphene_std::brush::brush_stroke::BrushStroke;
|
use graphene_std::brush::brush_stroke::BrushStroke;
|
||||||
use graphene_std::raster::BlendMode;
|
use graphene_std::raster::BlendMode;
|
||||||
use graphene_std::raster_types::Image;
|
use graphene_std::raster_types::Image;
|
||||||
use graphene_std::subpath::Subpath;
|
|
||||||
use graphene_std::text::{Font, TypesettingConfig};
|
use graphene_std::text::{Font, TypesettingConfig};
|
||||||
use graphene_std::vector::style::{GradientForm, GradientHueDirection, GradientInterpolation, GradientSettings, GradientSpace, GradientSpread, PaintOrder, Stroke};
|
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 graphene_std::{Artboard, Color, Graphic};
|
||||||
|
use kurbo::BezPath;
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum TransformIn {
|
pub enum TransformIn {
|
||||||
@@ -146,9 +146,9 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
path_id
|
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)
|
// 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 modification = Box::new(VectorModification::create_from_vector(&vector));
|
||||||
|
|
||||||
let shape = resolve_network_node_type("Path")
|
let shape = resolve_network_node_type("Path")
|
||||||
|
|||||||
@@ -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::common_functionality::shape_editor::{SelectedLayerState, ShapeState};
|
||||||
use crate::messages::tool::tool_messages::tool_prelude::DocumentMessageHandler;
|
use crate::messages::tool::tool_messages::tool_prelude::DocumentMessageHandler;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphene_std::subpath::BezierHandles;
|
use graphene_std::vector::misc::{BezierHandles, ManipulatorPointId, point_to_dvec2, segment_to_handles};
|
||||||
use graphene_std::vector::misc::{ManipulatorPointId, point_to_dvec2, segment_to_handles};
|
|
||||||
use graphene_std::vector::{PointId, SegmentId, Vector};
|
use graphene_std::vector::{PointId, SegmentId, Vector};
|
||||||
use kurbo::{Affine, ParamCurve, PathSeg};
|
use kurbo::{Affine, ParamCurve, PathSeg};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ impl NodeNetworkInterface {
|
|||||||
|
|
||||||
let mut targets = Vec::new();
|
let mut targets = Vec::new();
|
||||||
let mut combined = BezPath::new();
|
let mut combined = BezPath::new();
|
||||||
for subpath in vector.stroke_bezier_paths() {
|
for bezpath in vector.stroke_bezpath_iter() {
|
||||||
combined.extend(subpath.to_bezpath().elements().iter().copied());
|
combined.extend(bezpath.elements().iter().copied());
|
||||||
}
|
}
|
||||||
if !combined.is_empty() {
|
if !combined.is_empty() {
|
||||||
targets.push(ClickTargetType::Path(combined));
|
targets.push(ClickTargetType::Path(combined));
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ use crate::messages::tool::common_functionality::shapes::spiral_shape::calculate
|
|||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use graph_craft::document::NodeInput;
|
use graph_craft::document::NodeInput;
|
||||||
use graph_craft::document::value::TaggedValue;
|
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 graphene_std::vector::misc::SpiralType;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::f64::consts::TAU;
|
use std::f64::consts::TAU;
|
||||||
|
|||||||
@@ -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::common_functionality::utility_functions::is_visible_point;
|
||||||
use crate::messages::tool::tool_messages::path_tool::{PathOverlayMode, PointSelectState};
|
use crate::messages::tool::tool_messages::path_tool::{PathOverlayMode, PointSelectState};
|
||||||
use glam::{DAffine2, DVec2};
|
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::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 graphene_std::vector::{HandleExt, PointId, SegmentId, Vector, VectorModificationType};
|
||||||
use kurbo::{Affine, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveNearest, PathSeg, Rect, Shape};
|
use kurbo::{Affine, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveNearest, PathSeg, Rect, Shape};
|
||||||
use std::f64::consts::TAU;
|
use std::f64::consts::TAU;
|
||||||
@@ -750,9 +749,9 @@ impl ShapeState {
|
|||||||
|
|
||||||
let mut selected_stack = Vec::new();
|
let mut selected_stack = Vec::new();
|
||||||
// Find all subpaths that have been clicked
|
// Find all subpaths that have been clicked
|
||||||
for stroke in vector.stroke_bezier_paths() {
|
for (groups, closed) in vector.stroke_manipulator_groups() {
|
||||||
if stroke.contains_point(layer_mouse)
|
if bezpath_from_manipulator_groups(&groups, closed).contains(dvec2_to_point(layer_mouse))
|
||||||
&& let Some(first) = stroke.manipulator_groups().first()
|
&& let Some(first) = groups.first()
|
||||||
{
|
{
|
||||||
selected_stack.push(first.id);
|
selected_stack.push(first.id);
|
||||||
}
|
}
|
||||||
@@ -2223,12 +2222,11 @@ impl ShapeState {
|
|||||||
assert!(vector.point_domain.ids().contains(&end));
|
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 {
|
if polygon.len() < 2 {
|
||||||
return (points_inside, segments_inside);
|
return (points_inside, segments_inside);
|
||||||
}
|
}
|
||||||
let polygon: Subpath<PointId> = Subpath::from_anchors(polygon.to_vec(), true);
|
Some(polyline_bezpath(polygon.iter().copied(), true))
|
||||||
Some(polygon)
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -2256,13 +2254,13 @@ impl ShapeState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SelectionShape::Lasso(_) => {
|
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
|
// 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);
|
let points = pathseg_compute_lookup_table(segment, Some(10), false);
|
||||||
match selection_mode {
|
match selection_mode {
|
||||||
SelectionMode::Enclosed => points.map(|p| transform.transform_point2(p)).all(|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_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 {
|
let select = match selection_shape {
|
||||||
SelectionShape::Box(rect) => rect.contains(dvec2_to_point(transformed_position)),
|
SelectionShape::Box(rect) => rect.contains(dvec2_to_point(transformed_position)),
|
||||||
SelectionShape::Lasso(_) => polygon_subpath
|
SelectionShape::Lasso(_) => polygon_bezpath
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("If `selection_shape` is a polygon then subpath is constructed beforehand.")
|
.expect("If `selection_shape` is a polygon then its path is constructed beforehand.")
|
||||||
.contains_point(transformed_position),
|
.contains(dvec2_to_point(transformed_position)),
|
||||||
};
|
};
|
||||||
|
|
||||||
if select && select_points {
|
if select && select_points {
|
||||||
@@ -2306,10 +2304,10 @@ impl ShapeState {
|
|||||||
|
|
||||||
let select = match selection_shape {
|
let select = match selection_shape {
|
||||||
SelectionShape::Box(rect) => rect.contains(dvec2_to_point(transformed_position)),
|
SelectionShape::Box(rect) => rect.contains(dvec2_to_point(transformed_position)),
|
||||||
SelectionShape::Lasso(_) => polygon_subpath
|
SelectionShape::Lasso(_) => polygon_bezpath
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("If `selection_shape` is a polygon then subpath is constructed beforehand.")
|
.expect("If `selection_shape` is a polygon then its path is constructed beforehand.")
|
||||||
.contains_point(transformed_position),
|
.contains(dvec2_to_point(transformed_position)),
|
||||||
};
|
};
|
||||||
|
|
||||||
if select && select_points {
|
if select && select_points {
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ use crate::messages::tool::utility_types::*;
|
|||||||
use glam::{DAffine2, DMat2, DVec2};
|
use glam::{DAffine2, DMat2, DVec2};
|
||||||
use graph_craft::document::NodeInput;
|
use graph_craft::document::NodeInput;
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graphene_std::subpath::Subpath;
|
use graphene_std::vector::algorithms::shapes::{arc_bezpath, regular_polygon_bezpath, star_polygon_bezpath};
|
||||||
use graphene_std::vector::PointId;
|
|
||||||
use graphene_std::vector::click_target::ClickTargetType;
|
use graphene_std::vector::click_target::ClickTargetType;
|
||||||
use graphene_std::vector::misc::{ArcType, GridType, SpiralType, dvec2_to_point};
|
use graphene_std::vector::misc::{ArcType, GridType, SpiralType, dvec2_to_point};
|
||||||
use kurbo::{BezPath, PathEl, Shape};
|
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 viewport = document.metadata().transform_to_viewport(layer);
|
||||||
|
|
||||||
let points = sides as u64;
|
let points = sides as u64;
|
||||||
let diameter: f64 = radius1 * 2.;
|
let targets: Vec<ClickTargetType> = vec![ClickTargetType::Path(star_polygon_bezpath(DVec2::ZERO, points, radius1, radius2))];
|
||||||
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(),
|
|
||||||
)];
|
|
||||||
|
|
||||||
overlay_context.outline(targets.iter(), viewport, None);
|
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 viewport = document.metadata().transform_to_viewport(layer);
|
||||||
|
|
||||||
let points = sides as u64;
|
let points = sides as u64;
|
||||||
let radius: f64 = radius * 2.;
|
let targets: Vec<ClickTargetType> = vec![ClickTargetType::Path(regular_polygon_bezpath(DVec2::ZERO, points, radius))];
|
||||||
|
|
||||||
let targets: Vec<ClickTargetType> = vec![ClickTargetType::Path(Subpath::<PointId>::new_regular_polygon(DVec2::splat(-radius), points, radius).to_bezpath())];
|
|
||||||
|
|
||||||
overlay_context.outline(targets.iter(), viewport, None);
|
overlay_context.outline(targets.iter(), viewport, None);
|
||||||
}
|
}
|
||||||
@@ -478,8 +470,8 @@ pub fn arc_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMessag
|
|||||||
return;
|
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 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.to_bezpath())];
|
let targets: Vec<ClickTargetType> = vec![ClickTargetType::Path(arc)];
|
||||||
let viewport = document.metadata().transform_to_viewport(layer);
|
let viewport = document.metadata().transform_to_viewport(layer);
|
||||||
|
|
||||||
overlay_context.outline(targets.iter(), viewport, None);
|
overlay_context.outline(targets.iter(), viewport, None);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use crate::messages::tool::tool_messages::tool_prelude::*;
|
|||||||
use glam::DAffine2;
|
use glam::DAffine2;
|
||||||
use graph_craft::document::NodeInput;
|
use graph_craft::document::NodeInput;
|
||||||
use graph_craft::document::value::TaggedValue;
|
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 graphene_std::vector::misc::SpiralType;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ use crate::messages::prelude::*;
|
|||||||
use glam::{DAffine2, DVec2, FloatExt};
|
use glam::{DAffine2, DVec2, FloatExt};
|
||||||
use graphene_std::math::math_ext::QuadExt;
|
use graphene_std::math::math_ext::QuadExt;
|
||||||
use graphene_std::renderer::Quad;
|
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::PointId;
|
||||||
use graphene_std::vector::algorithms::bezpath_algorithms::{pathseg_normals_to_point, pathseg_tangents_to_point};
|
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::algorithms::intersection::filtered_segment_intersections;
|
||||||
use graphene_std::vector::misc::dvec2_to_point;
|
use graphene_std::vector::algorithms::shapes::{ellipse_bezpath, line_bezpath};
|
||||||
use graphene_std::vector::misc::point_to_dvec2;
|
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};
|
use kurbo::{Affine, BezPath, ParamCurve, PathEl, PathSeg};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
@@ -47,7 +45,7 @@ impl LayerSnapper {
|
|||||||
self.paths_to_snap.push(SnapCandidatePath {
|
self.paths_to_snap.push(SnapCandidatePath {
|
||||||
document_curve,
|
document_curve,
|
||||||
layer,
|
layer,
|
||||||
start: PointId::new(),
|
start: PointId::generate(),
|
||||||
target,
|
target,
|
||||||
bounds: Some(bounds),
|
bounds: Some(bounds),
|
||||||
});
|
});
|
||||||
@@ -82,7 +80,7 @@ impl LayerSnapper {
|
|||||||
self.paths_to_snap.push(SnapCandidatePath {
|
self.paths_to_snap.push(SnapCandidatePath {
|
||||||
document_curve: Affine::new(transform.to_cols_array()) * curve,
|
document_curve: Affine::new(transform.to_cols_array()) * curve,
|
||||||
layer,
|
layer,
|
||||||
start: PointId::new(),
|
start: PointId::generate(),
|
||||||
target: SnapTarget::Path(PathSnapTarget::AlongPath),
|
target: SnapTarget::Path(PathSnapTarget::AlongPath),
|
||||||
bounds: None,
|
bounds: None,
|
||||||
});
|
});
|
||||||
@@ -95,10 +93,11 @@ impl LayerSnapper {
|
|||||||
if let Some(vector) = document.network_interface.upstream_path_node_vector(layer) {
|
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);
|
let path_aware_transform = document.metadata().transform_to_document_if_feeds(layer, &document.network_interface);
|
||||||
if path_aware_transform.is_finite() {
|
if path_aware_transform.is_finite() {
|
||||||
for subpath in vector.stroke_bezier_paths() {
|
for (groups, closed) in vector.stroke_manipulator_groups() {
|
||||||
for (start_index, curve) in subpath.iter().enumerate() {
|
let bezpath = bezpath_from_manipulator_groups(&groups, closed);
|
||||||
let start = subpath.manipulator_groups()[start_index].id;
|
for (start_index, curve) in bezpath.segments().enumerate() {
|
||||||
let end = subpath.manipulator_groups()[(start_index + 1) % subpath.len()].id;
|
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) {
|
if snap_data.ignore_manipulator(layer, start) || snap_data.ignore_manipulator(layer, end) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -163,17 +162,17 @@ impl LayerSnapper {
|
|||||||
|
|
||||||
let tolerance = snap_tolerance(document);
|
let tolerance = snap_tolerance(document);
|
||||||
let constraint_path = if let SnapConstraint::Circle { center, radius } = constraint {
|
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 {
|
} else {
|
||||||
let constrained_point = constraint.projection(point.document_point);
|
let constrained_point = constraint.projection(point.document_point);
|
||||||
let direction = constraint.direction().normalize_or_zero();
|
let direction = constraint.direction().normalize_or_zero();
|
||||||
let start = constrained_point - tolerance * direction;
|
let start = constrained_point - tolerance * direction;
|
||||||
let end = 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 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) {
|
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 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
|
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 document = snap_data.document;
|
||||||
|
let bezpath = bezpath_from_manipulator_groups(groups, closed);
|
||||||
|
|
||||||
// Midpoints of linear segments
|
// Midpoints of linear segments
|
||||||
if document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::LineMidpoint)) {
|
if document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::LineMidpoint)) {
|
||||||
for (index, curve) in subpath.iter().enumerate() {
|
for (index, curve) in bezpath.segments().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) {
|
if snap_data.ignore_manipulator(layer, groups[index].id) || snap_data.ignore_manipulator(layer, groups[(index + 1) % groups.len()].id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if points.len() >= crate::consts::MAX_LAYER_SNAP_POINTS {
|
if points.len() >= crate::consts::MAX_LAYER_SNAP_POINTS {
|
||||||
@@ -583,7 +583,7 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<Poin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Anchors
|
// 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) {
|
if snap_data.ignore_manipulator(layer, manipulators.id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -592,7 +592,7 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<Poin
|
|||||||
return;
|
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
|
// Colinear handles
|
||||||
if colinear && document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AnchorPointWithColinearHandles)) {
|
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 anchor = manipulators.anchor;
|
||||||
let handle_in = manipulators.in_handle.map(|handle| anchor - handle).filter(handle_not_under(to_document));
|
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 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)
|
// 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))
|
!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
|
// matching what the Path tool's overlay shows
|
||||||
if let Some(vector) = document.network_interface.upstream_path_node_vector(layer) {
|
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);
|
let to_document = document.metadata().transform_to_document_if_feeds(layer, &document.network_interface);
|
||||||
for subpath in vector.stroke_bezier_paths() {
|
for (groups, closed) in vector.stroke_manipulator_groups() {
|
||||||
subpath_anchor_snap_points(layer, &subpath, snap_data, points, to_document);
|
manipulator_groups_anchor_snap_points(layer, &groups, closed, snap_data, points, to_document);
|
||||||
}
|
}
|
||||||
|
|
||||||
if document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AnchorPointWithFreeHandles)) {
|
if document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AnchorPointWithFreeHandles)) {
|
||||||
|
|||||||
@@ -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 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::Color;
|
||||||
use graphene_std::renderer::Quad;
|
use graphene_std::renderer::Quad;
|
||||||
use graphene_std::subpath::pathseg_points;
|
|
||||||
use graphene_std::transform::ReferencePoint;
|
use graphene_std::transform::ReferencePoint;
|
||||||
use graphene_std::uuid::NodeId;
|
use graphene_std::uuid::NodeId;
|
||||||
use graphene_std::vector::algorithms::util::pathseg_tangent;
|
use graphene_std::vector::algorithms::util::pathseg_tangent;
|
||||||
use graphene_std::vector::click_target::ClickTargetType;
|
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::misc::{HandleId, ManipulatorPointId, dvec2_to_point, point_to_dvec2, segment_to_handles};
|
||||||
use graphene_std::vector::{HandleExt, NoHashBuilder, PointId, SegmentId, Vector, VectorModificationType};
|
use graphene_std::vector::{HandleExt, NoHashBuilder, PointId, SegmentId, Vector, VectorModificationType};
|
||||||
use kurbo::{DEFAULT_ACCURACY, ParamCurve, ParamCurveNearest, PathSeg, Rect};
|
use kurbo::{DEFAULT_ACCURACY, ParamCurve, ParamCurveNearest, PathSeg, Rect};
|
||||||
|
|||||||
@@ -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 crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, closest_point, should_extend};
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
use graphene_std::Color;
|
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::misc::{HandleId, ManipulatorPointId, dvec2_to_point};
|
||||||
use graphene_std::vector::style::FillChoice;
|
use graphene_std::vector::style::FillChoice;
|
||||||
use graphene_std::vector::{NoHashBuilder, PointId, SegmentId, StrokeId, Vector, VectorModificationType};
|
use graphene_std::vector::{NoHashBuilder, PointId, SegmentId, StrokeId, Vector, VectorModificationType};
|
||||||
@@ -1866,8 +1866,8 @@ impl Fsm for PenToolFsmState {
|
|||||||
.find(|(id, _, _, _)| id == &edge.id)
|
.find(|(id, _, _, _)| id == &edge.id)
|
||||||
.map(|(_, start, end, bezier)| if start == edge.start { (bezier, start, end) } else { (bezier.reversed(), end, start) })
|
.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) {
|
if let Some(bezpath) = vector.bezpath_from_segments_ignore_discontinuities(segments) {
|
||||||
fill_region.extend(subpath.to_bezpath().elements().iter().copied());
|
fill_region.extend(bezpath.elements().iter().copied());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ use graph_craft::document::NodeId;
|
|||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
use graphene_std::renderer::Quad;
|
use graphene_std::renderer::Quad;
|
||||||
use graphene_std::renderer::Rect;
|
use graphene_std::renderer::Rect;
|
||||||
use graphene_std::subpath::Subpath;
|
|
||||||
use graphene_std::transform::ReferencePoint;
|
use graphene_std::transform::ReferencePoint;
|
||||||
|
use graphene_std::vector::algorithms::shapes::polyline_bezpath;
|
||||||
use graphene_std::vector::misc::BooleanOperation;
|
use graphene_std::vector::misc::BooleanOperation;
|
||||||
use graphene_std::vector::style::FillChoice;
|
use graphene_std::vector::style::FillChoice;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@@ -575,7 +575,7 @@ impl SelectToolData {
|
|||||||
if self.lasso_polygon.len() < 2 {
|
if self.lasso_polygon.len() < 2 {
|
||||||
return Vec::new();
|
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()
|
document.intersect_polygon_no_artboards(polygon, viewport).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,7 +583,7 @@ impl SelectToolData {
|
|||||||
if self.lasso_polygon.len() < 2 {
|
if self.lasso_polygon.len() < 2 {
|
||||||
return false;
|
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)
|
document.is_layer_fully_inside_polygon(layer, viewport, polygon)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,35 @@
|
|||||||
use glam::DVec2;
|
use kurbo::{BezPath, Point};
|
||||||
use vector_types::subpath::{ManipulatorGroup, Subpath};
|
|
||||||
use vector_types::vector::PointId;
|
|
||||||
|
|
||||||
pub fn convert_usvg_path(path: &usvg::Path) -> Vec<Subpath<PointId>> {
|
pub fn convert_usvg_path(path: &usvg::Path) -> BezPath {
|
||||||
let mut subpaths = Vec::new();
|
let mut bezpath = BezPath::new();
|
||||||
let mut manipulators_list = Vec::new();
|
|
||||||
|
|
||||||
let mut points = path.data().points().iter();
|
let mut points = path.data().points().iter();
|
||||||
let to_vec = |p: &usvg::tiny_skia_path::Point| DVec2::new(p.x as f64, p.y as f64);
|
let to_point = |p: &usvg::tiny_skia_path::Point| Point::new(p.x as f64, p.y as f64);
|
||||||
|
|
||||||
for verb in path.data().verbs() {
|
for verb in path.data().verbs() {
|
||||||
match verb {
|
match verb {
|
||||||
usvg::tiny_skia_path::PathVerb::Move => {
|
usvg::tiny_skia_path::PathVerb::Move => {
|
||||||
subpaths.push(Subpath::new(std::mem::take(&mut manipulators_list), false));
|
let Some(start) = points.next().map(to_point) else { continue };
|
||||||
let Some(start) = points.next().map(to_vec) else { continue };
|
bezpath.move_to(start);
|
||||||
manipulators_list.push(ManipulatorGroup::new(start, Some(start), Some(start)));
|
|
||||||
}
|
}
|
||||||
usvg::tiny_skia_path::PathVerb::Line => {
|
usvg::tiny_skia_path::PathVerb::Line => {
|
||||||
let Some(end) = points.next().map(to_vec) else { continue };
|
let Some(end) = points.next().map(to_point) else { continue };
|
||||||
manipulators_list.push(ManipulatorGroup::new(end, Some(end), Some(end)));
|
bezpath.line_to(end);
|
||||||
}
|
}
|
||||||
usvg::tiny_skia_path::PathVerb::Quad => {
|
usvg::tiny_skia_path::PathVerb::Quad => {
|
||||||
let Some(handle) = points.next().map(to_vec) else { continue };
|
let Some(handle) = points.next().map(to_point) else { continue };
|
||||||
let Some(end) = points.next().map(to_vec) else { continue };
|
let Some(end) = points.next().map(to_point) else { continue };
|
||||||
if let Some(last) = manipulators_list.last_mut() {
|
bezpath.quad_to(handle, end);
|
||||||
last.out_handle = Some(last.anchor + (2. / 3.) * (handle - last.anchor));
|
|
||||||
}
|
|
||||||
manipulators_list.push(ManipulatorGroup::new(end, Some(end + (2. / 3.) * (handle - end)), Some(end)));
|
|
||||||
}
|
}
|
||||||
usvg::tiny_skia_path::PathVerb::Cubic => {
|
usvg::tiny_skia_path::PathVerb::Cubic => {
|
||||||
let Some(first_handle) = points.next().map(to_vec) else { continue };
|
let Some(first_handle) = points.next().map(to_point) else { continue };
|
||||||
let Some(second_handle) = points.next().map(to_vec) else { continue };
|
let Some(second_handle) = points.next().map(to_point) else { continue };
|
||||||
let Some(end) = points.next().map(to_vec) else { continue };
|
let Some(end) = points.next().map(to_point) else { continue };
|
||||||
if let Some(last) = manipulators_list.last_mut() {
|
bezpath.curve_to(first_handle, second_handle, end);
|
||||||
last.out_handle = Some(first_handle);
|
|
||||||
}
|
|
||||||
manipulators_list.push(ManipulatorGroup::new(end, Some(second_handle), Some(end)));
|
|
||||||
}
|
|
||||||
usvg::tiny_skia_path::PathVerb::Close => {
|
|
||||||
subpaths.push(Subpath::new(std::mem::take(&mut manipulators_list), true));
|
|
||||||
}
|
}
|
||||||
|
usvg::tiny_skia_path::PathVerb::Close => bezpath.close_path(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
subpaths.push(Subpath::new(manipulators_list, false));
|
|
||||||
subpaths
|
bezpath
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ use graphene_hash::CacheHashWrapper;
|
|||||||
use graphene_resource::Resource;
|
use graphene_resource::Resource;
|
||||||
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture};
|
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture};
|
||||||
use graphic_types::vector_types::gradient::{Gradient, GradientForm};
|
use graphic_types::vector_types::gradient::{Gradient, GradientForm};
|
||||||
use graphic_types::vector_types::subpath::Subpath;
|
|
||||||
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
|
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
|
||||||
use graphic_types::vector_types::vector::misc::dvec2_to_point;
|
use graphic_types::vector_types::vector::misc::dvec2_to_point;
|
||||||
use graphic_types::vector_types::vector::style::{RenderMode, StrokeAlign, StrokeCap, StrokeJoin};
|
use graphic_types::vector_types::vector::style::{RenderMode, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
@@ -476,11 +475,9 @@ fn get_outline_styles(render_params: &RenderParams) -> (kurbo::Stroke, peniko::C
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_raster_outline(scene: &mut Scene, outline_transform: &DAffine2, render_params: &RenderParams) {
|
fn draw_raster_outline(scene: &mut Scene, outline_transform: &DAffine2, render_params: &RenderParams) {
|
||||||
use graphic_types::vector_types::vector::PointId;
|
|
||||||
|
|
||||||
let (outline_stroke, outline_color_peniko) = get_outline_styles(render_params);
|
let (outline_stroke, outline_color_peniko) = get_outline_styles(render_params);
|
||||||
|
|
||||||
let mut outline_path = Subpath::<PointId>::new_rectangle(DVec2::ZERO, DVec2::ONE).to_bezpath();
|
let mut outline_path = rectangle_path(DVec2::ZERO, DVec2::ONE);
|
||||||
outline_path.apply_affine(Affine::new(outline_transform.to_cols_array()));
|
outline_path.apply_affine(Affine::new(outline_transform.to_cols_array()));
|
||||||
|
|
||||||
scene.stroke(&outline_stroke, Affine::IDENTITY, outline_color_peniko, None, &outline_path);
|
scene.stroke(&outline_stroke, Affine::IDENTITY, outline_color_peniko, None, &outline_path);
|
||||||
@@ -1486,7 +1483,7 @@ fn render_vector_shape_svg(item: ItemRef<'_, Vector>, vector: &Vector, render: &
|
|||||||
MaskType::Mask
|
MaskType::Mask
|
||||||
};
|
};
|
||||||
|
|
||||||
let path_is_closed = vector.stroke_bezier_paths().all(|path| path.closed());
|
let path_is_closed = vector.stroke_bezpath_iter().all(|path| matches!(path.elements().last(), Some(PathEl::ClosePath)));
|
||||||
let can_draw_aligned_stroke = path_is_closed
|
let can_draw_aligned_stroke = path_is_closed
|
||||||
&& stroke_params.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered())
|
&& stroke_params.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered())
|
||||||
&& stroke_paint.is_some_and(|graphic| !graphic.is_guaranteed_fully_transparent());
|
&& stroke_paint.is_some_and(|graphic| !graphic.is_guaranteed_fully_transparent());
|
||||||
@@ -1750,7 +1747,9 @@ fn render_vector_item_to_vello(
|
|||||||
// the function ignores the arg for Center align) and the `SrcIn`/`SrcOut` aligned-stroke branch further down.
|
// the function ignores the arg for Center align) and the `SrcIn`/`SrcOut` aligned-stroke branch further down.
|
||||||
let stroke = stroke_params.as_ref();
|
let stroke = stroke_params.as_ref();
|
||||||
let stroke_fully_transparent = stroke_paint.is_none_or(|paint| paint.is_guaranteed_fully_transparent());
|
let stroke_fully_transparent = stroke_paint.is_none_or(|paint| paint.is_guaranteed_fully_transparent());
|
||||||
let can_draw_aligned_stroke = !stroke_fully_transparent && stroke.is_some_and(|s| s.has_renderable_stroke() && s.align.is_not_centered()) && element.stroke_bezier_paths().all(|p| p.closed());
|
let can_draw_aligned_stroke = !stroke_fully_transparent
|
||||||
|
&& stroke.is_some_and(|s| s.has_renderable_stroke() && s.align.is_not_centered())
|
||||||
|
&& element.stroke_bezpath_iter().all(|p| matches!(p.elements().last(), Some(PathEl::ClosePath)));
|
||||||
|
|
||||||
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
|
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
|
||||||
let needs_blend_layer = opacity < 1. || blend_mode_attr != BlendMode::default();
|
let needs_blend_layer = opacity < 1. || blend_mode_attr != BlendMode::default();
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ extern crate log;
|
|||||||
|
|
||||||
pub mod gradient;
|
pub mod gradient;
|
||||||
pub mod math;
|
pub mod math;
|
||||||
pub mod subpath;
|
|
||||||
pub mod vector;
|
pub mod vector;
|
||||||
|
|
||||||
// Re-export commonly used types at the crate root
|
// Re-export commonly used types at the crate root
|
||||||
pub use core_types as gcore;
|
pub use core_types as gcore;
|
||||||
pub use gradient::{Gradient, GradientForm, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, GradientStop};
|
pub use gradient::{Gradient, GradientForm, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, GradientStop};
|
||||||
pub use math::QuadExt;
|
pub use math::QuadExt;
|
||||||
pub use subpath::Subpath;
|
|
||||||
pub use vector::Vector;
|
pub use vector::Vector;
|
||||||
pub use vector::reference_point::ReferencePoint;
|
pub use vector::reference_point::ReferencePoint;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
// Implementation constants
|
|
||||||
|
|
||||||
/// Constant used to determine if `f64`s are equivalent.
|
|
||||||
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
|
|
||||||
@@ -1,386 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use crate::vector::misc::{ArcType, SpiralType, point_to_dvec2};
|
|
||||||
use glam::DVec2;
|
|
||||||
use kurbo::PathSeg;
|
|
||||||
use std::f64::consts::TAU;
|
|
||||||
|
|
||||||
pub struct PathSegPoints {
|
|
||||||
pub p0: DVec2,
|
|
||||||
pub p1: Option<DVec2>,
|
|
||||||
pub p2: Option<DVec2>,
|
|
||||||
pub p3: DVec2,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PathSegPoints {
|
|
||||||
pub fn new(p0: DVec2, p1: Option<DVec2>, p2: Option<DVec2>, 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<PointId: Identifier> Subpath<PointId> {
|
|
||||||
/// 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<ManipulatorGroup<PointId>>, closed: bool) -> Self {
|
|
||||||
assert!(!closed || !manipulator_groups.is_empty(), "A closed Subpath must contain more than 0 ManipulatorGroups.");
|
|
||||||
Self { manipulator_groups, closed }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 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<PointId>] {
|
|
||||||
&self.manipulator_groups
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a mutable reference to the [ManipulatorGroup]s in the `Subpath`.
|
|
||||||
pub fn manipulator_groups_mut(&mut self) -> &mut Vec<ManipulatorGroup<PointId>> {
|
|
||||||
&mut self.manipulator_groups
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_anchors(anchor_positions: impl IntoIterator<Item = DVec2>, closed: bool) -> Self {
|
|
||||||
Self::new(anchor_positions.into_iter().map(|anchor| ManipulatorGroup::new_anchor(anchor)).collect(), closed)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Constructs a rectangle with `corner1` and `corner2` as the two corners.
|
|
||||||
pub fn new_rectangle(corner1: DVec2, corner2: DVec2) -> Self {
|
|
||||||
Self::from_anchors([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_rectangle(corner1: DVec2, corner2: DVec2, corner_radii: [f64; 4]) -> Self {
|
|
||||||
if corner_radii.iter().all(|radii| radii.abs() < f64::EPSILON * 100.) {
|
|
||||||
return Self::new_rectangle(corner1, corner2);
|
|
||||||
}
|
|
||||||
|
|
||||||
use std::f64::consts::{FRAC_1_SQRT_2, PI};
|
|
||||||
|
|
||||||
let new_arc = |center: DVec2, corner: DVec2, radius: f64| -> Vec<ManipulatorGroup<PointId>> {
|
|
||||||
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)];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constant from 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).is_multiple_of(2) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Constructs an arrow shape from start and end points with parametric control over dimensions
|
|
||||||
pub fn new_arrow(start: DVec2, end: DVec2, shaft_width: f64, head_width: f64, head_length: f64) -> Self {
|
|
||||||
let delta = end - start;
|
|
||||||
let length = delta.length();
|
|
||||||
|
|
||||||
if length < 1e-10 {
|
|
||||||
// Degenerate case: return a point
|
|
||||||
return Self::from_anchors([start], true);
|
|
||||||
}
|
|
||||||
|
|
||||||
let direction = delta / length;
|
|
||||||
let perpendicular = DVec2::new(-direction.y, direction.x);
|
|
||||||
|
|
||||||
let half_shaft = shaft_width * 0.5;
|
|
||||||
let half_head = head_width * 0.5;
|
|
||||||
let head_base_distance = (length - head_length).max(0.);
|
|
||||||
let head_base = start + direction * head_base_distance;
|
|
||||||
|
|
||||||
// Arrow path starts at the tail, traces around the shape, and returns to the tail
|
|
||||||
let anchors = [
|
|
||||||
start, // Tail center (origin)
|
|
||||||
start + perpendicular * half_shaft, // Tail top
|
|
||||||
head_base + perpendicular * half_shaft, // Head base top (shaft)
|
|
||||||
head_base + perpendicular * half_head, // Head base top (wide)
|
|
||||||
end, // Tip
|
|
||||||
head_base - perpendicular * half_head, // Head base bottom (wide)
|
|
||||||
head_base - perpendicular * half_shaft, // Head base bottom (shaft)
|
|
||||||
start - perpendicular * half_shaft, // Tail bottom
|
|
||||||
];
|
|
||||||
|
|
||||||
Self::from_anchors(anchors, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_spiral(a: f64, outer_radius: f64, turns: f64, start_angle: f64, delta_theta: f64, spiral_type: SpiralType) -> Self {
|
|
||||||
let mut manipulator_groups = Vec::new();
|
|
||||||
let mut prev_in_handle = None;
|
|
||||||
let theta_end = turns * std::f64::consts::TAU + start_angle;
|
|
||||||
|
|
||||||
let a = if spiral_type == SpiralType::Logarithmic { a.max(1e-10) } else { a };
|
|
||||||
let b = calculate_growth_factor(a, turns, outer_radius, spiral_type);
|
|
||||||
|
|
||||||
let mut theta = start_angle;
|
|
||||||
while theta < theta_end {
|
|
||||||
let theta_next = f64::min(theta + delta_theta, theta_end);
|
|
||||||
|
|
||||||
let p0 = spiral_point(theta, a, b, spiral_type);
|
|
||||||
let p3 = spiral_point(theta_next, a, b, spiral_type);
|
|
||||||
let t0 = spiral_tangent(theta, a, b, spiral_type);
|
|
||||||
let t1 = spiral_tangent(theta_next, a, b, spiral_type);
|
|
||||||
|
|
||||||
let arc_len = spiral_arc_length(theta, theta_next, a, b, spiral_type);
|
|
||||||
let d = arc_len / 3.;
|
|
||||||
|
|
||||||
let p1 = p0 + d * t0;
|
|
||||||
let p2 = p3 - d * t1;
|
|
||||||
|
|
||||||
manipulator_groups.push(ManipulatorGroup::new(p0, prev_in_handle, Some(p1)));
|
|
||||||
prev_in_handle = Some(p2);
|
|
||||||
|
|
||||||
// If final segment, end with anchor at theta_end
|
|
||||||
if (theta_next - theta_end).abs() < f64::EPSILON {
|
|
||||||
manipulator_groups.push(ManipulatorGroup::new(p3, prev_in_handle, None));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
theta = theta_next;
|
|
||||||
}
|
|
||||||
|
|
||||||
Self::new(manipulator_groups, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn calculate_growth_factor(a: f64, turns: f64, outer_radius: f64, spiral_type: SpiralType) -> f64 {
|
|
||||||
match spiral_type {
|
|
||||||
SpiralType::Archimedean => {
|
|
||||||
let total_theta = turns * TAU;
|
|
||||||
(outer_radius - a) / total_theta
|
|
||||||
}
|
|
||||||
SpiralType::Logarithmic => {
|
|
||||||
let total_theta = turns * TAU;
|
|
||||||
((outer_radius.abs() / a).ln()) / total_theta
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a point on the given spiral type at angle `theta`.
|
|
||||||
pub fn spiral_point(theta: f64, a: f64, b: f64, spiral_type: SpiralType) -> DVec2 {
|
|
||||||
match spiral_type {
|
|
||||||
SpiralType::Archimedean => archimedean_spiral_point(theta, a, b),
|
|
||||||
SpiralType::Logarithmic => log_spiral_point(theta, a, b),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the tangent direction at angle `theta` for the given spiral type.
|
|
||||||
fn spiral_tangent(theta: f64, a: f64, b: f64, spiral_type: SpiralType) -> DVec2 {
|
|
||||||
match spiral_type {
|
|
||||||
SpiralType::Archimedean => archimedean_spiral_tangent(theta, a, b),
|
|
||||||
SpiralType::Logarithmic => log_spiral_tangent(theta, a, b),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Computes arc length between two angles for the given spiral type.
|
|
||||||
fn spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64, spiral_type: SpiralType) -> f64 {
|
|
||||||
match spiral_type {
|
|
||||||
SpiralType::Archimedean => archimedean_spiral_arc_length(theta_start, theta_end, a, b),
|
|
||||||
SpiralType::Logarithmic => log_spiral_arc_length(theta_start, theta_end, a, b),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a point on a logarithmic spiral at angle `theta`.
|
|
||||||
fn log_spiral_point(theta: f64, a: f64, b: f64) -> DVec2 {
|
|
||||||
let r = a * (b * theta).exp(); // a * e^(bθ)
|
|
||||||
DVec2::new(r * theta.cos(), -r * theta.sin())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Computes arc length along a logarithmic spiral between two angles.
|
|
||||||
fn log_spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64) -> f64 {
|
|
||||||
let factor = (1. + b * b).sqrt();
|
|
||||||
(a / b) * factor * ((b * theta_end).exp() - (b * theta_start).exp())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the tangent direction of a logarithmic spiral at angle `theta`.
|
|
||||||
fn log_spiral_tangent(theta: f64, a: f64, b: f64) -> DVec2 {
|
|
||||||
let r = a * (b * theta).exp();
|
|
||||||
let dx = r * (b * theta.cos() - theta.sin());
|
|
||||||
let dy = r * (b * theta.sin() + theta.cos());
|
|
||||||
|
|
||||||
DVec2::new(dx, -dy).normalize_or(DVec2::X)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a point on an Archimedean spiral at angle `theta`.
|
|
||||||
fn archimedean_spiral_point(theta: f64, a: f64, b: f64) -> DVec2 {
|
|
||||||
let r = a + b * theta;
|
|
||||||
DVec2::new(r * theta.cos(), -r * theta.sin())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the tangent direction of an Archimedean spiral at angle `theta`.
|
|
||||||
fn archimedean_spiral_tangent(theta: f64, a: f64, b: f64) -> DVec2 {
|
|
||||||
let r = a + b * theta;
|
|
||||||
let dx = b * theta.cos() - r * theta.sin();
|
|
||||||
let dy = b * theta.sin() + r * theta.cos();
|
|
||||||
DVec2::new(dx, -dy).normalize_or(DVec2::X)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Computes arc length along an Archimedean spiral between two angles.
|
|
||||||
fn archimedean_spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64) -> f64 {
|
|
||||||
archimedean_spiral_arc_length_origin(theta_end, a, b) - archimedean_spiral_arc_length_origin(theta_start, a, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Computes arc length from origin to a point on Archimedean spiral at angle `theta`.
|
|
||||||
fn archimedean_spiral_arc_length_origin(theta: f64, a: f64, b: f64) -> f64 {
|
|
||||||
let r = a + b * theta;
|
|
||||||
let sqrt_term = (r * r + b * b).sqrt();
|
|
||||||
(r * sqrt_term + b * b * ((r + sqrt_term).ln())) / (2. * b)
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
use super::consts::MAX_ABSOLUTE_DIFFERENCE;
|
|
||||||
use super::*;
|
|
||||||
use crate::vector::algorithms::bezpath_algorithms::pathseg_length_centroid_and_length;
|
|
||||||
use crate::vector::algorithms::intersection::{filtered_all_segment_intersections, pathseg_self_intersections};
|
|
||||||
use core_types::math::polynomial::pathseg_to_parametric_polynomial;
|
|
||||||
use glam::DVec2;
|
|
||||||
|
|
||||||
impl<PointId: Identifier> Subpath<PointId> {
|
|
||||||
/// 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.
|
|
||||||
fn all_self_intersections(&self, accuracy: Option<f64>, minimum_separation: Option<f64>) -> 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 list
|
|
||||||
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<f64>, minimum_separation: Option<f64>) -> 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<f64>, 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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test_centroid {
|
|
||||||
use crate::vector::PointId;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
#[test]
|
|
||||||
fn centroid_rect() {
|
|
||||||
let rect = Subpath::<PointId>::new_rectangle(DVec2::new(100., 100.), DVec2::new(300., 200.));
|
|
||||||
let (center, area) = rect.area_centroid_and_area(Some(1e-3), Some(1e-3)).unwrap();
|
|
||||||
assert_eq!(area, 200. * 100.);
|
|
||||||
assert_eq!(center, DVec2::new(200., 150.))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
// use super::consts::MAX_ABSOLUTE_DIFFERENCE;
|
|
||||||
// use super::utils::{SubpathTValue};
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
impl<PointId: super::structs::Identifier> Subpath<PointId> {
|
|
||||||
/// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push a manipulator group to the end.
|
|
||||||
pub fn push_manipulator_group(&mut self, group: ManipulatorGroup<PointId>) {
|
|
||||||
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<PointId>> {
|
|
||||||
self.manipulator_groups.last_mut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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};
|
|
||||||
pub use structs::*;
|
|
||||||
|
|
||||||
/// Structure used to represent a path composed of [Bezier] curves.
|
|
||||||
#[derive(Clone, PartialEq, graphene_hash::CacheHash)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
|
||||||
pub struct Subpath<PointId: Identifier> {
|
|
||||||
manipulator_groups: Vec<ManipulatorGroup<PointId>>,
|
|
||||||
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<PointId>,
|
|
||||||
is_always_closed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<PointId: Identifier> 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<Self::Item> {
|
|
||||||
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.manipulator_groups[start_index].to_bezier(&self.subpath.manipulator_groups[end_index]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<PointId: Identifier> Debug for Subpath<PointId> {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
|
||||||
f.debug_struct("Subpath").field("closed", &self.closed).field("manipulator_groups", &self.manipulator_groups).finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
use crate::subpath::{Identifier, Subpath};
|
|
||||||
use crate::vector::algorithms::bezpath_algorithms::bezpath_is_inside_bezpath;
|
|
||||||
use crate::vector::misc::dvec2_to_point;
|
|
||||||
use glam::DVec2;
|
|
||||||
use kurbo::{Affine, BezPath, Shape};
|
|
||||||
|
|
||||||
impl<PointId: Identifier> Subpath<PointId> {
|
|
||||||
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<PointId>, accuracy: Option<f64>, minimum_separation: Option<f64>) -> 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])])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
use crate::vector::misc::dvec2_to_point;
|
|
||||||
use glam::{DAffine2, DVec2};
|
|
||||||
use kurbo::{CubicBez, Line, PathSeg, QuadBez};
|
|
||||||
use std::fmt::{Debug, Formatter, Result};
|
|
||||||
use std::hash::Hash;
|
|
||||||
|
|
||||||
/// An id type used for each [ManipulatorGroup].
|
|
||||||
pub trait Identifier: Sized + Clone + PartialEq + Hash + graphene_hash::CacheHash + 'static {
|
|
||||||
fn new() -> Self;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Structure used to represent a single anchor with up to two optional associated handles along a `Subpath`
|
|
||||||
#[derive(Copy, Clone, PartialEq, graphene_hash::CacheHash)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
|
||||||
pub struct ManipulatorGroup<PointId: Identifier> {
|
|
||||||
pub anchor: DVec2,
|
|
||||||
pub in_handle: Option<DVec2>,
|
|
||||||
pub out_handle: Option<DVec2>,
|
|
||||||
pub id: PointId,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<PointId: Identifier> Debug for ManipulatorGroup<PointId> {
|
|
||||||
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<PointId: Identifier> ManipulatorGroup<PointId> {
|
|
||||||
/// Construct a new manipulator group from an anchor, in handle and out handle
|
|
||||||
pub fn new(anchor: DVec2, in_handle: Option<DVec2>, out_handle: Option<DVec2>) -> 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, 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<DVec2>, out_handle: Option<DVec2>, 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<PointId>) -> 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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Representation of the handle point(s) in a bezier segment.
|
|
||||||
#[derive(Copy, Clone, PartialEq, Debug, graphene_hash::CacheHash)]
|
|
||||||
#[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 BezierHandles {
|
|
||||||
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<DVec2> {
|
|
||||||
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<DVec2> {
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
use super::structs::Identifier;
|
|
||||||
use super::*;
|
|
||||||
use glam::DAffine2;
|
|
||||||
|
|
||||||
impl<PointId: Identifier> Subpath<PointId> {
|
|
||||||
/// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::intersection::bezpath_intersections;
|
use super::intersection::{bezpath_intersections, filtered_all_segment_intersections, pathseg_self_intersections};
|
||||||
use super::poisson_disk::poisson_disk_sample;
|
use super::poisson_disk::poisson_disk_sample;
|
||||||
use super::util::pathseg_tangent;
|
use super::util::pathseg_tangent;
|
||||||
use crate::vector::misc::{PointSpacingType, dvec2_to_point, point_to_dvec2};
|
use crate::vector::misc::{PointSpacingType, dvec2_to_point, point_to_dvec2};
|
||||||
@@ -7,6 +7,9 @@ use glam::{DMat2, DVec2};
|
|||||||
use kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArclen, ParamCurveDeriv, PathEl, PathSeg, Point, QuadBez, Rect, Shape, Vec2};
|
use kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArclen, ParamCurveDeriv, PathEl, PathSeg, Point, QuadBez, Rect, Shape, Vec2};
|
||||||
use std::f64::consts::{FRAC_PI_2, PI};
|
use std::f64::consts::{FRAC_PI_2, PI};
|
||||||
|
|
||||||
|
/// Default threshold for comparing floating point values in intersection and centroid math.
|
||||||
|
const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
|
||||||
|
|
||||||
/// Splits the [`BezPath`] at segment index at `t` value which lie in the range of [0, 1].
|
/// Splits the [`BezPath`] at segment index at `t` value which lie in the range of [0, 1].
|
||||||
/// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1.
|
/// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1.
|
||||||
pub fn split_bezpath_at_segment(bezpath: &BezPath, segment_index: usize, t: f64) -> Option<(BezPath, BezPath)> {
|
pub fn split_bezpath_at_segment(bezpath: &BezPath, segment_index: usize, t: f64) -> Option<(BezPath, BezPath)> {
|
||||||
@@ -414,8 +417,8 @@ pub fn poisson_disk_points(bezpath_index: usize, bezpaths: &[(BezPath, Rect)], s
|
|||||||
poisson_disk_sample(offset, width, height, separation_disk_diameter, point_in_shape_checker, line_intersect_shape_checker, rng)
|
poisson_disk_sample(offset, width, height, separation_disk_diameter, point_in_shape_checker, line_intersect_shape_checker, rng)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: If a segment curls back on itself tightly enough it could intersect again at the portion that should be trimmed. This could cause the Subpaths to be clipped
|
// TODO: If a segment curls back on itself tightly enough it could intersect again at the portion that should be trimmed. This could cause the subpaths to be clipped
|
||||||
// TODO: at the incorrect location. This can be avoided by first trimming the two Subpaths at any extrema, effectively ignoring loopbacks.
|
// TODO: at the incorrect location. This can be avoided by first trimming the two subpaths at any extrema, effectively ignoring loopbacks.
|
||||||
/// Helper function to clip overlap of two intersecting open BezPaths. Returns an Option because intersections may not exist for certain arrangements and distances.
|
/// Helper function to clip overlap of two intersecting open BezPaths. Returns an Option because intersections may not exist for certain arrangements and distances.
|
||||||
/// Assumes that the BezPaths represents simple Bezier segments, and clips the BezPaths at the last intersection of the first BezPath, and first intersection of the last BezPath.
|
/// Assumes that the BezPaths represents simple Bezier segments, and clips the BezPaths at the last intersection of the first BezPath, and first intersection of the last BezPath.
|
||||||
pub fn clip_simple_bezpaths(bezpath1: &BezPath, bezpath2: &BezPath) -> Option<(BezPath, BezPath)> {
|
pub fn clip_simple_bezpaths(bezpath1: &BezPath, bezpath2: &BezPath) -> Option<(BezPath, BezPath)> {
|
||||||
@@ -565,6 +568,124 @@ pub fn bezpath_is_inside_bezpath(bezpath1: &BezPath, bezpath2: &BezPath, accurac
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The segments of the [`BezPath`] always considering it as a closed path, synthesizing the closing line when it is open.
|
||||||
|
fn closed_segments(bezpath: &BezPath) -> Vec<PathSeg> {
|
||||||
|
let mut segments = bezpath.segments().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if let (Some(first), Some(last)) = (segments.first(), segments.last())
|
||||||
|
&& last.end() != first.start()
|
||||||
|
{
|
||||||
|
segments.push(PathSeg::Line(Line::new(last.end(), first.start())));
|
||||||
|
}
|
||||||
|
|
||||||
|
segments
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a list of `t` values that correspond to all the self intersection points of the path always considering it as a closed path.
|
||||||
|
/// The index and `t` value of both will be returned that corresponds to a point, sorted based on their index and `t` respectively.
|
||||||
|
fn closed_bezpath_self_intersections(segments: &[PathSeg], accuracy: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
|
||||||
|
let mut intersections_vec = Vec::new();
|
||||||
|
let err = accuracy.unwrap_or(MAX_ABSOLUTE_DIFFERENCE);
|
||||||
|
let num_curves = segments.len();
|
||||||
|
|
||||||
|
// O(n²) in the number of segments, since every segment pair is compared
|
||||||
|
segments.iter().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)]));
|
||||||
|
segments.iter().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 [`BezPath`] always considering it as a closed path. 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 path's closed shape would balance at this point.
|
||||||
|
///
|
||||||
|
/// It will return `None` if no segment is present. If the area is less than `error`, it will return `Some((DVec2::NAN, 0.))`.
|
||||||
|
///
|
||||||
|
/// Because the calculation of area and centroid for a 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 bezpath_area_centroid_and_area(bezpath: &BezPath, error: Option<f64>, minimum_separation: Option<f64>) -> Option<(DVec2, f64)> {
|
||||||
|
let segments = closed_segments(bezpath);
|
||||||
|
let all_intersections = closed_bezpath_self_intersections(&segments, error, minimum_separation);
|
||||||
|
let mut current_sign: f64 = 1.;
|
||||||
|
|
||||||
|
let (x_sum, y_sum, area) = segments
|
||||||
|
.iter()
|
||||||
|
.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 [`BezPath`].
|
||||||
|
///
|
||||||
|
/// The length centroid is the center of mass for the arc length of the solid shape's perimeter.
|
||||||
|
/// An infinitely thin wire forming the path's shape would balance at this point.
|
||||||
|
///
|
||||||
|
/// It will return `None` if no segment is present.
|
||||||
|
/// - `accuracy` is used to approximate the curve.
|
||||||
|
/// - `always_closed` is to consider the path as closed always.
|
||||||
|
pub fn bezpath_length_centroid_and_length(bezpath: &BezPath, accuracy: Option<f64>, always_closed: bool) -> Option<(DVec2, f64)> {
|
||||||
|
let segments = if always_closed { closed_segments(bezpath) } else { bezpath.segments().collect() };
|
||||||
|
|
||||||
|
segments
|
||||||
|
.into_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))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
// TODO: add more intersection tests
|
// TODO: add more intersection tests
|
||||||
@@ -594,4 +715,13 @@ mod tests {
|
|||||||
let line_inside = Line::new(Point::new(101., 101.5), Point::new(150.2, 499.)).to_path(DEFAULT_ACCURACY);
|
let line_inside = Line::new(Point::new(101., 101.5), Point::new(150.2, 499.)).to_path(DEFAULT_ACCURACY);
|
||||||
assert!(bezpath_is_inside_bezpath(&line_inside, &boundary_polygon, None, None));
|
assert!(bezpath_is_inside_bezpath(&line_inside, &boundary_polygon, None, None));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn centroid_rect() {
|
||||||
|
let rect = crate::vector::algorithms::shapes::rectangle_bezpath(glam::DVec2::new(100., 100.), glam::DVec2::new(300., 200.));
|
||||||
|
let (center, area) = super::bezpath_area_centroid_and_area(&rect, Some(1e-3), Some(1e-3)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(area, 200. * 100.);
|
||||||
|
assert_eq!(center, glam::DVec2::new(200., 150.));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ pub mod intersection;
|
|||||||
pub mod merge_by_distance;
|
pub mod merge_by_distance;
|
||||||
pub mod offset_subpath;
|
pub mod offset_subpath;
|
||||||
pub mod poisson_disk;
|
pub mod poisson_disk;
|
||||||
|
pub mod shapes;
|
||||||
pub mod spline;
|
pub mod spline;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ pub fn offset_bezpath(bezpath: &BezPath, distance: f64, join: Join, miter_limit:
|
|||||||
return BezPath::new();
|
return BezPath::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clip or join consecutive Subpaths
|
// Clip or join consecutive subpaths
|
||||||
for i in 0..bezpaths.len() - 1 {
|
for i in 0..bezpaths.len() - 1 {
|
||||||
let j = i + 1;
|
let j = i + 1;
|
||||||
let bezpath1 = &bezpaths[i];
|
let bezpath1 = &bezpaths[i];
|
||||||
@@ -63,7 +63,7 @@ pub fn offset_bezpath(bezpath: &BezPath, distance: f64, join: Join, miter_limit:
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The angle is concave. The Subpath overlap and must be clipped
|
// The angle is concave. The subpaths overlap and must be clipped
|
||||||
let mut apply_join = true;
|
let mut apply_join = true;
|
||||||
|
|
||||||
if let Some((clipped_subpath1, clipped_subpath2)) = clip_simple_bezpaths(bezpath1, bezpath2) {
|
if let Some((clipped_subpath1, clipped_subpath2)) = clip_simple_bezpaths(bezpath1, bezpath2) {
|
||||||
@@ -71,7 +71,7 @@ pub fn offset_bezpath(bezpath: &BezPath, distance: f64, join: Join, miter_limit:
|
|||||||
bezpaths[j] = clipped_subpath2;
|
bezpaths[j] = clipped_subpath2;
|
||||||
apply_join = false;
|
apply_join = false;
|
||||||
}
|
}
|
||||||
// The angle is convex. The Subpath must be joined using the specified join type
|
// The angle is convex. The subpaths must be joined using the specified join type
|
||||||
if apply_join {
|
if apply_join {
|
||||||
match join {
|
match join {
|
||||||
Join::Bevel => {
|
Join::Bevel => {
|
||||||
|
|||||||
@@ -0,0 +1,368 @@
|
|||||||
|
//! Constructors for the primitive shapes used by the vector generator nodes.
|
||||||
|
//!
|
||||||
|
//! Anchor order and winding direction are load-bearing, since fills rely on every generator agreeing.
|
||||||
|
|
||||||
|
use crate::vector::misc::{ArcType, SpiralType, dvec2_to_point};
|
||||||
|
use glam::DVec2;
|
||||||
|
use kurbo::BezPath;
|
||||||
|
use std::f64::consts::TAU;
|
||||||
|
|
||||||
|
/// Constant from <https://pomax.github.io/bezierinfo/#circles_cubic>
|
||||||
|
const HANDLE_OFFSET_FACTOR: f64 = 0.551784777779014;
|
||||||
|
|
||||||
|
/// An anchor point with its optional incoming and outgoing handle positions, in absolute coordinates.
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Anchor {
|
||||||
|
position: DVec2,
|
||||||
|
in_handle: Option<DVec2>,
|
||||||
|
out_handle: Option<DVec2>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Anchor {
|
||||||
|
fn new(position: DVec2, in_handle: Option<DVec2>, out_handle: Option<DVec2>) -> Self {
|
||||||
|
Self { position, in_handle, out_handle }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sharp(position: DVec2) -> Self {
|
||||||
|
Self::new(position, None, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stitches anchors into a path, emitting a cubic when both facing handles exist, a quadratic when only one does, and a line otherwise.
|
||||||
|
fn bezpath_from_anchors(anchors: &[Anchor], closed: bool) -> BezPath {
|
||||||
|
let mut bezpath = BezPath::new();
|
||||||
|
|
||||||
|
let Some(first) = anchors.first() else { return bezpath };
|
||||||
|
bezpath.move_to(dvec2_to_point(first.position));
|
||||||
|
let mut out_handle = first.out_handle;
|
||||||
|
|
||||||
|
let connect_to = |bezpath: &mut BezPath, out_handle: Option<DVec2>, anchor: &Anchor| match (out_handle, anchor.in_handle) {
|
||||||
|
(Some(handle_start), Some(handle_end)) => bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(anchor.position)),
|
||||||
|
(None, None) => bezpath.line_to(dvec2_to_point(anchor.position)),
|
||||||
|
(None, Some(handle)) | (Some(handle), None) => bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(anchor.position)),
|
||||||
|
};
|
||||||
|
|
||||||
|
for anchor in anchors.iter().skip(1) {
|
||||||
|
connect_to(&mut bezpath, out_handle, anchor);
|
||||||
|
out_handle = anchor.out_handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if closed {
|
||||||
|
connect_to(&mut bezpath, out_handle, first);
|
||||||
|
bezpath.close_path();
|
||||||
|
}
|
||||||
|
|
||||||
|
bezpath
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stitches a sequence of sharp (handleless) anchors into a polyline, or a closed polygon.
|
||||||
|
pub fn polyline_bezpath(positions: impl IntoIterator<Item = DVec2>, closed: bool) -> BezPath {
|
||||||
|
let anchors: Vec<Anchor> = positions.into_iter().map(Anchor::sharp).collect();
|
||||||
|
bezpath_from_anchors(&anchors, closed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a rectangle with `corner1` and `corner2` as the two corners.
|
||||||
|
pub fn rectangle_bezpath(corner1: DVec2, corner2: DVec2) -> BezPath {
|
||||||
|
polyline_bezpath([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 rounded_rectangle_bezpath(corner1: DVec2, corner2: DVec2, corner_radii: [f64; 4]) -> BezPath {
|
||||||
|
if corner_radii.iter().all(|radius| radius.abs() < f64::EPSILON * 100.) {
|
||||||
|
return rectangle_bezpath(corner1, corner2);
|
||||||
|
}
|
||||||
|
|
||||||
|
use std::f64::consts::{FRAC_1_SQRT_2, PI};
|
||||||
|
|
||||||
|
// The pair of anchors where one rounded corner's arc leaves and rejoins the straight edges
|
||||||
|
let corner_anchors = |center: DVec2, corner: DVec2, radius: f64| -> Vec<Anchor> {
|
||||||
|
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![Anchor::sharp(point1), Anchor::sharp(point2)];
|
||||||
|
}
|
||||||
|
|
||||||
|
let handle_offset = radius * HANDLE_OFFSET_FACTOR;
|
||||||
|
vec![
|
||||||
|
Anchor::new(point1, None, Some(point1 + handle_offset * (corner - point1).normalize())),
|
||||||
|
Anchor::new(point2, Some(point2 + handle_offset * (corner - point2).normalize()), None),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
let anchors = [
|
||||||
|
corner_anchors(DVec2::new(corner1.x + corner_radii[0], corner1.y + corner_radii[0]), DVec2::new(corner1.x, corner1.y), corner_radii[0]),
|
||||||
|
corner_anchors(DVec2::new(corner2.x - corner_radii[1], corner1.y + corner_radii[1]), DVec2::new(corner2.x, corner1.y), corner_radii[1]),
|
||||||
|
corner_anchors(DVec2::new(corner2.x - corner_radii[2], corner2.y - corner_radii[2]), DVec2::new(corner2.x, corner2.y), corner_radii[2]),
|
||||||
|
corner_anchors(DVec2::new(corner1.x + corner_radii[3], corner2.y - corner_radii[3]), DVec2::new(corner1.x, corner2.y), corner_radii[3]),
|
||||||
|
]
|
||||||
|
.concat();
|
||||||
|
|
||||||
|
bezpath_from_anchors(&anchors, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs an ellipse with `corner1` and `corner2` as the two corners of the bounding box.
|
||||||
|
pub fn ellipse_bezpath(corner1: DVec2, corner2: DVec2) -> BezPath {
|
||||||
|
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);
|
||||||
|
|
||||||
|
let handle_offset = size * HANDLE_OFFSET_FACTOR * 0.5;
|
||||||
|
|
||||||
|
let anchors = [
|
||||||
|
Anchor::new(top, Some(top - handle_offset * DVec2::X), Some(top + handle_offset * DVec2::X)),
|
||||||
|
Anchor::new(right, Some(right - handle_offset * DVec2::Y), Some(right + handle_offset * DVec2::Y)),
|
||||||
|
Anchor::new(bottom, Some(bottom + handle_offset * DVec2::X), Some(bottom - handle_offset * DVec2::X)),
|
||||||
|
Anchor::new(left, Some(left + handle_offset * DVec2::Y), Some(left - handle_offset * DVec2::Y)),
|
||||||
|
];
|
||||||
|
|
||||||
|
bezpath_from_anchors(&anchors, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs an arc by a `radius`, `start_angle` and `sweep_angle`. Angles must be in radians. The arc type makes it look like a pie or pacman.
|
||||||
|
pub fn arc_bezpath(radius: f64, start_angle: f64, sweep_angle: f64, arc_type: ArcType) -> BezPath {
|
||||||
|
// Prevents glitches from numerical imprecision that have been observed during animation playback after about a minute
|
||||||
|
let start_angle = start_angle % (TAU * 2.);
|
||||||
|
let sweep_angle = sweep_angle % (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 / TAU).floor() as u32).is_multiple_of(2) {
|
||||||
|
sweep_angle %= TAU;
|
||||||
|
} else {
|
||||||
|
start_angle = sweep_angle % TAU;
|
||||||
|
sweep_angle = 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 anchors = 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;
|
||||||
|
|
||||||
|
anchors.push(Anchor::new(start, prev_in_handle, Some(handle_start)));
|
||||||
|
prev_in_handle = Some(handle_end);
|
||||||
|
prev_end = end;
|
||||||
|
}
|
||||||
|
anchors.push(Anchor::new(prev_end, prev_in_handle, None));
|
||||||
|
|
||||||
|
if slice {
|
||||||
|
anchors.push(Anchor::sharp(center));
|
||||||
|
}
|
||||||
|
|
||||||
|
bezpath_from_anchors(&anchors, closed || slice)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a regular polygon (ngon). Based on `sides` and `radius`, which is the distance from the center to any vertex.
|
||||||
|
pub fn regular_polygon_bezpath(center: DVec2, sides: u64, radius: f64) -> BezPath {
|
||||||
|
let sides = sides.max(3);
|
||||||
|
let angle_increment = TAU / (sides as f64);
|
||||||
|
let positions = (0..sides).map(|i| {
|
||||||
|
let angle = (i as f64) * angle_increment - std::f64::consts::FRAC_PI_2;
|
||||||
|
center + radius * DVec2::new(f64::cos(angle), f64::sin(angle))
|
||||||
|
});
|
||||||
|
|
||||||
|
polyline_bezpath(positions, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a star polygon (n-star). See [`regular_polygon_bezpath`], but with interspersed vertices at an `inner_radius`.
|
||||||
|
pub fn star_polygon_bezpath(center: DVec2, sides: u64, radius: f64, inner_radius: f64) -> BezPath {
|
||||||
|
let sides = sides.max(2);
|
||||||
|
let angle_increment = 0.5 * TAU / (sides as f64);
|
||||||
|
let positions = (0..sides * 2).map(|i| {
|
||||||
|
let angle = (i as f64) * angle_increment - std::f64::consts::FRAC_PI_2;
|
||||||
|
let radius = if i % 2 == 0 { radius } else { inner_radius };
|
||||||
|
center + radius * DVec2::new(f64::cos(angle), f64::sin(angle))
|
||||||
|
});
|
||||||
|
|
||||||
|
polyline_bezpath(positions, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a line from `point1` to `point2`.
|
||||||
|
pub fn line_bezpath(point1: DVec2, point2: DVec2) -> BezPath {
|
||||||
|
polyline_bezpath([point1, point2], false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs an arrow shape from start and end points with parametric control over dimensions.
|
||||||
|
pub fn arrow_bezpath(start: DVec2, end: DVec2, shaft_width: f64, head_width: f64, head_length: f64) -> BezPath {
|
||||||
|
let delta = end - start;
|
||||||
|
let length = delta.length();
|
||||||
|
|
||||||
|
// Degenerate case: return a point
|
||||||
|
if length < 1e-10 {
|
||||||
|
return polyline_bezpath([start], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let direction = delta / length;
|
||||||
|
let perpendicular = DVec2::new(-direction.y, direction.x);
|
||||||
|
|
||||||
|
let half_shaft = shaft_width * 0.5;
|
||||||
|
let half_head = head_width * 0.5;
|
||||||
|
let head_base_distance = (length - head_length).max(0.);
|
||||||
|
let head_base = start + direction * head_base_distance;
|
||||||
|
|
||||||
|
// Arrow path starts at the tail, traces around the shape, and returns to the tail
|
||||||
|
let positions = [
|
||||||
|
start, // Tail center (origin)
|
||||||
|
start + perpendicular * half_shaft, // Tail top
|
||||||
|
head_base + perpendicular * half_shaft, // Head base top (shaft)
|
||||||
|
head_base + perpendicular * half_head, // Head base top (wide)
|
||||||
|
end, // Tip
|
||||||
|
head_base - perpendicular * half_head, // Head base bottom (wide)
|
||||||
|
head_base - perpendicular * half_shaft, // Head base bottom (shaft)
|
||||||
|
start - perpendicular * half_shaft, // Tail bottom
|
||||||
|
];
|
||||||
|
|
||||||
|
polyline_bezpath(positions, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a spiral winding from an inner radius `a` out to `outer_radius`, sampled every `delta_theta` radians.
|
||||||
|
pub fn spiral_bezpath(a: f64, outer_radius: f64, turns: f64, start_angle: f64, delta_theta: f64, spiral_type: SpiralType) -> BezPath {
|
||||||
|
let mut anchors = Vec::new();
|
||||||
|
let mut prev_in_handle = None;
|
||||||
|
let theta_end = turns * TAU + start_angle;
|
||||||
|
|
||||||
|
let a = if spiral_type == SpiralType::Logarithmic { a.max(1e-10) } else { a };
|
||||||
|
let b = calculate_growth_factor(a, turns, outer_radius, spiral_type);
|
||||||
|
|
||||||
|
let mut theta = start_angle;
|
||||||
|
while theta < theta_end {
|
||||||
|
let theta_next = f64::min(theta + delta_theta, theta_end);
|
||||||
|
|
||||||
|
let p0 = spiral_point(theta, a, b, spiral_type);
|
||||||
|
let p3 = spiral_point(theta_next, a, b, spiral_type);
|
||||||
|
let t0 = spiral_tangent(theta, a, b, spiral_type);
|
||||||
|
let t1 = spiral_tangent(theta_next, a, b, spiral_type);
|
||||||
|
|
||||||
|
let arc_length = spiral_arc_length(theta, theta_next, a, b, spiral_type);
|
||||||
|
let handle_distance = arc_length / 3.;
|
||||||
|
|
||||||
|
let p1 = p0 + handle_distance * t0;
|
||||||
|
let p2 = p3 - handle_distance * t1;
|
||||||
|
|
||||||
|
anchors.push(Anchor::new(p0, prev_in_handle, Some(p1)));
|
||||||
|
prev_in_handle = Some(p2);
|
||||||
|
|
||||||
|
// If final segment, end with anchor at theta_end
|
||||||
|
if (theta_next - theta_end).abs() < f64::EPSILON {
|
||||||
|
anchors.push(Anchor::new(p3, prev_in_handle, None));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
theta = theta_next;
|
||||||
|
}
|
||||||
|
|
||||||
|
bezpath_from_anchors(&anchors, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate_growth_factor(a: f64, turns: f64, outer_radius: f64, spiral_type: SpiralType) -> f64 {
|
||||||
|
match spiral_type {
|
||||||
|
SpiralType::Archimedean => {
|
||||||
|
let total_theta = turns * TAU;
|
||||||
|
(outer_radius - a) / total_theta
|
||||||
|
}
|
||||||
|
SpiralType::Logarithmic => {
|
||||||
|
let total_theta = turns * TAU;
|
||||||
|
((outer_radius.abs() / a).ln()) / total_theta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a point on the given spiral type at angle `theta`.
|
||||||
|
pub fn spiral_point(theta: f64, a: f64, b: f64, spiral_type: SpiralType) -> DVec2 {
|
||||||
|
match spiral_type {
|
||||||
|
SpiralType::Archimedean => archimedean_spiral_point(theta, a, b),
|
||||||
|
SpiralType::Logarithmic => log_spiral_point(theta, a, b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the tangent direction at angle `theta` for the given spiral type.
|
||||||
|
fn spiral_tangent(theta: f64, a: f64, b: f64, spiral_type: SpiralType) -> DVec2 {
|
||||||
|
match spiral_type {
|
||||||
|
SpiralType::Archimedean => archimedean_spiral_tangent(theta, a, b),
|
||||||
|
SpiralType::Logarithmic => log_spiral_tangent(theta, a, b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes arc length between two angles for the given spiral type.
|
||||||
|
fn spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64, spiral_type: SpiralType) -> f64 {
|
||||||
|
match spiral_type {
|
||||||
|
SpiralType::Archimedean => archimedean_spiral_arc_length(theta_start, theta_end, a, b),
|
||||||
|
SpiralType::Logarithmic => log_spiral_arc_length(theta_start, theta_end, a, b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a point on a logarithmic spiral at angle `theta`.
|
||||||
|
fn log_spiral_point(theta: f64, a: f64, b: f64) -> DVec2 {
|
||||||
|
let r = a * (b * theta).exp(); // a * e^(bθ)
|
||||||
|
DVec2::new(r * theta.cos(), -r * theta.sin())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes arc length along a logarithmic spiral between two angles.
|
||||||
|
fn log_spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64) -> f64 {
|
||||||
|
let factor = (1. + b * b).sqrt();
|
||||||
|
(a / b) * factor * ((b * theta_end).exp() - (b * theta_start).exp())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the tangent direction of a logarithmic spiral at angle `theta`.
|
||||||
|
fn log_spiral_tangent(theta: f64, a: f64, b: f64) -> DVec2 {
|
||||||
|
let r = a * (b * theta).exp();
|
||||||
|
let dx = r * (b * theta.cos() - theta.sin());
|
||||||
|
let dy = r * (b * theta.sin() + theta.cos());
|
||||||
|
|
||||||
|
DVec2::new(dx, -dy).normalize_or(DVec2::X)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a point on an Archimedean spiral at angle `theta`.
|
||||||
|
fn archimedean_spiral_point(theta: f64, a: f64, b: f64) -> DVec2 {
|
||||||
|
let r = a + b * theta;
|
||||||
|
DVec2::new(r * theta.cos(), -r * theta.sin())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the tangent direction of an Archimedean spiral at angle `theta`.
|
||||||
|
fn archimedean_spiral_tangent(theta: f64, a: f64, b: f64) -> DVec2 {
|
||||||
|
let r = a + b * theta;
|
||||||
|
let dx = b * theta.cos() - r * theta.sin();
|
||||||
|
let dy = b * theta.sin() + r * theta.cos();
|
||||||
|
DVec2::new(dx, -dy).normalize_or(DVec2::X)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes arc length along an Archimedean spiral between two angles.
|
||||||
|
fn archimedean_spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64) -> f64 {
|
||||||
|
archimedean_spiral_arc_length_origin(theta_end, a, b) - archimedean_spiral_arc_length_origin(theta_start, a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes arc length from origin to a point on Archimedean spiral at angle `theta`.
|
||||||
|
fn archimedean_spiral_arc_length_origin(theta: f64, a: f64, b: f64) -> f64 {
|
||||||
|
let r = a + b * theta;
|
||||||
|
let sqrt_term = (r * r + b * b).sqrt();
|
||||||
|
(r * sqrt_term + b * b * ((r + sqrt_term).ln())) / (2. * b)
|
||||||
|
}
|
||||||
@@ -150,7 +150,7 @@ mod tests {
|
|||||||
// List of first handle or second point in a cubic bezier curve.
|
// List of first handle or second point in a cubic bezier curve.
|
||||||
let first_handles = solve_spline_first_handle_closed(&points);
|
let first_handles = solve_spline_first_handle_closed(&points);
|
||||||
|
|
||||||
// Construct the Subpath
|
// Construct the subpath
|
||||||
let mut bezpath = BezPath::new();
|
let mut bezpath = BezPath::new();
|
||||||
bezpath.move_to(dvec2_to_point(points[0]));
|
bezpath.move_to(dvec2_to_point(points[0]));
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use super::PointId;
|
use super::PointId;
|
||||||
use super::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
|
use super::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
|
||||||
use crate::subpath::{BezierHandles, ManipulatorGroup};
|
|
||||||
use crate::vector::{SegmentId, Vector};
|
use crate::vector::{SegmentId, Vector};
|
||||||
use core_types::list::{Item, List};
|
use core_types::list::{Item, List};
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::DVec2;
|
use glam::{DAffine2, DVec2};
|
||||||
use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveDeriv, PathSeg, Point, QuadBez};
|
use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveDeriv, PathSeg, Point, QuadBez};
|
||||||
|
use std::fmt::{Debug, Formatter};
|
||||||
use std::ops::Sub;
|
use std::ops::Sub;
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
@@ -246,7 +246,7 @@ pub fn handles_to_segment(start: DVec2, handles: BezierHandles, end: DVec2) -> P
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bezpath_from_manipulator_groups(manipulator_groups: &[ManipulatorGroup<PointId>], closed: bool) -> BezPath {
|
pub fn bezpath_from_manipulator_groups(manipulator_groups: &[ManipulatorGroup], closed: bool) -> BezPath {
|
||||||
let mut bezpath = kurbo::BezPath::new();
|
let mut bezpath = kurbo::BezPath::new();
|
||||||
let mut out_handle;
|
let mut out_handle;
|
||||||
|
|
||||||
@@ -276,8 +276,8 @@ pub fn bezpath_from_manipulator_groups(manipulator_groups: &[ManipulatorGroup<Po
|
|||||||
bezpath
|
bezpath
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bezpath_to_manipulator_groups(bezpath: &BezPath) -> (Vec<ManipulatorGroup<PointId>>, bool) {
|
pub fn bezpath_to_manipulator_groups(bezpath: &BezPath) -> (Vec<ManipulatorGroup>, bool) {
|
||||||
let mut manipulator_groups = Vec::<ManipulatorGroup<PointId>>::new();
|
let mut manipulator_groups = Vec::<ManipulatorGroup>::new();
|
||||||
let mut is_closed = false;
|
let mut is_closed = false;
|
||||||
|
|
||||||
for element in bezpath.elements() {
|
for element in bezpath.elements() {
|
||||||
@@ -653,3 +653,168 @@ graphene_hash::impl_via_hash!(
|
|||||||
SpiralType,
|
SpiralType,
|
||||||
InterpolationDistribution
|
InterpolationDistribution
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Structure used to represent a single anchor with up to two optional associated handles along a path.
|
||||||
|
#[derive(Copy, Clone, PartialEq, graphene_hash::CacheHash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub struct ManipulatorGroup {
|
||||||
|
pub anchor: DVec2,
|
||||||
|
pub in_handle: Option<DVec2>,
|
||||||
|
pub out_handle: Option<DVec2>,
|
||||||
|
pub id: PointId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for ManipulatorGroup {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::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<DVec2>, out_handle: Option<DVec2>) -> Self {
|
||||||
|
let id = PointId::generate();
|
||||||
|
Self { anchor, in_handle, out_handle, id }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<DVec2>, out_handle: Option<DVec2>, id: PointId) -> Self {
|
||||||
|
Self { anchor, in_handle, out_handle, 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Representation of the handle point(s) in a bezier segment.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug, graphene_hash::CacheHash)]
|
||||||
|
#[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 BezierHandles {
|
||||||
|
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<DVec2> {
|
||||||
|
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<DVec2> {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PathSegPoints {
|
||||||
|
pub p0: DVec2,
|
||||||
|
pub p1: Option<DVec2>,
|
||||||
|
pub p2: Option<DVec2>,
|
||||||
|
pub p3: DVec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PathSegPoints {
|
||||||
|
pub fn new(p0: DVec2, p1: Option<DVec2>, p2: Option<DVec2>, 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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use crate::subpath::{BezierHandles, Identifier, ManipulatorGroup, Subpath};
|
use crate::vector::misc::{BezierHandles, HandleId, ManipulatorGroup, Tangent, dvec2_to_point};
|
||||||
use crate::vector::misc::{HandleId, Tangent, dvec2_to_point};
|
|
||||||
use crate::vector::vector_types::Vector;
|
use crate::vector::vector_types::Vector;
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use fixedbitset::FixedBitSet;
|
use fixedbitset::FixedBitSet;
|
||||||
@@ -984,8 +983,8 @@ impl Vector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct a [`Subpath`] from an iterator of segments with (handles, start point, end point) independently of discontinuities.
|
/// Construct a [`kurbo::BezPath`] from an iterator of segments with (handles, start point, end point) independently of discontinuities.
|
||||||
pub fn subpath_from_segments_ignore_discontinuities(&self, segments: impl Iterator<Item = (BezierHandles, usize, usize)>) -> Option<Subpath<PointId>> {
|
pub fn bezpath_from_segments_ignore_discontinuities(&self, segments: impl Iterator<Item = (BezierHandles, usize, usize)>) -> Option<kurbo::BezPath> {
|
||||||
let mut first_point = None;
|
let mut first_point = None;
|
||||||
let mut manipulators_list = Vec::new();
|
let mut manipulators_list = Vec::new();
|
||||||
let mut last: Option<(usize, BezierHandles)> = None;
|
let mut last: Option<(usize, BezierHandles)> = None;
|
||||||
@@ -1018,7 +1017,7 @@ impl Vector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(Subpath::new(manipulators_list, closed))
|
Some(crate::vector::misc::bezpath_from_manipulator_groups(&manipulators_list, closed))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_stroke_path_iter(&self) -> StrokePathIter<'_> {
|
pub fn build_stroke_path_iter(&self) -> StrokePathIter<'_> {
|
||||||
@@ -1036,14 +1035,9 @@ impl Vector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct a [`Subpath`] for each stroke path.
|
/// Construct and return an iterator of `(Vec<ManipulatorGroup>, bool)` for each stroke.
|
||||||
pub fn stroke_bezier_paths(&self) -> impl Iterator<Item = Subpath<PointId>> {
|
|
||||||
self.build_stroke_path_iter().map(|(manipulators_list, closed)| Subpath::new(manipulators_list, closed))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct and return an iterator of Vec of `(ManipulatorGroup<PointId>], bool)` for stroke.
|
|
||||||
/// The boolean in the tuple indicates if the path is closed.
|
/// The boolean in the tuple indicates if the path is closed.
|
||||||
pub fn stroke_manipulator_groups(&self) -> impl Iterator<Item = (Vec<ManipulatorGroup<PointId>>, bool)> {
|
pub fn stroke_manipulator_groups(&self) -> impl Iterator<Item = (Vec<ManipulatorGroup>, bool)> {
|
||||||
self.build_stroke_path_iter()
|
self.build_stroke_path_iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1257,7 +1251,7 @@ pub struct StrokePathIter<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Iterator for StrokePathIter<'_> {
|
impl Iterator for StrokePathIter<'_> {
|
||||||
type Item = (Vec<ManipulatorGroup<PointId>>, bool);
|
type Item = (Vec<ManipulatorGroup>, bool);
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
let mut current_start = None;
|
let mut current_start = None;
|
||||||
@@ -1326,12 +1320,6 @@ impl Iterator for StrokePathIter<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Identifier for PointId {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self::generate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the conversion of IDs used when concatenating vector paths with conflicting IDs.
|
/// Represents the conversion of IDs used when concatenating vector paths with conflicting IDs.
|
||||||
pub struct IdMap {
|
pub struct IdMap {
|
||||||
pub point_offset: usize,
|
pub point_offset: usize,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::subpath::BezierHandles;
|
use crate::vector::misc::BezierHandles;
|
||||||
use crate::vector::misc::{HandleId, HandleType, point_to_dvec2, segment_to_handles};
|
use crate::vector::misc::{HandleId, HandleType, point_to_dvec2, segment_to_handles};
|
||||||
use core_types::uuid::generate_uuid;
|
use core_types::uuid::generate_uuid;
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
@@ -746,7 +746,11 @@ impl<'a> AppendBezpath<'a> {
|
|||||||
let close_path = elements.peek().is_some_and(|elm| **elm == PathEl::ClosePath);
|
let close_path = elements.peek().is_some_and(|elm| **elm == PathEl::ClosePath);
|
||||||
|
|
||||||
match *element {
|
match *element {
|
||||||
PathEl::MoveTo(point) => this.append_first_point(point),
|
PathEl::MoveTo(point) => {
|
||||||
|
// Clear any segment state left by a preceding open contour so its segments don't leak into this contour's region
|
||||||
|
this.reset();
|
||||||
|
this.append_first_point(point);
|
||||||
|
}
|
||||||
PathEl::LineTo(point) => {
|
PathEl::LineTo(point) => {
|
||||||
let handle = BezierHandles::Linear;
|
let handle = BezierHandles::Linear;
|
||||||
if close_path {
|
if close_path {
|
||||||
@@ -814,12 +818,13 @@ impl HandleExt for HandleId {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::subpath::{ManipulatorGroup, Subpath};
|
use crate::vector::algorithms::shapes::{ellipse_bezpath, rectangle_bezpath};
|
||||||
use kurbo::{PathSeg, QuadBez};
|
use kurbo::{PathSeg, QuadBez};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn modify_new() {
|
fn modify_new() {
|
||||||
let vector: Vector = Vector::from_subpaths([Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE), Subpath::new_rectangle(DVec2::NEG_ONE, DVec2::ZERO)], false);
|
let mut vector = Vector::from_bezpath(ellipse_bezpath(DVec2::ZERO, DVec2::ONE));
|
||||||
|
vector.append_bezpath(rectangle_bezpath(DVec2::NEG_ONE, DVec2::ZERO));
|
||||||
|
|
||||||
let modify = VectorModification::create_from_vector(&vector);
|
let modify = VectorModification::create_from_vector(&vector);
|
||||||
|
|
||||||
@@ -830,19 +835,14 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn modify_existing() {
|
fn modify_existing() {
|
||||||
let subpaths = [
|
let mut open_quads = BezPath::new();
|
||||||
Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE),
|
open_quads.move_to(Point::new(0., 0.));
|
||||||
Subpath::new_rectangle(DVec2::NEG_ONE, DVec2::ZERO),
|
open_quads.quad_to(Point::new(5., 10.), Point::new(10., 0.));
|
||||||
Subpath::new(
|
open_quads.quad_to(Point::new(15., 10.), Point::new(20., 0.));
|
||||||
vec![
|
|
||||||
ManipulatorGroup::new(DVec2::new(0., 0.), None, None),
|
let mut vector = Vector::from_bezpath(ellipse_bezpath(DVec2::ZERO, DVec2::ONE));
|
||||||
ManipulatorGroup::new(DVec2::new(10., 0.), Some(DVec2::new(5., 10.)), None),
|
vector.append_bezpath(rectangle_bezpath(DVec2::NEG_ONE, DVec2::ZERO));
|
||||||
ManipulatorGroup::new(DVec2::new(20., 0.), Some(DVec2::new(15., 10.)), None),
|
vector.append_bezpath(open_quads);
|
||||||
],
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
let mut vector: Vector = Vector::from_subpaths(subpaths, false);
|
|
||||||
|
|
||||||
let mut modify_new = VectorModification::create_from_vector(&vector);
|
let mut modify_new = VectorModification::create_from_vector(&vector);
|
||||||
let mut modify_original = VectorModification::default();
|
let mut modify_original = VectorModification::default();
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
use super::misc::dvec2_to_point;
|
use super::misc::dvec2_to_point;
|
||||||
use super::style::{Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
use super::style::{Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
pub use super::vector_attributes::*;
|
pub use super::vector_attributes::*;
|
||||||
use crate::subpath::{BezierHandles, ManipulatorGroup, Subpath};
|
use crate::vector::misc::{BezierHandles, ManipulatorGroup};
|
||||||
use crate::vector::misc::{HandleId, ManipulatorPointId};
|
use crate::vector::misc::{HandleId, ManipulatorPointId};
|
||||||
use crate::vector::vector_modification::VectorExt;
|
use crate::vector::vector_modification::VectorExt;
|
||||||
use core::borrow::Borrow;
|
|
||||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||||
use core_types::render_complexity::RenderComplexity;
|
use core_types::render_complexity::RenderComplexity;
|
||||||
use dyn_any::StaticType;
|
use dyn_any::StaticType;
|
||||||
@@ -74,13 +73,12 @@ impl core_types::transform::BakeTransform for Vector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Vector {
|
impl Vector {
|
||||||
/// Add a subpath to this vector path.
|
/// Add a path of manipulator groups to this vector path.
|
||||||
pub fn append_subpath(&mut self, subpath: impl Borrow<Subpath<PointId>>, preserve_id: bool) {
|
pub fn append_manipulator_groups(&mut self, manipulator_groups: &[ManipulatorGroup], closed: bool, preserve_id: bool) {
|
||||||
let subpath: &Subpath<PointId> = subpath.borrow();
|
|
||||||
let stroke_id = StrokeId::ZERO;
|
let stroke_id = StrokeId::ZERO;
|
||||||
let mut point_id = self.point_domain.next_id();
|
let mut point_id = self.point_domain.next_id();
|
||||||
|
|
||||||
let handles = |a: &ManipulatorGroup<_>, b: &ManipulatorGroup<_>| match (a.out_handle, b.in_handle) {
|
let handles = |a: &ManipulatorGroup, b: &ManipulatorGroup| match (a.out_handle, b.in_handle) {
|
||||||
(None, None) => BezierHandles::Linear,
|
(None, None) => BezierHandles::Linear,
|
||||||
(Some(handle), None) | (None, Some(handle)) => BezierHandles::Quadratic { handle },
|
(Some(handle), None) | (None, Some(handle)) => BezierHandles::Quadratic { handle },
|
||||||
(Some(handle_start), Some(handle_end)) => BezierHandles::Cubic { handle_start, handle_end },
|
(Some(handle_start), Some(handle_end)) => BezierHandles::Cubic { handle_start, handle_end },
|
||||||
@@ -91,7 +89,7 @@ impl Vector {
|
|||||||
let mut first_point = None;
|
let mut first_point = None;
|
||||||
|
|
||||||
// Construct a bezier segment from the two manipulators on the subpath.
|
// Construct a bezier segment from the two manipulators on the subpath.
|
||||||
for pair in subpath.manipulator_groups().windows(2) {
|
for pair in manipulator_groups.windows(2) {
|
||||||
let start = last_point.unwrap_or_else(|| {
|
let start = last_point.unwrap_or_else(|| {
|
||||||
let id = if preserve_id && !self.point_domain.ids().contains(&pair[0].id) {
|
let id = if preserve_id && !self.point_domain.ids().contains(&pair[0].id) {
|
||||||
pair[0].id
|
pair[0].id
|
||||||
@@ -120,8 +118,8 @@ impl Vector {
|
|||||||
|
|
||||||
let fill_id = FillId::ZERO;
|
let fill_id = FillId::ZERO;
|
||||||
|
|
||||||
if subpath.closed() {
|
if closed {
|
||||||
if let (Some(last), Some(first), Some(first_id), Some(last_id)) = (subpath.manipulator_groups().last(), subpath.manipulator_groups().first(), first_point, last_point) {
|
if let (Some(last), Some(first), Some(first_id), Some(last_id)) = (manipulator_groups.last(), manipulator_groups.first(), first_point, last_point) {
|
||||||
let id = segment_id.next_id();
|
let id = segment_id.next_id();
|
||||||
first_seg = Some(first_seg.unwrap_or(id));
|
first_seg = Some(first_seg.unwrap_or(id));
|
||||||
last_seg = Some(id);
|
last_seg = Some(id);
|
||||||
@@ -134,11 +132,6 @@ impl Vector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct some new vector path from a single subpath with an identity transform and black fill.
|
|
||||||
pub fn from_subpath(subpath: impl Borrow<Subpath<PointId>>) -> Self {
|
|
||||||
Self::from_subpaths([subpath], false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct some new vector path from a single [`BezPath`] with an identity transform and black fill.
|
/// Construct some new vector path from a single [`BezPath`] with an identity transform and black fill.
|
||||||
pub fn from_bezpath(bezpath: BezPath) -> Self {
|
pub fn from_bezpath(bezpath: BezPath) -> Self {
|
||||||
let mut vector = Self::default();
|
let mut vector = Self::default();
|
||||||
@@ -146,17 +139,6 @@ impl Vector {
|
|||||||
vector
|
vector
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct some new vector path from subpaths with an identity transform and black fill.
|
|
||||||
pub fn from_subpaths(subpaths: impl IntoIterator<Item = impl Borrow<Subpath<PointId>>>, preserve_id: bool) -> Self {
|
|
||||||
let mut vector = Self::default();
|
|
||||||
|
|
||||||
for subpath in subpaths.into_iter() {
|
|
||||||
vector.append_subpath(subpath, preserve_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
vector
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute the bounding boxes of the bezpaths without any transform
|
/// Compute the bounding boxes of the bezpaths without any transform
|
||||||
pub fn bounding_box_rect(&self) -> Option<Rect> {
|
pub fn bounding_box_rect(&self) -> Option<Rect> {
|
||||||
self.bounding_box_with_transform_rect(DAffine2::IDENTITY)
|
self.bounding_box_with_transform_rect(DAffine2::IDENTITY)
|
||||||
@@ -217,7 +199,7 @@ impl Vector {
|
|||||||
let Some(stroke) = stroke else { return path_bounds };
|
let Some(stroke) = stroke else { return path_bounds };
|
||||||
// Stroke alignment is only honored by the renderer when every subpath is closed; open paths fall
|
// Stroke alignment is only honored by the renderer when every subpath is closed; open paths fall
|
||||||
// back to drawing a Center-aligned `weight`-wide stroke. Match that behavior to keep bounds in sync.
|
// back to drawing a Center-aligned `weight`-wide stroke. Match that behavior to keep bounds in sync.
|
||||||
let aligned_renders = stroke.align != StrokeAlign::Center && self.stroke_bezier_paths().all(|p| p.closed());
|
let aligned_renders = stroke.align != StrokeAlign::Center && self.stroke_bezpath_iter().all(|path| matches!(path.elements().last(), Some(kurbo::PathEl::ClosePath)));
|
||||||
let kurbo_width = if aligned_renders { stroke.effective_width() } else { stroke.weight };
|
let kurbo_width = if aligned_renders { stroke.effective_width() } else { stroke.weight };
|
||||||
// `Inside`-aligned strokes never expand beyond the path bounds; a zero-weight stroke is invisible
|
// `Inside`-aligned strokes never expand beyond the path bounds; a zero-weight stroke is invisible
|
||||||
if kurbo_width <= 0. {
|
if kurbo_width <= 0. {
|
||||||
@@ -519,75 +501,68 @@ impl RenderComplexity for Vector {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::vector::algorithms::shapes::ellipse_bezpath;
|
||||||
use kurbo::{CubicBez, PathSeg, Point};
|
use kurbo::{CubicBez, PathSeg, Point};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn assert_subpath_eq(generated: &[Subpath<PointId>], expected: &[Subpath<PointId>]) {
|
fn open_curve_bezpath() -> BezPath {
|
||||||
assert_eq!(generated.len(), expected.len());
|
let mut bezpath = BezPath::new();
|
||||||
for (generated, expected) in generated.iter().zip(expected) {
|
bezpath.move_to(Point::ZERO);
|
||||||
assert_eq!(generated.manipulator_groups().len(), expected.manipulator_groups().len());
|
bezpath.curve_to(Point::new(-1., -1.), Point::new(1., 1.), Point::new(1., 0.));
|
||||||
assert_eq!(generated.closed(), expected.closed());
|
bezpath
|
||||||
for (generated, expected) in generated.manipulator_groups().iter().zip(expected.manipulator_groups()) {
|
|
||||||
assert_eq!(generated.in_handle, expected.in_handle);
|
|
||||||
assert_eq!(generated.out_handle, expected.out_handle);
|
|
||||||
assert_eq!(generated.anchor, expected.anchor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn construct_closed_subpath() {
|
fn construct_closed_path() {
|
||||||
let circle = Subpath::new_ellipse(DVec2::NEG_ONE, DVec2::ONE);
|
let circle = ellipse_bezpath(DVec2::NEG_ONE, DVec2::ONE);
|
||||||
let vector: Vector = Vector::from_subpath(&circle);
|
let vector = Vector::from_bezpath(circle.clone());
|
||||||
assert_eq!(vector.point_domain.ids().len(), 4);
|
assert_eq!(vector.point_domain.ids().len(), 4);
|
||||||
let bezier_paths = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();
|
|
||||||
assert_eq!(bezier_paths.len(), 4);
|
|
||||||
assert!(bezier_paths.iter().all(|&bezier| circle.iter().any(|original_bezier| original_bezier == bezier)));
|
|
||||||
|
|
||||||
let generated = vector.stroke_bezier_paths().collect::<Vec<_>>();
|
let segments = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();
|
||||||
assert_subpath_eq(&generated, &[circle]);
|
assert_eq!(segments.len(), 4);
|
||||||
|
assert!(segments.iter().all(|&segment| circle.segments().any(|original| original == segment)));
|
||||||
|
|
||||||
|
let generated = vector.stroke_bezpath_iter().collect::<Vec<_>>();
|
||||||
|
assert_eq!(generated.len(), 1);
|
||||||
|
assert_eq!(generated[0].elements(), circle.elements());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn construct_open_subpath() {
|
fn construct_open_path() {
|
||||||
let bezier = PathSeg::Cubic(CubicBez::new(Point::ZERO, Point::new(-1., -1.), Point::new(1., 1.), Point::new(1., 0.)));
|
let curve = open_curve_bezpath();
|
||||||
let subpath = Subpath::new(
|
let vector = Vector::from_bezpath(curve.clone());
|
||||||
vec![
|
|
||||||
ManipulatorGroup::new(DVec2::ZERO, None, Some(DVec2::new(-1., -1.))),
|
|
||||||
ManipulatorGroup::new(DVec2::new(1., 0.), Some(DVec2::new(1., 1.)), None),
|
|
||||||
],
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
let vector: Vector = Vector::from_subpath(&subpath);
|
|
||||||
assert_eq!(vector.point_domain.ids().len(), 2);
|
assert_eq!(vector.point_domain.ids().len(), 2);
|
||||||
let bezier_paths = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();
|
|
||||||
assert_eq!(bezier_paths, vec![bezier]);
|
|
||||||
|
|
||||||
let generated = vector.stroke_bezier_paths().collect::<Vec<_>>();
|
let segments = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();
|
||||||
assert_subpath_eq(&generated, &[subpath]);
|
assert_eq!(segments, vec![PathSeg::Cubic(CubicBez::new(Point::ZERO, Point::new(-1., -1.), Point::new(1., 1.), Point::new(1., 0.)))]);
|
||||||
|
|
||||||
|
let generated = vector.stroke_manipulator_groups().collect::<Vec<_>>();
|
||||||
|
assert_eq!(generated.len(), 1);
|
||||||
|
let (groups, closed) = &generated[0];
|
||||||
|
assert!(!closed);
|
||||||
|
assert_eq!(groups.len(), 2);
|
||||||
|
assert_eq!((groups[0].anchor, groups[0].in_handle, groups[0].out_handle), (DVec2::ZERO, None, Some(DVec2::new(-1., -1.))));
|
||||||
|
assert_eq!((groups[1].anchor, groups[1].in_handle, groups[1].out_handle), (DVec2::new(1., 0.), Some(DVec2::new(1., 1.)), None));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn construct_many_subpath() {
|
fn construct_many_paths() {
|
||||||
let curve = Subpath::new(
|
let curve = open_curve_bezpath();
|
||||||
vec![
|
let circle = ellipse_bezpath(DVec2::NEG_ONE, DVec2::ONE);
|
||||||
ManipulatorGroup::new(DVec2::ZERO, None, Some(DVec2::new(-1., -1.))),
|
|
||||||
ManipulatorGroup::new(DVec2::new(1., 0.), Some(DVec2::new(1., 1.)), None),
|
|
||||||
],
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
let circle = Subpath::new_ellipse(DVec2::NEG_ONE, DVec2::ONE);
|
|
||||||
|
|
||||||
let vector: Vector = Vector::from_subpaths([&curve, &circle], false);
|
let mut vector = Vector::from_bezpath(curve.clone());
|
||||||
|
vector.append_bezpath(circle.clone());
|
||||||
assert_eq!(vector.point_domain.ids().len(), 6);
|
assert_eq!(vector.point_domain.ids().len(), 6);
|
||||||
|
|
||||||
let bezier_paths = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();
|
let segments = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();
|
||||||
assert_eq!(bezier_paths.len(), 5);
|
assert_eq!(segments.len(), 5);
|
||||||
assert!(bezier_paths.iter().all(|&bezier| circle.iter().chain(curve.iter()).any(|original_bezier| original_bezier == bezier)));
|
assert!(segments.iter().all(|&segment| circle.segments().chain(curve.segments()).any(|original| original == segment)));
|
||||||
|
|
||||||
let generated = vector.stroke_bezier_paths().collect::<Vec<_>>();
|
let generated = vector.stroke_bezpath_iter().collect::<Vec<_>>();
|
||||||
assert_subpath_eq(&generated, &[curve, circle]);
|
assert_eq!(generated.len(), 2);
|
||||||
|
assert_eq!(generated[0].elements(), curve.elements());
|
||||||
|
assert_eq!(generated[1].elements(), circle.elements());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verifies the `DVec2 -> List<Vector>` conversion that replaced the former "Vec2 to Point" node yields a path
|
// Verifies the `DVec2 -> List<Vector>` conversion that replaced the former "Vec2 to Point" node yields a path
|
||||||
|
|||||||
@@ -53,10 +53,6 @@ pub mod artboard {
|
|||||||
pub use graphic_types::artboard::*;
|
pub use graphic_types::artboard::*;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod subpath {
|
|
||||||
pub use vector_types::subpath::*;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod gradient {
|
pub mod gradient {
|
||||||
pub use vector_types::{Gradient, GradientStop};
|
pub use vector_types::{Gradient, GradientStop};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
use core_types::list::{ATTR_APPEARANCE, Item, List, NodeIdPath};
|
use core_types::list::{ATTR_APPEARANCE, Item, List, NodeIdPath};
|
||||||
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Ctx};
|
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Ctx};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::DAffine2;
|
||||||
use graphic_types::Appearance;
|
use graphic_types::Appearance;
|
||||||
use graphic_types::graphic::bake_paint_transforms;
|
use graphic_types::graphic::bake_paint_transforms;
|
||||||
use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath};
|
use graphic_types::vector_types::vector::VectorExt;
|
||||||
use graphic_types::vector_types::vector::PointId;
|
|
||||||
use graphic_types::vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
|
use graphic_types::vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
|
||||||
use graphic_types::{Graphic, Vector};
|
use graphic_types::{Graphic, Vector};
|
||||||
use linesweeper::topology::Topology;
|
use linesweeper::topology::Topology;
|
||||||
use linesweeper::{BinaryOp, FillRule, binary_op};
|
use linesweeper::{BinaryOp, FillRule, binary_op};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use vector_types::kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez};
|
use vector_types::kurbo::{Affine, BezPath, CubicBez, Line, ParamCurve, PathEl, PathSeg, Point, QuadBez};
|
||||||
pub use vector_types::vector::misc::BooleanOperation;
|
pub use vector_types::vector::misc::BooleanOperation;
|
||||||
|
|
||||||
// TODO: Fix boolean ops to work by removing .transform() and .one_instance_*() calls,
|
// TODO: Fix boolean ops to work by removing .transform() and .one_instance_*() calls,
|
||||||
@@ -168,8 +167,8 @@ fn boolean_operation_on_vector_list(vector: &List<Vector>, boolean_operation: Bo
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let contours = top.contours(|winding| winding.is_inside(boolean_operation));
|
let contours = top.contours(|winding| winding.is_inside(boolean_operation));
|
||||||
for subpath in from_bez_paths(contours.contours().map(|c| &c.path)) {
|
for contour in contours.contours() {
|
||||||
row.element_mut().append_subpath(subpath, false);
|
row.element_mut().append_bezpath(closed(contour.path.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
list.push(row);
|
list.push(row);
|
||||||
@@ -292,65 +291,36 @@ fn quantize_segment(seg: PathSeg) -> PathSeg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_bez_path(vector: &Vector, transform: DAffine2) -> BezPath {
|
/// Every operand and result region is treated as closed, so an open path gets its closing segment here.
|
||||||
let mut path = BezPath::new();
|
fn closed(mut path: BezPath) -> BezPath {
|
||||||
for subpath in vector.stroke_bezier_paths() {
|
if !path.elements().is_empty() && path.elements().last() != Some(&PathEl::ClosePath) {
|
||||||
push_subpath(&mut path, &subpath, transform);
|
path.close_path();
|
||||||
}
|
}
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_subpath(path: &mut BezPath, subpath: &Subpath<PointId>, transform: DAffine2) {
|
fn to_bez_path(vector: &Vector, transform: DAffine2) -> BezPath {
|
||||||
let transform = Affine::new(transform.to_cols_array());
|
let transform = Affine::new(transform.to_cols_array());
|
||||||
let mut first = true;
|
let mut path = BezPath::new();
|
||||||
|
|
||||||
for seg in subpath.iter_closed() {
|
for subpath in vector.stroke_bezpath_iter() {
|
||||||
let quantized = quantize_segment(transform * seg);
|
let mut first = true;
|
||||||
if first {
|
|
||||||
first = false;
|
for segment in closed(subpath).segments() {
|
||||||
path.move_to(quantized.start());
|
let quantized = quantize_segment(transform * segment);
|
||||||
|
if first {
|
||||||
|
first = false;
|
||||||
|
path.move_to(quantized.start());
|
||||||
|
}
|
||||||
|
path.push(quantized.as_path_el());
|
||||||
}
|
}
|
||||||
path.push(quantized.as_path_el());
|
|
||||||
}
|
|
||||||
path.close_path();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_bez_paths<'a>(paths: impl Iterator<Item = &'a BezPath>) -> Vec<Subpath<PointId>> {
|
if !first {
|
||||||
let mut all_subpaths = Vec::new();
|
path.close_path();
|
||||||
|
|
||||||
for path in paths {
|
|
||||||
let cubics: Vec<CubicBez> = path.segments().map(|segment| segment.to_cubic()).collect();
|
|
||||||
let mut manipulators_list = Vec::new();
|
|
||||||
let mut current_start = None;
|
|
||||||
|
|
||||||
for (index, cubic) in cubics.iter().enumerate() {
|
|
||||||
let d = |p: Point| DVec2::new(p.x, p.y);
|
|
||||||
let [start, handle1, handle2, end] = [d(cubic.p0), d(cubic.p1), d(cubic.p2), d(cubic.p3)];
|
|
||||||
|
|
||||||
if current_start.is_none() {
|
|
||||||
// Use the correct in-handle (None) and out-handle for the start point
|
|
||||||
manipulators_list.push(ManipulatorGroup::new(start, None, Some(handle1)));
|
|
||||||
} else {
|
|
||||||
// Update the out-handle of the previous point
|
|
||||||
if let Some(last) = manipulators_list.last_mut() {
|
|
||||||
last.out_handle = Some(handle1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the end point with the correct in-handle and out-handle (None)
|
|
||||||
manipulators_list.push(ManipulatorGroup::new(end, Some(handle2), None));
|
|
||||||
|
|
||||||
current_start = Some(end);
|
|
||||||
|
|
||||||
// Check if this is the last segment
|
|
||||||
if index == cubics.len() - 1 {
|
|
||||||
all_subpaths.push(Subpath::new(manipulators_list, true));
|
|
||||||
manipulators_list = Vec::new(); // Reset manipulators for the next path
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
all_subpaths
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn boolean_intersect(a: &BezPath, b: &BezPath) -> Vec<BezPath> {
|
pub fn boolean_intersect(a: &BezPath, b: &BezPath) -> Vec<BezPath> {
|
||||||
|
|||||||
@@ -245,7 +245,6 @@ mod test {
|
|||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use vector_nodes::generator_nodes::RectangleNode;
|
use vector_nodes::generator_nodes::RectangleNode;
|
||||||
use vector_types::subpath::Subpath;
|
|
||||||
use vector_types::vector::misc::BoxCorners;
|
use vector_types::vector::misc::BoxCorners;
|
||||||
|
|
||||||
fn vector_node_from_bezpath(bezpath: BezPath) -> List<Vector> {
|
fn vector_node_from_bezpath(bezpath: BezPath) -> List<Vector> {
|
||||||
@@ -294,7 +293,12 @@ mod test {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let positions = [DVec2::new(40., 20.), DVec2::ONE, DVec2::new(-42., 9.), DVec2::new(10., 345.)];
|
let positions = [DVec2::new(40., 20.), DVec2::ONE, DVec2::new(-42., 9.), DVec2::new(10., 345.)];
|
||||||
let points = List::new_from_element(Vector::from_subpath(Subpath::from_anchors(positions, false)));
|
let mut polyline = BezPath::new();
|
||||||
|
polyline.move_to((positions[0].x, positions[0].y));
|
||||||
|
for position in &positions[1..] {
|
||||||
|
polyline.line_to((position.x, position.y));
|
||||||
|
}
|
||||||
|
let points = vector_node_from_bezpath(polyline);
|
||||||
let generated = super::repeat_on_points(context, points, &RaiseToListNode(rect), Item::new_from_element(false)).await;
|
let generated = super::repeat_on_points(context, points, &RaiseToListNode(rect), Item::new_from_element(false)).await;
|
||||||
assert_eq!(generated.len(), positions.len());
|
assert_eq!(generated.len(), positions.len());
|
||||||
for (position, index) in positions.into_iter().zip(0..generated.len()) {
|
for (position, index) in positions.into_iter().zip(0..generated.len()) {
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ use skrifa::instance::{LocationRef, NormalizedCoord, Size};
|
|||||||
use skrifa::outline::{DrawSettings, OutlinePen};
|
use skrifa::outline::{DrawSettings, OutlinePen};
|
||||||
use skrifa::raw::FontRef as ReadFontsRef;
|
use skrifa::raw::FontRef as ReadFontsRef;
|
||||||
use skrifa::{MetadataProvider, OutlineGlyph};
|
use skrifa::{MetadataProvider, OutlineGlyph};
|
||||||
use vector_types::subpath::{ManipulatorGroup, Subpath};
|
use vector_types::kurbo::{Affine, BezPath, Point, Rect, Shape};
|
||||||
use vector_types::vector::{PointId, Vector};
|
use vector_types::vector::{Vector, VectorExt};
|
||||||
|
|
||||||
pub struct PathBuilder {
|
pub struct PathBuilder {
|
||||||
current_subpath: Subpath<PointId>,
|
|
||||||
origin: DVec2,
|
origin: DVec2,
|
||||||
glyph_subpaths: Vec<Subpath<PointId>>,
|
/// Contours of the glyph currently being drawn, accumulated as a single path.
|
||||||
|
glyph_bezpath: BezPath,
|
||||||
pub vector_list: List<Vector>,
|
pub vector_list: List<Vector>,
|
||||||
/// Per-glyph AABBs collected in single-item mode, published as `ATTR_EDITOR_CLICK_TARGET` in `finalize()`.
|
/// Per-glyph AABBs collected in single-item mode, published as `ATTR_EDITOR_CLICK_TARGET` in `finalize()`.
|
||||||
merged_click_target_bboxes: Vec<[DVec2; 2]>,
|
merged_click_target_bboxes: Vec<[DVec2; 2]>,
|
||||||
@@ -27,14 +27,12 @@ pub struct PathBuilder {
|
|||||||
/// `local_transforms` stays stable when all glyphs are clipped during a resize drag.
|
/// `local_transforms` stays stable when all glyphs are clipped during a resize drag.
|
||||||
first_glyph_offset: DVec2,
|
first_glyph_offset: DVec2,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
id: PointId,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PathBuilder {
|
impl PathBuilder {
|
||||||
pub fn new(per_glyph_items: bool, scale: f64, text_frame_size: DVec2, first_glyph_offset: DVec2) -> Self {
|
pub fn new(per_glyph_items: bool, scale: f64, text_frame_size: DVec2, first_glyph_offset: DVec2) -> Self {
|
||||||
Self {
|
Self {
|
||||||
current_subpath: Subpath::new(Vec::new(), false),
|
glyph_bezpath: BezPath::new(),
|
||||||
glyph_subpaths: Vec::new(),
|
|
||||||
vector_list: if per_glyph_items { List::new() } else { List::new_from_element(Vector::default()) },
|
vector_list: if per_glyph_items { List::new() } else { List::new_from_element(Vector::default()) },
|
||||||
merged_click_target_bboxes: Vec::new(),
|
merged_click_target_bboxes: Vec::new(),
|
||||||
merged_click_target_baselines: Vec::new(),
|
merged_click_target_baselines: Vec::new(),
|
||||||
@@ -42,13 +40,12 @@ impl PathBuilder {
|
|||||||
text_frame_size,
|
text_frame_size,
|
||||||
first_glyph_offset,
|
first_glyph_offset,
|
||||||
scale,
|
scale,
|
||||||
id: PointId::ZERO,
|
|
||||||
origin: DVec2::default(),
|
origin: DVec2::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn point(&self, x: f32, y: f32) -> DVec2 {
|
fn point(&self, x: f32, y: f32) -> Point {
|
||||||
DVec2::new(self.origin.x + x as f64, self.origin.y - y as f64) * self.scale
|
Point::new((self.origin.x + x as f64) * self.scale, (self.origin.y - y as f64) * self.scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@@ -65,26 +62,23 @@ impl PathBuilder {
|
|||||||
let location_ref = LocationRef::new(normalized_coords);
|
let location_ref = LocationRef::new(normalized_coords);
|
||||||
let settings = DrawSettings::unhinted(Size::new(size), location_ref);
|
let settings = DrawSettings::unhinted(Size::new(size), location_ref);
|
||||||
glyph.draw(settings, self).unwrap();
|
glyph.draw(settings, self).unwrap();
|
||||||
let has_geometry = !self.glyph_subpaths.is_empty();
|
let has_geometry = !self.glyph_bezpath.is_empty();
|
||||||
|
|
||||||
// Apply transforms in correct order: style-based skew first, then user-requested skew
|
// Apply transforms in correct order: style-based skew first, then user-requested skew
|
||||||
// This ensures font synthesis (italic) is applied before user transformations
|
// This ensures font synthesis (italic) is applied before user transformations
|
||||||
for glyph_subpath in &mut self.glyph_subpaths {
|
if let Some(style_skew) = style_skew {
|
||||||
if let Some(style_skew) = style_skew {
|
self.glyph_bezpath.apply_affine(Affine::new(style_skew.to_cols_array()));
|
||||||
glyph_subpath.apply_transform(style_skew);
|
|
||||||
}
|
|
||||||
|
|
||||||
glyph_subpath.apply_transform(skew);
|
|
||||||
}
|
}
|
||||||
|
self.glyph_bezpath.apply_affine(Affine::new(skew.to_cols_array()));
|
||||||
|
|
||||||
let glyph_bbox = subpaths_bounding_box(&self.glyph_subpaths);
|
let glyph_bbox = bezpath_bounding_box(&self.glyph_bezpath);
|
||||||
|
|
||||||
if per_glyph_items {
|
if per_glyph_items {
|
||||||
// Frame in item-local space: top-left at `-glyph_offset` so the item transform cancels it
|
// Frame in item-local space: top-left at `-glyph_offset` so the item transform cancels it
|
||||||
// back to the layer-local frame origin, regardless of which glyph survived
|
// back to the layer-local frame origin, regardless of which glyph survived
|
||||||
let frame_in_item_local = DAffine2::from_scale_angle_translation(self.text_frame_size, 0., -glyph_offset);
|
let frame_in_item_local = DAffine2::from_scale_angle_translation(self.text_frame_size, 0., -glyph_offset);
|
||||||
|
|
||||||
let item = Item::new_from_element(Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false))
|
let item = Item::new_from_element(Vector::from_bezpath(core::mem::take(&mut self.glyph_bezpath)))
|
||||||
.with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(glyph_offset))
|
.with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(glyph_offset))
|
||||||
.with_attribute(ATTR_EDITOR_TEXT_FRAME, frame_in_item_local);
|
.with_attribute(ATTR_EDITOR_TEXT_FRAME, frame_in_item_local);
|
||||||
self.vector_list.push(item);
|
self.vector_list.push(item);
|
||||||
@@ -92,10 +86,9 @@ impl PathBuilder {
|
|||||||
// Defer click target creation to `finalize()` where adjacent AABBs get widened
|
// Defer click target creation to `finalize()` where adjacent AABBs get widened
|
||||||
self.per_glyph_bboxes.push(glyph_bbox);
|
self.per_glyph_bboxes.push(glyph_bbox);
|
||||||
} else {
|
} else {
|
||||||
for subpath in self.glyph_subpaths.drain(..) {
|
// Unwrapping here is ok because `self.vector_list` is initialized with a single `List<Vector>` item
|
||||||
// Unwrapping here is ok because `self.vector_list` is initialized with a single `List<Vector>` item
|
self.vector_list.element_mut(0).unwrap().append_bezpath(core::mem::take(&mut self.glyph_bezpath));
|
||||||
self.vector_list.element_mut(0).unwrap().append_subpath(subpath, false);
|
|
||||||
}
|
|
||||||
if let Some(bbox) = glyph_bbox {
|
if let Some(bbox) = glyph_bbox {
|
||||||
self.merged_click_target_bboxes.push(bbox);
|
self.merged_click_target_bboxes.push(bbox);
|
||||||
self.merged_click_target_baselines.push(glyph_offset.y);
|
self.merged_click_target_baselines.push(glyph_offset.y);
|
||||||
@@ -196,8 +189,8 @@ impl PathBuilder {
|
|||||||
// Project back to glyph-local and stamp as click targets
|
// Project back to glyph-local and stamp as click targets
|
||||||
for (entry, widened) in entries.iter().zip(layer_bboxes.iter()) {
|
for (entry, widened) in entries.iter().zip(layer_bboxes.iter()) {
|
||||||
let glyph_local = [widened[0] - entry.1, widened[1] - entry.1];
|
let glyph_local = [widened[0] - entry.1, widened[1] - entry.1];
|
||||||
let rect = Subpath::new_rectangle(glyph_local[0], glyph_local[1]);
|
let rect = rectangle_bezpath(glyph_local[0], glyph_local[1]);
|
||||||
self.vector_list.set_attribute(ATTR_EDITOR_CLICK_TARGET, entry.0, Vector::from_subpaths([rect], false));
|
self.vector_list.set_attribute(ATTR_EDITOR_CLICK_TARGET, entry.0, Vector::from_bezpath(rect));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,8 +199,11 @@ impl PathBuilder {
|
|||||||
let mut bboxes = self.merged_click_target_bboxes;
|
let mut bboxes = self.merged_click_target_bboxes;
|
||||||
widen_horizontal_gaps(&mut bboxes, &self.merged_click_target_baselines);
|
widen_horizontal_gaps(&mut bboxes, &self.merged_click_target_baselines);
|
||||||
|
|
||||||
let widened_subpaths: Vec<_> = bboxes.iter().map(|[min, max]| Subpath::new_rectangle(*min, *max)).collect();
|
let mut widened_bezpath = BezPath::new();
|
||||||
self.vector_list.set_attribute(ATTR_EDITOR_CLICK_TARGET, 0, Vector::from_subpaths(widened_subpaths, false));
|
for [min, max] in &bboxes {
|
||||||
|
widened_bezpath.extend(rectangle_bezpath(*min, *max));
|
||||||
|
}
|
||||||
|
self.vector_list.set_attribute(ATTR_EDITOR_CLICK_TARGET, 0, Vector::from_bezpath(widened_bezpath));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill in text frame for items that don't have one yet (single-item mode, where item 0 = identity)
|
// Fill in text frame for items that don't have one yet (single-item mode, where item 0 = identity)
|
||||||
@@ -252,40 +248,37 @@ fn widen_horizontal_gaps(bboxes: &mut [[DVec2; 2]], baselines: &[f64]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subpaths_bounding_box(subpaths: &[Subpath<PointId>]) -> Option<[DVec2; 2]> {
|
fn bezpath_bounding_box(bezpath: &BezPath) -> Option<[DVec2; 2]> {
|
||||||
subpaths
|
if bezpath.is_empty() {
|
||||||
.iter()
|
return None;
|
||||||
.filter_map(|subpath| subpath.bounding_box())
|
}
|
||||||
.reduce(|[a_min, a_max], [b_min, b_max]| [a_min.min(b_min), a_max.max(b_max)])
|
|
||||||
|
let rect = bezpath.bounding_box();
|
||||||
|
Some([DVec2::new(rect.x0, rect.y0), DVec2::new(rect.x1, rect.y1)])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rectangle_bezpath(corner1: DVec2, corner2: DVec2) -> BezPath {
|
||||||
|
Rect::new(corner1.x, corner1.y, corner2.x, corner2.y).to_path(0.)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutlinePen for PathBuilder {
|
impl OutlinePen for PathBuilder {
|
||||||
fn move_to(&mut self, x: f32, y: f32) {
|
fn move_to(&mut self, x: f32, y: f32) {
|
||||||
if !self.current_subpath.is_empty() {
|
self.glyph_bezpath.move_to(self.point(x, y));
|
||||||
self.glyph_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
|
|
||||||
}
|
|
||||||
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.point(x, y), self.id.next_id()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn line_to(&mut self, x: f32, y: f32) {
|
fn line_to(&mut self, x: f32, y: f32) {
|
||||||
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.point(x, y), self.id.next_id()));
|
self.glyph_bezpath.line_to(self.point(x, y));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn quad_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) {
|
fn quad_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) {
|
||||||
let [handle, anchor] = [self.point(x1, y1), self.point(x2, y2)];
|
self.glyph_bezpath.quad_to(self.point(x1, y1), self.point(x2, y2));
|
||||||
self.current_subpath.last_manipulator_group_mut().unwrap().out_handle = Some(handle);
|
|
||||||
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_with_id(anchor, None, None, self.id.next_id()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) {
|
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) {
|
||||||
let [handle1, handle2, anchor] = [self.point(x1, y1), self.point(x2, y2), self.point(x3, y3)];
|
self.glyph_bezpath.curve_to(self.point(x1, y1), self.point(x2, y2), self.point(x3, y3));
|
||||||
self.current_subpath.last_manipulator_group_mut().unwrap().out_handle = Some(handle1);
|
|
||||||
self.current_subpath
|
|
||||||
.push_manipulator_group(ManipulatorGroup::new_with_id(anchor, Some(handle2), None, self.id.next_id()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn close(&mut self) {
|
fn close(&mut self) {
|
||||||
self.current_subpath.set_closed(true);
|
self.glyph_bezpath.close_path();
|
||||||
self.glyph_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ use core_types::{CacheHash, Ctx};
|
|||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use graphic_types::Vector;
|
use graphic_types::Vector;
|
||||||
use vector_types::subpath;
|
use vector_types::vector::VectorExt;
|
||||||
|
use vector_types::vector::algorithms::shapes;
|
||||||
|
use vector_types::vector::misc::BezierHandles;
|
||||||
use vector_types::vector::misc::{ArcType, AsU64, BoxCorners, GridType};
|
use vector_types::vector::misc::{ArcType, AsU64, BoxCorners, GridType};
|
||||||
use vector_types::vector::misc::{HandleId, SpiralType};
|
use vector_types::vector::misc::{HandleId, SpiralType};
|
||||||
use vector_types::vector::{PointId, SegmentId, StrokeId};
|
use vector_types::vector::{PointId, SegmentId, StrokeId};
|
||||||
@@ -19,7 +21,7 @@ fn circle(
|
|||||||
radius: Item<f64>,
|
radius: Item<f64>,
|
||||||
) -> Item<Vector> {
|
) -> Item<Vector> {
|
||||||
let radius = radius.element().abs();
|
let radius = radius.element().abs();
|
||||||
Item::new_from_element(Vector::from_subpath(subpath::Subpath::new_ellipse(DVec2::splat(-radius), DVec2::splat(radius))))
|
Item::new_from_element(Vector::from_bezpath(shapes::ellipse_bezpath(DVec2::splat(-radius), DVec2::splat(radius))))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates an arc shape forming a portion of a circle which may be open, closed, or a pie slice.
|
/// Generates an arc shape forming a portion of a circle which may be open, closed, or a pie slice.
|
||||||
@@ -38,7 +40,7 @@ fn arc(
|
|||||||
arc_type: Item<ArcType>,
|
arc_type: Item<ArcType>,
|
||||||
) -> Item<Vector> {
|
) -> Item<Vector> {
|
||||||
let (radius, start_angle, sweep_angle, arc_type) = (*radius.element(), *start_angle.element(), *sweep_angle.element(), arc_type.into_element());
|
let (radius, start_angle, sweep_angle, arc_type) = (*radius.element(), *start_angle.element(), *sweep_angle.element(), arc_type.into_element());
|
||||||
Item::new_from_element(Vector::from_subpath(subpath::Subpath::new_arc(
|
Item::new_from_element(Vector::from_bezpath(shapes::arc_bezpath(
|
||||||
radius,
|
radius,
|
||||||
start_angle / 360. * std::f64::consts::TAU,
|
start_angle / 360. * std::f64::consts::TAU,
|
||||||
sweep_angle / 360. * std::f64::consts::TAU,
|
sweep_angle / 360. * std::f64::consts::TAU,
|
||||||
@@ -65,7 +67,7 @@ fn spiral(
|
|||||||
*outer_radius.element(),
|
*outer_radius.element(),
|
||||||
*angular_resolution.element(),
|
*angular_resolution.element(),
|
||||||
);
|
);
|
||||||
Item::new_from_element(Vector::from_subpath(subpath::Subpath::new_spiral(
|
Item::new_from_element(Vector::from_bezpath(shapes::spiral_bezpath(
|
||||||
inner_radius,
|
inner_radius,
|
||||||
outer_radius,
|
outer_radius,
|
||||||
turns,
|
turns,
|
||||||
@@ -91,7 +93,7 @@ fn ellipse(
|
|||||||
let corner1 = -radius;
|
let corner1 = -radius;
|
||||||
let corner2 = radius;
|
let corner2 = radius;
|
||||||
|
|
||||||
let mut ellipse = Vector::from_subpath(subpath::Subpath::new_ellipse(corner1, corner2));
|
let mut ellipse = Vector::from_bezpath(shapes::ellipse_bezpath(corner1, corner2));
|
||||||
|
|
||||||
let len = ellipse.segment_domain.ids().len();
|
let len = ellipse.segment_domain.ids().len();
|
||||||
for i in 0..len {
|
for i in 0..len {
|
||||||
@@ -139,7 +141,7 @@ fn rectangle(
|
|||||||
radii
|
radii
|
||||||
};
|
};
|
||||||
|
|
||||||
Item::new_from_element(Vector::from_subpath(subpath::Subpath::new_rounded_rectangle(size / -2., size / 2., radii)))
|
Item::new_from_element(Vector::from_bezpath(shapes::rounded_rectangle_bezpath(size / -2., size / 2., radii)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a set of four corner values, such as a rectangle's corner radii, from a list of one, two, three, or four values.
|
/// Builds a set of four corner values, such as a rectangle's corner radii, from a list of one, two, three, or four values.
|
||||||
@@ -167,8 +169,7 @@ fn regular_polygon<T: AsU64>(
|
|||||||
radius: Item<f64>,
|
radius: Item<f64>,
|
||||||
) -> Item<Vector> {
|
) -> Item<Vector> {
|
||||||
let points = sides.element().as_u64();
|
let points = sides.element().as_u64();
|
||||||
let radius: f64 = *radius.element() * 2.;
|
Item::new_from_element(Vector::from_bezpath(shapes::regular_polygon_bezpath(DVec2::ZERO, points, *radius.element())))
|
||||||
Item::new_from_element(Vector::from_subpath(subpath::Subpath::new_regular_polygon(DVec2::splat(-radius), points, radius)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates an n-pointed star shape with inner and outer points at chosen radii from the center.
|
/// Generates an n-pointed star shape with inner and outer points at chosen radii from the center.
|
||||||
@@ -188,10 +189,7 @@ fn star<T: AsU64>(
|
|||||||
radius_2: Item<f64>,
|
radius_2: Item<f64>,
|
||||||
) -> Item<Vector> {
|
) -> Item<Vector> {
|
||||||
let points = sides.element().as_u64();
|
let points = sides.element().as_u64();
|
||||||
let diameter: f64 = *radius_1.element() * 2.;
|
Item::new_from_element(Vector::from_bezpath(shapes::star_polygon_bezpath(DVec2::ZERO, points, *radius_1.element(), *radius_2.element())))
|
||||||
let inner_diameter = *radius_2.element() * 2.;
|
|
||||||
|
|
||||||
Item::new_from_element(Vector::from_subpath(subpath::Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
@@ -249,11 +247,7 @@ fn qr_code(
|
|||||||
for x in 0..dimension {
|
for x in 0..dimension {
|
||||||
if qr_code.get_module(x as i32, y as i32) {
|
if qr_code.get_module(x as i32, y as i32) {
|
||||||
let corner1 = DVec2::new(x as f64, y as f64);
|
let corner1 = DVec2::new(x as f64, y as f64);
|
||||||
let corner2 = corner1 + DVec2::splat(1.);
|
vector.append_bezpath(shapes::rectangle_bezpath(corner1, corner1 + DVec2::splat(1.)));
|
||||||
vector.append_subpath(
|
|
||||||
subpath::Subpath::from_anchors([corner1, DVec2::new(corner2.x, corner1.y), corner2, DVec2::new(corner1.x, corner2.y)], true),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,12 +275,12 @@ fn arrow(
|
|||||||
#[default(20)] head_length: Item<PixelLength>,
|
#[default(20)] head_length: Item<PixelLength>,
|
||||||
) -> Item<Vector> {
|
) -> Item<Vector> {
|
||||||
let (arrow_to, shaft_width, head_width, head_length) = (*arrow_to.element(), *shaft_width.element(), *head_width.element(), *head_length.element());
|
let (arrow_to, shaft_width, head_width, head_length) = (*arrow_to.element(), *shaft_width.element(), *head_width.element(), *head_length.element());
|
||||||
Item::new_from_element(Vector::from_subpath(subpath::Subpath::new_arrow(DVec2::ZERO, arrow_to, shaft_width, head_width, head_length)))
|
Item::new_from_element(Vector::from_bezpath(shapes::arrow_bezpath(DVec2::ZERO, arrow_to, shaft_width, head_width, head_length)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Vector: Shape"))]
|
#[node_macro::node(category("Vector: Shape"))]
|
||||||
fn line(_: impl Ctx, _primary: (), #[default(100., 100.)] line_to: Item<PixelSize>) -> Item<Vector> {
|
fn line(_: impl Ctx, _primary: (), #[default(100., 100.)] line_to: Item<PixelSize>) -> Item<Vector> {
|
||||||
Item::new_from_element(Vector::from_subpath(subpath::Subpath::new_line(DVec2::ZERO, *line_to.element())))
|
Item::new_from_element(Vector::from_bezpath(shapes::line_bezpath(DVec2::ZERO, *line_to.element())))
|
||||||
}
|
}
|
||||||
|
|
||||||
trait GridSpacing {
|
trait GridSpacing {
|
||||||
@@ -370,9 +364,7 @@ fn grid<T: GridSpacing>(
|
|||||||
// Helper function to connect points with line segments.
|
// Helper function to connect points with line segments.
|
||||||
let mut push_segment = |to_index: Option<usize>| {
|
let mut push_segment = |to_index: Option<usize>| {
|
||||||
if let Some(other_index) = to_index {
|
if let Some(other_index) = to_index {
|
||||||
vector
|
vector.segment_domain.push(segment_id.next_id(), other_index, current_index, BezierHandles::Linear, StrokeId::ZERO);
|
||||||
.segment_domain
|
|
||||||
.push(segment_id.next_id(), other_index, current_index, subpath::BezierHandles::Linear, StrokeId::ZERO);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use graphic_types::Vector;
|
use graphic_types::Vector;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use vector_types::subpath;
|
use vector_types::vector::VectorExt;
|
||||||
|
use vector_types::vector::algorithms::shapes;
|
||||||
|
|
||||||
pub fn merge_qr_squares(qr_code: &qrcodegen::QrCode) -> Vector {
|
pub fn merge_qr_squares(qr_code: &qrcodegen::QrCode) -> Vector {
|
||||||
let mut vector = Vector::default();
|
let mut vector = Vector::default();
|
||||||
@@ -106,7 +107,7 @@ pub fn merge_qr_squares(qr_code: &qrcodegen::QrCode) -> Vector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !simplified.is_empty() {
|
if !simplified.is_empty() {
|
||||||
vector.append_subpath(subpath::Subpath::from_anchors(simplified, true), false);
|
vector.append_bezpath(shapes::polyline_bezpath(simplified, true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,14 +23,15 @@ use std::collections::hash_map::DefaultHasher;
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use vector_types::GradientForm;
|
use vector_types::GradientForm;
|
||||||
use vector_types::gradient::{build_transform_with_y_preservation, initial_gradient_transform_for_bounding_box};
|
use vector_types::gradient::{build_transform_with_y_preservation, initial_gradient_transform_for_bounding_box};
|
||||||
use vector_types::subpath::{BezierHandles, ManipulatorGroup};
|
use vector_types::vector::algorithms::bezpath_algorithms::{
|
||||||
use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, eval_pathseg_euclidean, evaluate_bezpath, split_bezpath, tangent_on_bezpath};
|
self, TValue, bezpath_area_centroid_and_area, bezpath_length_centroid_and_length, eval_pathseg_euclidean, evaluate_bezpath, split_bezpath, tangent_on_bezpath,
|
||||||
|
};
|
||||||
use vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
|
use vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
|
||||||
use vector_types::vector::algorithms::offset_subpath::offset_bezpath;
|
use vector_types::vector::algorithms::offset_subpath::offset_bezpath;
|
||||||
use vector_types::vector::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
|
use vector_types::vector::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
|
||||||
use vector_types::vector::misc::{
|
use vector_types::vector::misc::{
|
||||||
CentroidType, ExtrudeJoiningAlgorithm, HandleId, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, bezpath_from_manipulator_groups,
|
BezierHandles, CentroidType, ExtrudeJoiningAlgorithm, HandleId, InterpolationDistribution, ManipulatorGroup, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns,
|
||||||
bezpath_to_manipulator_groups, handles_to_segment, is_linear, point_to_dvec2, segment_to_handles,
|
bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, handles_to_segment, is_linear, point_to_dvec2, segment_to_handles,
|
||||||
};
|
};
|
||||||
use vector_types::vector::style::{DashPattern, Gradient, GradientSettings, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
use vector_types::vector::style::{DashPattern, Gradient, GradientSettings, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt};
|
use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt};
|
||||||
@@ -692,11 +693,11 @@ fn merge_by_distance<V: MapVectorItems + Send + Sync + 'static>(
|
|||||||
pub mod extrude_algorithms {
|
pub mod extrude_algorithms {
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use kurbo::{ParamCurve, ParamCurveDeriv};
|
use kurbo::{ParamCurve, ParamCurveDeriv};
|
||||||
use vector_types::subpath::BezierHandles;
|
|
||||||
use vector_types::vector::StrokeId;
|
use vector_types::vector::StrokeId;
|
||||||
|
use vector_types::vector::misc::BezierHandles;
|
||||||
use vector_types::vector::misc::ExtrudeJoiningAlgorithm;
|
use vector_types::vector::misc::ExtrudeJoiningAlgorithm;
|
||||||
|
|
||||||
/// Convert [`kurbo::CubicBez`] to [`vector_types::subpath::BezierHandles`].
|
/// Convert [`kurbo::CubicBez`] to [`vector_types::vector::misc::BezierHandles`].
|
||||||
fn cubic_to_handles(cubic_bez: kurbo::CubicBez) -> BezierHandles {
|
fn cubic_to_handles(cubic_bez: kurbo::CubicBez) -> BezierHandles {
|
||||||
BezierHandles::Cubic {
|
BezierHandles::Cubic {
|
||||||
handle_start: DVec2::new(cubic_bez.p1.x, cubic_bez.p1.y),
|
handle_start: DVec2::new(cubic_bez.p1.x, cubic_bez.p1.y),
|
||||||
@@ -1114,20 +1115,22 @@ async fn auto_tangents<V: MapVectorItems + 'n + Send>(
|
|||||||
|
|
||||||
let mut result = Vector::default();
|
let mut result = Vector::default();
|
||||||
|
|
||||||
for mut subpath in source.stroke_bezier_paths() {
|
for (mut manipulators_list, is_closed) in source.stroke_manipulator_groups() {
|
||||||
subpath.apply_transform(transform);
|
for manipulator in &mut manipulators_list {
|
||||||
|
manipulator.anchor = transform.transform_point2(manipulator.anchor);
|
||||||
|
manipulator.in_handle = manipulator.in_handle.map(|handle| transform.transform_point2(handle));
|
||||||
|
manipulator.out_handle = manipulator.out_handle.map(|handle| transform.transform_point2(handle));
|
||||||
|
}
|
||||||
|
|
||||||
let manipulators_list = subpath.manipulator_groups();
|
|
||||||
if manipulators_list.len() < 2 {
|
if manipulators_list.len() < 2 {
|
||||||
// Not enough points for softening or handle removal
|
// Not enough points for softening or handle removal
|
||||||
result.append_subpath(subpath, true);
|
result.append_manipulator_groups(&manipulators_list, is_closed, true);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut new_manipulators_list = Vec::with_capacity(manipulators_list.len());
|
let mut new_manipulators_list = Vec::with_capacity(manipulators_list.len());
|
||||||
// Track which manipulator indices were given auto-tangent (colinear) handles
|
// Track which manipulator indices were given auto-tangent (colinear) handles
|
||||||
let mut auto_tangented = vec![false; manipulators_list.len()];
|
let mut auto_tangented = vec![false; manipulators_list.len()];
|
||||||
let is_closed = subpath.closed();
|
|
||||||
|
|
||||||
for i in 0..manipulators_list.len() {
|
for i in 0..manipulators_list.len() {
|
||||||
let current = &manipulators_list[i];
|
let current = &manipulators_list[i];
|
||||||
@@ -2514,7 +2517,7 @@ async fn morph(
|
|||||||
|
|
||||||
/// Subdivides the last segment of a manipulator group list at its midpoint, adding one new manipulator.
|
/// Subdivides the last segment of a manipulator group list at its midpoint, adding one new manipulator.
|
||||||
/// For closed paths, the "last segment" is the closing segment from the last back to the first manipulator.
|
/// For closed paths, the "last segment" is the closing segment from the last back to the first manipulator.
|
||||||
fn subdivide_last_manipulator_segment(manips: &mut Vec<ManipulatorGroup<PointId>>, closed: bool) {
|
fn subdivide_last_manipulator_segment(manips: &mut Vec<ManipulatorGroup>, closed: bool) {
|
||||||
let len = manips.len();
|
let len = manips.len();
|
||||||
if len < 2 {
|
if len < 2 {
|
||||||
return;
|
return;
|
||||||
@@ -2562,7 +2565,7 @@ async fn morph(
|
|||||||
|
|
||||||
/// Pushes a subpath (list of manipulators) directly into a Vector's point, segment, and region domains,
|
/// Pushes a subpath (list of manipulators) directly into a Vector's point, segment, and region domains,
|
||||||
/// bypassing the BezPath intermediate representation used by `append_bezpath`.
|
/// bypassing the BezPath intermediate representation used by `append_bezpath`.
|
||||||
fn push_manipulators_to_vector(vector: &mut Vector, manips: &[ManipulatorGroup<PointId>], closed: bool, point_id: &mut PointId, segment_id: &mut SegmentId) {
|
fn push_manipulators_to_vector(vector: &mut Vector, manips: &[ManipulatorGroup], closed: bool, point_id: &mut PointId, segment_id: &mut SegmentId) {
|
||||||
let Some(first) = manips.first() else { return };
|
let Some(first) = manips.first() else { return };
|
||||||
|
|
||||||
let first_point_index = vector.point_domain.ids().len();
|
let first_point_index = vector.point_domain.ids().len();
|
||||||
@@ -3047,7 +3050,7 @@ async fn morph(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build interpolated manipulator groups
|
// Build interpolated manipulator groups
|
||||||
let mut interpolated: Vec<ManipulatorGroup<PointId>> = source_manips
|
let mut interpolated: Vec<ManipulatorGroup> = source_manips
|
||||||
.iter()
|
.iter()
|
||||||
.zip(target_manips.iter())
|
.zip(target_manips.iter())
|
||||||
.map(|(s, t)| ManipulatorGroup {
|
.map(|(s, t)| ManipulatorGroup {
|
||||||
@@ -3552,14 +3555,14 @@ fn element_centroid(element: &Vector, transform: DAffine2, centroid_type: Centro
|
|||||||
let mut centroid = DVec2::ZERO;
|
let mut centroid = DVec2::ZERO;
|
||||||
let mut sum = 0.;
|
let mut sum = 0.;
|
||||||
|
|
||||||
for subpath in element.stroke_bezier_paths() {
|
for bezpath in element.stroke_bezpath_iter() {
|
||||||
let partial = match centroid_type {
|
let partial = match centroid_type {
|
||||||
CentroidType::Area => subpath.area_centroid_and_area(Some(1e-3), Some(1e-3)).filter(|(_, area)| *area > 0.),
|
CentroidType::Area => bezpath_area_centroid_and_area(&bezpath, Some(1e-3), Some(1e-3)).filter(|(_, area)| *area > 0.),
|
||||||
CentroidType::Length => subpath.length_centroid_and_length(None, true),
|
CentroidType::Length => bezpath_length_centroid_and_length(&bezpath, None, true),
|
||||||
};
|
};
|
||||||
if let Some((subpath_centroid, area_or_length)) = partial {
|
if let Some((path_centroid, area_or_length)) = partial {
|
||||||
sum += area_or_length;
|
sum += area_or_length;
|
||||||
centroid += area_or_length * transform.transform_point2(subpath_centroid);
|
centroid += area_or_length * transform.transform_point2(path_centroid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3671,11 +3674,11 @@ mod test {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Rectangle and Ellipse generators define the framework's fill winding convention; each is built from these
|
// The Rectangle and Ellipse generators define the framework's fill winding convention, built from these
|
||||||
// subpath constructors (`Subpath::new_rectangle` / `Subpath::new_ellipse`), so their winding is the source of truth.
|
// shape constructors (`rectangle_bezpath` / `ellipse_bezpath`), so their winding is the source of truth.
|
||||||
use vector_types::subpath::Subpath;
|
use vector_types::vector::algorithms::shapes::{ellipse_bezpath, rectangle_bezpath};
|
||||||
let rectangle = Vector::from_subpath(Subpath::new_rectangle(DVec2::new(-50., -50.), DVec2::new(50., 50.)));
|
let rectangle = Vector::from_bezpath(rectangle_bezpath(DVec2::new(-50., -50.), DVec2::new(50., 50.)));
|
||||||
let ellipse = Vector::from_subpath(Subpath::new_ellipse(DVec2::new(-50., -25.), DVec2::new(50., 25.)));
|
let ellipse = Vector::from_bezpath(ellipse_bezpath(DVec2::new(-50., -25.), DVec2::new(50., 25.)));
|
||||||
let expected = subpath_winding_signs(&rectangle)[0];
|
let expected = subpath_winding_signs(&rectangle)[0];
|
||||||
assert_eq!(subpath_winding_signs(&ellipse)[0], expected, "Rectangle and Ellipse should agree on winding");
|
assert_eq!(subpath_winding_signs(&ellipse)[0], expected, "Rectangle and Ellipse should agree on winding");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user