Replace Footprint/() call arguments with dynamically-bound Contexts (#2232)

* Implement experimental Context struct and traits

* Add Ctx super trait

* Checkpoint

* Return Any instead of DynAny

* Fix send implementation for inputs with lifetimes

* Port more nodes

* Uncomment nodes

* Port more nodes

* Port vector nodes

* Partial progress (the stuff I'm more sure about)

* Partial progress (the stuff that's not compiling and I'm not sure about)

* Fix more errors

* First pass of fixing errors introduced by rebase

* Port wasm application io

* Fix brush node types

* Add type annotation

* Fix warnings and wasm compilation

* Change types for Document Node definitions

* Improve debugging for footprint not found errors

* Forward context in append artboard node

* Fix thumbnails

* Fix loading most demo artwork

* Wrap output type of all nodes in future

* Encode futures as part of the type

* Fix document node definitions for future types

* Remove Clippy warnings

* Fix more things

* Fix opening demo art with manual composition upgrading

* Set correct type for manual composition

* Fix brush

* Fix tests

* Update docs for deps

* Fix up some node signature issues

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
Co-authored-by: hypercube <0hypercube@gmail.com>
This commit is contained in:
Dennis Kobert
2025-03-01 23:54:52 +01:00
committed by Keavon Chambers
parent 0c1e96b9c6
commit 4ff2bdb04f
43 changed files with 1338 additions and 1545 deletions

View File

@@ -54,6 +54,7 @@ winit = { workspace = true }
[dev-dependencies]
# Workspace dependencies
graph-craft = { workspace = true, features = ["loading"] }
pretty_assertions = { workspace = true }
# Required dependencies
criterion = { version = "0.5", features = ["html_reports"]}

View File

@@ -304,7 +304,7 @@ impl DocumentNode {
match first {
NodeInput::Value { tagged_value, .. } => {
assert_eq!(self.inputs.len(), 0, "A value node cannot have any inputs. Current inputs: {:?}", self.inputs);
(ProtoNodeInput::None, ConstructionArgs::Value(tagged_value))
(ProtoNodeInput::ManualComposition(concrete!(graphene_core::Context<'static>)), ConstructionArgs::Value(tagged_value))
}
NodeInput::Node { node_id, output_index, lambda } => {
assert_eq!(output_index, 0, "Outputs should be flattened before converting to proto node");
@@ -1567,7 +1567,7 @@ mod test {
NodeId(14),
ProtoNode {
identifier: "graphene_core::value::ClonedNode".into(),
input: ProtoNodeInput::None,
input: ProtoNodeInput::ManualComposition(concrete!(graphene_core::Context)),
construction_args: ConstructionArgs::Value(TaggedValue::U32(2).into()),
original_location: OriginalLocation {
path: Some(vec![NodeId(1), NodeId(4)]),
@@ -1589,7 +1589,7 @@ mod test {
println!("{:#?}", resolved_network[0]);
println!("{construction_network:#?}");
assert_eq!(resolved_network[0], construction_network);
pretty_assertions::assert_eq!(resolved_network[0], construction_network);
}
fn flat_network() -> NodeNetwork {

View File

@@ -102,8 +102,8 @@ macro_rules! tagged_value {
})
}
Type::Fn(_, output) => TaggedValue::from_type(output),
Type::Future(_) => {
None
Type::Future(output) => {
TaggedValue::from_type(output)
}
}
}
@@ -270,7 +270,7 @@ impl TaggedValue {
Some(ty)
}
Type::Fn(_, output) => TaggedValue::from_primitive_string(string, output),
Type::Future(_) => None,
Type::Future(fut) => TaggedValue::from_primitive_string(string, fut),
}
}

View File

@@ -162,7 +162,7 @@ impl Default for ProtoNode {
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ProtoNodeInput {
/// [`ProtoNode`]s do not require any input, e.g. the value node just takes in [`ConstructionArgs`].
/// This input will be converted to `()` as the call argument.
None,
/// A ManualComposition input represents an input that opts out of being resolved through the `ComposeNode`, which first runs the previous (upstream) node, then passes that evaluated
/// result to this node. Instead, ManualComposition lets this node actually consume the provided input instead of passing it to its predecessor.
@@ -203,6 +203,7 @@ impl ProtoNode {
if self.skip_deduplication {
self.original_location.path.hash(&mut hasher);
}
std::mem::discriminant(&self.input).hash(&mut hasher);
match self.input {
ProtoNodeInput::None => (),
@@ -212,6 +213,7 @@ impl ProtoNode {
ProtoNodeInput::Node(id) => (id, false).hash(&mut hasher),
ProtoNodeInput::NodeLambda(id) => (id, true).hash(&mut hasher),
};
Some(NodeId(hasher.finish()))
}
@@ -224,7 +226,7 @@ impl ProtoNode {
Self {
identifier: ProtoNodeIdentifier::new("graphene_core::value::ClonedNode"),
construction_args: value,
input: ProtoNodeInput::None,
input: ProtoNodeInput::ManualComposition(concrete!(Context)),
original_location: OriginalLocation {
path: Some(path),
inputs_exposed: vec![false; inputs_exposed],
@@ -552,10 +554,11 @@ impl core::fmt::Debug for GraphErrorType {
GraphErrorType::NoImplementations => write!(f, "No implementations found"),
GraphErrorType::NoConstructor => write!(f, "No construct found for node"),
GraphErrorType::InvalidImplementations { inputs, error_inputs } => {
let format_error = |(index, (_found, expected)): &(usize, (Type, Type))| format!("• Input {}: {expected}", index + 1);
let format_error_list = |errors: &Vec<(usize, (Type, Type))>| errors.iter().map(format_error).collect::<Vec<_>>().join("\n");
let format_error = |(index, (_found, expected)): &(usize, (Type, Type))| format!("• Input {}: {expected}, found: {_found}", index + 1);
let format_error_list = |errors: &Vec<(usize, (Type, Type))>| errors.iter().map(format_error).collect::<Vec<_>>().join("\n").replace("Option<Arc<OwnedContextImpl>>", "Context");
let mut errors = error_inputs.iter().map(format_error_list).collect::<Vec<_>>();
errors.sort();
let inputs = inputs.replace("Option<Arc<OwnedContextImpl>>", "Context");
write!(
f,
"This node isn't compatible with the com-\n\
@@ -651,9 +654,9 @@ impl TypingContext {
let inputs = match node.construction_args {
// If the node has a value input we can infer the return type from it
ConstructionArgs::Value(ref v) => {
assert!(matches!(node.input, ProtoNodeInput::None));
assert!(matches!(node.input, ProtoNodeInput::None) || matches!(node.input, ProtoNodeInput::ManualComposition(ref x) if x == &concrete!(Context)));
// TODO: This should return a reference to the value
let types = NodeIOTypes::new(concrete!(()), v.ty(), vec![v.ty()]);
let types = NodeIOTypes::new(concrete!(Context), Type::Future(Box::new(v.ty())), vec![]);
self.inferred.insert(node_id, types.clone());
return Ok(types);
}
@@ -696,6 +699,8 @@ impl TypingContext {
match (from, to) {
// Direct comparison of two concrete types.
(Type::Concrete(type1), Type::Concrete(type2)) => type1 == type2,
// Check inner type for futures
(Type::Future(type1), Type::Future(type2)) => type1 == type2,
// Loose comparison of function types, where loose means that functions are considered on a "greater than or equal to" basis of its function type's generality.
// That means we compare their types with a contravariant relationship, which means that a more general type signature may be substituted for a more specific type signature.
// For example, we allow `T -> V` to be substituted with `T' -> V` or `() -> V` where T' and () are more specific than T.