diff --git a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs index da5fe1c073..872b55678c 100644 --- a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs +++ b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs @@ -38,120 +38,17 @@ fn upload_to_texture(device: &wgpu::Device, queue: &wgpu::Queue, image: &Raster< ) } -/// Converts a Raster texture to Raster by downloading the underlying texture data. -/// -/// Assumptions: -/// - 2D texture, mip level 0 -/// - 4 bytes-per-pixel RGBA8 -/// - Texture has COPY_SRC usage -struct RasterGpuToRasterCpuConverter { - buffer: wgpu::Buffer, - width: u32, - height: u32, - unpadded_bytes_per_row: u32, - padded_bytes_per_row: u32, - _source: raster_types::Texture, -} -impl RasterGpuToRasterCpuConverter { - fn new(device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, data_gpu: Raster) -> Self { - let texture = data_gpu.data(); - let width = texture.width(); - let height = texture.height(); - let bytes_per_pixel = 4; // RGBA8 - let unpadded_bytes_per_row = width * bytes_per_pixel; - let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; - let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align; - let buffer_size = padded_bytes_per_row as u64 * height as u64; - - let buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("texture_download_buffer"), - size: buffer_size, - usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, - mapped_at_creation: false, - }); - - encoder.copy_texture_to_buffer( - wgpu::TexelCopyTextureInfo { - texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - wgpu::TexelCopyBufferInfo { - buffer: &buffer, - layout: wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(padded_bytes_per_row), - rows_per_image: Some(height), - }, - }, - Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - ); - - Self { - buffer, - width, - height, - unpadded_bytes_per_row, - padded_bytes_per_row, - // Keep source texture alive - _source: data_gpu.texture.clone(), - } - } - - async fn convert(self, device: &wgpu::Device) -> Result, wgpu::BufferAsyncError> { - let buffer_slice = self.buffer.slice(..); - let (sender, receiver) = futures::channel::oneshot::channel(); - buffer_slice.map_async(wgpu::MapMode::Read, move |result| { - let _ = sender.send(result); - }); - - let _ = device.poll(wgpu::wgt::PollType::wait_indefinitely()); - - receiver.await.expect("Failed to receive map result")?; - - let view = buffer_slice.get_mapped_range(); - - let row_stride = self.padded_bytes_per_row as usize; - let row_bytes = self.unpadded_bytes_per_row as usize; - let mut cpu_data: Vec = Vec::with_capacity((self.width * self.height) as usize); - for row in 0..self.height as usize { - let start = row * row_stride; - let row_slice = &view[start..start + row_bytes]; - for px in row_slice.chunks_exact(4) { - // `Image` pixels are stored linear-light with associated (premultiplied) alpha - let srgba = SRGBA8::new(px[0], px[1], px[2], px[3]); - cpu_data.push(Color::from(srgba).apply_opacity(px[3] as f32 / 255.)); - } - } - - drop(view); - self.buffer.unmap(); - let cpu_image = Image { - data: cpu_data, - width: self.width, - height: self.height, - base64_string: None, - }; - - Ok(Raster::new_cpu(cpu_image)) - } -} /// Passthrough conversion for GPU `List`s - no conversion needed impl<'i> Convert>, &'i WgpuExecutor> for List> { - async fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> List> { + fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> List> { self } } /// Converts a `List>` to `List>` by uploading each image to a texture impl<'i> Convert>, &'i WgpuExecutor> for List> { - async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List> { + fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List> { let device = &executor.context().device; let queue = executor.context().queue.lock(); let list = self @@ -171,7 +68,7 @@ impl<'i> Convert>, &'i WgpuExecutor> for List> { /// Converts single CPU raster to GPU by uploading to texture impl<'i> Convert, &'i WgpuExecutor> for Raster { - async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster { + fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster { let device = &executor.context().device; let queue = executor.context().queue.lock(); let texture = upload_to_texture(device, &queue, &self); @@ -183,79 +80,7 @@ impl<'i> Convert, &'i WgpuExecutor> for Raster { /// Passthrough conversion for CPU `List`s - no conversion needed impl<'i> Convert>, &'i WgpuExecutor> for List> { - async fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> List> { + fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> List> { self } } - -/// Converts a `List>` to `List>` by downloading texture data in one go then asynchronously maps all buffers and processes the results. -impl<'i> Convert>, &'i WgpuExecutor> for List> { - async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List> { - let device = &executor.context().device; - let queue = &executor.context().queue; - - let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("batch_texture_download_encoder"), - }); - - let mut converters = Vec::new(); - let mut rows_meta = Vec::new(); - - for row in self { - let (element, attributes) = row.into_parts(); - converters.push(RasterGpuToRasterCpuConverter::new(device, &mut encoder, element)); - rows_meta.push(Item::from_parts((), attributes)); - } - - queue.submit([encoder.finish()]); - - let mut map_futures = Vec::new(); - for converter in converters { - map_futures.push(converter.convert(device)); - } - - let map_results = futures::future::try_join_all(map_futures) - .await - .map_err(|_| "Failed to receive map result") - .expect("Buffer mapping communication failed"); - - map_results - .into_iter() - .zip(rows_meta) - .map(|(element, row)| { - let (_, attributes) = row.into_parts(); - Item::from_parts(element, attributes) - }) - .collect() - } -} - -/// Converts single GPU raster to CPU by downloading texture data -impl<'i> Convert, &'i WgpuExecutor> for Raster { - async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster { - let device = &executor.context().device; - let queue = &executor.context().queue; - - let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("single_texture_download_encoder"), - }); - - let converter = RasterGpuToRasterCpuConverter::new(device, &mut encoder, self); - - queue.submit([encoder.finish()]); - - converter.convert(device).await.expect("Failed to download texture data") - } -} - -/// Uploads an raster texture from the CPU to the GPU. This is now deprecated and the Convert node should be used in the future. -/// -/// Accepts either individual raster data or a `List` of raster elements and converts it to the GPU format using the WgpuExecutor's device and queue. -#[node_macro::node(category(""))] -pub async fn upload_texture<'a: 'n, T: Convert>, &'a WgpuExecutor>>( - _: impl Ctx, - #[implementations(List>, List>)] input: T, - executor: &'a WgpuExecutor, -) -> List> { - input.convert(Footprint::DEFAULT, executor).await -} diff --git a/node-graph/nodes/gcore/src/animation.rs b/node-graph/nodes/gcore/src/animation.rs index 4182847d00..9bd9ef2993 100644 --- a/node-graph/nodes/gcore/src/animation.rs +++ b/node-graph/nodes/gcore/src/animation.rs @@ -1,6 +1,7 @@ +use core_types::gpoll::GPoll; use core_types::list::List; use core_types::transform::Footprint; -use core_types::{CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl}; +use core_types::{CacheHash, Color, Context, Ctx, DeriveCtx, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime}; use glam::{DAffine2, DVec2}; use graphic_types::vector_types::GradientStops; use graphic_types::{Artboard, Graphic, Vector}; @@ -61,8 +62,8 @@ fn animation_time( } #[node_macro::node(category("Debug"))] -async fn quantize_real_time( - ctx: impl Ctx + ExtractAll + CloneVarArgs, +fn quantize_real_time( + ctx: impl Ctx + ExtractRealTime + DeriveCtx, #[implementations( Context -> bool, Context -> u32, @@ -84,11 +85,11 @@ async fn quantize_real_time( Context -> List, Context -> (), )] - value: impl Node<'n, Context<'static>, Output = T>, + value: impl Node, Output = T>, #[default(1)] #[unit("sec")] quantum: f64, -) -> T { +) -> GPoll { let time = ctx.try_real_time().unwrap_or_default(); let time = time / 1000.; let mut quantized_time = (time * quantum.recip()).round() / quantum.recip(); @@ -96,13 +97,13 @@ async fn quantize_real_time( quantized_time = time; } let quantized_time = quantized_time * 1000.; - let new_context = OwnedContextImpl::from(ctx).with_real_time(quantized_time); - value.eval(Some(new_context.into())).await + let scope = ctx.scope().with_real_time(Some(quantized_time)); + value.eval(&ctx.with_scope(&scope)) } #[node_macro::node(category("Debug"))] -async fn quantize_animation_time( - ctx: impl Ctx + ExtractAll + CloneVarArgs, +fn quantize_animation_time( + ctx: impl Ctx + ExtractAnimationTime + DeriveCtx, #[implementations( Context -> bool, Context -> u32, @@ -124,18 +125,18 @@ async fn quantize_animation_time( Context -> List, Context -> (), )] - value: impl Node<'n, Context<'static>, Output = T>, + value: impl Node, Output = T>, #[default(1)] #[unit("sec")] quantum: f64, -) -> T { +) -> GPoll { let time = ctx.try_animation_time().unwrap_or_default(); let mut quantized_time = (time * quantum.recip()).round() / quantum.recip(); if !quantized_time.is_finite() { quantized_time = time; } - let new_context = OwnedContextImpl::from(ctx).with_animation_time(quantized_time); - value.eval(Some(new_context.into())).await + let scope = ctx.scope().with_animation_time(Some(quantized_time)); + value.eval(&ctx.with_scope(&scope)) } /// Produces the current position of the user's pointer within the document canvas. diff --git a/node-graph/nodes/gcore/src/context_modification.rs b/node-graph/nodes/gcore/src/context_modification.rs index aa8f72f1d4..bd36a0626e 100644 --- a/node-graph/nodes/gcore/src/context_modification.rs +++ b/node-graph/nodes/gcore/src/context_modification.rs @@ -1,5 +1,6 @@ use core::f64; -use core_types::context::{CloneVarArgs, Context, ContextFeatures, Ctx, ExtractAll}; +use core_types::context::{Context, ContextFeatures, Ctx, DeriveCtx}; +use core_types::gpoll::GPoll; use core_types::list::{AttributeDyn, AttributeValueDyn, List, ListDyn}; use core_types::transform::Footprint; use core_types::uuid::NodeId; @@ -12,8 +13,8 @@ use raster_types::{CPU, GPU, Raster}; /// Filters out what should be unused components of the context based on the specified requirements. /// This node is inserted by the compiler to "zero out" unused context components. #[node_macro::node(category(""))] -async fn context_modification( - ctx: impl Ctx + CloneVarArgs + ExtractAll, +fn context_modification( + ctx: impl Ctx + DeriveCtx, /// The data to pass through, evaluated with the stripped down context. #[implementations( Context -> (), @@ -41,13 +42,12 @@ async fn context_modification( Context -> AttributeValueDyn, Context -> ListDyn, )] - value: impl Node, Output = T>, + value: impl Node, Output = T>, /// The parts of the context to keep when evaluating the input value. All other parts are nullified. features_to_keep: ContextFeatures, -) -> T { - let new_context = OwnedContextImpl::from_flags(ctx, features_to_keep); - - value.eval(Some(new_context.into())).await +) -> GPoll { + let scope = ctx.scope().nullified(features_to_keep); + value.eval(&ctx.nullified(features_to_keep, &scope)) } #[cfg(test)] diff --git a/node-graph/nodes/gcore/src/memo.rs b/node-graph/nodes/gcore/src/memo.rs index cc5befe0e2..ddd44010b1 100644 --- a/node-graph/nodes/gcore/src/memo.rs +++ b/node-graph/nodes/gcore/src/memo.rs @@ -1,4 +1,4 @@ -use core_types::WasmNotSend; +use core_types::gpoll::Interrupt; use core_types::graphene_hash::CacheHash; use core_types::memo::*; use std::hash::DefaultHasher; @@ -10,7 +10,7 @@ use std::sync::Mutex; /// /// Stores the last evaluated data that flowed through this node and immediately returns that data on subsequent renders if the context has not changed. #[node_macro::node(category("General"), path(graphene_core::memo), skip_impl)] -async fn memoize(input: I, #[data] cache: Arc>>, content: impl Node) -> T { +fn memoize(input: I, #[data] cache: Arc>>, content: impl Node) -> Result { // Caches the output of a given node called with a specific input. // // A cache miss occurs when the Option is None. In this case, the node evaluates the inner node and memoizes (stores) the result. @@ -24,28 +24,31 @@ async fn memoize(input: I, #[d let hash = hasher.finish(); if let Some(data) = cache.lock().as_ref().unwrap().as_ref().and_then(|data| (data.0 == hash).then_some(data.1.clone())) { - return data; + return Ok(data); } - let value = content.eval(input).await; + let value = content.eval(input)?; *cache.lock().unwrap() = Some((hash, value.clone())); - value + Ok(value) } type MonitorValue = Arc>>>>; /// The Monitor node is used by the editor to access the data flowing through it. #[node_macro::node(category(""), path(graphene_core::memo), serialize(serialize_monitor), properties("monitor_properties"), skip_impl)] -async fn monitor( +fn monitor( input: I, #[allow(clippy::type_complexity)] #[data] io: MonitorValue, content: impl Node, -) -> T { - let output = content.eval(input.clone()).await; - *io.lock().unwrap() = Some(Arc::new(IORecord { input, output: output.clone() })); - output +) -> Result { + let output = content.eval(input)?; + *io.lock().unwrap() = Some(Arc::new(IORecord { + input: input.clone(), + output: output.clone(), + })); + Ok(output) } fn serialize_monitor(io: &MonitorValue) -> Option> { diff --git a/node-graph/nodes/gcore/src/ops.rs b/node-graph/nodes/gcore/src/ops.rs index 378d73569c..d42ea78d88 100644 --- a/node-graph/nodes/gcore/src/ops.rs +++ b/node-graph/nodes/gcore/src/ops.rs @@ -16,8 +16,8 @@ fn into<'i, T: 'i + Send + Into, O: 'i + Send>(_: impl Ctx, value: T, _out_ty } #[node_macro::node(category(""), skip_impl)] -async fn convert<'i, T: 'i + Send + Convert, O: 'i + Send, C: 'i + Send>(ctx: impl Ctx + ExtractFootprint, value: T, converter: C, _out_ty: PhantomData) -> O { - value.convert(*ctx.try_footprint().unwrap_or(&Footprint::DEFAULT), converter).await +fn convert, O: Send, C: Send>(ctx: impl Ctx + ExtractFootprint, value: T, converter: C, _out_ty: PhantomData) -> O { + value.convert(*ctx.try_footprint().unwrap_or(&Footprint::DEFAULT), converter) } #[cfg(test)] diff --git a/node-graph/nodes/graphic/src/artboard.rs b/node-graph/nodes/graphic/src/artboard.rs index 4bee951646..d4a9a28afc 100644 --- a/node-graph/nodes/graphic/src/artboard.rs +++ b/node-graph/nodes/graphic/src/artboard.rs @@ -1,6 +1,7 @@ +use core_types::gpoll::Interrupt; use core_types::list::{Item, List}; use core_types::transform::TransformMut; -use core_types::{ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_LOCATION, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; +use core_types::{ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_LOCATION, Color, Context, Ctx, DeriveCtx, ExtractFootprint}; use glam::{DAffine2, DVec2}; use graphic_types::graphic::{Graphic, IntoGraphicList}; use graphic_types::{Artboard, Vector}; @@ -9,8 +10,8 @@ use vector_types::GradientStops; /// Constructs a single-element `Artboard[]` with the given content and metadata stored as row attributes. #[node_macro::node(category(""))] -pub async fn create_artboard( - ctx: impl ExtractAll + CloneVarArgs + Ctx, +pub fn create_artboard( + ctx: impl Ctx + ExtractFootprint + DeriveCtx, /// Graphics to include within the artboard. #[implementations( Context -> List, @@ -22,7 +23,7 @@ pub async fn create_artboard( Context -> List, Context -> DAffine2, )] - content: impl Node, Output = T>, + content: impl Node, Output = T>, /// Coordinate of the top-left corner of the artboard within the document. location: DVec2, /// Width and height of the artboard within the document. @@ -32,14 +33,9 @@ pub async fn create_artboard( /// Whether to cut off the contained content that extends outside the artboard, or keep it visible. #[default(true)] clip: bool, -) -> List { - let footprint = ctx.try_footprint().copied(); - let mut new_ctx = OwnedContextImpl::from(ctx); - if let Some(mut footprint) = footprint { - footprint.translate(location); - new_ctx = new_ctx.with_footprint(footprint); - } - let content = content.eval(new_ctx.into_context()).await.into_graphic_list(); +) -> Result, Interrupt> { + let translated = ctx.modify_footprint(|footprint| footprint.translate(location)); + let content = content.eval(&translated.ctx())?.into_graphic_list(); // Normalize so `location` is the top-left corner and `dimensions` are positive (allowing negative input // dimensions to represent dragging from the opposite corner). Compute the corner using the raw signed @@ -50,11 +46,11 @@ pub async fn create_artboard( let background = background.element(0).copied().unwrap_or(Color::WHITE); // Name is not stored here, it's resolved live from the parent layer's display name - List::new_from_item( + Ok(List::new_from_item( Item::new_from_element(Artboard::new(content)) .with_attribute(ATTR_LOCATION, normalized_location) .with_attribute(ATTR_DIMENSIONS, normalized_dimensions) .with_attribute(ATTR_BACKGROUND, background) .with_attribute(ATTR_CLIP, clip), - ) + )) } diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 93ab6a4079..8698375f6b 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -2,7 +2,8 @@ use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::list::{AttributeDyn, AttributeValueDyn, Item, List, ListDyn}; use core_types::registry::types::{Angle, SignedInteger}; use core_types::uuid::NodeId; -use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, AnyHash, BlendMode, CacheHash, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; +use core_types::gpoll::Interrupt; +use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, AnyHash, BlendMode, CacheHash, Color, Context, Ctx, DeriveCtx}; use glam::{DAffine2, DVec2}; use graphic_types::graphic::{Graphic, IntoGraphicList}; use graphic_types::{Artboard, Vector}; @@ -108,8 +109,8 @@ pub fn extract_element( } #[node_macro::node(category("General"))] -async fn map( - ctx: impl Ctx + CloneVarArgs + ExtractAll, +fn map( + ctx: impl Ctx + DeriveCtx, #[implementations( List, List, @@ -127,23 +128,24 @@ async fn map( Context -> List, Context -> List, )] - mapped: impl Node, Output = List>, -) -> List { + mapped: impl Node, Output = List>, +) -> Result, Interrupt> { + let spilled = ctx.index_head(); let mut rows = List::new(); for (i, row) in content.into_iter().enumerate() { - let owned_ctx = OwnedContextImpl::from(ctx.clone()); - let owned_ctx = owned_ctx.with_vararg(Box::new(List::new_from_item(row))).with_index(i); - let list = mapped.eval(owned_ctx.into_context()).await; + let item = List::new_from_item(row); + let scoped = ctx.push_vararg(&item); + let list = mapped.eval(&scoped.ctx().promoted(&spilled, i as u64))?; rows.extend(list); } - rows + Ok(rows) } #[node_macro::node(category("General"))] -async fn mirror( +fn mirror( _: impl Ctx, #[implementations( List, @@ -229,8 +231,8 @@ pub fn path_of_subgraph(_: impl Ctx, node_path: List) -> List { /// The value is type-erased into an `AttributeValueDyn` by an auto-inserted convert node, so this node only /// monomorphizes over `T` instead of the cartesian product `(T, U)`. #[node_macro::node(category("Attributes: Write"))] -async fn write_attribute( - ctx: impl ExtractAll + CloneVarArgs + Ctx, +fn write_attribute( + ctx: impl Ctx + DeriveCtx, /// The `List` to set the named attribute on (one value per item). #[implementations( List, @@ -252,15 +254,17 @@ async fn write_attribute( name: String, /// The node that produces the attribute value for each item. Called once per item with the item's index in context. #[implementations(Context -> AttributeValueDyn)] - value: impl Node<'n, Context<'static>, Output = AttributeValueDyn>, -) -> List { + value: impl Node, Output = AttributeValueDyn>, +) -> Result, Interrupt> { + let spilled = ctx.index_head(); for index in 0..content.len() { let row = content.clone_item(index).expect("index is within bounds"); - let owned_ctx = OwnedContextImpl::from(ctx.clone()).with_vararg(Box::new(List::new_from_item(row))).with_index(index); - let v = value.eval(owned_ctx.into_context()).await; + let item = List::new_from_item(row); + let scoped = ctx.push_vararg(&item); + let v = value.eval(&scoped.ctx().promoted(&spilled, index as u64))?; content.set_attribute_value_dyn(&name, index, v); } - content + Ok(content) } /// Sets a named attribute on the primary list, with each value taken from the corresponding item's element in the source list (paired by index, wrapping if the source has fewer items). @@ -497,7 +501,7 @@ fn read_attribute_raster( /// Joins two `List`s of the same type, extending the base `List` with the items from the new `List`. #[node_macro::node(category("General"))] -pub async fn extend( +pub fn extend( _: impl Ctx, /// The `List` whose items will appear at the start of the extended `List`. #[implementations(List, List, List, List, List>, List>, List, List)] @@ -517,7 +521,7 @@ pub async fn extend( /// Performs an obsolete function as part of a migration from an older document format. /// Users are advised to delete this node and replace it with a new one. #[node_macro::node(category(""))] -pub async fn legacy_layer_extend( +pub fn legacy_layer_extend( _: impl Ctx, #[implementations(List, List, List, List, List>, List>, List, List)] base: List, #[expose] @@ -544,7 +548,7 @@ pub async fn legacy_layer_extend( /// Nests the input graphical content in a wrapper graphic. This essentially "groups" the input. /// The inverse of this node is 'Flatten Graphic'. #[node_macro::node(category("General"))] -pub async fn wrap_graphic + 'n>( +pub fn wrap_graphic>( _: impl Ctx, #[implementations( List, diff --git a/node-graph/nodes/gstd/src/render_cache.rs b/node-graph/nodes/gstd/src/render_cache.rs index 43e84aab2b..c21377e6a2 100644 --- a/node-graph/nodes/gstd/src/render_cache.rs +++ b/node-graph/nodes/gstd/src/render_cache.rs @@ -325,7 +325,7 @@ pub async fn render_output_cache<'a: 'n>( ctx: impl Ctx + ExtractAll + CloneVarArgs + ExtractRealTime + ExtractAnimationTime + ExtractPointerPosition + Sync, #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<&'a WgpuExecutor>, #[scope(crate::platform_application_io::editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi, - data: impl Node, Output = RenderOutput> + Send + Sync, + data: impl Node, Output = RenderOutput> + Send + Sync, #[data] tile_cache: TileCache, ) -> RenderOutput { let footprint = ctx.footprint(); @@ -409,7 +409,7 @@ async fn render_missing_region( viewport_origin_offset: &DVec2, ) -> CachedRegion where - F: Fn(Context<'static>) -> Fut, + F: Fn(Context<'_>) -> Fut, Fut: std::future::Future, { let min_tile = region.tiles.iter().fold(IVec2::new(i32::MAX, i32::MAX), |acc, t| acc.min(IVec2::new(t.x, t.y))); diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index dcd65a368a..f53aa69fdc 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -34,7 +34,7 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + Context -> List, Context -> List, )] - data: impl Node, Output = T>, + data: impl Node, Output = T>, ) -> RenderIntermediate { let render_params = ctx .vararg(0) @@ -147,7 +147,7 @@ async fn render<'a: 'n>( async fn create_context<'a: 'n>( // Context injections are defined in the wrap_network_in_scope function render_config: RenderConfig, - data: impl Node, Output = RenderOutput>, + data: impl Node, Output = RenderOutput>, ) -> RenderOutput { let render_output_type = match render_config.export_format { ExportFormat::Svg => RenderOutputTypeRequest::Svg, diff --git a/node-graph/nodes/gstd/src/render_pixel_preview.rs b/node-graph/nodes/gstd/src/render_pixel_preview.rs index f668ed8a37..549633362d 100644 --- a/node-graph/nodes/gstd/src/render_pixel_preview.rs +++ b/node-graph/nodes/gstd/src/render_pixel_preview.rs @@ -11,7 +11,7 @@ use wgpu_executor::{AsyncWgpuPipeline, WgpuExecutor, WgpuPipelineCache}; pub async fn render_pixel_preview<'a: 'n>( ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync, #[scope(pixel_preview_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache, - data: impl Node, Output = RenderOutput> + Send + Sync, + data: impl Node, Output = RenderOutput> + Send + Sync, ) -> RenderOutput { let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::()).cloned() else { log::error!("invalid render params for pixel preview"); diff --git a/node-graph/nodes/math/src/lib.rs b/node-graph/nodes/math/src/lib.rs index 8e7f9785c1..14e48a738d 100644 --- a/node-graph/nodes/math/src/lib.rs +++ b/node-graph/nodes/math/src/lib.rs @@ -1,4 +1,5 @@ use core_types::Context; +use core_types::gpoll::GPoll; use core_types::list::List; use core_types::registry::types::{Fraction, Percentage, PixelSize}; use core_types::transform::Footprint; @@ -740,7 +741,7 @@ fn logical_not( /// Evaluates either the "If True" or "If False" input branch based on whether the input condition is true or false. #[node_macro::node(category("Math: Logic"))] -async fn switch( +fn switch( #[implementations(Context)] ctx: C, condition: bool, #[expose] @@ -781,8 +782,8 @@ async fn switch( Context -> List, )] if_false: impl Node, -) -> T { - if condition { if_true.eval(ctx).await } else { if_false.eval(ctx).await } +) -> GPoll { + if condition { if_true.eval(ctx) } else { if_false.eval(ctx) } } /// Constructs a bool value which may be set to true or false. @@ -995,36 +996,36 @@ mod test { pub fn dot_product_function() { let vector_a = DVec2::new(1., 2.); let vector_b = DVec2::new(3., 4.); - assert_eq!(dot_product((), vector_a, vector_b, false), 11.); + assert_eq!(dot_product(&(), vector_a, vector_b, false), 11.); } #[test] pub fn length_function() { let vector = DVec2::new(3., 4.); - assert_eq!(length((), vector), 5.); + assert_eq!(length(&(), vector), 5.); } #[test] fn test_basic_expression() { - let result = math((), 0., "2 + 2".to_string(), 0.); + let result = math(&(), 0., "2 + 2".to_string(), 0.); assert_eq!(result, 4.); } #[test] fn test_complex_expression() { - let result = math((), 0., "(5 * 3) + (10 / 2)".to_string(), 0.); + let result = math(&(), 0., "(5 * 3) + (10 / 2)".to_string(), 0.); assert_eq!(result, 20.); } #[test] fn test_default_expression() { - let result = math((), 0., "0".to_string(), 0.); + let result = math(&(), 0., "0".to_string(), 0.); assert_eq!(result, 0.); } #[test] fn test_invalid_expression() { - let result = math((), 0., "invalid".to_string(), 0.); + let result = math(&(), 0., "invalid".to_string(), 0.); assert_eq!(result, 0.); } @@ -1036,26 +1037,228 @@ mod test { #[test] pub fn add_vectors() { - assert_eq!(super::add((), DVec2::ONE, DVec2::ONE), DVec2::ONE * 2.); + assert_eq!(super::add(&(), DVec2::ONE, DVec2::ONE), DVec2::ONE * 2.); } #[test] pub fn subtract_f64() { - assert_eq!(super::subtract((), 5_f64, 3_f64), 2.); + assert_eq!(super::subtract(&(), 5_f64, 3_f64), 2.); } #[test] pub fn divide_vectors() { - assert_eq!(super::divide((), DVec2::ONE, 2_f64), DVec2::ONE / 2.); + assert_eq!(super::divide(&(), DVec2::ONE, 2_f64), DVec2::ONE / 2.); } #[test] pub fn modulo_positive() { - assert_eq!(super::modulo((), -5_f64, 2_f64, true), 1_f64); + assert_eq!(super::modulo(&(), -5_f64, 2_f64, true), 1_f64); } #[test] pub fn modulo_negative() { - assert_eq!(super::modulo((), -5_f64, 2_f64, false), -1_f64); + assert_eq!(super::modulo(&(), -5_f64, 2_f64, false), -1_f64); + } +} + +#[cfg(test)] +mod graphene_test { + use super::*; + use core_types::arena::Arena; + use core_types::context::{ContextImpl, EvalScope, ExtractIndex}; + use core_types::gnode::{BatchStatus, GNode}; + use core_types::gpoll::{Finality, GPoll}; + use core_types::wire::{EdgeHandle, ErasedGNode, resolve_and_wire}; + use std::mem::MaybeUninit; + + struct SourceNode(T); + + impl GNode for SourceNode { + type Output = T; + + fn eval(&self, _input: &Input) -> GPoll { + GPoll::Final(self.0.clone()) + } + } + + struct IndexNode; + + impl GNode for IndexNode { + type Output = f64; + + fn eval(&self, input: &Input) -> GPoll { + GPoll::Final(input.innermost_index() as f64) + } + } + + fn scope_fixture(arena: &Arena) -> EvalScope<'_> { + EvalScope::new(None, None, None, &[], arena) + } + + #[test] + fn generated_add_evaluates_through_the_gnode_path() { + let arena = Arena::new(64); + let scope = scope_fixture(&arena); + let ctx = ContextImpl::root(&scope); + + let graph = AddNode::new(SourceNode(1.0f64), SourceNode(2.0f64)); + assert_eq!(GNode::eval(&graph, &ctx), GPoll::Final(3.0)); + } + + #[test] + fn generated_add_batches_through_the_erased_edge() { + let arena = Arena::new(64); + let scope = scope_fixture(&arena); + let ctx = ContextImpl::root(&scope); + + let erased: Box> = Box::new(AddNode::new(IndexNode, SourceNode(10.0f64))); + let mut scratch = [const { MaybeUninit::uninit() }; 4]; + let status = erased.eval_batch(&ctx, 2..6, Some(&mut scratch)); + let BatchStatus::Filled(lanes, finality) = status else { + panic!("expected filled, got {status:?}"); + }; + assert_eq!(lanes, &[12.0, 13.0, 14.0, 15.0]); + assert_eq!(finality, Finality::AllFinal); + } + + #[test] + fn generated_wire_constructor_resolves_and_wires() { + let arena = Arena::new(64); + let scope = scope_fixture(&arena); + let ctx = ContextImpl::root(&scope); + + let entries = logical_or_entries(); + let value = EdgeHandle::new(Box::new(SourceNode(true)) as Box>); + let other_value = EdgeHandle::new(Box::new(SourceNode(false)) as Box>); + let wired = resolve_and_wire(&entries[0], vec![value, other_value]).unwrap().downcast::().unwrap(); + + assert_eq!(GNode::eval(&wired, &ctx), GPoll::Final(true)); + } + + #[test] + fn generic_add_registers_one_entry_per_implementation() { + let arena = Arena::new(64); + let scope = scope_fixture(&arena); + let ctx = ContextImpl::root(&scope); + + let entries = add_entries(); + assert_eq!(entries.len(), 6); + assert_eq!(entries[0].io.inputs, vec![core_types::concrete!(f64), core_types::concrete!(f64)]); + assert_eq!(entries[0].io.output, core_types::concrete!(f64)); + assert_eq!(entries[3].io.inputs, vec![core_types::concrete!(DVec2), core_types::concrete!(DVec2)]); + assert_eq!(entries[3].io.output, core_types::concrete!(DVec2)); + + let augend = EdgeHandle::new(Box::new(SourceNode(1.5f64)) as Box>); + let addend = EdgeHandle::new(Box::new(SourceNode(2.5f64)) as Box>); + let wired = resolve_and_wire(&entries[0], vec![augend, addend]).unwrap().downcast::().unwrap(); + + assert_eq!(GNode::eval(&wired, &ctx), GPoll::Final(4.0)); + } + + #[test] + fn converted_switch_evaluates_only_the_taken_branch() { + use std::sync::Arc; + use std::sync::atomic::{AtomicU32, Ordering}; + + struct CountingSource(Arc, f64); + + impl GNode for CountingSource { + type Output = f64; + + fn eval(&self, _input: &Input) -> GPoll { + self.0.fetch_add(1, Ordering::Relaxed); + GPoll::Final(self.1) + } + } + + let arena = Arena::new(64); + let scope = scope_fixture(&arena); + let ctx = ContextImpl::root(&scope); + + let taken = Arc::new(AtomicU32::new(0)); + let untaken = Arc::new(AtomicU32::new(0)); + let graph = SwitchNode::new(SourceNode(true), CountingSource(taken.clone(), 1.0), CountingSource(untaken.clone(), 2.0)); + + assert_eq!(GNode::eval(&graph, &ctx), GPoll::Final(1.0)); + assert_eq!(taken.load(Ordering::Relaxed), 1); + assert_eq!(untaken.load(Ordering::Relaxed), 0); + } + + #[test] + fn converted_switch_passes_branch_status_through() { + struct PendingSource; + + impl GNode for PendingSource { + type Output = f64; + + fn eval(&self, _input: &Input) -> GPoll { + GPoll::Pending + } + } + + struct PartialSource; + + impl GNode for PartialSource { + type Output = f64; + + fn eval(&self, _input: &Input) -> GPoll { + GPoll::Partial(7.0) + } + } + + let arena = Arena::new(64); + let scope = scope_fixture(&arena); + let ctx = ContextImpl::root(&scope); + + let pending = SwitchNode::new(SourceNode(true), PendingSource, PartialSource); + assert_eq!(GNode::eval(&pending, &ctx), GPoll::Pending); + + let partial = SwitchNode::new(SourceNode(false), PendingSource, PartialSource); + assert_eq!(GNode::eval(&partial, &ctx), GPoll::Partial(7.0)); + } + + #[test] + fn converted_switch_merges_condition_status_into_the_branch_result() { + struct PartialCondition; + + impl GNode for PartialCondition { + type Output = bool; + + fn eval(&self, _input: &Input) -> GPoll { + GPoll::Partial(true) + } + } + + let arena = Arena::new(64); + let scope = scope_fixture(&arena); + let ctx = ContextImpl::root(&scope); + + let graph = SwitchNode::new(PartialCondition, SourceNode(1.0f64), SourceNode(2.0f64)); + assert_eq!(GNode::eval(&graph, &ctx), GPoll::Partial(1.0)); + } + + #[test] + fn generated_eval_computes_on_stand_in_and_traces_fallback() { + struct FallbackNode; + + impl GNode for FallbackNode { + type Output = f64; + + fn eval(&self, _input: &Input) -> GPoll { + GPoll::fallback(0.0, "upstream failed") + } + } + + let arena = Arena::new(64); + let scope = scope_fixture(&arena); + let ctx = ContextImpl::root(&scope); + + let graph = AddNode::new(FallbackNode, SourceNode(5.0f64)); + let GPoll::Fallback(boxed) = GNode::eval(&graph, &ctx) else { + panic!("fallback must propagate with the computed stand-in"); + }; + assert_eq!(boxed.0, 5.0); + assert!(boxed.1.kind == "upstream failed"); + assert_eq!(boxed.1.trace, vec![0]); } } diff --git a/node-graph/nodes/raster/src/std_nodes.rs b/node-graph/nodes/raster/src/std_nodes.rs index 6ff551843d..3556b2ef26 100644 --- a/node-graph/nodes/raster/src/std_nodes.rs +++ b/node-graph/nodes/raster/src/std_nodes.rs @@ -241,7 +241,7 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: List>, bounds: DAf let image_data = &row.element().data; let (image_width, image_height) = (row.element().width, row.element().height); if image_width == 0 || image_height == 0 { - return empty_image((), bounds, List::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap(); + return empty_image(&(), bounds, List::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap(); } let orig_image_scale = DVec2::new(image_width as f64, image_height as f64); @@ -290,7 +290,7 @@ pub fn empty_image(_: impl Ctx, transform: DAffine2, color: List) -> List } #[node_macro::node(category(""))] -pub fn image<'a: 'n>(_: impl Ctx, resource: Resource) -> List> { +pub fn image(_: impl Ctx, resource: Resource) -> List> { let image_data = resource.as_ref(); let Some(image) = ::image::load_from_memory(image_data).ok() else { diff --git a/node-graph/nodes/repeat/src/repeat_nodes.rs b/node-graph/nodes/repeat/src/repeat_nodes.rs index a4137dc978..d55ac22a6b 100644 --- a/node-graph/nodes/repeat/src/repeat_nodes.rs +++ b/node-graph/nodes/repeat/src/repeat_nodes.rs @@ -1,16 +1,17 @@ use crate::gcore::Context; use core::f64::consts::TAU; +use core_types::gpoll::Interrupt; use core_types::list::List; use core_types::registry::types::{Angle, PixelSize}; -use core_types::{ATTR_TRANSFORM, CloneVarArgs, Color, Ctx, ExtractAll, InjectVarArgs, OwnedContextImpl}; +use core_types::{ATTR_TRANSFORM, Color, Ctx, DeriveCtx, InjectVarArgs}; use glam::{DAffine2, DVec2}; use graphic_types::{Graphic, Vector}; use raster_types::{CPU, Raster}; use vector_types::GradientStops; #[node_macro::node(category("Repeat"))] -async fn repeat + Default + Send + Clone + 'static>( - ctx: impl ExtractAll + CloneVarArgs + Ctx, +fn repeat + Default + Send + Clone + 'static>( + ctx: impl Ctx + DeriveCtx, #[implementations( Context -> List, Context -> List, @@ -18,35 +19,35 @@ async fn repeat + Default + Send + Clone + 'static>( Context -> List, Context -> List, )] - content: impl Node<'n, Context<'static>, Output = List>, + content: impl Node, Output = List>, #[default(1)] #[hard(1..)] count: u32, reverse: bool, -) -> List { +) -> Result, Interrupt> { // Someday this node can have the option to generate infinitely instead of a fixed count (basically `std::iter::repeat`). - let count = count as usize; + let count = count as u64; + let spilled = ctx.index_head(); let mut result_list = List::new(); for index in 0..count { let index = if reverse { count - index - 1 } else { index }; - let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index); - let generated_content = content.eval(new_ctx.into_context()).await; + let generated_content = content.eval(&ctx.promoted(&spilled, index))?; for generated_row in generated_content.into_iter() { result_list.push(generated_row); } } - result_list + Ok(result_list) } #[node_macro::node(category("Repeat"))] -pub async fn repeat_array + Default + Send + Clone + 'static>( - ctx: impl ExtractAll + CloneVarArgs + Ctx, +pub fn repeat_array + Default + Send + Clone + 'static>( + ctx: impl Ctx + DeriveCtx, #[implementations( Context -> List, Context -> List, @@ -54,7 +55,7 @@ pub async fn repeat_array + Default + Send + Clone + 'static>( Context -> List, Context -> List, )] - content: impl Node<'n, Context<'static>, Output = List>, + content: impl Node, Output = List>, #[default(100., 100.)] // TODO: When using a custom Properties panel layout in document_node_definitions.rs and this default is set, the widget weirdly doesn't show up in the Properties panel. Investigation is needed. direction: PixelSize, @@ -62,10 +63,11 @@ pub async fn repeat_array + Default + Send + Clone + 'static>( #[default(5)] #[hard(1..)] count: u32, -) -> List { +) -> Result, Interrupt> { let angle = angle.to_radians(); // A single copy has no steps between copies, so the denominator is kept at 1 to avoid `0. / 0.` producing a NaN transform let total = (count - 1).max(1) as f64; + let spilled = ctx.index_head(); let mut result_list = List::new(); @@ -74,8 +76,7 @@ pub async fn repeat_array + Default + Send + Clone + 'static>( let translation = index as f64 * direction / total; let transform = DAffine2::from_angle(angle) * DAffine2::from_translation(translation); - let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index as usize); - let generated_content = content.eval(new_ctx.into_context()).await; + let generated_content = content.eval(&ctx.promoted(&spilled, index as u64))?; for row_index in 0..generated_content.len() { let Some(mut row) = generated_content.clone_item(row_index) else { continue }; @@ -89,12 +90,12 @@ pub async fn repeat_array + Default + Send + Clone + 'static>( } } - result_list + Ok(result_list) } #[node_macro::node(category("Repeat"))] -async fn repeat_radial + Default + Send + Clone + 'static>( - ctx: impl ExtractAll + CloneVarArgs + Ctx, +fn repeat_radial + Default + Send + Clone + 'static>( + ctx: impl Ctx + DeriveCtx, #[implementations( Context -> List, Context -> List, @@ -102,7 +103,7 @@ async fn repeat_radial + Default + Send + Clone + 'static>( Context -> List, Context -> List, )] - content: impl Node<'n, Context<'static>, Output = List>, + content: impl Node, Output = List>, start_angle: Angle, #[unit(" px")] #[default(5)] @@ -110,7 +111,8 @@ async fn repeat_radial + Default + Send + Clone + 'static>( #[default(5)] #[hard(1..)] count: u32, -) -> List { +) -> Result, Interrupt> { + let spilled = ctx.index_head(); let mut result_list = List::new(); for index in 0..count { @@ -118,8 +120,7 @@ async fn repeat_radial + Default + Send + Clone + 'static>( let translation = DAffine2::from_translation(radius * DVec2::Y); let transform = angle * translation; - let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index as usize); - let generated_content = content.eval(new_ctx.into_context()).await; + let generated_content = content.eval(&ctx.promoted(&spilled, index as u64))?; for row_index in 0..generated_content.len() { let Some(mut row) = generated_content.clone_item(row_index) else { continue }; @@ -133,12 +134,12 @@ async fn repeat_radial + Default + Send + Clone + 'static>( } } - result_list + Ok(result_list) } #[node_macro::node(category("Repeat"), name("Repeat on Points"))] -async fn repeat_on_points + Default + Send + Clone + 'static>( - ctx: impl ExtractAll + CloneVarArgs + Sync + Ctx + InjectVarArgs, +fn repeat_on_points + Default + Send + Clone + 'static>( + ctx: impl Ctx + DeriveCtx + InjectVarArgs, points: List, #[implementations( Context -> List, @@ -147,40 +148,36 @@ async fn repeat_on_points + Default + Send + Clone + 'static>( Context -> List, Context -> List, )] - content: impl Node<'n, Context<'static>, Output = List>, + content: impl Node, Output = List>, reverse: bool, -) -> List { +) -> Result, Interrupt> { + let spilled = ctx.index_head(); let mut result_list = List::new(); for points_index in 0..points.len() { let Some(points_element) = points.element(points_index) else { continue }; let transform: DAffine2 = points.attribute_cloned_or_default(ATTR_TRANSFORM, points_index); - let mut iteration = async |index, point| { + let positions = points_element.point_domain.positions(); + let range: Box> = match reverse { + true => Box::new(positions.iter().enumerate().rev()), + false => Box::new(positions.iter().enumerate()), + }; + + for (index, &point) in range { let transformed_point = transform.transform_point2(point); - let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_position(transformed_point); - let generated_content = content.eval(new_ctx.into_context()).await; + let scoped = ctx.push_position(transformed_point); + let generated_content = content.eval(&scoped.ctx().promoted(&spilled, index as u64))?; for mut generated_row in generated_content.into_iter() { generated_row.attribute_mut_or_insert_default::(ATTR_TRANSFORM).translation = transformed_point; result_list.push(generated_row); } - }; - - let range = points_element.point_domain.positions().iter().enumerate(); - if reverse { - for (index, &point) in range.rev() { - iteration(index, point).await; - } - } else { - for (index, &point) in range { - iteration(index, point).await; - } } } - result_list + Ok(result_list) } #[cfg(test)] diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index 489ce33d54..5a8ae12eb2 100644 --- a/node-graph/nodes/text/src/lib.rs +++ b/node-graph/nodes/text/src/lib.rs @@ -7,10 +7,11 @@ mod text_context; mod to_path; use convert_case::{Boundary, Converter, pattern}; +use core_types::gpoll::Interrupt; use core_types::graphene_hash::CacheHash; use core_types::list::{Item, List}; use core_types::registry::types::{SignedInteger, TextArea}; -use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractVarArgs, OwnedContextImpl}; +use core_types::{Context, Ctx, DeriveCtx, ExtractVarArgs}; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; use unicode_segmentation::UnicodeSegmentation; @@ -768,25 +769,25 @@ fn string_join( /// Iterates over a list of strings, evaluating the mapped operation for each one. Use the **Read String** node to access the current string inside the loop. #[node_macro::node(category("Text"))] -async fn map_string( - ctx: impl Ctx + CloneVarArgs + ExtractAll, +fn map_string( + ctx: impl Ctx + DeriveCtx, strings: List, #[expose] #[implementations(Context -> String)] - mapped: impl Node, Output = String>, -) -> List { + mapped: impl Node, Output = String>, +) -> Result, Interrupt> { + let spilled = ctx.index_head(); let mut result = List::new(); for (i, row) in strings.into_iter().enumerate() { let string = row.into_element(); - let owned_ctx = OwnedContextImpl::from(ctx.clone()); - let owned_ctx = owned_ctx.with_vararg(Box::new(string)).with_index(i); - let mapped_string = mapped.eval(owned_ctx.into_context()).await; + let scoped = ctx.push_vararg(&string); + let mapped_string = mapped.eval(&scoped.ctx().promoted(&spilled, i as u64))?; result.push(Item::new_from_element(mapped_string)); } - result + Ok(result) } /// Reads the current string from within a **Map String** node's loop. diff --git a/node-graph/nodes/transform/src/transform_nodes.rs b/node-graph/nodes/transform/src/transform_nodes.rs index 8ed04f2cfe..ee85f409a2 100644 --- a/node-graph/nodes/transform/src/transform_nodes.rs +++ b/node-graph/nodes/transform/src/transform_nodes.rs @@ -2,7 +2,8 @@ use core::f64; use core_types::color::Color; use core_types::list::{List, ListDyn}; use core_types::transform::{ApplyTransform, ScaleType, Transform}; -use core_types::{ATTR_TRANSFORM, CloneVarArgs, Context, Ctx, ExtractAll, InjectFootprint, ModifyFootprint, OwnedContextImpl}; +use core_types::gpoll::Interrupt; +use core_types::{ATTR_TRANSFORM, Context, Ctx, DeriveCtx, ExtractFootprint, InjectFootprint, ModifyFootprint}; use glam::{DAffine2, DMat2, DVec2}; use graphic_types::Graphic; use graphic_types::Vector; @@ -11,8 +12,8 @@ use vector_types::GradientStops; /// Applies the specified transform to the input value, which may be a graphic type or another transform. #[node_macro::node(category("Math: Transform"))] -async fn transform( - ctx: impl Ctx + CloneVarArgs + ExtractAll + ModifyFootprint, +fn transform( + ctx: impl Ctx + ExtractFootprint + DeriveCtx + ModifyFootprint, #[implementations( Context -> DAffine2, Context -> DVec2, @@ -24,31 +25,24 @@ async fn transform( Context -> List, Context -> List, )] - content: impl Node, Output = T>, + content: impl Node, Output = T>, #[widget(ParsedWidgetOverride::Custom = "transform_translation")] translation: DVec2, #[widget(ParsedWidgetOverride::Custom = "transform_rotation")] rotation: f64, #[widget(ParsedWidgetOverride::Custom = "transform_scale")] #[default(1., 1.)] scale: DVec2, #[widget(ParsedWidgetOverride::Custom = "transform_skew")] skew: DVec2, -) -> T { +) -> Result { let trs = DAffine2::from_scale_angle_translation(scale, rotation.to_radians(), translation); let skew = DAffine2::from_cols_array(&[1., skew.y.to_radians().tan(), skew.x.to_radians().tan(), 1., 0., 0.]); let matrix = trs * skew; - let footprint = ctx.try_footprint().copied(); - - let mut ctx = OwnedContextImpl::from(ctx); - if let Some(mut footprint) = footprint { - footprint.apply_transform(&matrix); - ctx = ctx.with_footprint(footprint); - } - - let mut transform_target = content.eval(ctx.into_context()).await; + let transformed = ctx.modify_footprint(|footprint| footprint.apply_transform(&matrix)); + let mut transform_target = content.eval(&transformed.ctx())?; transform_target.left_apply_transform(&matrix); - transform_target + Ok(transform_target) } /// Resets the desired components of the input transform to their default values. If all components are reset, the output will be set to the identity transform. diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 3b08ac33d1..113d38c94b 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -7,9 +7,10 @@ use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List, use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue}; use core_types::transform::{Footprint, Transform}; use core_types::uuid::NodeId; +use core_types::gpoll::Interrupt; use core_types::{ - ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, CloneVarArgs, - Color, Context, Ctx, ExtractAll, OwnedContextImpl, + ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color, Context, + Ctx, DeriveCtx, }; use glam::{DAffine2, DMat2, DVec2}; use graphic_types::Vector; @@ -115,7 +116,7 @@ async fn assign_colors( repeat_every: u32, ) -> T where - T: VectorListIterMut + 'n + Send, + T: VectorListIterMut+ Send, { let Some(row) = gradient.into_iter().next() else { return content }; @@ -156,7 +157,7 @@ where /// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry. #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))] -async fn fill( +async fn fill( _: impl Ctx, /// The content with vector paths to apply the fill style to. #[implementations( @@ -251,7 +252,7 @@ impl IntoF64Vec for String { /// Applies a stroke style to the vector content, giving an appearance to the area within the outline of the geometry. #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))] -async fn stroke( +async fn stroke( _: impl Ctx, /// The content with vector paths to apply the stroke style to. #[implementations( @@ -323,7 +324,7 @@ async fn stroke( dash_offset: f64, ) -> List where - List: VectorListIterMut + 'n + Send, + List: VectorListIterMut+ Send, { let dash_lengths = dash_lengths.into_vec().into_iter().map(|length| length.max(0.)).collect(); @@ -356,7 +357,7 @@ where } #[node_macro::node(name("Copy to Points"), category("Repeat"), path(core_types::vector))] -async fn copy_to_points( +async fn copy_to_points( _: impl Ctx, points: List, /// Artwork to be copied and placed at each point. @@ -870,7 +871,7 @@ fn bilinear_interpolate(t: DVec2, quad: &[DVec2; 4]) -> DVec2 { } #[node_macro::node(category("Vector"), path(graphene_core::vector))] -async fn pack_strips( +async fn pack_strips( _: impl Ctx, #[implementations( List, @@ -1412,20 +1413,20 @@ async fn path_is_closed( } #[node_macro::node(category("Vector"), path(graphene_core::vector))] -async fn map_points(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: List, mapped: impl Node, Output = DVec2>) -> List { +fn map_points(ctx: impl Ctx + DeriveCtx, content: List, mapped: impl Node, Output = DVec2>) -> Result, Interrupt> { + let spilled = ctx.index_head(); let mut content = content; let mut index = 0; for vector in content.iter_element_values_mut() { for (_, position) in vector.point_domain.positions_mut() { - let owned_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_position(*position); + let scoped = ctx.push_position(*position); + *position = mapped.eval(&scoped.ctx().promoted(&spilled, index))?; index += 1; - - *position = mapped.eval(owned_ctx.into_context()).await; } } - content + Ok(content) } // TODO: Rename to "Combine Paths" and make this happen per-element instead of flattening every element into a single path. The migration for this should then become a Flatten Vector -> Combine Paths pair of nodes. @@ -3189,26 +3190,24 @@ async fn path_length(_: impl Ctx, source: List) -> f64 { } #[node_macro::node(category("Vector: Measure"), path(core_types::vector))] -async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node, Output = List>) -> f64 { - let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context(); - let vector = content.eval(new_ctx).await; +fn area(ctx: impl Ctx + DeriveCtx, content: impl Node, Output = List>) -> Result { + let vector = content.eval(&ctx.with_footprint(&Footprint::DEFAULT))?; - (0..vector.len()) + Ok((0..vector.len()) .map(|index| { let transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM, index); let area_scale = transform.matrix2.determinant().abs(); vector.element(index).unwrap().stroke_bezpath_iter().map(|subpath| subpath.area() * area_scale).sum::() }) - .sum() + .sum()) } #[node_macro::node(category("Vector: Measure"), path(core_types::vector))] -async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node, Output = List>, centroid_type: CentroidType) -> DVec2 { - let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context(); - let vector = content.eval(new_ctx).await; +fn centroid(ctx: impl Ctx + DeriveCtx, content: impl Node, Output = List>, centroid_type: CentroidType) -> Result { + let vector = content.eval(&ctx.with_footprint(&Footprint::DEFAULT))?; if vector.is_empty() { - return DVec2::ZERO; + return Ok(DVec2::ZERO); } // All subpath centroid positions added together as if they were vectors from the origin. @@ -3234,7 +3233,7 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node< } if sum > 0. { - centroid / sum + Ok(centroid / sum) } // Without a summed denominator, return the average of all positions instead else { @@ -3255,7 +3254,7 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node< .inspect(|_| count += 1) .sum::(); - if count != 0 { summed_positions / (count as f64) } else { DVec2::ZERO } + if count != 0 { Ok(summed_positions / (count as f64)) } else { Ok(DVec2::ZERO) } } }