mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-01 17:18:11 +08:00
Added Subpaths to bezier-rs (#730)
* Added Subpath constructor, iterator and length * Asserted that subpaths of len < 2 cannot be closed. Added len() and comments * Added subpath to_svg(), made structs public, made quadratic use either in_handle or out_handle * add bezier handles * Added basic interactivity and index traits * Added SubPath interactivity * Added svg styling * Broke subpath impl across multiple files. More sylistic changes per review * Fixed format error * Added closed subpath to documentation page * Modified subpath to_svg to use functional style * Stylistic changes per review * Fixed build errors * More sylistic changes per review * Moved svg commands to constants * Moved formatting for svg arguments to ToSVGOptions * Renamed files in git * Even more stylistic changes per review
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
use super::*;
|
||||
use crate::consts::*;
|
||||
|
||||
/// Functionality relating to core `Subpath` operations, such as constructors and `iter`.
|
||||
impl Subpath {
|
||||
/// Create a new `Subpath` using a list of [ManipulatorGroup]s.
|
||||
/// A `Subpath` with less than 2 [ManipulatorGroup]s may not be closed.
|
||||
pub fn new(manipulator_groups: Vec<ManipulatorGroup>, closed: bool) -> Subpath {
|
||||
assert!(!closed || manipulator_groups.len() > 1, "A closed Subpath must contain more than 1 ManipulatorGroup.");
|
||||
Subpath { manipulator_groups, closed }
|
||||
}
|
||||
|
||||
/// Create a `Subpath` consisting of 2 manipulator groups from a `Bezier`.
|
||||
pub fn from_bezier(bezier: Bezier) -> Self {
|
||||
Subpath::new(
|
||||
vec![
|
||||
ManipulatorGroup {
|
||||
anchor: bezier.start(),
|
||||
in_handle: None,
|
||||
out_handle: bezier.handle_start(),
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: bezier.end(),
|
||||
in_handle: bezier.handle_end(),
|
||||
out_handle: None,
|
||||
},
|
||||
],
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
SubpathIter { sub_path: self, index: 0 }
|
||||
}
|
||||
|
||||
/// Returns an SVG representation of the `Subpath`.
|
||||
pub fn to_svg(&self, options: ToSVGOptions) -> String {
|
||||
if self.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let curve_start_argument = format!("{SVG_ARG_MOVE}{} {}", self[0].anchor.x, self[0].anchor.y);
|
||||
let mut curve_arguments: Vec<String> = self.iter().map(|bezier| bezier.svg_curve_argument()).collect();
|
||||
if self.closed {
|
||||
curve_arguments.push(String::from(SVG_ARG_CLOSED));
|
||||
}
|
||||
|
||||
let anchor_arguments = options.formatted_anchor_arguments();
|
||||
let anchor_circles = self
|
||||
.manipulator_groups
|
||||
.iter()
|
||||
.map(|point| format!(r#"<circle cx="{}" cy="{}" {}/>"#, point.anchor.x, point.anchor.y, anchor_arguments))
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let handle_point_arguments = options.formatted_handle_point_arguments();
|
||||
let handle_circles: Vec<String> = self
|
||||
.manipulator_groups
|
||||
.iter()
|
||||
.flat_map(|group| [group.in_handle, group.out_handle])
|
||||
.flatten()
|
||||
.map(|handle| format!(r#"<circle cx="{}" cy="{}" {}/>"#, handle.x, handle.y, handle_point_arguments))
|
||||
.collect();
|
||||
|
||||
let handle_pieces: Vec<String> = self.iter().filter_map(|bezier| bezier.svg_handle_line_argument()).collect();
|
||||
|
||||
format!(
|
||||
r#"<path d="{} {}" {}/><path d="{}" {}/>{}{}"#,
|
||||
curve_start_argument,
|
||||
curve_arguments.join(" "),
|
||||
options.formatted_curve_arguments(),
|
||||
handle_pieces.join(" "),
|
||||
options.formatted_handle_line_arguments(),
|
||||
handle_circles.join(""),
|
||||
anchor_circles.join(""),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
use super::*;
|
||||
|
||||
/// Functionality relating to looking up properties of the `Subpath` or points along the `Subpath`.
|
||||
impl Subpath {
|
||||
/// Return the sum of the approximation of the length of each `Bezier` curve along the `Subpath`.
|
||||
/// - `num_subdivisions` - Number of subdivisions used to approximate the curve. The default value is `1000`.
|
||||
pub fn length(&self, num_subdivisions: Option<i32>) -> f64 {
|
||||
self.iter().fold(0., |accumulator, bezier| accumulator + bezier.length(num_subdivisions))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Bezier;
|
||||
use glam::DVec2;
|
||||
|
||||
#[test]
|
||||
fn length_quadratic() {
|
||||
let start = DVec2::new(20., 30.);
|
||||
let middle = DVec2::new(80., 90.);
|
||||
let end = DVec2::new(60., 45.);
|
||||
let handle1 = DVec2::new(75., 85.);
|
||||
let handle2 = DVec2::new(40., 30.);
|
||||
let handle3 = DVec2::new(10., 10.);
|
||||
|
||||
let bezier1 = Bezier::from_quadratic_dvec2(start, handle1, middle);
|
||||
let bezier2 = Bezier::from_quadratic_dvec2(middle, handle2, end);
|
||||
let bezier3 = Bezier::from_quadratic_dvec2(end, handle3, start);
|
||||
|
||||
let mut subpath = Subpath::new(
|
||||
vec![
|
||||
ManipulatorGroup {
|
||||
anchor: start,
|
||||
in_handle: None,
|
||||
out_handle: Some(handle1),
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: middle,
|
||||
in_handle: None,
|
||||
out_handle: Some(handle2),
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: end,
|
||||
in_handle: None,
|
||||
out_handle: Some(handle3),
|
||||
},
|
||||
],
|
||||
false,
|
||||
);
|
||||
assert_eq!(subpath.length(None), bezier1.length(None) + bezier2.length(None));
|
||||
|
||||
subpath.closed = true;
|
||||
assert_eq!(subpath.length(None), bezier1.length(None) + bezier2.length(None) + bezier3.length(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn length_mixed() {
|
||||
let start = DVec2::new(20., 30.);
|
||||
let middle = DVec2::new(70., 70.);
|
||||
let end = DVec2::new(60., 45.);
|
||||
let handle1 = DVec2::new(75., 85.);
|
||||
let handle2 = DVec2::new(40., 30.);
|
||||
let handle3 = DVec2::new(10., 10.);
|
||||
|
||||
let linear_bezier = Bezier::from_linear_dvec2(start, middle);
|
||||
let quadratic_bezier = Bezier::from_quadratic_dvec2(middle, handle1, end);
|
||||
let cubic_bezier = Bezier::from_cubic_dvec2(end, handle2, handle3, start);
|
||||
|
||||
let mut subpath = Subpath::new(
|
||||
vec![
|
||||
ManipulatorGroup {
|
||||
anchor: start,
|
||||
in_handle: Some(handle3),
|
||||
out_handle: None,
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: middle,
|
||||
in_handle: None,
|
||||
out_handle: Some(handle1),
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: end,
|
||||
in_handle: None,
|
||||
out_handle: Some(handle2),
|
||||
},
|
||||
],
|
||||
false,
|
||||
);
|
||||
assert_eq!(subpath.length(None), linear_bezier.length(None) + quadratic_bezier.length(None));
|
||||
|
||||
subpath.closed = true;
|
||||
assert_eq!(subpath.length(None), linear_bezier.length(None) + quadratic_bezier.length(None) + cubic_bezier.length(None));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
mod core;
|
||||
mod lookup;
|
||||
mod structs;
|
||||
pub use structs::*;
|
||||
|
||||
use crate::Bezier;
|
||||
|
||||
use std::ops::{Index, IndexMut};
|
||||
|
||||
/// Structure used to represent a path composed of [Bezier] curves.
|
||||
pub struct Subpath {
|
||||
manipulator_groups: Vec<ManipulatorGroup>,
|
||||
closed: bool,
|
||||
}
|
||||
|
||||
/// Iteration structure for iterating across each curve of a `Subpath`, using an intermediate `Bezier` representation.
|
||||
pub struct SubpathIter<'a> {
|
||||
index: usize,
|
||||
sub_path: &'a Subpath,
|
||||
}
|
||||
|
||||
impl Index<usize> for Subpath {
|
||||
type Output = ManipulatorGroup;
|
||||
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
assert!(index < self.len(), "Index out of bounds in trait Index of SubPath.");
|
||||
&self.manipulator_groups[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<usize> for Subpath {
|
||||
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
|
||||
assert!(index < self.len(), "Index out of bounds in trait IndexMut of SubPath.");
|
||||
&mut self.manipulator_groups[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SubpathIter<'_> {
|
||||
type Item = Bezier;
|
||||
|
||||
// Returns the Bezier representation of each `Subpath` segment, defined between a pair of adjacent manipulator points.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let len = self.sub_path.len() - 1
|
||||
+ match self.sub_path.closed {
|
||||
true => 1,
|
||||
false => 0,
|
||||
};
|
||||
if self.index >= len {
|
||||
return None;
|
||||
}
|
||||
let start_index = self.index;
|
||||
let end_index = (self.index + 1) % self.sub_path.len();
|
||||
self.index += 1;
|
||||
|
||||
let start = self.sub_path[start_index].anchor;
|
||||
let end = self.sub_path[end_index].anchor;
|
||||
let out_handle = self.sub_path[start_index].out_handle;
|
||||
let in_handle = self.sub_path[end_index].in_handle;
|
||||
|
||||
if let (Some(handle1), Some(handle2)) = (out_handle, in_handle) {
|
||||
Some(Bezier::from_cubic_dvec2(start, handle1, handle2, end))
|
||||
} else if let Some(handle) = out_handle.or(in_handle) {
|
||||
Some(Bezier::from_quadratic_dvec2(start, handle, end))
|
||||
} else {
|
||||
Some(Bezier::from_linear_dvec2(start, end))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
use glam::DVec2;
|
||||
|
||||
/// Structure used to represent a single anchor with up to two optional associated handles along a `Subpath`
|
||||
pub struct ManipulatorGroup {
|
||||
pub anchor: DVec2,
|
||||
pub in_handle: Option<DVec2>,
|
||||
pub out_handle: Option<DVec2>,
|
||||
}
|
||||
|
||||
/// Structure to represent optional parameters that can be passed to the `into_svg` function.
|
||||
pub struct ToSVGOptions {
|
||||
/// Color of the line segments along the `Subpath`. Defaulted to `black`.
|
||||
pub curve_stroke_color: String,
|
||||
/// Width of the line segments along the `Subpath`. Defaulted to `2.`.
|
||||
pub curve_stroke_width: f64,
|
||||
/// Stroke color outlining circles marking anchors on the `Subpath`. Defaulted to `black`.
|
||||
pub anchor_stroke_color: String,
|
||||
/// Stroke width outlining circles marking anchors on the `Subpath`. Defaulted to `2.`.
|
||||
pub anchor_stroke_width: f64,
|
||||
/// Radius of the circles marking anchors on the `Subpath`. Defaulted to `4.`.
|
||||
pub anchor_radius: f64,
|
||||
/// Fill color of the circles marking anchors on the `Subpath`. Defaulted to `white`.
|
||||
pub anchor_fill: String,
|
||||
/// Color of the line segments connecting anchors to handle points. Defaulted to `gray`.
|
||||
pub handle_line_stroke_color: String,
|
||||
/// Width of the line segments connecting anchors to handle points. Defaulted to `1.`.
|
||||
pub handle_line_stroke_width: f64,
|
||||
/// Stroke color outlining circles marking the handles of `Subpath`. Defaulted to `gray`.
|
||||
pub handle_point_stroke_color: String,
|
||||
/// Stroke color outlining circles marking the handles of `Subpath`. Defaulted to `1.5`.
|
||||
pub handle_point_stroke_width: f64,
|
||||
/// Radius of the circles marking the handles of `Subpath`. Defaulted to `3.`.
|
||||
pub handle_point_radius: f64,
|
||||
/// Fill color of the circles marking the handles of `Subpath`. Defaulted to `white`.
|
||||
pub handle_point_fill: String,
|
||||
}
|
||||
|
||||
impl ToSVGOptions {
|
||||
/// Combine and format curve styling options for an SVG path.
|
||||
pub(crate) fn formatted_curve_arguments(&self) -> String {
|
||||
format!(r#"stroke="{}" stroke-width="{}" fill="none""#, self.curve_stroke_color, self.curve_stroke_width)
|
||||
}
|
||||
|
||||
/// Combine and format anchor styling options an SVG circle.
|
||||
pub(crate) fn formatted_anchor_arguments(&self) -> String {
|
||||
format!(
|
||||
r#"r="{}", stroke="{}" stroke-width="{}" fill="{}""#,
|
||||
self.anchor_radius, self.anchor_stroke_color, self.anchor_stroke_width, self.anchor_fill
|
||||
)
|
||||
}
|
||||
|
||||
/// Combine and format handle point styling options for an SVG circle.
|
||||
pub(crate) fn formatted_handle_point_arguments(&self) -> String {
|
||||
format!(
|
||||
r#"r="{}", stroke="{}" stroke-width="{}" fill="{}""#,
|
||||
self.handle_point_radius, self.handle_point_stroke_color, self.handle_point_stroke_width, self.handle_point_fill
|
||||
)
|
||||
}
|
||||
|
||||
/// Combine and format handle line styling options an SVG path.
|
||||
pub(crate) fn formatted_handle_line_arguments(&self) -> String {
|
||||
format!(r#"stroke="{}" stroke-width="{}" fill="none""#, self.handle_line_stroke_color, self.handle_line_stroke_width)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ToSVGOptions {
|
||||
fn default() -> Self {
|
||||
ToSVGOptions {
|
||||
curve_stroke_color: String::from("black"),
|
||||
curve_stroke_width: 2.,
|
||||
anchor_stroke_color: String::from("black"),
|
||||
anchor_stroke_width: 2.,
|
||||
anchor_radius: 4.,
|
||||
anchor_fill: String::from("white"),
|
||||
handle_line_stroke_color: String::from("gray"),
|
||||
handle_line_stroke_width: 1.,
|
||||
handle_point_stroke_color: String::from("gray"),
|
||||
handle_point_stroke_width: 1.5,
|
||||
handle_point_radius: 3.,
|
||||
handle_point_fill: String::from("white"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user