Add some additional image effect nodes (#869)

* Move the Subpath type to graphene-std

* Add the transform subpath node

* Delete selected nodes

* Inserting node list on right click

* Add several bitmap manipulator nodes

* Convert add node to use f64

* Add posterize node

* Rename names randomly

* Fix naming

* Exposure node

* Fix typo

* Adjust exposure node range

* Comment out vector nodes

* Adjust exposure range again

* Posterise as ints

* Rename input

* Use >= in the to hsl function
This commit is contained in:
0HyperCube 2022-12-03 22:29:45 +00:00 committed by Keavon Chambers
parent 59b638e4e4
commit eb9848365f
50 changed files with 1310 additions and 481 deletions

7
Cargo.lock generated
View File

@ -223,6 +223,7 @@ name = "dyn-any"
version = "0.2.1"
dependencies = [
"dyn-any-derive",
"glam",
"log",
]
@ -357,6 +358,7 @@ dependencies = [
"borrow_stack",
"dyn-any",
"dyn-clone",
"glam",
"graphene-core",
"graphene-std",
"log",
@ -390,12 +392,16 @@ dependencies = [
name = "graphene-std"
version = "0.1.0"
dependencies = [
"bezier-rs",
"borrow_stack",
"dyn-any",
"dyn-clone",
"glam",
"graph-proc-macros",
"graphene-core",
"image",
"kurbo",
"log",
"once_cell",
"proc-macro2",
"quote",
@ -440,6 +446,7 @@ dependencies = [
"bezier-rs",
"glam",
"graph-craft",
"graphene-std",
"image",
"kurbo",
"log",

View File

@ -27,6 +27,10 @@ pub fn default_mapping() -> Mapping {
),
// NORMAL PRIORITY:
//
// NodeGraphMessage
entry!(KeyDown(Delete); action_dispatch=NodeGraphMessage::DeleteSelectedNodes),
entry!(KeyDown(Backspace); action_dispatch=NodeGraphMessage::DeleteSelectedNodes),
//
// TransformLayerMessage
entry!(KeyDown(Enter); action_dispatch=TransformLayerMessage::ApplyTransformOperation),
entry!(KeyDown(Lmb); action_dispatch=TransformLayerMessage::ApplyTransformOperation),

View File

@ -263,6 +263,18 @@ impl WidgetHolder {
pub fn new(widget: Widget) -> Self {
Self { widget_id: generate_uuid(), widget }
}
pub fn unrelated_seperator() -> Self {
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
}))
}
pub fn text_widget(text: impl Into<String>) -> Self {
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: text.into(),
..Default::default()
}))
}
}
#[derive(Clone)]

View File

@ -29,8 +29,8 @@ use graphene::layers::imaginate_layer::{ImaginateBaseImage, ImaginateGenerationP
use graphene::layers::layer_info::{LayerDataType, LayerDataTypeDiscriminant};
use graphene::layers::style::{Fill, RenderData, ViewMode};
use graphene::layers::text_layer::{Font, FontCache};
use graphene::layers::vector::subpath::Subpath;
use graphene::{DocumentError, DocumentResponse, LayerId, Operation as DocumentOperation};
use graphene_std::vector::subpath::Subpath;
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
@ -928,6 +928,7 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
}
common.extend(self.navigation_handler.actions());
common.extend(self.transform_layer_handler.actions());
common.extend(self.node_graph_handler.actions());
common
}
}

View File

@ -15,12 +15,14 @@ pub enum NodeGraphMessage {
CreateNode {
// Having the caller generate the id means that we don't have to return it. This can be a random u64.
node_id: Option<NodeId>,
// I don't really know what this is for (perhaps a user identifiable name).
node_type: String,
x: i32,
y: i32,
},
DeleteNode {
node_id: NodeId,
},
DeleteSelectedNodes,
ExposeInput {
node_id: NodeId,
input_index: usize,

View File

@ -1,6 +1,6 @@
use crate::messages::layout::utility_types::layout_widget::LayoutGroup;
use crate::messages::prelude::*;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, DocumentNodeMetadata, NodeInput, NodeNetwork};
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, DocumentNodeMetadata, NodeId, NodeInput, NodeNetwork};
use graphene::document::Document;
use graphene::layers::layer_info::LayerDataType;
use graphene::layers::nodegraph_layer::NodeGraphFrameLayer;
@ -18,9 +18,13 @@ pub enum FrontendGraphDataType {
#[serde(rename = "color")]
Color,
#[serde(rename = "vector")]
Vector,
Subpath,
#[serde(rename = "number")]
Number,
#[serde(rename = "boolean")]
Boolean,
#[serde(rename = "vec2")]
Vector,
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
@ -57,10 +61,14 @@ pub struct FrontendNodeLink {
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FrontendNodeType {
pub name: String,
pub category: String,
}
impl FrontendNodeType {
pub fn new(name: &'static str) -> Self {
Self { name: name.to_string() }
pub fn new(name: &'static str, category: &'static str) -> Self {
Self {
name: name.to_string(),
category: category.to_string(),
}
}
}
@ -148,6 +156,54 @@ impl NodeGraphMessageHandler {
log::debug!("Nodes:\n{:#?}\n\nFrontend Nodes:\n{:#?}\n\nLinks:\n{:#?}", network.nodes, nodes, links);
responses.push_back(FrontendMessage::UpdateNodeGraph { nodes, links }.into());
}
fn remove_node(&mut self, network: &mut NodeNetwork, node_id: NodeId) -> bool {
fn remove_from_network(network: &mut NodeNetwork, node_id: NodeId) -> bool {
if network.inputs.iter().any(|&id| id == node_id) {
warn!("Deleting input node");
return false;
}
if network.output == node_id {
warn!("Deleting the output node!");
return false;
}
for (id, node) in network.nodes.iter_mut() {
if *id == node_id {
continue;
}
for (input_index, input) in node.inputs.iter_mut().enumerate() {
let NodeInput::Node(id) = input else {
continue;
};
if *id != node_id {
continue;
}
let Some(node_type) = document_node_types::resolve_document_node_type(&node.name) else{
warn!("Removing input of invalid node type '{}'", node.name);
return false;
};
if let NodeInput::Value { tagged_value, .. } = &node_type.inputs[input_index].default {
*input = NodeInput::Value {
tagged_value: tagged_value.clone(),
exposed: true,
};
}
}
if let DocumentNodeImplementation::Network(network) = &mut node.implementation {
remove_from_network(network, node_id);
}
}
true
}
if remove_from_network(network, node_id) {
network.nodes.remove(&node_id);
self.selected_nodes.retain(|&id| id != node_id);
true
} else {
false
}
}
}
impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageHandler)> for NodeGraphMessageHandler {
@ -188,7 +244,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
Self::send_graph(network, responses);
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
}
NodeGraphMessage::CreateNode { node_id, node_type } => {
NodeGraphMessage::CreateNode { node_id, node_type, x, y } => {
let node_id = node_id.unwrap_or_else(crate::application::generate_uuid);
let Some(network) = self.get_active_network_mut(document) else{
warn!("No network");
@ -218,7 +274,6 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
.into_iter()
.collect(),
};
let far_right_node = network.nodes.iter().map(|node| node.1.metadata.position).max_by_key(|pos| pos.0).unwrap_or_default();
network.nodes.insert(
node_id,
@ -227,19 +282,29 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
inputs: document_node_type.inputs.iter().map(|input| input.default.clone()).collect(),
// TODO: Allow inserting nodes that contain other nodes.
implementation: DocumentNodeImplementation::Network(inner_network),
metadata: graph_craft::document::DocumentNodeMetadata {
// TODO: Better position default
position: (far_right_node.0 + 7, far_right_node.1 + 2),
},
metadata: graph_craft::document::DocumentNodeMetadata { position: (x, y) },
},
);
Self::send_graph(network, responses);
}
NodeGraphMessage::DeleteNode { node_id } => {
if let Some(network) = self.get_active_network_mut(document) {
network.nodes.remove(&node_id);
Self::send_graph(network, responses);
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
if self.remove_node(network, node_id) {
Self::send_graph(network, responses);
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
}
}
}
NodeGraphMessage::DeleteSelectedNodes => {
if let Some(network) = self.get_active_network_mut(document) {
let mut modified = false;
for node_id in self.selected_nodes.clone() {
modified = modified || self.remove_node(network, node_id);
}
if modified {
Self::send_graph(network, responses);
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
}
}
}
NodeGraphMessage::ExposeInput { node_id, input_index, new_exposed } => {
@ -314,5 +379,11 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
}
}
advertise_actions!(NodeGraphMessageDiscriminant; DeleteNode,);
fn actions(&self) -> ActionList {
if self.layer_path.is_some() && !self.selected_nodes.is_empty() {
actions!(NodeGraphMessageDiscriminant; DeleteSelectedNodes,)
} else {
actions!(NodeGraphMessageDiscriminant;)
}
}
}

View File

@ -1,11 +1,13 @@
use super::{FrontendGraphDataType, FrontendNodeType};
use super::{node_properties, FrontendGraphDataType, FrontendNodeType};
use crate::messages::layout::utility_types::layout_widget::{LayoutGroup, Widget, WidgetHolder};
use crate::messages::layout::utility_types::widgets::label_widgets::TextLabel;
use glam::DVec2;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, NodeId, NodeInput};
use graph_craft::proto::{NodeIdentifier, Type};
use graphene_std::raster::Image;
use graphene_std::vector::subpath::Subpath;
use std::borrow::Cow;
@ -15,8 +17,19 @@ pub struct DocumentInputType {
pub default: NodeInput,
}
impl DocumentInputType {
pub const fn none() -> Self {
Self {
name: "None",
data_type: FrontendGraphDataType::General,
default: NodeInput::value(TaggedValue::None, false),
}
}
}
pub struct DocumentNodeType {
pub name: &'static str,
pub category: &'static str,
pub identifier: NodeIdentifier,
pub inputs: &'static [DocumentInputType],
pub outputs: &'static [FrontendGraphDataType],
@ -24,9 +37,10 @@ pub struct DocumentNodeType {
}
// TODO: Dynamic node library
static DOCUMENT_NODE_TYPES: [DocumentNodeType; 7] = [
static DOCUMENT_NODE_TYPES: &[DocumentNodeType] = &[
DocumentNodeType {
name: "Identity",
category: "Meta",
identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Concrete(Cow::Borrowed("Any<'_>"))]),
inputs: &[DocumentInputType {
name: "In",
@ -45,6 +59,7 @@ static DOCUMENT_NODE_TYPES: [DocumentNodeType; 7] = [
},
DocumentNodeType {
name: "Input",
category: "Meta",
identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Concrete(Cow::Borrowed("Any<'_>"))]),
inputs: &[DocumentInputType {
name: "In",
@ -52,129 +67,273 @@ static DOCUMENT_NODE_TYPES: [DocumentNodeType; 7] = [
default: NodeInput::Network,
}],
outputs: &[FrontendGraphDataType::Raster],
properties: |_document_node, _node_id| {
vec![LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "The input to the graph is the bitmap under the frame".to_string(),
..Default::default()
}))],
}]
},
properties: |_document_node, _node_id| node_properties::string_properties("The input to the graph is the bitmap under the frame".to_string()),
},
DocumentNodeType {
name: "Output",
category: "Meta",
identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Concrete(Cow::Borrowed("Any<'_>"))]),
inputs: &[DocumentInputType {
name: "In",
data_type: FrontendGraphDataType::Raster,
default: NodeInput::Value {
tagged_value: TaggedValue::Image(Image::empty()),
exposed: true,
},
default: NodeInput::value(TaggedValue::Image(Image::empty()), true),
}],
outputs: &[],
properties: |_document_node, _node_id| {
vec![LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "The output to the graph is rendered in the frame".to_string(),
..Default::default()
}))],
}]
},
properties: |_document_node, _node_id| node_properties::string_properties("The output to the graph is rendered in the frame".to_string()),
},
DocumentNodeType {
name: "Grayscale Image",
category: "Image Color Correction",
identifier: NodeIdentifier::new("graphene_std::raster::GrayscaleImageNode", &[]),
inputs: &[DocumentInputType {
name: "Image",
data_type: FrontendGraphDataType::Raster,
default: NodeInput::Value {
tagged_value: TaggedValue::Image(Image::empty()),
exposed: true,
},
default: NodeInput::value(TaggedValue::Image(Image::empty()), true),
}],
outputs: &[FrontendGraphDataType::Raster],
properties: |_document_node, _node_id| {
vec![LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "The output to the graph is rendered in the frame".to_string(),
..Default::default()
}))],
}]
},
properties: node_properties::no_properties,
},
DocumentNodeType {
name: "Brighten Image",
identifier: NodeIdentifier::new("graphene_std::raster::BrightenImageNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
name: "Invert Image Color",
category: "Image Color Correction",
identifier: NodeIdentifier::new("graphene_std::raster::InvertImageColorNode", &[]),
inputs: &[DocumentInputType {
name: "Image",
data_type: FrontendGraphDataType::Raster,
default: NodeInput::value(TaggedValue::Image(Image::empty()), true),
}],
outputs: &[FrontendGraphDataType::Raster],
properties: node_properties::no_properties,
},
DocumentNodeType {
name: "Shift Image HSL",
category: "Image Color Correction",
identifier: NodeIdentifier::new("graphene_std::raster::ShiftImageHSLNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
inputs: &[
DocumentInputType {
name: "Image",
data_type: FrontendGraphDataType::Raster,
default: NodeInput::Value {
tagged_value: TaggedValue::Image(Image::empty()),
exposed: true,
},
default: NodeInput::value(TaggedValue::Image(Image::empty()), true),
},
DocumentInputType {
name: "Amount",
name: "Hue Shift",
data_type: FrontendGraphDataType::Number,
default: NodeInput::Value {
tagged_value: TaggedValue::F32(10.),
exposed: false,
},
default: NodeInput::value(TaggedValue::F64(0.), false),
},
DocumentInputType {
name: "Saturation Shift",
data_type: FrontendGraphDataType::Number,
default: NodeInput::value(TaggedValue::F64(0.), false),
},
DocumentInputType {
name: "Luminance Shift",
data_type: FrontendGraphDataType::Number,
default: NodeInput::value(TaggedValue::F64(0.), false),
},
],
outputs: &[FrontendGraphDataType::Raster],
properties: super::node_properties::brighten_image_properties,
properties: node_properties::adjust_hsl_properties,
},
DocumentNodeType {
name: "Hue Shift Image",
identifier: NodeIdentifier::new("graphene_std::raster::HueShiftImage", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
name: "Image Contrast and Brightness",
category: "Image Color Correction",
identifier: NodeIdentifier::new("graphene_std::raster::ImageBrightnessAndContrast", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
inputs: &[
DocumentInputType {
name: "Image",
data_type: FrontendGraphDataType::Raster,
default: NodeInput::Value {
tagged_value: TaggedValue::Image(Image::empty()),
exposed: true,
},
default: NodeInput::value(TaggedValue::Image(Image::empty()), true),
},
DocumentInputType {
name: "Amount",
name: "Brightness",
data_type: FrontendGraphDataType::Number,
default: NodeInput::Value {
tagged_value: TaggedValue::F32(10.),
exposed: false,
},
default: NodeInput::value(TaggedValue::F64(0.), false),
},
DocumentInputType {
name: "Contrast",
data_type: FrontendGraphDataType::Number,
default: NodeInput::value(TaggedValue::F64(0.), false),
},
],
outputs: &[FrontendGraphDataType::Raster],
properties: super::node_properties::hue_shift_image_properties,
properties: node_properties::brighten_image_properties,
},
DocumentNodeType {
name: "Adjust Image Gamma",
category: "Image Color Correction",
identifier: NodeIdentifier::new("graphene_std::raster::ImageGammaNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
inputs: &[
DocumentInputType {
name: "Image",
data_type: FrontendGraphDataType::Raster,
default: NodeInput::value(TaggedValue::Image(Image::empty()), true),
},
DocumentInputType {
name: "Gamma",
data_type: FrontendGraphDataType::Number,
default: NodeInput::value(TaggedValue::F64(1.), false),
},
],
outputs: &[FrontendGraphDataType::Raster],
properties: node_properties::adjust_gamma_properties,
},
DocumentNodeType {
name: "Multiply Image Opactiy",
category: "Image Color Correction",
identifier: NodeIdentifier::new("graphene_std::raster::ImageOpacityNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
inputs: &[
DocumentInputType {
name: "Image",
data_type: FrontendGraphDataType::Raster,
default: NodeInput::value(TaggedValue::Image(Image::empty()), true),
},
DocumentInputType {
name: "Factor",
data_type: FrontendGraphDataType::Number,
default: NodeInput::value(TaggedValue::F64(1.), false),
},
],
outputs: &[FrontendGraphDataType::Raster],
properties: node_properties::multiply_opacity,
},
DocumentNodeType {
name: "Posterize",
category: "Image Filters",
identifier: NodeIdentifier::new("graphene_std::raster::Posterize", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
inputs: &[
DocumentInputType {
name: "Image",
data_type: FrontendGraphDataType::Raster,
default: NodeInput::value(TaggedValue::Image(Image::empty()), true),
},
DocumentInputType {
name: "Value",
data_type: FrontendGraphDataType::Number,
default: NodeInput::value(TaggedValue::F64(5.), false),
},
],
outputs: &[FrontendGraphDataType::Raster],
properties: node_properties::posterize_properties,
},
DocumentNodeType {
name: "Exposure",
category: "Image Color Correction",
identifier: NodeIdentifier::new("graphene_std::raster::ExposureNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
inputs: &[
DocumentInputType {
name: "Image",
data_type: FrontendGraphDataType::Raster,
default: NodeInput::value(TaggedValue::Image(Image::empty()), true),
},
DocumentInputType {
name: "Value",
data_type: FrontendGraphDataType::Number,
default: NodeInput::value(TaggedValue::F64(1.), false),
},
],
outputs: &[FrontendGraphDataType::Raster],
properties: node_properties::exposure_properties,
},
DocumentNodeType {
name: "Add",
category: "Mathmatics",
identifier: NodeIdentifier::new("graphene_core::ops::AddNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
inputs: &[
DocumentInputType {
name: "Input",
data_type: FrontendGraphDataType::Number,
default: NodeInput::Value {
tagged_value: TaggedValue::F32(0.),
exposed: true,
},
default: NodeInput::value(TaggedValue::F64(0.), true),
},
DocumentInputType {
name: "Addend",
data_type: FrontendGraphDataType::Number,
default: NodeInput::Value {
tagged_value: TaggedValue::F32(0.),
exposed: true,
},
default: NodeInput::value(TaggedValue::F64(0.), true),
},
],
outputs: &[FrontendGraphDataType::Number],
properties: super::node_properties::add_properties,
properties: node_properties::add_properties,
},
/*DocumentNodeType {
name: "Unit Circle Generator",
category: "Vector",
identifier: NodeIdentifier::new("graphene_std::vector::generator_nodes::UnitCircleGenerator", &[]),
inputs: &[DocumentInputType::none()],
outputs: &[FrontendGraphDataType::Subpath],
properties: node_properties::no_properties,
},
DocumentNodeType {
name: "Unit Square Generator",
category: "Vector",
identifier: NodeIdentifier::new("graphene_std::vector::generator_nodes::UnitSquareGenerator", &[]),
inputs: &[DocumentInputType::none()],
outputs: &[FrontendGraphDataType::Subpath],
properties: node_properties::no_properties,
},
DocumentNodeType {
name: "Path Generator",
category: "Vector",
identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Concrete(Cow::Borrowed("Any<'_>"))]),
inputs: &[DocumentInputType {
name: "Path Data",
data_type: FrontendGraphDataType::Subpath,
default: NodeInput::value(TaggedValue::Subpath(Subpath::new()), false),
}],
outputs: &[FrontendGraphDataType::Subpath],
properties: node_properties::no_properties,
},
DocumentNodeType {
name: "Transform Subpath",
category: "Vector",
identifier: NodeIdentifier::new("graphene_std::vector::generator_nodes::TransformSubpathNode", &[]),
inputs: &[
DocumentInputType {
name: "Subpath",
data_type: FrontendGraphDataType::Subpath,
default: NodeInput::value(TaggedValue::Subpath(Subpath::new()), true),
},
DocumentInputType {
name: "Translation",
data_type: FrontendGraphDataType::Vector,
default: NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false),
},
DocumentInputType {
name: "Rotation",
data_type: FrontendGraphDataType::Number,
default: NodeInput::value(TaggedValue::F64(0.), false),
},
DocumentInputType {
name: "Scale",
data_type: FrontendGraphDataType::Vector,
default: NodeInput::value(TaggedValue::DVec2(DVec2::ONE), false),
},
DocumentInputType {
name: "Skew",
data_type: FrontendGraphDataType::Vector,
default: NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false),
},
],
outputs: &[FrontendGraphDataType::Subpath],
properties: node_properties::transform_properties,
},
DocumentNodeType {
name: "Blit Subpath",
category: "Vector",
identifier: NodeIdentifier::new("graphene_std::vector::generator_nodes::BlitSubpath", &[]),
inputs: &[
DocumentInputType {
name: "Image",
data_type: FrontendGraphDataType::Raster,
default: NodeInput::value(TaggedValue::Image(Image::empty()), true),
},
DocumentInputType {
name: "Subpath",
data_type: FrontendGraphDataType::Subpath,
default: NodeInput::value(TaggedValue::Subpath(Subpath::new()), true),
},
],
outputs: &[FrontendGraphDataType::Raster],
properties: node_properties::no_properties,
},*/
];
pub fn resolve_document_node_type(name: &str) -> Option<&DocumentNodeType> {
@ -185,6 +344,6 @@ pub fn collect_node_types() -> Vec<FrontendNodeType> {
DOCUMENT_NODE_TYPES
.iter()
.filter(|node_type| !matches!(node_type.name, "Input" | "Output"))
.map(|node_type| FrontendNodeType { name: node_type.name.to_string() })
.map(|node_type| FrontendNodeType::new(node_type.name, node_type.category))
.collect()
}

View File

@ -1,163 +1,128 @@
use crate::messages::layout::utility_types::layout_widget::{LayoutGroup, Widget, WidgetCallback, WidgetHolder};
use crate::messages::layout::utility_types::widgets::button_widgets::ParameterExposeButton;
use crate::messages::layout::utility_types::widgets::input_widgets::{NumberInput, NumberInputMode};
use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType, TextLabel};
use crate::messages::prelude::NodeGraphMessage;
use glam::DVec2;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, NodeId, NodeInput};
use super::FrontendGraphDataType;
pub fn hue_shift_image_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<LayoutGroup> {
let index = 1;
pub fn string_properties(text: impl Into<String>) -> Vec<LayoutGroup> {
let widget = WidgetHolder::text_widget(text);
vec![LayoutGroup::Row { widgets: vec![widget] }]
}
fn update_value<T, F: Fn(&T) -> TaggedValue + 'static>(value: F, node_id: NodeId, input_index: usize) -> WidgetCallback<T> {
WidgetCallback::new(move |number_input: &T| {
NodeGraphMessage::SetInputValue {
node: node_id,
input_index,
value: value(number_input),
}
.into()
})
}
fn expose_widget(node_id: NodeId, index: usize, data_type: FrontendGraphDataType, exposed: bool) -> WidgetHolder {
WidgetHolder::new(Widget::ParameterExposeButton(ParameterExposeButton {
exposed,
data_type,
tooltip: "Expose input parameter in node graph".into(),
on_update: WidgetCallback::new(move |_parameter| {
NodeGraphMessage::ExposeInput {
node_id,
input_index: index,
new_exposed: !exposed,
}
.into()
}),
..Default::default()
}))
}
fn number_range_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, range_min: Option<f64>, range_max: Option<f64>, unit: String, is_integer: bool) -> Vec<WidgetHolder> {
let input: &NodeInput = document_node.inputs.get(index).unwrap();
let exposed = input.is_exposed();
let mut widgets = vec![
WidgetHolder::new(Widget::ParameterExposeButton(ParameterExposeButton {
exposed,
data_type: FrontendGraphDataType::Number,
tooltip: "Expose input parameter in node graph".into(),
on_update: WidgetCallback::new(move |_parameter| {
NodeGraphMessage::ExposeInput {
node_id,
input_index: index,
new_exposed: !exposed,
}
.into()
}),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Shift Degrees".into(),
..Default::default()
})),
expose_widget(node_id, index, FrontendGraphDataType::Number, input.is_exposed()),
WidgetHolder::unrelated_seperator(),
WidgetHolder::text_widget(name),
];
if let NodeInput::Value {
tagged_value: TaggedValue::F32(x),
tagged_value: TaggedValue::F64(x),
exposed: false,
} = document_node.inputs[index]
{
widgets.extend_from_slice(&[
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::unrelated_seperator(),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(x as f64),
unit: "°".into(),
mode: NumberInputMode::Range,
range_min: Some(-180.),
range_max: Some(180.),
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
NodeGraphMessage::SetInputValue {
node: node_id,
input_index: 1,
value: TaggedValue::F32(number_input.value.unwrap() as f32),
}
.into()
}),
value: Some(x),
mode: if range_max.is_some() { NumberInputMode::Range } else { NumberInputMode::Increment },
range_min,
range_max,
unit,
is_integer,
on_update: update_value(|x: &NumberInput| TaggedValue::F64(x.value.unwrap()), node_id, index),
..NumberInput::default()
})),
])
}
widgets
}
vec![LayoutGroup::Row { widgets }]
pub fn adjust_hsl_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<LayoutGroup> {
let hue_shift = number_range_widget(document_node, node_id, 1, "Hue Shift", Some(-180.), Some(180.), "°".into(), false);
let saturation_shift = number_range_widget(document_node, node_id, 2, "Saturation Shift", Some(-100.), Some(100.), "%".into(), false);
let luminance_shift = number_range_widget(document_node, node_id, 3, "Luminance Shift", Some(-100.), Some(100.), "%".into(), false);
vec![
LayoutGroup::Row { widgets: hue_shift },
LayoutGroup::Row { widgets: saturation_shift },
LayoutGroup::Row { widgets: luminance_shift },
]
}
pub fn brighten_image_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<LayoutGroup> {
let index = 1;
let input: &NodeInput = document_node.inputs.get(index).unwrap();
let exposed = input.is_exposed();
let brightness = number_range_widget(document_node, node_id, 1, "Brightness", Some(-255.), Some(255.), "".into(), false);
let contrast = number_range_widget(document_node, node_id, 2, "Contrast", Some(-255.), Some(255.), "".into(), false);
let mut widgets = vec![
WidgetHolder::new(Widget::ParameterExposeButton(ParameterExposeButton {
exposed,
data_type: FrontendGraphDataType::Number,
tooltip: "Expose input parameter in node graph".into(),
on_update: WidgetCallback::new(move |_parameter| {
NodeGraphMessage::ExposeInput {
node_id,
input_index: index,
new_exposed: !exposed,
}
.into()
}),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Brighten Amount".into(),
..Default::default()
})),
];
vec![LayoutGroup::Row { widgets: brightness }, LayoutGroup::Row { widgets: contrast }]
}
if let NodeInput::Value {
tagged_value: TaggedValue::F32(x),
exposed: false,
} = document_node.inputs[index]
{
widgets.extend_from_slice(&[
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(x as f64),
mode: NumberInputMode::Range,
range_min: Some(-255.),
range_max: Some(255.),
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
NodeGraphMessage::SetInputValue {
node: node_id,
input_index: 1,
value: TaggedValue::F32(number_input.value.unwrap() as f32),
}
.into()
}),
..NumberInput::default()
})),
])
}
pub fn adjust_gamma_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<LayoutGroup> {
let gamma = number_range_widget(document_node, node_id, 1, "Gamma", Some(0.01), None, "".into(), false);
vec![LayoutGroup::Row { widgets }]
vec![LayoutGroup::Row { widgets: gamma }]
}
pub fn multiply_opacity(document_node: &DocumentNode, node_id: NodeId) -> Vec<LayoutGroup> {
let gamma = number_range_widget(document_node, node_id, 1, "Factor", Some(0.), Some(1.), "".into(), false);
vec![LayoutGroup::Row { widgets: gamma }]
}
pub fn posterize_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<LayoutGroup> {
let value = number_range_widget(document_node, node_id, 1, "Levels", Some(2.), Some(255.), "".into(), true);
vec![LayoutGroup::Row { widgets: value }]
}
pub fn exposure_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<LayoutGroup> {
let value = number_range_widget(document_node, node_id, 1, "Value", Some(-3.), Some(3.), "".into(), false);
vec![LayoutGroup::Row { widgets: value }]
}
pub fn add_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<LayoutGroup> {
let operand = |name: &str, index| {
let input: &NodeInput = document_node.inputs.get(index).unwrap();
let exposed = input.is_exposed();
let mut widgets = vec![
WidgetHolder::new(Widget::ParameterExposeButton(ParameterExposeButton {
exposed,
data_type: FrontendGraphDataType::Number,
tooltip: "Expose input parameter in node graph".into(),
on_update: WidgetCallback::new(move |_parameter| {
NodeGraphMessage::ExposeInput {
node_id,
input_index: index,
new_exposed: !exposed,
}
.into()
}),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: name.into(),
..Default::default()
})),
expose_widget(node_id, index, FrontendGraphDataType::Number, input.is_exposed()),
WidgetHolder::unrelated_seperator(),
WidgetHolder::text_widget(name),
];
if let NodeInput::Value {
@ -166,21 +131,11 @@ pub fn add_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<Layo
} = document_node.inputs[index]
{
widgets.extend_from_slice(&[
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::unrelated_seperator(),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(x as f64),
mode: NumberInputMode::Increment,
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
NodeGraphMessage::SetInputValue {
node: node_id,
input_index: index,
value: TaggedValue::F32(number_input.value.unwrap() as f32),
}
.into()
}),
on_update: update_value(|number_input: &NumberInput| TaggedValue::F32(number_input.value.unwrap() as f32), node_id, index),
..NumberInput::default()
})),
]);
@ -191,13 +146,123 @@ pub fn add_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<Layo
vec![operand("Input", 0), operand("Addend", 1)]
}
pub fn transform_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<LayoutGroup> {
let translation = {
let index = 1;
let input: &NodeInput = document_node.inputs.get(index).unwrap();
let mut widgets = vec![
expose_widget(node_id, index, FrontendGraphDataType::Vector, input.is_exposed()),
WidgetHolder::unrelated_seperator(),
WidgetHolder::text_widget("Translation"),
];
if let NodeInput::Value {
tagged_value: TaggedValue::DVec2(vec2),
exposed: false,
} = document_node.inputs[index]
{
widgets.extend_from_slice(&[
WidgetHolder::unrelated_seperator(),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(vec2.x),
label: "X".into(),
unit: " px".into(),
on_update: update_value(move |number_input: &NumberInput| TaggedValue::DVec2(DVec2::new(number_input.value.unwrap(), vec2.y)), node_id, index),
..NumberInput::default()
})),
WidgetHolder::unrelated_seperator(),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(vec2.y),
label: "Y".into(),
unit: " px".into(),
on_update: update_value(move |number_input: &NumberInput| TaggedValue::DVec2(DVec2::new(vec2.x, number_input.value.unwrap())), node_id, index),
..NumberInput::default()
})),
]);
}
LayoutGroup::Row { widgets }
};
let rotation = {
let index = 2;
let input: &NodeInput = document_node.inputs.get(index).unwrap();
let mut widgets = vec![
expose_widget(node_id, index, FrontendGraphDataType::Number, input.is_exposed()),
WidgetHolder::unrelated_seperator(),
WidgetHolder::text_widget("Rotation"),
];
if let NodeInput::Value {
tagged_value: TaggedValue::F64(val),
exposed: false,
} = document_node.inputs[index]
{
widgets.extend_from_slice(&[
WidgetHolder::unrelated_seperator(),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(val.to_degrees()),
unit: "°".into(),
mode: NumberInputMode::Range,
range_min: Some(-180.),
range_max: Some(180.),
on_update: update_value(|number_input: &NumberInput| TaggedValue::F64(number_input.value.unwrap().to_radians()), node_id, index),
..NumberInput::default()
})),
]);
}
LayoutGroup::Row { widgets }
};
let scale = {
let index = 3;
let input: &NodeInput = document_node.inputs.get(index).unwrap();
let mut widgets = vec![
expose_widget(node_id, index, FrontendGraphDataType::Vector, input.is_exposed()),
WidgetHolder::unrelated_seperator(),
WidgetHolder::text_widget("Scale"),
];
if let NodeInput::Value {
tagged_value: TaggedValue::DVec2(vec2),
exposed: false,
} = document_node.inputs[index]
{
widgets.extend_from_slice(&[
WidgetHolder::unrelated_seperator(),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(vec2.x),
label: "X".into(),
unit: "".into(),
on_update: update_value(move |number_input: &NumberInput| TaggedValue::DVec2(DVec2::new(number_input.value.unwrap(), vec2.y)), node_id, index),
..NumberInput::default()
})),
WidgetHolder::unrelated_seperator(),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(vec2.y),
label: "Y".into(),
unit: "".into(),
on_update: update_value(move |number_input: &NumberInput| TaggedValue::DVec2(DVec2::new(vec2.x, number_input.value.unwrap())), node_id, index),
..NumberInput::default()
})),
]);
}
LayoutGroup::Row { widgets }
};
vec![translation, rotation, scale]
}
fn unknown_node_properties(document_node: &DocumentNode) -> Vec<LayoutGroup> {
vec![LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: format!("Node '{}' cannot be found in library", document_node.name),
..Default::default()
}))],
}]
string_properties(format!("Node '{}' cannot be found in library", document_node.name))
}
pub fn no_properties(document_node: &DocumentNode, _node_id: NodeId) -> Vec<LayoutGroup> {
string_properties(format!("The {} node requires no properties.", document_node.name.to_lowercase()))
}
pub fn generate_node_properties(document_node: &DocumentNode, node_id: NodeId) -> LayoutGroup {

View File

@ -136,6 +136,7 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
}
.into(),
);
responses.push_back(NodeGraphMessage::CloseNodeGraph.into());
}
}
ResendActiveProperties => {

View File

@ -6,11 +6,11 @@ use crate::messages::prelude::*;
use graphene::color::Color;
use graphene::document::Document;
use graphene::layers::style::{self, Fill, Stroke};
use graphene::layers::vector::consts::ManipulatorType;
use graphene::layers::vector::manipulator_group::ManipulatorGroup;
use graphene::layers::vector::manipulator_point::ManipulatorPoint;
use graphene::layers::vector::subpath::Subpath;
use graphene::{LayerId, Operation};
use graphene_std::vector::consts::ManipulatorType;
use graphene_std::vector::manipulator_group::ManipulatorGroup;
use graphene_std::vector::manipulator_point::ManipulatorPoint;
use graphene_std::vector::subpath::Subpath;
use glam::{DAffine2, DVec2};

View File

@ -6,8 +6,8 @@ use graphene::intersection::Quad;
use graphene::layers::layer_info::LayerDataType;
use graphene::layers::style::{self, Fill, Stroke};
use graphene::layers::text_layer::FontCache;
use graphene::layers::vector::subpath::Subpath;
use graphene::{LayerId, Operation};
use graphene_std::vector::subpath::Subpath;
use glam::{DAffine2, DVec2};

View File

@ -1,11 +1,11 @@
use crate::messages::prelude::*;
use bezier_rs::ComputeType;
use graphene::layers::vector::consts::ManipulatorType;
use graphene::layers::vector::manipulator_group::ManipulatorGroup;
use graphene::layers::vector::manipulator_point::ManipulatorPoint;
use graphene::layers::vector::subpath::{BezierId, Subpath};
use graphene::{LayerId, Operation};
use graphene_std::vector::consts::ManipulatorType;
use graphene_std::vector::manipulator_group::ManipulatorGroup;
use graphene_std::vector::manipulator_point::ManipulatorPoint;
use graphene_std::vector::subpath::{BezierId, Subpath};
use glam::DVec2;
use graphene::document::Document;

View File

@ -7,8 +7,8 @@ use crate::messages::prelude::*;
use graphene::layers::layer_info::{Layer, LayerDataType};
use graphene::layers::style::{self, Stroke};
use graphene::layers::vector::consts::ManipulatorType;
use graphene::{LayerId, Operation};
use graphene_std::vector::consts::ManipulatorType;
use glam::{DAffine2, DVec2};
use std::f64::consts::PI;

View File

@ -10,7 +10,7 @@ use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHan
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::intersection::Quad;
use graphene::layers::vector::consts::ManipulatorType;
use graphene_std::vector::consts::ManipulatorType;
use glam::DVec2;
use serde::{Deserialize, Serialize};

View File

@ -10,10 +10,10 @@ use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHan
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::layers::style;
use graphene::layers::vector::consts::ManipulatorType;
use graphene::layers::vector::manipulator_group::ManipulatorGroup;
use graphene::LayerId;
use graphene::Operation;
use graphene_std::vector::consts::ManipulatorType;
use graphene_std::vector::manipulator_group::ManipulatorGroup;
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};

View File

@ -40,27 +40,18 @@
--color-f-white-rgb: 255, 255, 255;
--color-data-general: #c5c5c5;
--color-data-general-rgb: 197, 197, 197;
--color-data-general-dim: #767676;
--color-data-general-dim-rgb: 118, 118, 118;
--color-data-vector: #65bbe5;
--color-data-vector-rgb: 101, 187, 229;
--color-data-vector-dim: #4b778c;
--color-data-vector-dim-rgb: 75, 119, 140;
--color-data-raster: #e4bb72;
--color-data-raster-rgb: 228, 187, 114;
--color-data-raster-dim: #8b7752;
--color-data-raster-dim-rgb: 139, 119, 82;
--color-data-mask: #8d85c7;
--color-data-mask-rgb: 141, 133, 199;
--color-data-number: #d6536e;
--color-data-number-rgb: 214, 83, 110;
--color-data-number-dim: #803242;
--color-data-number-dim-rgb: 128, 50, 66;
--color-data-vec2: #cc00ff;
--color-data-vec2-dim: #71008d;
--color-data-color: #70a898;
--color-data-color-rgb: 112, 168, 152;
--color-data-color-dim: #43645b;
--color-data-color-dim-rgb: 67, 100, 91;
--color-none: white;
--color-none-repeat: no-repeat;

View File

@ -1,12 +1,6 @@
<template>
<LayoutCol class="node-graph">
<LayoutRow class="options-bar"></LayoutRow>
<div class="node-list">
<LayoutRow>Nodes:</LayoutRow>
<LayoutRow>
<TextButton v-for="nodeType in nodeTypes" v-bind:key="String(nodeType)" :label="nodeType.name + ' Node'" :action="() => createNode(nodeType.name)"></TextButton>
</LayoutRow>
</div>
<LayoutRow
class="graph"
ref="graph"
@ -21,6 +15,14 @@
'--dot-radius': `${dotRadius}px`,
}"
>
<LayoutCol class="node-list" v-if="nodeListLocation" :style="{ marginLeft: `${nodeListX}px`, marginTop: `${nodeListY}px` }">
<TextInput placeholder="Search Nodes..." :value="searchTerm" @update:value="(val) => (searchTerm = val)" v-focus />
<LayoutCol v-for="nodeCategory in nodeCategories" :key="nodeCategory[0]">
<TextLabel>{{ nodeCategory[0] }}</TextLabel>
<TextButton v-for="nodeType in nodeCategory[1]" v-bind:key="String(nodeType)" :label="nodeType.name + ' Node'" :action="() => createNode(nodeType.name)" />
</LayoutCol>
<TextLabel v-if="nodeCategories.length === 0">No search results :(</TextLabel>
</LayoutCol>
<div
class="nodes"
ref="nodesContainer"
@ -37,8 +39,6 @@
:style="{
'--offset-left': (node.position?.x || 0) + (selected.includes(node.id) ? draggingNodes?.roundX || 0 : 0),
'--offset-top': (node.position?.y || 0) + (selected.includes(node.id) ? draggingNodes?.roundY || 0 : 0),
'--data-color': 'var(--color-data-raster)',
'--data-color-dim': 'var(--color-data-raster-dim)',
}"
:data-node="node.id"
>
@ -111,10 +111,9 @@
.node-list {
width: max-content;
position: fixed;
padding: 20px;
margin: 40px 10px;
padding: 5px;
z-index: 3;
background-color: var(--color-4-dimgray);
background-color: var(--color-3-darkgray);
}
.options-bar {
@ -181,7 +180,7 @@
border-radius: 4px;
background: var(--color-4-dimgray);
left: calc((var(--offset-left) + 0.5) * 24px);
top: calc((var(--offset-top) + 0.5) * 24px);
top: calc((var(--offset-top) - 0.5) * 24px);
&.selected {
border: 1px solid var(--color-e-nearwhite);
@ -209,6 +208,7 @@
.arguments {
display: flex;
flex-direction: column;
width: 100%;
position: relative;
@ -297,6 +297,7 @@ import type { IconName } from "@/utility-functions/icons";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import TextButton from "@/components/widgets/buttons/TextButton.vue";
import TextInput from "@/components/widgets/inputs/TextInput.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
@ -316,6 +317,8 @@ export default defineComponent({
linkInProgressFromConnector: undefined as HTMLDivElement | undefined,
linkInProgressToConnector: undefined as HTMLDivElement | DOMRect | undefined,
nodeLinkPaths: [] as [string, string][],
searchTerm: "",
nodeListLocation: undefined as { x: number; y: number } | undefined,
};
},
computed: {
@ -335,8 +338,24 @@ export default defineComponent({
nodes() {
return this.nodeGraph.state.nodes;
},
nodeTypes() {
return this.nodeGraph.state.nodeTypes;
nodeCategories() {
const categories = new Map();
this.nodeGraph.state.nodeTypes.forEach((node) => {
if (this.searchTerm.length && !node.name.toLowerCase().includes(this.searchTerm.toLowerCase()) && !node.category.toLowerCase().includes(this.searchTerm.toLowerCase())) return;
const category = categories.get(node.category);
if (category) category.push(node);
else categories.set(node.category, [node]);
});
const result = Array.from(categories);
return result;
},
nodeListX() {
return ((this.nodeListLocation?.x || 0) * GRID_SIZE + this.transform.x) * this.transform.scale;
},
nodeListY() {
return ((this.nodeListLocation?.y || 0) * GRID_SIZE + this.transform.y) * this.transform.scale;
},
linkPathInProgress(): [string, string] | undefined {
if (this.linkInProgressFromConnector && this.linkInProgressToConnector) {
@ -461,8 +480,19 @@ export default defineComponent({
}
},
pointerDown(e: PointerEvent) {
if (e.button === 2) {
const graphDiv: HTMLDivElement | undefined = (this.$refs.graph as typeof LayoutCol | undefined)?.$el;
const graph = graphDiv?.getBoundingClientRect() || new DOMRect();
this.nodeListLocation = {
x: Math.round(((e.clientX - graph.x) / this.transform.scale - this.transform.x) / GRID_SIZE),
y: Math.round(((e.clientY - graph.y) / this.transform.scale - this.transform.y) / GRID_SIZE),
};
return;
}
const port = (e.target as HTMLDivElement).closest("[data-port]") as HTMLDivElement;
const node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined;
const nodeList = (e.target as HTMLElement).closest(".node-list") as HTMLElement | undefined;
if (port) {
const isOutput = Boolean(port.getAttribute("data-port") === "output");
@ -488,7 +518,7 @@ export default defineComponent({
}
this.editor.instance.selectNodes(new BigUint64Array(this.selected));
} else {
} else if (!nodeList) {
this.selected = [];
this.editor.instance.selectNodes(new BigUint64Array(this.selected));
const graphDiv: HTMLDivElement | undefined = (this.$refs.graph as typeof LayoutCol | undefined)?.$el;
@ -560,7 +590,10 @@ export default defineComponent({
this.linkInProgressToConnector = undefined;
},
createNode(nodeType: string): void {
this.editor.instance.createNode(nodeType);
if (!this.nodeListLocation) return;
this.editor.instance.createNode(nodeType, this.nodeListLocation.x, this.nodeListLocation.y);
this.nodeListLocation = undefined;
},
},
mounted() {
@ -578,6 +611,7 @@ export default defineComponent({
LayoutRow,
TextLabel,
TextButton,
TextInput,
},
});
</script>

View File

@ -1,6 +1,6 @@
<template>
<LayoutRow class="parameter-expose-button">
<button :class="{ exposed }" :style="{ '--data-type-color': dataTypeColor }" @click="(e: MouseEvent) => action(e)" :title="tooltip" :tabindex="0"></button>
<button :class="{ exposed }" :style="{ '--data-type-color': `var(--color-data-${dataType})` }" @click="(e: MouseEvent) => action(e)" :title="tooltip" :tabindex="0"></button>
</LayoutRow>
</template>
@ -53,21 +53,6 @@ export default defineComponent({
// Callbacks
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
},
computed: {
dataTypeColor(): string {
// TODO: Move this function somewhere where it can be reused by other components
const colorsMap = {
general: "var(--color-data-general)",
vector: "var(--color-data-vector)",
raster: "var(--color-data-raster)",
mask: "var(--color-data-mask)",
number: "var(--color-data-number)",
color: "var(--color-data-color)",
} as const;
return colorsMap[this.dataType as keyof typeof colorsMap] || colorsMap.general;
},
},
components: { LayoutRow },
});
</script>

View File

@ -10,6 +10,7 @@
v-model="inputValue"
:spellcheck="spellcheck"
:disabled="disabled"
:placeholder="placeholder"
@focus="() => $emit('textFocused')"
@blur="() => $emit('textChanged')"
@change="() => $emit('textChanged')"
@ -148,6 +149,7 @@ export default defineComponent({
textarea: { type: Boolean as PropType<boolean>, default: false },
tooltip: { type: String as PropType<string | undefined>, required: false },
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
placeholder: { type: String as PropType<string>, required: false },
},
data() {
return {

View File

@ -7,6 +7,7 @@
:spellcheck="true"
:disabled="disabled"
:tooltip="tooltip"
:placeholder="placeholder"
:style="{ 'min-width': minWidth > 0 ? `${minWidth}px` : undefined }"
:sharpRightCorners="sharpRightCorners"
@textFocused="() => onTextFocused()"
@ -41,6 +42,7 @@ export default defineComponent({
// Label
label: { type: String as PropType<string>, required: false },
tooltip: { type: String as PropType<string | undefined>, required: false },
placeholder: { type: String as PropType<string>, required: false },
// Disabled
disabled: { type: Boolean as PropType<boolean>, default: false },

View File

@ -17,5 +17,20 @@ import App from "@/App.vue";
await initWasm();
// Initialize the Vue application
createApp(App).mount("#app");
createApp(App)
.directive("focus", {
// When the bound element is inserted into the DOM
mounted(el) {
let focus = el;
// Find actual relevant child
while (focus.children.length) focus = focus.children[0];
// Random timeout needed?
setTimeout(() => {
focus.focus(); // Focus the element
}, 0);
},
})
.mount("#app");
};

View File

@ -102,6 +102,8 @@ export class FrontendNodeLink {
export class FrontendNodeType {
readonly name!: string;
readonly category!: string;
}
export class IndexedDbDocumentDetails extends DocumentDetails {

View File

@ -552,8 +552,8 @@ impl JsEditorHandle {
/// Creates a new document node in the node graph
#[wasm_bindgen(js_name = createNode)]
pub fn create_node(&self, node_type: String) {
let message = NodeGraphMessage::CreateNode { node_id: None, node_type };
pub fn create_node(&self, node_type: String, x: i32, y: i32) {
let message = NodeGraphMessage::CreateNode { node_id: None, node_type, x, y };
self.dispatch(message);
}

View File

@ -12,6 +12,7 @@ license = "Apache-2.0"
[dependencies]
graph-craft = { path = "../node-graph/graph-craft", features = ["serde"] }
graphene-std = { path = "../node-graph/gstd", features = ["serde"] }
image = { version = "0.24", default-features = false }
log = "0.4"

View File

@ -8,9 +8,10 @@ use crate::layers::nodegraph_layer::NodeGraphFrameLayer;
use crate::layers::shape_layer::ShapeLayer;
use crate::layers::style::RenderData;
use crate::layers::text_layer::{Font, FontCache, TextLayer};
use crate::layers::vector::subpath::Subpath;
use crate::{DocumentError, DocumentResponse, Operation};
use graphene_std::vector::subpath::Subpath;
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
use std::cell::RefCell;

View File

@ -1,6 +1,7 @@
use crate::boolean_ops::{split_path_seg, subdivide_path_seg};
use crate::consts::{F64LOOSE, F64PRECISE};
use crate::layers::vector::subpath::Subpath;
use graphene_std::vector::subpath::Subpath;
use glam::{DAffine2, DMat2, DVec2};
use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveDeriv, ParamCurveExtrema, PathSeg, Point, QuadBez, Rect, Shape, Vec2};

View File

@ -6,12 +6,13 @@ use super::nodegraph_layer::NodeGraphFrameLayer;
use super::shape_layer::ShapeLayer;
use super::style::{PathStyle, RenderData};
use super::text_layer::TextLayer;
use super::vector::subpath::Subpath;
use crate::intersection::Quad;
use crate::layers::text_layer::FontCache;
use crate::DocumentError;
use crate::LayerId;
use graphene_std::vector::subpath::Subpath;
use core::fmt;
use glam::{DAffine2, DMat2, DVec2};
use serde::{Deserialize, Serialize};
@ -96,6 +97,34 @@ impl From<&LayerDataType> for LayerDataTypeDiscriminant {
}
}
// ** CONVERSIONS **
impl<'a> TryFrom<&'a mut Layer> for &'a mut Subpath {
type Error = &'static str;
/// Convert a mutable layer into a mutable [Subpath].
fn try_from(layer: &'a mut Layer) -> Result<&'a mut Subpath, Self::Error> {
match &mut layer.data {
LayerDataType::Shape(layer) => Ok(&mut layer.shape),
// TODO Resolve converting text into a Subpath at the layer level
// LayerDataType::Text(text) => Some(Subpath::new(path_to_shape.to_vec(), viewport_transform, true)),
_ => Err("Did not find any shape data in the layer"),
}
}
}
impl<'a> TryFrom<&'a Layer> for &'a Subpath {
type Error = &'static str;
/// Convert a reference to a layer into a reference of a [Subpath].
fn try_from(layer: &'a Layer) -> Result<&'a Subpath, Self::Error> {
match &layer.data {
LayerDataType::Shape(layer) => Ok(&layer.shape),
// TODO Resolve converting text into a Subpath at the layer level
// LayerDataType::Text(text) => Some(Subpath::new(path_to_shape.to_vec(), viewport_transform, true)),
_ => Err("Did not find any shape data in the layer"),
}
}
}
/// Defines shared behavior for every layer type.
pub trait LayerData {
/// Render the layer as an SVG tag to a given string.

View File

@ -20,7 +20,6 @@ pub mod base64_serde;
pub mod blend_mode;
/// Contains the [FolderLayer](folder_layer::FolderLayer) type that encapsulates other layers, including more folders.
pub mod folder_layer;
pub mod id_vec;
/// Contains the [ImageLayer](image_layer::ImageLayer) type that contains a bitmap image.
pub mod image_layer;
/// Contains the [ImaginateLayer](imaginate_layer::ImaginateLayer) type that contains a bitmap image generated based on a prompt and optionally the layers beneath it.
@ -34,4 +33,3 @@ pub mod shape_layer;
pub mod style;
/// Contains the [TextLayer](text_layer::TextLayer) type.
pub mod text_layer;
pub mod vector;

View File

@ -119,49 +119,12 @@ impl Default for NodeGraphFrameLayer {
fn default() -> Self {
use graph_craft::document::*;
use graph_craft::proto::NodeIdentifier;
let brighten_network = NodeNetwork {
inputs: vec![0, 0],
output: 0,
nodes: [(
0,
DocumentNode {
name: "Brighten Image Node".into(),
inputs: vec![NodeInput::Network, NodeInput::Network],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new(
"graphene_std::raster::BrightenImageNode",
&[graph_craft::proto::Type::Concrete(std::borrow::Cow::Borrowed("&TypeErasedNode"))],
)),
metadata: DocumentNodeMetadata::default(),
},
)]
.into_iter()
.collect(),
};
let hue_shift_network = NodeNetwork {
inputs: vec![0, 0],
output: 0,
nodes: [(
0,
DocumentNode {
name: "Hue Shift Image Node".into(),
inputs: vec![NodeInput::Network, NodeInput::Network],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new(
"graphene_std::raster::HueShiftImage",
&[graph_craft::proto::Type::Concrete(std::borrow::Cow::Borrowed("&TypeErasedNode"))],
)),
metadata: DocumentNodeMetadata::default(),
},
)]
.into_iter()
.collect(),
};
Self {
mime: String::new(),
network: NodeNetwork {
inputs: vec![0],
output: 3,
output: 1,
nodes: [
(
0,
@ -174,41 +137,11 @@ impl Default for NodeGraphFrameLayer {
),
(
1,
DocumentNode {
name: "Hue Shift Image".into(),
inputs: vec![
NodeInput::Node(0),
NodeInput::Value {
tagged_value: value::TaggedValue::F32(50.),
exposed: false,
},
],
implementation: DocumentNodeImplementation::Network(hue_shift_network),
metadata: DocumentNodeMetadata { position: (8 + 7, 4 + 2) },
},
),
(
2,
DocumentNode {
name: "Brighten Image".into(),
inputs: vec![
NodeInput::Node(1),
NodeInput::Value {
tagged_value: value::TaggedValue::F32(10.),
exposed: false,
},
],
implementation: DocumentNodeImplementation::Network(brighten_network),
metadata: DocumentNodeMetadata { position: (8 + 7 * 2, 4 + 2 * 2) },
},
),
(
3,
DocumentNode {
name: "Output".into(),
inputs: vec![NodeInput::Node(2)],
inputs: vec![NodeInput::Node(0)],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Generic])),
metadata: DocumentNodeMetadata { position: (8 + 7 * 3, 4) },
metadata: DocumentNodeMetadata { position: (20, 4) },
},
),
]

View File

@ -1,10 +1,11 @@
use super::layer_info::LayerData;
use super::style::{self, PathStyle, RenderData, ViewMode};
use super::vector::subpath::Subpath;
use crate::intersection::{intersect_quad_bez_path, Quad};
use crate::layers::text_layer::FontCache;
use crate::LayerId;
use graphene_std::vector::subpath::Subpath;
use glam::{DAffine2, DMat2, DVec2};
use serde::{Deserialize, Serialize};
use std::fmt::Write;

View File

@ -1,10 +1,11 @@
use super::layer_info::LayerData;
use super::style::{PathStyle, RenderData, ViewMode};
use super::vector::subpath::Subpath;
use crate::intersection::{intersect_quad_bez_path, Quad};
use crate::LayerId;
pub use font_cache::{Font, FontCache};
use graphene_std::vector::subpath::Subpath;
use glam::{DAffine2, DMat2, DVec2};
use rustybuzz::Face;
use serde::{Deserialize, Serialize};

View File

@ -1,7 +1,7 @@
use crate::layers::vector::consts::ManipulatorType;
use crate::layers::vector::manipulator_group::ManipulatorGroup;
use crate::layers::vector::manipulator_point::ManipulatorPoint;
use crate::layers::vector::subpath::Subpath;
use graphene_std::vector::consts::ManipulatorType;
use graphene_std::vector::manipulator_group::ManipulatorGroup;
use graphene_std::vector::manipulator_point::ManipulatorPoint;
use graphene_std::vector::subpath::Subpath;
use glam::DVec2;
use rustybuzz::{GlyphBuffer, UnicodeBuffer};

View File

@ -3,11 +3,12 @@ use crate::layers::blend_mode::BlendMode;
use crate::layers::imaginate_layer::{ImaginateMaskFillContent, ImaginateMaskPaintMode, ImaginateSamplingMethod, ImaginateStatus};
use crate::layers::layer_info::Layer;
use crate::layers::style::{self, Stroke};
use crate::layers::vector::consts::ManipulatorType;
use crate::layers::vector::manipulator_group::ManipulatorGroup;
use crate::layers::vector::subpath::Subpath;
use crate::LayerId;
use graphene_std::vector::consts::ManipulatorType;
use graphene_std::vector::manipulator_group::ManipulatorGroup;
use graphene_std::vector::subpath::Subpath;
use serde::{Deserialize, Serialize};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

View File

@ -14,10 +14,15 @@ documentation = "https://docs.rs/dyn-any"
[dependencies]
dyn-any-derive = { path = "derive", version = "0.2.0", optional = true }
log = { version = "0.4", optional = true }
glam = { version = "0.17", optional = true }
[features]
derive = ["dyn-any-derive"]
log-bad-types = ["log"]
# Opt into impls for Rc<T> and Arc<T>.
rc = []
# Opt into impls for some glam types
glam = ["dep:glam"]
[package.metadata.docs.rs]
all-features = true

View File

@ -190,13 +190,30 @@ use std::{
vec::Vec,
};
impl_type!(Option<T>,Result<T, E>,Cell<T>,UnsafeCell<T>,RefCell<T>,MaybeUninit<T>,
Vec<T>, String, BTreeMap<K,V>,BTreeSet<V>, LinkedList<T>, VecDeque<T>,
BinaryHeap<T>, ManuallyDrop<T>, PhantomData<T>, PhantomPinned,Empty<T>,
Wrapping<T>, Duration, Once, Mutex<T>, RwLock<T>, bool, f32, f64, char,
u8, AtomicU8, u16,AtomicU16, u32,AtomicU32, u64,AtomicU64, usize,AtomicUsize,
i8,AtomicI8, i16,AtomicI16, i32,AtomicI32, i64,AtomicI64, isize,AtomicIsize,
i128, u128, AtomicBool, AtomicPtr<T>
impl_type!(
Option<T>, Result<T, E>, Cell<T>, UnsafeCell<T>, RefCell<T>, MaybeUninit<T>,
Vec<T>, String, BTreeMap<K,V>,BTreeSet<V>, LinkedList<T>, VecDeque<T>,
BinaryHeap<T>, ManuallyDrop<T>, PhantomData<T>, PhantomPinned, Empty<T>,
Wrapping<T>, Duration, Once, Mutex<T>, RwLock<T>, bool, f32, f64, char,
u8, AtomicU8, u16, AtomicU16, u32, AtomicU32, u64, AtomicU64, usize, AtomicUsize,
i8, AtomicI8, i16, AtomicI16, i32, AtomicI32, i64, AtomicI64, isize, AtomicIsize,
i128, u128, AtomicBool, AtomicPtr<T>
);
#[cfg(feature = "rc")]
use std::{rc::Rc, sync::Arc};
#[cfg(feature = "rc")]
impl_type!(Rc<T>, Arc<T>);
#[cfg(feature = "glam")]
use glam::*;
#[cfg(feature = "glam")]
#[rustfmt::skip]
impl_type!(
IVec2, IVec3, IVec4, UVec2, UVec3, UVec4, BVec2, BVec3, BVec4,
Vec2, Vec3, Vec3A, Vec4, DVec2, DVec3, DVec4,
Mat2, Mat3, Mat3A, Mat4, DMat2, DMat3, DMat4,
Quat, Affine2, Affine3A, DAffine2, DAffine3, DQuat
);
impl<T: crate::StaticType + ?Sized> crate::StaticType for Box<T> {

View File

@ -209,9 +209,9 @@ impl Color {
} else {
(max_channel - min_channel) / (2. - max_channel - min_channel)
};
let hue = if self.red > self.green && self.red > self.blue {
let hue = if self.red >= self.green && self.red >= self.blue {
(self.green - self.blue) / (max_channel - min_channel)
} else if self.green > self.red && self.green > self.blue {
} else if self.green >= self.red && self.green >= self.blue {
2. + (self.blue - self.red) / (max_channel - min_channel)
} else {
4. + (self.red - self.green) / (max_channel - min_channel)
@ -282,6 +282,7 @@ fn hsl_roundtrip() {
(95, 79, 88),
(13, 34, 4),
(82, 84, 84),
(255, 255, 178),
] {
let col = Color::from_rgb8(red, green, blue);
let [hue, saturation, luminance, alpha] = col.to_hsla();

View File

@ -9,13 +9,14 @@ license = "MIT OR Apache-2.0"
[dependencies]
graphene-core = { path = "../gcore", features = ["async", "std"] }
graphene-std = { path = "../gstd" }
dyn-any = { path = "../../libraries/dyn-any", features = ["log-bad-types"] }
dyn-any = { path = "../../libraries/dyn-any", features = ["log-bad-types", "rc", "glam"] }
num-traits = "0.2"
borrow_stack = { path = "../borrow_stack" }
dyn-clone = "1.0"
rand_chacha = "0.3.1"
log = "0.4"
serde = { version = "1", features = ["derive"], optional = true }
serde = { version = "1", features = ["derive", "rc"], optional = true }
glam = { version = "0.17" }
[features]
serde = ["dep:serde", "graphene-std/serde"]
serde = ["dep:serde", "graphene-std/serde", "glam/serde"]

View File

@ -58,6 +58,7 @@ impl DocumentNode {
}
fn resolve_proto_node(mut self) -> ProtoNode {
assert_ne!(self.inputs.len(), 0, "Resolving document node {:#?} with no inputs", self);
let first = self.inputs.remove(0);
if let DocumentNodeImplementation::Unresolved(fqn) = self.implementation {
let (input, mut args) = match first {
@ -102,6 +103,9 @@ pub enum NodeInput {
}
impl NodeInput {
pub const fn value(tagged_value: value::TaggedValue, exposed: bool) -> Self {
Self::Value { tagged_value, exposed }
}
fn map_ids(&mut self, f: impl Fn(NodeId) -> NodeId) {
if let NodeInput::Node(id) = self {
*self = NodeInput::Node(f(*id))

View File

@ -1,28 +1,40 @@
use dyn_any::StaticType;
use dyn_clone::DynClone;
use dyn_any::{DynAny, Upcast};
use dyn_clone::DynClone;
use glam::DVec2;
use std::sync::Arc;
/// A type that is known, allowing serialization (serde::Deserialize is not object safe)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TaggedValue {
None,
String(String),
U32(u32),
F32(f32),
F64(f64),
Bool(bool),
DVec2(DVec2),
Image(graphene_std::raster::Image),
Color(graphene_core::raster::color::Color),
Subpath(graphene_std::vector::subpath::Subpath),
RcSubpath(Arc<graphene_std::vector::subpath::Subpath>),
}
impl TaggedValue {
pub fn to_value(self) -> Value {
match self {
TaggedValue::None => Box::new(()),
TaggedValue::String(x) => Box::new(x),
TaggedValue::U32(x) => Box::new(x),
TaggedValue::F32(x) => Box::new(x),
TaggedValue::F64(x) => Box::new(x),
TaggedValue::Bool(x) => Box::new(x),
TaggedValue::DVec2(x) => Box::new(x),
TaggedValue::Image(x) => Box::new(x),
TaggedValue::Color(x) => Box::new(x),
TaggedValue::Subpath(x) => Box::new(x),
TaggedValue::RcSubpath(x) => Box::new(x),
}
}
}

View File

@ -1,6 +1,7 @@
use std::borrow::Cow;
use borrow_stack::FixedSizeStack;
use glam::DVec2;
use graphene_core::generic::FnNode;
use graphene_core::ops::AddNode;
use graphene_core::raster::color::Color;
@ -9,6 +10,7 @@ use graphene_core::Node;
use graphene_std::any::DowncastBothNode;
use graphene_std::any::{Any, DowncastNode, DynAnyNode, IntoTypeErasedNode, TypeErasedNode};
use graphene_std::raster::Image;
use graphene_std::vector::subpath::Subpath;
use crate::proto::Type;
use crate::proto::{ConstructionArgs, NodeIdentifier, ProtoNode, ProtoNodeInput, Type::Concrete};
@ -48,8 +50,8 @@ static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[
stack.push_fn(move |nodes| {
let ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("Add Node constructed with out rhs input node") };
let value_node = nodes.get(construction_nodes[0] as usize).unwrap();
let input_node: DowncastBothNode<_, (), f32> = DowncastBothNode::new(value_node);
let node: DynAnyNode<_, f32, _, _> = DynAnyNode::new(ConsNode::new(input_node).then(graphene_core::ops::AddNode));
let input_node: DowncastBothNode<_, (), f64> = DowncastBothNode::new(value_node);
let node: DynAnyNode<_, f64, _, _> = DynAnyNode::new(ConsNode::new(input_node).then(graphene_core::ops::AddNode));
if let ProtoNodeInput::Node(node_id) = proto_node.input {
let pre_node = nodes.get(node_id as usize).unwrap();
@ -297,14 +299,28 @@ static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[
}
})
}),
(NodeIdentifier::new("graphene_std::raster::InvertImageColorNode", &[]), |proto_node, stack| {
stack.push_fn(move |nodes| {
let node = DynAnyNode::new(graphene_std::raster::InvertImageColorNode);
if let ProtoNodeInput::Node(node_id) = proto_node.input {
let pre_node = nodes.get(node_id as usize).unwrap();
(pre_node).then(node).into_type_erased()
} else {
node.into_type_erased()
}
})
}),
(
NodeIdentifier::new("graphene_std::raster::HueShiftImage", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
NodeIdentifier::new("graphene_std::raster::ShiftImageHSLNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
|proto_node, stack| {
stack.push_fn(move |nodes| {
let ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("Hue Shift Image Node constructed with out shift input node") };
let value_node = nodes.get(construction_nodes[0] as usize).unwrap();
let input_node: DowncastBothNode<_, (), f32> = DowncastBothNode::new(value_node);
let node = DynAnyNode::new(graphene_std::raster::HueShiftImage::new(input_node));
let ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("ShiftImageHSLNode Node constructed without inputs") };
let hue: DowncastBothNode<_, (), f64> = DowncastBothNode::new(nodes.get(construction_nodes[0] as usize).unwrap());
let saturation: DowncastBothNode<_, (), f64> = DowncastBothNode::new(nodes.get(construction_nodes[1] as usize).unwrap());
let luminance: DowncastBothNode<_, (), f64> = DowncastBothNode::new(nodes.get(construction_nodes[2] as usize).unwrap());
let node = DynAnyNode::new(graphene_std::raster::ShiftImageHSLNode::new(hue, saturation, luminance));
if let ProtoNodeInput::Node(node_id) = proto_node.input {
let pre_node = nodes.get(node_id as usize).unwrap();
@ -316,13 +332,82 @@ static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[
},
),
(
NodeIdentifier::new("graphene_std::raster::BrightenImageNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
NodeIdentifier::new("graphene_std::raster::ImageBrightnessAndContrast", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
|proto_node, stack| {
stack.push_fn(move |nodes| {
let ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("Brighten Image Node constructed with out brighten input node") };
let value_node = nodes.get(construction_nodes[0] as usize).unwrap();
let input_node: DowncastBothNode<_, (), f32> = DowncastBothNode::new(value_node);
let node = DynAnyNode::new(graphene_std::raster::BrightenImageNode::new(input_node));
let ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("ImageBrightnessAndContrast Node constructed without inputs") };
let brightness: DowncastBothNode<_, (), f64> = DowncastBothNode::new(nodes.get(construction_nodes[0] as usize).unwrap());
let contrast: DowncastBothNode<_, (), f64> = DowncastBothNode::new(nodes.get(construction_nodes[1] as usize).unwrap());
let node = DynAnyNode::new(graphene_std::raster::ImageBrightnessAndContrast::new(brightness, contrast));
if let ProtoNodeInput::Node(node_id) = proto_node.input {
let pre_node = nodes.get(node_id as usize).unwrap();
(pre_node).then(node).into_type_erased()
} else {
node.into_type_erased()
}
})
},
),
(
NodeIdentifier::new("graphene_std::raster::ImageGammaNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
|proto_node, stack| {
stack.push_fn(move |nodes| {
let ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("ImageGammaNode Node constructed without inputs") };
let gamma: DowncastBothNode<_, (), f64> = DowncastBothNode::new(nodes.get(construction_nodes[0] as usize).unwrap());
let node = DynAnyNode::new(graphene_std::raster::ImageGammaNode::new(gamma));
if let ProtoNodeInput::Node(node_id) = proto_node.input {
let pre_node = nodes.get(node_id as usize).unwrap();
(pre_node).then(node).into_type_erased()
} else {
node.into_type_erased()
}
})
},
),
(
NodeIdentifier::new("graphene_std::raster::ImageOpacityNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
|proto_node, stack| {
stack.push_fn(move |nodes| {
let ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("ImageOpacityNode Node constructed without inputs") };
let opacity: DowncastBothNode<_, (), f64> = DowncastBothNode::new(nodes.get(construction_nodes[0] as usize).unwrap());
let node = DynAnyNode::new(graphene_std::raster::ImageOpacityNode::new(opacity));
if let ProtoNodeInput::Node(node_id) = proto_node.input {
let pre_node = nodes.get(node_id as usize).unwrap();
(pre_node).then(node).into_type_erased()
} else {
node.into_type_erased()
}
})
},
),
(
NodeIdentifier::new("graphene_std::raster::Posterize", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
|proto_node, stack| {
stack.push_fn(move |nodes| {
let ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("Posterize Node constructed without inputs") };
let value: DowncastBothNode<_, (), f64> = DowncastBothNode::new(nodes.get(construction_nodes[0] as usize).unwrap());
let node = DynAnyNode::new(graphene_std::raster::Posterize::new(value));
if let ProtoNodeInput::Node(node_id) = proto_node.input {
let pre_node = nodes.get(node_id as usize).unwrap();
(pre_node).then(node).into_type_erased()
} else {
node.into_type_erased()
}
})
},
),
(
NodeIdentifier::new("graphene_std::raster::ExposureNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
|proto_node, stack| {
stack.push_fn(move |nodes| {
let ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("ExposureNode constructed without inputs") };
let value: DowncastBothNode<_, (), f64> = DowncastBothNode::new(nodes.get(construction_nodes[0] as usize).unwrap());
let node = DynAnyNode::new(graphene_std::raster::ExposureNode::new(value));
if let ProtoNodeInput::Node(node_id) = proto_node.input {
let pre_node = nodes.get(node_id as usize).unwrap();
@ -378,6 +463,45 @@ static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[
}
},
),
(NodeIdentifier::new("graphene_std::vector::generator_nodes::UnitCircleGenerator", &[]), |_proto_node, stack| {
stack.push_fn(|_nodes| DynAnyNode::new(graphene_std::vector::generator_nodes::UnitCircleGenerator).into_type_erased())
}),
(NodeIdentifier::new("graphene_std::vector::generator_nodes::UnitSquareGenerator", &[]), |_proto_node, stack| {
stack.push_fn(|_nodes| DynAnyNode::new(graphene_std::vector::generator_nodes::UnitSquareGenerator).into_type_erased())
}),
(NodeIdentifier::new("graphene_std::vector::generator_nodes::BlitSubpath", &[]), |proto_node, stack| {
stack.push_fn(move |nodes| {
let ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("BlitSubpath without subpath input") };
let value_node = nodes.get(construction_nodes[0] as usize).unwrap();
let input_node: DowncastBothNode<_, (), Subpath> = DowncastBothNode::new(value_node);
let node = DynAnyNode::new(graphene_std::vector::generator_nodes::BlitSubpath::new(input_node));
if let ProtoNodeInput::Node(node_id) = proto_node.input {
let pre_node = nodes.get(node_id as usize).unwrap();
(pre_node).then(node).into_type_erased()
} else {
node.into_type_erased()
}
})
}),
(NodeIdentifier::new("graphene_std::vector::generator_nodes::TransformSubpathNode", &[]), |proto_node, stack| {
stack.push_fn(move |nodes| {
let ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("TransformSubpathNode without subpath input") };
let translate_node: DowncastBothNode<_, (), DVec2> = DowncastBothNode::new(nodes.get(construction_nodes[0] as usize).unwrap());
let rotate_node: DowncastBothNode<_, (), f64> = DowncastBothNode::new(nodes.get(construction_nodes[1] as usize).unwrap());
let scale_node: DowncastBothNode<_, (), DVec2> = DowncastBothNode::new(nodes.get(construction_nodes[2] as usize).unwrap());
let shear_node: DowncastBothNode<_, (), DVec2> = DowncastBothNode::new(nodes.get(construction_nodes[3] as usize).unwrap());
let node = DynAnyNode::new(graphene_std::vector::generator_nodes::TransformSubpathNode::new(translate_node, rotate_node, scale_node, shear_node));
if let ProtoNodeInput::Node(node_id) = proto_node.input {
let pre_node = nodes.get(node_id as usize).unwrap();
(pre_node).then(node).into_type_erased()
} else {
node.into_type_erased()
}
})
}),
];
pub fn push_node(proto_node: ProtoNode, stack: &FixedSizeStack<TypeErasedNode<'static>>) {

View File

@ -27,6 +27,13 @@ quote = {version = "1.0", default-features = false }
image = "*"
dyn-clone = "1.0"
log = "0.4"
bezier-rs = { path = "../../libraries/bezier-rs" }
kurbo = { git = "https://github.com/linebender/kurbo.git", features = [
"serde",
] }
glam = { version = "0.17", features = ["serde"] }
[dependencies.serde]
version = "1.0"
optional = true

View File

@ -1,3 +1,7 @@
// `macro_use` puts the log macros (`error!`, `warn!`, `debug!`, `info!` and `trace!`) in scope for the crate
#[macro_use]
extern crate log;
//pub mod value;
//#![feature(const_type_name)]
@ -5,6 +9,7 @@
pub mod memo;
pub mod raster;
pub mod vector;
pub mod any;

View File

@ -176,89 +176,286 @@ pub fn export_image_node<'n>() -> impl Node<(Image, &'n str), Output = Result<()
})
}
fn grayscale_image(mut image: Image) -> Image {
for pixel in &mut image.data {
let avg = (pixel.r() + pixel.g() + pixel.b()) / 3.;
*pixel = Color::from_rgbaf32_unchecked(avg, avg, avg, pixel.a());
}
image
}
#[derive(Debug, Clone, Copy)]
pub struct GrayscaleImageNode;
impl Node<Image> for GrayscaleImageNode {
type Output = Image;
fn eval(self, mut image: Image) -> Image {
for pixel in &mut image.data {
let avg = (pixel.r() + pixel.g() + pixel.b()) / 3.;
*pixel = Color::from_rgbaf32_unchecked(avg, avg, avg, pixel.a());
}
image
fn eval(self, image: Image) -> Image {
grayscale_image(image)
}
}
impl Node<Image> for &GrayscaleImageNode {
type Output = Image;
fn eval(self, mut image: Image) -> Image {
for pixel in &mut image.data {
let avg = (pixel.r() + pixel.g() + pixel.b()) / 3.;
*pixel = Color::from_rgbaf32_unchecked(avg, avg, avg, pixel.a());
}
image
fn eval(self, image: Image) -> Image {
grayscale_image(image)
}
}
fn invert_image(mut image: Image) -> Image {
for pixel in &mut image.data {
*pixel = Color::from_rgbaf32_unchecked(1. - pixel.r(), 1. - pixel.g(), 1. - pixel.b(), pixel.a());
}
image
}
#[derive(Debug, Clone, Copy)]
pub struct BrightenImageNode<N: Node<(), Output = f32>>(N);
pub struct InvertImageColorNode;
impl<N: Node<(), Output = f32>> Node<Image> for BrightenImageNode<N> {
impl Node<Image> for InvertImageColorNode {
type Output = Image;
fn eval(self, mut image: Image) -> Image {
let brightness = self.0.eval(());
let per_channel = |col: f32| (col + brightness / 255.).clamp(0., 1.);
for pixel in &mut image.data {
*pixel = Color::from_rgbaf32_unchecked(per_channel(pixel.r()), per_channel(pixel.g()), per_channel(pixel.b()), pixel.a());
}
image
fn eval(self, image: Image) -> Image {
invert_image(image)
}
}
impl<N: Node<(), Output = f32> + Copy> Node<Image> for &BrightenImageNode<N> {
impl Node<Image> for &InvertImageColorNode {
type Output = Image;
fn eval(self, mut image: Image) -> Image {
let brightness = self.0.eval(());
let per_channel = |col: f32| (col + brightness / 255.).clamp(0., 1.);
for pixel in &mut image.data {
*pixel = Color::from_rgbaf32_unchecked(per_channel(pixel.r()), per_channel(pixel.g()), per_channel(pixel.b()), pixel.a());
}
image
fn eval(self, image: Image) -> Image {
invert_image(image)
}
}
impl<N: Node<(), Output = f32> + Copy> BrightenImageNode<N> {
fn shift_image_hsl(mut image: Image, hue_shift: f32, saturation_shift: f32, luminance_shift: f32) -> Image {
for pixel in &mut image.data {
let [hue, saturation, luminance, alpha] = pixel.to_hsla();
*pixel = Color::from_hsla(
(hue + hue_shift / 360.) % 1.,
(saturation + saturation_shift / 100.).clamp(0., 1.),
(luminance + luminance_shift / 100.).clamp(0., 1.),
alpha,
);
}
image
}
#[derive(Debug, Clone, Copy)]
pub struct ShiftImageHSLNode<Hue, Sat, Lum>
where
Hue: Node<(), Output = f64>,
Sat: Node<(), Output = f64>,
Lum: Node<(), Output = f64>,
{
hue: Hue,
saturation: Sat,
luminance: Lum,
}
impl<Hue, Sat, Lum> Node<Image> for ShiftImageHSLNode<Hue, Sat, Lum>
where
Hue: Node<(), Output = f64>,
Sat: Node<(), Output = f64>,
Lum: Node<(), Output = f64>,
{
type Output = Image;
fn eval(self, image: Image) -> Image {
shift_image_hsl(image, self.hue.eval(()) as f32, self.saturation.eval(()) as f32, self.luminance.eval(()) as f32)
}
}
impl<Hue, Sat, Lum> Node<Image> for &ShiftImageHSLNode<Hue, Sat, Lum>
where
Hue: Node<(), Output = f64> + Copy,
Sat: Node<(), Output = f64> + Copy,
Lum: Node<(), Output = f64> + Copy,
{
type Output = Image;
fn eval(self, image: Image) -> Image {
shift_image_hsl(image, self.hue.eval(()) as f32, self.saturation.eval(()) as f32, self.luminance.eval(()) as f32)
}
}
impl<Hue, Sat, Lum> ShiftImageHSLNode<Hue, Sat, Lum>
where
Hue: Node<(), Output = f64>,
Sat: Node<(), Output = f64>,
Lum: Node<(), Output = f64>,
{
pub fn new(hue: Hue, saturation: Sat, luminance: Lum) -> Self {
Self { hue, saturation, luminance }
}
}
// Copy pasta from https://stackoverflow.com/questions/2976274/adjust-bitmap-image-brightness-contrast-using-c
fn adjust_image_brightness_and_contrast(mut image: Image, brightness_shift: f32, contrast: f32) -> Image {
let factor = (259. * (contrast + 255.)) / (255. * (259. - contrast));
let channel = |channel: f32| ((factor * (channel * 255. + brightness_shift - 128.) + 128.) / 255.).clamp(0., 1.);
for pixel in &mut image.data {
*pixel = Color::from_rgbaf32_unchecked(channel(pixel.r()), channel(pixel.g()), channel(pixel.b()), pixel.a())
}
image
}
#[derive(Debug, Clone, Copy)]
pub struct ImageBrightnessAndContrast<Brightness, Contrast>
where
Brightness: Node<(), Output = f64>,
Contrast: Node<(), Output = f64>,
{
brightness: Brightness,
contrast: Contrast,
}
impl<Brightness, Contrast> Node<Image> for ImageBrightnessAndContrast<Brightness, Contrast>
where
Brightness: Node<(), Output = f64>,
Contrast: Node<(), Output = f64>,
{
type Output = Image;
fn eval(self, image: Image) -> Image {
adjust_image_brightness_and_contrast(image, self.brightness.eval(()) as f32, self.contrast.eval(()) as f32)
}
}
impl<Brightness, Contrast> Node<Image> for &ImageBrightnessAndContrast<Brightness, Contrast>
where
Brightness: Node<(), Output = f64> + Copy,
Contrast: Node<(), Output = f64> + Copy,
{
type Output = Image;
fn eval(self, image: Image) -> Image {
adjust_image_brightness_and_contrast(image, self.brightness.eval(()) as f32, self.contrast.eval(()) as f32)
}
}
impl<Brightness, Contrast> ImageBrightnessAndContrast<Brightness, Contrast>
where
Brightness: Node<(), Output = f64>,
Contrast: Node<(), Output = f64>,
{
pub fn new(brightness: Brightness, contrast: Contrast) -> Self {
Self { brightness, contrast }
}
}
// https://www.dfstudios.co.uk/articles/programming/image-programming-algorithms/image-processing-algorithms-part-6-gamma-correction/
fn image_gamma(mut image: Image, gamma: f32) -> Image {
let inverse_gamma = 1. / gamma;
let channel = |channel: f32| channel.powf(inverse_gamma);
for pixel in &mut image.data {
*pixel = Color::from_rgbaf32_unchecked(channel(pixel.r()), channel(pixel.g()), channel(pixel.b()), pixel.a())
}
image
}
#[derive(Debug, Clone, Copy)]
pub struct ImageGammaNode<N: Node<(), Output = f64>>(N);
impl<N: Node<(), Output = f64>> Node<Image> for ImageGammaNode<N> {
type Output = Image;
fn eval(self, image: Image) -> Image {
image_gamma(image, self.0.eval(()) as f32)
}
}
impl<N: Node<(), Output = f64> + Copy> Node<Image> for &ImageGammaNode<N> {
type Output = Image;
fn eval(self, image: Image) -> Image {
image_gamma(image, self.0.eval(()) as f32)
}
}
impl<N: Node<(), Output = f64> + Copy> ImageGammaNode<N> {
pub fn new(node: N) -> Self {
Self(node)
}
}
fn image_opacity(mut image: Image, opacity_multiplier: f32) -> Image {
for pixel in &mut image.data {
*pixel = Color::from_rgbaf32_unchecked(pixel.r(), pixel.g(), pixel.b(), pixel.a() * opacity_multiplier)
}
image
}
// Based on http://www.axiomx.com/posterize.htm
fn posterize(mut image: Image, posterize_value: f32) -> Image {
let number_of_areas = posterize_value.recip();
let size_of_areas = (posterize_value - 1.).recip();
let channel = |channel: f32| (channel / number_of_areas).floor() * size_of_areas;
for pixel in &mut image.data {
*pixel = Color::from_rgbaf32_unchecked(channel(pixel.r()), channel(pixel.g()), channel(pixel.b()), pixel.a())
}
image
}
// Based on https://stackoverflow.com/questions/12166117/what-is-the-math-behind-exposure-adjustment-on-photoshop
fn exposure(mut image: Image, exposure: f32) -> Image {
let multiplier = 2f32.powf(exposure);
let channel = |channel: f32| channel * multiplier;
for pixel in &mut image.data {
*pixel = Color::from_rgbaf32_unchecked(channel(pixel.r()), channel(pixel.g()), channel(pixel.b()), pixel.a())
}
image
}
#[derive(Debug, Clone, Copy)]
pub struct Posterize<N: Node<(), Output = f64>>(N);
impl<N: Node<(), Output = f64>> Node<Image> for Posterize<N> {
type Output = Image;
fn eval(self, image: Image) -> Image {
posterize(image, self.0.eval(()) as f32)
}
}
impl<N: Node<(), Output = f64> + Copy> Node<Image> for &Posterize<N> {
type Output = Image;
fn eval(self, image: Image) -> Image {
posterize(image, self.0.eval(()) as f32)
}
}
impl<N: Node<(), Output = f64> + Copy> Posterize<N> {
pub fn new(node: N) -> Self {
Self(node)
}
}
#[derive(Debug, Clone, Copy)]
pub struct HueShiftImage<N: Node<(), Output = f32>>(N);
pub struct ImageOpacityNode<N: Node<(), Output = f64>>(N);
impl<N: Node<(), Output = f32>> Node<Image> for HueShiftImage<N> {
impl<N: Node<(), Output = f64>> Node<Image> for ImageOpacityNode<N> {
type Output = Image;
fn eval(self, mut image: Image) -> Image {
let hue_shift = self.0.eval(());
for pixel in &mut image.data {
let [hue, saturation, luminance, alpha] = pixel.to_hsla();
*pixel = Color::from_hsla(hue + hue_shift / 360., saturation, luminance, alpha);
}
image
fn eval(self, image: Image) -> Image {
image_opacity(image, self.0.eval(()) as f32)
}
}
impl<N: Node<(), Output = f32> + Copy> Node<Image> for &HueShiftImage<N> {
impl<N: Node<(), Output = f64> + Copy> Node<Image> for &ImageOpacityNode<N> {
type Output = Image;
fn eval(self, mut image: Image) -> Image {
let hue_shift = self.0.eval(());
for pixel in &mut image.data {
let [hue, saturation, luminance, alpha] = pixel.to_hsla();
*pixel = Color::from_hsla(hue + hue_shift / 360., saturation, luminance, alpha);
}
image
fn eval(self, image: Image) -> Image {
image_opacity(image, self.0.eval(()) as f32)
}
}
impl<N: Node<(), Output = f32> + Copy> HueShiftImage<N> {
impl<N: Node<(), Output = f64> + Copy> ImageOpacityNode<N> {
pub fn new(node: N) -> Self {
Self(node)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ExposureNode<N: Node<(), Output = f64>>(N);
impl<N: Node<(), Output = f64>> Node<Image> for ExposureNode<N> {
type Output = Image;
fn eval(self, image: Image) -> Image {
exposure(image, self.0.eval(()) as f32)
}
}
impl<N: Node<(), Output = f64> + Copy> Node<Image> for &ExposureNode<N> {
type Output = Image;
fn eval(self, image: Image) -> Image {
exposure(image, self.0.eval(()) as f32)
}
}
impl<N: Node<(), Output = f64> + Copy> ExposureNode<N> {
pub fn new(node: N) -> Self {
Self(node)
}

View File

@ -0,0 +1,158 @@
use std::sync::Arc;
use glam::{DAffine2, DVec2};
use graphene_core::Node;
use super::subpath::Subpath;
type VectorData = Subpath;
pub struct UnitCircleGenerator;
impl Node<()> for UnitCircleGenerator {
type Output = VectorData;
fn eval(self, input: ()) -> Self::Output {
(&self).eval(input)
}
}
impl Node<()> for &UnitCircleGenerator {
type Output = VectorData;
fn eval(self, _input: ()) -> Self::Output {
Subpath::new_ellipse(DVec2::ZERO, DVec2::ONE)
}
}
#[derive(Debug, Clone, Copy)]
pub struct UnitSquareGenerator;
impl Node<()> for UnitSquareGenerator {
type Output = VectorData;
fn eval(self, input: ()) -> Self::Output {
(&self).eval(input)
}
}
impl Node<()> for &UnitSquareGenerator {
type Output = VectorData;
fn eval(self, _input: ()) -> Self::Output {
Subpath::new_rect(DVec2::ZERO, DVec2::ONE)
}
}
#[derive(Debug, Clone)]
pub struct PathGenerator(Arc<Subpath>);
impl Node<()> for PathGenerator {
type Output = VectorData;
fn eval(self, input: ()) -> Self::Output {
(&self).eval(input)
}
}
impl Node<()> for &PathGenerator {
type Output = VectorData;
fn eval(self, _input: ()) -> Self::Output {
(*self.0).clone()
}
}
use crate::raster::Image;
#[derive(Debug, Clone, Copy)]
pub struct BlitSubpath<N: Node<(), Output = Subpath>>(N);
impl<N: Node<(), Output = Subpath>> Node<Image> for BlitSubpath<N> {
type Output = Image;
fn eval(self, input: Image) -> Self::Output {
let subpath = self.0.eval(());
info!("Blitting subpath {subpath:?}");
input
}
}
impl<N: Node<(), Output = Subpath> + Copy> Node<Image> for &BlitSubpath<N> {
type Output = Image;
fn eval(self, input: Image) -> Self::Output {
let subpath = self.0.eval(());
info!("Blitting subpath {subpath:?}");
input
}
}
impl<N: Node<(), Output = Subpath>> BlitSubpath<N> {
pub fn new(node: N) -> Self {
Self(node)
}
}
#[derive(Debug, Clone, Copy)]
pub struct TransformSubpathNode<Translation, Rotation, Scale, Shear>
where
Translation: Node<(), Output = DVec2>,
Rotation: Node<(), Output = f64>,
Scale: Node<(), Output = DVec2>,
Shear: Node<(), Output = DVec2>,
{
translate_node: Translation,
rotate_node: Rotation,
scale_node: Scale,
shear_node: Shear,
}
impl<Translation, Rotation, Scale, Shear> TransformSubpathNode<Translation, Rotation, Scale, Shear>
where
Translation: Node<(), Output = DVec2>,
Rotation: Node<(), Output = f64>,
Scale: Node<(), Output = DVec2>,
Shear: Node<(), Output = DVec2>,
{
pub fn new(translate_node: Translation, rotate_node: Rotation, scale_node: Scale, shear_node: Shear) -> Self {
Self {
translate_node,
rotate_node,
scale_node,
shear_node,
}
}
}
impl<Translation, Rotation, Scale, Shear> Node<Subpath> for TransformSubpathNode<Translation, Rotation, Scale, Shear>
where
Translation: Node<(), Output = DVec2>,
Rotation: Node<(), Output = f64>,
Scale: Node<(), Output = DVec2>,
Shear: Node<(), Output = DVec2>,
{
type Output = Subpath;
fn eval(self, mut subpath: Subpath) -> Subpath {
let translate = self.translate_node.eval(());
let rotate = self.rotate_node.eval(());
let scale = self.scale_node.eval(());
let shear = self.shear_node.eval(());
let (sin, cos) = rotate.sin_cos();
subpath.apply_affine(DAffine2::from_cols_array(&[scale.x + cos, shear.y + sin, shear.x - sin, scale.y + cos, translate.x, translate.y]));
subpath
}
}
impl<Translation, Rotation, Scale, Shear> Node<Subpath> for &TransformSubpathNode<Translation, Rotation, Scale, Shear>
where
Translation: Node<(), Output = DVec2> + Copy,
Rotation: Node<(), Output = f64> + Copy,
Scale: Node<(), Output = DVec2> + Copy,
Shear: Node<(), Output = DVec2> + Copy,
{
type Output = Subpath;
fn eval(self, mut subpath: Subpath) -> Subpath {
let translate = self.translate_node.eval(());
let rotate = self.rotate_node.eval(());
let scale = self.scale_node.eval(());
let shear = self.shear_node.eval(());
let (sin, cos) = rotate.sin_cos();
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

@ -25,6 +25,15 @@ pub struct IdBackedVec<T> {
}
impl<T> IdBackedVec<T> {
/// Creates a new empty vector
pub const fn new() -> Self {
IdBackedVec {
elements: vec![],
element_ids: vec![],
next_id: 0,
}
}
/// Push a new element to the start of the vector
pub fn push_front(&mut self, element: T) -> Option<ElementId> {
self.next_id += 1;
@ -135,11 +144,7 @@ impl<T> IdBackedVec<T> {
impl<T> Default for IdBackedVec<T> {
fn default() -> Self {
IdBackedVec {
elements: vec![],
element_ids: vec![],
next_id: 0,
}
Self::new()
}
}

View File

@ -1,4 +1,6 @@
pub mod consts;
pub mod generator_nodes;
pub mod id_vec;
pub mod manipulator_group;
pub mod manipulator_point;
pub mod subpath;

View File

@ -1,9 +1,9 @@
use super::consts::ManipulatorType;
use super::id_vec::IdBackedVec;
use super::manipulator_group::ManipulatorGroup;
use super::manipulator_point::ManipulatorPoint;
use crate::layers::id_vec::IdBackedVec;
use crate::layers::layer_info::{Layer, LayerDataType};
use dyn_any::{DynAny, StaticType};
use glam::{DAffine2, DVec2};
use kurbo::{BezPath, PathEl, Shape};
use serde::{Deserialize, Serialize};
@ -11,15 +11,15 @@ use serde::{Deserialize, Serialize};
/// [Subpath] represents a single vector path, containing many [ManipulatorGroups].
/// For each closed shape we keep a [Subpath] which contains the [ManipulatorGroup]s (handles and anchors) that define that shape.
// TODO Add "closed" bool to subpath
#[derive(PartialEq, Clone, Debug, Default, Serialize, Deserialize)]
#[derive(PartialEq, Clone, Debug, Default, Serialize, Deserialize, DynAny)]
pub struct Subpath(IdBackedVec<ManipulatorGroup>);
impl Subpath {
// ** INITIALIZATION **
/// Create a new [Subpath] with no [ManipulatorGroup]s.
pub fn new() -> Self {
Subpath { ..Default::default() }
pub const fn new() -> Self {
Subpath(IdBackedVec::new())
}
/// Construct a [Subpath] from a point iterator
@ -421,34 +421,6 @@ impl Subpath {
}
}
// ** CONVERSIONS **
impl<'a> TryFrom<&'a mut Layer> for &'a mut Subpath {
type Error = &'static str;
/// Convert a mutable layer into a mutable [Subpath].
fn try_from(layer: &'a mut Layer) -> Result<&'a mut Subpath, Self::Error> {
match &mut layer.data {
LayerDataType::Shape(layer) => Ok(&mut layer.shape),
// TODO Resolve converting text into a Subpath at the layer level
// LayerDataType::Text(text) => Some(Subpath::new(path_to_shape.to_vec(), viewport_transform, true)),
_ => Err("Did not find any shape data in the layer"),
}
}
}
impl<'a> TryFrom<&'a Layer> for &'a Subpath {
type Error = &'static str;
/// Convert a reference to a layer into a reference of a [Subpath].
fn try_from(layer: &'a Layer) -> Result<&'a Subpath, Self::Error> {
match &layer.data {
LayerDataType::Shape(layer) => Ok(&layer.shape),
// TODO Resolve converting text into a Subpath at the layer level
// LayerDataType::Text(text) => Some(Subpath::new(path_to_shape.to_vec(), viewport_transform, true)),
_ => Err("Did not find any shape data in the layer"),
}
}
}
/// A wrapper around [`bezier_rs::Bezier`] containing also the IDs for the [`ManipulatorGroup`]s where the points are from.
pub struct BezierId {
/// The internal [`bezier_rs::Bezier`].