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 GitHub
parent 966695e066
commit 2cf4ee0fab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 707 additions and 151 deletions

1
Cargo.lock generated
View file

@ -1624,6 +1624,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"base64",
"bezier-rs",
"bytemuck",
"dyn-any",
"dyn-clone",

View file

@ -21,7 +21,7 @@ use std::hash::{Hash, Hasher};
/// This does not technically need to be unique globally, only within a folder.
pub type LayerId = u64;
#[derive(Debug, Clone, Deserialize, Serialize, specta::Type)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Document {
/// The root layer, usually a [FolderLayer](layers::folder_layer::FolderLayer) that contains all other [Layers](layers::layer_info::Layer).
pub root: Layer,

View file

@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
/// A layer that encapsulates other layers, including potentially more folders.
/// The contained layers are rendered in the same order they are
/// stored in the [layers](FolderLayer::layers) field.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default, specta::Type)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
pub struct FolderLayer {
/// The ID that will be assigned to the next layer that is added to the folder
next_assignment_id: LayerId,

View file

@ -15,7 +15,7 @@ use glam::{DAffine2, DMat2, DVec2};
use serde::{Deserialize, Serialize};
use std::fmt::Write;
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, specta::Type)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
/// Represents different types of layers.
pub enum LayerDataType {
/// A layer that wraps a [FolderLayer] struct.
@ -213,7 +213,7 @@ fn return_true() -> bool {
true
}
#[derive(Debug, PartialEq, Deserialize, Serialize, specta::Type)]
#[derive(Debug, PartialEq, Deserialize, Serialize)]
pub struct Layer {
/// Whether the layer is currently visible or hidden.
pub visible: bool,

View file

@ -9,7 +9,7 @@ use kurbo::{Affine, BezPath, Shape as KurboShape};
use serde::{Deserialize, Serialize};
use std::fmt::Write;
#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize, specta::Type)]
#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
pub struct NodeGraphFrameLayer {
// Image stored in layer after generation completes
pub mime: String,

View file

@ -13,7 +13,7 @@ use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
#[repr(C)]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, specta::Type)]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
// TODO: Rename all instances of `path` to `layer_path`
/// Operations that can be performed to mutate the document.
pub enum Operation {

View file

@ -12,7 +12,7 @@ use graphene_core::raster::color::Color;
use glam::DAffine2;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, specta::Type)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct ArtboardMessageHandler {
pub artboards_document: DocumentLegacy,
pub artboard_ids: Vec<LayerId>,

View file

@ -46,7 +46,7 @@ impl FrontendGraphDataType {
TaggedValue::Image(_) => Self::Raster,
TaggedValue::ImageFrame(_) => Self::Raster,
TaggedValue::Color(_) => Self::Color,
TaggedValue::RcSubpath(_) | TaggedValue::Subpath(_) => Self::Subpath,
TaggedValue::RcSubpath(_) | TaggedValue::Subpath(_) | TaggedValue::VectorData(_) => Self::Subpath,
_ => Self::General,
}
}

View file

@ -32,7 +32,7 @@ impl DocumentInputType {
Self { name, data_type, default }
}
pub const fn _none() -> Self {
pub const fn none() -> Self {
Self {
name: "None",
data_type: FrontendGraphDataType::General,
@ -589,18 +589,10 @@ fn static_nodes() -> Vec<DocumentNodeType> {
properties: node_properties::add_properties,
},
(*IMAGINATE_NODE).clone(),
/*DocumentNodeType {
DocumentNodeType {
name: "Unit Circle Generator",
category: "Vector",
identifier: NodeImplementation::proto("graphene_std::vector::generator_nodes::UnitCircleGenerator", &[]),
inputs: vec![DocumentInputType::none()],
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)],
properties: node_properties::no_properties,
},
DocumentNodeType {
name: "Unit Square Generator",
category: "Vector",
identifier: NodeImplementation::proto("graphene_std::vector::generator_nodes::UnitSquareGenerator", &[]),
identifier: NodeImplementation::proto("graphene_core::vector::generator_nodes::UnitCircleGenerator"),
inputs: vec![DocumentInputType::none()],
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)],
properties: node_properties::no_properties,
@ -608,40 +600,63 @@ fn static_nodes() -> Vec<DocumentNodeType> {
DocumentNodeType {
name: "Path Generator",
category: "Vector",
identifier: NodeImplementation::proto("graphene_core::ops::IdNode"),
identifier: NodeImplementation::proto("graphene_core::vector::generator_nodes::PathGenerator"),
inputs: vec![DocumentInputType {
name: "Path Data",
data_type: FrontendGraphDataType::Subpath,
default: NodeInput::value(TaggedValue::Subpath(Subpath::new()), false),
default: NodeInput::value(TaggedValue::Subpath(bezier_rs::Subpath::new(Vec::new(), false)), false),
}],
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)],
properties: node_properties::no_properties,
},
DocumentNodeType {
name: "Transform Subpath",
name: "Transform",
category: "Vector",
identifier: NodeImplementation::proto("graphene_std::vector::generator_nodes::TransformSubpathNode", &[]),
identifier: NodeImplementation::proto("graphene_core::vector::TransformNode<_, _, _, _>"),
inputs: vec![
DocumentInputType::new("Subpath", TaggedValue::Subpath(Subpath::empty()), true),
DocumentInputType::new("Translation", TaggedValue::DVec2(DVec2::ZERO), false),
DocumentInputType::new("Rotation", TaggedValue::F64(0.), false),
DocumentInputType::new("Scale", TaggedValue::DVec2(DVec2::ONE), false),
DocumentInputType::new("Skew", TaggedValue::DVec2(DVec2::ZERO), false),
DocumentInputType::value("Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true),
DocumentInputType::value("Translation", TaggedValue::DVec2(DVec2::ZERO), false),
DocumentInputType::value("Rotation", TaggedValue::F64(0.), false),
DocumentInputType::value("Scale", TaggedValue::DVec2(DVec2::ONE), false),
DocumentInputType::value("Skew", TaggedValue::DVec2(DVec2::ZERO), false),
],
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)],
properties: node_properties::transform_properties,
},
DocumentNodeType {
name: "Blit Subpath",
name: "Fill",
category: "Vector",
identifier: NodeImplementation::proto("graphene_std::vector::generator_nodes::BlitSubpath", &[]),
identifier: NodeImplementation::proto("graphene_core::vector::SetFillNode<_, _, _, _, _, _, _>"),
inputs: vec![
DocumentInputType::new("Image", TaggedValue::Image(Image::empty()), true),
DocumentInputType::new("Subpath", TaggedValue::Subpath(Subpath::empty()), true),
DocumentInputType::value("Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true),
DocumentInputType::value("Fill Type", TaggedValue::FillType(vector::style::FillType::Solid), false),
DocumentInputType::value("Solid Color", TaggedValue::Color(Color::BLACK), false),
DocumentInputType::value("Gradient Type", TaggedValue::GradientType(vector::style::GradientType::Linear), false),
DocumentInputType::value("Start", TaggedValue::DVec2(DVec2::new(0., 0.5)), false),
DocumentInputType::value("End", TaggedValue::DVec2(DVec2::new(1., 0.5)), false),
DocumentInputType::value("Transform", TaggedValue::DAffine2(DAffine2::IDENTITY), false),
DocumentInputType::value("Positions", TaggedValue::GradientPositions(vec![(0., Some(Color::BLACK)), (1., Some(Color::WHITE))]), false),
],
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Raster)],
properties: node_properties::no_properties,
},*/
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)],
properties: node_properties::fill_properties,
},
DocumentNodeType {
name: "Stroke",
category: "Vector",
identifier: NodeImplementation::proto("graphene_core::vector::SetStrokeNode<_, _, _, _, _, _, _>"),
inputs: vec![
DocumentInputType::value("Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true),
DocumentInputType::value("Color", TaggedValue::Color(Color::BLACK), false),
DocumentInputType::value("Weight", TaggedValue::F64(0.), false),
DocumentInputType::value("Dash Lengths", TaggedValue::VecF32(Vec::new()), false),
DocumentInputType::value("Dash Offset", TaggedValue::F64(0.), false),
DocumentInputType::value("Line Cap", TaggedValue::LineCap(graphene_core::vector::style::LineCap::Butt), false),
DocumentInputType::value("Line Join", TaggedValue::LineJoin(graphene_core::vector::style::LineJoin::Miter), false),
DocumentInputType::value("Miter Limit", TaggedValue::F64(4.), false),
],
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Subpath)],
properties: node_properties::stroke_properties,
},
]
}
@ -718,14 +733,25 @@ impl DocumentNodeType {
DocumentNodeImplementation::Network(inner_network)
}
/// Converts the [DocumentNodeType] type to a [DocumentNode], based on the inputs from the graph (which must be the correct length) and the metadata
pub fn to_document_node(&self, inputs: impl IntoIterator<Item = NodeInput>, metadata: graph_craft::document::DocumentNodeMetadata) -> DocumentNode {
let inputs: Vec<_> = inputs.into_iter().collect();
assert_eq!(inputs.len(), self.inputs.len(), "Inputs passed from the graph must be equal to the number required");
DocumentNode {
name: self.name.to_string(),
inputs: inputs.into_iter().collect(),
inputs,
implementation: self.generate_implementation(),
metadata,
}
}
/// Converts the [DocumentNodeType] type to a [DocumentNode], using the provided `input_override` and falling back to the default inputs.
/// `input_override` does not have to be the correct length.
pub fn to_document_node_default_inputs(&self, input_override: impl IntoIterator<Item = Option<NodeInput>>, metadata: graph_craft::document::DocumentNodeMetadata) -> DocumentNode {
let mut input_override = input_override.into_iter();
let inputs = self.inputs.iter().map(|default| input_override.next().unwrap_or_default().unwrap_or_else(|| default.default.clone()));
self.to_document_node(inputs, metadata)
}
}
pub fn wrap_network_in_scope(network: NodeNetwork) -> NodeNetwork {
@ -764,7 +790,7 @@ pub fn new_image_network(output_offset: i32, output_node_id: NodeId) -> NodeNetw
nodes: [
resolve_document_node_type("Input")
.expect("Input node does not exist")
.to_document_node([NodeInput::Network(concrete!(ImageFrame))], DocumentNodeMetadata::position((8, 4))),
.to_document_node_default_inputs([], DocumentNodeMetadata::position((8, 4))),
resolve_document_node_type("Output")
.expect("Output node does not exist")
.to_document_node([NodeInput::node(output_node_id, 0)], DocumentNodeMetadata::position((output_offset + 8, 4))),
@ -776,3 +802,37 @@ pub fn new_image_network(output_offset: i32, output_node_id: NodeId) -> NodeNetw
..Default::default()
}
}
pub fn new_vector_network(subpath: bezier_rs::Subpath<uuid::ManipulatorGroupId>) -> NodeNetwork {
let input = resolve_document_node_type("Input").expect("Input node does not exist");
let path_generator = resolve_document_node_type("Path Generator").expect("Path Generator node does not exist");
let transform = resolve_document_node_type("Transform").expect("Transform node does not exist");
let fill = resolve_document_node_type("Fill").expect("Fill node does not exist");
let stroke = resolve_document_node_type("Stroke").expect("Stroke node does not exist");
let output = resolve_document_node_type("Output").expect("Output node does not exist");
let mut pos = 0;
let mut next_pos = || {
let node_pos = DocumentNodeMetadata::position((pos, 4));
pos += 8;
node_pos
};
NodeNetwork {
inputs: vec![0],
outputs: vec![NodeOutput::new(5, 0)],
nodes: [
input.to_document_node_default_inputs([], next_pos()),
path_generator.to_document_node_default_inputs([Some(NodeInput::value(TaggedValue::Subpath(subpath), false))], next_pos()),
transform.to_document_node_default_inputs([Some(NodeInput::node(1, 0))], next_pos()),
fill.to_document_node_default_inputs([Some(NodeInput::node(2, 0))], next_pos()),
stroke.to_document_node_default_inputs([Some(NodeInput::node(3, 0))], next_pos()),
output.to_document_node_default_inputs([Some(NodeInput::node(4, 0))], next_pos()),
]
.into_iter()
.enumerate()
.map(|(id, node)| (id as NodeId, node))
.collect(),
..Default::default()
}
}

View file

@ -9,6 +9,7 @@ use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, NodeId, NodeInput};
use graph_craft::imaginate_input::*;
use graphene_core::raster::{BlendMode, Color, LuminanceCalculation};
use graphene_core::vector::style::{FillType, GradientType, LineCap, LineJoin};
use super::document_node_types::NodePropertiesContext;
use super::{FrontendGraphDataType, IMAGINATE_NODE};
@ -18,17 +19,20 @@ pub fn string_properties(text: impl Into<String>) -> Vec<LayoutGroup> {
vec![LayoutGroup::Row { widgets: vec![widget] }]
}
fn update_value<T>(value: impl Fn(&T) -> TaggedValue + 'static + Send + Sync, node_id: NodeId, input_index: usize) -> impl Fn(&T) -> Message + 'static + Send + Sync {
fn optionally_update_value<T>(value: impl Fn(&T) -> Option<TaggedValue> + 'static + Send + Sync, node_id: NodeId, input_index: usize) -> impl Fn(&T) -> Message + 'static + Send + Sync {
move |input_value: &T| {
NodeGraphMessage::SetInputValue {
node_id,
input_index,
value: value(input_value),
if let Some(value) = value(input_value) {
NodeGraphMessage::SetInputValue { node_id, input_index, value }.into()
} else {
Message::NoOp
}
.into()
}
}
fn update_value<T>(value: impl Fn(&T) -> TaggedValue + 'static + Send + Sync, node_id: NodeId, input_index: usize) -> impl Fn(&T) -> Message + 'static + Send + Sync {
optionally_update_value(move |v| Some(value(v)), node_id, input_index)
}
fn expose_widget(node_id: NodeId, index: usize, data_type: FrontendGraphDataType, exposed: bool) -> WidgetHolder {
ParameterExposeButton::new()
.exposed(exposed)
@ -45,6 +49,15 @@ fn expose_widget(node_id: NodeId, index: usize, data_type: FrontendGraphDataType
.widget_holder()
}
fn add_blank_assist(widgets: &mut Vec<WidgetHolder>) {
widgets.extend_from_slice(&[
WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px,
WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area.
WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists.
WidgetHolder::unrelated_separator(), // TODO: This last one is the separator after the 24px assist.
]);
}
fn start_widgets(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, data_type: FrontendGraphDataType, blank_assist: bool) -> Vec<WidgetHolder> {
let input = document_node.inputs.get(index).unwrap();
let mut widgets = vec![
@ -53,12 +66,7 @@ fn start_widgets(document_node: &DocumentNode, node_id: NodeId, index: usize, na
WidgetHolder::text_widget(name),
];
if blank_assist {
widgets.extend_from_slice(&[
WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px,
WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area.
WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists.
WidgetHolder::unrelated_separator(), // TODO: This last one is the separator after the 24px assist.
]);
add_blank_assist(&mut widgets);
}
widgets
}
@ -117,6 +125,35 @@ fn bool_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name
widgets
}
fn vec_f32_input(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, text_props: TextInput, blank_assist: bool) -> Vec<WidgetHolder> {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Color, blank_assist);
let from_string = |string: &str| {
string
.split(&[',', ' '])
.filter(|x| !x.is_empty())
.map(str::parse::<f32>)
.collect::<Result<Vec<_>, _>>()
.ok()
.map(TaggedValue::VecF32)
};
if let NodeInput::Value {
tagged_value: TaggedValue::VecF32(x),
exposed: false,
} = &document_node.inputs[index]
{
widgets.extend_from_slice(&[
WidgetHolder::unrelated_separator(),
text_props
.value(x.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", "))
.on_update(optionally_update_value(move |x: &TextInput| from_string(&x.value), node_id, index))
.widget_holder(),
])
}
widgets
}
fn number_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, number_props: NumberInput, blank_assist: bool) -> Vec<WidgetHolder> {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Number, blank_assist);
@ -191,6 +228,172 @@ fn luminance_calculation(document_node: &DocumentNode, node_id: u64, index: usiz
LayoutGroup::Row { widgets }.with_tooltip("Formula used to calculate the luminance of a pixel")
}
fn line_cap_widget(document_node: &DocumentNode, node_id: u64, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
if let &NodeInput::Value {
tagged_value: TaggedValue::LineCap(line_cap),
exposed: false,
} = &document_node.inputs[index]
{
let entries = [("Butt", LineCap::Butt), ("Round", LineCap::Round), ("Square", LineCap::Square)]
.into_iter()
.map(|(name, val)| RadioEntryData::new(name).on_update(update_value(move |_| TaggedValue::LineCap(val), node_id, index)))
.collect();
widgets.extend_from_slice(&[WidgetHolder::unrelated_separator(), RadioInput::new(entries).selected_index(line_cap as u32).widget_holder()]);
}
LayoutGroup::Row { widgets }
}
fn line_join_widget(document_node: &DocumentNode, node_id: u64, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
if let &NodeInput::Value {
tagged_value: TaggedValue::LineJoin(line_join),
exposed: false,
} = &document_node.inputs[index]
{
let entries = [("Miter", LineJoin::Miter), ("Bevel", LineJoin::Bevel), ("Round", LineJoin::Round)]
.into_iter()
.map(|(name, val)| RadioEntryData::new(name).on_update(update_value(move |_| TaggedValue::LineJoin(val), node_id, index)))
.collect();
widgets.extend_from_slice(&[WidgetHolder::unrelated_separator(), RadioInput::new(entries).selected_index(line_join as u32).widget_holder()]);
}
LayoutGroup::Row { widgets }
}
fn gradient_type_widget(document_node: &DocumentNode, node_id: u64, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
if let &NodeInput::Value {
tagged_value: TaggedValue::GradientType(gradient_type),
exposed: false,
} = &document_node.inputs[index]
{
let entries = [("Linear", GradientType::Linear), ("Radial", GradientType::Radial)]
.into_iter()
.map(|(name, val)| RadioEntryData::new(name).on_update(update_value(move |_| TaggedValue::GradientType(val), node_id, index)))
.collect();
widgets.extend_from_slice(&[WidgetHolder::unrelated_separator(), RadioInput::new(entries).selected_index(gradient_type as u32).widget_holder()]);
}
LayoutGroup::Row { widgets }
}
fn gradient_row(row: &mut Vec<WidgetHolder>, positions: &Vec<(f64, Option<Color>)>, index: usize, node_id: NodeId, input_index: usize) {
let label = TextLabel::new(format!("Gradient: {:.0}%", positions[index].0 * 100.)).tooltip("Adjustable by dragging the gradient stops in the viewport with the Gradient tool active");
row.push(label.widget_holder());
let on_update = {
let positions = positions.clone();
move |color_input: &ColorInput| {
let mut new_positions = positions.clone();
new_positions[index].1 = color_input.value;
TaggedValue::GradientPositions(new_positions)
}
};
let color = ColorInput::new(positions[index].1).on_update(update_value(on_update, node_id, input_index));
add_blank_assist(row);
row.push(WidgetHolder::unrelated_separator());
row.push(color.widget_holder());
let mut skip_separator = false;
// Remove button
if positions.len() != index + 1 && index != 0 {
let on_update = {
let in_positions = positions.clone();
move |_: &IconButton| {
let mut new_positions = in_positions.clone();
new_positions.remove(index);
TaggedValue::GradientPositions(new_positions)
}
};
skip_separator = true;
row.push(WidgetHolder::related_separator());
row.push(
IconButton::new("Remove", 16)
.tooltip("Remove this gradient stop")
.on_update(update_value(on_update, node_id, input_index))
.widget_holder(),
);
}
// Add button
if positions.len() != index + 1 {
let on_update = {
let positions = positions.clone();
move |_: &IconButton| {
let mut new_positions = positions.clone();
// Blend linearly between the two colours.
let get_color = |index: usize| match (new_positions[index].1, new_positions.get(index + 1).and_then(|x| x.1)) {
(Some(a), Some(b)) => Color::from_rgbaf32((a.r() + b.r()) / 2., (a.g() + b.g()) / 2., (a.b() + b.b()) / 2., ((a.a() + b.a()) / 2.).clamp(0., 1.)),
(Some(v), _) | (_, Some(v)) => Some(v),
_ => Some(Color::WHITE),
};
let get_pos = |index: usize| (new_positions[index].0 + new_positions.get(index + 1).map(|v| v.0).unwrap_or(1.)) / 2.;
new_positions.push((get_pos(index), get_color(index)));
new_positions.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
TaggedValue::GradientPositions(new_positions)
}
};
if !skip_separator {
row.push(WidgetHolder::related_separator());
}
row.push(
IconButton::new("Add", 16)
.tooltip("Add a gradient stop after this")
.on_update(update_value(on_update, node_id, input_index))
.widget_holder(),
);
}
}
fn gradient_positions(rows: &mut Vec<LayoutGroup>, document_node: &DocumentNode, name: &str, node_id: u64, input_index: usize) {
let mut widgets = vec![expose_widget(node_id, input_index, FrontendGraphDataType::General, document_node.inputs[input_index].is_exposed())];
widgets.push(WidgetHolder::unrelated_separator());
if let NodeInput::Value {
tagged_value: TaggedValue::GradientPositions(gradient_positions),
exposed: false,
} = &document_node.inputs[input_index]
{
for index in 0..gradient_positions.len() {
gradient_row(&mut widgets, gradient_positions, index, node_id, input_index);
let widgets = std::mem::take(&mut widgets);
rows.push(LayoutGroup::Row { widgets });
}
let on_update = {
let gradient_positions = gradient_positions.clone();
move |_: &TextButton| {
let mut new_positions = gradient_positions.clone();
new_positions = new_positions.iter().map(|(distance, color)| (1. - distance, *color)).collect();
new_positions.reverse();
TaggedValue::GradientPositions(new_positions)
}
};
let invert = TextButton::new("Invert")
.icon(Some("Swap".into()))
.tooltip("Reverse the order of each color stop")
.on_update(update_value(on_update, node_id, input_index))
.widget_holder();
if widgets.is_empty() {
widgets.push(TextLabel::new("").widget_holder());
add_blank_assist(&mut widgets);
}
widgets.push(WidgetHolder::unrelated_separator());
widgets.push(invert);
rows.push(LayoutGroup::Row { widgets });
} else {
widgets.push(TextLabel::new(name).widget_holder());
rows.push(LayoutGroup::Row { widgets })
}
}
fn color_widget(document_node: &DocumentNode, node_id: u64, index: usize, name: &str, color_props: ColorInput, blank_assist: bool) -> LayoutGroup {
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Number, blank_assist);
@ -365,11 +568,11 @@ pub fn add_properties(document_node: &DocumentNode, node_id: NodeId, _context: &
vec![operand("Input", 0), operand("Addend", 1)]
}
pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext, blank_assist: bool) -> Vec<LayoutGroup> {
pub fn transform_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let translation = {
let index = 1;
let mut widgets = start_widgets(document_node, node_id, index, "Translation", FrontendGraphDataType::Vector, blank_assist);
let mut widgets = start_widgets(document_node, node_id, index, "Translation", FrontendGraphDataType::Vector, true);
if let NodeInput::Value {
tagged_value: TaggedValue::DVec2(vec2),
@ -383,7 +586,7 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId, _con
.unit(" px")
.on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(input.value.unwrap(), vec2.y)), node_id, index))
.widget_holder(),
WidgetHolder::unrelated_separator(),
WidgetHolder::related_separator(),
NumberInput::new(Some(vec2.y))
.label("Y")
.unit(" px")
@ -398,7 +601,7 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId, _con
let rotation = {
let index = 2;
let mut widgets = start_widgets(document_node, node_id, index, "Rotation", FrontendGraphDataType::Number, blank_assist);
let mut widgets = start_widgets(document_node, node_id, index, "Rotation", FrontendGraphDataType::Number, true);
if let NodeInput::Value {
tagged_value: TaggedValue::F64(val),
@ -423,7 +626,7 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId, _con
let scale = {
let index = 3;
let mut widgets = start_widgets(document_node, node_id, index, "Scale", FrontendGraphDataType::Vector, blank_assist);
let mut widgets = start_widgets(document_node, node_id, index, "Scale", FrontendGraphDataType::Vector, true);
if let NodeInput::Value {
tagged_value: TaggedValue::DVec2(vec2),
@ -436,7 +639,7 @@ pub fn _transform_properties(document_node: &DocumentNode, node_id: NodeId, _con
.label("X")
.on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(input.value.unwrap(), vec2.y)), node_id, index))
.widget_holder(),
WidgetHolder::unrelated_separator(),
WidgetHolder::related_separator(),
NumberInput::new(Some(vec2.y))
.label("Y")
.on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(vec2.x, input.value.unwrap())), node_id, index))
@ -996,3 +1199,86 @@ pub fn generate_node_properties(document_node: &DocumentNode, node_id: NodeId, c
};
LayoutGroup::Section { name, layout }
}
pub fn stroke_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let color_index = 1;
let weight_index = 2;
let dash_lengths_index = 3;
let dash_offset_index = 4;
let line_cap_index = 5;
let line_join_index = 6;
let miter_limit_index = 7;
let color = color_widget(document_node, node_id, color_index, "Color", ColorInput::default(), true);
let weight = number_widget(document_node, node_id, weight_index, "Weight", NumberInput::default().unit("px").min(0.), true);
let dash_lengths = vec_f32_input(document_node, node_id, dash_lengths_index, "Dash Lengths", TextInput::default().centered(true), true);
let dash_offset = number_widget(document_node, node_id, dash_offset_index, "Dash Offset", NumberInput::default().unit("px").min(0.), true);
let line_cap = line_cap_widget(document_node, node_id, line_cap_index, "Line Cap", true);
let line_join = line_join_widget(document_node, node_id, line_join_index, "Line Join", true);
let miter_limit = number_widget(document_node, node_id, miter_limit_index, "Miter Limit", NumberInput::default().min(0.), true);
vec![
color,
LayoutGroup::Row { widgets: weight },
LayoutGroup::Row { widgets: dash_lengths },
LayoutGroup::Row { widgets: dash_offset },
line_cap,
line_join,
LayoutGroup::Row { widgets: miter_limit },
]
}
pub fn fill_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
let fill_type_index = 1;
let solid_color_index = 2;
let gradient_type_index = 3;
let positions_index = 7;
let fill_type = if let &NodeInput::Value {
tagged_value: TaggedValue::FillType(fill_type),
..
} = &document_node.inputs[fill_type_index]
{
Some(fill_type)
} else {
None
};
let mut widgets = Vec::new();
let gradient = fill_type == Some(graphene_core::vector::style::FillType::Gradient);
let solid = fill_type == Some(graphene_core::vector::style::FillType::Solid);
if fill_type.is_none() || solid {
let solid_color = color_widget(document_node, node_id, solid_color_index, "Color", ColorInput::default(), true);
widgets.push(solid_color);
}
if fill_type.is_none() || gradient {
let gradient_type = gradient_type_widget(document_node, node_id, gradient_type_index, "Gradient Type", true);
widgets.push(gradient_type);
gradient_positions(&mut widgets, document_node, "Gradient Positions", node_id, positions_index);
}
if gradient || solid {
let new_fill_type = if gradient { FillType::Solid } else { FillType::Gradient };
let switch_button = TextButton::new(if gradient { "Use Solid Color" } else { "Use Gradient" })
.tooltip(if gradient {
"Change this fill from a gradient to a solid color, keeping the 0% stop color"
} else {
"Change this fill from a solid color to a gradient"
})
.on_update(update_value(move |_| TaggedValue::FillType(new_fill_type), node_id, fill_type_index));
widgets.push(LayoutGroup::Row {
widgets: {
let mut widgets = Vec::new();
widgets.push(TextLabel::new("").widget_holder());
add_blank_assist(&mut widgets);
widgets.push(WidgetHolder::unrelated_separator());
widgets.push(switch_button.widget_holder());
widgets
},
});
}
widgets
}

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
}

View file

@ -6,7 +6,7 @@ license = "MIT OR Apache-2.0"
[features]
default = []
serde = ["dep:serde", "graphene-core/serde", "glam/serde"]
serde = ["dep:serde", "graphene-core/serde", "glam/serde", "bezier-rs/serde"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -19,6 +19,7 @@ log = "0.4"
serde = { version = "1", features = ["derive", "rc"], optional = true }
glam = { version = "0.22" }
base64 = "0.13"
bezier-rs = { path = "../../libraries/bezier-rs", features = ["dyn-any"] }
specta.workspace = true
bytemuck = {version = "1.8" }

View file

@ -33,7 +33,7 @@ impl DocumentNodeMetadata {
}
}
#[derive(Clone, Debug, PartialEq, specta::Type)]
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DocumentNode {
pub name: String,
@ -122,7 +122,7 @@ impl DocumentNode {
}
}
#[derive(Debug, Clone, PartialEq, Hash, specta::Type)]
#[derive(Debug, Clone, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum NodeInput {
Node { node_id: NodeId, output_index: usize, lambda: bool },
@ -165,7 +165,7 @@ impl NodeInput {
}
}
#[derive(Clone, Debug, PartialEq, specta::Type)]
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DocumentNodeImplementation {
Network(NodeNetwork),
@ -202,7 +202,7 @@ impl NodeOutput {
}
}
#[derive(Clone, Debug, Default, PartialEq, DynAny, specta::Type)]
#[derive(Clone, Debug, Default, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NodeNetwork {
pub inputs: Vec<NodeId>,

View file

@ -11,7 +11,7 @@ use crate::executor::Any;
pub use crate::imaginate_input::{ImaginateMaskStartingFill, ImaginateSamplingMethod, ImaginateStatus};
/// A type that is known, allowing serialization (serde::Deserialize is not object safe)
#[derive(Clone, Debug, PartialEq, specta::Type)]
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TaggedValue {
None,
@ -27,17 +27,26 @@ pub enum TaggedValue {
RcImage(Option<Arc<graphene_core::raster::Image>>),
ImageFrame(graphene_core::raster::ImageFrame),
Color(graphene_core::raster::color::Color),
Subpath(graphene_core::vector::subpath::Subpath),
RcSubpath(Arc<graphene_core::vector::subpath::Subpath>),
Subpath(bezier_rs::Subpath<graphene_core::uuid::ManipulatorGroupId>),
RcSubpath(Arc<bezier_rs::Subpath<graphene_core::uuid::ManipulatorGroupId>>),
BlendMode(BlendMode),
LuminanceCalculation(LuminanceCalculation),
ImaginateSamplingMethod(ImaginateSamplingMethod),
ImaginateMaskStartingFill(ImaginateMaskStartingFill),
ImaginateStatus(ImaginateStatus),
LayerPath(Option<Vec<u64>>),
VectorData(graphene_core::vector::VectorData),
Fill(graphene_core::vector::style::Fill),
Stroke(graphene_core::vector::style::Stroke),
VecF32(Vec<f32>),
LineCap(graphene_core::vector::style::LineCap),
LineJoin(graphene_core::vector::style::LineJoin),
FillType(graphene_core::vector::style::FillType),
GradientType(graphene_core::vector::style::GradientType),
GradientPositions(Vec<(f64, Option<graphene_core::Color>)>),
}
#[allow(clippy::derive_hash_xor_eq)]
#[allow(clippy::derived_hash_with_manual_eq)]
impl Hash for TaggedValue {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
@ -120,10 +129,52 @@ impl Hash for TaggedValue {
p.hash(state)
}
Self::ImageFrame(i) => {
20.hash(state);
21.hash(state);
i.image.hash(state);
i.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state))
}
Self::VectorData(vector_data) => {
22.hash(state);
vector_data.subpaths.hash(state);
vector_data.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state));
vector_data.style.hash(state);
}
Self::Fill(fill) => {
23.hash(state);
fill.hash(state);
}
Self::Stroke(stroke) => {
24.hash(state);
stroke.hash(state);
}
Self::VecF32(vec_f32) => {
25.hash(state);
vec_f32.iter().for_each(|val| val.to_bits().hash(state));
}
Self::LineCap(line_cap) => {
26.hash(state);
line_cap.hash(state);
}
Self::LineJoin(line_join) => {
27.hash(state);
line_join.hash(state);
}
Self::FillType(fill_type) => {
28.hash(state);
fill_type.hash(state);
}
Self::GradientType(gradient_type) => {
29.hash(state);
gradient_type.hash(state);
}
Self::GradientPositions(gradient_positions) => {
30.hash(state);
gradient_positions.len().hash(state);
for (position, color) in gradient_positions {
position.to_bits().hash(state);
color.hash(state);
}
}
}
}
}
@ -153,6 +204,15 @@ impl<'a> TaggedValue {
TaggedValue::ImaginateMaskStartingFill(x) => Box::new(x),
TaggedValue::ImaginateStatus(x) => Box::new(x),
TaggedValue::LayerPath(x) => Box::new(x),
TaggedValue::VectorData(x) => Box::new(x),
TaggedValue::Fill(x) => Box::new(x),
TaggedValue::Stroke(x) => Box::new(x),
TaggedValue::VecF32(x) => Box::new(x),
TaggedValue::LineCap(x) => Box::new(x),
TaggedValue::LineJoin(x) => Box::new(x),
TaggedValue::FillType(x) => Box::new(x),
TaggedValue::GradientType(x) => Box::new(x),
TaggedValue::GradientPositions(x) => Box::new(x),
}
}
@ -172,8 +232,8 @@ impl<'a> TaggedValue {
TaggedValue::RcImage(_) => concrete!(Option<Arc<graphene_core::raster::Image>>),
TaggedValue::ImageFrame(_) => concrete!(graphene_core::raster::ImageFrame),
TaggedValue::Color(_) => concrete!(graphene_core::raster::Color),
TaggedValue::Subpath(_) => concrete!(graphene_core::vector::subpath::Subpath),
TaggedValue::RcSubpath(_) => concrete!(Arc<graphene_core::vector::subpath::Subpath>),
TaggedValue::Subpath(_) => concrete!(bezier_rs::Subpath<graphene_core::uuid::ManipulatorGroupId>),
TaggedValue::RcSubpath(_) => concrete!(Arc<bezier_rs::Subpath<graphene_core::uuid::ManipulatorGroupId>>),
TaggedValue::BlendMode(_) => concrete!(BlendMode),
TaggedValue::ImaginateSamplingMethod(_) => concrete!(ImaginateSamplingMethod),
TaggedValue::ImaginateMaskStartingFill(_) => concrete!(ImaginateMaskStartingFill),
@ -181,6 +241,15 @@ impl<'a> TaggedValue {
TaggedValue::LayerPath(_) => concrete!(Option<Vec<u64>>),
TaggedValue::DAffine2(_) => concrete!(DAffine2),
TaggedValue::LuminanceCalculation(_) => concrete!(LuminanceCalculation),
TaggedValue::VectorData(_) => concrete!(graphene_core::vector::VectorData),
TaggedValue::Fill(_) => concrete!(graphene_core::vector::style::Fill),
TaggedValue::Stroke(_) => concrete!(graphene_core::vector::style::Stroke),
TaggedValue::VecF32(_) => concrete!(Vec<f32>),
TaggedValue::LineCap(_) => concrete!(graphene_core::vector::style::LineCap),
TaggedValue::LineJoin(_) => concrete!(graphene_core::vector::style::LineJoin),
TaggedValue::FillType(_) => concrete!(graphene_core::vector::style::FillType),
TaggedValue::GradientType(_) => concrete!(graphene_core::vector::style::GradientType),
TaggedValue::GradientPositions(_) => concrete!(Vec<(f64, Option<graphene_core::Color>)>),
}
}
}

View file

@ -14,7 +14,7 @@ pub enum ImaginateStatus {
Terminated,
}
#[allow(clippy::derive_hash_xor_eq)]
#[allow(clippy::derived_hash_with_manual_eq)]
impl core::hash::Hash for ImaginateStatus {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
match self {

View file

@ -1,6 +1,7 @@
use glam::{DAffine2, DVec2};
use graph_craft::imaginate_input::{ImaginateMaskStartingFill, ImaginateSamplingMethod, ImaginateStatus};
use graphene_core::ops::{CloneNode, IdNode, TypeNode};
use graphene_core::vector::VectorData;
use once_cell::sync::Lazy;
use std::collections::HashMap;
@ -294,6 +295,15 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
],
register_node!(graphene_core::structural::ConsNode<_, _>, input: Image, params: [&str]),
register_node!(graphene_std::raster::ImageFrameNode<_>, input: Image, params: [DAffine2]),
register_node!(graphene_core::vector::TransformNode<_, _, _, _>, input: VectorData, params: [DVec2, f64, DVec2, DVec2]),
register_node!(graphene_core::vector::SetFillNode<_, _, _, _, _, _, _>, input: VectorData, params: [ graphene_core::vector::style::FillType, graphene_core::Color, graphene_core::vector::style::GradientType, DVec2, DVec2, DAffine2, Vec<(f64, Option<graphene_core::Color>)>]),
register_node!(graphene_core::vector::SetStrokeNode<_, _, _, _, _, _, _>, input: VectorData, params: [graphene_core::Color, f64, Vec<f32>, f64, graphene_core::vector::style::LineCap, graphene_core::vector::style::LineJoin, f64]),
register_node!(graphene_core::vector::generator_nodes::UnitCircleGenerator, input: (), params: []),
register_node!(
graphene_core::vector::generator_nodes::PathGenerator,
input: graphene_core::vector::bezier_rs::Subpath<graphene_core::uuid::ManipulatorGroupId>,
params: []
),
/*
(NodeIdentifier::new("graphene_std::raster::ImageNode", &[concrete!("&str")]), |_proto_node, stack| {
stack.push_fn(|_nodes| {

View file

@ -42,7 +42,7 @@ pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream {
// Extract primary input as first argument
let primary_input = function_inputs.next().expect("Primary input required - set to `()` if not needed.");
let Pat::Ident(PatIdent{ident: primary_input_ident,..} ) =&*primary_input.pat else {
let Pat::Ident(PatIdent{ident: primary_input_ident, mutability: primary_input_mutability,..} ) =&*primary_input.pat else {
panic!("Expected ident as primary input.");
};
let primary_input_ty = &primary_input.ty;
@ -51,13 +51,15 @@ pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream {
// Extract secondary inputs as all other arguments
let parameter_inputs = function_inputs.collect::<Vec<_>>();
let parameter_idents = parameter_inputs
let parameter_pat_ident_patterns = parameter_inputs
.iter()
.map(|input| {
let Pat::Ident(PatIdent { ident: primary_input_ident,.. }) = &*input.pat else { panic!("Expected ident for secondary input."); };
primary_input_ident
let Pat::Ident(pat_ident) = &*input.pat else { panic!("Expected ident for secondary input."); };
pat_ident
})
.collect::<Vec<_>>();
let parameter_idents = parameter_pat_ident_patterns.iter().map(|pat_ident| &pat_ident.ident).collect::<Vec<_>>();
let parameter_mutability = parameter_pat_ident_patterns.iter().map(|pat_ident| &pat_ident.mutability);
// Extract the output type of the entire node - `()` by default
let output = if let ReturnType::Type(_, ty) = &function.sig.output {
@ -129,9 +131,9 @@ pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream {
{
type Output = #output;
#[inline]
fn eval<'node: 'input>(&'node self, #primary_input_ident: #primary_input_ty) -> Self::Output {
fn eval<'node: 'input>(&'node self, #primary_input_mutability #primary_input_ident: #primary_input_ty) -> Self::Output {
#(
let #parameter_idents = self.#parameter_idents.eval(());
let #parameter_mutability #parameter_idents = self.#parameter_idents.eval(());
)*
#body