Migrate vector data and tools to use nodes (#1065)

* Add rendering to vector nodes

* Add line, shape, rectange and freehand tool

* Fix transforms, strokes and fills

* Migrate spline tool

* Remove blank lines

* Fix test

* Fix fill in properties

* Select layers when filling

* Properties panel transform around pivot

* Fix select tool outlines

* Select tool modifies node graph pivot

* Add the pivot assist to the properties

* Improve setting non existant fill UX

* Cleanup hash function

* Path and pen tools

* Bug fixes

* Disable boolean ops

* Fix default handle smoothing on ellipses

* Fix test and warnings

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube 2023-03-26 08:03:51 +01:00 committed by Keavon Chambers
parent 639a24d8ad
commit 959e790cdf
64 changed files with 2639 additions and 1552 deletions

View file

@ -123,6 +123,16 @@ impl Bezier {
format!("{handle_args} {} {}", self.end.x, self.end.y)
}
/// Write the curve argument to the string
pub fn write_curve_argument(&self, svg: &mut String) -> std::fmt::Result {
match self.handles {
BezierHandles::Linear => svg.push_str(SVG_ARG_LINEAR),
BezierHandles::Quadratic { handle } => write!(svg, "{SVG_ARG_QUADRATIC}{},{}", handle.x, handle.y)?,
BezierHandles::Cubic { handle_start, handle_end } => write!(svg, "{SVG_ARG_CUBIC}{},{} {},{}", handle_start.x, handle_start.y, handle_end.x, handle_end.y)?,
}
write!(svg, " {},{}", self.end.x, self.end.y)
}
/// Return the string argument used to create the lines connecting handles to endpoints in an SVG `path`
pub(crate) fn svg_handle_line_argument(&self) -> Option<String> {
match self.handles {

View file

@ -1,5 +1,5 @@
use super::*;
use crate::utils::TValue;
use crate::utils::{solve_cubic, solve_quadratic, TValue};
use glam::DMat2;
use std::ops::Range;
@ -403,6 +403,105 @@ impl Bezier {
let handle2 = other.start - other.non_normalized_tangent(0.) / 3.;
Bezier::from_cubic_dvec2(self.end, handle1, handle2, other.start)
}
/// Compute the winding order (number of times crossing an infinate line to the left of the point)
///
/// Assumes curve is split at the extrema.
fn pre_split_winding_number(&self, target_point: DVec2) -> i32 {
// Clockwise is -1, anticlockwise is +1 (with +y as up)
// Looking only to the left (-x) of the target_point
let resulting_sign = if self.end.y > self.start.y {
if target_point.y < self.start.y || target_point.y >= self.end.y {
return 0;
}
-1
} else if self.end.y < self.start.y {
if target_point.y < self.end.y || target_point.y >= self.start.y {
return 0;
}
1
} else {
return 0;
};
match &self.handles {
BezierHandles::Linear => {
if target_point.x < self.start.x.min(self.end.x) {
return 0;
}
if target_point.x >= self.start.x.max(self.end.x) {
return resulting_sign;
}
// line equation ax + by = c
let a = self.end.y - self.start.y;
let b = self.start.x - self.end.x;
let c = a * self.start.x + b * self.start.y;
if (a * target_point.x + b * target_point.y - c) * (resulting_sign as f64) <= 0.0 {
resulting_sign
} else {
0
}
}
BezierHandles::Quadratic { handle: p1 } => {
if target_point.x < self.start.x.min(self.end.x).min(p1.x) {
return 0;
}
if target_point.x >= self.start.x.max(self.end.x).max(p1.x) {
return resulting_sign;
}
let a = self.end.y - 2.0 * p1.y + self.start.y;
let b = 2.0 * (p1.y - self.start.y);
let c = self.start.y - target_point.y;
let discriminant = b * b - 4. * a * c;
let two_times_a = 2. * a;
for t in solve_quadratic(discriminant, two_times_a, b, c) {
if (0.0..=1.0).contains(&t) {
let x = self.evaluate(TValue::Parametric(t)).x;
if target_point.x >= x {
return resulting_sign;
} else {
return 0;
}
}
}
0
}
BezierHandles::Cubic { handle_start: p1, handle_end: p2 } => {
if target_point.x < self.start.x.min(self.end.x).min(p1.x).min(p2.x) {
return 0;
}
if target_point.x >= self.start.x.max(self.end.x).max(p1.x).max(p2.x) {
return resulting_sign;
}
let a = self.end.y - 3.0 * p2.y + 3.0 * p1.y - self.start.y;
let b = 3.0 * (p2.y - 2.0 * p1.y + self.start.y);
let c = 3.0 * (p1.y - self.start.y);
let d = self.start.y - target_point.y;
for t in solve_cubic(a, b, c, d) {
if (0.0..=1.0).contains(&t) {
let x = self.evaluate(TValue::Parametric(t)).x;
if target_point.x >= x {
return resulting_sign;
} else {
return 0;
}
}
}
0
}
}
}
/// Compute the winding number contribution of a single segment.
///
/// Cast a ray to the left and count intersections.
pub fn winding(&self, target_point: DVec2) -> i32 {
let extrema = self.get_extrema_t_list();
extrema
.windows(2)
.map(|t| self.trim(TValue::Parametric(t[0]), TValue::Parametric(t[1])).pre_split_winding_number(target_point))
.sum()
}
}
#[cfg(test)]

View file

@ -104,7 +104,7 @@ impl Bezier {
}
/// Returns a Bezier curve that results from applying the transformation function to each point in the Bezier.
pub fn apply_transformation(&self, transformation_function: &dyn Fn(DVec2) -> DVec2) -> Bezier {
pub fn apply_transformation(&self, transformation_function: impl Fn(DVec2) -> DVec2) -> Bezier {
let transformed_start = transformation_function(self.start);
let transformed_end = transformation_function(self.end);
match self.handles {
@ -125,18 +125,18 @@ impl Bezier {
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#bezier/rotate/solo" title="Rotate Demo"></iframe>
pub fn rotate(&self, angle: f64) -> Bezier {
let rotation_matrix = DMat2::from_angle(angle);
self.apply_transformation(&|point| rotation_matrix.mul_vec2(point))
self.apply_transformation(|point| rotation_matrix.mul_vec2(point))
}
/// Returns a Bezier curve that results from rotating the curve around the provided point by the given angle (in radians).
pub fn rotate_about_point(&self, angle: f64, pivot: DVec2) -> Bezier {
let rotation_matrix = DMat2::from_angle(angle);
self.apply_transformation(&|point| rotation_matrix.mul_vec2(point - pivot) + pivot)
self.apply_transformation(|point| rotation_matrix.mul_vec2(point - pivot) + pivot)
}
/// Returns a Bezier curve that results from translating the curve by the given `DVec2`.
pub fn translate(&self, translation: DVec2) -> Bezier {
self.apply_transformation(&|point| point + translation)
self.apply_transformation(|point| point + translation)
}
/// Determine if it is possible to scale the given curve, using the following conditions:
@ -163,7 +163,7 @@ impl Bezier {
}
/// Add the bezier endpoints if not already present, and combine and sort the dimensional extrema.
fn get_extrema_t_list(&self) -> Vec<f64> {
pub(crate) fn get_extrema_t_list(&self) -> Vec<f64> {
let mut extrema = self.local_extrema().into_iter().flatten().collect::<Vec<f64>>();
extrema.append(&mut vec![0., 1.]);
extrema.dedup();
@ -274,7 +274,7 @@ impl Bezier {
};
let should_flip_direction = (self.start - intersection).normalize().abs_diff_eq(normal_start, MAX_ABSOLUTE_DIFFERENCE);
intermediate.apply_transformation(&|point| {
intermediate.apply_transformation(|point| {
let mut direction_unit_vector = (intersection - point).normalize();
if should_flip_direction {
direction_unit_vector *= -1.;

View file

@ -123,6 +123,20 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
let _ = write!(svg, r#"<path d="{} {}" {attributes}/>"#, curve_start_argument, curve_arguments.join(" "));
}
/// Write the curve argument to the string (the d="..." part)
pub fn subpath_to_svg(&self, svg: &mut String, transform: glam::DAffine2) -> std::fmt::Result {
let start = transform.transform_point2(self[0].anchor);
write!(svg, "{SVG_ARG_MOVE}{},{}", start.x, start.y)?;
for bezier in self.iter() {
bezier.apply_transformation(|pos| transform.transform_point2(pos)).write_curve_argument(svg)?;
svg.push(' ');
}
if self.closed {
svg.push_str(SVG_ARG_CLOSED);
}
Ok(())
}
/// Appends to the `svg` mutable string with an SVG shape representation of the handle lines.
pub fn handle_lines_to_svg(&self, svg: &mut String, attributes: String) {
let handle_lines: Vec<String> = self.iter().filter_map(|bezier| bezier.svg_handle_line_argument()).collect();
@ -178,7 +192,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
Self::from_anchors([corner1, DVec2::new(corner2.x, corner1.y), corner2, DVec2::new(corner1.x, corner2.y)], true)
}
/// Constructs an elipse with `corner1` and `corner2` as the two corners of the bounding box.
/// 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.;
@ -192,10 +206,10 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
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)),
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)
}

View file

@ -9,6 +9,36 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
self.closed
}
/// Set if the subpath is closed.
pub fn set_closed(&mut self, new_closed: bool) {
self.closed = new_closed;
}
/// Access a [ManipulatorGroup] from a [ManipulatorGroupId].
pub fn manipulator_from_id(&self, id: ManipulatorGroupId) -> Option<&ManipulatorGroup<ManipulatorGroupId>> {
self.manipulator_groups.iter().find(|manipulator_group| manipulator_group.id == id)
}
/// Access a mutable [ManipulatorGroup] from a [ManipulatorGroupId].
pub fn manipulator_mut_from_id(&mut self, id: ManipulatorGroupId) -> Option<&mut ManipulatorGroup<ManipulatorGroupId>> {
self.manipulator_groups.iter_mut().find(|manipulator_group| manipulator_group.id == id)
}
/// Access the index of a [ManipulatorGroup] from a [ManipulatorGroupId].
pub fn manipulator_index_from_id(&self, id: ManipulatorGroupId) -> Option<usize> {
self.manipulator_groups.iter().position(|manipulator_group| manipulator_group.id == id)
}
/// Insert a manipulator group at an index
pub fn insert_manipulator_group(&mut self, index: usize, group: ManipulatorGroup<ManipulatorGroupId>) {
self.manipulator_groups.insert(index, group)
}
/// Remove a manipulator group at an index
pub fn remove_manipulator_group(&mut self, index: usize) -> ManipulatorGroup<ManipulatorGroupId> {
self.manipulator_groups.remove(index)
}
/// Inserts a `ManipulatorGroup` at a certain point along the subpath based on the parametric `t`-value provided.
/// Expects `t` to be within the inclusive range `[0, 1]`.
pub fn insert(&mut self, t: SubpathTValue) {

View file

@ -103,6 +103,13 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
self.iter().map(|bezier| bezier.bounding_box()).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| bezier.apply_transformation(&|v| transform.transform_point2(v)).bounding_box())
.reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])])
}
/// Returns list of `t`-values representing the inflection points of the subpath.
/// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`.
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/inflections/solo" title="Inflections Demo"></iframe>
@ -123,6 +130,11 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
// TODO: Consider the shared point between adjacent beziers.
inflection_t_values
}
/// Does a path contain a point? Based on the non zero winding
pub fn contains_point(&self, target_point: DVec2) -> bool {
self.iter().map(|bezier| bezier.winding(target_point)).sum::<i32>() != 0
}
}
#[cfg(test)]