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

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 {