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:
0HyperCube 2023-03-26 08:03:51 +01:00 committed by Keavon Chambers
parent 639a24d8ad
commit 959e790cdf
64 changed files with 2639 additions and 1552 deletions

View file

@ -7,26 +7,31 @@ use crate::vector::VectorData;
use crate::Node;
#[derive(Debug, Clone, Copy)]
pub struct TransformNode<Translation, Rotation, Scale, Shear> {
pub struct TransformNode<Translation, Rotation, Scale, Shear, Pivot> {
pub(crate) translate: Translation,
pub(crate) rotate: Rotation,
pub(crate) scale: Scale,
pub(crate) shear: Shear,
pub(crate) pivot: Pivot,
}
#[node_macro::node_fn(TransformNode)]
pub(crate) fn transform_vector_data(mut vector_data: VectorData, translate: DVec2, rotate: f64, scale: DVec2, shear: DVec2) -> VectorData {
let transform = generate_transform(shear, &vector_data.transform, scale, rotate, translate);
vector_data.transform = transform * vector_data.transform;
pub(crate) fn transform_vector_data(mut vector_data: VectorData, translate: DVec2, rotate: f64, scale: DVec2, shear: DVec2, pivot: DVec2) -> VectorData {
let pivot = DAffine2::from_translation(vector_data.local_pivot(pivot));
let modification = pivot * DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]) * pivot.inverse();
vector_data.transform = modification * vector_data.transform;
vector_data
}
impl<'input, Translation: 'input, Rotation: 'input, Scale: 'input, Shear: 'input> Node<'input, ImageFrame> for TransformNode<Translation, Rotation, Scale, Shear>
impl<'input, Translation: 'input, Rotation: 'input, Scale: 'input, Shear: 'input, Pivot: 'input> Node<'input, ImageFrame> for TransformNode<Translation, Rotation, Scale, Shear, Pivot>
where
Translation: for<'any_input> Node<'any_input, (), Output = DVec2>,
Rotation: for<'any_input> Node<'any_input, (), Output = f64>,
Scale: for<'any_input> Node<'any_input, (), Output = DVec2>,
Shear: for<'any_input> Node<'any_input, (), Output = DVec2>,
Pivot: for<'any_input> Node<'any_input, (), Output = DVec2>,
{
type Output = ImageFrame;
#[inline]
@ -48,6 +53,5 @@ fn generate_transform(shear: DVec2, transform: &DAffine2, scale: DVec2, rotate:
let pivot = transform.transform_point2(DVec2::splat(0.5));
let translate_to_center = DAffine2::from_translation(-pivot);
let transformation = translate_to_center.inverse() * DAffine2::from_scale_angle_translation(scale, rotate, translate) * shear_matrix * translate_to_center;
transformation
translate_to_center.inverse() * DAffine2::from_scale_angle_translation(scale, rotate, translate) * shear_matrix * translate_to_center
}

View file

@ -1,3 +1,4 @@
use dyn_any::{DynAny, StaticType};
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Serialize, Deserialize, specta::Type)]
@ -70,7 +71,7 @@ mod uuid_generation {
pub use uuid_generation::*;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ManipulatorGroupId(u64);

View file

@ -17,41 +17,43 @@ pub struct UnitSquareGenerator;
#[node_macro::node_fn(UnitSquareGenerator)]
fn unit_square(_input: ()) -> VectorData {
super::VectorData::from_subpath(Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE))
super::VectorData::from_subpaths(vec![Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE)])
}
// TODO: I removed the Arc requirement we shouuld think about when it makes sense to use its
// vs making a generic value node
#[derive(Debug, Clone)]
pub struct PathGenerator;
pub struct PathGenerator<Mirror> {
mirror: Mirror,
}
#[node_macro::node_fn(PathGenerator)]
fn generate_path(path_data: Subpath<ManipulatorGroupId>) -> super::VectorData {
super::VectorData::from_subpath(path_data)
fn generate_path(path_data: Vec<Subpath<ManipulatorGroupId>>, mirror: Vec<ManipulatorGroupId>) -> super::VectorData {
let mut vector_data = super::VectorData::from_subpaths(path_data);
vector_data.mirror_angle = mirror;
vector_data
}
use crate::raster::Image;
// #[derive(Debug, Clone, Copy)]
// pub struct BlitSubpath<P> {
// path_data: P,
// }
#[derive(Debug, Clone, Copy)]
pub struct BlitSubpath<P> {
path_data: P,
}
// #[node_macro::node_fn(BlitSubpath)]
// fn blit_subpath(base_image: Image, path_data: VectorData) -> Image {
// // TODO: Get forma to compile
// use forma::prelude::*;
// let composition = Composition::new();
// let mut renderer = cpu::Renderer::new();
// let mut path_builder = PathBuilder::new();
// for path_segment in path_data.bezier_iter() {
// let points = path_segment.internal.get_points().collect::<Vec<_>>();
// match points.len() {
// 2 => path_builder.line_to(points[1].into()),
// 3 => path_builder.quad_to(points[1].into(), points[2].into()),
// 4 => path_builder.cubic_to(points[1].into(), points[2].into(), points[3].into()),
// }
// }
#[node_macro::node_fn(BlitSubpath)]
fn bilt_subpath(base_image: Image, path_data: VectorData) -> Image {
// TODO: Get forma to compile
/*use forma::prelude::*;
let composition = Composition::new();
let mut renderer = cpu::Renderer::new();
let mut path_builder = PathBuilder::new();
for path_segment in path_data.bezier_iter() {
let points = path_segment.internal.get_points().collect::<Vec<_>>();
match points.len() {
2 => path_builder.line_to(points[1].into()),
3 => path_builder.quad_to(points[1].into(), points[2].into()),
4 => path_builder.cubic_to(points[1].into(), points[2].into(), points[3].into()),
}
}*/
base_image
}
// base_image
// }

View file

@ -10,7 +10,7 @@ pub mod subpath;
pub use subpath::Subpath;
mod vector_data;
pub use vector_data::VectorData;
pub use vector_data::*;
mod id_vec;
pub use id_vec::IdBackedVec;

View file

@ -1,3 +1,5 @@
use crate::uuid::ManipulatorGroupId;
use super::consts::ManipulatorType;
use super::id_vec::IdBackedVec;
use super::manipulator_group::ManipulatorGroup;
@ -45,6 +47,21 @@ impl Subpath {
shape.path_elements(0.1).into()
}
/// Convert to the legacy Subpath from the `bezier_rs::Subpath`.
pub fn from_bezier_crate(value: &[bezier_rs::Subpath<ManipulatorGroupId>]) -> Self {
let mut groups = IdBackedVec::new();
for subpath in value {
for group in subpath.manipulator_groups() {
groups.push(ManipulatorGroup::new_with_handles(group.anchor, group.in_handle, group.out_handle));
}
if subpath.closed() {
let group = subpath.manipulator_groups()[0];
groups.push(ManipulatorGroup::new_with_handles(group.anchor, group.in_handle, group.out_handle));
}
}
Self(groups)
}
// ** PRIMITIVE CONSTRUCTION **
/// constructs a rectangle with `p1` as the lower left and `p2` as the top right

View file

@ -1,8 +1,9 @@
use super::style::{PathStyle, Stroke};
use crate::{uuid::ManipulatorGroupId, Color};
use bezier_rs::ManipulatorGroup;
use dyn_any::{DynAny, StaticType};
use glam::DAffine2;
use glam::{DAffine2, DVec2};
/// [VectorData] is passed between nodes.
/// It contains a list of subpaths (that may be open or closed), a transform and some style information.
@ -12,22 +13,146 @@ pub struct VectorData {
pub subpaths: Vec<bezier_rs::Subpath<ManipulatorGroupId>>,
pub transform: DAffine2,
pub style: PathStyle,
pub mirror_angle: Vec<ManipulatorGroupId>,
}
impl VectorData {
/// An empty subpath with no data, an identity transform and a black fill.
pub const fn empty() -> Self {
Self {
subpaths: Vec::new(),
transform: DAffine2::IDENTITY,
style: PathStyle::new(Some(Stroke::new(Color::BLACK, 0.)), super::style::Fill::Solid(Color::BLACK)),
style: PathStyle::new(Some(Stroke::new(Color::BLACK, 0.)), super::style::Fill::None),
mirror_angle: Vec::new(),
}
}
/// Iterator over the manipulator groups of the subpaths
pub fn manipulator_groups(&self) -> impl Iterator<Item = &ManipulatorGroup<ManipulatorGroupId>> + DoubleEndedIterator {
self.subpaths.iter().flat_map(|subpath| subpath.manipulator_groups())
}
pub fn manipulator_from_id(&self, id: ManipulatorGroupId) -> Option<&ManipulatorGroup<ManipulatorGroupId>> {
self.subpaths.iter().find_map(|subpath| subpath.manipulator_from_id(id))
}
/// Construct some new vector data from a single subpath with an identy transform and black fill.
pub fn from_subpath(subpath: bezier_rs::Subpath<ManipulatorGroupId>) -> Self {
super::VectorData {
subpaths: vec![subpath],
transform: DAffine2::IDENTITY,
style: PathStyle::default(),
Self::from_subpaths(vec![subpath])
}
/// Construct some new vector data from subpaths with an identy transform and black fill.
pub fn from_subpaths(subpaths: Vec<bezier_rs::Subpath<ManipulatorGroupId>>) -> Self {
super::VectorData { subpaths, ..Self::empty() }
}
/// Compute the bounding boxes of the subpaths without any transform
pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
self.bounding_box_with_transform(DAffine2::IDENTITY)
}
/// Compute the bounding boxes of the subpaths with the specified transform
pub fn bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.subpaths
.iter()
.filter_map(|subpath| subpath.bounding_box_with_transform(transform))
.reduce(|b1, b2| [b1[0].min(b2[0]), b1[1].max(b2[1])])
}
/// Calculate the corners of the bounding box but with a nonzero size.
///
/// If the layer bounds are `0` in either axis then they are changed to be `1`.
pub fn nonzero_bounding_box(&self) -> [DVec2; 2] {
let [bounds_min, mut bounds_max] = self.bounding_box().unwrap_or_default();
let bounds_size = bounds_max - bounds_min;
if bounds_size.x < 1e-10 {
bounds_max.x = bounds_min.x + 1.;
}
if bounds_size.y < 1e-10 {
bounds_max.y = bounds_min.y + 1.;
}
[bounds_min, bounds_max]
}
/// Compute the pivot of the layer in layerspace (the coordinates of the subpaths)
pub fn layerspace_pivot(&self, normalised_pivot: DVec2) -> DVec2 {
let [bounds_min, bounds_max] = self.nonzero_bounding_box();
let bounds_size = bounds_max - bounds_min;
bounds_min + bounds_size * normalised_pivot
}
/// Compute the pivot in local space with the current transform applied
pub fn local_pivot(&self, normalised_pivot: DVec2) -> DVec2 {
self.transform.transform_point2(self.layerspace_pivot(normalised_pivot))
}
}
impl Default for VectorData {
fn default() -> Self {
Self::empty()
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ManipulatorPointId {
pub group: ManipulatorGroupId,
pub manipulator_type: SelectedType,
}
impl ManipulatorPointId {
pub fn new(group: ManipulatorGroupId, manipulator_type: SelectedType) -> Self {
Self { group, manipulator_type }
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SelectedType {
Anchor = 1 << 0,
InHandle = 1 << 1,
OutHandle = 1 << 2,
}
impl SelectedType {
/// Get the location of the [SelectedType] in the [ManipulatorGroup]
pub fn get_position(&self, manipulator_group: &ManipulatorGroup<ManipulatorGroupId>) -> Option<DVec2> {
match self {
Self::Anchor => Some(manipulator_group.anchor),
Self::InHandle => manipulator_group.in_handle,
Self::OutHandle => manipulator_group.out_handle,
}
}
/// Get the closest [SelectedType] in the [ManipulatorGroup].
pub fn closest_widget(manipulator_group: &ManipulatorGroup<ManipulatorGroupId>, transform_space: DAffine2, target: DVec2, hide_handle_distance: f64) -> (Self, f64) {
let anchor = transform_space.transform_point2(manipulator_group.anchor);
// Skip handles under the anchor
let not_under_anchor = |&(selected_type, position): &(SelectedType, DVec2)| selected_type == Self::Anchor || position.distance_squared(anchor) > hide_handle_distance.powi(2);
let compute_distance = |selected_type: Self| {
selected_type.get_position(manipulator_group).and_then(|position| {
Some((selected_type, transform_space.transform_point2(position)))
.filter(not_under_anchor)
.map(|(selected_type, pos)| (selected_type, pos.distance_squared(target)))
})
};
[Self::Anchor, Self::InHandle, Self::OutHandle]
.into_iter()
.filter_map(compute_distance)
.min_by(|a, b| a.1.total_cmp(&b.1))
.unwrap_or((Self::Anchor, manipulator_group.anchor.distance_squared(target)))
}
/// Opposite handle
pub fn opposite(&self) -> Self {
match self {
SelectedType::Anchor => SelectedType::Anchor,
SelectedType::InHandle => SelectedType::OutHandle,
SelectedType::OutHandle => SelectedType::InHandle,
}
}
/// Check if handle
pub fn is_handle(self) -> bool {
self != SelectedType::Anchor
}
}

View file

@ -18,7 +18,7 @@ pub struct SetFillNode<FillType, SolidColor, GradientType, Start, End, Transform
fn set_vector_data_fill(
mut vector_data: VectorData,
fill_type: FillType,
solid_color: Color,
solid_color: Option<Color>,
gradient_type: GradientType,
start: DVec2,
end: DVec2,
@ -26,8 +26,7 @@ fn set_vector_data_fill(
positions: Vec<(f64, Option<Color>)>,
) -> VectorData {
vector_data.style.set_fill(match fill_type {
FillType::None => Fill::None,
FillType::Solid => Fill::Solid(solid_color),
FillType::None | FillType::Solid => solid_color.map_or(Fill::None, |solid_color| Fill::Solid(solid_color)),
FillType::Gradient => Fill::Gradient(Gradient {
start,
end,