Node graph improvements (#855)

* Selecting multiple nodes

* Improve logs

* Log bad types in dyn any

* Add (broken) node links

* New topological sort

* Fix reorder ids function

* Input and output node

* Add nodes that operate on images

* Fixups

* Show node parameters together with layer properties

* New nodes don't crash editor

* Fix tests

* Node positions backend

* Generate node graph on value change

* Add expose input message

* Fix tests

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube 2022-11-17 23:36:23 +00:00 committed by Keavon Chambers
parent bbe98d3fe3
commit 2994afa6b8
23 changed files with 734 additions and 331 deletions

2
Cargo.lock generated
View file

@ -223,6 +223,7 @@ name = "dyn-any"
version = "0.2.1"
dependencies = [
"dyn-any-derive",
"log",
]
[[package]]
@ -398,6 +399,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"serde",
"syn",
]

View file

@ -1,35 +1,35 @@
use crate::messages::prelude::*;
use graph_craft::document::{value::TaggedValue, NodeId};
use graph_craft::proto::NodeIdentifier;
#[remain::sorted]
#[impl_message(Message, DocumentMessage, NodeGraph)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum NodeGraphMessage {
// Messages
AddLink {
from: NodeId,
to: NodeId,
to_index: usize,
},
CloseNodeGraph,
ConnectNodesByLink {
output_node: u64,
input_node: u64,
input_node_connector_index: u32,
input_node_connector_index: usize,
},
CreateNode {
// Having the caller generate the id means that we don't have to return it. This can be a random u64.
node_id: NodeId,
// I don't really know what this is for (perhaps a user identifiable name).
name: String,
// The node identifier must mach that found in `node-graph/graph-craft/src/node_registry.rs` e.g. "graphene_core::raster::GrayscaleNode
identifier: NodeIdentifier,
num_inputs: u32,
node_type: String,
},
DeleteNode {
node_id: NodeId,
},
ExposeInput {
node_id: NodeId,
input_index: usize,
new_exposed: bool,
},
MoveSelectedNodes {
displacement_x: i32,
displacement_y: i32,
},
OpenNodeGraph {
layer_path: Vec<graphene::LayerId>,
},

View file

@ -1,18 +1,27 @@
use crate::messages::layout::utility_types::layout_widget::{LayoutGroup, Widget, WidgetCallback, WidgetHolder};
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::layout::utility_types::layout_widget::LayoutGroup;
use crate::messages::prelude::*;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork};
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, DocumentNodeMetadata, NodeInput, NodeNetwork};
use graphene::document::Document;
use graphene::layers::layer_info::LayerDataType;
use graphene::layers::nodegraph_layer::NodeGraphFrameLayer;
mod document_node_types;
mod node_properties;
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum DataType {
Raster,
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FrontendNode {
pub id: graph_craft::document::NodeId,
#[serde(rename = "displayName")]
pub display_name: String,
#[serde(rename = "exposedInputs")]
pub exposed_inputs: Vec<DataType>,
pub outputs: Vec<DataType>,
pub position: (i32, i32),
}
// (link_start, link_end, link_end_input_index)
@ -55,111 +64,11 @@ impl NodeGraphMessageHandler {
let network = &node_graph_frame.network;
let mut section = Vec::new();
for node_id in &self.selected_nodes {
let node = *node_id;
let Some(document_node) = network.nodes.get(node_id) else {
continue;
};
let name = format!("Node {} Properties", document_node.name);
let layout = match &document_node.implementation {
DocumentNodeImplementation::Network(_) => match document_node.name.as_str() {
"Hue Shift Color" => vec![LayoutGroup::Row {
widgets: vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Shift degrees".into(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some({
let NodeInput::Value (TaggedValue::F32(x)) = document_node.inputs[1] else {
panic!("Hue rotate should be f32")
};
x as f64
}),
unit: "°".into(),
mode: NumberInputMode::Range,
range_min: Some(-180.),
range_max: Some(180.),
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
NodeGraphMessage::SetInputValue {
node,
input_index: 1,
value: TaggedValue::F32(number_input.value.unwrap() as f32),
}
.into()
}),
..NumberInput::default()
})),
],
}],
"Brighten Color" => vec![LayoutGroup::Row {
widgets: vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Brighten Amount".into(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some({
let NodeInput::Value (TaggedValue::F32(x)) = document_node.inputs[1] else {
panic!("Brighten amount should be f32")
};
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,
input_index: 1,
value: TaggedValue::F32(number_input.value.unwrap() as f32),
}
.into()
}),
..NumberInput::default()
})),
],
}],
_ => vec![LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: format!("Cannot currently display properties for network {}", document_node.name),
..Default::default()
}))],
}],
},
DocumentNodeImplementation::Unresolved(identifier) => match identifier.name.as_ref() {
"graphene_std::raster::MapImageNode" | "graphene_core::ops::IdNode" => vec![LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: format!("{} requires no properties", document_node.name),
..Default::default()
}))],
}],
unknown => {
vec![
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: format!("TODO: {} properties", unknown),
..Default::default()
}))],
},
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Add in editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs".to_string(),
..Default::default()
}))],
},
]
}
},
};
section.push(LayoutGroup::Section { name, layout });
section.push(node_properties::generate_node_properties(document_node, *node_id));
}
section
@ -173,7 +82,7 @@ impl NodeGraphMessageHandler {
let links = network
.nodes
.iter()
.flat_map(|(link_end, node)| node.inputs.iter().enumerate().map(move |(index, input)| (input, link_end, index)))
.flat_map(|(link_end, node)| node.inputs.iter().filter(|input| input.is_exposed()).enumerate().map(move |(index, input)| (input, link_end, index)))
.filter_map(|(input, &link_end, link_end_input_index)| {
if let NodeInput::Node(link_start) = *input {
Some(FrontendNodeLink {
@ -192,6 +101,9 @@ impl NodeGraphMessageHandler {
nodes.push(FrontendNode {
id: *id,
display_name: node.name.clone(),
exposed_inputs: node.inputs.iter().filter(|input| input.is_exposed()).map(|_| DataType::Raster).collect(),
outputs: vec![DataType::Raster],
position: node.metadata.position,
})
}
log::debug!("Nodes:\n{:#?}\n\nFrontend Nodes:\n{:#?}\n\nLinks:\n{:#?}", network.nodes, nodes, links);
@ -204,19 +116,6 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
fn process_message(&mut self, message: NodeGraphMessage, (document, _ipp): (&mut Document, &InputPreprocessorMessageHandler), responses: &mut VecDeque<Message>) {
#[remain::sorted]
match message {
NodeGraphMessage::AddLink { from, to, to_index } => {
log::debug!("Connect primary output from node {from} to input of index {to_index} on node {to}.");
if let Some(network) = self.get_active_network_mut(document) {
if let Some(to) = network.nodes.get_mut(&to) {
// Extend number of inputs if not already large enough
if to_index >= to.inputs.len() {
to.inputs.extend(((to.inputs.len() - 1)..to_index).map(|_| NodeInput::Network));
}
to.inputs[to_index] = NodeInput::Node(from);
}
}
}
NodeGraphMessage::CloseNodeGraph => {
if let Some(_old_layer_path) = self.layer_path.take() {
info!("Closing node graph");
@ -231,45 +130,103 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
input_node_connector_index,
} => {
log::debug!("Connect primary output from node {output_node} to input of index {input_node_connector_index} on node {input_node}.");
let Some(network) = self.get_active_network_mut(document) else {
error!("No network");
return;
};
let Some(input_node) = network.nodes.get_mut(&input_node) else {
error!("No to");
return;
};
let Some((actual_index, _)) = input_node.inputs.iter().enumerate().filter(|input|input.1.is_exposed()).nth(input_node_connector_index) else {
error!("Failed to find actual index of connector indes {input_node_connector_index} on node {input_node:#?}");
return;
};
input_node.inputs[actual_index] = NodeInput::Node(output_node);
info!("Inputs: {:?}", input_node.inputs);
Self::send_graph(network, responses);
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
}
NodeGraphMessage::CreateNode {
node_id,
name,
identifier,
num_inputs,
} => {
if let Some(network) = self.get_active_network_mut(document) {
let inner_network = NodeNetwork {
inputs: (0..num_inputs).map(|_| 0).collect(),
output: 0,
nodes: [(
node_id,
DocumentNode {
name: format!("{}_impl", name),
// TODO: Allow inserting nodes that contain other nodes.
implementation: DocumentNodeImplementation::Unresolved(identifier),
inputs: (0..num_inputs).map(|_| NodeInput::Network).collect(),
},
)]
.into_iter()
.collect(),
};
network.nodes.insert(
node_id,
NodeGraphMessage::CreateNode { node_id, node_type } => {
let Some(network) = self.get_active_network_mut(document) else{
warn!("No network");
return;
};
let Some(document_node_type) = document_node_types::resolve_document_node_type(&node_type) else{
responses.push_back(DialogMessage::DisplayDialogError { title: "Cannot insert node".to_string(), description: format!("The document node '{node_type}' does not exist in the document node list") }.into());
return;
};
let num_inputs = document_node_type.default_inputs.len();
let inner_network = NodeNetwork {
inputs: (0..num_inputs).map(|_| 0).collect(),
output: 0,
nodes: [(
0,
DocumentNode {
name,
inputs: (0..num_inputs).map(|_| NodeInput::Network).collect(),
name: format!("{}_impl", document_node_type.name),
// TODO: Allow inserting nodes that contain other nodes.
implementation: DocumentNodeImplementation::Network(inner_network),
implementation: DocumentNodeImplementation::Unresolved(document_node_type.identifier.clone()),
inputs: (0..num_inputs).map(|_| NodeInput::Network).collect(),
metadata: DocumentNodeMetadata::default(),
},
);
Self::send_graph(network, responses);
}
)]
.into_iter()
.collect(),
};
network.nodes.insert(
node_id,
DocumentNode {
name: node_type.clone(),
inputs: document_node_type.default_inputs.to_vec(),
// TODO: Allow inserting nodes that contain other nodes.
implementation: DocumentNodeImplementation::Network(inner_network),
metadata: graph_craft::document::DocumentNodeMetadata {
// TODO: Better position default
position: (node_id as i32 * 7 - 41, node_id as i32 * 2 - 10),
},
},
);
Self::send_graph(network, responses);
}
NodeGraphMessage::DeleteNode { node_id } => {
if let Some(network) = self.get_active_network_mut(document) {
network.nodes.remove(&node_id);
// TODO: Update UI if it is not already updated.
Self::send_graph(network, responses);
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
}
}
NodeGraphMessage::ExposeInput { node_id, input_index, new_exposed } => {
let Some(network) = self.get_active_network_mut(document) else{
warn!("No network");
return;
};
let Some(node) = network.nodes.get_mut(&node_id) else {
warn!("No node");
return;
};
if let NodeInput::Value { exposed, .. } = &mut node.inputs[input_index] {
*exposed = new_exposed;
}
Self::send_graph(network, responses);
}
NodeGraphMessage::MoveSelectedNodes { displacement_x, displacement_y } => {
let Some(network) = self.get_active_network_mut(document) else{
warn!("No network");
return;
};
for node_id in &self.selected_nodes {
if let Some(node) = network.nodes.get_mut(node_id) {
node.metadata.position.0 += displacement_x;
node.metadata.position.1 += displacement_y;
}
}
}
NodeGraphMessage::OpenNodeGraph { layer_path } => {
@ -283,20 +240,8 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
Self::send_graph(network, responses);
// TODO: Dynamic node library
responses.push_back(
FrontendMessage::UpdateNodeTypes {
node_types: vec![
FrontendNodeType::new("Identity"),
FrontendNodeType::new("Grayscale Color"),
FrontendNodeType::new("Brighten Color"),
FrontendNodeType::new("Hue Shift Color"),
FrontendNodeType::new("Add"),
FrontendNodeType::new("Map Image"),
],
}
.into(),
);
let node_types = document_node_types::collect_node_types();
responses.push_back(FrontendMessage::UpdateNodeTypes { node_types }.into());
}
}
NodeGraphMessage::SelectNodes { nodes } => {
@ -310,7 +255,8 @@ impl MessageHandler<NodeGraphMessage, (&mut Document, &InputPreprocessorMessageH
if input_index >= node.inputs.len() {
node.inputs.extend(((node.inputs.len() - 1)..input_index).map(|_| NodeInput::Network));
}
node.inputs[input_index] = NodeInput::Value(value);
node.inputs[input_index] = NodeInput::Value { tagged_value: value, exposed: false };
responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into());
}
}
}

View file

@ -0,0 +1,93 @@
use std::borrow::Cow;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::NodeInput;
use graph_craft::proto::{NodeIdentifier, Type};
use graphene_std::raster::Image;
use super::FrontendNodeType;
pub struct DocumentNodeType {
pub name: &'static str,
pub identifier: NodeIdentifier,
pub default_inputs: &'static [NodeInput],
}
// TODO: Dynamic node library
static DOCUMENT_NODE_TYPES: [DocumentNodeType; 5] = [
DocumentNodeType {
name: "Identity",
identifier: NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Concrete(Cow::Borrowed("Any<'_>"))]),
default_inputs: &[NodeInput::Node(0)],
},
DocumentNodeType {
name: "Grayscale Image",
identifier: NodeIdentifier::new("graphene_std::raster::GrayscaleImageNode", &[]),
default_inputs: &[NodeInput::Value {
tagged_value: TaggedValue::Image(Image {
width: 0,
height: 0,
data: Vec::new(),
}),
exposed: true,
}],
},
DocumentNodeType {
name: "Brighten Image",
identifier: NodeIdentifier::new("graphene_std::raster::BrightenImageNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
default_inputs: &[
NodeInput::Value {
tagged_value: TaggedValue::Image(Image {
width: 0,
height: 0,
data: Vec::new(),
}),
exposed: true,
},
NodeInput::Value {
tagged_value: TaggedValue::F32(10.),
exposed: false,
},
],
},
DocumentNodeType {
name: "Hue Shift Image",
identifier: NodeIdentifier::new("graphene_std::raster::HueShiftImage", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
default_inputs: &[
NodeInput::Value {
tagged_value: TaggedValue::Image(Image {
width: 0,
height: 0,
data: Vec::new(),
}),
exposed: true,
},
NodeInput::Value {
tagged_value: TaggedValue::F32(50.),
exposed: false,
},
],
},
DocumentNodeType {
name: "Add",
identifier: NodeIdentifier::new("graphene_core::ops::AddNode", &[Type::Concrete(Cow::Borrowed("u32")), Type::Concrete(Cow::Borrowed("u32"))]),
default_inputs: &[
NodeInput::Value {
tagged_value: TaggedValue::U32(0),
exposed: false,
},
NodeInput::Value {
tagged_value: TaggedValue::U32(0),
exposed: false,
},
],
},
];
pub fn resolve_document_node_type(name: &str) -> Option<&DocumentNodeType> {
DOCUMENT_NODE_TYPES.iter().find(|node| node.name == name)
}
pub fn collect_node_types() -> Vec<FrontendNodeType> {
DOCUMENT_NODE_TYPES.iter().map(|node_type| FrontendNodeType { name: node_type.name.to_string() }).collect()
}

View file

@ -0,0 +1,111 @@
use crate::messages::layout::utility_types::layout_widget::{LayoutGroup, Widget, WidgetCallback, WidgetHolder};
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 graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
pub fn generate_node_properties(document_node: &DocumentNode, node_id: NodeId) -> LayoutGroup {
let name = document_node.name.clone();
let layout = match &document_node.implementation {
DocumentNodeImplementation::Network(_) => match document_node.name.as_str() {
"Hue Shift Image" => vec![LayoutGroup::Row {
widgets: vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Shift Degrees".into(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some({
let NodeInput::Value {tagged_value: TaggedValue::F32(x), ..} = document_node.inputs[1] else {
panic!("Hue rotate should be f32")
};
x as f64
}),
unit: "°".into(),
mode: NumberInputMode::Range,
range_min: Some(-180.),
range_max: Some(180.),
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
NodeGraphMessage::SetInputValue {
node: node_id,
input_index: 1,
value: TaggedValue::F32(number_input.value.unwrap() as f32),
}
.into()
}),
..NumberInput::default()
})),
],
}],
"Brighten Image" => vec![LayoutGroup::Row {
widgets: vec![
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Brighten Amount".into(),
..Default::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
separator_type: SeparatorType::Unrelated,
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::NumberInput(NumberInput {
value: Some({
let NodeInput::Value {tagged_value: TaggedValue::F32(x), ..} = document_node.inputs[1] else {
panic!("Brighten amount should be f32")
};
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![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: format!("Cannot currently display parameters for network {}", document_node.name),
..Default::default()
}))],
}],
},
DocumentNodeImplementation::Unresolved(identifier) => match identifier.name.as_ref() {
"graphene_std::raster::MapImageNode" | "graphene_core::ops::IdNode" => vec![LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: format!("{} exposes no parameters", document_node.name),
..Default::default()
}))],
}],
unknown => {
vec![
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: format!("TODO: {} parameters", unknown),
..Default::default()
}))],
},
LayoutGroup::Row {
widgets: vec![WidgetHolder::new(Widget::TextLabel(TextLabel {
value: "Add in editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs".to_string(),
..Default::default()
}))],
},
]
}
},
};
LayoutGroup::Section { name, layout }
}

View file

@ -269,7 +269,10 @@ pub fn register_artwork_layer_properties(
direction: SeparatorDirection::Horizontal,
})),
WidgetHolder::new(Widget::TextLabel(TextLabel {
value: LayerDataTypeDiscriminant::from(&layer.data).to_string(),
value: match &layer.data {
LayerDataType::NodeGraphFrame(_) => "Node Graph Frame".into(),
other => LayerDataTypeDiscriminant::from(other).to_string(),
},
..TextLabel::default()
})),
WidgetHolder::new(Widget::Separator(Separator {
@ -322,14 +325,16 @@ pub fn register_artwork_layer_properties(
LayerDataType::NodeGraphFrame(node_graph_frame) => {
let is_graph_open = node_graph_message_handler.layer_path.as_ref().filter(|node_graph| *node_graph == &layer_path).is_some();
let selected_nodes = &node_graph_message_handler.selected_nodes;
let mut properties_sections = vec![
node_section_transform(layer, persistent_data),
node_section_node_graph_frame(layer_path, node_graph_frame, is_graph_open),
];
if !selected_nodes.is_empty() && is_graph_open {
node_graph_message_handler.collate_properties(&node_graph_frame)
} else {
vec![
node_section_transform(layer, persistent_data),
node_section_node_graph_frame(layer_path, node_graph_frame, is_graph_open),
]
let parameters_sections = node_graph_message_handler.collate_properties(node_graph_frame);
properties_sections.extend(parameters_sections.into_iter());
}
properties_sections
}
LayerDataType::Folder(_) => {
vec![node_section_transform(layer, persistent_data)]

View file

@ -437,7 +437,7 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
// Execute the node graph
let mut network = node_graph_frame.network.clone();
info!("Network {network:?}");
info!("Executing network {network:#?}");
let stack = borrow_stack::FixedSizeStack::new(256);
for node_id in node_graph_frame.network.nodes.keys() {
@ -447,14 +447,18 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
let mut proto_network = network.into_proto_network();
proto_network.reorder_ids();
info!("proto_network with reordered ids: {proto_network:#?}");
assert_ne!(proto_network.nodes.len(), 0, "No protonodes exist?");
for (_id, node) in proto_network.nodes {
info!("Node {:?}", node);
info!("Inserting proto node {:?}", node);
graph_craft::node_registry::push_node(node, &stack);
}
use borrow_stack::BorrowStack;
use dyn_any::IntoDynAny;
use graphene_core::Node;
let result = unsafe { stack.get().last().unwrap().eval(image.into_dyn()) };
let result = *dyn_any::downcast::<Image>(result).unwrap();
@ -491,7 +495,7 @@ impl MessageHandler<PortfolioMessage, (&InputPreprocessorMessageHandler, &Prefer
}
Err(description) => responses.push_back(
DialogMessage::DisplayDialogError {
title: "Failed to update image".to_string(),
title: "Failed to update node graph".to_string(),
description,
}
.into(),

View file

@ -35,8 +35,8 @@
class="node"
:class="{ selected: selected.includes(node.id) }"
:style="{
'--offset-left': 8 + Number(node.id < 9n ? node.id : node.id - 9n) * 7,
'--offset-top': 4 + Number(node.id < 9n ? node.id : node.id - 9n) * 2,
'--offset-left': node.position?.x || 0,
'--offset-top': node.position?.y || 0,
'--data-color': 'var(--color-data-raster)',
'--data-color-dim': 'var(--color-data-raster-dim)',
}"
@ -438,11 +438,17 @@ export default defineComponent({
const nodeId = node?.getAttribute("data-node") || undefined;
if (nodeId) {
const id = BigInt(nodeId);
this.editor.instance.selectNodes(new BigUint64Array([id]));
this.selected = [id];
if (e.shiftKey || e.ctrlKey) {
if (this.selected.includes(id)) this.selected.splice(this.selected.lastIndexOf(id), 1);
else this.selected.push(id);
} else {
this.selected = [id];
}
this.editor.instance.selectNodes(new BigUint64Array(this.selected));
} else {
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;
graphDiv?.setPointerCapture(e.pointerId);
@ -483,9 +489,9 @@ export default defineComponent({
const inputNodeConnectionIndex = inputNodeConnectionIndexSearch > -1 ? inputNodeConnectionIndexSearch : undefined;
if (inputNodeConnectionIndex !== undefined) {
const oneBasedIndex = inputNodeConnectionIndex + 1;
// const oneBasedIndex = inputNodeConnectionIndex + 1;
this.editor.instance.connectNodesByLink(BigInt(outputConnectedNodeID), BigInt(inputConnectedNodeID), oneBasedIndex);
this.editor.instance.connectNodesByLink(BigInt(outputConnectedNodeID), BigInt(inputConnectedNodeID), inputNodeConnectionIndex);
}
}
}

View file

@ -88,11 +88,11 @@
.widget-row {
&:first-child {
margin-top: -1px;
margin-top: calc(4px - 1px);
}
&:last-child {
margin-bottom: -1px;
margin-bottom: calc(4px - 1px);
}
> .text-label:first-of-type {

View file

@ -12,6 +12,11 @@ export class JsMessage {
static readonly jsMessageMarker = true;
}
const TupleToVec2 = Transform(({ value }: { value: [number, number] | undefined }) => (value === undefined ? undefined : { x: value[0], y: value[1] }));
const BigIntTupleToVec2 = Transform(({ value }: { value: [bigint, bigint] | undefined }) => (value === undefined ? undefined : { x: Number(value[0]), y: Number(value[1]) }));
export type XY = { x: number; y: number };
// ============================================================================
// Add additional classes below to replicate Rust's `FrontendMessage`s and data structures.
//
@ -64,10 +69,19 @@ export class FrontendDocumentDetails extends DocumentDetails {
readonly id!: bigint;
}
export type DataType = "Raster" | "Color" | "Image" | "F32";
export class FrontendNode {
readonly id!: bigint;
readonly displayName!: string;
readonly exposedInputs!: DataType[];
readonly outputs!: DataType[];
@TupleToVec2
readonly position!: XY | undefined;
}
export class FrontendNodeLink {
@ -403,11 +417,6 @@ export class UpdateDocumentArtboards extends JsMessage {
readonly svg!: string;
}
const TupleToVec2 = Transform(({ value }: { value: [number, number] | undefined }) => (value === undefined ? undefined : { x: value[0], y: value[1] }));
const BigIntTupleToVec2 = Transform(({ value }: { value: [bigint, bigint] | undefined }) => (value === undefined ? undefined : { x: Number(value[0]), y: Number(value[1]) }));
export type XY = { x: number; y: number };
export class UpdateDocumentScrollbars extends JsMessage {
@TupleToVec2
readonly position!: XY;

View file

@ -541,7 +541,7 @@ impl JsEditorHandle {
/// Notifies the backend that the user connected a node's primary output to one of another node's inputs
#[wasm_bindgen(js_name = connectNodesByLink)]
pub fn connect_nodes_by_link(&self, output_node: u64, input_node: u64, input_node_connector_index: u32) {
pub fn connect_nodes_by_link(&self, output_node: u64, input_node: u64, input_node_connector_index: usize) {
let message = NodeGraphMessage::ConnectNodesByLink {
output_node,
input_node,
@ -553,9 +553,6 @@ impl JsEditorHandle {
/// Creates a new document node in the node graph
#[wasm_bindgen(js_name = createNode)]
pub fn create_node(&self, node_type: String) {
use graph_craft::proto::{NodeIdentifier, Type};
use std::borrow::Cow;
fn generate_node_id() -> u64 {
static mut NODE_ID: u64 = 10;
unsafe {
@ -564,24 +561,9 @@ impl JsEditorHandle {
}
}
let (ident, args) = match node_type.as_str() {
"Identity" => (NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Concrete(Cow::Borrowed("Any<'_>"))]), 1),
"Grayscale Color" => (NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Concrete(Cow::Borrowed("Any<'_>"))]), 1),
"Brighten Color" => (NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Concrete(Cow::Borrowed("Any<'_>"))]), 1),
"Hue Shift Color" => (NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Concrete(Cow::Borrowed("Any<'_>"))]), 1),
"Add" => (
NodeIdentifier::new("graphene_core::ops::AddNode", &[Type::Concrete(Cow::Borrowed("u32")), Type::Concrete(Cow::Borrowed("u32"))]),
2,
),
"Map Image" => (NodeIdentifier::new("graphene_std::raster::MapImageNode", &[]), 2),
_ => panic!("Invalid node type: {}", node_type),
};
let message = NodeGraphMessage::CreateNode {
node_id: generate_node_id(),
name: node_type,
identifier: ident,
num_inputs: args,
node_type,
};
self.dispatch(message);
}
@ -593,6 +575,13 @@ impl JsEditorHandle {
self.dispatch(message);
}
/// Notifies the backend that the selected nodes have been moved
#[wasm_bindgen(js_name = moveSelectedNodes)]
pub fn move_selected_nodes(&self, displacement_x: i32, displacement_y: i32) {
let message = NodeGraphMessage::MoveSelectedNodes { displacement_x, displacement_y };
self.dispatch(message);
}
/// Pastes an image
#[wasm_bindgen(js_name = pasteImage)]
pub fn paste_image(&self, mime: String, image_data: Vec<u8>, mouse_x: Option<f64>, mouse_y: Option<f64>) {

View file

@ -6,6 +6,7 @@ use crate::layers::text_layer::FontCache;
use crate::LayerId;
use glam::{DAffine2, DMat2, DVec2};
use graph_craft::proto::Type;
use kurbo::{Affine, BezPath, Shape as KurboShape};
use serde::{Deserialize, Serialize};
use std::fmt::Write;
@ -124,12 +125,13 @@ impl Default for NodeGraphFrameLayer {
nodes: [(
0,
DocumentNode {
name: "brighten".into(),
name: "Brighten Image Node".into(),
inputs: vec![NodeInput::Network, NodeInput::Network],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new(
"graphene_core::raster::BrightenColorNode",
"graphene_std::raster::BrightenImageNode",
&[graph_craft::proto::Type::Concrete(std::borrow::Cow::Borrowed("&TypeErasedNode"))],
)),
metadata: DocumentNodeMetadata::default(),
},
)]
.into_iter()
@ -142,12 +144,13 @@ impl Default for NodeGraphFrameLayer {
nodes: [(
0,
DocumentNode {
name: "hue shift".into(),
name: "Hue Shift Image Node".into(),
inputs: vec![NodeInput::Network, NodeInput::Network],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new(
"graphene_core::raster::HueShiftNode",
"graphene_std::raster::HueShiftImage",
&[graph_craft::proto::Type::Concrete(std::borrow::Cow::Borrowed("&TypeErasedNode"))],
)),
metadata: DocumentNodeMetadata::default(),
},
)]
.into_iter()
@ -157,31 +160,55 @@ impl Default for NodeGraphFrameLayer {
Self {
mime: String::new(),
network: NodeNetwork {
inputs: vec![2, 1],
output: 2,
inputs: vec![0],
output: 3,
nodes: [
(
0,
DocumentNode {
name: "Hue Shift Color".into(),
inputs: vec![NodeInput::Network, NodeInput::Value(value::TaggedValue::F32(50.))],
implementation: DocumentNodeImplementation::Network(hue_shift_network),
name: "Input".into(),
inputs: vec![NodeInput::Network],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Generic])),
metadata: DocumentNodeMetadata { position: (8, 4) },
},
),
(
1,
DocumentNode {
name: "Brighten Color".into(),
inputs: vec![NodeInput::Node(0), NodeInput::Value(value::TaggedValue::F32(10.))],
implementation: DocumentNodeImplementation::Network(brighten_network),
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: "Map Image".into(),
inputs: vec![NodeInput::Network, NodeInput::Node(1)],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_std::raster::MapImageNode", &[])),
name: "Brighten Image".into(),
inputs: vec![
NodeInput::Node(1),
NodeInput::Value {
tagged_value: value::TaggedValue::F32(10.),
exposed: false,
},
],
implementation: DocumentNodeImplementation::Network(brighten_network),
metadata: DocumentNodeMetadata { position: (8 + 7 * 2, 4 + 2 * 2) },
},
),
(
3,
DocumentNode {
name: "Output".into(),
inputs: vec![NodeInput::Node(2)],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Generic])),
metadata: DocumentNodeMetadata { position: (8 + 7 * 3, 4) },
},
),
]

View file

@ -13,9 +13,11 @@ documentation = "https://docs.rs/dyn-any"
[dependencies]
dyn-any-derive = { path = "derive", version = "0.2.0", optional = true }
log = { version = "0.4", optional = true }
[features]
derive = ["dyn-any-derive"]
log-bad-types = ["log"]
[package.metadata.docs.rs]
all-features = true

View file

@ -50,12 +50,18 @@ impl<'a, T: DynAny<'a> + 'a> UpcastFrom<T> for dyn DynAny<'a> + 'a {
pub trait DynAny<'a> {
fn type_id(&self) -> TypeId;
#[cfg(feature = "log-bad-types")]
fn type_name(&self) -> &'static str;
}
impl<'a, T: StaticType> DynAny<'a> for T {
fn type_id(&self) -> std::any::TypeId {
std::any::TypeId::of::<T::Static>()
}
#[cfg(feature = "log-bad-types")]
fn type_name(&self) -> &'static str {
std::any::type_name::<T>()
}
}
pub fn downcast_ref<'a, V: StaticType>(i: &'a dyn DynAny<'a>) -> Option<&'a V> {
if i.type_id() == std::any::TypeId::of::<<V as StaticType>::Static>() {
@ -74,7 +80,11 @@ pub fn downcast<'a, V: StaticType>(i: Box<dyn DynAny<'a> + 'a>) -> Option<Box<V>
let ptr = Box::into_raw(i) as *mut dyn DynAny<'a> as *mut V;
Some(unsafe { Box::from_raw(ptr) })
} else {
// TODO: add log error
#[cfg(feature = "log-bad-types")]
{
log::error!("Tried to downcast a {} to a {}", DynAny::type_name(i.as_ref()), core::any::type_name::<V>());
}
if type_id == std::any::TypeId::of::<&dyn DynAny<'static>>() {
panic!("downcast error: type_id == std::any::TypeId::of::<dyn DynAny<'a>>()");
}
@ -192,9 +202,12 @@ impl_type!(Option<T>,Result<T, E>,Cell<T>,UnsafeCell<T>,RefCell<T>,MaybeUninit<T
impl<T: crate::StaticType + ?Sized> crate::StaticType for Box<T> {
type Static = Box<<T as crate::StaticType>::Static>;
}
fn test() {
let foo = (Box::new(&1 as &dyn DynAny<'static>), Box::new(&2 as &dyn DynAny<'static>));
//let bar = &foo as &dyn DynAny<'static>;
#[test]
fn test_tuple_of_boxes() {
let tuple = (Box::new(&1 as &dyn DynAny<'static>), Box::new(&2 as &dyn DynAny<'static>));
let dyn_any = &tuple as &dyn DynAny;
assert_eq!(&1, downcast_ref(*downcast_ref::<(Box<&dyn DynAny>, Box<&dyn DynAny>)>(dyn_any).unwrap().0).unwrap());
assert_eq!(&2, downcast_ref(*downcast_ref::<(Box<&dyn DynAny>, Box<&dyn DynAny>)>(dyn_any).unwrap().1).unwrap());
}
macro_rules! impl_tuple {

View file

@ -5,16 +5,16 @@ use self::color::Color;
pub mod color;
#[derive(Debug, Clone, Copy)]
pub struct GrayscaleNode;
pub struct GrayscaleColorNode;
impl Node<Color> for GrayscaleNode {
impl Node<Color> for GrayscaleColorNode {
type Output = Color;
fn eval(self, color: Color) -> Color {
let avg = (color.r() + color.g() + color.b()) / 3.0;
Color::from_rgbaf32_unchecked(avg, avg, avg, color.a())
}
}
impl<'n> Node<Color> for &'n GrayscaleNode {
impl<'n> Node<Color> for &'n GrayscaleColorNode {
type Output = Color;
fn eval(self, color: Color) -> Color {
let avg = (color.r() + color.g() + color.b()) / 3.0;
@ -49,9 +49,9 @@ impl<N: Node<(), Output = f32> + Copy> BrightenColorNode<N> {
}
#[derive(Debug, Clone, Copy)]
pub struct HueShiftNode<N: Node<(), Output = f32>>(N);
pub struct HueShiftColorNode<N: Node<(), Output = f32>>(N);
impl<N: Node<(), Output = f32>> Node<Color> for HueShiftNode<N> {
impl<N: Node<(), Output = f32>> Node<Color> for HueShiftColorNode<N> {
type Output = Color;
fn eval(self, color: Color) -> Color {
let hue_shift = self.0.eval(());
@ -59,7 +59,7 @@ impl<N: Node<(), Output = f32>> Node<Color> for HueShiftNode<N> {
Color::from_hsla(hue + hue_shift / 360., saturation, luminance, alpha)
}
}
impl<N: Node<(), Output = f32> + Copy> Node<Color> for &HueShiftNode<N> {
impl<N: Node<(), Output = f32> + Copy> Node<Color> for &HueShiftColorNode<N> {
type Output = Color;
fn eval(self, color: Color) -> Color {
let hue_shift = self.0.eval(());
@ -68,7 +68,7 @@ impl<N: Node<(), Output = f32> + Copy> Node<Color> for &HueShiftNode<N> {
}
}
impl<N: Node<(), Output = f32> + Copy> HueShiftNode<N> {
impl<N: Node<(), Output = f32> + Copy> HueShiftColorNode<N> {
pub fn new(node: N) -> Self {
Self(node)
}
@ -105,7 +105,7 @@ mod test {
#[test]
fn map_node() {
// let array = &mut [Color::from_rgbaf32(1.0, 0.0, 0.0, 1.0).unwrap()];
(&GrayscaleNode).eval(Color::from_rgbf32_unchecked(1., 0., 0.));
(&GrayscaleColorNode).eval(Color::from_rgbf32_unchecked(1., 0., 0.));
/*let map = ForEachNode(MutWrapper(GrayscaleNode));
(&map).eval(array.iter_mut());
assert_eq!(array[0], Color::from_rgbaf32(0.33333334, 0.33333334, 0.33333334, 1.0).unwrap());*/

View file

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

View file

@ -28,12 +28,19 @@ fn merge_ids(a: u64, b: u64) -> u64 {
hasher.finish()
}
#[derive(Clone, Debug, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DocumentNodeMetadata {
pub position: (i32, i32),
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DocumentNode {
pub name: String,
pub inputs: Vec<NodeInput>,
pub implementation: DocumentNodeImplementation,
pub metadata: DocumentNodeMetadata,
}
impl DocumentNode {
@ -54,7 +61,7 @@ impl DocumentNode {
let first = self.inputs.remove(0);
if let DocumentNodeImplementation::Unresolved(fqn) = self.implementation {
let (input, mut args) = match first {
NodeInput::Value(tagged_value) => {
NodeInput::Value { tagged_value, .. } => {
assert_eq!(self.inputs.len(), 0);
(ProtoNodeInput::None, ConstructionArgs::Value(tagged_value.to_value()))
}
@ -63,7 +70,7 @@ impl DocumentNode {
};
assert!(!self.inputs.iter().any(|input| matches!(input, NodeInput::Network)), "recieved non resolved parameter");
assert!(
!self.inputs.iter().any(|input| matches!(input, NodeInput::Value(_))),
!self.inputs.iter().any(|input| matches!(input, NodeInput::Value { .. })),
"recieved value as parameter. inupts: {:#?}, construction_args: {:#?}",
&self.inputs,
&args
@ -90,7 +97,7 @@ impl DocumentNode {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum NodeInput {
Node(NodeId),
Value(value::TaggedValue),
Value { tagged_value: value::TaggedValue, exposed: bool },
Network,
}
@ -100,13 +107,20 @@ impl NodeInput {
*self = NodeInput::Node(f(*id))
}
}
pub fn is_exposed(&self) -> bool {
if let NodeInput::Value { exposed, .. } = self {
*exposed
} else {
true
}
}
}
impl PartialEq for NodeInput {
fn eq(&self, other: &Self) -> bool {
match (&self, &other) {
(Self::Node(n1), Self::Node(n2)) => n1 == n2,
(Self::Value(v1), Self::Value(v2)) => v1 == v2,
(Self::Value { tagged_value: v1, .. }, Self::Value { tagged_value: v2, .. }) => v1 == v2,
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
}
}
@ -169,13 +183,14 @@ impl NodeNetwork {
let network_input = self.nodes.get_mut(network_input).unwrap();
network_input.populate_first_network_input(node, *offset);
}
NodeInput::Value(value) => {
let name = format!("Value: {:?}", value.clone().to_value());
NodeInput::Value { tagged_value, exposed } => {
let name = format!("Value: {:?}", tagged_value.clone().to_value());
let new_id = map_ids(id, gen_id());
let value_node = DocumentNode {
name: name.clone(),
inputs: vec![NodeInput::Value(value)],
inputs: vec![NodeInput::Value { tagged_value, exposed }],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::value::ValueNode", &[Type::Generic])),
metadata: DocumentNodeMetadata::default(),
};
assert!(!self.nodes.contains_key(&new_id));
self.nodes.insert(new_id, value_node);
@ -235,17 +250,19 @@ mod test {
(
0,
DocumentNode {
name: "cons".into(),
name: "Cons".into(),
inputs: vec![NodeInput::Network, NodeInput::Network],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::structural::ConsNode", &[Type::Generic, Type::Generic])),
metadata: DocumentNodeMetadata::default(),
},
),
(
1,
DocumentNode {
name: "add".into(),
name: "Add".into(),
inputs: vec![NodeInput::Node(0)],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::AddNode", &[Type::Generic, Type::Generic])),
metadata: DocumentNodeMetadata::default(),
},
),
]
@ -265,17 +282,19 @@ mod test {
(
1,
DocumentNode {
name: "cons".into(),
name: "Cons".into(),
inputs: vec![NodeInput::Network, NodeInput::Network],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::structural::ConsNode", &[Type::Generic, Type::Generic])),
metadata: DocumentNodeMetadata::default(),
},
),
(
2,
DocumentNode {
name: "add".into(),
name: "Add".into(),
inputs: vec![NodeInput::Node(1)],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::AddNode", &[Type::Generic, Type::Generic])),
metadata: DocumentNodeMetadata::default(),
},
),
]
@ -294,8 +313,15 @@ mod test {
1,
DocumentNode {
name: "Inc".into(),
inputs: vec![NodeInput::Network, NodeInput::Value(value::TaggedValue::U32(2))],
inputs: vec![
NodeInput::Network,
NodeInput::Value {
tagged_value: value::TaggedValue::U32(2),
exposed: false,
},
],
implementation: DocumentNodeImplementation::Network(add_network()),
metadata: DocumentNodeMetadata::default(),
},
)]
.into_iter()
@ -312,9 +338,10 @@ mod test {
#[test]
fn resolve_proto_node_add() {
let document_node = DocumentNode {
name: "cons".into(),
name: "Cons".into(),
inputs: vec![NodeInput::Network, NodeInput::Node(0)],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::structural::ConsNode", &[Type::Generic, Type::Generic])),
metadata: DocumentNodeMetadata::default(),
};
let proto_node = document_node.resolve_proto_node();
@ -380,30 +407,37 @@ mod test {
name: "Inc".into(),
inputs: vec![NodeInput::Node(11)],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Generic])),
metadata: DocumentNodeMetadata::default(),
},
),
(
10,
DocumentNode {
name: "cons".into(),
name: "Cons".into(),
inputs: vec![NodeInput::Network, NodeInput::Node(14)],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::structural::ConsNode", &[Type::Generic, Type::Generic])),
metadata: DocumentNodeMetadata::default(),
},
),
(
14,
DocumentNode {
name: "Value: 2".into(),
inputs: vec![NodeInput::Value(value::TaggedValue::U32(2))],
inputs: vec![NodeInput::Value {
tagged_value: value::TaggedValue::U32(2),
exposed: false,
}],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::value::ValueNode", &[Type::Generic])),
metadata: DocumentNodeMetadata::default(),
},
),
(
11,
DocumentNode {
name: "add".into(),
name: "Add".into(),
inputs: vec![NodeInput::Node(10)],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new("graphene_core::ops::AddNode", &[Type::Generic, Type::Generic])),
metadata: DocumentNodeMetadata::default(),
},
),
]

View file

@ -11,7 +11,7 @@ pub enum TaggedValue {
String(String),
U32(u32),
F32(f32),
//Image(graphene_std::raster::Image),
Image(graphene_std::raster::Image),
Color(graphene_core::raster::color::Color),
}
@ -21,6 +21,7 @@ impl TaggedValue {
TaggedValue::String(x) => Box::new(x),
TaggedValue::U32(x) => Box::new(x),
TaggedValue::F32(x) => Box::new(x),
TaggedValue::Image(x) => Box::new(x),
TaggedValue::Color(x) => Box::new(x),
}
}

View file

@ -68,23 +68,25 @@ mod tests {
(
0,
DocumentNode {
name: "cons".into(),
name: "Cons".into(),
inputs: vec![NodeInput::Network, NodeInput::Network],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new(
"graphene_core::structural::ConsNode",
&[Type::Concrete(std::borrow::Cow::Borrowed("u32")), Type::Concrete(std::borrow::Cow::Borrowed("u32"))],
)),
metadata: DocumentNodeMetadata::default(),
},
),
(
1,
DocumentNode {
name: "add".into(),
name: "Add".into(),
inputs: vec![NodeInput::Node(0)],
implementation: DocumentNodeImplementation::Unresolved(NodeIdentifier::new(
"graphene_core::ops::AddNode",
&[Type::Concrete(std::borrow::Cow::Borrowed("u32")), Type::Concrete(std::borrow::Cow::Borrowed("u32"))],
)),
metadata: DocumentNodeMetadata::default(),
},
),
]
@ -100,8 +102,15 @@ mod tests {
0,
DocumentNode {
name: "Inc".into(),
inputs: vec![NodeInput::Network, NodeInput::Value(value::TaggedValue::U32(1))],
inputs: vec![
NodeInput::Network,
NodeInput::Value {
tagged_value: value::TaggedValue::U32(1),
exposed: false,
},
],
implementation: DocumentNodeImplementation::Network(add_network()),
metadata: DocumentNodeMetadata::default(),
},
)]
.into_iter()

View file

@ -21,17 +21,25 @@ static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[
NodeIdentifier::new("graphene_core::ops::IdNode", &[Concrete(std::borrow::Cow::Borrowed("Any<'_>"))]),
|proto_node, stack| {
stack.push_fn(|nodes| {
let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap();
let node = pre_node.then(graphene_core::ops::IdNode);
node.into_type_erased()
if let ProtoNodeInput::Node(pre_id) = proto_node.input {
let pre_node = nodes.get(pre_id as usize).unwrap();
let node = pre_node.then(graphene_core::ops::IdNode);
node.into_type_erased()
} else {
graphene_core::ops::IdNode.into_type_erased()
}
})
},
),
(NodeIdentifier::new("graphene_core::ops::IdNode", &[Type::Generic]), |proto_node, stack| {
stack.push_fn(|nodes| {
let pre_node = nodes.get(proto_node.input.unwrap_node() as usize).unwrap();
let node = pre_node.then(graphene_core::ops::IdNode);
node.into_type_erased()
if let ProtoNodeInput::Node(pre_id) = proto_node.input {
let pre_node = nodes.get(pre_id as usize).unwrap();
let node = pre_node.then(graphene_core::ops::IdNode);
node.into_type_erased()
} else {
graphene_core::ops::IdNode.into_type_erased()
}
})
}),
(
@ -190,9 +198,9 @@ static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[
}
})
}),
(NodeIdentifier::new("graphene_core::raster::GrayscaleNode", &[]), |proto_node, stack| {
(NodeIdentifier::new("graphene_core::raster::GrayscaleColorNode", &[]), |proto_node, stack| {
stack.push_fn(|nodes| {
let node = DynAnyNode::new(graphene_core::raster::GrayscaleNode);
let node = DynAnyNode::new(graphene_core::raster::GrayscaleColorNode);
if let ProtoNodeInput::Node(pre_id) = proto_node.input {
let pre_node = nodes.get(pre_id as usize).unwrap();
@ -222,14 +230,14 @@ static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[
},
),
(
NodeIdentifier::new("graphene_core::raster::HueShiftNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
NodeIdentifier::new("graphene_core::raster::HueShiftColorNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
|proto_node, stack| {
info!("proto node {:?}", proto_node);
stack.push_fn(|nodes| {
let ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("Hue Shift Color Node constructed with out shift input node") };
let value_node = nodes.get(construction_nodes[0] as usize).unwrap();
let input_node: DowncastBothNode<_, (), f32> = DowncastBothNode::new(value_node);
let node = DynAnyNode::new(graphene_core::raster::HueShiftNode::new(input_node));
let node = DynAnyNode::new(graphene_core::raster::HueShiftColorNode::new(input_node));
if let ProtoNodeInput::Node(pre_id) = proto_node.input {
let pre_node = nodes.get(pre_id as usize).unwrap();
@ -243,6 +251,7 @@ static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[
(NodeIdentifier::new("graphene_std::raster::MapImageNode", &[]), |proto_node, stack| {
if let ConstructionArgs::Nodes(operation_node_id) = proto_node.construction_args {
stack.push_fn(move |nodes| {
info!("Map image Depending upon id {:?}", operation_node_id);
let operation_node = nodes.get(operation_node_id[0] as usize).unwrap();
let operation_node: DowncastBothNode<_, Color, Color> = DowncastBothNode::new(operation_node);
let map_node = DynAnyNode::new(graphene_std::raster::MapImageNode::new(operation_node));
@ -258,45 +267,54 @@ static NODE_REGISTRY: &[(NodeIdentifier, NodeConstructor)] = &[
unimplemented!()
}
}),
(NodeIdentifier::new("graph_craft::node_registry::GrayscaleImage", &[]), |proto_node, stack| {
(NodeIdentifier::new("graphene_std::raster::GrayscaleImageNode", &[]), |proto_node, stack| {
stack.push_fn(move |nodes| {
let grayscale_node = DynAnyNode::new(FnNode::new(|mut 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
}));
let node = DynAnyNode::new(graphene_std::raster::GrayscaleImageNode);
if let ProtoNodeInput::Node(node_id) = proto_node.input {
let pre_node = nodes.get(node_id as usize).unwrap();
(pre_node).then(grayscale_node).into_type_erased()
(pre_node).then(node).into_type_erased()
} else {
grayscale_node.into_type_erased()
node.into_type_erased()
}
})
}),
(NodeIdentifier::new("graph_craft::node_registry::HueShiftImage", &[]), |_proto_node, _stack| {
todo!();
// stack.push_fn(move |nodes| {
(
NodeIdentifier::new("graphene_std::raster::HueShiftImage", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
|proto_node, stack| {
stack.push_fn(move |nodes| {
let ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("Hue Shift Image Node constructed with out shift input node") };
let value_node = nodes.get(construction_nodes[0] as usize).unwrap();
let input_node: DowncastBothNode<_, (), f32> = DowncastBothNode::new(value_node);
let node = DynAnyNode::new(graphene_std::raster::HueShiftImage::new(input_node));
// let hue_shift_node = DynAnyNode::new(FnNode::new(|(mut image, amount): (Image, f32)| {
// for pixel in &mut image.data {
// let [mut hue, saturation, luminance, alpha] = pixel.to_hsla();
// hue += amount;
// *pixel = Color::from_hsla(hue, saturation, luminance, alpha);
// }
// image
// }));
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::BrightenImageNode", &[Type::Concrete(Cow::Borrowed("&TypeErasedNode"))]),
|proto_node, stack| {
stack.push_fn(move |nodes| {
let ConstructionArgs::Nodes(construction_nodes) = proto_node.construction_args else { unreachable!("Brighten Image Node constructed with out brighten input node") };
let value_node = nodes.get(construction_nodes[0] as usize).unwrap();
let input_node: DowncastBothNode<_, (), f32> = DowncastBothNode::new(value_node);
let node = DynAnyNode::new(graphene_std::raster::BrightenImageNode::new(input_node));
// if let ProtoNodeInput::Node(node_id) = proto_node.input {
// let pre_node = nodes.get(node_id as usize).unwrap();
// (pre_node).then(hue_shift_node).into_type_erased()
// } else {
// hue_shift_node.into_type_erased()
// }
// })
}),
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::ImageNode", &[Concrete(std::borrow::Cow::Borrowed("&str"))]),
|_proto_node, stack| {
@ -401,7 +419,7 @@ mod protograph_testing {
let grayscale_protonode = ProtoNode {
construction_args: ConstructionArgs::Nodes(vec![]),
input: ProtoNodeInput::Node(0),
identifier: NodeIdentifier::new("graphene_core::raster::GrayscaleNode", &[]),
identifier: NodeIdentifier::new("graphene_core::raster::GrayscaleColorNode", &[]),
};
push_node(grayscale_protonode, &stack);
@ -438,7 +456,7 @@ mod protograph_testing {
let grayscale_protonode = ProtoNode {
construction_args: ConstructionArgs::Nodes(vec![]),
input: ProtoNodeInput::None,
identifier: NodeIdentifier::new("graphene_core::raster::GrayscaleNode", &[]),
identifier: NodeIdentifier::new("graphene_core::raster::GrayscaleColorNode", &[]),
};
push_node(grayscale_protonode, &stack);

View file

@ -1,4 +1,4 @@
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use crate::document::value;
use crate::document::NodeId;
@ -165,7 +165,37 @@ impl ProtoNetwork {
edges
}
// Based on https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm
// Based on https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search
// This approach excludes nodes that are not connected
pub fn topological_sort(&self) -> Vec<NodeId> {
let mut sorted = Vec::new();
let inwards_edges = self.collect_inwards_edges();
fn visit(node_id: NodeId, temp_marks: &mut HashSet<NodeId>, sorted: &mut Vec<NodeId>, inwards_edges: &HashMap<NodeId, Vec<NodeId>>) {
if sorted.contains(&node_id) {
return;
};
if temp_marks.contains(&node_id) {
panic!("Cycle detected");
}
info!("Visiting {node_id}");
if let Some(dependencies) = inwards_edges.get(&node_id) {
temp_marks.insert(node_id);
for &dependant in dependencies {
visit(dependant, temp_marks, sorted, inwards_edges);
}
temp_marks.remove(&node_id);
}
sorted.push(node_id);
}
assert!(self.nodes.iter().any(|(id, _)| *id == self.output), "Output id {} does not exist", self.output);
visit(self.output, &mut HashSet::new(), &mut sorted, &inwards_edges);
info!("Sorted order {sorted:?}");
sorted
}
/*// Based on https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm
pub fn topological_sort(&self) -> Vec<NodeId> {
let mut sorted = Vec::new();
let outwards_edges = self.collect_outwards_edges();
@ -187,21 +217,24 @@ impl ProtoNetwork {
}
}
}
info!("Sorted order {sorted:?}");
sorted
}
}*/
pub fn reorder_ids(&mut self) {
let order = self.topological_sort();
let lookup = self
.nodes
// Map of node ids to indexes (which become the node ids as they are inserted into the borrow stack)
let lookup: HashMap<_, _> = order.iter().enumerate().map(|(pos, id)| (*id, pos as NodeId)).collect();
info!("Order {order:?}");
self.nodes = order
.iter()
.map(|(id, _)| (*id, order.iter().position(|x| x == id).unwrap() as u64))
.collect::<HashMap<u64, u64>>();
self.nodes.sort_by_key(|(id, _)| lookup.get(id).unwrap());
self.nodes.iter_mut().for_each(|(id, node)| {
node.map_ids(|id| *lookup.get(&id).unwrap());
*id = *lookup.get(id).unwrap()
});
.map(|id| {
let mut node = self.nodes.swap_remove(self.nodes.iter().position(|(test_id, _)| test_id == id).unwrap()).1;
node.map_ids(|id| *lookup.get(&id).unwrap());
(*lookup.get(id).unwrap(), node)
})
.collect();
assert_eq!(order.len(), self.nodes.len());
}
}
@ -217,6 +250,14 @@ mod test {
inputs: vec![10],
output: 1,
nodes: [
(
7,
ProtoNode {
identifier: "id".into(),
input: ProtoNodeInput::Node(11),
construction_args: ConstructionArgs::Nodes(vec![]),
},
),
(
1,
ProtoNode {

View file

@ -26,3 +26,8 @@ proc-macro2 = {version = "1.0", default-features = false, features = ["proc-macr
quote = {version = "1.0", default-features = false }
image = "*"
dyn-clone = "1.0"
[dependencies.serde]
version = "1.0"
optional = true
features = ["derive"]

View file

@ -98,7 +98,8 @@ impl<Reader: std::io::Read> Node<Reader> for BufferNode {
}
}
#[derive(Clone, DynAny)]
#[derive(Clone, Debug, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Image {
pub width: u32,
pub height: u32,
@ -165,16 +166,104 @@ pub fn export_image_node<'n>() -> impl Node<(Image, &'n str), Output = Result<()
})
}
#[derive(Debug, Clone, Copy)]
pub struct GrayscaleImageNode;
impl Node<Image> for GrayscaleImageNode {
type Output = Image;
fn eval(self, mut image: Image) -> Image {
for pixel in &mut image.data {
let avg = (pixel.r() + pixel.g() + pixel.b()) / 3.;
*pixel = Color::from_rgbaf32_unchecked(avg, avg, avg, pixel.a());
}
image
}
}
impl Node<Image> for &GrayscaleImageNode {
type Output = Image;
fn eval(self, mut image: Image) -> Image {
for pixel in &mut image.data {
let avg = (pixel.r() + pixel.g() + pixel.b()) / 3.;
*pixel = Color::from_rgbaf32_unchecked(avg, avg, avg, pixel.a());
}
image
}
}
#[derive(Debug, Clone, Copy)]
pub struct BrightenImageNode<N: Node<(), Output = f32>>(N);
impl<N: Node<(), Output = f32>> Node<Image> for BrightenImageNode<N> {
type Output = Image;
fn eval(self, mut image: Image) -> Image {
let brightness = self.0.eval(());
let per_channel = |col: f32| (col + brightness / 255.).clamp(0., 1.);
for pixel in &mut image.data {
*pixel = Color::from_rgbaf32_unchecked(per_channel(pixel.r()), per_channel(pixel.g()), per_channel(pixel.b()), pixel.a());
}
image
}
}
impl<N: Node<(), Output = f32> + Copy> Node<Image> for &BrightenImageNode<N> {
type Output = Image;
fn eval(self, mut image: Image) -> Image {
let brightness = self.0.eval(());
let per_channel = |col: f32| (col + brightness / 255.).clamp(0., 1.);
for pixel in &mut image.data {
*pixel = Color::from_rgbaf32_unchecked(per_channel(pixel.r()), per_channel(pixel.g()), per_channel(pixel.b()), pixel.a());
}
image
}
}
impl<N: Node<(), Output = f32> + Copy> BrightenImageNode<N> {
pub fn new(node: N) -> Self {
Self(node)
}
}
#[derive(Debug, Clone, Copy)]
pub struct HueShiftImage<N: Node<(), Output = f32>>(N);
impl<N: Node<(), Output = f32>> Node<Image> for HueShiftImage<N> {
type Output = Image;
fn eval(self, mut image: Image) -> Image {
let hue_shift = self.0.eval(());
for pixel in &mut image.data {
let [hue, saturation, luminance, alpha] = pixel.to_hsla();
*pixel = Color::from_hsla(hue + hue_shift / 360., saturation, luminance, alpha);
}
image
}
}
impl<N: Node<(), Output = f32> + Copy> Node<Image> for &HueShiftImage<N> {
type Output = Image;
fn eval(self, mut image: Image) -> Image {
let hue_shift = self.0.eval(());
for pixel in &mut image.data {
let [hue, saturation, luminance, alpha] = pixel.to_hsla();
*pixel = Color::from_hsla(hue + hue_shift / 360., saturation, luminance, alpha);
}
image
}
}
impl<N: Node<(), Output = f32> + Copy> HueShiftImage<N> {
pub fn new(node: N) -> Self {
Self(node)
}
}
#[cfg(test)]
mod test {
use super::*;
use graphene_core::raster::color::Color;
use graphene_core::raster::GrayscaleNode;
use graphene_core::raster::GrayscaleColorNode;
#[test]
fn map_node() {
let array = [Color::from_rgbaf32(1.0, 0.0, 0.0, 1.0).unwrap()];
let map = MapNode(GrayscaleNode, PhantomData);
let map = MapNode(GrayscaleColorNode, PhantomData);
let values = map.eval(array.into_iter());
assert_eq!(values[0], Color::from_rgbaf32(0.33333334, 0.33333334, 0.33333334, 1.0).unwrap());
}
@ -182,7 +271,7 @@ mod test {
#[test]
fn load_image() {
let image = image_node::<&str>();
let gray = MapImageNode::new(GrayscaleNode);
let gray = MapImageNode::new(GrayscaleColorNode);
let grayscale_picture = image.then(MapResultNode::new(&gray));
let export = export_image_node();