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

View file

@ -27,6 +27,10 @@ pub fn default_mapping() -> Mapping {
), ),
// NORMAL PRIORITY: // NORMAL PRIORITY:
// //
// NodeGraphMessage
entry!(KeyDown(Delete); action_dispatch=NodeGraphMessage::DeleteSelectedNodes),
entry!(KeyDown(Backspace); action_dispatch=NodeGraphMessage::DeleteSelectedNodes),
//
// TransformLayerMessage // TransformLayerMessage
entry!(KeyDown(Enter); action_dispatch=TransformLayerMessage::ApplyTransformOperation), entry!(KeyDown(Enter); action_dispatch=TransformLayerMessage::ApplyTransformOperation),
entry!(KeyDown(Lmb); 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 { pub fn new(widget: Widget) -> Self {
Self { widget_id: generate_uuid(), widget } 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)] #[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::layer_info::{LayerDataType, LayerDataTypeDiscriminant};
use graphene::layers::style::{Fill, RenderData, ViewMode}; use graphene::layers::style::{Fill, RenderData, ViewMode};
use graphene::layers::text_layer::{Font, FontCache}; use graphene::layers::text_layer::{Font, FontCache};
use graphene::layers::vector::subpath::Subpath;
use graphene::{DocumentError, DocumentResponse, LayerId, Operation as DocumentOperation}; use graphene::{DocumentError, DocumentResponse, LayerId, Operation as DocumentOperation};
use graphene_std::vector::subpath::Subpath;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -928,6 +928,7 @@ impl MessageHandler<DocumentMessage, (u64, &InputPreprocessorMessageHandler, &Pe
} }
common.extend(self.navigation_handler.actions()); common.extend(self.navigation_handler.actions());
common.extend(self.transform_layer_handler.actions()); common.extend(self.transform_layer_handler.actions());
common.extend(self.node_graph_handler.actions());
common common
} }
} }

View file

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

View file

@ -1,6 +1,6 @@
use crate::messages::layout::utility_types::layout_widget::LayoutGroup; use crate::messages::layout::utility_types::layout_widget::LayoutGroup;
use crate::messages::prelude::*; 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::document::Document;
use graphene::layers::layer_info::LayerDataType; use graphene::layers::layer_info::LayerDataType;
use graphene::layers::nodegraph_layer::NodeGraphFrameLayer; use graphene::layers::nodegraph_layer::NodeGraphFrameLayer;
@ -18,9 +18,13 @@ pub enum FrontendGraphDataType {
#[serde(rename = "color")] #[serde(rename = "color")]
Color, Color,
#[serde(rename = "vector")] #[serde(rename = "vector")]
Vector, Subpath,
#[serde(rename = "number")] #[serde(rename = "number")]
Number, Number,
#[serde(rename = "boolean")]
Boolean,
#[serde(rename = "vec2")]
Vector,
} }
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] #[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)] #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FrontendNodeType { pub struct FrontendNodeType {
pub name: String, pub name: String,
pub category: String,
} }
impl FrontendNodeType { impl FrontendNodeType {
pub fn new(name: &'static str) -> Self { pub fn new(name: &'static str, category: &'static str) -> Self {
Self { name: name.to_string() } 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); log::debug!("Nodes:\n{:#?}\n\nFrontend Nodes:\n{:#?}\n\nLinks:\n{:#?}", network.nodes, nodes, links);
responses.push_back(FrontendMessage::UpdateNodeGraph { nodes, links }.into()); 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 { impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageHandler)> for NodeGraphMessageHandler {
@ -188,7 +244,7 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
Self::send_graph(network, responses); Self::send_graph(network, responses);
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into()); 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 node_id = node_id.unwrap_or_else(crate::application::generate_uuid);
let Some(network) = self.get_active_network_mut(document) else{ let Some(network) = self.get_active_network_mut(document) else{
warn!("No network"); warn!("No network");
@ -218,7 +274,6 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
.into_iter() .into_iter()
.collect(), .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( network.nodes.insert(
node_id, node_id,
@ -227,19 +282,29 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
inputs: document_node_type.inputs.iter().map(|input| input.default.clone()).collect(), inputs: document_node_type.inputs.iter().map(|input| input.default.clone()).collect(),
// TODO: Allow inserting nodes that contain other nodes. // TODO: Allow inserting nodes that contain other nodes.
implementation: DocumentNodeImplementation::Network(inner_network), implementation: DocumentNodeImplementation::Network(inner_network),
metadata: graph_craft::document::DocumentNodeMetadata { metadata: graph_craft::document::DocumentNodeMetadata { position: (x, y) },
// TODO: Better position default
position: (far_right_node.0 + 7, far_right_node.1 + 2),
},
}, },
); );
Self::send_graph(network, responses); Self::send_graph(network, responses);
} }
NodeGraphMessage::DeleteNode { node_id } => { NodeGraphMessage::DeleteNode { node_id } => {
if let Some(network) = self.get_active_network_mut(document) { if let Some(network) = self.get_active_network_mut(document) {
network.nodes.remove(&node_id); if self.remove_node(network, node_id) {
Self::send_graph(network, responses); Self::send_graph(network, responses);
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into()); 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 } => { 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::layout_widget::{LayoutGroup, Widget, WidgetHolder};
use crate::messages::layout::utility_types::widgets::label_widgets::TextLabel; use crate::messages::layout::utility_types::widgets::label_widgets::TextLabel;
use glam::DVec2;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, NodeId, NodeInput}; use graph_craft::document::{DocumentNode, NodeId, NodeInput};
use graph_craft::proto::{NodeIdentifier, Type}; use graph_craft::proto::{NodeIdentifier, Type};
use graphene_std::raster::Image; use graphene_std::raster::Image;
use graphene_std::vector::subpath::Subpath;
use std::borrow::Cow; use std::borrow::Cow;
@ -15,8 +17,19 @@ pub struct DocumentInputType {
pub default: NodeInput, 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 struct DocumentNodeType {
pub name: &'static str, pub name: &'static str,
pub category: &'static str,
pub identifier: NodeIdentifier, pub identifier: NodeIdentifier,
pub inputs: &'static [DocumentInputType], pub inputs: &'static [DocumentInputType],
pub outputs: &'static [FrontendGraphDataType], pub outputs: &'static [FrontendGraphDataType],
@ -24,9 +37,10 @@ pub struct DocumentNodeType {
} }
// TODO: Dynamic node library // TODO: Dynamic node library
static DOCUMENT_NODE_TYPES: [DocumentNodeType; 7] = [ static DOCUMENT_NODE_TYPES: &[DocumentNodeType] = &[
DocumentNodeType { DocumentNodeType {
name: "Identity", name: "Identity",
category: "Meta",
identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Concrete(Cow::Borrowed("Any<'_>"))]), identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Concrete(Cow::Borrowed("Any<'_>"))]),
inputs: &[DocumentInputType { inputs: &[DocumentInputType {
name: "In", name: "In",
@ -45,6 +59,7 @@ static DOCUMENT_NODE_TYPES: [DocumentNodeType; 7] = [
}, },
DocumentNodeType { DocumentNodeType {
name: "Input", name: "Input",
category: "Meta",
identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Concrete(Cow::Borrowed("Any<'_>"))]), identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Concrete(Cow::Borrowed("Any<'_>"))]),
inputs: &[DocumentInputType { inputs: &[DocumentInputType {
name: "In", name: "In",
@ -52,129 +67,273 @@ static DOCUMENT_NODE_TYPES: [DocumentNodeType; 7] = [
default: NodeInput::Network, default: NodeInput::Network,
}], }],
outputs: &[FrontendGraphDataType::Raster], outputs: &[FrontendGraphDataType::Raster],
properties: |_document_node, _node_id| { properties: |_document_node, _node_id| node_properties::string_properties("The input to the graph is the bitmap under the frame".to_string()),
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()
}))],
}]
},
}, },
DocumentNodeType { DocumentNodeType {
name: "Output", name: "Output",
category: "Meta",
identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Concrete(Cow::Borrowed("Any<'_>"))]), identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Concrete(Cow::Borrowed("Any<'_>"))]),
inputs: &[DocumentInputType { inputs: &[DocumentInputType {
name: "In", name: "In",
data_type: FrontendGraphDataType::Raster, data_type: FrontendGraphDataType::Raster,
default: NodeInput::Value { default: NodeInput::value(TaggedValue::Image(Image::empty()), true),
tagged_value: TaggedValue::Image(Image::empty()),
exposed: true,
},
}], }],
outputs: &[], outputs: &[],
properties: |_document_node, _node_id| { properties: |_document_node, _node_id| node_properties::string_properties("The output to the graph is rendered in the frame".to_string()),
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()
}))],
}]
},
}, },
DocumentNodeType { DocumentNodeType {
name: "Grayscale Image", name: "Grayscale Image",
category: "Image Color Correction",
identifier: NodeIdentifier::new("graphene_std::raster::GrayscaleImageNode", &[]), identifier: NodeIdentifier::new("graphene_std::raster::GrayscaleImageNode", &[]),
inputs: &[DocumentInputType { inputs: &[DocumentInputType {
name: "Image", name: "Image",
data_type: FrontendGraphDataType::Raster, data_type: FrontendGraphDataType::Raster,
default: NodeInput::Value { default: NodeInput::value(TaggedValue::Image(Image::empty()), true),
tagged_value: TaggedValue::Image(Image::empty()),
exposed: true,
},
}], }],
outputs: &[FrontendGraphDataType::Raster], outputs: &[FrontendGraphDataType::Raster],
properties: |_document_node, _node_id| { properties: node_properties::no_properties,
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()
}))],
}]
},
}, },
DocumentNodeType { DocumentNodeType {
name: "Brighten Image", name: "Invert Image Color",
identifier: NodeIdentifier::new("graphene_std::raster::BrightenImageNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]), 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: &[ inputs: &[
DocumentInputType { DocumentInputType {
name: "Image", name: "Image",
data_type: FrontendGraphDataType::Raster, data_type: FrontendGraphDataType::Raster,
default: NodeInput::Value { default: NodeInput::value(TaggedValue::Image(Image::empty()), true),
tagged_value: TaggedValue::Image(Image::empty()),
exposed: true,
},
}, },
DocumentInputType { DocumentInputType {
name: "Amount", name: "Hue Shift",
data_type: FrontendGraphDataType::Number, data_type: FrontendGraphDataType::Number,
default: NodeInput::Value { default: NodeInput::value(TaggedValue::F64(0.), false),
tagged_value: TaggedValue::F32(10.), },
exposed: 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], outputs: &[FrontendGraphDataType::Raster],
properties: super::node_properties::brighten_image_properties, properties: node_properties::adjust_hsl_properties,
}, },
DocumentNodeType { DocumentNodeType {
name: "Hue Shift Image", name: "Image Contrast and Brightness",
identifier: NodeIdentifier::new("graphene_std::raster::HueShiftImage", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]), category: "Image Color Correction",
identifier: NodeIdentifier::new("graphene_std::raster::ImageBrightnessAndContrast", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
inputs: &[ inputs: &[
DocumentInputType { DocumentInputType {
name: "Image", name: "Image",
data_type: FrontendGraphDataType::Raster, data_type: FrontendGraphDataType::Raster,
default: NodeInput::Value { default: NodeInput::value(TaggedValue::Image(Image::empty()), true),
tagged_value: TaggedValue::Image(Image::empty()),
exposed: true,
},
}, },
DocumentInputType { DocumentInputType {
name: "Amount", name: "Brightness",
data_type: FrontendGraphDataType::Number, data_type: FrontendGraphDataType::Number,
default: NodeInput::Value { default: NodeInput::value(TaggedValue::F64(0.), false),
tagged_value: TaggedValue::F32(10.), },
exposed: false, DocumentInputType {
}, name: "Contrast",
data_type: FrontendGraphDataType::Number,
default: NodeInput::value(TaggedValue::F64(0.), false),
}, },
], ],
outputs: &[FrontendGraphDataType::Raster], 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 { DocumentNodeType {
name: "Add", name: "Add",
category: "Mathmatics",
identifier: NodeIdentifier::new("graphene_core::ops::AddNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]), identifier: NodeIdentifier::new("graphene_core::ops::AddNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
inputs: &[ inputs: &[
DocumentInputType { DocumentInputType {
name: "Input", name: "Input",
data_type: FrontendGraphDataType::Number, data_type: FrontendGraphDataType::Number,
default: NodeInput::Value { default: NodeInput::value(TaggedValue::F64(0.), true),
tagged_value: TaggedValue::F32(0.),
exposed: true,
},
}, },
DocumentInputType { DocumentInputType {
name: "Addend", name: "Addend",
data_type: FrontendGraphDataType::Number, data_type: FrontendGraphDataType::Number,
default: NodeInput::Value { default: NodeInput::value(TaggedValue::F64(0.), true),
tagged_value: TaggedValue::F32(0.),
exposed: true,
},
}, },
], ],
outputs: &[FrontendGraphDataType::Number], 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> { pub fn resolve_document_node_type(name: &str) -> Option<&DocumentNodeType> {
@ -185,6 +344,6 @@ pub fn collect_node_types() -> Vec<FrontendNodeType> {
DOCUMENT_NODE_TYPES DOCUMENT_NODE_TYPES
.iter() .iter()
.filter(|node_type| !matches!(node_type.name, "Input" | "Output")) .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() .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::layout_widget::{LayoutGroup, Widget, WidgetCallback, WidgetHolder};
use crate::messages::layout::utility_types::widgets::button_widgets::ParameterExposeButton; 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::input_widgets::{NumberInput, NumberInputMode};
use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType, TextLabel};
use crate::messages::prelude::NodeGraphMessage; use crate::messages::prelude::NodeGraphMessage;
use glam::DVec2;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, NodeId, NodeInput}; use graph_craft::document::{DocumentNode, NodeId, NodeInput};
use super::FrontendGraphDataType; use super::FrontendGraphDataType;
pub fn hue_shift_image_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<LayoutGroup> { pub fn string_properties(text: impl Into<String>) -> Vec<LayoutGroup> {
let index = 1; 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 input: &NodeInput = document_node.inputs.get(index).unwrap();
let exposed = input.is_exposed();
let mut widgets = vec![ let mut widgets = vec![
WidgetHolder::new(Widget::ParameterExposeButton(ParameterExposeButton { expose_widget(node_id, index, FrontendGraphDataType::Number, input.is_exposed()),
exposed, WidgetHolder::unrelated_seperator(),
data_type: FrontendGraphDataType::Number, WidgetHolder::text_widget(name),
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()
})),
]; ];
if let NodeInput::Value { if let NodeInput::Value {
tagged_value: TaggedValue::F32(x), tagged_value: TaggedValue::F64(x),
exposed: false, exposed: false,
} = document_node.inputs[index] } = document_node.inputs[index]
{ {
widgets.extend_from_slice(&[ widgets.extend_from_slice(&[
WidgetHolder::new(Widget::Separator(Separator { WidgetHolder::unrelated_seperator(),
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput { WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(x as f64), value: Some(x),
unit: "°".into(), mode: if range_max.is_some() { NumberInputMode::Range } else { NumberInputMode::Increment },
mode: NumberInputMode::Range, range_min,
range_min: Some(-180.), range_max,
range_max: Some(180.), unit,
on_update: WidgetCallback::new(move |number_input: &NumberInput| { is_integer,
NodeGraphMessage::SetInputValue { on_update: update_value(|x: &NumberInput| TaggedValue::F64(x.value.unwrap()), node_id, index),
node: node_id,
input_index: 1,
value: TaggedValue::F32(number_input.value.unwrap() as f32),
}
.into()
}),
..NumberInput::default() ..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> { pub fn brighten_image_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<LayoutGroup> {
let index = 1; let brightness = number_range_widget(document_node, node_id, 1, "Brightness", Some(-255.), Some(255.), "".into(), false);
let input: &NodeInput = document_node.inputs.get(index).unwrap(); let contrast = number_range_widget(document_node, node_id, 2, "Contrast", Some(-255.), Some(255.), "".into(), false);
let exposed = input.is_exposed();
let mut widgets = vec![ vec![LayoutGroup::Row { widgets: brightness }, LayoutGroup::Row { widgets: contrast }]
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()
})),
];
if let NodeInput::Value { pub fn adjust_gamma_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<LayoutGroup> {
tagged_value: TaggedValue::F32(x), let gamma = number_range_widget(document_node, node_id, 1, "Gamma", Some(0.01), None, "".into(), false);
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()
})),
])
}
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> { pub fn add_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<LayoutGroup> {
let operand = |name: &str, index| { let operand = |name: &str, index| {
let input: &NodeInput = document_node.inputs.get(index).unwrap(); let input: &NodeInput = document_node.inputs.get(index).unwrap();
let exposed = input.is_exposed();
let mut widgets = vec![ let mut widgets = vec![
WidgetHolder::new(Widget::ParameterExposeButton(ParameterExposeButton { expose_widget(node_id, index, FrontendGraphDataType::Number, input.is_exposed()),
exposed, WidgetHolder::unrelated_seperator(),
data_type: FrontendGraphDataType::Number, WidgetHolder::text_widget(name),
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()
})),
]; ];
if let NodeInput::Value { if let NodeInput::Value {
@ -166,21 +131,11 @@ pub fn add_properties(document_node: &DocumentNode, node_id: NodeId) -> Vec<Layo
} = document_node.inputs[index] } = document_node.inputs[index]
{ {
widgets.extend_from_slice(&[ widgets.extend_from_slice(&[
WidgetHolder::new(Widget::Separator(Separator { WidgetHolder::unrelated_seperator(),
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput { WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some(x as f64), value: Some(x as f64),
mode: NumberInputMode::Increment, mode: NumberInputMode::Increment,
on_update: WidgetCallback::new(move |number_input: &NumberInput| { on_update: update_value(|number_input: &NumberInput| TaggedValue::F32(number_input.value.unwrap() as f32), node_id, index),
NodeGraphMessage::SetInputValue {
node: node_id,
input_index: index,
value: TaggedValue::F32(number_input.value.unwrap() as f32),
}
.into()
}),
..NumberInput::default() ..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)] 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> { fn unknown_node_properties(document_node: &DocumentNode) -> Vec<LayoutGroup> {
vec![LayoutGroup::Row { string_properties(format!("Node '{}' cannot be found in library", document_node.name))
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel { }
value: format!("Node '{}' cannot be found in library", document_node.name),
..Default::default() 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 { 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(), .into(),
); );
responses.push_back(NodeGraphMessage::CloseNodeGraph.into());
} }
} }
ResendActiveProperties => { ResendActiveProperties => {

View file

@ -6,11 +6,11 @@ use crate::messages::prelude::*;
use graphene::color::Color; use graphene::color::Color;
use graphene::document::Document; use graphene::document::Document;
use graphene::layers::style::{self, Fill, Stroke}; 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::{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}; use glam::{DAffine2, DVec2};

View file

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

View file

@ -1,11 +1,11 @@
use crate::messages::prelude::*; use crate::messages::prelude::*;
use bezier_rs::ComputeType; 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::{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 glam::DVec2;
use graphene::document::Document; use graphene::document::Document;

View file

@ -7,8 +7,8 @@ use crate::messages::prelude::*;
use graphene::layers::layer_info::{Layer, LayerDataType}; use graphene::layers::layer_info::{Layer, LayerDataType};
use graphene::layers::style::{self, Stroke}; use graphene::layers::style::{self, Stroke};
use graphene::layers::vector::consts::ManipulatorType;
use graphene::{LayerId, Operation}; use graphene::{LayerId, Operation};
use graphene_std::vector::consts::ManipulatorType;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use std::f64::consts::PI; 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 crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::intersection::Quad; use graphene::intersection::Quad;
use graphene::layers::vector::consts::ManipulatorType; use graphene_std::vector::consts::ManipulatorType;
use glam::DVec2; use glam::DVec2;
use serde::{Deserialize, Serialize}; 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 crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use graphene::layers::style; use graphene::layers::style;
use graphene::layers::vector::consts::ManipulatorType;
use graphene::layers::vector::manipulator_group::ManipulatorGroup;
use graphene::LayerId; use graphene::LayerId;
use graphene::Operation; use graphene::Operation;
use graphene_std::vector::consts::ManipulatorType;
use graphene_std::vector::manipulator_group::ManipulatorGroup;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View file

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

View file

@ -1,12 +1,6 @@
<template> <template>
<LayoutCol class="node-graph"> <LayoutCol class="node-graph">
<LayoutRow class="options-bar"></LayoutRow> <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 <LayoutRow
class="graph" class="graph"
ref="graph" ref="graph"
@ -21,6 +15,14 @@
'--dot-radius': `${dotRadius}px`, '--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 <div
class="nodes" class="nodes"
ref="nodesContainer" ref="nodesContainer"
@ -37,8 +39,6 @@
:style="{ :style="{
'--offset-left': (node.position?.x || 0) + (selected.includes(node.id) ? draggingNodes?.roundX || 0 : 0), '--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), '--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" :data-node="node.id"
> >
@ -111,10 +111,9 @@
.node-list { .node-list {
width: max-content; width: max-content;
position: fixed; position: fixed;
padding: 20px; padding: 5px;
margin: 40px 10px;
z-index: 3; z-index: 3;
background-color: var(--color-4-dimgray); background-color: var(--color-3-darkgray);
} }
.options-bar { .options-bar {
@ -181,7 +180,7 @@
border-radius: 4px; border-radius: 4px;
background: var(--color-4-dimgray); background: var(--color-4-dimgray);
left: calc((var(--offset-left) + 0.5) * 24px); 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 { &.selected {
border: 1px solid var(--color-e-nearwhite); border: 1px solid var(--color-e-nearwhite);
@ -209,6 +208,7 @@
.arguments { .arguments {
display: flex; display: flex;
flex-direction: column;
width: 100%; width: 100%;
position: relative; position: relative;
@ -297,6 +297,7 @@ import type { IconName } from "@/utility-functions/icons";
import LayoutCol from "@/components/layout/LayoutCol.vue"; import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue"; import LayoutRow from "@/components/layout/LayoutRow.vue";
import TextButton from "@/components/widgets/buttons/TextButton.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 IconLabel from "@/components/widgets/labels/IconLabel.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue"; import TextLabel from "@/components/widgets/labels/TextLabel.vue";
@ -316,6 +317,8 @@ export default defineComponent({
linkInProgressFromConnector: undefined as HTMLDivElement | undefined, linkInProgressFromConnector: undefined as HTMLDivElement | undefined,
linkInProgressToConnector: undefined as HTMLDivElement | DOMRect | undefined, linkInProgressToConnector: undefined as HTMLDivElement | DOMRect | undefined,
nodeLinkPaths: [] as [string, string][], nodeLinkPaths: [] as [string, string][],
searchTerm: "",
nodeListLocation: undefined as { x: number; y: number } | undefined,
}; };
}, },
computed: { computed: {
@ -335,8 +338,24 @@ export default defineComponent({
nodes() { nodes() {
return this.nodeGraph.state.nodes; return this.nodeGraph.state.nodes;
}, },
nodeTypes() { nodeCategories() {
return this.nodeGraph.state.nodeTypes; 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 { linkPathInProgress(): [string, string] | undefined {
if (this.linkInProgressFromConnector && this.linkInProgressToConnector) { if (this.linkInProgressFromConnector && this.linkInProgressToConnector) {
@ -461,8 +480,19 @@ export default defineComponent({
} }
}, },
pointerDown(e: PointerEvent) { 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 port = (e.target as HTMLDivElement).closest("[data-port]") as HTMLDivElement;
const node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined; 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) { if (port) {
const isOutput = Boolean(port.getAttribute("data-port") === "output"); const isOutput = Boolean(port.getAttribute("data-port") === "output");
@ -488,7 +518,7 @@ export default defineComponent({
} }
this.editor.instance.selectNodes(new BigUint64Array(this.selected)); this.editor.instance.selectNodes(new BigUint64Array(this.selected));
} else { } else if (!nodeList) {
this.selected = []; this.selected = [];
this.editor.instance.selectNodes(new BigUint64Array(this.selected)); this.editor.instance.selectNodes(new BigUint64Array(this.selected));
const graphDiv: HTMLDivElement | undefined = (this.$refs.graph as typeof LayoutCol | undefined)?.$el; const graphDiv: HTMLDivElement | undefined = (this.$refs.graph as typeof LayoutCol | undefined)?.$el;
@ -560,7 +590,10 @@ export default defineComponent({
this.linkInProgressToConnector = undefined; this.linkInProgressToConnector = undefined;
}, },
createNode(nodeType: string): void { 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() { mounted() {
@ -578,6 +611,7 @@ export default defineComponent({
LayoutRow, LayoutRow,
TextLabel, TextLabel,
TextButton, TextButton,
TextInput,
}, },
}); });
</script> </script>

View file

@ -1,6 +1,6 @@
<template> <template>
<LayoutRow class="parameter-expose-button"> <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> </LayoutRow>
</template> </template>
@ -53,21 +53,6 @@ export default defineComponent({
// Callbacks // Callbacks
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true }, 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 }, components: { LayoutRow },
}); });
</script> </script>

View file

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

View file

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

View file

@ -17,5 +17,20 @@ import App from "@/App.vue";
await initWasm(); await initWasm();
// Initialize the Vue application // 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 { export class FrontendNodeType {
readonly name!: string; readonly name!: string;
readonly category!: string;
} }
export class IndexedDbDocumentDetails extends DocumentDetails { export class IndexedDbDocumentDetails extends DocumentDetails {

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
use crate::boolean_ops::{split_path_seg, subdivide_path_seg}; use crate::boolean_ops::{split_path_seg, subdivide_path_seg};
use crate::consts::{F64LOOSE, F64PRECISE}; use crate::consts::{F64LOOSE, F64PRECISE};
use crate::layers::vector::subpath::Subpath;
use graphene_std::vector::subpath::Subpath;
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveDeriv, ParamCurveExtrema, PathSeg, Point, QuadBez, Rect, Shape, Vec2}; 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::shape_layer::ShapeLayer;
use super::style::{PathStyle, RenderData}; use super::style::{PathStyle, RenderData};
use super::text_layer::TextLayer; use super::text_layer::TextLayer;
use super::vector::subpath::Subpath;
use crate::intersection::Quad; use crate::intersection::Quad;
use crate::layers::text_layer::FontCache; use crate::layers::text_layer::FontCache;
use crate::DocumentError; use crate::DocumentError;
use crate::LayerId; use crate::LayerId;
use graphene_std::vector::subpath::Subpath;
use core::fmt; use core::fmt;
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use serde::{Deserialize, Serialize}; 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. /// Defines shared behavior for every layer type.
pub trait LayerData { pub trait LayerData {
/// Render the layer as an SVG tag to a given string. /// 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; pub mod blend_mode;
/// Contains the [FolderLayer](folder_layer::FolderLayer) type that encapsulates other layers, including more folders. /// Contains the [FolderLayer](folder_layer::FolderLayer) type that encapsulates other layers, including more folders.
pub mod folder_layer; pub mod folder_layer;
pub mod id_vec;
/// Contains the [ImageLayer](image_layer::ImageLayer) type that contains a bitmap image. /// Contains the [ImageLayer](image_layer::ImageLayer) type that contains a bitmap image.
pub mod image_layer; 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. /// 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; pub mod style;
/// Contains the [TextLayer](text_layer::TextLayer) type. /// Contains the [TextLayer](text_layer::TextLayer) type.
pub mod text_layer; pub mod text_layer;
pub mod vector;

View file

@ -119,49 +119,12 @@ impl Default for NodeGraphFrameLayer {
fn default() -> Self { fn default() -> Self {
use graph_craft::document::*; use graph_craft::document::*;
use graph_craft::proto::NodeIdentifier; 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 { Self {
mime: String::new(), mime: String::new(),
network: NodeNetwork { network: NodeNetwork {
inputs: vec![0], inputs: vec![0],
output: 3, output: 1,
nodes: [ nodes: [
( (
0, 0,
@ -174,41 +137,11 @@ impl Default for NodeGraphFrameLayer {
), ),
( (
1, 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 { DocumentNode {
name: "Output".into(), name: "Output".into(),
inputs: vec![NodeInput::Node(2)], inputs: vec![NodeInput::Node(0)],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Generic])), 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::layer_info::LayerData;
use super::style::{self, PathStyle, RenderData, ViewMode}; use super::style::{self, PathStyle, RenderData, ViewMode};
use super::vector::subpath::Subpath;
use crate::intersection::{intersect_quad_bez_path, Quad}; use crate::intersection::{intersect_quad_bez_path, Quad};
use crate::layers::text_layer::FontCache; use crate::layers::text_layer::FontCache;
use crate::LayerId; use crate::LayerId;
use graphene_std::vector::subpath::Subpath;
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Write; use std::fmt::Write;

View file

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

View file

@ -1,7 +1,7 @@
use crate::layers::vector::consts::ManipulatorType; use graphene_std::vector::consts::ManipulatorType;
use crate::layers::vector::manipulator_group::ManipulatorGroup; use graphene_std::vector::manipulator_group::ManipulatorGroup;
use crate::layers::vector::manipulator_point::ManipulatorPoint; use graphene_std::vector::manipulator_point::ManipulatorPoint;
use crate::layers::vector::subpath::Subpath; use graphene_std::vector::subpath::Subpath;
use glam::DVec2; use glam::DVec2;
use rustybuzz::{GlyphBuffer, UnicodeBuffer}; 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::imaginate_layer::{ImaginateMaskFillContent, ImaginateMaskPaintMode, ImaginateSamplingMethod, ImaginateStatus};
use crate::layers::layer_info::Layer; use crate::layers::layer_info::Layer;
use crate::layers::style::{self, Stroke}; 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 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 serde::{Deserialize, Serialize};
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};

View file

@ -14,10 +14,15 @@ documentation = "https://docs.rs/dyn-any"
[dependencies] [dependencies]
dyn-any-derive = { path = "derive", version = "0.2.0", optional = true } dyn-any-derive = { path = "derive", version = "0.2.0", optional = true }
log = { version = "0.4", optional = true } log = { version = "0.4", optional = true }
glam = { version = "0.17", optional = true }
[features] [features]
derive = ["dyn-any-derive"] derive = ["dyn-any-derive"]
log-bad-types = ["log"] 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] [package.metadata.docs.rs]
all-features = true all-features = true

View file

@ -190,13 +190,30 @@ use std::{
vec::Vec, vec::Vec,
}; };
impl_type!(Option<T>,Result<T, E>,Cell<T>,UnsafeCell<T>,RefCell<T>,MaybeUninit<T>, impl_type!(
Vec<T>, String, BTreeMap<K,V>,BTreeSet<V>, LinkedList<T>, VecDeque<T>, Option<T>, Result<T, E>, Cell<T>, UnsafeCell<T>, RefCell<T>, MaybeUninit<T>,
BinaryHeap<T>, ManuallyDrop<T>, PhantomData<T>, PhantomPinned,Empty<T>, Vec<T>, String, BTreeMap<K,V>,BTreeSet<V>, LinkedList<T>, VecDeque<T>,
Wrapping<T>, Duration, Once, Mutex<T>, RwLock<T>, bool, f32, f64, char, BinaryHeap<T>, ManuallyDrop<T>, PhantomData<T>, PhantomPinned, Empty<T>,
u8, AtomicU8, u16,AtomicU16, u32,AtomicU32, u64,AtomicU64, usize,AtomicUsize, Wrapping<T>, Duration, Once, Mutex<T>, RwLock<T>, bool, f32, f64, char,
i8,AtomicI8, i16,AtomicI16, i32,AtomicI32, i64,AtomicI64, isize,AtomicIsize, u8, AtomicU8, u16, AtomicU16, u32, AtomicU32, u64, AtomicU64, usize, AtomicUsize,
i128, u128, AtomicBool, AtomicPtr<T> 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> { impl<T: crate::StaticType + ?Sized> crate::StaticType for Box<T> {

View file

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

View file

@ -9,13 +9,14 @@ license = "MIT OR Apache-2.0"
[dependencies] [dependencies]
graphene-core = { path = "../gcore", features = ["async", "std"] } graphene-core = { path = "../gcore", features = ["async", "std"] }
graphene-std = { path = "../gstd" } 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" num-traits = "0.2"
borrow_stack = { path = "../borrow_stack" } borrow_stack = { path = "../borrow_stack" }
dyn-clone = "1.0" dyn-clone = "1.0"
rand_chacha = "0.3.1" rand_chacha = "0.3.1"
log = "0.4" log = "0.4"
serde = { version = "1", features = ["derive"], optional = true } serde = { version = "1", features = ["derive", "rc"], optional = true }
glam = { version = "0.17" }
[features] [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 { 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); let first = self.inputs.remove(0);
if let DocumentNodeImplementation::Unresolved(fqn) = self.implementation { if let DocumentNodeImplementation::Unresolved(fqn) = self.implementation {
let (input, mut args) = match first { let (input, mut args) = match first {
@ -102,6 +103,9 @@ pub enum NodeInput {
} }
impl 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) { fn map_ids(&mut self, f: impl Fn(NodeId) -> NodeId) {
if let NodeInput::Node(id) = self { if let NodeInput::Node(id) = self {
*self = NodeInput::Node(f(*id)) *self = NodeInput::Node(f(*id))

View file

@ -1,28 +1,40 @@
use dyn_any::StaticType; use dyn_any::StaticType;
use dyn_clone::DynClone;
use dyn_any::{DynAny, Upcast}; 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) /// A type that is known, allowing serialization (serde::Deserialize is not object safe)
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TaggedValue { pub enum TaggedValue {
None,
String(String), String(String),
U32(u32), U32(u32),
F32(f32), F32(f32),
F64(f64),
Bool(bool),
DVec2(DVec2),
Image(graphene_std::raster::Image), Image(graphene_std::raster::Image),
Color(graphene_core::raster::color::Color), Color(graphene_core::raster::color::Color),
Subpath(graphene_std::vector::subpath::Subpath),
RcSubpath(Arc<graphene_std::vector::subpath::Subpath>),
} }
impl TaggedValue { impl TaggedValue {
pub fn to_value(self) -> Value { pub fn to_value(self) -> Value {
match self { match self {
TaggedValue::None => Box::new(()),
TaggedValue::String(x) => Box::new(x), TaggedValue::String(x) => Box::new(x),
TaggedValue::U32(x) => Box::new(x), TaggedValue::U32(x) => Box::new(x),
TaggedValue::F32(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::Image(x) => Box::new(x),
TaggedValue::Color(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 std::borrow::Cow;
use borrow_stack::FixedSizeStack; use borrow_stack::FixedSizeStack;
use glam::DVec2;
use graphene_core::generic::FnNode; use graphene_core::generic::FnNode;
use graphene_core::ops::AddNode; use graphene_core::ops::AddNode;
use graphene_core::raster::color::Color; use graphene_core::raster::color::Color;
@ -9,6 +10,7 @@ use graphene_core::Node;
use graphene_std::any::DowncastBothNode; use graphene_std::any::DowncastBothNode;
use graphene_std::any::{Any, DowncastNode, DynAnyNode, IntoTypeErasedNode, TypeErasedNode}; use graphene_std::any::{Any, DowncastNode, DynAnyNode, IntoTypeErasedNode, TypeErasedNode};
use graphene_std::raster::Image; use graphene_std::raster::Image;
use graphene_std::vector::subpath::Subpath;
use crate::proto::Type; use crate::proto::Type;
use crate::proto::{ConstructionArgs, NodeIdentifier, ProtoNode, ProtoNodeInput, Type::Concrete}; use crate::proto::{ConstructionArgs, NodeIdentifier, ProtoNode, ProtoNodeInput, Type::Concrete};
@ -48,8 +50,8 @@ static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[
stack.push_fn(move |nodes| { 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 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 value_node = nodes.get(construction_nodes[0] as usize).unwrap();
let input_node: DowncastBothNode<_, (), f32> = DowncastBothNode::new(value_node); let input_node: DowncastBothNode<_, (), f64> = DowncastBothNode::new(value_node);
let node: DynAnyNode<_, f32, _, _> = DynAnyNode::new(ConsNode::new(input_node).then(graphene_core::ops::AddNode)); let node: DynAnyNode<_, f64, _, _> = DynAnyNode::new(ConsNode::new(input_node).then(graphene_core::ops::AddNode));
if let ProtoNodeInput::Node(node_id) = proto_node.input { if let ProtoNodeInput::Node(node_id) = proto_node.input {
let pre_node = nodes.get(node_id as usize).unwrap(); 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| { |proto_node, stack| {
stack.push_fn(move |nodes| { 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 ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("ShiftImageHSLNode Node constructed without inputs") };
let value_node = nodes.get(construction_nodes[0] as usize).unwrap();
let input_node: DowncastBothNode<_, (), f32> = DowncastBothNode::new(value_node); let hue: DowncastBothNode<_, (), f64> = DowncastBothNode::new(nodes.get(construction_nodes[0] as usize).unwrap());
let node = DynAnyNode::new(graphene_std::raster::HueShiftImage::new(input_node)); 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 { if let ProtoNodeInput::Node(node_id) = proto_node.input {
let pre_node = nodes.get(node_id as usize).unwrap(); 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| { |proto_node, stack| {
stack.push_fn(move |nodes| { 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 ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("ImageBrightnessAndContrast Node constructed without inputs") };
let value_node = nodes.get(construction_nodes[0] as usize).unwrap();
let input_node: DowncastBothNode<_, (), f32> = DowncastBothNode::new(value_node); let brightness: DowncastBothNode<_, (), f64> = DowncastBothNode::new(nodes.get(construction_nodes[0] as usize).unwrap());
let node = DynAnyNode::new(graphene_std::raster::BrightenImageNode::new(input_node)); 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 { if let ProtoNodeInput::Node(node_id) = proto_node.input {
let pre_node = nodes.get(node_id as usize).unwrap(); 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>>) { 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 = "*" image = "*"
dyn-clone = "1.0" 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] [dependencies.serde]
version = "1.0" version = "1.0"
optional = true 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; //pub mod value;
//#![feature(const_type_name)] //#![feature(const_type_name)]
@ -5,6 +9,7 @@
pub mod memo; pub mod memo;
pub mod raster; pub mod raster;
pub mod vector;
pub mod any; 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)] #[derive(Debug, Clone, Copy)]
pub struct GrayscaleImageNode; pub struct GrayscaleImageNode;
impl Node<Image> for GrayscaleImageNode { impl Node<Image> for GrayscaleImageNode {
type Output = Image; type Output = Image;
fn eval(self, mut image: Image) -> Image { fn eval(self, image: Image) -> Image {
for pixel in &mut image.data { grayscale_image(image)
let avg = (pixel.r() + pixel.g() + pixel.b()) / 3.;
*pixel = Color::from_rgbaf32_unchecked(avg, avg, avg, pixel.a());
}
image
} }
} }
impl Node<Image> for &GrayscaleImageNode { impl Node<Image> for &GrayscaleImageNode {
type Output = Image; type Output = Image;
fn eval(self, mut image: Image) -> Image { fn eval(self, image: Image) -> Image {
for pixel in &mut image.data { grayscale_image(image)
let avg = (pixel.r() + pixel.g() + pixel.b()) / 3.;
*pixel = Color::from_rgbaf32_unchecked(avg, avg, avg, pixel.a());
}
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)] #[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; type Output = Image;
fn eval(self, mut image: Image) -> Image { fn eval(self, image: Image) -> Image {
let brightness = self.0.eval(()); invert_image(image)
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
} }
} }
impl<N: Node<(), Output = f32> + Copy> Node<Image> for &BrightenImageNode<N> { impl Node<Image> for &InvertImageColorNode {
type Output = Image; type Output = Image;
fn eval(self, mut image: Image) -> Image { fn eval(self, image: Image) -> Image {
let brightness = self.0.eval(()); invert_image(image)
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
} }
} }
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 { pub fn new(node: N) -> Self {
Self(node) Self(node)
} }
} }
#[derive(Debug, Clone, Copy)] #[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; type Output = Image;
fn eval(self, mut image: Image) -> Image { fn eval(self, image: Image) -> Image {
let hue_shift = self.0.eval(()); image_opacity(image, self.0.eval(()) as f32)
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
} }
} }
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; type Output = Image;
fn eval(self, mut image: Image) -> Image { fn eval(self, image: Image) -> Image {
let hue_shift = self.0.eval(()); image_opacity(image, self.0.eval(()) as f32)
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
} }
} }
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 { pub fn new(node: N) -> Self {
Self(node) 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> { 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 /// Push a new element to the start of the vector
pub fn push_front(&mut self, element: T) -> Option<ElementId> { pub fn push_front(&mut self, element: T) -> Option<ElementId> {
self.next_id += 1; self.next_id += 1;
@ -135,11 +144,7 @@ impl<T> IdBackedVec<T> {
impl<T> Default for IdBackedVec<T> { impl<T> Default for IdBackedVec<T> {
fn default() -> Self { fn default() -> Self {
IdBackedVec { Self::new()
elements: vec![],
element_ids: vec![],
next_id: 0,
}
} }
} }

View file

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

View file

@ -1,9 +1,9 @@
use super::consts::ManipulatorType; use super::consts::ManipulatorType;
use super::id_vec::IdBackedVec;
use super::manipulator_group::ManipulatorGroup; use super::manipulator_group::ManipulatorGroup;
use super::manipulator_point::ManipulatorPoint; 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 glam::{DAffine2, DVec2};
use kurbo::{BezPath, PathEl, Shape}; use kurbo::{BezPath, PathEl, Shape};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -11,15 +11,15 @@ use serde::{Deserialize, Serialize};
/// [Subpath] represents a single vector path, containing many [ManipulatorGroups]. /// [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. /// 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 // 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>); pub struct Subpath(IdBackedVec<ManipulatorGroup>);
impl Subpath { impl Subpath {
// ** INITIALIZATION ** // ** INITIALIZATION **
/// Create a new [Subpath] with no [ManipulatorGroup]s. /// Create a new [Subpath] with no [ManipulatorGroup]s.
pub fn new() -> Self { pub const fn new() -> Self {
Subpath { ..Default::default() } Subpath(IdBackedVec::new())
} }
/// Construct a [Subpath] from a point iterator /// 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. /// A wrapper around [`bezier_rs::Bezier`] containing also the IDs for the [`ManipulatorGroup`]s where the points are from.
pub struct BezierId { pub struct BezierId {
/// The internal [`bezier_rs::Bezier`]. /// The internal [`bezier_rs::Bezier`].