mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Start adapting nodes to new version
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user