mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-07 15:55:00 +00:00
Add automatic type conversion and the node graph preprocessor (#2478)
* Prototype document network level into node insertion * Implement Convert trait / node for places we can't use Into * Add isize/usize and i128/u128 implementations for Convert trait * Factor out substitutions into preprocessor crate * Simplify layer node further * Code review * Mark preprocessed networks as generated * Revert changes to layer node definition * Skip generated flag for serialization * Don't expand for tests * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
86da69e33f
commit
a40a760f27
15 changed files with 484 additions and 128 deletions
|
@ -571,6 +571,76 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// The [`Convert`] trait allows for conversion between Rust primitive numeric types.
|
||||
/// Because number casting is lossy, we cannot use the normal [`Into`] trait like we do for other types.
|
||||
pub trait Convert<T>: Sized {
|
||||
/// Converts this type into the (usually inferred) output type.
|
||||
#[must_use]
|
||||
fn convert(self) -> T;
|
||||
}
|
||||
|
||||
/// Implements the [`Convert`] trait for conversion between the cartesian product of Rust's primitive numeric types.
|
||||
macro_rules! impl_convert {
|
||||
($from:ty,$to:ty) => {
|
||||
impl Convert<$to> for $from {
|
||||
fn convert(self) -> $to {
|
||||
self as $to
|
||||
}
|
||||
}
|
||||
};
|
||||
($to:ty) => {
|
||||
impl_convert!(f32, $to);
|
||||
impl_convert!(f64, $to);
|
||||
impl_convert!(i8, $to);
|
||||
impl_convert!(u8, $to);
|
||||
impl_convert!(u16, $to);
|
||||
impl_convert!(i16, $to);
|
||||
impl_convert!(i32, $to);
|
||||
impl_convert!(u32, $to);
|
||||
impl_convert!(i64, $to);
|
||||
impl_convert!(u64, $to);
|
||||
impl_convert!(i128, $to);
|
||||
impl_convert!(u128, $to);
|
||||
impl_convert!(isize, $to);
|
||||
impl_convert!(usize, $to);
|
||||
};
|
||||
}
|
||||
impl_convert!(f32);
|
||||
impl_convert!(f64);
|
||||
impl_convert!(i8);
|
||||
impl_convert!(u8);
|
||||
impl_convert!(u16);
|
||||
impl_convert!(i16);
|
||||
impl_convert!(i32);
|
||||
impl_convert!(u32);
|
||||
impl_convert!(i64);
|
||||
impl_convert!(u64);
|
||||
impl_convert!(i128);
|
||||
impl_convert!(u128);
|
||||
impl_convert!(isize);
|
||||
impl_convert!(usize);
|
||||
|
||||
// Convert
|
||||
pub struct ConvertNode<O>(PhantomData<O>);
|
||||
impl<_O> ConvertNode<_O> {
|
||||
pub const fn new() -> Self {
|
||||
Self(core::marker::PhantomData)
|
||||
}
|
||||
}
|
||||
impl<_O> Default for ConvertNode<_O> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
impl<'input, I: 'input + Convert<_O> + Sync + Send, _O: 'input> Node<'input, I> for ConvertNode<_O> {
|
||||
type Output = ::dyn_any::DynFuture<'input, _O>;
|
||||
|
||||
#[inline]
|
||||
fn eval(&'input self, input: I) -> Self::Output {
|
||||
Box::pin(async move { input.convert() })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
|
|
@ -683,6 +683,8 @@ pub struct NodeNetwork {
|
|||
#[serde(default)]
|
||||
#[serde(serialize_with = "graphene_core::vector::serialize_hashmap", deserialize_with = "graphene_core::vector::deserialize_hashmap")]
|
||||
pub scope_injections: FxHashMap<String, (NodeId, Type)>,
|
||||
#[serde(skip)]
|
||||
pub generated: bool,
|
||||
}
|
||||
|
||||
impl Hash for NodeNetwork {
|
||||
|
@ -797,7 +799,9 @@ impl NodeNetwork {
|
|||
pub fn generate_node_paths(&mut self, prefix: &[NodeId]) {
|
||||
for (node_id, node) in &mut self.nodes {
|
||||
let mut new_path = prefix.to_vec();
|
||||
new_path.push(*node_id);
|
||||
if !self.generated {
|
||||
new_path.push(*node_id);
|
||||
}
|
||||
if let DocumentNodeImplementation::Network(network) = &mut node.implementation {
|
||||
network.generate_node_paths(new_path.as_slice());
|
||||
}
|
||||
|
|
|
@ -12,11 +12,7 @@ wgpu = ["wgpu-executor", "gpu", "graphene-std/wgpu"]
|
|||
wayland = ["graphene-std/wayland"]
|
||||
profiling = ["wgpu-executor/profiling"]
|
||||
passthrough = ["wgpu-executor/passthrough"]
|
||||
gpu = [
|
||||
"interpreted-executor/gpu",
|
||||
"graphene-std/gpu",
|
||||
"wgpu-executor",
|
||||
]
|
||||
gpu = ["interpreted-executor/gpu", "graphene-std/gpu", "wgpu-executor"]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
|
@ -24,6 +20,7 @@ graphene-core = { workspace = true }
|
|||
graphene-std = { workspace = true }
|
||||
interpreted-executor = { workspace = true }
|
||||
graph-craft = { workspace = true, features = ["loading"] }
|
||||
preprocessor = { workspace = true }
|
||||
|
||||
# Workspace dependencies
|
||||
log = { workspace = true }
|
||||
|
|
|
@ -184,7 +184,11 @@ fn compile_graph(document_string: String, editor_api: Arc<WasmEditorApi>) -> Res
|
|||
let mut network = load_network(&document_string);
|
||||
fix_nodes(&mut network);
|
||||
|
||||
let substitutions = preprocessor::generate_node_substitutions();
|
||||
preprocessor::expand_network(&mut network, &substitutions);
|
||||
|
||||
let wrapped_network = wrap_network_in_scope(network.clone(), editor_api);
|
||||
|
||||
let compiler = Compiler {};
|
||||
compiler.compile_single(wrapped_network).map_err(|x| x.into())
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ use graphene_std::GraphicElement;
|
|||
use graphene_std::any::{ComposeTypeErased, DowncastBothNode, DynAnyNode, IntoTypeErasedNode};
|
||||
use graphene_std::application_io::{ImageTexture, SurfaceFrame};
|
||||
use graphene_std::wasm_application_io::*;
|
||||
use node_registry_macros::{async_node, into_node};
|
||||
use node_registry_macros::{async_node, convert_node, into_node};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
@ -23,10 +23,7 @@ use wgpu_executor::{WgpuExecutor, WgpuSurface, WindowHandle};
|
|||
|
||||
// TODO: turn into hashmap
|
||||
fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> {
|
||||
let node_types: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![
|
||||
into_node!(from: f64, to: f64),
|
||||
into_node!(from: u32, to: f64),
|
||||
into_node!(from: u8, to: u32),
|
||||
let mut node_types: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![
|
||||
into_node!(from: VectorDataTable, to: VectorDataTable),
|
||||
into_node!(from: VectorDataTable, to: GraphicElement),
|
||||
into_node!(from: VectorDataTable, to: GraphicGroupTable),
|
||||
|
@ -35,6 +32,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||
into_node!(from: RasterDataTable<CPU>, to: RasterDataTable<CPU>),
|
||||
// into_node!(from: RasterDataTable<CPU>, to: RasterDataTable<SRGBA8>),
|
||||
into_node!(from: RasterDataTable<CPU>, to: GraphicElement),
|
||||
into_node!(from: RasterDataTable<GPU>, to: GraphicElement),
|
||||
into_node!(from: RasterDataTable<CPU>, to: GraphicGroupTable),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => RasterDataTable<CPU>]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ImageTexture]),
|
||||
|
@ -137,6 +135,26 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||
},
|
||||
),
|
||||
];
|
||||
node_types.extend(
|
||||
[
|
||||
convert_node!(from: f32, to: numbers),
|
||||
convert_node!(from: f64, to: numbers),
|
||||
convert_node!(from: i8, to: numbers),
|
||||
convert_node!(from: u8, to: numbers),
|
||||
convert_node!(from: u16, to: numbers),
|
||||
convert_node!(from: i16, to: numbers),
|
||||
convert_node!(from: i32, to: numbers),
|
||||
convert_node!(from: u32, to: numbers),
|
||||
convert_node!(from: i64, to: numbers),
|
||||
convert_node!(from: u64, to: numbers),
|
||||
convert_node!(from: i128, to: numbers),
|
||||
convert_node!(from: u128, to: numbers),
|
||||
convert_node!(from: isize, to: numbers),
|
||||
convert_node!(from: usize, to: numbers),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
);
|
||||
|
||||
let mut map: HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> = HashMap::new();
|
||||
|
||||
|
@ -151,12 +169,14 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||
// This occurs for the ChannelMixerNode presumably because of the long name.
|
||||
// This might be caused by the stringify! macro
|
||||
let mut new_name = id.name.replace('\n', " ");
|
||||
// Remove struct generics for all nodes except for the IntoNode
|
||||
if !new_name.contains("IntoNode") {
|
||||
|
||||
// Remove struct generics for all nodes except for the IntoNode and ConvertNode
|
||||
if !(new_name.contains("IntoNode") || new_name.contains("ConvertNode")) {
|
||||
if let Some((path, _generics)) = new_name.split_once("<") {
|
||||
new_name = path.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
let nid = ProtoNodeIdentifier { name: Cow::Owned(new_name) };
|
||||
map.entry(nid).or_default().insert(types.clone(), c);
|
||||
}
|
||||
|
@ -203,9 +223,8 @@ mod node_registry_macros {
|
|||
(from: $from:ty, to: $to:ty) => {
|
||||
(
|
||||
ProtoNodeIdentifier::new(concat!["graphene_core::ops::IntoNode<", stringify!($to), ">"]),
|
||||
|mut args| {
|
||||
|_| {
|
||||
Box::pin(async move {
|
||||
args.reverse();
|
||||
let node = graphene_core::ops::IntoNode::<$to>::new();
|
||||
let any: DynAnyNode<$from, _, _> = graphene_std::any::DynAnyNode::new(node);
|
||||
Box::new(any) as TypeErasedBox
|
||||
|
@ -220,7 +239,47 @@ mod node_registry_macros {
|
|||
)
|
||||
};
|
||||
}
|
||||
macro_rules! convert_node {
|
||||
(from: $from:ty, to: numbers) => {{
|
||||
let x: Vec<(ProtoNodeIdentifier, NodeConstructor, NodeIOTypes)> = vec![
|
||||
convert_node!(from: $from, to: f32),
|
||||
convert_node!(from: $from, to: f64),
|
||||
convert_node!(from: $from, to: i8),
|
||||
convert_node!(from: $from, to: u8),
|
||||
convert_node!(from: $from, to: u16),
|
||||
convert_node!(from: $from, to: i16),
|
||||
convert_node!(from: $from, to: i32),
|
||||
convert_node!(from: $from, to: u32),
|
||||
convert_node!(from: $from, to: i64),
|
||||
convert_node!(from: $from, to: u64),
|
||||
convert_node!(from: $from, to: i128),
|
||||
convert_node!(from: $from, to: u128),
|
||||
convert_node!(from: $from, to: isize),
|
||||
convert_node!(from: $from, to: usize),
|
||||
];
|
||||
x
|
||||
}};
|
||||
(from: $from:ty, to: $to:ty) => {
|
||||
(
|
||||
ProtoNodeIdentifier::new(concat!["graphene_core::ops::ConvertNode<", stringify!($to), ">"]),
|
||||
|_| {
|
||||
Box::pin(async move {
|
||||
let node = graphene_core::ops::ConvertNode::<$to>::new();
|
||||
let any: DynAnyNode<$from, _, _> = graphene_std::any::DynAnyNode::new(node);
|
||||
Box::new(any) as TypeErasedBox
|
||||
})
|
||||
},
|
||||
{
|
||||
let node = graphene_core::ops::ConvertNode::<$to>::new();
|
||||
let mut node_io = NodeIO::<'_, $from>::to_async_node_io(&node, vec![]);
|
||||
node_io.call_argument = future!(<$from as StaticType>::Static);
|
||||
node_io
|
||||
},
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use async_node;
|
||||
pub(crate) use convert_node;
|
||||
pub(crate) use into_node;
|
||||
}
|
||||
|
|
|
@ -78,5 +78,7 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc<WasmEdito
|
|||
exports: vec![NodeInput::node(NodeId(1), 0)],
|
||||
nodes: nodes.into_iter().enumerate().map(|(id, node)| (NodeId(id as u64), node)).collect(),
|
||||
scope_injections: [("editor-api".to_string(), (NodeId(2), concrete!(&WasmEditorApi)))].into_iter().collect(),
|
||||
// TODO(TrueDoctor): check if it makes sense to set `generated` to `true`
|
||||
generated: false,
|
||||
}
|
||||
}
|
||||
|
|
29
node-graph/preprocessor/Cargo.toml
Normal file
29
node-graph/preprocessor/Cargo.toml
Normal file
|
@ -0,0 +1,29 @@
|
|||
[package]
|
||||
name = "preprocessor"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[features]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
dyn-any = { path = "../../libraries/dyn-any", features = [
|
||||
"log-bad-types",
|
||||
"rc",
|
||||
"glam",
|
||||
] }
|
||||
|
||||
# Workspace dependencies
|
||||
graphene-std = { workspace = true, features = ["gpu"] }
|
||||
graph-craft = { workspace = true }
|
||||
interpreted-executor = { workspace = true }
|
||||
log = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
glam = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
|
||||
# Optional workspace dependencies
|
||||
serde = { workspace = true, optional = true }
|
||||
tokio = { workspace = true, optional = true }
|
||||
serde_json = { workspace = true, optional = true }
|
164
node-graph/preprocessor/src/lib.rs
Normal file
164
node-graph/preprocessor/src/lib.rs
Normal file
|
@ -0,0 +1,164 @@
|
|||
use graph_craft::document::value::*;
|
||||
use graph_craft::document::*;
|
||||
use graph_craft::proto::RegistryValueSource;
|
||||
use graph_craft::{ProtoNodeIdentifier, concrete};
|
||||
use graphene_std::registry::*;
|
||||
use graphene_std::*;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
pub fn expand_network(network: &mut NodeNetwork, substitutions: &HashMap<String, DocumentNode>) {
|
||||
if network.generated {
|
||||
return;
|
||||
}
|
||||
|
||||
for node in network.nodes.values_mut() {
|
||||
match &mut node.implementation {
|
||||
DocumentNodeImplementation::Network(node_network) => expand_network(node_network, substitutions),
|
||||
DocumentNodeImplementation::ProtoNode(proto_node_identifier) => {
|
||||
if let Some(new_node) = substitutions.get(proto_node_identifier.name.as_ref()) {
|
||||
node.implementation = new_node.implementation.clone();
|
||||
}
|
||||
}
|
||||
DocumentNodeImplementation::Extract => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_node_substitutions() -> HashMap<String, DocumentNode> {
|
||||
let mut custom = HashMap::new();
|
||||
let node_registry = graphene_core::registry::NODE_REGISTRY.lock().unwrap();
|
||||
for (id, metadata) in graphene_core::registry::NODE_METADATA.lock().unwrap().iter() {
|
||||
let id = id.clone();
|
||||
|
||||
let NodeMetadata { fields, .. } = metadata;
|
||||
let Some(implementations) = &node_registry.get(&id) else { continue };
|
||||
let valid_inputs: HashSet<_> = implementations.iter().map(|(_, node_io)| node_io.call_argument.clone()).collect();
|
||||
let first_node_io = implementations.first().map(|(_, node_io)| node_io).unwrap_or(const { &NodeIOTypes::empty() });
|
||||
let mut node_io_types = vec![HashSet::new(); fields.len()];
|
||||
for (_, node_io) in implementations.iter() {
|
||||
for (i, ty) in node_io.inputs.iter().enumerate() {
|
||||
node_io_types[i].insert(ty.clone());
|
||||
}
|
||||
}
|
||||
let mut input_type = &first_node_io.call_argument;
|
||||
if valid_inputs.len() > 1 {
|
||||
input_type = &const { generic!(D) };
|
||||
}
|
||||
|
||||
let inputs: Vec<_> = node_inputs(fields, first_node_io);
|
||||
let input_count = inputs.len();
|
||||
let network_inputs = (0..input_count).map(|i| NodeInput::node(NodeId(i as u64), 0)).collect();
|
||||
|
||||
let identity_node = ProtoNodeIdentifier::new("graphene_core::ops::IdentityNode");
|
||||
|
||||
let into_node_registry = &interpreted_executor::node_registry::NODE_REGISTRY;
|
||||
|
||||
let mut generated_nodes = 0;
|
||||
let mut nodes: HashMap<_, _, _> = node_io_types
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, inputs)| {
|
||||
(
|
||||
NodeId(i as u64),
|
||||
match inputs.len() {
|
||||
1 => {
|
||||
let input = inputs.iter().next().unwrap();
|
||||
let input_ty = input.nested_type();
|
||||
|
||||
let into_node_identifier = ProtoNodeIdentifier {
|
||||
name: format!("graphene_core::ops::IntoNode<{}>", input_ty.clone()).into(),
|
||||
};
|
||||
let convert_node_identifier = ProtoNodeIdentifier {
|
||||
name: format!("graphene_core::ops::ConvertNode<{}>", input_ty.clone()).into(),
|
||||
};
|
||||
|
||||
let proto_node = if into_node_registry.keys().any(|ident: &ProtoNodeIdentifier| ident.name.as_ref() == into_node_identifier.name.as_ref()) {
|
||||
generated_nodes += 1;
|
||||
into_node_identifier
|
||||
} else if into_node_registry.keys().any(|ident| ident.name.as_ref() == convert_node_identifier.name.as_ref()) {
|
||||
generated_nodes += 1;
|
||||
convert_node_identifier
|
||||
} else {
|
||||
identity_node.clone()
|
||||
};
|
||||
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(input.clone(), i)],
|
||||
// manual_composition: Some(fn_input.clone()),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(proto_node),
|
||||
visible: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
_ => DocumentNode {
|
||||
inputs: vec![NodeInput::network(generic!(X), i)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(identity_node.clone()),
|
||||
visible: false,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if generated_nodes == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let document_node = DocumentNode {
|
||||
inputs: network_inputs,
|
||||
manual_composition: Some(input_type.clone()),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(id.clone().into()),
|
||||
visible: true,
|
||||
skip_deduplication: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
nodes.insert(NodeId(input_count as u64), document_node);
|
||||
|
||||
let node = DocumentNode {
|
||||
inputs,
|
||||
manual_composition: Some(input_type.clone()),
|
||||
implementation: DocumentNodeImplementation::Network(NodeNetwork {
|
||||
exports: vec![NodeInput::Node {
|
||||
node_id: NodeId(input_count as u64),
|
||||
output_index: 0,
|
||||
lambda: false,
|
||||
}],
|
||||
nodes,
|
||||
scope_injections: Default::default(),
|
||||
generated: true,
|
||||
}),
|
||||
visible: true,
|
||||
skip_deduplication: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
custom.insert(id.clone(), node);
|
||||
}
|
||||
|
||||
custom
|
||||
}
|
||||
|
||||
pub fn node_inputs(fields: &[registry::FieldMetadata], first_node_io: &NodeIOTypes) -> Vec<NodeInput> {
|
||||
fields
|
||||
.iter()
|
||||
.zip(first_node_io.inputs.iter())
|
||||
.enumerate()
|
||||
.map(|(index, (field, node_io_ty))| {
|
||||
let ty = field.default_type.as_ref().unwrap_or(node_io_ty);
|
||||
let exposed = if index == 0 { *ty != fn_type_fut!(Context, ()) } else { field.exposed };
|
||||
|
||||
match field.value_source {
|
||||
RegistryValueSource::None => {}
|
||||
RegistryValueSource::Default(data) => return NodeInput::value(TaggedValue::from_primitive_string(data, ty).unwrap_or(TaggedValue::None), exposed),
|
||||
RegistryValueSource::Scope(data) => return NodeInput::scope(Cow::Borrowed(data)),
|
||||
};
|
||||
|
||||
if let Some(type_default) = TaggedValue::from_type(ty) {
|
||||
return NodeInput::value(type_default, exposed);
|
||||
}
|
||||
NodeInput::value(TaggedValue::None, true)
|
||||
})
|
||||
.collect()
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue