mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
Bézier-rs: Add utils to subpath (#1058)
* Add utils to bezier-rs subpath * Apply code review changes * Remove tan from constant * Fix compile * Fix tests
This commit is contained in:
parent
7254c008f9
commit
66ec85a3c9
12 changed files with 206 additions and 10 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -281,7 +281,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
|||
name = "bezier-rs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dyn-any",
|
||||
"glam",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -281,7 +281,7 @@ impl NodeGraphMessageHandler {
|
|||
node_id: link_start,
|
||||
output_index: link_start_index,
|
||||
// TODO: add ui for lambdas
|
||||
lambda,
|
||||
lambda: _,
|
||||
} = *input
|
||||
{
|
||||
Some(FrontendNodeLink {
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::messages::prelude::*;
|
|||
|
||||
use document_legacy::{document::pick_safe_imaginate_resolution, layers::layer_info::LayerDataType};
|
||||
use document_legacy::{LayerId, Operation};
|
||||
use graph_craft::document::{generate_uuid, value::TaggedValue, NodeId, NodeInput, NodeNetwork, NodeOutput};
|
||||
use graph_craft::document::{generate_uuid, NodeId, NodeInput, NodeNetwork, NodeOutput};
|
||||
use graph_craft::executor::Compiler;
|
||||
use graphene_core::raster::{Image, ImageFrame};
|
||||
use interpreted_executor::executor::DynamicExecutor;
|
||||
|
|
|
@ -15,3 +15,6 @@ documentation = "https://graphite.rs/bezier-rs-demos/"
|
|||
|
||||
[dependencies]
|
||||
glam = { version = "0.22", features = ["serde"] }
|
||||
|
||||
dyn-any = { path = "../dyn-any", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
|
|
|
@ -15,6 +15,7 @@ use std::fmt::{Debug, Formatter, Result};
|
|||
|
||||
/// Representation of the handle point(s) in a bezier segment.
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
enum BezierHandles {
|
||||
Linear,
|
||||
/// Handles for a quadratic curve.
|
||||
|
@ -31,8 +32,14 @@ enum BezierHandles {
|
|||
},
|
||||
}
|
||||
|
||||
#[cfg(feature = "dyn-any")]
|
||||
impl dyn_any::StaticType for BezierHandles {
|
||||
type Static = BezierHandles;
|
||||
}
|
||||
|
||||
/// Representation of a bezier curve with 2D points.
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Bezier {
|
||||
/// Start point of the bezier curve.
|
||||
start: DVec2,
|
||||
|
@ -54,3 +61,8 @@ impl Debug for Bezier {
|
|||
debug_struct_ref.field("end", &self.end).finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "dyn-any")]
|
||||
impl dyn_any::StaticType for Bezier {
|
||||
type Static = Bezier;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use super::*;
|
||||
use crate::consts::*;
|
||||
|
||||
use glam::DVec2;
|
||||
use std::fmt::Write;
|
||||
|
||||
/// Functionality relating to core `Subpath` operations, such as constructors and `iter`.
|
||||
|
@ -65,6 +66,11 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
SubpathIter { subpath: self, index: 0 }
|
||||
}
|
||||
|
||||
/// Returns a slice of the [ManipulatorGroup]s in the `Subpath`.
|
||||
pub fn manipulator_groups(&self) -> &[ManipulatorGroup<ManipulatorGroupId>] {
|
||||
&self.manipulator_groups
|
||||
}
|
||||
|
||||
/// Appends to the `svg` mutable string with an SVG shape representation of the curve.
|
||||
pub fn curve_to_svg(&self, svg: &mut String, attributes: String) {
|
||||
let curve_start_argument = format!("{SVG_ARG_MOVE}{} {}", self[0].anchor.x, self[0].anchor.y);
|
||||
|
@ -120,4 +126,115 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
self.handles_to_svg(svg, handle_attributes);
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a [Subpath] from an iter of anchor positions.
|
||||
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_rect(corner1: DVec2, corner2: DVec2) -> Self {
|
||||
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.
|
||||
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 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 anchor_positions = (0..sides).map(|i| {
|
||||
let angle = (i as f64) * std::f64::consts::TAU / (sides as f64);
|
||||
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 line from `p1` to `p2`
|
||||
pub fn new_line(p1: DVec2, p2: DVec2) -> Self {
|
||||
Self::from_anchors([p1, p2], false)
|
||||
}
|
||||
|
||||
/// Construct a cubic spline from a list of points.
|
||||
/// Based on https://mathworld.wolfram.com/CubicSpline.html
|
||||
pub fn new_cubic_spline(points: Vec<DVec2>) -> Self {
|
||||
// Number of points = number of points to find handles for
|
||||
let len_points = points.len();
|
||||
|
||||
// matrix coefficients a, b and c (see https://mathworld.wolfram.com/CubicSpline.html)
|
||||
// because the 'a' coefficients are all 1 they need not be stored
|
||||
// this algorithm does a variation of the above algorithm.
|
||||
// Instead of using the traditional cubic: a + bt + ct^2 + dt^3, we use the bezier cubic.
|
||||
|
||||
let mut b = vec![DVec2::new(4., 4.); len_points];
|
||||
b[0] = DVec2::new(2., 2.);
|
||||
b[len_points - 1] = DVec2::new(2., 2.);
|
||||
|
||||
let mut c = vec![DVec2::new(1., 1.); len_points];
|
||||
|
||||
// 'd' is the the second point in a cubic bezier, which is what we solve for
|
||||
let mut d = vec![DVec2::ZERO; len_points];
|
||||
|
||||
d[0] = DVec2::new(2. * points[1].x + points[0].x, 2. * points[1].y + points[0].y);
|
||||
d[len_points - 1] = DVec2::new(3. * points[len_points - 1].x, 3. * points[len_points - 1].y);
|
||||
for idx in 1..(len_points - 1) {
|
||||
d[idx] = DVec2::new(4. * points[idx].x + 2. * points[idx + 1].x, 4. * points[idx].y + 2. * points[idx + 1].y);
|
||||
}
|
||||
|
||||
// Solve with Thomas algorithm (see https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm)
|
||||
// do row operations to eliminate `a` coefficients
|
||||
c[0] /= -b[0];
|
||||
d[0] /= -b[0];
|
||||
#[allow(clippy::assign_op_pattern)]
|
||||
for i in 1..len_points {
|
||||
b[i] += c[i - 1];
|
||||
// for some reason the below line makes the borrow checker mad
|
||||
//d[i] += d[i-1]
|
||||
d[i] = d[i] + d[i - 1];
|
||||
c[i] /= -b[i];
|
||||
d[i] /= -b[i];
|
||||
}
|
||||
|
||||
// at this point b[i] == -a[i + 1], a[i] == 0,
|
||||
// do row operations to eliminate 'c' coefficients and solve
|
||||
d[len_points - 1] *= -1.;
|
||||
#[allow(clippy::assign_op_pattern)]
|
||||
for i in (0..len_points - 1).rev() {
|
||||
d[i] = d[i] - (c[i] * d[i + 1]);
|
||||
d[i] *= -1.; //d[i] /= b[i]
|
||||
}
|
||||
|
||||
let mut subpath = Subpath::new(Vec::new(), false);
|
||||
|
||||
// given the second point in the n'th cubic bezier, the third point is given by 2 * points[n+1] - b[n+1].
|
||||
// to find 'handle1_pos' for the n'th point we need the n-1 cubic bezier
|
||||
subpath.manipulator_groups.push(ManipulatorGroup::new(points[0], None, Some(d[0])));
|
||||
for i in 1..len_points - 1 {
|
||||
subpath.manipulator_groups.push(ManipulatorGroup::new(points[i], Some(2. * points[i] - d[i]), Some(d[i])));
|
||||
}
|
||||
subpath
|
||||
.manipulator_groups
|
||||
.push(ManipulatorGroup::new(points[len_points - 1], Some(2. * points[len_points - 1] - d[len_points - 1]), None));
|
||||
|
||||
subpath
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,12 +12,18 @@ use std::fmt::{Debug, Formatter, Result};
|
|||
use std::ops::{Index, IndexMut};
|
||||
|
||||
/// Structure used to represent a path composed of [Bezier] curves.
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[derive(Clone, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Subpath<ManipulatorGroupId: crate::Identifier> {
|
||||
manipulator_groups: Vec<ManipulatorGroup<ManipulatorGroupId>>,
|
||||
closed: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "dyn-any")]
|
||||
impl<ManipulatorGroupId: crate::Identifier> dyn_any::StaticType for Subpath<ManipulatorGroupId> {
|
||||
type Static = Subpath<ManipulatorGroupId>;
|
||||
}
|
||||
|
||||
/// Iteration structure for iterating across each curve of a `Subpath`, using an intermediate `Bezier` representation.
|
||||
pub struct SubpathIter<'a, ManipulatorGroupId: crate::Identifier> {
|
||||
index: usize,
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
use super::Bezier;
|
||||
|
||||
use glam::DVec2;
|
||||
use std::fmt::{Debug, Formatter, Result};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::{
|
||||
fmt::{Debug, Formatter, Result},
|
||||
hash::Hash,
|
||||
};
|
||||
|
||||
/// An id type used for each [ManipulatorGroup].
|
||||
pub trait Identifier: Sized + Clone + PartialEq {
|
||||
pub trait Identifier: Sized + Clone + PartialEq + Hash + 'static {
|
||||
fn new() -> Self;
|
||||
}
|
||||
|
||||
/// An empty id type for use in tests
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
|
||||
#[cfg(test)]
|
||||
pub(crate) struct EmptyId;
|
||||
|
||||
|
@ -22,6 +25,7 @@ impl Identifier for EmptyId {
|
|||
|
||||
/// Structure used to represent a single anchor with up to two optional associated handles along a `Subpath`
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ManipulatorGroup<ManipulatorGroupId: crate::Identifier> {
|
||||
pub anchor: DVec2,
|
||||
pub in_handle: Option<DVec2>,
|
||||
|
@ -29,6 +33,23 @@ pub struct ManipulatorGroup<ManipulatorGroupId: crate::Identifier> {
|
|||
pub id: ManipulatorGroupId,
|
||||
}
|
||||
|
||||
// TODO: Remove once we no longer need to hash floats in Graphite
|
||||
impl<ManipulatorGroupId: crate::Identifier> Hash for ManipulatorGroup<ManipulatorGroupId> {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
self.anchor.to_array().iter().for_each(|x| x.to_bits().hash(state));
|
||||
self.in_handle.is_some().hash(state);
|
||||
self.in_handle.map(|in_handle| in_handle.to_array().iter().for_each(|x| x.to_bits().hash(state)));
|
||||
self.out_handle.is_some().hash(state);
|
||||
self.out_handle.map(|out_handle| out_handle.to_array().iter().for_each(|x| x.to_bits().hash(state)));
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "dyn-any")]
|
||||
impl<ManipulatorGroupId: crate::Identifier> dyn_any::StaticType for ManipulatorGroup<ManipulatorGroupId> {
|
||||
type Static = ManipulatorGroup<ManipulatorGroupId>;
|
||||
}
|
||||
|
||||
impl<ManipulatorGroupId: crate::Identifier> Debug for ManipulatorGroup<ManipulatorGroupId> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
f.debug_struct("ManipulatorGroup")
|
||||
|
@ -40,6 +61,18 @@ impl<ManipulatorGroupId: crate::Identifier> Debug for ManipulatorGroup<Manipulat
|
|||
}
|
||||
|
||||
impl<ManipulatorGroupId: crate::Identifier> ManipulatorGroup<ManipulatorGroupId> {
|
||||
/// 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 = ManipulatorGroupId::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, Some(anchor), Some(anchor))
|
||||
}
|
||||
|
||||
/// 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<ManipulatorGroupId>) -> Bezier {
|
||||
let start = self.anchor;
|
||||
let end = end_group.anchor;
|
||||
|
@ -52,4 +85,11 @@ impl<ManipulatorGroupId: crate::Identifier> ManipulatorGroup<ManipulatorGroupId>
|
|||
(None, None) => Bezier::from_linear_dvec2(start, 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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ use super::*;
|
|||
use crate::utils::SubpathTValue;
|
||||
use crate::utils::TValue;
|
||||
|
||||
use glam::DAffine2;
|
||||
|
||||
/// Helper function to ensure the index and t value pair is mapped within a maximum index value.
|
||||
/// Allows for the point to be fetched without needing to handle an additional edge case.
|
||||
/// - Ex. Via `subpath.iter().nth(index).evaluate(t);`
|
||||
|
@ -263,6 +265,13 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
closed: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -719,4 +728,10 @@ mod tests {
|
|||
assert!(result.manipulator_groups[0].out_handle.is_none());
|
||||
assert_eq!(result.manipulator_groups.len(), 1);
|
||||
}
|
||||
|
||||
fn transform_subpath() {
|
||||
let mut subpath = set_up_open_subpath();
|
||||
subpath.apply_transform(glam::DAffine2::IDENTITY);
|
||||
assert_eq!(subpath, set_up_open_subpath());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,7 +70,8 @@ mod uuid_generation {
|
|||
|
||||
pub use uuid_generation::*;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ManipulatorGroupId(u64);
|
||||
|
||||
impl bezier_rs::Identifier for ManipulatorGroupId {
|
||||
|
|
|
@ -153,7 +153,7 @@ mod tests {
|
|||
};
|
||||
|
||||
use crate::executor::DynamicExecutor;
|
||||
use graph_craft::executor::{Compiler, Executor};
|
||||
use graph_craft::executor::Compiler;
|
||||
|
||||
let compiler = Compiler {};
|
||||
let protograph = compiler.compile_single(network, true).expect("Graph should be generated");
|
||||
|
|
|
@ -6,7 +6,7 @@ use glam::DVec2;
|
|||
use std::fmt::Write;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[derive(Clone, PartialEq, Hash)]
|
||||
pub(crate) struct EmptyId;
|
||||
|
||||
impl bezier_rs::Identifier for EmptyId {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue