mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
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:
parent
639a24d8ad
commit
959e790cdf
64 changed files with 2639 additions and 1552 deletions
|
@ -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 {
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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.;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue