mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-22 02:58:11 +08:00
Add point insertion to the Path tool (#754)
* Messaging cleanup * Add bezier iter * Add splitting * Use bezier_rs bounding box * Cleanup * Fix comments * Fix typo * Code review tweaks Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
d8b4055bcb
commit
3775aedab1
@@ -815,6 +815,24 @@ impl Document {
|
||||
}
|
||||
Some([update_thumbnails_upstream(&layer_path), vec![DocumentChanged, LayerChanged { path: layer_path }]].concat())
|
||||
}
|
||||
Operation::SetManipulatorPoints {
|
||||
layer_path,
|
||||
id,
|
||||
manipulator_type,
|
||||
position,
|
||||
} => {
|
||||
if let Ok(Some(shape)) = self.layer_mut(&layer_path).map(|layer| layer.as_subpath_mut()) {
|
||||
if let Some(manipulator_group) = shape.manipulator_groups_mut().by_id_mut(id) {
|
||||
if let Some(position) = position {
|
||||
manipulator_group.set_point_position(manipulator_type as usize, position.into());
|
||||
} else {
|
||||
manipulator_group.points[manipulator_type] = None;
|
||||
}
|
||||
self.mark_as_dirty(&layer_path)?;
|
||||
}
|
||||
}
|
||||
Some([update_thumbnails_upstream(&layer_path), vec![DocumentChanged, LayerChanged { path: layer_path }]].concat())
|
||||
}
|
||||
Operation::RemoveManipulatorPoint {
|
||||
layer_path,
|
||||
id,
|
||||
|
||||
@@ -29,8 +29,7 @@ impl LayerData for ShapeLayer {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: RenderData) {
|
||||
let mut subpath = self.shape.clone();
|
||||
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = subpath.bounding_box();
|
||||
let layer_bounds = [(x0, y0).into(), (x1, y1).into()];
|
||||
let layer_bounds = subpath.bounding_box().unwrap_or_default();
|
||||
|
||||
let transform = self.transform(transforms, render_data.view_mode);
|
||||
let inverse = transform.inverse();
|
||||
@@ -40,8 +39,7 @@ impl LayerData for ShapeLayer {
|
||||
}
|
||||
subpath.apply_affine(transform);
|
||||
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = subpath.bounding_box();
|
||||
let transformed_bounds = [(x0, y0).into(), (x1, y1).into()];
|
||||
let transformed_bounds = subpath.bounding_box().unwrap_or_default();
|
||||
|
||||
let _ = writeln!(svg, r#"<g transform="matrix("#);
|
||||
inverse.to_cols_array().iter().enumerate().for_each(|(i, entry)| {
|
||||
@@ -64,8 +62,7 @@ impl LayerData for ShapeLayer {
|
||||
}
|
||||
subpath.apply_affine(transform);
|
||||
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = subpath.bounding_box();
|
||||
Some([(x0, y0).into(), (x1, y1).into()])
|
||||
subpath.bounding_box()
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, _font_cache: &FontCache) {
|
||||
|
||||
@@ -70,13 +70,11 @@ impl LayerData for TextLayer {
|
||||
|
||||
let mut path = self.to_subpath(buzz_face);
|
||||
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
|
||||
let bounds = [(x0, y0).into(), (x1, y1).into()];
|
||||
let bounds = path.bounding_box().unwrap_or_default();
|
||||
|
||||
path.apply_affine(transform);
|
||||
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
|
||||
let transformed_bounds = [(x0, y0).into(), (x1, y1).into()];
|
||||
let transformed_bounds = path.bounding_box().unwrap_or_default();
|
||||
|
||||
let _ = write!(
|
||||
svg,
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::layers::id_vec::IdBackedVec;
|
||||
use crate::layers::layer_info::{Layer, LayerDataType};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use kurbo::{BezPath, PathEl, Rect, Shape};
|
||||
use kurbo::{BezPath, PathEl, Shape};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// [Subpath] represents a single vector path, containing many [ManipulatorGroups].
|
||||
@@ -334,15 +334,14 @@ impl Subpath {
|
||||
&mut self.0
|
||||
}
|
||||
|
||||
// ** INTERFACE WITH KURBO **
|
||||
|
||||
// TODO Implement our own a local bounding box calculation
|
||||
/// Return the bounding box of the shape
|
||||
pub fn bounding_box(&self) -> Rect {
|
||||
<&Self as Into<BezPath>>::into(self).bounding_box()
|
||||
/// Return the bounding box of the shape.
|
||||
pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
|
||||
self.bezier_iter()
|
||||
.map(|bezier| bezier.internal.bounding_box())
|
||||
.reduce(|[a_min, a_max], [b_min, b_max]| [a_min.min(b_min), a_max.max(b_max)])
|
||||
}
|
||||
|
||||
/// Use kurbo to convert this shape into an SVG path
|
||||
/// Generate an SVG `path` elements's `d` attribute: `<path d="...">`.
|
||||
pub fn to_svg(&mut self) -> String {
|
||||
fn write_positions(result: &mut String, values: [Option<DVec2>; 3]) {
|
||||
use std::fmt::Write;
|
||||
@@ -404,6 +403,20 @@ impl Subpath {
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Convert to an iter over [`bezier_rs::Bezier`] segments.
|
||||
pub fn bezier_iter(&self) -> PathIter {
|
||||
PathIter {
|
||||
path: self.manipulator_groups().enumerate(),
|
||||
last_anchor: None,
|
||||
last_out_handle: None,
|
||||
last_id: None,
|
||||
first_in_handle: None,
|
||||
first_anchor: None,
|
||||
first_id: None,
|
||||
start_new_contour: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ** CONVERSIONS **
|
||||
@@ -434,6 +447,101 @@ impl<'a> TryFrom<&'a Layer> for &'a Subpath {
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around [`bezier_rs::Bezier`] containing also the IDs for the [`ManipulatorGroup`]s where the points are from.
|
||||
pub struct BezierId {
|
||||
/// The internal [`bezier_rs::Bezier`].
|
||||
pub internal: bezier_rs::Bezier,
|
||||
/// The ID of the [ManipulatorGroup] of the start point and, if cubic, the start handle.
|
||||
pub start: u64,
|
||||
/// The ID of the [ManipulatorGroup] of the end point and, if cubic, the end handle.
|
||||
pub end: u64,
|
||||
/// The ID of the [ManipulatorGroup] of the handle on a quadratic (if applicable).
|
||||
pub mid: Option<u64>,
|
||||
}
|
||||
|
||||
impl BezierId {
|
||||
/// Construct a `BezierId` by encapsulating a [`bezier_rs::Bezier`] and providing the IDs of the [`ManipulatorGroup`]s the constituent points belong to.
|
||||
fn new(internal: bezier_rs::Bezier, start: u64, end: u64, mid: Option<u64>) -> Self {
|
||||
Self { internal, start, end, mid }
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over [`bezier_rs::Bezier`] segments constructable via [`Subpath::bezier_iter`].
|
||||
pub struct PathIter<'a> {
|
||||
path: std::iter::Zip<core::slice::Iter<'a, u64>, core::slice::Iter<'a, ManipulatorGroup>>,
|
||||
|
||||
last_anchor: Option<DVec2>,
|
||||
last_out_handle: Option<DVec2>,
|
||||
last_id: Option<u64>,
|
||||
|
||||
first_in_handle: Option<DVec2>,
|
||||
first_anchor: Option<DVec2>,
|
||||
first_id: Option<u64>,
|
||||
|
||||
start_new_contour: bool,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for PathIter<'a> {
|
||||
type Item = BezierId;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
use bezier_rs::Bezier;
|
||||
|
||||
let mut result = None;
|
||||
|
||||
while result.is_none() {
|
||||
let (&id, manipulator_group) = self.path.next()?;
|
||||
|
||||
let in_handle = manipulator_group.points[ManipulatorType::InHandle].as_ref().map(|point| point.position);
|
||||
let anchor = manipulator_group.points[ManipulatorType::Anchor].as_ref().map(|point| point.position);
|
||||
let out_handle = manipulator_group.points[ManipulatorType::OutHandle].as_ref().map(|point| point.position);
|
||||
|
||||
let mut start_new_contour = false;
|
||||
|
||||
// Move to
|
||||
if anchor.is_some() && self.start_new_contour {
|
||||
// Update the last moveto position
|
||||
(self.first_in_handle, self.first_anchor) = (in_handle, anchor);
|
||||
self.first_id = Some(id);
|
||||
}
|
||||
// Cubic to
|
||||
else if let (Some(p1), Some(p2), Some(p3), Some(p4), Some(last_id)) = (self.last_anchor, self.last_out_handle, in_handle, anchor, self.last_id) {
|
||||
result = Some(BezierId::new(Bezier::from_cubic_dvec2(p1, p2, p3, p4), last_id, id, None));
|
||||
}
|
||||
// Quadratic to
|
||||
else if let (Some(p1), Some(p2), Some(p3), Some(last_id)) = (self.last_anchor, self.last_out_handle.or(in_handle), anchor, self.last_id) {
|
||||
let mid = if self.last_out_handle.is_some() { last_id } else { id };
|
||||
result = Some(BezierId::new(Bezier::from_quadratic_dvec2(p1, p2, p3), last_id, id, Some(mid)));
|
||||
}
|
||||
// Line to
|
||||
else if let (Some(p1), Some(p2), Some(last_id)) = (self.last_anchor, anchor, self.last_id) {
|
||||
result = Some(BezierId::new(Bezier::from_linear_dvec2(p1, p2), last_id, id, None));
|
||||
}
|
||||
// Close path
|
||||
else if in_handle.is_none() && anchor.is_none() {
|
||||
start_new_contour = true;
|
||||
if let (Some(last_id), Some(first_id)) = (self.last_id, self.first_id) {
|
||||
// Complete the last curve
|
||||
if let (Some(p1), Some(p2), Some(p3), Some(p4)) = (self.last_anchor, self.last_out_handle, self.first_in_handle, self.first_anchor) {
|
||||
result = Some(BezierId::new(Bezier::from_cubic_dvec2(p1, p2, p3, p4), last_id, first_id, None));
|
||||
} else if let (Some(p1), Some(p2), Some(p3)) = (self.last_anchor, self.last_out_handle.or(self.first_in_handle), self.first_anchor) {
|
||||
let mid = if self.last_out_handle.is_some() { last_id } else { first_id };
|
||||
result = Some(BezierId::new(Bezier::from_quadratic_dvec2(p1, p2, p3), last_id, first_id, Some(mid)));
|
||||
} else if let (Some(p1), Some(p2)) = (self.last_anchor, self.first_anchor) {
|
||||
result = Some(BezierId::new(Bezier::from_linear_dvec2(p1, p2), last_id, first_id, None));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.start_new_contour = start_new_contour;
|
||||
self.last_out_handle = out_handle;
|
||||
self.last_anchor = anchor;
|
||||
self.last_id = Some(id);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Subpath> for BezPath {
|
||||
/// Create a [BezPath] from a [Subpath].
|
||||
fn from(subpath: &Subpath) -> Self {
|
||||
|
||||
@@ -130,6 +130,12 @@ pub enum Operation {
|
||||
manipulator_type: ManipulatorType,
|
||||
position: (f64, f64),
|
||||
},
|
||||
SetManipulatorPoints {
|
||||
layer_path: Vec<LayerId>,
|
||||
id: u64,
|
||||
manipulator_type: ManipulatorType,
|
||||
position: Option<(f64, f64)>,
|
||||
},
|
||||
RenameLayer {
|
||||
layer_path: Vec<LayerId>,
|
||||
new_name: String,
|
||||
|
||||
Reference in New Issue
Block a user