Start adapting nodes to new version

This commit is contained in:
Dennis Kobert
2026-07-26 20:59:12 +00:00
parent 44277c9636
commit f0f7d6a5d7
16 changed files with 380 additions and 357 deletions
@@ -38,120 +38,17 @@ fn upload_to_texture(device: &wgpu::Device, queue: &wgpu::Queue, image: &Raster<
) )
} }
/// Converts a Raster<GPU> texture to Raster<CPU> 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<GPU>) -> 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<Raster<CPU>, 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<Color> = 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<Color>` 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 /// Passthrough conversion for GPU `List`s - no conversion needed
impl<'i> Convert<List<Raster<GPU>>, &'i WgpuExecutor> for List<Raster<GPU>> { impl<'i> Convert<List<Raster<GPU>>, &'i WgpuExecutor> for List<Raster<GPU>> {
async fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> List<Raster<GPU>> { fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> List<Raster<GPU>> {
self self
} }
} }
/// Converts a `List<Raster<CPU>>` to `List<Raster<GPU>>` by uploading each image to a texture /// Converts a `List<Raster<CPU>>` to `List<Raster<GPU>>` by uploading each image to a texture
impl<'i> Convert<List<Raster<GPU>>, &'i WgpuExecutor> for List<Raster<CPU>> { impl<'i> Convert<List<Raster<GPU>>, &'i WgpuExecutor> for List<Raster<CPU>> {
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List<Raster<GPU>> { fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List<Raster<GPU>> {
let device = &executor.context().device; let device = &executor.context().device;
let queue = executor.context().queue.lock(); let queue = executor.context().queue.lock();
let list = self let list = self
@@ -171,7 +68,7 @@ impl<'i> Convert<List<Raster<GPU>>, &'i WgpuExecutor> for List<Raster<CPU>> {
/// Converts single CPU raster to GPU by uploading to texture /// Converts single CPU raster to GPU by uploading to texture
impl<'i> Convert<Raster<GPU>, &'i WgpuExecutor> for Raster<CPU> { impl<'i> Convert<Raster<GPU>, &'i WgpuExecutor> for Raster<CPU> {
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster<GPU> { fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster<GPU> {
let device = &executor.context().device; let device = &executor.context().device;
let queue = executor.context().queue.lock(); let queue = executor.context().queue.lock();
let texture = upload_to_texture(device, &queue, &self); let texture = upload_to_texture(device, &queue, &self);
@@ -183,79 +80,7 @@ impl<'i> Convert<Raster<GPU>, &'i WgpuExecutor> for Raster<CPU> {
/// Passthrough conversion for CPU `List`s - no conversion needed /// Passthrough conversion for CPU `List`s - no conversion needed
impl<'i> Convert<List<Raster<CPU>>, &'i WgpuExecutor> for List<Raster<CPU>> { impl<'i> Convert<List<Raster<CPU>>, &'i WgpuExecutor> for List<Raster<CPU>> {
async fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> List<Raster<CPU>> { fn convert(self, _: Footprint, _converter: &'i WgpuExecutor) -> List<Raster<CPU>> {
self self
} }
} }
/// Converts a `List<Raster<GPU>>` to `List<Raster<CPU>>` by downloading texture data in one go then asynchronously maps all buffers and processes the results.
impl<'i> Convert<List<Raster<CPU>>, &'i WgpuExecutor> for List<Raster<GPU>> {
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List<Raster<CPU>> {
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<Raster<CPU>, &'i WgpuExecutor> for Raster<GPU> {
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster<CPU> {
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<List<Raster<GPU>>, &'a WgpuExecutor>>(
_: impl Ctx,
#[implementations(List<Raster<CPU>>, List<Raster<GPU>>)] input: T,
executor: &'a WgpuExecutor,
) -> List<Raster<GPU>> {
input.convert(Footprint::DEFAULT, executor).await
}
+14 -13
View File
@@ -1,6 +1,7 @@
use core_types::gpoll::GPoll;
use core_types::list::List; use core_types::list::List;
use core_types::transform::Footprint; 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 glam::{DAffine2, DVec2};
use graphic_types::vector_types::GradientStops; use graphic_types::vector_types::GradientStops;
use graphic_types::{Artboard, Graphic, Vector}; use graphic_types::{Artboard, Graphic, Vector};
@@ -61,8 +62,8 @@ fn animation_time(
} }
#[node_macro::node(category("Debug"))] #[node_macro::node(category("Debug"))]
async fn quantize_real_time<T>( fn quantize_real_time<T>(
ctx: impl Ctx + ExtractAll + CloneVarArgs, ctx: impl Ctx + ExtractRealTime + DeriveCtx,
#[implementations( #[implementations(
Context -> bool, Context -> bool,
Context -> u32, Context -> u32,
@@ -84,11 +85,11 @@ async fn quantize_real_time<T>(
Context -> List<f64>, Context -> List<f64>,
Context -> (), Context -> (),
)] )]
value: impl Node<'n, Context<'static>, Output = T>, value: impl Node<Context<'_>, Output = T>,
#[default(1)] #[default(1)]
#[unit("sec")] #[unit("sec")]
quantum: f64, quantum: f64,
) -> T { ) -> GPoll<T> {
let time = ctx.try_real_time().unwrap_or_default(); let time = ctx.try_real_time().unwrap_or_default();
let time = time / 1000.; let time = time / 1000.;
let mut quantized_time = (time * quantum.recip()).round() / quantum.recip(); let mut quantized_time = (time * quantum.recip()).round() / quantum.recip();
@@ -96,13 +97,13 @@ async fn quantize_real_time<T>(
quantized_time = time; quantized_time = time;
} }
let quantized_time = quantized_time * 1000.; let quantized_time = quantized_time * 1000.;
let new_context = OwnedContextImpl::from(ctx).with_real_time(quantized_time); let scope = ctx.scope().with_real_time(Some(quantized_time));
value.eval(Some(new_context.into())).await value.eval(&ctx.with_scope(&scope))
} }
#[node_macro::node(category("Debug"))] #[node_macro::node(category("Debug"))]
async fn quantize_animation_time<T>( fn quantize_animation_time<T>(
ctx: impl Ctx + ExtractAll + CloneVarArgs, ctx: impl Ctx + ExtractAnimationTime + DeriveCtx,
#[implementations( #[implementations(
Context -> bool, Context -> bool,
Context -> u32, Context -> u32,
@@ -124,18 +125,18 @@ async fn quantize_animation_time<T>(
Context -> List<f64>, Context -> List<f64>,
Context -> (), Context -> (),
)] )]
value: impl Node<'n, Context<'static>, Output = T>, value: impl Node<Context<'_>, Output = T>,
#[default(1)] #[default(1)]
#[unit("sec")] #[unit("sec")]
quantum: f64, quantum: f64,
) -> T { ) -> GPoll<T> {
let time = ctx.try_animation_time().unwrap_or_default(); let time = ctx.try_animation_time().unwrap_or_default();
let mut quantized_time = (time * quantum.recip()).round() / quantum.recip(); let mut quantized_time = (time * quantum.recip()).round() / quantum.recip();
if !quantized_time.is_finite() { if !quantized_time.is_finite() {
quantized_time = time; quantized_time = time;
} }
let new_context = OwnedContextImpl::from(ctx).with_animation_time(quantized_time); let scope = ctx.scope().with_animation_time(Some(quantized_time));
value.eval(Some(new_context.into())).await value.eval(&ctx.with_scope(&scope))
} }
/// Produces the current position of the user's pointer within the document canvas. /// Produces the current position of the user's pointer within the document canvas.
@@ -1,5 +1,6 @@
use core::f64; 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::list::{AttributeDyn, AttributeValueDyn, List, ListDyn};
use core_types::transform::Footprint; use core_types::transform::Footprint;
use core_types::uuid::NodeId; 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. /// 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. /// This node is inserted by the compiler to "zero out" unused context components.
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn context_modification<T>( fn context_modification<T>(
ctx: impl Ctx + CloneVarArgs + ExtractAll, ctx: impl Ctx + DeriveCtx,
/// The data to pass through, evaluated with the stripped down context. /// The data to pass through, evaluated with the stripped down context.
#[implementations( #[implementations(
Context -> (), Context -> (),
@@ -41,13 +42,12 @@ async fn context_modification<T>(
Context -> AttributeValueDyn, Context -> AttributeValueDyn,
Context -> ListDyn, Context -> ListDyn,
)] )]
value: impl Node<Context<'static>, Output = T>, value: impl Node<Context<'_>, Output = T>,
/// The parts of the context to keep when evaluating the input value. All other parts are nullified. /// The parts of the context to keep when evaluating the input value. All other parts are nullified.
features_to_keep: ContextFeatures, features_to_keep: ContextFeatures,
) -> T { ) -> GPoll<T> {
let new_context = OwnedContextImpl::from_flags(ctx, features_to_keep); let scope = ctx.scope().nullified(features_to_keep);
value.eval(&ctx.nullified(features_to_keep, &scope))
value.eval(Some(new_context.into())).await
} }
#[cfg(test)] #[cfg(test)]
+13 -10
View File
@@ -1,4 +1,4 @@
use core_types::WasmNotSend; use core_types::gpoll::Interrupt;
use core_types::graphene_hash::CacheHash; use core_types::graphene_hash::CacheHash;
use core_types::memo::*; use core_types::memo::*;
use std::hash::DefaultHasher; 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. /// 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)] #[node_macro::node(category("General"), path(graphene_core::memo), skip_impl)]
async fn memoize<I: CacheHash + Send + 'n, T: Clone + WasmNotSend>(input: I, #[data] cache: Arc<Mutex<Option<(u64, T)>>>, content: impl Node<I, Output = T>) -> T { fn memoize<I: CacheHash, T: Clone>(input: I, #[data] cache: Arc<Mutex<Option<(u64, T)>>>, content: impl Node<I, Output = T>) -> Result<T, Interrupt> {
// Caches the output of a given node called with a specific input. // 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. // 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<I: CacheHash + Send + 'n, T: Clone + WasmNotSend>(input: I, #[d
let hash = hasher.finish(); 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())) { 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())); *cache.lock().unwrap() = Some((hash, value.clone()));
value Ok(value)
} }
type MonitorValue<I, T> = Arc<Mutex<Option<Arc<IORecord<I, T>>>>>; type MonitorValue<I, T> = Arc<Mutex<Option<Arc<IORecord<I, T>>>>>;
/// The Monitor node is used by the editor to access the data flowing through it. /// 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)] #[node_macro::node(category(""), path(graphene_core::memo), serialize(serialize_monitor), properties("monitor_properties"), skip_impl)]
async fn monitor<I: Clone + 'static + Send + Sync, T: Clone + 'static + Send + Sync>( fn monitor<I: Clone + 'static + Send + Sync, T: Clone + 'static + Send + Sync>(
input: I, input: I,
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
#[data] #[data]
io: MonitorValue<I, T>, io: MonitorValue<I, T>,
content: impl Node<I, Output = T>, content: impl Node<I, Output = T>,
) -> T { ) -> Result<T, Interrupt> {
let output = content.eval(input.clone()).await; let output = content.eval(input)?;
*io.lock().unwrap() = Some(Arc::new(IORecord { input, output: output.clone() })); *io.lock().unwrap() = Some(Arc::new(IORecord {
output input: input.clone(),
output: output.clone(),
}));
Ok(output)
} }
fn serialize_monitor<I: Clone + 'static + Send + Sync, T: Clone + 'static + Send + Sync>(io: &MonitorValue<I, T>) -> Option<Arc<dyn std::any::Any + Send + Sync>> { fn serialize_monitor<I: Clone + 'static + Send + Sync, T: Clone + 'static + Send + Sync>(io: &MonitorValue<I, T>) -> Option<Arc<dyn std::any::Any + Send + Sync>> {
+2 -2
View File
@@ -16,8 +16,8 @@ fn into<'i, T: 'i + Send + Into<O>, O: 'i + Send>(_: impl Ctx, value: T, _out_ty
} }
#[node_macro::node(category(""), skip_impl)] #[node_macro::node(category(""), skip_impl)]
async fn convert<'i, T: 'i + Send + Convert<O, C>, O: 'i + Send, C: 'i + Send>(ctx: impl Ctx + ExtractFootprint, value: T, converter: C, _out_ty: PhantomData<O>) -> O { fn convert<T: Send + Convert<O, C>, O: Send, C: Send>(ctx: impl Ctx + ExtractFootprint, value: T, converter: C, _out_ty: PhantomData<O>) -> O {
value.convert(*ctx.try_footprint().unwrap_or(&Footprint::DEFAULT), converter).await value.convert(*ctx.try_footprint().unwrap_or(&Footprint::DEFAULT), converter)
} }
#[cfg(test)] #[cfg(test)]
+10 -14
View File
@@ -1,6 +1,7 @@
use core_types::gpoll::Interrupt;
use core_types::list::{Item, List}; use core_types::list::{Item, List};
use core_types::transform::TransformMut; 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 glam::{DAffine2, DVec2};
use graphic_types::graphic::{Graphic, IntoGraphicList}; use graphic_types::graphic::{Graphic, IntoGraphicList};
use graphic_types::{Artboard, Vector}; 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. /// Constructs a single-element `Artboard[]` with the given content and metadata stored as row attributes.
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
pub async fn create_artboard<T: IntoGraphicList>( pub fn create_artboard<T: IntoGraphicList>(
ctx: impl ExtractAll + CloneVarArgs + Ctx, ctx: impl Ctx + ExtractFootprint + DeriveCtx,
/// Graphics to include within the artboard. /// Graphics to include within the artboard.
#[implementations( #[implementations(
Context -> List<Graphic>, Context -> List<Graphic>,
@@ -22,7 +23,7 @@ pub async fn create_artboard<T: IntoGraphicList>(
Context -> List<GradientStops>, Context -> List<GradientStops>,
Context -> DAffine2, Context -> DAffine2,
)] )]
content: impl Node<Context<'static>, Output = T>, content: impl Node<Context<'_>, Output = T>,
/// Coordinate of the top-left corner of the artboard within the document. /// Coordinate of the top-left corner of the artboard within the document.
location: DVec2, location: DVec2,
/// Width and height of the artboard within the document. /// Width and height of the artboard within the document.
@@ -32,14 +33,9 @@ pub async fn create_artboard<T: IntoGraphicList>(
/// Whether to cut off the contained content that extends outside the artboard, or keep it visible. /// Whether to cut off the contained content that extends outside the artboard, or keep it visible.
#[default(true)] #[default(true)]
clip: bool, clip: bool,
) -> List<Artboard> { ) -> Result<List<Artboard>, Interrupt> {
let footprint = ctx.try_footprint().copied(); let translated = ctx.modify_footprint(|footprint| footprint.translate(location));
let mut new_ctx = OwnedContextImpl::from(ctx); let content = content.eval(&translated.ctx())?.into_graphic_list();
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();
// Normalize so `location` is the top-left corner and `dimensions` are positive (allowing negative input // 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 // dimensions to represent dragging from the opposite corner). Compute the corner using the raw signed
@@ -50,11 +46,11 @@ pub async fn create_artboard<T: IntoGraphicList>(
let background = background.element(0).copied().unwrap_or(Color::WHITE); 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 // 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)) Item::new_from_element(Artboard::new(content))
.with_attribute(ATTR_LOCATION, normalized_location) .with_attribute(ATTR_LOCATION, normalized_location)
.with_attribute(ATTR_DIMENSIONS, normalized_dimensions) .with_attribute(ATTR_DIMENSIONS, normalized_dimensions)
.with_attribute(ATTR_BACKGROUND, background) .with_attribute(ATTR_BACKGROUND, background)
.with_attribute(ATTR_CLIP, clip), .with_attribute(ATTR_CLIP, clip),
) ))
} }
+24 -20
View File
@@ -2,7 +2,8 @@ use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::list::{AttributeDyn, AttributeValueDyn, Item, List, ListDyn}; use core_types::list::{AttributeDyn, AttributeValueDyn, Item, List, ListDyn};
use core_types::registry::types::{Angle, SignedInteger}; use core_types::registry::types::{Angle, SignedInteger};
use core_types::uuid::NodeId; 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 glam::{DAffine2, DVec2};
use graphic_types::graphic::{Graphic, IntoGraphicList}; use graphic_types::graphic::{Graphic, IntoGraphicList};
use graphic_types::{Artboard, Vector}; use graphic_types::{Artboard, Vector};
@@ -108,8 +109,8 @@ pub fn extract_element<T: Clone + Default + Send + Sync + 'static>(
} }
#[node_macro::node(category("General"))] #[node_macro::node(category("General"))]
async fn map<Item: AnyHash + Send + Sync + CacheHash>( fn map<Item: AnyHash + Send + Sync + CacheHash>(
ctx: impl Ctx + CloneVarArgs + ExtractAll, ctx: impl Ctx + DeriveCtx,
#[implementations( #[implementations(
List<Graphic>, List<Graphic>,
List<Vector>, List<Vector>,
@@ -127,23 +128,24 @@ async fn map<Item: AnyHash + Send + Sync + CacheHash>(
Context -> List<GradientStops>, Context -> List<GradientStops>,
Context -> List<String>, Context -> List<String>,
)] )]
mapped: impl Node<Context<'static>, Output = List<Item>>, mapped: impl Node<Context<'_>, Output = List<Item>>,
) -> List<Item> { ) -> Result<List<Item>, Interrupt> {
let spilled = ctx.index_head();
let mut rows = List::new(); let mut rows = List::new();
for (i, row) in content.into_iter().enumerate() { for (i, row) in content.into_iter().enumerate() {
let owned_ctx = OwnedContextImpl::from(ctx.clone()); let item = List::new_from_item(row);
let owned_ctx = owned_ctx.with_vararg(Box::new(List::new_from_item(row))).with_index(i); let scoped = ctx.push_vararg(&item);
let list = mapped.eval(owned_ctx.into_context()).await; let list = mapped.eval(&scoped.ctx().promoted(&spilled, i as u64))?;
rows.extend(list); rows.extend(list);
} }
rows Ok(rows)
} }
#[node_macro::node(category("General"))] #[node_macro::node(category("General"))]
async fn mirror<T: 'n + Send + Clone>( fn mirror<T: Send + Clone>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Graphic>, List<Graphic>,
@@ -229,8 +231,8 @@ pub fn path_of_subgraph(_: impl Ctx, node_path: List<NodeId>) -> List<NodeId> {
/// The value is type-erased into an `AttributeValueDyn` by an auto-inserted convert node, so this node only /// 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)`. /// monomorphizes over `T` instead of the cartesian product `(T, U)`.
#[node_macro::node(category("Attributes: Write"))] #[node_macro::node(category("Attributes: Write"))]
async fn write_attribute<T: AnyHash + Clone + Send + Sync + CacheHash>( fn write_attribute<T: AnyHash + Clone + Send + Sync + CacheHash>(
ctx: impl ExtractAll + CloneVarArgs + Ctx, ctx: impl Ctx + DeriveCtx,
/// The `List` to set the named attribute on (one value per item). /// The `List` to set the named attribute on (one value per item).
#[implementations( #[implementations(
List<Artboard>, List<Artboard>,
@@ -252,15 +254,17 @@ async fn write_attribute<T: AnyHash + Clone + Send + Sync + CacheHash>(
name: String, name: String,
/// The node that produces the attribute value for each item. Called once per item with the item's index in context. /// The node that produces the attribute value for each item. Called once per item with the item's index in context.
#[implementations(Context -> AttributeValueDyn)] #[implementations(Context -> AttributeValueDyn)]
value: impl Node<'n, Context<'static>, Output = AttributeValueDyn>, value: impl Node<Context<'_>, Output = AttributeValueDyn>,
) -> List<T> { ) -> Result<List<T>, Interrupt> {
let spilled = ctx.index_head();
for index in 0..content.len() { for index in 0..content.len() {
let row = content.clone_item(index).expect("index is within bounds"); 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 item = List::new_from_item(row);
let v = value.eval(owned_ctx.into_context()).await; 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.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). /// 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`. /// Joins two `List`s of the same type, extending the base `List` with the items from the new `List`.
#[node_macro::node(category("General"))] #[node_macro::node(category("General"))]
pub async fn extend<T: 'n + Send + Clone>( pub fn extend<T: Send + Clone>(
_: impl Ctx, _: impl Ctx,
/// The `List` whose items will appear at the start of the extended `List`. /// The `List` whose items will appear at the start of the extended `List`.
#[implementations(List<Artboard>, List<Graphic>, List<Vector>, List<String>, List<Raster<CPU>>, List<Raster<GPU>>, List<Color>, List<GradientStops>)] #[implementations(List<Artboard>, List<Graphic>, List<Vector>, List<String>, List<Raster<CPU>>, List<Raster<GPU>>, List<Color>, List<GradientStops>)]
@@ -517,7 +521,7 @@ pub async fn extend<T: 'n + Send + Clone>(
/// Performs an obsolete function as part of a migration from an older document format. /// 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. /// Users are advised to delete this node and replace it with a new one.
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
pub async fn legacy_layer_extend<T: 'n + Send + Clone>( pub fn legacy_layer_extend<T: Send + Clone>(
_: impl Ctx, _: impl Ctx,
#[implementations(List<Artboard>, List<Graphic>, List<Vector>, List<String>, List<Raster<CPU>>, List<Raster<GPU>>, List<Color>, List<GradientStops>)] base: List<T>, #[implementations(List<Artboard>, List<Graphic>, List<Vector>, List<String>, List<Raster<CPU>>, List<Raster<GPU>>, List<Color>, List<GradientStops>)] base: List<T>,
#[expose] #[expose]
@@ -544,7 +548,7 @@ pub async fn legacy_layer_extend<T: 'n + Send + Clone>(
/// Nests the input graphical content in a wrapper graphic. This essentially "groups" the input. /// Nests the input graphical content in a wrapper graphic. This essentially "groups" the input.
/// The inverse of this node is 'Flatten Graphic'. /// The inverse of this node is 'Flatten Graphic'.
#[node_macro::node(category("General"))] #[node_macro::node(category("General"))]
pub async fn wrap_graphic<T: Into<Graphic> + 'n>( pub fn wrap_graphic<T: Into<Graphic>>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Graphic>, List<Graphic>,
+2 -2
View File
@@ -325,7 +325,7 @@ pub async fn render_output_cache<'a: 'n>(
ctx: impl Ctx + ExtractAll + CloneVarArgs + ExtractRealTime + ExtractAnimationTime + ExtractPointerPosition + Sync, 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::try_wgpu_executor::IDENTIFIER)] executor: Option<&'a WgpuExecutor>,
#[scope(crate::platform_application_io::editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi, #[scope(crate::platform_application_io::editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi,
data: impl Node<Context<'static>, Output = RenderOutput> + Send + Sync, data: impl Node<Context<'_>, Output = RenderOutput> + Send + Sync,
#[data] tile_cache: TileCache, #[data] tile_cache: TileCache,
) -> RenderOutput { ) -> RenderOutput {
let footprint = ctx.footprint(); let footprint = ctx.footprint();
@@ -409,7 +409,7 @@ async fn render_missing_region<F, Fut>(
viewport_origin_offset: &DVec2, viewport_origin_offset: &DVec2,
) -> CachedRegion ) -> CachedRegion
where where
F: Fn(Context<'static>) -> Fut, F: Fn(Context<'_>) -> Fut,
Fut: std::future::Future<Output = RenderOutput>, Fut: std::future::Future<Output = RenderOutput>,
{ {
let min_tile = region.tiles.iter().fold(IVec2::new(i32::MAX, i32::MAX), |acc, t| acc.min(IVec2::new(t.x, t.y))); let min_tile = region.tiles.iter().fold(IVec2::new(i32::MAX, i32::MAX), |acc, t| acc.min(IVec2::new(t.x, t.y)));
+2 -2
View File
@@ -34,7 +34,7 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
Context -> List<GradientStops>, Context -> List<GradientStops>,
Context -> List<String>, Context -> List<String>,
)] )]
data: impl Node<Context<'static>, Output = T>, data: impl Node<Context<'_>, Output = T>,
) -> RenderIntermediate { ) -> RenderIntermediate {
let render_params = ctx let render_params = ctx
.vararg(0) .vararg(0)
@@ -147,7 +147,7 @@ async fn render<'a: 'n>(
async fn create_context<'a: 'n>( async fn create_context<'a: 'n>(
// Context injections are defined in the wrap_network_in_scope function // Context injections are defined in the wrap_network_in_scope function
render_config: RenderConfig, render_config: RenderConfig,
data: impl Node<Context<'static>, Output = RenderOutput>, data: impl Node<Context<'_>, Output = RenderOutput>,
) -> RenderOutput { ) -> RenderOutput {
let render_output_type = match render_config.export_format { let render_output_type = match render_config.export_format {
ExportFormat::Svg => RenderOutputTypeRequest::Svg, ExportFormat::Svg => RenderOutputTypeRequest::Svg,
@@ -11,7 +11,7 @@ use wgpu_executor::{AsyncWgpuPipeline, WgpuExecutor, WgpuPipelineCache};
pub async fn render_pixel_preview<'a: 'n>( pub async fn render_pixel_preview<'a: 'n>(
ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync, ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync,
#[scope(pixel_preview_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache, #[scope(pixel_preview_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache,
data: impl Node<Context<'static>, Output = RenderOutput> + Send + Sync, data: impl Node<Context<'_>, Output = RenderOutput> + Send + Sync,
) -> RenderOutput { ) -> RenderOutput {
let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::<RenderParams>()).cloned() else { let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::<RenderParams>()).cloned() else {
log::error!("invalid render params for pixel preview"); log::error!("invalid render params for pixel preview");
+217 -14
View File
@@ -1,4 +1,5 @@
use core_types::Context; use core_types::Context;
use core_types::gpoll::GPoll;
use core_types::list::List; use core_types::list::List;
use core_types::registry::types::{Fraction, Percentage, PixelSize}; use core_types::registry::types::{Fraction, Percentage, PixelSize};
use core_types::transform::Footprint; 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. /// 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"))] #[node_macro::node(category("Math: Logic"))]
async fn switch<T, C: Send + 'n + Clone>( fn switch<T, C>(
#[implementations(Context)] ctx: C, #[implementations(Context)] ctx: C,
condition: bool, condition: bool,
#[expose] #[expose]
@@ -781,8 +782,8 @@ async fn switch<T, C: Send + 'n + Clone>(
Context -> List<GradientStops>, Context -> List<GradientStops>,
)] )]
if_false: impl Node<C, Output = T>, if_false: impl Node<C, Output = T>,
) -> T { ) -> GPoll<T> {
if condition { if_true.eval(ctx).await } else { if_false.eval(ctx).await } if condition { if_true.eval(ctx) } else { if_false.eval(ctx) }
} }
/// Constructs a bool value which may be set to true or false. /// Constructs a bool value which may be set to true or false.
@@ -995,36 +996,36 @@ mod test {
pub fn dot_product_function() { pub fn dot_product_function() {
let vector_a = DVec2::new(1., 2.); let vector_a = DVec2::new(1., 2.);
let vector_b = DVec2::new(3., 4.); 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] #[test]
pub fn length_function() { pub fn length_function() {
let vector = DVec2::new(3., 4.); let vector = DVec2::new(3., 4.);
assert_eq!(length((), vector), 5.); assert_eq!(length(&(), vector), 5.);
} }
#[test] #[test]
fn test_basic_expression() { 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.); assert_eq!(result, 4.);
} }
#[test] #[test]
fn test_complex_expression() { 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.); assert_eq!(result, 20.);
} }
#[test] #[test]
fn test_default_expression() { fn test_default_expression() {
let result = math((), 0., "0".to_string(), 0.); let result = math(&(), 0., "0".to_string(), 0.);
assert_eq!(result, 0.); assert_eq!(result, 0.);
} }
#[test] #[test]
fn test_invalid_expression() { fn test_invalid_expression() {
let result = math((), 0., "invalid".to_string(), 0.); let result = math(&(), 0., "invalid".to_string(), 0.);
assert_eq!(result, 0.); assert_eq!(result, 0.);
} }
@@ -1036,26 +1037,228 @@ mod test {
#[test] #[test]
pub fn add_vectors() { 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] #[test]
pub fn subtract_f64() { pub fn subtract_f64() {
assert_eq!(super::subtract((), 5_f64, 3_f64), 2.); assert_eq!(super::subtract(&(), 5_f64, 3_f64), 2.);
} }
#[test] #[test]
pub fn divide_vectors() { 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] #[test]
pub fn modulo_positive() { 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] #[test]
pub fn modulo_negative() { 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>(T);
impl<T: Clone, Input> GNode<Input> for SourceNode<T> {
type Output = T;
fn eval(&self, _input: &Input) -> GPoll<T> {
GPoll::Final(self.0.clone())
}
}
struct IndexNode;
impl<Input: ExtractIndex> GNode<Input> for IndexNode {
type Output = f64;
fn eval(&self, input: &Input) -> GPoll<f64> {
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<ErasedGNode<f64>> = 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<ErasedGNode<bool>>);
let other_value = EdgeHandle::new(Box::new(SourceNode(false)) as Box<ErasedGNode<bool>>);
let wired = resolve_and_wire(&entries[0], vec![value, other_value]).unwrap().downcast::<bool>().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<ErasedGNode<f64>>);
let addend = EdgeHandle::new(Box::new(SourceNode(2.5f64)) as Box<ErasedGNode<f64>>);
let wired = resolve_and_wire(&entries[0], vec![augend, addend]).unwrap().downcast::<f64>().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<AtomicU32>, f64);
impl<Input> GNode<Input> for CountingSource {
type Output = f64;
fn eval(&self, _input: &Input) -> GPoll<f64> {
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<Input> GNode<Input> for PendingSource {
type Output = f64;
fn eval(&self, _input: &Input) -> GPoll<f64> {
GPoll::Pending
}
}
struct PartialSource;
impl<Input> GNode<Input> for PartialSource {
type Output = f64;
fn eval(&self, _input: &Input) -> GPoll<f64> {
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<Input> GNode<Input> for PartialCondition {
type Output = bool;
fn eval(&self, _input: &Input) -> GPoll<bool> {
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<Input> GNode<Input> for FallbackNode {
type Output = f64;
fn eval(&self, _input: &Input) -> GPoll<f64> {
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]);
} }
} }
+2 -2
View File
@@ -241,7 +241,7 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: List<Raster<CPU>>, bounds: DAf
let image_data = &row.element().data; let image_data = &row.element().data;
let (image_width, image_height) = (row.element().width, row.element().height); let (image_width, image_height) = (row.element().width, row.element().height);
if image_width == 0 || image_height == 0 { 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); 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<Color>) -> List
} }
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
pub fn image<'a: 'n>(_: impl Ctx, resource: Resource) -> List<Raster<CPU>> { pub fn image(_: impl Ctx, resource: Resource) -> List<Raster<CPU>> {
let image_data = resource.as_ref(); let image_data = resource.as_ref();
let Some(image) = ::image::load_from_memory(image_data).ok() else { let Some(image) = ::image::load_from_memory(image_data).ok() else {
+39 -42
View File
@@ -1,16 +1,17 @@
use crate::gcore::Context; use crate::gcore::Context;
use core::f64::consts::TAU; use core::f64::consts::TAU;
use core_types::gpoll::Interrupt;
use core_types::list::List; use core_types::list::List;
use core_types::registry::types::{Angle, PixelSize}; 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 glam::{DAffine2, DVec2};
use graphic_types::{Graphic, Vector}; use graphic_types::{Graphic, Vector};
use raster_types::{CPU, Raster}; use raster_types::{CPU, Raster};
use vector_types::GradientStops; use vector_types::GradientStops;
#[node_macro::node(category("Repeat"))] #[node_macro::node(category("Repeat"))]
async fn repeat<T: Into<Graphic> + Default + Send + Clone + 'static>( fn repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Ctx, ctx: impl Ctx + DeriveCtx,
#[implementations( #[implementations(
Context -> List<Graphic>, Context -> List<Graphic>,
Context -> List<Vector>, Context -> List<Vector>,
@@ -18,35 +19,35 @@ async fn repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(
Context -> List<Color>, Context -> List<Color>,
Context -> List<GradientStops>, Context -> List<GradientStops>,
)] )]
content: impl Node<'n, Context<'static>, Output = List<T>>, content: impl Node<Context<'_>, Output = List<T>>,
#[default(1)] #[default(1)]
#[hard(1..)] #[hard(1..)]
count: u32, count: u32,
reverse: bool, reverse: bool,
) -> List<T> { ) -> Result<List<T>, Interrupt> {
// Someday this node can have the option to generate infinitely instead of a fixed count (basically `std::iter::repeat`). // 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(); let mut result_list = List::new();
for index in 0..count { for index in 0..count {
let index = if reverse { count - index - 1 } else { index }; let index = if reverse { count - index - 1 } else { index };
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index); let generated_content = content.eval(&ctx.promoted(&spilled, index))?;
let generated_content = content.eval(new_ctx.into_context()).await;
for generated_row in generated_content.into_iter() { for generated_row in generated_content.into_iter() {
result_list.push(generated_row); result_list.push(generated_row);
} }
} }
result_list Ok(result_list)
} }
#[node_macro::node(category("Repeat"))] #[node_macro::node(category("Repeat"))]
pub async fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>( pub fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Ctx, ctx: impl Ctx + DeriveCtx,
#[implementations( #[implementations(
Context -> List<Graphic>, Context -> List<Graphic>,
Context -> List<Vector>, Context -> List<Vector>,
@@ -54,7 +55,7 @@ pub async fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
Context -> List<Color>, Context -> List<Color>,
Context -> List<GradientStops>, Context -> List<GradientStops>,
)] )]
content: impl Node<'n, Context<'static>, Output = List<T>>, content: impl Node<Context<'_>, Output = List<T>>,
#[default(100., 100.)] #[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. // 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, direction: PixelSize,
@@ -62,10 +63,11 @@ pub async fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
#[default(5)] #[default(5)]
#[hard(1..)] #[hard(1..)]
count: u32, count: u32,
) -> List<T> { ) -> Result<List<T>, Interrupt> {
let angle = angle.to_radians(); 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 // 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 total = (count - 1).max(1) as f64;
let spilled = ctx.index_head();
let mut result_list = List::new(); let mut result_list = List::new();
@@ -74,8 +76,7 @@ pub async fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
let translation = index as f64 * direction / total; let translation = index as f64 * direction / total;
let transform = DAffine2::from_angle(angle) * DAffine2::from_translation(translation); 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(&ctx.promoted(&spilled, index as u64))?;
let generated_content = content.eval(new_ctx.into_context()).await;
for row_index in 0..generated_content.len() { for row_index in 0..generated_content.len() {
let Some(mut row) = generated_content.clone_item(row_index) else { continue }; let Some(mut row) = generated_content.clone_item(row_index) else { continue };
@@ -89,12 +90,12 @@ pub async fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
} }
} }
result_list Ok(result_list)
} }
#[node_macro::node(category("Repeat"))] #[node_macro::node(category("Repeat"))]
async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>( fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Ctx, ctx: impl Ctx + DeriveCtx,
#[implementations( #[implementations(
Context -> List<Graphic>, Context -> List<Graphic>,
Context -> List<Vector>, Context -> List<Vector>,
@@ -102,7 +103,7 @@ async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
Context -> List<Color>, Context -> List<Color>,
Context -> List<GradientStops>, Context -> List<GradientStops>,
)] )]
content: impl Node<'n, Context<'static>, Output = List<T>>, content: impl Node<Context<'_>, Output = List<T>>,
start_angle: Angle, start_angle: Angle,
#[unit(" px")] #[unit(" px")]
#[default(5)] #[default(5)]
@@ -110,7 +111,8 @@ async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
#[default(5)] #[default(5)]
#[hard(1..)] #[hard(1..)]
count: u32, count: u32,
) -> List<T> { ) -> Result<List<T>, Interrupt> {
let spilled = ctx.index_head();
let mut result_list = List::new(); let mut result_list = List::new();
for index in 0..count { for index in 0..count {
@@ -118,8 +120,7 @@ async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
let translation = DAffine2::from_translation(radius * DVec2::Y); let translation = DAffine2::from_translation(radius * DVec2::Y);
let transform = angle * translation; let transform = angle * translation;
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index as usize); let generated_content = content.eval(&ctx.promoted(&spilled, index as u64))?;
let generated_content = content.eval(new_ctx.into_context()).await;
for row_index in 0..generated_content.len() { for row_index in 0..generated_content.len() {
let Some(mut row) = generated_content.clone_item(row_index) else { continue }; let Some(mut row) = generated_content.clone_item(row_index) else { continue };
@@ -133,12 +134,12 @@ async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
} }
} }
result_list Ok(result_list)
} }
#[node_macro::node(category("Repeat"), name("Repeat on Points"))] #[node_macro::node(category("Repeat"), name("Repeat on Points"))]
async fn repeat_on_points<T: Into<Graphic> + Default + Send + Clone + 'static>( fn repeat_on_points<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Sync + Ctx + InjectVarArgs, ctx: impl Ctx + DeriveCtx + InjectVarArgs,
points: List<Vector>, points: List<Vector>,
#[implementations( #[implementations(
Context -> List<Graphic>, Context -> List<Graphic>,
@@ -147,40 +148,36 @@ async fn repeat_on_points<T: Into<Graphic> + Default + Send + Clone + 'static>(
Context -> List<Color>, Context -> List<Color>,
Context -> List<GradientStops>, Context -> List<GradientStops>,
)] )]
content: impl Node<'n, Context<'static>, Output = List<T>>, content: impl Node<Context<'_>, Output = List<T>>,
reverse: bool, reverse: bool,
) -> List<T> { ) -> Result<List<T>, Interrupt> {
let spilled = ctx.index_head();
let mut result_list = List::new(); let mut result_list = List::new();
for points_index in 0..points.len() { for points_index in 0..points.len() {
let Some(points_element) = points.element(points_index) else { continue }; let Some(points_element) = points.element(points_index) else { continue };
let transform: DAffine2 = points.attribute_cloned_or_default(ATTR_TRANSFORM, points_index); 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<dyn Iterator<Item = (usize, &DVec2)>> = 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 transformed_point = transform.transform_point2(point);
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_position(transformed_point); let scoped = ctx.push_position(transformed_point);
let generated_content = content.eval(new_ctx.into_context()).await; let generated_content = content.eval(&scoped.ctx().promoted(&spilled, index as u64))?;
for mut generated_row in generated_content.into_iter() { for mut generated_row in generated_content.into_iter() {
generated_row.attribute_mut_or_insert_default::<DAffine2>(ATTR_TRANSFORM).translation = transformed_point; generated_row.attribute_mut_or_insert_default::<DAffine2>(ATTR_TRANSFORM).translation = transformed_point;
result_list.push(generated_row); 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)] #[cfg(test)]
+10 -9
View File
@@ -7,10 +7,11 @@ mod text_context;
mod to_path; mod to_path;
use convert_case::{Boundary, Converter, pattern}; use convert_case::{Boundary, Converter, pattern};
use core_types::gpoll::Interrupt;
use core_types::graphene_hash::CacheHash; use core_types::graphene_hash::CacheHash;
use core_types::list::{Item, List}; use core_types::list::{Item, List};
use core_types::registry::types::{SignedInteger, TextArea}; 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 dyn_any::DynAny;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use unicode_segmentation::UnicodeSegmentation; 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. /// 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"))] #[node_macro::node(category("Text"))]
async fn map_string( fn map_string(
ctx: impl Ctx + CloneVarArgs + ExtractAll, ctx: impl Ctx + DeriveCtx,
strings: List<String>, strings: List<String>,
#[expose] #[expose]
#[implementations(Context -> String)] #[implementations(Context -> String)]
mapped: impl Node<Context<'static>, Output = String>, mapped: impl Node<Context<'_>, Output = String>,
) -> List<String> { ) -> Result<List<String>, Interrupt> {
let spilled = ctx.index_head();
let mut result = List::new(); let mut result = List::new();
for (i, row) in strings.into_iter().enumerate() { for (i, row) in strings.into_iter().enumerate() {
let string = row.into_element(); let string = row.into_element();
let owned_ctx = OwnedContextImpl::from(ctx.clone()); let scoped = ctx.push_vararg(&string);
let owned_ctx = owned_ctx.with_vararg(Box::new(string)).with_index(i); let mapped_string = mapped.eval(&scoped.ctx().promoted(&spilled, i as u64))?;
let mapped_string = mapped.eval(owned_ctx.into_context()).await;
result.push(Item::new_from_element(mapped_string)); result.push(Item::new_from_element(mapped_string));
} }
result Ok(result)
} }
/// Reads the current string from within a **Map String** node's loop. /// Reads the current string from within a **Map String** node's loop.
@@ -2,7 +2,8 @@ use core::f64;
use core_types::color::Color; use core_types::color::Color;
use core_types::list::{List, ListDyn}; use core_types::list::{List, ListDyn};
use core_types::transform::{ApplyTransform, ScaleType, Transform}; 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 glam::{DAffine2, DMat2, DVec2};
use graphic_types::Graphic; use graphic_types::Graphic;
use graphic_types::Vector; 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. /// Applies the specified transform to the input value, which may be a graphic type or another transform.
#[node_macro::node(category("Math: Transform"))] #[node_macro::node(category("Math: Transform"))]
async fn transform<T: ApplyTransform + 'n + 'static>( fn transform<T: ApplyTransform + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll + ModifyFootprint, ctx: impl Ctx + ExtractFootprint + DeriveCtx + ModifyFootprint,
#[implementations( #[implementations(
Context -> DAffine2, Context -> DAffine2,
Context -> DVec2, Context -> DVec2,
@@ -24,31 +25,24 @@ async fn transform<T: ApplyTransform + 'n + 'static>(
Context -> List<Color>, Context -> List<Color>,
Context -> List<GradientStops>, Context -> List<GradientStops>,
)] )]
content: impl Node<Context<'static>, Output = T>, content: impl Node<Context<'_>, Output = T>,
#[widget(ParsedWidgetOverride::Custom = "transform_translation")] translation: DVec2, #[widget(ParsedWidgetOverride::Custom = "transform_translation")] translation: DVec2,
#[widget(ParsedWidgetOverride::Custom = "transform_rotation")] rotation: f64, #[widget(ParsedWidgetOverride::Custom = "transform_rotation")] rotation: f64,
#[widget(ParsedWidgetOverride::Custom = "transform_scale")] #[widget(ParsedWidgetOverride::Custom = "transform_scale")]
#[default(1., 1.)] #[default(1., 1.)]
scale: DVec2, scale: DVec2,
#[widget(ParsedWidgetOverride::Custom = "transform_skew")] skew: DVec2, #[widget(ParsedWidgetOverride::Custom = "transform_skew")] skew: DVec2,
) -> T { ) -> Result<T, Interrupt> {
let trs = DAffine2::from_scale_angle_translation(scale, rotation.to_radians(), translation); 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 skew = DAffine2::from_cols_array(&[1., skew.y.to_radians().tan(), skew.x.to_radians().tan(), 1., 0., 0.]);
let matrix = trs * skew; let matrix = trs * skew;
let footprint = ctx.try_footprint().copied(); let transformed = ctx.modify_footprint(|footprint| footprint.apply_transform(&matrix));
let mut transform_target = content.eval(&transformed.ctx())?;
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;
transform_target.left_apply_transform(&matrix); 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. /// 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.
+23 -24
View File
@@ -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::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
use core_types::transform::{Footprint, Transform}; use core_types::transform::{Footprint, Transform};
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
use core_types::gpoll::Interrupt;
use core_types::{ 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, 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,
Color, Context, Ctx, ExtractAll, OwnedContextImpl, Ctx, DeriveCtx,
}; };
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use graphic_types::Vector; use graphic_types::Vector;
@@ -115,7 +116,7 @@ async fn assign_colors<T>(
repeat_every: u32, repeat_every: u32,
) -> T ) -> T
where where
T: VectorListIterMut + 'n + Send, T: VectorListIterMut+ Send,
{ {
let Some(row) = gradient.into_iter().next() else { return content }; 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. /// 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"))] #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))]
async fn fill<V: VectorListIterMut + 'n + Send, F: IntoGraphicList + 'n + Send + 'static>( async fn fill<V: VectorListIterMut+ Send, F: IntoGraphicList+ Send + 'static>(
_: impl Ctx, _: impl Ctx,
/// The content with vector paths to apply the fill style to. /// The content with vector paths to apply the fill style to.
#[implementations( #[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. /// 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"))] #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))]
async fn stroke<V, L: IntoF64Vec, P: IntoGraphicList + 'n + Send + 'static>( async fn stroke<V, L: IntoF64Vec, P: IntoGraphicList+ Send + 'static>(
_: impl Ctx, _: impl Ctx,
/// The content with vector paths to apply the stroke style to. /// The content with vector paths to apply the stroke style to.
#[implementations( #[implementations(
@@ -323,7 +324,7 @@ async fn stroke<V, L: IntoF64Vec, P: IntoGraphicList + 'n + Send + 'static>(
dash_offset: f64, dash_offset: f64,
) -> List<V> ) -> List<V>
where where
List<V>: VectorListIterMut + 'n + Send, List<V>: VectorListIterMut+ Send,
{ {
let dash_lengths = dash_lengths.into_vec().into_iter().map(|length| length.max(0.)).collect(); 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))] #[node_macro::node(name("Copy to Points"), category("Repeat"), path(core_types::vector))]
async fn copy_to_points<I: 'n + Send + Clone>( async fn copy_to_points<I: Send + Clone>(
_: impl Ctx, _: impl Ctx,
points: List<Vector>, points: List<Vector>,
/// Artwork to be copied and placed at each point. /// 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))] #[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn pack_strips<T: 'n + Send + Clone>( async fn pack_strips<T: Send + Clone>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Graphic>, List<Graphic>,
@@ -1412,20 +1413,20 @@ async fn path_is_closed(
} }
#[node_macro::node(category("Vector"), path(graphene_core::vector))] #[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn map_points(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: List<Vector>, mapped: impl Node<Context<'static>, Output = DVec2>) -> List<Vector> { fn map_points(ctx: impl Ctx + DeriveCtx, content: List<Vector>, mapped: impl Node<Context<'_>, Output = DVec2>) -> Result<List<Vector>, Interrupt> {
let spilled = ctx.index_head();
let mut content = content; let mut content = content;
let mut index = 0; let mut index = 0;
for vector in content.iter_element_values_mut() { for vector in content.iter_element_values_mut() {
for (_, position) in vector.point_domain.positions_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; 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. // 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<Vector>) -> f64 {
} }
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))] #[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node<Context<'static>, Output = List<Vector>>) -> f64 { fn area(ctx: impl Ctx + DeriveCtx, content: impl Node<Context<'_>, Output = List<Vector>>) -> Result<f64, Interrupt> {
let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context(); let vector = content.eval(&ctx.with_footprint(&Footprint::DEFAULT))?;
let vector = content.eval(new_ctx).await;
(0..vector.len()) Ok((0..vector.len())
.map(|index| { .map(|index| {
let transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let area_scale = transform.matrix2.determinant().abs(); let area_scale = transform.matrix2.determinant().abs();
vector.element(index).unwrap().stroke_bezpath_iter().map(|subpath| subpath.area() * area_scale).sum::<f64>() vector.element(index).unwrap().stroke_bezpath_iter().map(|subpath| subpath.area() * area_scale).sum::<f64>()
}) })
.sum() .sum())
} }
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))] #[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node<Context<'static>, Output = List<Vector>>, centroid_type: CentroidType) -> DVec2 { fn centroid(ctx: impl Ctx + DeriveCtx, content: impl Node<Context<'_>, Output = List<Vector>>, centroid_type: CentroidType) -> Result<DVec2, Interrupt> {
let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context(); let vector = content.eval(&ctx.with_footprint(&Footprint::DEFAULT))?;
let vector = content.eval(new_ctx).await;
if vector.is_empty() { 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. // 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. { if sum > 0. {
centroid / sum Ok(centroid / sum)
} }
// Without a summed denominator, return the average of all positions instead // Without a summed denominator, return the average of all positions instead
else { else {
@@ -3255,7 +3254,7 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node<
.inspect(|_| count += 1) .inspect(|_| count += 1)
.sum::<DVec2>(); .sum::<DVec2>();
if count != 0 { summed_positions / (count as f64) } else { DVec2::ZERO } if count != 0 { Ok(summed_positions / (count as f64)) } else { Ok(DVec2::ZERO) }
} }
} }