From d69cbc7dee6eacb3388ca9c8e16ab4b36aa0359f Mon Sep 17 00:00:00 2001 From: "dennis@kobert.dev" Date: Fri, 9 Aug 2024 12:03:21 +0200 Subject: [PATCH 1/2] Add explicit clip area to footprint --- .../node_graph/document_node_types.rs | 5 ++- .../document/node_graph/node_properties.rs | 18 ++++++----- editor/src/node_graph_executor.rs | 5 +-- node-graph/gcore/src/graphic_element.rs | 31 +++++++++++++++++-- node-graph/gcore/src/raster/bbox.rs | 29 +++++++++++++++-- node-graph/gcore/src/transform.rs | 20 ++++++++---- node-graph/gstd/src/raster.rs | 7 ++++- node-graph/gstd/src/wasm_application_io.rs | 14 +++++---- node-graph/wgpu-executor/src/lib.rs | 2 +- 9 files changed, 102 insertions(+), 29 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_types.rs b/editor/src/messages/portfolio/document/node_graph/document_node_types.rs index 2ef6c16da3..8a31adcb30 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_types.rs @@ -651,7 +651,10 @@ fn static_nodes() -> Vec { NodeInput::value( TaggedValue::Footprint(Footprint { transform: DAffine2::from_scale_angle_translation(DVec2::new(100., 100.), 0., DVec2::new(0., 0.)), - resolution: UVec2::new(100, 100), + clip: raster::bbox::AxisAlignedBbox { + start: DVec2::ZERO, + end: DVec2::new(100., 100.), + }, ..Default::default() }), false, diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 41504b892b..a5c0a24cae 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -9,6 +9,7 @@ use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNode, NodeId, NodeInput}; use graph_craft::imaginate_input::{ImaginateSamplingMethod, ImaginateServerStatus, ImaginateStatus}; use graphene_core::memo::IORecord; +use graphene_core::raster::bbox::AxisAlignedBbox; use graphene_core::raster::{ BlendMode, CellularDistanceFunction, CellularReturnType, Color, DomainWarpType, FractalType, ImageFrame, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, SelectiveColorChoice, @@ -144,7 +145,7 @@ fn footprint_widget(document_node: &DocumentNode, node_id: NodeId, index: usize) if let Some(&TaggedValue::Footprint(footprint)) = &document_node.inputs[index].as_non_exposed_value() { let top_left = footprint.transform.transform_point2(DVec2::ZERO); let bounds = footprint.scale(); - let oversample = footprint.resolution.as_dvec2() / bounds; + let oversample = footprint.clip.size() / bounds; location_widgets.extend_from_slice(&[ NumberInput::new(Some(top_left.x)) @@ -159,7 +160,7 @@ fn footprint_widget(document_node: &DocumentNode, node_id: NodeId, index: usize) let footprint = Footprint { transform: DAffine2::from_scale_angle_translation(scale, 0., offset), - resolution: (oversample * scale).as_uvec2(), + clip: AxisAlignedBbox::from_size(oversample * scale), ..footprint }; @@ -183,7 +184,7 @@ fn footprint_widget(document_node: &DocumentNode, node_id: NodeId, index: usize) let footprint = Footprint { transform: DAffine2::from_scale_angle_translation(scale, 0., offset), - resolution: (oversample * scale).as_uvec2(), + clip: AxisAlignedBbox::from_size(oversample * scale), ..footprint }; @@ -206,7 +207,7 @@ fn footprint_widget(document_node: &DocumentNode, node_id: NodeId, index: usize) let footprint = Footprint { transform: DAffine2::from_scale_angle_translation(scale, 0., offset), - resolution: (oversample * scale).as_uvec2(), + clip: AxisAlignedBbox::from_size(oversample * scale), ..footprint }; @@ -227,7 +228,7 @@ fn footprint_widget(document_node: &DocumentNode, node_id: NodeId, index: usize) let footprint = Footprint { transform: DAffine2::from_scale_angle_translation(scale, 0., offset), - resolution: (oversample * scale).as_uvec2(), + clip: AxisAlignedBbox::from_size(oversample * scale), ..footprint }; @@ -241,14 +242,15 @@ fn footprint_widget(document_node: &DocumentNode, node_id: NodeId, index: usize) ]); resolution_widgets.push( - NumberInput::new(Some((footprint.resolution.as_dvec2() / bounds).x * 100.)) + NumberInput::new(Some((footprint.clip.size() / bounds).x * 100.)) .label("Resolution") .unit("%") .on_update(update_value( move |x: &NumberInput| { - let resolution = (bounds * x.value.unwrap_or(100.) / 100.).as_uvec2().max((1, 1).into()).min((4000, 4000).into()); + let resolution = (bounds * x.value.unwrap_or(100.) / 100.).max((1., 1.).into()).min((4000., 4000.).into()); + let clip = AxisAlignedBbox::from_size(resolution); - let footprint = Footprint { resolution, ..footprint }; + let footprint = Footprint { clip, ..footprint }; TaggedValue::Footprint(footprint) }, node_id, diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index dec15d0b4b..08c097bd76 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -12,6 +12,7 @@ use graph_craft::proto::GraphErrors; use graph_craft::wasm_application_io::EditorPreferences; use graphene_core::application_io::{NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig}; use graphene_core::memo::IORecord; +use graphene_core::raster::bbox::AxisAlignedBbox; use graphene_core::raster::ImageFrame; use graphene_core::renderer::{ClickTarget, GraphicElementRendered, ImageRenderMode, RenderParams, SvgRender}; use graphene_core::renderer::{RenderSvgSegmentList, SvgSegment}; @@ -479,7 +480,7 @@ impl NodeGraphExecutor { let render_config = RenderConfig { viewport: Footprint { transform: document.metadata().document_to_viewport, - resolution: viewport_resolution, + clip: AxisAlignedBbox::from_size(viewport_resolution.as_dvec2()), ..Default::default() }, #[cfg(any(feature = "resvg", feature = "vello"))] @@ -516,7 +517,7 @@ impl NodeGraphExecutor { let render_config = RenderConfig { viewport: Footprint { transform: transform * DAffine2::from_scale(DVec2::splat(export_config.scale_factor)), - resolution: (size * export_config.scale_factor).as_uvec2(), + clip: AxisAlignedBbox::from_size(size * export_config.scale_factor), ..Default::default() }, export_format: graphene_core::application_io::ExportFormat::Svg, diff --git a/node-graph/gcore/src/graphic_element.rs b/node-graph/gcore/src/graphic_element.rs index 1a4aad31b6..0922a5b284 100644 --- a/node-graph/gcore/src/graphic_element.rs +++ b/node-graph/gcore/src/graphic_element.rs @@ -1,4 +1,5 @@ use crate::application_io::TextureFrame; +use crate::raster::bbox::AxisAlignedBbox; use crate::raster::{BlendMode, ImageFrame}; use crate::transform::{Footprint, Transform, TransformMut}; use crate::vector::VectorData; @@ -221,8 +222,34 @@ async fn construct_artboard( background: Color, clip: bool, ) -> Artboard { - footprint.transform *= DAffine2::from_translation(location.as_dvec2()); - let graphic_group = self.contents.eval(footprint).await; + let mut new_footprint = footprint; + + let viewport_bounds = footprint.viewport_bounds_in_local_space(); + let artboard_bounds = AxisAlignedBbox { + start: location.as_dvec2(), + end: (location + dimensions).as_dvec2(), + }; + let intersection = viewport_bounds.intersect(&artboard_bounds); + let offset = intersection.start; + let scale = footprint.scale(); + let intersection = intersection.transformed(footprint.transform); + let resolution = (scale * intersection.size()).as_uvec2(); + log::debug!("offset: {offset:?}, resolution: {resolution:?}"); + + if clip { + new_footprint = Footprint { + transform: DAffine2::IDENTITY, + clip: intersection, + ..footprint + }; + } + + let mut graphic_group = self.contents.eval(new_footprint).await; + + if clip { + let mut data_transform = graphic_group.transform_mut(); + // *data_transform = DAffine2::from_translation(offset) * *data_transform; + } Artboard { graphic_group, diff --git a/node-graph/gcore/src/raster/bbox.rs b/node-graph/gcore/src/raster/bbox.rs index 04a9d8de35..548d0802c0 100644 --- a/node-graph/gcore/src/raster/bbox.rs +++ b/node-graph/gcore/src/raster/bbox.rs @@ -1,17 +1,31 @@ use dyn_any::{DynAny, StaticType}; -use glam::{DAffine2, DVec2}; +use glam::{DAffine2, DVec2, Vec2Swizzles}; #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] -#[derive(Clone, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, dyn_any::DynAny, PartialEq)] pub struct AxisAlignedBbox { pub start: DVec2, pub end: DVec2, } +impl core::hash::Hash for AxisAlignedBbox { + fn hash(&self, state: &mut H) { + self.start.x.to_bits().hash(state); + self.start.y.to_bits().hash(state); + self.end.x.to_bits().hash(state); + self.end.y.to_bits().hash(state); + } +} + impl AxisAlignedBbox { pub const ZERO: Self = Self { start: DVec2::ZERO, end: DVec2::ZERO }; pub const ONE: Self = Self { start: DVec2::ZERO, end: DVec2::ONE }; + pub fn from_size(size: DVec2) -> Self { + Self { start: DVec2::ZERO, end: size } + } + pub fn size(&self) -> DVec2 { self.end - self.start } @@ -28,12 +42,14 @@ impl AxisAlignedBbox { other.start.x <= self.end.x && other.end.x >= self.start.x && other.start.y <= self.end.y && other.end.y >= self.start.y } + #[must_use] pub fn union(&self, other: &AxisAlignedBbox) -> AxisAlignedBbox { AxisAlignedBbox { start: DVec2::new(self.start.x.min(other.start.x), self.start.y.min(other.start.y)), end: DVec2::new(self.end.x.max(other.end.x), self.end.y.max(other.end.y)), } } + #[must_use] pub fn union_non_empty(&self, other: &AxisAlignedBbox) -> Option { match (self.size() == DVec2::ZERO, other.size() == DVec2::ZERO) { (true, true) => None, @@ -46,12 +62,21 @@ impl AxisAlignedBbox { } } + #[must_use] pub fn intersect(&self, other: &AxisAlignedBbox) -> AxisAlignedBbox { AxisAlignedBbox { start: DVec2::new(self.start.x.max(other.start.x), self.start.y.max(other.start.y)), end: DVec2::new(self.end.x.min(other.end.x), self.end.y.min(other.end.y)), } } + + #[must_use] + pub fn transformed(&self, transform: DAffine2) -> Self { + Self { + start: transform.transform_point2(self.start), + end: transform.transform_point2(self.end), + } + } } #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] diff --git a/node-graph/gcore/src/transform.rs b/node-graph/gcore/src/transform.rs index 81cf83bc80..0ae2fdf1ff 100644 --- a/node-graph/gcore/src/transform.rs +++ b/node-graph/gcore/src/transform.rs @@ -2,6 +2,7 @@ use dyn_any::StaticType; use glam::DAffine2; use glam::DVec2; +use glam::UVec2; use crate::raster::bbox::AxisAlignedBbox; use crate::raster::ImageFrame; @@ -140,8 +141,8 @@ pub enum RenderQuality { pub struct Footprint { /// Inverse of the transform which will be applied to the node output during the rendering process pub transform: DAffine2, - /// Resolution of the target output area in pixels - pub resolution: glam::UVec2, + /// Target area which is displayed on the screen + pub clip: AxisAlignedBbox, /// Quality of the render, this may be used by caching nodes to decide if the cached render is sufficient pub quality: RenderQuality, /// When the transform is set downstream, all upstream modifications have to be ignored @@ -152,7 +153,10 @@ impl Default for Footprint { fn default() -> Self { Self { transform: DAffine2::IDENTITY, - resolution: glam::UVec2::new(1920, 1080), + clip: AxisAlignedBbox { + start: DVec2::ZERO, + end: DVec2::new(1920., 1080.), + }, quality: RenderQuality::Full, ignore_modifications: false, } @@ -162,8 +166,8 @@ impl Default for Footprint { impl Footprint { pub fn viewport_bounds_in_local_space(&self) -> AxisAlignedBbox { let inverse = self.transform.inverse(); - let start = inverse.transform_point2((0., 0.).into()); - let end = inverse.transform_point2(self.resolution.as_dvec2()); + let start = inverse.transform_point2(self.clip.start); + let end = inverse.transform_point2(self.clip.end); AxisAlignedBbox { start, end } } @@ -174,6 +178,10 @@ impl Footprint { pub fn offset(&self) -> DVec2 { self.transform.transform_point2(DVec2::ZERO) } + + pub fn resolution(&self) -> UVec2 { + self.clip.size().as_uvec2() + } } #[derive(Debug, Clone, Copy)] @@ -190,7 +198,7 @@ fn cull_vector_data(footprint: Footprint, vector_data: T) -> T { impl core::hash::Hash for Footprint { fn hash(&self, state: &mut H) { self.transform.to_cols_array().iter().for_each(|x| x.to_le_bytes().hash(state)); - self.resolution.hash(state) + self.clip.hash(state) } } diff --git a/node-graph/gstd/src/raster.rs b/node-graph/gstd/src/raster.rs index d8ae16930d..1963e2cd97 100644 --- a/node-graph/gstd/src/raster.rs +++ b/node-graph/gstd/src/raster.rs @@ -643,6 +643,7 @@ fn noise_pattern( let mut size = viewport_bounds.size(); let mut offset = viewport_bounds.start; + log::debug!("size: {size:?}, offset: {offset:?}"); if clip { // TODO: Remove "clip" entirely (and its arbitrary 100x100 clipping square) once we have proper resolution-aware layer clipping const CLIPPING_SQUARE_SIZE: f64 = 100.; @@ -661,6 +662,9 @@ fn noise_pattern( let footprint_scale = footprint.scale(); let width = (size.x * footprint_scale.x) as u32; let height = (size.y * footprint_scale.y) as u32; + log::debug!("resolution: {:?}", footprint.resolution()); + let width = footprint.resolution().x; + let height = footprint.resolution().y; // All let mut image = Image::new(width, height, Color::from_luminance(0.5)); @@ -761,10 +765,11 @@ fn noise_pattern( } } + log::debug!("clip: {:?}", footprint.clip); // Return the coherent noise image ImageFrame:: { image, - transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size), + transform: DAffine2::from_translation(footprint.clip.start) * DAffine2::from_scale(footprint.clip.size() * footprint.scale()), alpha_blending: AlphaBlending::default(), } } diff --git a/node-graph/gstd/src/wasm_application_io.rs b/node-graph/gstd/src/wasm_application_io.rs index 21d8ac4367..d1130fdf6b 100644 --- a/node-graph/gstd/src/wasm_application_io.rs +++ b/node-graph/gstd/src/wasm_application_io.rs @@ -91,8 +91,8 @@ fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_p render.leaf_tag("rect", |attributes| { attributes.push("x", "0"); attributes.push("y", "0"); - attributes.push("width", footprint.resolution.x.to_string()); - attributes.push("height", footprint.resolution.y.to_string()); + attributes.push("width", footprint.resolution().x.to_string()); + attributes.push("height", footprint.resolution().y.to_string()); let matrix = format_transform_matrix(footprint.transform.inverse()); if !matrix.is_empty() { attributes.push("transform", matrix); @@ -102,7 +102,7 @@ fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_p } data.render_svg(&mut render, &render_params); - render.wrap_with_transform(footprint.transform, Some(footprint.resolution.as_dvec2())); + render.wrap_with_transform(footprint.transform, Some(footprint.clip.size())); RenderOutput::Svg(render.svg.to_svg_string()) } @@ -125,8 +125,10 @@ async fn render_canvas(render_config: RenderConfig, data: impl GraphicElementRen // TODO: Instead of applying the transform here, pass the transform during the translation to avoid the O(Nr cost scene.append(&child, Some(kurbo::Affine::new(footprint.transform.to_cols_array()))); + let resolution = footprint.resolution(); + log::debug!("rendering using resolution: {resolution:?}"); - exec.render_vello_scene(&scene, &surface_handle, footprint.resolution.x, footprint.resolution.y, &context) + exec.render_vello_scene(&scene, &surface_handle, resolution.x, resolution.y, &context) .await .expect("Failed to render Vello scene"); } else { @@ -134,7 +136,7 @@ async fn render_canvas(render_config: RenderConfig, data: impl GraphicElementRen } let frame = SurfaceFrame { surface_id: surface_handle.window_id, - resolution: render_config.viewport.resolution, + resolution: render_config.viewport.resolution(), transform: glam::DAffine2::IDENTITY, }; RenderOutput::CanvasFrame(frame) @@ -161,7 +163,7 @@ async fn rasterize<_T: GraphicElementRendered + graphene_core::transform::Transf } let aabb = Bbox::from_transform(footprint.transform).to_axis_aligned_bbox(); let size = aabb.size(); - let resolution = footprint.resolution; + let resolution = footprint.resolution(); let render_params = RenderParams { culling_bounds: None, ..Default::default() diff --git a/node-graph/wgpu-executor/src/lib.rs b/node-graph/wgpu-executor/src/lib.rs index 3278c186e0..7da84388bb 100644 --- a/node-graph/wgpu-executor/src/lib.rs +++ b/node-graph/wgpu-executor/src/lib.rs @@ -955,7 +955,7 @@ async fn render_texture_node<'a: 'input>(footprint: Footprint, image: impl Node< SurfaceFrame { surface_id, transform, - resolution: footprint.resolution, + resolution: footprint.resolution(), } } From ec4bab1a2bb965e863dd71ad820056e730ccb9ee Mon Sep 17 00:00:00 2001 From: "dennis@kobert.dev" Date: Fri, 9 Aug 2024 15:11:43 +0200 Subject: [PATCH 2/2] Fix graph compile error handling --- .../portfolio/document/node_graph/document_node_types.rs | 2 +- editor/src/node_graph_executor.rs | 5 +++++ node-graph/gcore/src/graphic_element.rs | 3 ++- node-graph/gstd/src/raster.rs | 8 +++++--- node-graph/gstd/src/wasm_application_io.rs | 1 - node-graph/interpreted-executor/src/node_registry.rs | 1 + 6 files changed, 14 insertions(+), 6 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_types.rs b/editor/src/messages/portfolio/document/node_graph/document_node_types.rs index 8a31adcb30..82e30df0c1 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_types.rs @@ -2065,7 +2065,7 @@ fn static_nodes() -> Vec { DocumentNode { manual_composition: Some(concrete!(Footprint)), inputs: vec![NodeInput::node(NodeId(1), 0)], - implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::ImpureMemoNode<_, _, _>")), + implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::MemoNode<_, _>")), ..Default::default() }, ] diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 08c097bd76..7f2590b9ed 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -612,6 +612,11 @@ impl NodeGraphExecutor { document.network_interface.document_metadata_mut().update_from_monitor(HashMap::new(), HashMap::new()); log::trace!("{e}"); + responses.add(NodeGraphMessage::UpdateTypes { + resolved_types: ResolvedDocumentNodeTypesDelta::default(), + node_graph_errors, + }); + responses.add(NodeGraphMessage::SendGraph); return Err("Node graph evaluation failed".to_string()); } Ok(result) => result, diff --git a/node-graph/gcore/src/graphic_element.rs b/node-graph/gcore/src/graphic_element.rs index 0922a5b284..7d16220de3 100644 --- a/node-graph/gcore/src/graphic_element.rs +++ b/node-graph/gcore/src/graphic_element.rs @@ -232,8 +232,9 @@ async fn construct_artboard( let intersection = viewport_bounds.intersect(&artboard_bounds); let offset = intersection.start; let scale = footprint.scale(); - let intersection = intersection.transformed(footprint.transform); + // let intersection = intersection.transformed(footprint.transform); let resolution = (scale * intersection.size()).as_uvec2(); + log::debug!("intersection: {intersection:?}"); log::debug!("offset: {offset:?}, resolution: {resolution:?}"); if clip { diff --git a/node-graph/gstd/src/raster.rs b/node-graph/gstd/src/raster.rs index 1963e2cd97..354c647836 100644 --- a/node-graph/gstd/src/raster.rs +++ b/node-graph/gstd/src/raster.rs @@ -656,15 +656,17 @@ fn noise_pattern( // If the image would not be visible, return an empty image if size.x <= 0. || size.y <= 0. { + log::debug!("empty size, aborting"); return ImageFrame::empty(); } let footprint_scale = footprint.scale(); let width = (size.x * footprint_scale.x) as u32; let height = (size.y * footprint_scale.y) as u32; - log::debug!("resolution: {:?}", footprint.resolution()); - let width = footprint.resolution().x; - let height = footprint.resolution().y; + log::debug!("w: {width} h: {height}"); + // log::debug!("resolution: {:?}", footprint.resolution()); + // let width = footprint.resolution().x; + // let height = footprint.resolution().y; // All let mut image = Image::new(width, height, Color::from_luminance(0.5)); diff --git a/node-graph/gstd/src/wasm_application_io.rs b/node-graph/gstd/src/wasm_application_io.rs index d1130fdf6b..b0d5d4c0cc 100644 --- a/node-graph/gstd/src/wasm_application_io.rs +++ b/node-graph/gstd/src/wasm_application_io.rs @@ -126,7 +126,6 @@ async fn render_canvas(render_config: RenderConfig, data: impl GraphicElementRen // TODO: Instead of applying the transform here, pass the transform during the translation to avoid the O(Nr cost scene.append(&child, Some(kurbo::Affine::new(footprint.transform.to_cols_array()))); let resolution = footprint.resolution(); - log::debug!("rendering using resolution: {resolution:?}"); exec.render_vello_scene(&scene, &surface_handle, resolution.x, resolution.y, &context) .await diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 8989ca72a1..cca4f86e35 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -621,6 +621,7 @@ fn node_registry() -> HashMap, input: (), output: RenderOutput, params: [RenderOutput]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, output: Image, fn_params: [Footprint => Image]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, output: ImageFrame, fn_params: [Footprint => ImageFrame]), + async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, output: TextureFrame, fn_params: [Footprint => TextureFrame]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, output: QuantizationChannels, fn_params: [Footprint => QuantizationChannels]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, output: Vec, fn_params: [Footprint => Vec]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Footprint, output: Arc, fn_params: [Footprint => Arc]),