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

View File

@@ -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
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
}
}
/// 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>> {
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 queue = executor.context().queue.lock();
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
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 queue = executor.context().queue.lock();
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
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
}
}
/// 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
}

View File

@@ -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<T>(
ctx: impl Ctx + ExtractAll + CloneVarArgs,
fn quantize_real_time<T>(
ctx: impl Ctx + ExtractRealTime + DeriveCtx,
#[implementations(
Context -> bool,
Context -> u32,
@@ -84,11 +85,11 @@ async fn quantize_real_time<T>(
Context -> List<f64>,
Context -> (),
)]
value: impl Node<'n, Context<'static>, Output = T>,
value: impl Node<Context<'_>, Output = T>,
#[default(1)]
#[unit("sec")]
quantum: f64,
) -> T {
) -> GPoll<T> {
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<T>(
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<T>(
ctx: impl Ctx + ExtractAll + CloneVarArgs,
fn quantize_animation_time<T>(
ctx: impl Ctx + ExtractAnimationTime + DeriveCtx,
#[implementations(
Context -> bool,
Context -> u32,
@@ -124,18 +125,18 @@ async fn quantize_animation_time<T>(
Context -> List<f64>,
Context -> (),
)]
value: impl Node<'n, Context<'static>, Output = T>,
value: impl Node<Context<'_>, Output = T>,
#[default(1)]
#[unit("sec")]
quantum: f64,
) -> T {
) -> GPoll<T> {
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.

View File

@@ -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<T>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
fn context_modification<T>(
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<T>(
Context -> AttributeValueDyn,
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.
features_to_keep: ContextFeatures,
) -> T {
let new_context = OwnedContextImpl::from_flags(ctx, features_to_keep);
value.eval(Some(new_context.into())).await
) -> GPoll<T> {
let scope = ctx.scope().nullified(features_to_keep);
value.eval(&ctx.nullified(features_to_keep, &scope))
}
#[cfg(test)]

View File

@@ -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<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.
//
// 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();
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<I, T> = Arc<Mutex<Option<Arc<IORecord<I, T>>>>>;
/// 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<I: Clone + 'static + Send + Sync, T: Clone + 'static + Send + Sync>(
fn monitor<I: Clone + 'static + Send + Sync, T: Clone + 'static + Send + Sync>(
input: I,
#[allow(clippy::type_complexity)]
#[data]
io: MonitorValue<I, T>,
content: impl Node<I, Output = T>,
) -> T {
let output = content.eval(input.clone()).await;
*io.lock().unwrap() = Some(Arc::new(IORecord { input, output: output.clone() }));
output
) -> Result<T, Interrupt> {
let output = content.eval(input)?;
*io.lock().unwrap() = Some(Arc::new(IORecord {
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>> {

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)]
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 {
value.convert(*ctx.try_footprint().unwrap_or(&Footprint::DEFAULT), converter).await
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)
}
#[cfg(test)]

View File

@@ -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<T: IntoGraphicList>(
ctx: impl ExtractAll + CloneVarArgs + Ctx,
pub fn create_artboard<T: IntoGraphicList>(
ctx: impl Ctx + ExtractFootprint + DeriveCtx,
/// Graphics to include within the artboard.
#[implementations(
Context -> List<Graphic>,
@@ -22,7 +23,7 @@ pub async fn create_artboard<T: IntoGraphicList>(
Context -> List<GradientStops>,
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.
location: DVec2,
/// 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.
#[default(true)]
clip: bool,
) -> List<Artboard> {
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<List<Artboard>, 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<T: IntoGraphicList>(
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),
)
))
}

View File

@@ -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<T: Clone + Default + Send + Sync + 'static>(
}
#[node_macro::node(category("General"))]
async fn map<Item: AnyHash + Send + Sync + CacheHash>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
fn map<Item: AnyHash + Send + Sync + CacheHash>(
ctx: impl Ctx + DeriveCtx,
#[implementations(
List<Graphic>,
List<Vector>,
@@ -127,23 +128,24 @@ async fn map<Item: AnyHash + Send + Sync + CacheHash>(
Context -> List<GradientStops>,
Context -> List<String>,
)]
mapped: impl Node<Context<'static>, Output = List<Item>>,
) -> List<Item> {
mapped: impl Node<Context<'_>, Output = List<Item>>,
) -> Result<List<Item>, 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<T: 'n + Send + Clone>(
fn mirror<T: Send + Clone>(
_: impl Ctx,
#[implementations(
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
/// monomorphizes over `T` instead of the cartesian product `(T, U)`.
#[node_macro::node(category("Attributes: Write"))]
async fn write_attribute<T: AnyHash + Clone + Send + Sync + CacheHash>(
ctx: impl ExtractAll + CloneVarArgs + Ctx,
fn write_attribute<T: AnyHash + Clone + Send + Sync + CacheHash>(
ctx: impl Ctx + DeriveCtx,
/// The `List` to set the named attribute on (one value per item).
#[implementations(
List<Artboard>,
@@ -252,15 +254,17 @@ async fn write_attribute<T: AnyHash + Clone + Send + Sync + CacheHash>(
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<T> {
value: impl Node<Context<'_>, Output = AttributeValueDyn>,
) -> Result<List<T>, 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<T: 'n + Send + Clone>(
pub fn extend<T: Send + Clone>(
_: impl Ctx,
/// 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>)]
@@ -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.
/// Users are advised to delete this node and replace it with a new one.
#[node_macro::node(category(""))]
pub async fn legacy_layer_extend<T: 'n + Send + Clone>(
pub fn legacy_layer_extend<T: Send + Clone>(
_: impl Ctx,
#[implementations(List<Artboard>, List<Graphic>, List<Vector>, List<String>, List<Raster<CPU>>, List<Raster<GPU>>, List<Color>, List<GradientStops>)] base: List<T>,
#[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.
/// The inverse of this node is 'Flatten Graphic'.
#[node_macro::node(category("General"))]
pub async fn wrap_graphic<T: Into<Graphic> + 'n>(
pub fn wrap_graphic<T: Into<Graphic>>(
_: impl Ctx,
#[implementations(
List<Graphic>,

View File

@@ -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<Context<'static>, Output = RenderOutput> + Send + Sync,
data: impl Node<Context<'_>, Output = RenderOutput> + Send + Sync,
#[data] tile_cache: TileCache,
) -> RenderOutput {
let footprint = ctx.footprint();
@@ -409,7 +409,7 @@ async fn render_missing_region<F, Fut>(
viewport_origin_offset: &DVec2,
) -> CachedRegion
where
F: Fn(Context<'static>) -> Fut,
F: Fn(Context<'_>) -> Fut,
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)));

View File

@@ -34,7 +34,7 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
Context -> List<GradientStops>,
Context -> List<String>,
)]
data: impl Node<Context<'static>, Output = T>,
data: impl Node<Context<'_>, 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<Context<'static>, Output = RenderOutput>,
data: impl Node<Context<'_>, Output = RenderOutput>,
) -> RenderOutput {
let render_output_type = match render_config.export_format {
ExportFormat::Svg => RenderOutputTypeRequest::Svg,

View File

@@ -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<Context<'static>, Output = RenderOutput> + Send + Sync,
data: impl Node<Context<'_>, Output = RenderOutput> + Send + Sync,
) -> RenderOutput {
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");

View File

@@ -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<T, C: Send + 'n + Clone>(
fn switch<T, C>(
#[implementations(Context)] ctx: C,
condition: bool,
#[expose]
@@ -781,8 +782,8 @@ async fn switch<T, C: Send + 'n + Clone>(
Context -> List<GradientStops>,
)]
if_false: impl Node<C, Output = T>,
) -> T {
if condition { if_true.eval(ctx).await } else { if_false.eval(ctx).await }
) -> GPoll<T> {
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>(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]);
}
}

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_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<Color>) -> List
}
#[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 Some(image) = ::image::load_from_memory(image_data).ok() else {

View File

@@ -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<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Ctx,
fn repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl Ctx + DeriveCtx,
#[implementations(
Context -> List<Graphic>,
Context -> List<Vector>,
@@ -18,35 +19,35 @@ async fn repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(
Context -> List<Color>,
Context -> List<GradientStops>,
)]
content: impl Node<'n, Context<'static>, Output = List<T>>,
content: impl Node<Context<'_>, Output = List<T>>,
#[default(1)]
#[hard(1..)]
count: u32,
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`).
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<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Ctx,
pub fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl Ctx + DeriveCtx,
#[implementations(
Context -> List<Graphic>,
Context -> List<Vector>,
@@ -54,7 +55,7 @@ pub async fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
Context -> List<Color>,
Context -> List<GradientStops>,
)]
content: impl Node<'n, Context<'static>, Output = List<T>>,
content: impl Node<Context<'_>, Output = List<T>>,
#[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<T: Into<Graphic> + Default + Send + Clone + 'static>(
#[default(5)]
#[hard(1..)]
count: u32,
) -> List<T> {
) -> Result<List<T>, 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<T: Into<Graphic> + 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<T: Into<Graphic> + Default + Send + Clone + 'static>(
}
}
result_list
Ok(result_list)
}
#[node_macro::node(category("Repeat"))]
async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Ctx,
fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl Ctx + DeriveCtx,
#[implementations(
Context -> List<Graphic>,
Context -> List<Vector>,
@@ -102,7 +103,7 @@ async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
Context -> List<Color>,
Context -> List<GradientStops>,
)]
content: impl Node<'n, Context<'static>, Output = List<T>>,
content: impl Node<Context<'_>, Output = List<T>>,
start_angle: Angle,
#[unit(" px")]
#[default(5)]
@@ -110,7 +111,8 @@ async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
#[default(5)]
#[hard(1..)]
count: u32,
) -> List<T> {
) -> Result<List<T>, 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<T: Into<Graphic> + 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<T: Into<Graphic> + Default + Send + Clone + 'static>(
}
}
result_list
Ok(result_list)
}
#[node_macro::node(category("Repeat"), name("Repeat on Points"))]
async fn repeat_on_points<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Sync + Ctx + InjectVarArgs,
fn repeat_on_points<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl Ctx + DeriveCtx + InjectVarArgs,
points: List<Vector>,
#[implementations(
Context -> List<Graphic>,
@@ -147,40 +148,36 @@ async fn repeat_on_points<T: Into<Graphic> + Default + Send + Clone + 'static>(
Context -> List<Color>,
Context -> List<GradientStops>,
)]
content: impl Node<'n, Context<'static>, Output = List<T>>,
content: impl Node<Context<'_>, Output = List<T>>,
reverse: bool,
) -> List<T> {
) -> Result<List<T>, 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<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 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::<DAffine2>(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)]

View File

@@ -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<String>,
#[expose]
#[implementations(Context -> String)]
mapped: impl Node<Context<'static>, Output = String>,
) -> List<String> {
mapped: impl Node<Context<'_>, Output = String>,
) -> Result<List<String>, 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.

View File

@@ -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<T: ApplyTransform + 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll + ModifyFootprint,
fn transform<T: ApplyTransform + 'static>(
ctx: impl Ctx + ExtractFootprint + DeriveCtx + ModifyFootprint,
#[implementations(
Context -> DAffine2,
Context -> DVec2,
@@ -24,31 +25,24 @@ async fn transform<T: ApplyTransform + 'n + 'static>(
Context -> List<Color>,
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_rotation")] rotation: f64,
#[widget(ParsedWidgetOverride::Custom = "transform_scale")]
#[default(1., 1.)]
scale: 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 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.

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::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<T>(
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<V: VectorListIterMut + 'n + Send, F: IntoGraphicList + 'n + Send + 'static>(
async fn fill<V: VectorListIterMut+ Send, F: IntoGraphicList+ Send + 'static>(
_: 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<V, L: IntoF64Vec, P: IntoGraphicList + 'n + Send + 'static>(
async fn stroke<V, L: IntoF64Vec, P: IntoGraphicList+ Send + 'static>(
_: impl Ctx,
/// The content with vector paths to apply the stroke style to.
#[implementations(
@@ -323,7 +324,7 @@ async fn stroke<V, L: IntoF64Vec, P: IntoGraphicList + 'n + Send + 'static>(
dash_offset: f64,
) -> List<V>
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();
@@ -356,7 +357,7 @@ where
}
#[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,
points: List<Vector>,
/// 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<T: 'n + Send + Clone>(
async fn pack_strips<T: Send + Clone>(
_: impl Ctx,
#[implementations(
List<Graphic>,
@@ -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<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 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<Vector>) -> f64 {
}
#[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 {
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<Context<'_>, Output = List<Vector>>) -> Result<f64, Interrupt> {
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::<f64>()
})
.sum()
.sum())
}
#[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 {
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<Context<'_>, Output = List<Vector>>, centroid_type: CentroidType) -> Result<DVec2, Interrupt> {
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::<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) }
}
}