Add basic vector nodes (#1059)

* Add some derives

* Allow node_fn with mutable inputs

* Add basic vector nodes

* Revert elipse tool changes

* Fix the elipse tool again

* Change spacer width

* Bubble serde feature in graph craft

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube
2023-03-02 21:54:23 +00:00
committed by Keavon Chambers
parent 7e37fb41a4
commit a2046a51b1
22 changed files with 707 additions and 151 deletions

View File

@@ -1,15 +1,15 @@
use crate::uuid::ManipulatorGroupId;
use crate::vector::VectorData;
use crate::Node;
use glam::{DAffine2, DVec2};
use super::subpath::Subpath;
type VectorData = Subpath;
use bezier_rs::Subpath;
use glam::DVec2;
pub struct UnitCircleGenerator;
#[node_macro::node_fn(UnitCircleGenerator)]
fn unit_circle(_input: ()) -> VectorData {
Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE)
super::VectorData::from_subpath(Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE))
}
#[derive(Debug, Clone, Copy)]
@@ -17,19 +17,17 @@ pub struct UnitSquareGenerator;
#[node_macro::node_fn(UnitSquareGenerator)]
fn unit_square(_input: ()) -> VectorData {
Subpath::new_rect(DVec2::ZERO, DVec2::ONE)
super::VectorData::from_subpath(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<P> {
path_data: P,
}
pub struct PathGenerator;
#[node_macro::node_fn(PathGenerator)]
fn generate_path(_input: (), path_data: Subpath) -> VectorData {
path_data
fn generate_path(path_data: Subpath<ManipulatorGroupId>) -> super::VectorData {
super::VectorData::from_subpath(path_data)
}
use crate::raster::Image;
@@ -40,7 +38,7 @@ pub struct BlitSubpath<P> {
}
#[node_macro::node_fn(BlitSubpath)]
fn bilt_subpath(base_image: Image, path_data: Subpath) -> Image {
fn bilt_subpath(base_image: Image, path_data: VectorData) -> Image {
log::info!("Blitting subpath {path_data:#?}");
// TODO: Get forma to compile
/*use forma::prelude::*;
@@ -58,20 +56,3 @@ fn bilt_subpath(base_image: Image, path_data: Subpath) -> Image {
base_image
}
#[derive(Debug, Clone, Copy)]
pub struct TransformSubpathNode<Translation, Rotation, Scale, Shear> {
translate: Translation,
rotate: Rotation,
scale: Scale,
shear: Shear,
}
#[node_macro::node_fn(TransformSubpathNode)]
fn transform_subpath(subpath: Subpath, translate: DVec2, rotate: f64, scale: DVec2, shear: DVec2) -> VectorData {
let (sin, cos) = rotate.sin_cos();
let mut subpath = subpath;
subpath.apply_affine(DAffine2::from_cols_array(&[scale.x + cos, shear.y + sin, shear.x - sin, scale.y + cos, translate.x, translate.y]));
subpath
}

View File

@@ -28,7 +28,7 @@ impl Default for ManipulatorPoint {
}
}
#[allow(clippy::derive_hash_xor_eq)]
#[allow(clippy::derived_hash_with_manual_eq)]
impl Hash for ManipulatorPoint {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.position.to_array().iter().for_each(|x| x.to_bits().hash(state));

View File

@@ -14,3 +14,8 @@ pub use vector_data::VectorData;
mod id_vec;
pub use id_vec::IdBackedVec;
mod vector_nodes;
pub use vector_nodes::*;
pub use bezier_rs;

View File

@@ -3,6 +3,7 @@
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
use crate::Color;
use dyn_any::{DynAny, StaticType};
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display, Write};
@@ -19,7 +20,7 @@ fn format_opacity(name: &str, opacity: f32) -> String {
}
}
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, Serialize, Deserialize, specta::Type)]
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, Serialize, Deserialize, DynAny, specta::Type)]
pub enum GradientType {
#[default]
Linear,
@@ -30,30 +31,41 @@ pub enum GradientType {
///
/// Contains the start and end points, along with the colors at varying points along the length.
#[repr(C)]
#[derive(Debug, PartialEq, Default, Serialize, Deserialize, specta::Type)]
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, DynAny, specta::Type)]
pub struct Gradient {
pub start: DVec2,
pub end: DVec2,
pub transform: DAffine2,
pub positions: Vec<(f64, Option<Color>)>,
uuid: u64,
pub gradient_type: GradientType,
}
impl core::hash::Hash for Gradient {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.positions.len().hash(state);
[].iter()
.chain(self.start.to_array().iter())
.chain(self.end.to_array().iter())
.chain(self.transform.to_cols_array().iter())
.chain(self.positions.iter().map(|(position, _)| position))
.for_each(|x| x.to_bits().hash(state));
self.positions.iter().for_each(|(_, color)| color.hash(state));
self.gradient_type.hash(state);
}
}
impl Gradient {
/// Constructs a new gradient with the colors at 0 and 1 specified.
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, uuid: u64, gradient_type: GradientType) -> Self {
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, _uuid: u64, gradient_type: GradientType) -> Self {
Gradient {
start,
end,
positions: vec![(0., Some(start_color)), (1., Some(end_color))],
transform,
uuid,
gradient_type,
}
}
/// Adds the gradient def with the uuid specified
fn render_defs(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) {
/// Adds the gradient def, returning the gradient id
fn render_defs(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> u64 {
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
let transformed_bound_transform = DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]);
let updated_transform = multiplied_transform * bound_transform;
@@ -78,12 +90,13 @@ impl Gradient {
.map(|(i, entry)| entry.to_string() + if i == 5 { "" } else { "," })
.collect::<String>();
let gradient_id = crate::uuid::generate_uuid();
match self.gradient_type {
GradientType::Linear => {
let _ = write!(
svg_defs,
r#"<linearGradient id="{}" x1="{}" x2="{}" y1="{}" y2="{}" gradientTransform="matrix({})">{}</linearGradient>"#,
self.uuid, start.x, end.x, start.y, end.y, transform, positions
gradient_id, start.x, end.x, start.y, end.y, transform, positions
);
}
GradientType::Radial => {
@@ -91,10 +104,12 @@ impl Gradient {
let _ = write!(
svg_defs,
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}" gradientTransform="matrix({})">{}</radialGradient>"#,
self.uuid, start.x, start.y, radius, transform, positions
gradient_id, start.x, start.y, radius, transform, positions
);
}
}
gradient_id
}
/// Insert a stop into the gradient, the index if successful
@@ -137,26 +152,11 @@ impl Gradient {
}
}
impl Clone for Gradient {
/// Clones the gradient, with the cloned gradient having the new uuid.
/// If multiple gradients have the same id then only one gradient will be shown in the final svg output.
fn clone(&self) -> Self {
Self {
start: self.start,
end: self.end,
transform: self.transform,
positions: self.positions.clone(),
uuid: crate::uuid::generate_uuid(),
gradient_type: self.gradient_type,
}
}
}
/// Describes the fill of a layer.
///
/// Can be None, a solid [Color], a linear [Gradient], a radial [Gradient] or potentially some sort of image or pattern in the future
#[repr(C)]
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, specta::Type)]
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, DynAny, Hash, specta::Type)]
pub enum Fill {
#[default]
None,
@@ -186,8 +186,8 @@ impl Fill {
Self::None => r#" fill="none""#.to_string(),
Self::Solid(color) => format!(r##" fill="#{}"{}"##, color.rgb_hex(), format_opacity("fill", color.a())),
Self::Gradient(gradient) => {
gradient.render_defs(svg_defs, multiplied_transform, bounds, transformed_bounds);
format!(r##" fill="url('#{}')""##, gradient.uuid)
let gradient_id = gradient.render_defs(svg_defs, multiplied_transform, bounds, transformed_bounds);
format!(r##" fill="url('#{}')""##, gradient_id)
}
}
}
@@ -207,9 +207,18 @@ impl Fill {
}
}
/// Enum describing the type of [Fill]
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, DynAny, Hash, specta::Type)]
pub enum FillType {
None,
Solid,
Gradient,
}
/// The stroke (outline) style of an SVG element.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, specta::Type)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, DynAny, specta::Type)]
pub enum LineCap {
Butt,
Round,
@@ -227,7 +236,7 @@ impl Display for LineCap {
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, specta::Type)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, DynAny, specta::Type)]
pub enum LineJoin {
Miter,
Bevel,
@@ -245,25 +254,42 @@ impl Display for LineJoin {
}
#[repr(C)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, specta::Type)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, DynAny, specta::Type)]
pub struct Stroke {
/// Stroke color
color: Option<Color>,
pub color: Option<Color>,
/// Line thickness
weight: f64,
dash_lengths: Vec<f32>,
dash_offset: f64,
line_cap: LineCap,
line_join: LineJoin,
line_join_miter_limit: f64,
pub weight: f64,
pub dash_lengths: Vec<f32>,
pub dash_offset: f64,
pub line_cap: LineCap,
pub line_join: LineJoin,
pub line_join_miter_limit: f64,
}
impl core::hash::Hash for Stroke {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.color.hash(state);
self.weight.to_bits().hash(state);
self.dash_lengths.len().hash(state);
self.dash_lengths.iter().for_each(|length| length.to_bits().hash(state));
self.dash_offset.to_bits().hash(state);
self.line_cap.hash(state);
self.line_join.hash(state);
self.line_join_miter_limit.to_bits().hash(state);
}
}
impl Stroke {
pub fn new(color: Color, weight: f64) -> Self {
pub const fn new(color: Color, weight: f64) -> Self {
Self {
color: Some(color),
weight,
..Default::default()
dash_lengths: Vec::new(),
dash_offset: 0.,
line_cap: LineCap::Butt,
line_join: LineJoin::Miter,
line_join_miter_limit: 4.,
}
}
@@ -278,7 +304,11 @@ impl Stroke {
}
pub fn dash_lengths(&self) -> String {
self.dash_lengths.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", ")
if self.dash_lengths.is_empty() {
"none".to_string()
} else {
self.dash_lengths.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", ")
}
}
pub fn dash_offset(&self) -> f64 {
@@ -367,7 +397,7 @@ impl Default for Stroke {
Self {
weight: 0.,
color: Some(Color::from_rgba8(0, 0, 0, 255)),
dash_lengths: vec![0.],
dash_lengths: Vec::new(),
dash_offset: 0.,
line_cap: LineCap::Butt,
line_join: LineJoin::Miter,
@@ -377,14 +407,14 @@ impl Default for Stroke {
}
#[repr(C)]
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, specta::Type)]
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, DynAny, Hash, specta::Type)]
pub struct PathStyle {
stroke: Option<Stroke>,
fill: Fill,
}
impl PathStyle {
pub fn new(stroke: Option<Stroke>, fill: Fill) -> Self {
pub const fn new(stroke: Option<Stroke>, fill: Fill) -> Self {
Self { stroke, fill }
}
@@ -508,7 +538,7 @@ impl PathStyle {
}
/// Represents different ways of rendering an object
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, specta::Type)]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Hash, DynAny, specta::Type)]
pub enum ViewMode {
/// Render with normal coloration at the current viewport resolution
#[default]

View File

@@ -1,12 +1,33 @@
use glam::DAffine2;
use super::style::{PathStyle, Stroke};
use crate::{uuid::ManipulatorGroupId, Color};
use super::style::PathStyle;
use crate::uuid::ManipulatorGroupId;
use dyn_any::{DynAny, StaticType};
use glam::DAffine2;
/// [VectorData] is passed between nodes.
/// It contains a list of subpaths (that may be open or closed), a transform and some style information.
#[derive(Clone, Debug, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VectorData {
pub subpaths: Vec<bezier_rs::Subpath<ManipulatorGroupId>>,
pub transform: DAffine2,
pub style: PathStyle,
}
impl VectorData {
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)),
}
}
pub fn from_subpath(subpath: bezier_rs::Subpath<ManipulatorGroupId>) -> Self {
super::VectorData {
subpaths: vec![subpath],
transform: DAffine2::IDENTITY,
style: PathStyle::default(),
}
}
}

View File

@@ -0,0 +1,90 @@
use super::style::{Fill, FillType, Gradient, GradientType, Stroke};
use super::VectorData;
use crate::{Color, Node};
use glam::{DAffine2, DVec2};
#[derive(Debug, Clone, Copy)]
pub struct TransformNode<Translation, Rotation, Scale, Shear> {
translate: Translation,
rotate: Rotation,
scale: Scale,
shear: Shear,
}
#[node_macro::node_fn(TransformNode)]
fn transform_vector_data(mut vector_data: VectorData, translate: DVec2, rotate: f64, scale: DVec2, shear: DVec2) -> VectorData {
let (sin, cos) = rotate.sin_cos();
vector_data.transform = vector_data.transform * DAffine2::from_cols_array(&[scale.x + cos, shear.y + sin, shear.x - sin, scale.y + cos, translate.x, translate.y]);
vector_data
}
#[derive(Debug, Clone, Copy)]
pub struct SetFillNode<FillType, SolidColor, GradientType, Start, End, Transform, Positions> {
fill_type: FillType,
solid_color: SolidColor,
gradient_type: GradientType,
start: Start,
end: End,
transform: Transform,
positions: Positions,
}
#[node_macro::node_fn(SetFillNode)]
fn set_vector_data_fill(
mut vector_data: VectorData,
fill_type: FillType,
solid_color: Color,
gradient_type: GradientType,
start: DVec2,
end: DVec2,
transform: DAffine2,
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::Gradient => Fill::Gradient(Gradient {
start,
end,
transform,
positions,
gradient_type,
}),
});
vector_data
}
#[derive(Debug, Clone, Copy)]
pub struct SetStrokeNode<Color, Weight, DashLengths, DashOffset, LineCap, LineJoin, MiterLimit> {
color: Color,
weight: Weight,
dash_lengths: DashLengths,
dash_offset: DashOffset,
line_cap: LineCap,
line_join: LineJoin,
miter_limit: MiterLimit,
}
#[node_macro::node_fn(SetStrokeNode)]
fn set_vector_data_stroke(
mut vector_data: VectorData,
color: crate::Color,
weight: f64,
dash_lengths: Vec<f32>,
dash_offset: f64,
line_cap: super::style::LineCap,
line_join: super::style::LineJoin,
miter_limit: f64,
) -> VectorData {
vector_data.style.set_stroke(Stroke {
color: Some(color),
weight,
dash_lengths,
dash_offset,
line_cap,
line_join,
line_join_miter_limit: miter_limit,
});
vector_data
}