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:
0HyperCube 2023-03-02 16:48:09 +00:00 committed by Keavon Chambers
parent 7254c008f9
commit 66ec85a3c9
12 changed files with 206 additions and 10 deletions

2
Cargo.lock generated
View file

@ -281,7 +281,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
name = "bezier-rs"
version = "0.1.0"
dependencies = [
"dyn-any",
"glam",
"serde",
]
[[package]]

View file

@ -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 {

View file

@ -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;

View file

@ -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 }

View file

@ -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;
}

View file

@ -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
}
}

View file

@ -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,

View file

@ -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));
}
}

View file

@ -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());
}
}

View file

@ -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 {

View file

@ -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");

View file

@ -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 {