Cut over to the graphene execution model

This commit is contained in:
Dennis Kobert
2026-08-04 13:15:21 +02:00
parent 7623b68318
commit 76ec799496
71 changed files with 3544 additions and 2378 deletions
+27 -40
View File
@@ -1,17 +1,14 @@
use crate::brush_cache::BrushCache;
use crate::brush_stroke::{BrushStroke, BrushStyle};
use core_types::Ctx;
use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::color::{Alpha, Color, Pixel, Sample};
use core_types::generic::FnNode;
use core_types::list::{Item, List};
use core_types::math::bbox::{AxisAlignedBbox, Bbox};
use core_types::registry::FutureWrapperNode;
use core_types::transform::Transform;
use core_types::uuid::NodeId;
use core_types::value::ClonedNode;
use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM};
use core_types::{Ctx, Node};
use glam::{DAffine2, DVec2};
use raster_nodes::blending_nodes::blend_colors;
use raster_nodes::std_nodes::{empty_image, extend_image_to_bounds};
@@ -63,7 +60,7 @@ impl<P: Pixel + Alpha> Sample for BrushStampGenerator<P> {
/// The feather exponent is calculated from hardness to determine edge softness.
/// Used internally to create the brush texture before stamping it repeatedly along a stroke path.
#[node_macro::node(category(""), skip_impl)]
fn brush_stamp_generator(#[unit(" px")] diameter: f64, color: Color, hardness: f64, flow: f64) -> BrushStampGenerator<Color> {
fn brush_stamp_generator(_: impl Ctx, #[unit(" px")] diameter: f64, color: Color, hardness: f64, flow: f64) -> BrushStampGenerator<Color> {
// Diameter
let radius = diameter / 2.;
@@ -83,9 +80,9 @@ fn brush_stamp_generator(#[unit(" px")] diameter: f64, color: Color, hardness: f
/// Used to efficiently paint brush strokes. Applies the same texture repeatedly at different positions with proper blending and boundary handling.
#[node_macro::node(category(""), skip_impl)]
fn blit<BlendFn>(mut target: List<Raster<CPU>>, texture: Raster<CPU>, positions: Vec<DVec2>, blend_mode: BlendFn) -> List<Raster<CPU>>
fn blit<BlendFn>(_: impl Ctx, mut target: List<Raster<CPU>>, texture: Raster<CPU>, positions: Vec<DVec2>, blend_mode: BlendFn) -> List<Raster<CPU>>
where
BlendFn: for<'any_input> Node<'any_input, (Color, Color), Output = Color>,
BlendFn: Fn(Color, Color) -> Color,
{
if positions.is_empty() {
return target;
@@ -125,7 +122,7 @@ where
for x in blit_area_offset.x..blit_area_offset.x + blit_area_dimensions.x {
let src_pixel = texture.data[texture_index(x, y)];
let dst_pixel = &mut element.data_mut().data[target_index(x + clamp_start.x, y + clamp_start.y)];
*dst_pixel = blend_mode.eval((src_pixel, *dst_pixel));
*dst_pixel = blend_mode(src_pixel, *dst_pixel);
}
}
}
@@ -134,10 +131,10 @@ where
target
}
pub async fn create_brush_texture(brush_style: &BrushStyle) -> Raster<CPU> {
let stamp = brush_stamp_generator(brush_style.diameter, brush_style.color, brush_style.hardness, brush_style.flow);
pub fn create_brush_texture(brush_style: &BrushStyle) -> Raster<CPU> {
let stamp = brush_stamp_generator(&(), brush_style.diameter, brush_style.color, brush_style.hardness, brush_style.flow);
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(brush_style.diameter), 0., -DVec2::splat(brush_style.diameter / 2.));
let blank_texture = empty_image((), transform, List::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap_or_default();
let blank_texture = empty_image(&(), transform, List::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap_or_default();
let image = blend_stamp_closure(stamp, blank_texture, |a, b| blend_colors(a, b, BlendMode::Normal, 1.));
image.into_element()
@@ -188,7 +185,7 @@ pub fn blend_with_mode(background: Item<Raster<CPU>>, foreground: Item<Raster<CP
/// Generates the brush strokes painted with the Brush tool as a raster image.
/// If an input image is supplied, strokes are drawn on top of it, expanding bounds as needed.
#[node_macro::node(category("Raster"))]
async fn brush(
fn brush(
_: impl Ctx,
/// Optional raster content that may be drawn onto.
mut background: List<Raster<CPU>>,
@@ -224,7 +221,7 @@ async fn brush(
let mut brush_plan = cache.compute_brush_plan(list_item, &draw_strokes);
// TODO: Find a way to handle more than one item
let Some(mut actual_image) = extend_image_to_bounds((), List::new_from_item(brush_plan.background), background_bounds).into_iter().next() else {
let Some(mut actual_image) = extend_image_to_bounds(&(), List::new_from_item(brush_plan.background), background_bounds).into_iter().next() else {
return List::new();
};
@@ -234,7 +231,7 @@ async fn brush(
// TODO: apply rotation from layer to stamp for non-rotationally-symmetric brushes.
let mut brush_texture = cache.get_cached_brush(&stroke.style);
if brush_texture.is_none() {
let tex = create_brush_texture(&stroke.style).await;
let tex = create_brush_texture(&stroke.style);
cache.store_brush(stroke.style.clone(), tex.clone());
brush_texture = Some(tex);
}
@@ -255,21 +252,14 @@ async fn brush(
let stroke_origin_in_layer = bbox.start - snap_offset - DVec2::splat(stroke.style.diameter / 2.);
let stroke_to_layer = DAffine2::from_translation(stroke_origin_in_layer) * DAffine2::from_scale(stroke_size);
let normal_blend = FnNode::new(|(a, b)| blend_colors(a, b, BlendMode::Normal, 1.));
let blit_node = BlitNode::new(
FutureWrapperNode::new(ClonedNode::new(brush_texture)),
FutureWrapperNode::new(ClonedNode::new(positions)),
FutureWrapperNode::new(ClonedNode::new(normal_blend)),
);
let blit_target = if idx == 0 {
let target = core::mem::take(&mut brush_plan.first_stroke_texture);
extend_image_to_bounds((), List::new_from_item(target), stroke_to_layer)
extend_image_to_bounds(&(), List::new_from_item(target), stroke_to_layer)
} else {
empty_image((), stroke_to_layer, List::new_from_element(Color::TRANSPARENT))
// EmptyImageNode::new(CopiedNode::new(stroke_to_layer), CopiedNode::new(Color::TRANSPARENT)).eval(())
empty_image(&(), stroke_to_layer, List::new_from_element(Color::TRANSPARENT))
};
let list = blit_node.eval(blit_target).await;
let list = blit(&(), blit_target, brush_texture, positions, |a, b| blend_colors(a, b, BlendMode::Normal, 1.));
assert_eq!(list.len(), 1);
list.into_iter().next().unwrap_or_default()
};
@@ -291,7 +281,7 @@ async fn brush(
for stroke in trace.into_iter().map(|row| row.into_element()) {
let mut brush_texture = cache.get_cached_brush(&stroke.style);
if brush_texture.is_none() {
let tex = create_brush_texture(&stroke.style).await;
let tex = create_brush_texture(&stroke.style);
cache.store_brush(stroke.style.clone(), tex.clone());
brush_texture = Some(tex);
}
@@ -305,17 +295,15 @@ async fn brush(
_ => BlendMode::Restore,
};
let blend_params = FnNode::new(move |(a, b)| blend_colors(a, b, mask_blend_mode, 1.));
let blit_node = BlitNode::new(
FutureWrapperNode::new(ClonedNode::new(brush_texture)),
FutureWrapperNode::new(ClonedNode::new(positions)),
FutureWrapperNode::new(ClonedNode::new(blend_params)),
);
erase_restore_mask = blit_node.eval(List::new_from_item(erase_restore_mask)).await.into_iter().next().unwrap_or_default();
erase_restore_mask = blit(&(), List::new_from_item(erase_restore_mask), brush_texture, positions, move |a, b| {
blend_colors(a, b, mask_blend_mode, 1.)
})
.into_iter()
.next()
.unwrap_or_default();
}
let blend_params = FnNode::new(|(a, b)| blend_colors(a, b, BlendMode::MultiplyAlpha, 1.));
actual_image = blend_image_closure(erase_restore_mask, actual_image, |a, b| blend_params.eval((a, b)));
actual_image = blend_image_closure(erase_restore_mask, actual_image, |a, b| blend_colors(a, b, BlendMode::MultiplyAlpha, 1.));
}
let transform: DAffine2 = actual_image.attribute_cloned_or_default(ATTR_TRANSFORM);
@@ -410,16 +398,16 @@ mod test {
#[test]
fn test_brush_texture() {
let size = 20.;
let image = brush_stamp_generator(size, Color::BLACK, 100., 100.);
let image = brush_stamp_generator(&(), size, Color::BLACK, 100., 100.);
assert_eq!(image.transform(), DAffine2::from_scale_angle_translation(DVec2::splat(size.ceil()), 0., -DVec2::splat(size / 2.)));
// center pixel should be BLACK
assert_eq!(image.sample(DVec2::splat(0.), DVec2::ONE), Some(Color::BLACK));
}
#[tokio::test]
async fn test_brush_output_size() {
#[test]
fn test_brush_output_size() {
let image = brush(
(),
&(),
&BrushCache::default(),
List::new_from_element(Raster::new_cpu(Image::<Color>::default())),
List::new_from_element(BrushStroke {
@@ -433,8 +421,7 @@ mod test {
blend_mode: BlendMode::Normal,
},
}),
)
.await;
);
assert_eq!(image.element(0).unwrap().width, 20);
}
}
+14 -13
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.
+2 -2
View File
@@ -47,7 +47,7 @@ fn read_gradient(ctx: impl Ctx + ExtractVarArgs) -> List<GradientStops> {
}
#[node_macro::node(category("Context"), path(core_types::vector))]
async fn read_position(
fn read_position(
ctx: impl Ctx + ExtractPosition,
_primary: (),
/// The number of nested loops to traverse outwards (from the innermost loop) to get the position from. The most upstream loop is level 0, and downstream loops add levels.
@@ -64,7 +64,7 @@ async fn read_position(
///
/// Nested loops can enable 2D or higher-dimensional iteration by using the *Loop Level* parameter to read the index from outer levels of loops.
#[node_macro::node(category("Context"), path(core_types::vector))]
async fn read_index(
fn read_index(
ctx: impl Ctx + ExtractIndex,
_primary: (),
/// The number of nested loops to traverse outwards (from the innermost loop) to get the index from. The most upstream loop is level 0, and downstream loops add levels.
@@ -1,9 +1,10 @@
use core::f64;
use core_types::context::{CloneVarArgs, Context, ContextFeatures, Ctx, ExtractAll};
use core_types::Color;
use core_types::context::{Context, ContextModification, 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;
use core_types::{Color, OwnedContextImpl};
use glam::{DAffine2, DVec2};
use graphic_types::vector_types::GradientStops;
use graphic_types::{Artboard, Graphic, Vector};
@@ -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,80 +42,10 @@ 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
}
#[cfg(test)]
mod tests {
use super::*;
use core_types::graphene_hash::CacheHash;
use core_types::transform::Footprint;
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
/// Verifies that nullified context fields don't affect the cache hash — only the kept features matter.
#[test]
fn test_nullified_context_hash_stability() {
use core_types::Context;
use std::sync::Arc;
let original_ctx: Context = Some(Arc::new(
OwnedContextImpl::empty()
.with_footprint(Footprint::default())
.with_index(1)
.with_real_time(10.5)
.with_vararg(Box::new("test"))
.with_animation_time(20.25),
));
// A second context with different values for the nullified fields
let changed_ctx: Context = Some(Arc::new(
OwnedContextImpl::empty()
.with_footprint(Footprint::default())
.with_index(2)
.with_real_time(999.9)
.with_vararg(Box::new("test"))
.with_animation_time(888.8),
));
// Nullify everything — both should hash the same regardless of their field values
let features_to_keep = ContextFeatures::empty();
let nullified1 = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), features_to_keep);
let nullified2 = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), features_to_keep);
let mut hasher1 = DefaultHasher::new();
nullified1.cache_hash(&mut hasher1);
let mut hasher2 = DefaultHasher::new();
nullified2.cache_hash(&mut hasher2);
assert_eq!(
hasher1.finish(),
hasher2.finish(),
"Hash of nullified context should remain stable regardless of input changes when features are nullified"
);
// Keep only footprint and varargs — both have the same footprint and vararg, so hash should still match
let partial_features = ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS;
let partial1 = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), partial_features);
let partial2 = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), partial_features);
let mut hasher3 = DefaultHasher::new();
partial1.cache_hash(&mut hasher3);
let mut hasher4 = DefaultHasher::new();
partial2.cache_hash(&mut hasher4);
assert_eq!(
hasher3.finish(),
hasher4.finish(),
"Hash should be stable when keeping only footprint and varargs and their values are the same"
);
}
modification: ContextModification,
) -> GPoll<T> {
let scope = ctx.scope().nullified(modification.features, Some(&modification.sources));
value.eval(&ctx.nullified(modification.features, &scope))
}
+244 -33
View File
@@ -1,54 +1,265 @@
use core_types::WasmNotSend;
use core_types::arena::{Arena, ArenaCell};
use core_types::context::{Ctx, CtxSnapshot, DeriveCtx, ExtractAll};
use core_types::frame_table::{FrameTable, Lookup};
use core_types::gpoll::{Extent, Finality, GPoll, Interrupt};
use core_types::graphene_hash::CacheHash;
use core_types::memo::*;
use std::hash::DefaultHasher;
use std::hash::Hasher;
use core_types::node::Node;
use core_types::registry::cache_key;
use std::sync::Arc;
use std::sync::Mutex;
/// Helps speed up repeated renders in a computationally-heavy part of the node graph.
///
/// 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 {
// Caches the output of a given node called with a specific input.
//
// A cache miss occurs when the Option is None. In this case, the node evaluates the inner node and memoizes (stores) the result.
//
// A cache hit occurs when the Option is Some and has a stored hash matching the hash of the call argument. In this case, the node returns the cached value without re-evaluating the inner node.
//
// Currently, only one input-output pair is cached. Subsequent calls with different inputs will overwrite the previous cache.
let mut hasher = DefaultHasher::new();
input.cache_hash(&mut hasher);
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;
#[node_macro::node(category("General"), path(graphene_core::memo), skip_impl, extent(memoize_extent))]
fn memoize<I: CacheHash, T: Clone>(input: I, #[data] cache: Arc<Mutex<Option<(u64, T, Finality)>>>, content: impl Node<I, Output = T>) -> GPoll<T> {
let key = cache_key(&input);
if let Some((hash, value, finality)) = cache.lock().unwrap().as_ref()
&& *hash == key
{
return match finality {
Finality::AllFinal => GPoll::Final(value.clone()),
Finality::Partial => GPoll::Partial(value.clone()),
};
}
let value = content.eval(input).await;
*cache.lock().unwrap() = Some((hash, value.clone()));
value
let result = content.eval(input);
match &result {
GPoll::Final(value) => *cache.lock().unwrap() = Some((key, value.clone(), Finality::AllFinal)),
GPoll::Partial(value) => *cache.lock().unwrap() = Some((key, value.clone(), Finality::Partial)),
GPoll::Pending | GPoll::Fallback(_) | GPoll::Error(_) => {}
}
result
}
type MonitorValue<I, T> = Arc<Mutex<Option<Arc<IORecord<I, T>>>>>;
fn memoize_extent<C, T, NodeContent>(node: &MemoizeNode<T, NodeContent>, ctx: &C) -> GPoll<Extent>
where
T: Clone,
NodeContent: Node<C, Output = T>,
{
node.content.extent(ctx)
}
#[node_macro::node(category(""), path(graphene_core::memo), skip_impl, extent(frame_memo_extent))]
fn frame_memo<'e, T: Clone + 'static>(ctx: impl Ctx + CacheHash + ExtractArena<'e>, #[data] cell: ArenaCell<FrameTable<T, 32>>, content: impl Node<Context<'_>, Output = T>) -> GPoll<&'e T> {
let arena = ctx.arena();
let table = match cell.load(arena) {
Some(table) => table,
None => match arena.alloc(FrameTable::new()) {
Some((table, weak)) => {
cell.store(weak);
table
}
None => return park(arena, content.eval(ctx)),
},
};
match table.lookup(cache_key(ctx)) {
Lookup::Hit(Finality::AllFinal, value) => GPoll::Final(value),
Lookup::Hit(Finality::Partial, value) => GPoll::Partial(value),
Lookup::Vacant(slot) => match content.eval(ctx) {
GPoll::Final(value) => GPoll::Final(slot.publish(value, Finality::AllFinal)),
GPoll::Partial(value) => GPoll::Partial(slot.publish(value, Finality::Partial)),
unpublishable => {
slot.release();
park(arena, unpublishable)
}
},
Lookup::Full => park(arena, content.eval(ctx)),
}
}
fn frame_memo_extent<C, T, NodeContent>(node: &FrameMemoNode<T, NodeContent>, ctx: &C) -> GPoll<Extent>
where
T: Clone + 'static,
NodeContent: Node<C, Output = T>,
{
node.content.extent(ctx)
}
pub fn park<T>(arena: &Arena, result: GPoll<T>) -> GPoll<&T> {
match result {
GPoll::Final(value) => match arena.alloc(value) {
Some((parked, _)) => GPoll::Final(parked),
None => GPoll::arena_exhausted(),
},
GPoll::Partial(value) => match arena.alloc(value) {
Some((parked, _)) => GPoll::Partial(parked),
None => GPoll::arena_exhausted(),
},
GPoll::Fallback(boxed) => {
let (value, error) = *boxed;
match arena.alloc(value) {
Some((parked, _)) => GPoll::Fallback(Box::new((parked, error))),
None => GPoll::arena_exhausted(),
}
}
GPoll::Pending => GPoll::Pending,
GPoll::Error(error) => GPoll::Error(error),
}
}
type MonitorValue<T> = Arc<Mutex<Option<Arc<IORecord<CtxSnapshot, 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>(
input: I,
fn monitor<T: Clone + 'static + Send + Sync>(
ctx: impl Ctx + DeriveCtx + ExtractAll,
#[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
io: MonitorValue<T>,
content: impl Node<Context<'_>, Output = T>,
) -> Result<T, Interrupt> {
let output = content.eval(&ctx.derived())?;
*io.lock().unwrap() = Some(Arc::new(IORecord {
input: CtxSnapshot::capture(ctx),
output: output.clone(),
}));
Ok(output)
}
fn serialize_monitor<I: Clone + 'static + Send + Sync, T: Clone + 'static + Send + Sync>(io: &MonitorValue<I, T>) -> Option<Arc<dyn std::any::Any + Send + Sync>> {
fn serialize_monitor<T: Clone + 'static + Send + Sync>(io: &MonitorValue<T>) -> Option<Arc<dyn std::any::Any + Send + Sync>> {
let io = io.lock().unwrap();
io.as_ref().map(|output| output.clone() as Arc<dyn std::any::Any + Send + Sync>)
}
#[cfg(test)]
mod tests {
use super::*;
use core_types::SourceId;
use core_types::Type;
use core_types::concrete;
use core_types::context::{ContextImpl, EvalScope};
use core_types::registry::{EdgeHandle, ErasedLendNode, ErasedNode};
use std::sync::atomic::{AtomicU32, Ordering};
struct CountingNode(AtomicU32);
impl<Input> Node<Input> for CountingNode {
type Output = u32;
fn eval(&self, _input: &Input) -> GPoll<u32> {
GPoll::Final(self.0.fetch_add(1, Ordering::Relaxed) + 1)
}
}
struct PartialCountingNode(AtomicU32);
impl<Input> Node<Input> for PartialCountingNode {
type Output = u32;
fn eval(&self, _input: &Input) -> GPoll<u32> {
GPoll::Partial(self.0.fetch_add(1, Ordering::Relaxed) + 1)
}
}
struct ValueNode<T>(T);
impl<T: Clone, Input> Node<Input> for ValueNode<T> {
type Output = T;
fn eval(&self, _input: &Input) -> GPoll<T> {
GPoll::Final(self.0.clone())
}
}
fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
EvalScope::new(Some(0.5), None, None, generations, arena)
}
#[test]
fn monitor_serialize_exposes_the_io_record_through_the_edge() {
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let handle = EdgeHandle::new(Arc::new(MonitorNode::new(ValueNode(11u32))) as Arc<ErasedNode<u32>>);
assert!(handle.serialize().is_none(), "no record before the first eval");
let edge = handle.duplicate().downcast::<u32>().unwrap();
assert_eq!(edge.eval(&ctx), GPoll::Final(11));
let record = handle.serialize().expect("the eval landed a record");
let record = record.downcast_ref::<IORecord<CtxSnapshot, u32>>().expect("the record is the monitor io");
assert_eq!(record.output, 11);
}
#[test]
fn memoize_caches_across_evals() {
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let memoized = MemoizeNode::new(CountingNode(AtomicU32::new(0)));
assert_eq!(memoized.eval(&ctx), GPoll::Final(1));
assert_eq!(memoized.eval(&ctx), GPoll::Final(1));
}
#[test]
fn memo_invalidates_on_generation_bump() {
let arena = Arena::new(1024);
let source: SourceId = 7;
let before = [(source, 1)];
let after = [(source, 2)];
let scope_before = scope_fixture(&before, &arena);
let scope_after = scope_fixture(&after, &arena);
let memoized = MemoizeNode::new(CountingNode(AtomicU32::new(0)));
assert_eq!(memoized.eval(&ContextImpl::root(&scope_before)), GPoll::Final(1));
assert_eq!(memoized.eval(&ContextImpl::root(&scope_before)), GPoll::Final(1));
assert_eq!(memoized.eval(&ContextImpl::root(&scope_after)), GPoll::Final(2));
}
#[test]
fn memo_replays_partiality_on_hit() {
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let memoized = MemoizeNode::new(PartialCountingNode(AtomicU32::new(0)));
assert_eq!(memoized.eval(&ctx), GPoll::Partial(1));
assert_eq!(memoized.eval(&ctx), GPoll::Partial(1));
}
#[test]
fn memoized_edges_stack_and_rewire() {
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let edge = EdgeHandle::new(Arc::new(CountingNode(AtomicU32::new(0))) as Arc<ErasedNode<u32>>);
let memoized = EdgeHandle::new(Arc::new(MemoizeNode::new(edge.downcast::<u32>().unwrap())) as Arc<ErasedNode<u32>>);
let stacked = MemoizeNode::new(memoized.downcast::<u32>().unwrap());
assert_eq!(stacked.eval(&ctx), GPoll::Final(1));
assert_eq!(stacked.eval(&ctx), GPoll::Final(1));
}
#[test]
fn frame_memo_turns_an_owned_edge_into_a_lending_edge() {
let arena = Arena::new(4096);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let edge = EdgeHandle::new(Arc::new(ValueNode("lent out".to_string())) as Arc<ErasedNode<String>>);
let lending = EdgeHandle::new_ref(Arc::new(FrameMemoNode::new(edge.downcast::<String>().unwrap())) as Arc<ErasedLendNode<String>>);
assert_eq!(*lending.ty(), core_types::registry::lend_edge_type::<String>());
let node = lending.downcast_lend::<String>().unwrap();
let GPoll::Final(first) = node.eval(&ctx) else {
panic!("lend must fill the frame table and lend");
};
let GPoll::Final(second) = node.eval(&ctx) else {
panic!("second eval must lend the published value");
};
assert_eq!(first, "lent out");
assert!(std::ptr::eq(first, second));
}
}
+12 -8
View File
@@ -1,9 +1,8 @@
use core_types::{Ctx, ExtractFootprint, ops::Convert, transform::Footprint};
use core_types::ExtractAll;
use core_types::runtime::SourceFuture;
use core_types::{Ctx, ops::Convert, ops::ConvertAsync, transform::Footprint};
use std::marker::PhantomData;
// Re-export TypeNode from core-types for convenience
pub use core_types::ops::TypeNode;
/// Passes-through the input value without changing it. This is useful for rerouting wires for organization purposes.
#[node_macro::node(category("General"), skip_impl)]
fn passthrough<'i, T: 'i + Send>(_: impl Ctx, content: T) -> T {
@@ -11,13 +10,18 @@ fn passthrough<'i, T: 'i + Send>(_: impl Ctx, content: T) -> T {
}
#[node_macro::node(category(""), skip_impl)]
fn into<'i, T: 'i + Send + Into<O>, O: 'i + Send>(_: impl Ctx, value: T, _out_ty: PhantomData<O>) -> O {
fn into<T: Send + Into<O>, O: Send>(_: impl Ctx, value: T, #[data] _out_ty: PhantomData<O>) -> O {
value.into()
}
#[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 + ExtractAll, value: T, converter: C, #[data] _out_ty: PhantomData<O>) -> O {
value.convert(*ctx.try_footprint().unwrap_or(&Footprint::DEFAULT), converter)
}
#[node_macro::node(category(""), skip_impl)]
fn convert_async<T: Send + ConvertAsync<O, C>, O: Send + 'static, C: Send>(ctx: impl Ctx + ExtractAll, value: T, converter: C, #[data] _out_ty: PhantomData<O>) -> SourceFuture<O> {
value.convert(*ctx.try_footprint().unwrap_or(&Footprint::DEFAULT), converter)
}
#[cfg(test)]
@@ -26,6 +30,6 @@ mod test {
#[test]
pub fn passthrough_node() {
assert_eq!(passthrough((), &4), &4);
assert_eq!(passthrough(&(), &4), &4);
}
}
+10 -14
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, ModifyFootprint};
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 + DeriveCtx + ModifyFootprint,
/// 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),
)
))
}
+30 -26
View File
@@ -1,8 +1,9 @@
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::gpoll::Interrupt;
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::{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 + Clone + 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>,
@@ -565,7 +569,7 @@ pub async fn wrap_graphic<T: Into<Graphic> + 'n>(
/// Converts a list of graphical content into a `Graphic[]` by placing it into an element of a new wrapper `Graphic[]`.
/// If it is already a `Graphic[]`, it is not wrapped again. Use the 'Wrap Graphic' node if wrapping is always desired.
#[node_macro::node(category("General"))]
pub async fn to_graphic<T: IntoGraphicList>(
pub fn to_graphic<T: IntoGraphicList>(
_: impl Ctx,
#[implementations(
List<Graphic>,
@@ -583,7 +587,7 @@ pub async fn to_graphic<T: IntoGraphicList>(
/// Removes a level of nesting from a `Graphic[]`, or all nesting if "Fully Flatten" is enabled.
#[node_macro::node(category("General"))]
pub async fn flatten_graphic(_: impl Ctx, content: List<Graphic>, fully_flatten: bool) -> List<Graphic> {
pub fn flatten_graphic(_: impl Ctx, content: List<Graphic>, fully_flatten: bool) -> List<Graphic> {
// TODO: Avoid mutable reference, instead return a new List<Graphic>?
fn flatten_list(output_graphic_list: &mut List<Graphic>, current_graphic_list: List<Graphic>, fully_flatten: bool, recursion_depth: usize) {
for index in 0..current_graphic_list.len() {
@@ -620,7 +624,7 @@ pub async fn flatten_graphic(_: impl Ctx, content: List<Graphic>, fully_flatten:
/// Converts a `Graphic[]` into a `Vector[]` by deeply flattening any vector content it contains, and discarding any non-vector content.
#[node_macro::node(category("Vector"))]
pub async fn flatten_vector<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
pub fn flatten_vector<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
let graphic_list = content.into_graphic_list();
let mut output: List<Vector> = graphic_list.clone().into_flattened_list();
@@ -653,19 +657,19 @@ pub async fn flatten_vector<T: IntoGraphicList>(_: impl Ctx, #[implementations(L
/// Converts a `Graphic[]` into a `Raster[]` by deeply flattening any raster content it contains, and discarding any non-raster content.
#[node_macro::node(category("Raster"))]
pub async fn flatten_raster<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Raster<CPU>>)] content: T) -> List<Raster<CPU>> {
pub fn flatten_raster<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Raster<CPU>>)] content: T) -> List<Raster<CPU>> {
content.into_flattened_list()
}
/// Converts a `Graphic[]` into a `Color[]` by deeply flattening any color content it contains, and discarding any non-color content.
#[node_macro::node(category("General"))]
pub async fn flatten_color<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Color>)] content: T) -> List<Color> {
pub fn flatten_color<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Color>)] content: T) -> List<Color> {
content.into_flattened_list()
}
/// Converts a `Graphic[]` into a `GradientStops[]` by deeply flattening any gradient content it contains, and discarding any non-gradient content.
#[node_macro::node(category("General"))]
pub async fn flatten_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<GradientStops>)] content: T) -> List<GradientStops> {
pub fn flatten_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<GradientStops>)] content: T) -> List<GradientStops> {
content.into_flattened_list()
}
-27
View File
@@ -1,27 +0,0 @@
use core_types::NodeIO;
use core_types::WasmNotSend;
pub use core_types::registry::{DowncastBothNode, DynAnyNode, FutureWrapperNode, PanicNode};
pub use core_types::{Node, generic, ops};
use dyn_any::StaticType;
pub use graph_craft::proto::{Any, NodeContainer, TypeErasedBox, TypeErasedNode};
use graph_craft::proto::{FutureAny, SharedNodeContainer};
pub trait IntoTypeErasedNode<'n> {
fn into_type_erased(self) -> TypeErasedBox<'n>;
}
impl<'n, N: 'n> IntoTypeErasedNode<'n> for N
where
N: for<'i> NodeIO<'i, Any<'i>, Output = FutureAny<'i>> + Sync + WasmNotSend,
{
fn into_type_erased(self) -> TypeErasedBox<'n> {
Box::new(self)
}
}
pub fn input_node<O: StaticType>(n: SharedNodeContainer) -> DowncastBothNode<(), O> {
downcast_node(n)
}
pub fn downcast_node<I: StaticType, O: StaticType>(n: SharedNodeContainer) -> DowncastBothNode<I, O> {
DowncastBothNode::new(n)
}
+1 -1
View File
@@ -1,9 +1,9 @@
pub mod any;
pub mod platform_application_io;
pub mod render_background;
pub mod render_cache;
pub mod render_node;
pub mod render_pixel_preview;
pub mod runtime;
pub mod text;
pub use blending_nodes;
pub use brush_nodes as brush;
@@ -3,9 +3,11 @@ use base64::Engine;
#[cfg(target_family = "wasm")]
use canvas_utils::{Canvas, CanvasHandle};
use core_types::color::SRGBA8;
use core_types::gpoll::GPoll;
use core_types::list::{Item, List};
#[cfg(target_family = "wasm")]
use core_types::math::bbox::Bbox;
use core_types::runtime::SourceFuture;
#[cfg(target_family = "wasm")]
use core_types::transform::Footprint;
#[cfg(target_family = "wasm")]
@@ -137,7 +139,7 @@ fn image_to_bytes(_: impl Ctx, image: List<Raster<CPU>>) -> List<u8> {
/// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue.
#[node_macro::node(category("Web Request"))]
async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[name("URL")] url: String) -> Arc<[u8]> {
async fn load_resource(_: impl Ctx, _primary: (), #[name("URL")] url: String) -> Arc<[u8]> {
let placeholder = || -> Arc<[u8]> { Arc::from(Vec::<u8>::new()) };
let response = match reqwest::Client::new().get(&url).send().await {
@@ -185,14 +187,14 @@ fn decode_image(_: impl Ctx, data: Arc<[u8]>) -> List<Raster<CPU>> {
#[cfg(target_family = "wasm")]
#[node_macro::node(category(""))]
async fn create_canvas(_: impl Ctx) -> CanvasHandle {
fn create_canvas(_: impl Ctx) -> CanvasHandle {
CanvasHandle::new()
}
/// Renders a view of the input graphic within an area defined by the *Footprint*.
#[cfg(target_family = "wasm")]
#[node_macro::node(category(""))]
async fn rasterize<T: WasmNotSend + Clone + 'n>(
async fn rasterize<T: WasmNotSend + Clone>(
_: impl Ctx,
#[implementations(
List<Vector>,
@@ -262,29 +264,37 @@ where
}
#[node_macro::node(category(""), inject_scope)]
pub async fn editor_api<'a: 'n>(_: impl Ctx, #[scope("editor-api")] editor_api: &'a PlatformEditorApi) -> &'a PlatformEditorApi {
pub fn editor_api(_: impl Ctx, #[scope("editor-api")] editor_api: Arc<PlatformEditorApi>) -> Arc<PlatformEditorApi> {
editor_api
}
#[node_macro::node(category(""))]
pub async fn resource<'a: 'n>(_: impl Ctx, hash: ResourceHash, #[scope(editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi) -> Resource {
let application_io = editor_api.application_io.as_ref().expect("ApplicationIo must be available when using resources");
application_io.load_resource(hash).await.unwrap_or_else(|| {
panic!("Resource {hash} not found");
pub fn resource(_: impl Ctx, hash: ResourceHash, #[scope(editor_api::IDENTIFIER)] editor_api: Arc<PlatformEditorApi>) -> SourceFuture<GPoll<Resource>> {
let application_io = editor_api.application_io.clone();
Box::pin(async move {
let Some(application_io) = application_io else {
return GPoll::error("ApplicationIo not available");
};
match application_io.load_resource(hash).await {
Some(resource) => GPoll::Final(resource),
None => GPoll::error("resource not found"),
}
})
}
#[node_macro::node(category(""), inject_scope)]
pub async fn wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi) -> &'a ::wgpu_executor::WgpuExecutor {
editor_api
.application_io
.as_ref()
.expect("ApplicationIo not not available")
.gpu_executor()
.expect("GPU executor not available")
pub fn wgpu_executor(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Arc<PlatformEditorApi>) -> ::wgpu_executor::WgpuExecutorHandle {
::wgpu_executor::WgpuExecutorHandle(
editor_api
.application_io
.as_ref()
.expect("ApplicationIo not not available")
.gpu_executor_arc()
.expect("GPU executor not available"),
)
}
#[node_macro::node(category(""), inject_scope)]
pub async fn try_wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi) -> Option<&'a ::wgpu_executor::WgpuExecutor> {
editor_api.application_io.as_ref()?.gpu_executor()
pub fn try_wgpu_executor(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Arc<PlatformEditorApi>) -> Option<::wgpu_executor::WgpuExecutorHandle> {
editor_api.application_io.as_ref()?.gpu_executor_arc().map(::wgpu_executor::WgpuExecutorHandle)
}
+13 -19
View File
@@ -9,14 +9,10 @@ use graphic_types::raster_types::Texture;
use rendering::{RenderParams, SvgRender, SvgRenderOutput};
use std::fmt::Write;
use wgpu::util::DeviceExt;
use wgpu_executor::{AsyncWgpuPipeline, WgpuExecutor, WgpuPipelineCache};
use wgpu_executor::{WgpuExecutor, WgpuPipeline, WgpuPipelineCache};
#[node_macro::node(category(""))]
async fn render_background<'a: 'n>(
ctx: impl Ctx + ExtractFootprint + ExtractVarArgs,
#[scope(composite_background_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache,
data: RenderOutput,
) -> RenderOutput {
fn render_background<'a>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, #[scope(composite_background_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache, data: RenderOutput) -> RenderOutput {
let footprint = ctx.footprint();
let render_params = ctx
.vararg(0)
@@ -35,14 +31,12 @@ async fn render_background<'a: 'n>(
let data = match foreground_data {
RenderOutputType::Texture(foreground_texture) => {
let doc_to_screen = render_params.footprint.transform.as_affine2();
let blended = pipeline
.run::<CompositeBackground>(&CompositeBackgroundArgs {
foreground: foreground_texture.as_ref(),
backgrounds: &metadata.backgrounds,
document_to_screen: doc_to_screen,
zoom: render_params.viewport_zoom.to_f32(),
})
.await;
let blended = pipeline.run::<CompositeBackground>(&CompositeBackgroundArgs {
foreground: foreground_texture.as_ref(),
backgrounds: &metadata.backgrounds,
document_to_screen: doc_to_screen,
zoom: render_params.viewport_zoom.to_f32(),
});
RenderOutputType::Texture(blended)
}
@@ -121,9 +115,9 @@ async fn render_background<'a: 'n>(
}
#[node_macro::node(category(""), inject_scope)]
async fn composite_background_pipeline<'a: 'n>(
fn composite_background_pipeline(
_ctx: impl Ctx,
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<&'a WgpuExecutor>,
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<wgpu_executor::WgpuExecutorHandle>,
#[data] pipeline: WgpuPipelineCache,
) -> WgpuPipelineCache {
if let Some(executor) = executor {
@@ -148,7 +142,7 @@ pub struct CompositeBackgroundArgs<'a> {
zoom: f32,
}
impl AsyncWgpuPipeline for CompositeBackground {
impl WgpuPipeline for CompositeBackground {
type Args<'a> = CompositeBackgroundArgs<'a>;
type Out = Texture;
@@ -331,7 +325,7 @@ impl AsyncWgpuPipeline for CompositeBackground {
}
}
async fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> Self::Out {
fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> Self::Out {
let &CompositeBackgroundArgs {
foreground,
backgrounds,
@@ -340,7 +334,7 @@ impl AsyncWgpuPipeline for CompositeBackground {
} = args;
let foreground_size = foreground.size();
let output = executor.request_texture(UVec2::new(foreground_size.width, foreground_size.height)).await;
let output = executor.request_texture(UVec2::new(foreground_size.width, foreground_size.height));
if zoom <= 0. {
return output;
+48 -70
View File
@@ -1,8 +1,9 @@
//! Tile-based render caching for efficient viewport panning.
use core_types::gpoll::Interrupt;
use core_types::math::bbox::AxisAlignedBbox;
use core_types::transform::{Footprint, RenderQuality, Transform};
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl};
use core_types::{Ctx, DeriveCtx, ExtractAll};
use glam::{DAffine2, DVec2, IVec2, UVec2};
use graph_craft::application_io::PlatformEditorApi;
use graph_craft::document::value::{RenderOutput, RenderOutputType};
@@ -11,7 +12,6 @@ use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams};
use std::collections::HashSet;
use std::hash::Hash;
use std::sync::{Arc, Mutex};
use wgpu_executor::WgpuExecutor;
pub const TILE_SIZE: u32 = 256;
pub const MAX_CACHE_MEMORY_BYTES: usize = 512 * 1024 * 1024;
@@ -321,25 +321,23 @@ fn flood_fill(start: &TileCoord, tile_set: &HashSet<TileCoord>, visited: &mut Ha
}
#[node_macro::node(category(""))]
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,
pub fn render_output_cache(
ctx: impl Ctx + ExtractAll + DeriveCtx,
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<wgpu_executor::WgpuExecutorHandle>,
#[scope(crate::platform_application_io::editor_api::IDENTIFIER)] editor_api: std::sync::Arc<PlatformEditorApi>,
data: impl Node<Context<'_>, Output = RenderOutput>,
#[data] tile_cache: TileCache,
) -> RenderOutput {
let footprint = ctx.footprint();
) -> Result<RenderOutput, Interrupt> {
let footprint = *ctx.footprint();
let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::<RenderParams>()) else {
log::warn!("render_output_cache: missing or invalid render params, falling back to direct render");
let context = OwnedContextImpl::from(ctx.clone()).with_footprint(*footprint);
return data.eval(context.into_context()).await;
return data.eval(&ctx.derived());
};
// Fall back to direct render for non-Vello or zero-size viewports
let physical_resolution = footprint.resolution;
if !matches!(render_params.render_output_type, RenderOutputTypeRequest::Vello) || physical_resolution.x == 0 || physical_resolution.y == 0 {
let context = OwnedContextImpl::from(ctx.clone()).with_footprint(*footprint).with_vararg(Box::new(render_params.clone()));
return data.eval(context.into_context()).await;
return data.eval(&ctx.derived());
}
let zoom = footprint.scale_magnitudes().x;
@@ -375,8 +373,38 @@ pub async fn render_output_cache<'a: 'n>(
if missing_region.tiles.is_empty() {
continue;
}
let region = render_missing_region(missing_region, |ctx| data.eval(ctx), ctx.clone(), render_params, &footprint.transform, &device_origin_offset).await;
new_regions.push(region);
let min_tile = missing_region.tiles.iter().fold(IVec2::new(i32::MAX, i32::MAX), |acc, t| acc.min(IVec2::new(t.x, t.y)));
let max_tile = missing_region.tiles.iter().fold(IVec2::new(i32::MIN, i32::MIN), |acc, t| acc.max(IVec2::new(t.x, t.y)));
let tile_count = (max_tile - min_tile) + IVec2::ONE;
let region_pixel_size = (tile_count * TILE_SIZE as i32).as_uvec2();
let tile_global_offset = min_tile.as_dvec2() * TILE_SIZE as f64 + device_origin_offset;
let region_transform = DAffine2::from_translation(-tile_global_offset) * footprint.transform;
let region_footprint = Footprint {
transform: region_transform,
resolution: region_pixel_size,
quality: RenderQuality::Full,
};
let mut result = data.eval(&ctx.with_footprint(&region_footprint))?;
let RenderOutputType::Texture(texture) = result.data else {
unreachable!("render_output_cache: expected texture output from Vello render");
};
result.metadata.apply_transform(region_transform.inverse());
let memory_size = (region_pixel_size.x * region_pixel_size.y) as usize * BYTES_PER_PIXEL;
new_regions.push(CachedRegion {
texture,
texture_size: region_pixel_size,
tiles: missing_region.tiles.clone(),
metadata: result.metadata,
last_access: 0,
memory_size,
});
}
tile_cache.store_regions(new_regions.clone());
@@ -385,68 +413,18 @@ pub async fn render_output_cache<'a: 'n>(
// If no regions, fall back to direct render
if all_regions.is_empty() {
let context = OwnedContextImpl::from(ctx.clone()).with_footprint(*footprint).with_vararg(Box::new(render_params.clone()));
return data.eval(context.into_context()).await;
return data.eval(&ctx.derived());
}
let executor = executor.expect("GPU executor not available");
let output_texture = executor.request_texture(physical_resolution).await;
let output_texture = executor.request_texture(physical_resolution);
let combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &footprint.transform, executor);
let combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &footprint.transform, &executor);
RenderOutput {
Ok(RenderOutput {
data: RenderOutputType::Texture(output_texture),
metadata: combined_metadata,
}
}
async fn render_missing_region<F, Fut>(
region: &RenderRegion,
render_fn: F,
ctx: impl Ctx + ExtractAll + CloneVarArgs,
render_params: &RenderParams,
viewport_transform: &DAffine2,
viewport_origin_offset: &DVec2,
) -> CachedRegion
where
F: Fn(Context<'static>) -> 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)));
let max_tile = region.tiles.iter().fold(IVec2::new(i32::MIN, i32::MIN), |acc, t| acc.max(IVec2::new(t.x, t.y)));
let tile_count = (max_tile - min_tile) + IVec2::ONE;
let region_pixel_size = (tile_count * TILE_SIZE as i32).as_uvec2();
let tile_global_offset = min_tile.as_dvec2() * TILE_SIZE as f64 + *viewport_origin_offset;
let region_transform = DAffine2::from_translation(-tile_global_offset) * *viewport_transform;
let region_footprint = Footprint {
transform: region_transform,
resolution: region_pixel_size,
quality: RenderQuality::Full,
};
let region_params = render_params.clone();
let region_ctx = OwnedContextImpl::from(ctx).with_footprint(region_footprint).with_vararg(Box::new(region_params)).into_context();
let mut result = render_fn(region_ctx).await;
let RenderOutputType::Texture(texture) = result.data else {
unreachable!("render_missing_region: expected texture output from Vello render");
};
let pixel_to_document = region_transform.inverse();
result.metadata.apply_transform(pixel_to_document);
let memory_size = (region_pixel_size.x * region_pixel_size.y) as usize * BYTES_PER_PIXEL;
CachedRegion {
texture,
texture_size: region_pixel_size,
tiles: region.tiles.clone(),
metadata: result.metadata,
last_access: 0,
memory_size,
}
})
}
fn composite_cached_regions(
+102 -30
View File
@@ -1,7 +1,7 @@
use core_types::gpoll::Interrupt;
use core_types::list::List;
use core_types::transform::{Footprint, Transform};
use core_types::{CloneVarArgs, ExtractAll, ExtractVarArgs};
use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNotSend};
use core_types::{Color, Context, Ctx, DeriveCtx, ExtractFootprint, ExtractVarArgs, VarArgLink, VarArgSlots, WasmNotSend};
use graph_craft::document::value::{RenderOutput, RenderOutputType};
use graphene_application_io::{ExportFormat, RenderConfig};
use graphic_types::raster_types::{CPU, Raster};
@@ -9,7 +9,7 @@ use graphic_types::{Artboard, Graphic, Vector};
use rendering::{Render, RenderMetadata, RenderOutputType as RenderOutputTypeRequest, RenderParams, SvgRender, SvgRenderOutput};
use std::sync::Arc;
use vector_types::GradientStops;
use wgpu_executor::{RenderContext, WgpuExecutor};
use wgpu_executor::RenderContext;
#[derive(Clone, dyn_any::DynAny)]
pub enum RenderIntermediateType {
@@ -23,8 +23,8 @@ pub struct RenderIntermediate {
}
#[node_macro::node(category(""))]
async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + Sync>(
ctx: impl Ctx + ExtractVarArgs + ExtractAll + CloneVarArgs,
fn render_intermediate<T: 'static + Render + WasmNotSend + Send + Sync>(
ctx: impl Ctx + ExtractVarArgs + DeriveCtx,
#[implementations(
Context -> List<Artboard>,
Context -> List<Graphic>,
@@ -34,21 +34,19 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
Context -> List<GradientStops>,
Context -> List<String>,
)]
data: impl Node<Context<'static>, Output = T>,
) -> RenderIntermediate {
data: impl Node<Context<'_>, Output = T>,
) -> Result<RenderIntermediate, Interrupt> {
let data = data.eval(&ctx.derived())?;
let render_params = ctx
.vararg(0)
.expect("Did not find var args")
.downcast_ref::<RenderParams>()
.expect("Downcasting render params yielded invalid type");
let ctx = OwnedContextImpl::from(ctx.clone()).into_context();
let data = data.eval(ctx).await;
let footprint = Footprint::default();
let mut metadata = RenderMetadata::default();
data.collect_metadata(&mut metadata, footprint, None);
match &render_params.render_output_type {
Ok(match &render_params.render_output_type {
RenderOutputTypeRequest::Vello => {
let mut scene = vello::Scene::new();
@@ -70,13 +68,13 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
metadata,
}
}
}
})
}
#[node_macro::node(category(""))]
async fn render<'a: 'n>(
fn render(
ctx: impl Ctx + ExtractFootprint + ExtractVarArgs,
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<&'a WgpuExecutor>,
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<wgpu_executor::WgpuExecutorHandle>,
data: RenderIntermediate,
) -> RenderOutput {
let footprint = ctx.footprint();
@@ -133,7 +131,6 @@ async fn render<'a: 'n>(
let texture = executor
.expect("GPU executor not available")
.render_vello_scene(&transformed_scene, footprint.resolution, context, None)
.await
.expect("Failed to render Vello scene");
RenderOutputType::Texture(texture)
}
@@ -144,11 +141,13 @@ async fn render<'a: 'n>(
}
#[node_macro::node(category(""))]
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>,
) -> RenderOutput {
fn create_context(ctx: impl Ctx + ExtractVarArgs + DeriveCtx, data: impl Node<Context<'_>, Output = RenderOutput>) -> Result<RenderOutput, Interrupt> {
let render_config = *ctx
.vararg(0)
.expect("Did not find var args")
.downcast_ref::<RenderConfig>()
.expect("Downcasting render config yielded invalid type");
let render_output_type = match render_config.export_format {
ExportFormat::Svg => RenderOutputTypeRequest::Svg,
ExportFormat::Raster => RenderOutputTypeRequest::Vello,
@@ -169,16 +168,89 @@ async fn create_context<'a: 'n>(
..Default::default()
};
let ctx = OwnedContextImpl::default()
.with_footprint(footprint)
.with_real_time(render_config.time.time)
.with_animation_time(render_config.time.animation_time.as_secs_f64())
.with_pointer_position(render_config.pointer)
.with_vararg(Box::new(render_params))
.into_context();
let mut result = data.eval(ctx).await;
let scope = ctx
.scope()
.with_real_time(Some(render_config.time.time))
.with_animation_time(Some(render_config.time.animation_time.as_secs_f64()))
.with_pointer_position(Some(render_config.pointer));
let varargs = VarArgLink {
args: VarArgSlots::Single(&render_params),
outer: None,
};
let scoped = ctx.with_scope(&scope);
let with_params = scoped.with_varargs(&varargs);
let mut result = data.eval(&with_params.with_footprint(&footprint))?;
result.metadata.apply_transform(glam::DAffine2::from_scale(glam::DVec2::splat(1. / render_config.scale)));
result
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use core_types::arena::Arena;
use core_types::context::{ContextImpl, EvalScope, VarArgsResult};
use core_types::gpoll::GPoll;
use core_types::node::Node;
use core_types::{ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime};
use graphene_application_io::TimingInformation;
struct ProbeNode;
impl<'a> Node<ContextImpl<'a>> for ProbeNode {
type Output = RenderOutput;
fn eval(&self, ctx: &ContextImpl<'a>) -> GPoll<RenderOutput> {
let render_params = ctx.vararg(0).unwrap().downcast_ref::<RenderParams>().expect("the vararg chain must start with RenderParams");
assert_eq!(render_params.scale, 2.0);
assert!(matches!(ctx.vararg(1), Err(VarArgsResult::IndexOutOfBounds)), "the RenderConfig must not leak downstream");
assert_eq!(ctx.footprint().transform, glam::DAffine2::from_scale(glam::DVec2::splat(2.0)) * Footprint::DEFAULT.transform);
assert_eq!(ctx.try_real_time(), Some(1.5));
assert_eq!(ctx.try_animation_time(), Some(2.0));
assert_eq!(ctx.try_pointer_position(), Some(glam::DVec2::new(3.0, 4.0)));
GPoll::Final(RenderOutput {
data: RenderOutputType::Buffer {
data: Vec::new(),
width: 0,
height: 0,
},
metadata: RenderMetadata::default(),
})
}
}
#[test]
fn create_context_builds_the_render_context_from_the_root_vararg() {
let arena = Arena::new(256);
let generations = [];
let scope = EvalScope::new(None, None, None, &generations, &arena);
let root = ContextImpl::root(&scope);
let render_config = RenderConfig {
scale: 2.0,
time: TimingInformation {
time: 1.5,
animation_time: std::time::Duration::from_secs(2),
},
pointer: glam::DVec2::new(3.0, 4.0),
..Default::default()
};
let varargs = VarArgLink {
args: VarArgSlots::Single(&render_config),
outer: None,
};
let ctx = root.with_varargs(&varargs);
let graph = CreateContextNode::new(ProbeNode);
let GPoll::Final(result) = <CreateContextNode<ProbeNode> as Node<ContextImpl>>::eval(&graph, &ctx) else {
panic!("create_context must complete synchronously");
};
assert_eq!(
result.data,
RenderOutputType::Buffer {
data: Vec::new(),
width: 0,
height: 0
}
);
}
}
@@ -1,22 +1,22 @@
use core_types::gpoll::Interrupt;
use core_types::transform::{Footprint, Transform};
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, OwnedContextImpl};
use core_types::{Ctx, DeriveCtx, ExtractAll};
use glam::{DAffine2, DVec2, UVec2, Vec2};
use graph_craft::document::value::{RenderOutput, RenderOutputType};
use graphic_types::raster_types::Texture;
use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams};
use vector_types::vector::style::RenderMode;
use wgpu_executor::{AsyncWgpuPipeline, WgpuExecutor, WgpuPipelineCache};
use wgpu_executor::{WgpuExecutor, WgpuPipeline, WgpuPipelineCache};
#[node_macro::node(category(""))]
pub async fn render_pixel_preview<'a: 'n>(
ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync,
pub fn render_pixel_preview(
ctx: impl Ctx + ExtractAll + DeriveCtx,
#[scope(pixel_preview_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache,
data: impl Node<Context<'static>, Output = RenderOutput> + Send + Sync,
) -> RenderOutput {
data: impl Node<Context<'_>, Output = RenderOutput>,
) -> Result<RenderOutput, Interrupt> {
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");
let context = OwnedContextImpl::from(ctx).into_context();
return data.eval(context).await;
return data.eval(&ctx.derived());
};
let physical_scale = render_params.scale;
@@ -24,8 +24,7 @@ pub async fn render_pixel_preview<'a: 'n>(
let viewport_zoom = footprint.scale_magnitudes().x;
if render_params.render_mode != RenderMode::PixelPreview || !matches!(render_params.render_output_type, RenderOutputTypeRequest::Vello) || viewport_zoom <= 1. {
let context = OwnedContextImpl::from(ctx).into_context();
return data.eval(context).await;
return data.eval(&ctx.derived());
}
let physical_resolution = footprint.resolution;
@@ -51,33 +50,31 @@ pub async fn render_pixel_preview<'a: 'n>(
quality: footprint.quality,
};
let new_ctx = OwnedContextImpl::from(ctx).with_footprint(upstream_footprint).with_vararg(Box::new(render_params)).into_context();
let mut result = data.eval(new_ctx).await;
let scoped = ctx.push_vararg(&render_params);
let mut result = data.eval(&scoped.ctx().with_footprint(&upstream_footprint))?;
let RenderOutputType::Texture(ref source_texture) = result.data else { return result };
let RenderOutputType::Texture(ref source_texture) = result.data else { return Ok(result) };
let logical_transform = DAffine2::from_scale(DVec2::splat(1. / physical_scale)) * footprint.transform;
let transform = DAffine2::from_translation(-upstream_min) * logical_transform.inverse() * DAffine2::from_scale(logical_resolution);
let resampled = pipeline
.run::<PixelPreview>(&PixelPreviewArgs {
source: source_texture.as_ref(),
transform: &transform,
size: physical_resolution,
})
.await;
let resampled = pipeline.run::<PixelPreview>(&PixelPreviewArgs {
source: source_texture.as_ref(),
transform: &transform,
size: physical_resolution,
});
result.data = RenderOutputType::Texture(resampled);
result.metadata.apply_transform(footprint.transform * DAffine2::from_translation(upstream_min));
result
Ok(result)
}
#[node_macro::node(category(""), inject_scope)]
async fn pixel_preview_pipeline<'a: 'n>(
fn pixel_preview_pipeline(
_ctx: impl Ctx,
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<&'a WgpuExecutor>,
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<wgpu_executor::WgpuExecutorHandle>,
#[data] pipeline: WgpuPipelineCache,
) -> WgpuPipelineCache {
if let Some(executor) = executor {
@@ -97,7 +94,7 @@ pub struct PixelPreviewArgs<'a> {
size: UVec2,
}
impl AsyncWgpuPipeline for PixelPreview {
impl WgpuPipeline for PixelPreview {
type Args<'a> = PixelPreviewArgs<'a>;
type Out = Texture;
@@ -169,11 +166,11 @@ impl AsyncWgpuPipeline for PixelPreview {
PixelPreview { pipeline, bind_group_layout }
}
async fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> Self::Out {
fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> Self::Out {
let context = &executor.context();
let &PixelPreviewArgs { source, transform, size } = args;
let output = executor.request_texture(size).await;
let output = executor.request_texture(size);
let source_view = source.create_view(&wgpu::TextureViewDescriptor::default());
let output_view = output.create_view(&wgpu::TextureViewDescriptor::default());
+11
View File
@@ -0,0 +1,11 @@
pub use core_types::runtime::*;
use crate::platform_application_io::editor_api;
use core_types::Ctx;
use graph_craft::application_io::PlatformEditorApi;
use std::sync::Arc;
#[node_macro::node(category(""), inject_scope)]
pub fn runtime(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Arc<PlatformEditorApi>) -> RuntimeHandle {
editor_api.runtime.clone()
}
+228 -14
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,239 @@ 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::gpoll::{Finality, GPoll};
use core_types::node::{BatchStatus, Node};
use core_types::registry::{EdgeHandle, ErasedNode, construct};
use std::mem::MaybeUninit;
use std::sync::Arc;
struct SourceNode<T>(T);
impl<T: Clone, Input> Node<Input> for SourceNode<T> {
type Output = T;
fn eval(&self, _input: &Input) -> GPoll<T> {
GPoll::Final(self.0.clone())
}
}
struct IndexNode;
impl<Input: ExtractIndex> Node<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_node_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!(Node::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<ErasedNode<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(Arc::new(SourceNode(true)) as Arc<ErasedNode<bool>>);
let other_value = EdgeHandle::new(Arc::new(SourceNode(false)) as Arc<ErasedNode<bool>>);
let wired = construct(&entries[0], vec![value, other_value]).unwrap().downcast::<bool>().unwrap();
assert_eq!(Node::eval(&wired, &ctx), GPoll::Final(true));
}
#[test]
fn ctor_registration_populates_the_node_registry() {
let registry = core_types::registry::NODE_REGISTRY.lock().unwrap();
let rows = registry
.iter()
.find_map(|(id, rows)| id.as_str().ends_with("::AddNode").then_some(rows))
.expect("AddNode rows registered at startup");
assert_eq!(rows.len(), 6);
}
#[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(Arc::new(SourceNode(1.5f64)) as Arc<ErasedNode<f64>>);
let addend = EdgeHandle::new(Arc::new(SourceNode(2.5f64)) as Arc<ErasedNode<f64>>);
let wired = construct(&entries[0], vec![augend, addend]).unwrap().downcast::<f64>().unwrap();
assert_eq!(Node::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> Node<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!(Node::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> Node<Input> for PendingSource {
type Output = f64;
fn eval(&self, _input: &Input) -> GPoll<f64> {
GPoll::Pending
}
}
struct PartialSource;
impl<Input> Node<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!(Node::eval(&pending, &ctx), GPoll::Pending);
let partial = SwitchNode::new(SourceNode(false), PendingSource, PartialSource);
assert_eq!(Node::eval(&partial, &ctx), GPoll::Partial(7.0));
}
#[test]
fn converted_switch_merges_condition_status_into_the_branch_result() {
struct PartialCondition;
impl<Input> Node<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!(Node::eval(&graph, &ctx), GPoll::Partial(1.0));
}
#[test]
fn generated_eval_computes_on_stand_in_and_traces_fallback() {
struct FallbackNode;
impl<Input> Node<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) = Node::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]);
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ pub use vector_types::vector::misc::BooleanOperation;
/// Combines the geometric forms of one or more closed paths into a new vector path that results from cutting or joining the paths by the chosen method.
#[node_macro::node(category("Vector: Modifier"), memoize)]
async fn boolean_operation<I: graphic_types::IntoGraphicList>(
fn boolean_operation<I: graphic_types::IntoGraphicList>(
_: impl Ctx,
/// The `List` of vector paths to perform the boolean operation on. Nested `List`s are automatically flattened.
#[implementations(List<Graphic>, List<Vector>)]
+1 -1
View File
@@ -8,7 +8,7 @@ use raster_types::{CPU, Raster};
use std::cmp::{max, min};
#[node_macro::node(category("Raster: Filter"))]
async fn dehaze(_: impl Ctx, image_frame: List<Raster<CPU>>, strength: Percentage) -> List<Raster<CPU>> {
fn dehaze(_: impl Ctx, image_frame: List<Raster<CPU>>, strength: Percentage) -> List<Raster<CPU>> {
image_frame
.into_iter()
.map(|mut row| {
+2 -2
View File
@@ -87,7 +87,7 @@ fn unpremultiply_gamma_to_linear(buffer: Image<PremultipliedGammaPixel>) -> Imag
/// Blurs the image with a Gaussian or box blur kernel filter.
#[node_macro::node(category("Raster: Filter"))]
async fn blur(
fn blur(
_: impl Ctx,
/// The image to be blurred.
image_frame: List<Raster<CPU>>,
@@ -124,7 +124,7 @@ async fn blur(
/// Applies a median filter to reduce noise while preserving edges.
#[node_macro::node(category("Raster: Filter"))]
async fn median_filter(
fn median_filter(
_: impl Ctx,
/// The image to be filtered.
image_frame: List<Raster<CPU>>,
+1 -1
View File
@@ -10,7 +10,7 @@ use vector_types::GradientStops;
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27grdm%27%20%3D%20Gradient%20Map
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Gradient%20settings%20(Photoshop%206.0)
#[node_macro::node(category("Raster: Adjustment"))]
async fn gradient_map<T: Adjust<Color>>(
fn gradient_map<T: Adjust<Color>>(
_: impl Ctx,
#[implementations(
List<Raster<CPU>>,
@@ -4,7 +4,7 @@ use core_types::list::{Item, List};
use raster_types::{CPU, Raster};
#[node_macro::node(category("Color"))]
async fn image_color_palette(
fn image_color_palette(
_: impl Ctx,
image: List<Raster<CPU>>,
#[default(4)]
+2 -2
View File
@@ -241,7 +241,7 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: List<Raster<CPU>>, bounds: DAf
let image_data = &row.element().data;
let (image_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 {
+39 -182
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,178 +148,34 @@ 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
}
#[cfg(test)]
mod test {
use super::*;
use core_types::Ctx;
use core_types::Node;
use core_types::transform::Footprint;
use glam::DVec2;
use graphene_core::ReadPositionNode;
use graphene_core::extract_xy::{ExtractXyNode, XY};
use graphic_types::Vector;
use kurbo::Shape;
use kurbo::{BezPath, DEFAULT_ACCURACY, Rect};
use std::future::Future;
use std::pin::Pin;
use vector_nodes::generator_nodes::RectangleNode;
use vector_types::subpath::Subpath;
fn vector_node_from_bezpath(bezpath: BezPath) -> List<Vector> {
List::new_from_element(Vector::from_bezpath(bezpath))
}
#[derive(Clone)]
pub struct FutureWrapperNode<T: Clone>(T);
impl<'i, I: Ctx, T: 'i + Clone + Send> Node<'i, I> for FutureWrapperNode<T> {
type Output = Pin<Box<dyn Future<Output = T> + 'i + Send>>;
fn eval(&'i self, _input: I) -> Self::Output {
let value = self.0.clone();
Box::pin(async move { value })
}
}
#[tokio::test]
async fn repeat_on_points_test() {
let context = OwnedContextImpl::default().into_context();
let rect = RectangleNode::new(
FutureWrapperNode(()),
ExtractXyNode::new(ReadPositionNode::new(FutureWrapperNode(()), FutureWrapperNode(0)), FutureWrapperNode(XY::Y)),
FutureWrapperNode(2_f64),
FutureWrapperNode(false),
FutureWrapperNode(0_f64),
FutureWrapperNode(false),
);
let positions = [DVec2::new(40., 20.), DVec2::ONE, DVec2::new(-42., 9.), DVec2::new(10., 345.)];
let points = List::new_from_element(Vector::from_subpath(Subpath::from_anchors(positions, false)));
let generated = super::repeat_on_points(context, points, &rect, false).await;
assert_eq!(generated.len(), positions.len());
for (position, index) in positions.into_iter().zip(0..generated.len()) {
let bounds = generated
.element(index)
.unwrap()
.bounding_box_with_transform(generated.attribute_cloned_or_default(ATTR_TRANSFORM, index))
.unwrap();
assert!(position.abs_diff_eq((bounds[0] + bounds[1]) / 2., 1e-10));
assert_eq!((bounds[1] - bounds[0]).x, position.y);
}
}
#[tokio::test]
async fn repeat() {
let direction = DVec2::X * 1.5;
let count = 3;
let context = OwnedContextImpl::default().into_context();
let repeated = super::repeat_array(
context,
&FutureWrapperNode(vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY))),
direction,
0.,
count,
)
.await;
let vector_list = vector_nodes::flatten_path(Footprint::default(), repeated).await;
let vector = vector_list.element(0).unwrap();
assert_eq!(vector.region_manipulator_groups().count(), 3);
for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() {
assert!((manipulator_groups[0].anchor - direction * index as f64 / (count - 1) as f64).length() < 1e-5);
}
}
#[tokio::test]
async fn repeat_single_copy() {
let context = OwnedContextImpl::default().into_context();
let repeated = super::repeat_array(
context,
&FutureWrapperNode(vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY))),
DVec2::new(12., 10.),
45.,
1,
)
.await;
let vector_list = vector_nodes::flatten_path(Footprint::default(), repeated).await;
let vector = vector_list.element(0).unwrap();
assert_eq!(vector.region_manipulator_groups().count(), 1);
let (_, manipulator_groups) = vector.region_manipulator_groups().next().unwrap();
let anchor = manipulator_groups[0].anchor;
assert!(anchor.length() < 1e-5, "Expected the single copy to be untransformed, found anchor {anchor}");
}
#[tokio::test]
async fn repeat_transform_position() {
let direction = DVec2::new(12., 10.);
let count = 8;
let context = OwnedContextImpl::default().into_context();
let repeated = super::repeat_array(
context,
&FutureWrapperNode(vector_node_from_bezpath(Rect::new(0., 0., 1., 1.).to_path(DEFAULT_ACCURACY))),
direction,
0.,
count,
)
.await;
let vector_list = vector_nodes::flatten_path(Footprint::default(), repeated).await;
let vector = vector_list.element(0).unwrap();
assert_eq!(vector.region_manipulator_groups().count(), 8);
for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() {
assert!((manipulator_groups[0].anchor - direction * index as f64 / (count - 1) as f64).length() < 1e-5);
}
}
#[tokio::test]
async fn repeat_radial() {
let context = OwnedContextImpl::default().into_context();
let repeated = super::repeat_radial(context, &FutureWrapperNode(vector_node_from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY))), 45., 4., 8).await;
let vector_list = vector_nodes::flatten_path(Footprint::default(), repeated).await;
let vector = vector_list.element(0).unwrap();
assert_eq!(vector.region_manipulator_groups().count(), 8);
for (index, (_, manipulator_groups)) in vector.region_manipulator_groups().enumerate() {
let expected_angle = (index as f64 + 1.) * 45.;
let center = (manipulator_groups[0].anchor + manipulator_groups[2].anchor) / 2.;
let actual_angle = DVec2::Y.angle_to(center).to_degrees();
assert!((actual_angle - expected_angle).abs() % 360. < 1e-5, "Expected {expected_angle} found {actual_angle}");
}
}
Ok(result_list)
}
+10 -9
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.
@@ -1,8 +1,9 @@
use core::f64;
use core_types::color::Color;
use core_types::gpoll::Interrupt;
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::{ATTR_TRANSFORM, Context, Ctx, DeriveCtx, 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 + 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.
@@ -114,7 +108,7 @@ fn replace_transform<T>(
// TODO: Figure out how this node should behave once #2982 is implemented.
/// Obtains the transform of the first item in the input `List`, if present.
#[node_macro::node(category("Math: Transform"), path(core_types::vector))]
async fn extract_transform(_: impl Ctx, content: ListDyn) -> DAffine2 {
fn extract_transform(_: impl Ctx, content: ListDyn) -> DAffine2 {
content.attribute::<DAffine2>(ATTR_TRANSFORM, 0).copied().unwrap_or_default()
}
@@ -7,7 +7,7 @@ use vector_types::vector::VectorModification;
/// Applies a differential modification to a vector path, associating changes made by the Pen and Path tools to indices of edited points and segments.
#[node_macro::node(category(""))]
async fn path_modify(_ctx: impl Ctx, mut vector: List<Vector>, modification: Box<VectorModification>, node_path: List<NodeId>) -> List<Vector> {
fn path_modify(_ctx: impl Ctx, mut vector: List<Vector>, modification: Box<VectorModification>, node_path: List<NodeId>) -> List<Vector> {
use core_types::list::Item;
if vector.is_empty() {
@@ -35,7 +35,7 @@ async fn path_modify(_ctx: impl Ctx, mut vector: List<Vector>, modification: Box
/// Applies the vector path's local transformation to its geometry and resets the transform to the identity.
#[node_macro::node(category("Vector"))]
async fn apply_transform(_ctx: impl Ctx, mut vector: List<Vector>) -> List<Vector> {
fn apply_transform(_ctx: impl Ctx, mut vector: List<Vector>) -> List<Vector> {
let (elements, transforms) = vector.element_and_attribute_slices_mut::<DAffine2>(ATTR_TRANSFORM);
for (element, transform) in elements.iter_mut().zip(transforms.iter_mut()) {
for (_, point) in element.point_domain.positions_mut() {
+52 -53
View File
@@ -3,13 +3,14 @@ use core::f64::consts::{PI, TAU};
use core::hash::{Hash, Hasher};
use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::gpoll::Interrupt;
use core_types::list::{ATTR_FILL, ATTR_STROKE, Item, ItemAttributeValues, List, ListDyn};
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::{
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, Ctx,
DeriveCtx,
};
use glam::{DAffine2, DMat2, DVec2};
use graphic_types::Vector;
@@ -88,7 +89,7 @@ impl VectorListIterMut for List<Vector> {
/// Uniquely sets the fill and/or stroke style of every vector element to individual colors sampled along a chosen gradient.
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector))]
async fn assign_colors<T>(
fn assign_colors<T>(
_: impl Ctx,
/// The content with vector paths to apply the fill and/or stroke style to.
#[implementations(List<Graphic>, List<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>(
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>(
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>(
fn copy_to_points<I: Send + Clone>(
_: impl Ctx,
points: List<Vector>,
/// Artwork to be copied and placed at each point.
@@ -440,7 +441,7 @@ async fn copy_to_points<I: 'n + Send + Clone>(
}
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn round_corners(
fn round_corners(
_: impl Ctx,
source: List<Vector>,
#[hard(0..)]
@@ -777,7 +778,7 @@ pub mod extrude_algorithms {
}
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn extrude(_: impl Ctx, mut source: List<Vector>, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) -> List<Vector> {
fn extrude(_: impl Ctx, mut source: List<Vector>, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) -> List<Vector> {
for vector in source.iter_element_values_mut() {
extrude_algorithms::extrude(vector, direction, joining_algorithm);
}
@@ -785,7 +786,7 @@ async fn extrude(_: impl Ctx, mut source: List<Vector>, direction: DVec2, joinin
}
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn box_warp(_: impl Ctx, content: List<Vector>, #[expose] rectangle: List<Vector>) -> List<Vector> {
fn box_warp(_: impl Ctx, content: List<Vector>, #[expose] rectangle: List<Vector>) -> List<Vector> {
let Some(target) = rectangle.element(0).cloned() else { return content };
let target_transform: DAffine2 = rectangle.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
@@ -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>(
fn pack_strips<T: Send + Clone>(
_: impl Ctx,
#[implementations(
List<Graphic>,
@@ -991,7 +992,7 @@ where
/// Automatically constructs tangents (Bézier handles) for anchor points in a vector path.
#[node_macro::node(category("Vector: Modifier"), name("Auto-Tangents"), path(core_types::vector))]
async fn auto_tangents(
fn auto_tangents(
_: impl Ctx,
source: List<Vector>,
/// The amount of spread for the auto-tangents, from 0 (sharp corner) to 1 (full spread).
@@ -1145,7 +1146,7 @@ async fn auto_tangents(
}
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn bounding_box(_: impl Ctx, content: List<Vector>) -> List<Vector> {
fn bounding_box(_: impl Ctx, content: List<Vector>) -> List<Vector> {
content
.into_iter()
.map(|mut row| {
@@ -1170,7 +1171,7 @@ async fn bounding_box(_: impl Ctx, content: List<Vector>) -> List<Vector> {
}
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
async fn dimensions(_: impl Ctx, content: List<Vector>) -> DVec2 {
fn dimensions(_: impl Ctx, content: List<Vector>) -> DVec2 {
(0..content.len())
.filter_map(|index| content.element(index).unwrap().bounding_box_with_transform(content.attribute_cloned_or_default(ATTR_TRANSFORM, index)))
.reduce(|[acc_top_left, acc_bottom_right], [top_left, bottom_right]| [acc_top_left.min(top_left), acc_bottom_right.max(bottom_right)])
@@ -1186,7 +1187,7 @@ fn as_vector(_: impl Ctx, value: List<Vector>) -> List<Vector> {
/// Creates a polyline from a series of vector points, replacing any existing segments and regions that may already exist.
#[node_macro::node(category("Vector"), name("Points to Polyline"), path(core_types::vector))]
async fn points_to_polyline(_: impl Ctx, mut points: List<Vector>, #[default(true)] closed: bool) -> List<Vector> {
fn points_to_polyline(_: impl Ctx, mut points: List<Vector>, #[default(true)] closed: bool) -> List<Vector> {
for vector in points.iter_element_values_mut() {
let mut segment_domain = SegmentDomain::new();
let mut next_id = SegmentId::ZERO;
@@ -1214,7 +1215,7 @@ async fn points_to_polyline(_: impl Ctx, mut points: List<Vector>, #[default(tru
}
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("offset_path_properties"))]
async fn offset_path(_: impl Ctx, content: List<Vector>, distance: f64, join: StrokeJoin, #[default(4.)] miter_limit: f64) -> List<Vector> {
fn offset_path(_: impl Ctx, content: List<Vector>, distance: f64, join: StrokeJoin, #[default(4.)] miter_limit: f64) -> List<Vector> {
content
.into_iter()
.map(|mut row| {
@@ -1258,7 +1259,7 @@ async fn offset_path(_: impl Ctx, content: List<Vector>, distance: f64, join: St
}
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn solidify_stroke<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
fn solidify_stroke<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
// TODO: Make this node support stroke align, which it currently ignores
let graphic_list = content.into_graphic_list();
@@ -1366,7 +1367,7 @@ async fn solidify_stroke<T: IntoGraphicList>(_: impl Ctx, #[implementations(List
}
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn separate_subpaths(_: impl Ctx, content: List<Vector>) -> List<Vector> {
fn separate_subpaths(_: impl Ctx, content: List<Vector>) -> List<Vector> {
content
.into_iter()
.flat_map(|row| {
@@ -1397,7 +1398,7 @@ async fn separate_subpaths(_: impl Ctx, content: List<Vector>) -> List<Vector> {
/// Determines if the subpath at the given index (across all vector element subpaths) is closed, meaning its ends are connected together forming a loop.
#[node_macro::node(name("Path is Closed"), category("Vector: Measure"), path(core_types::vector))]
async fn path_is_closed(
fn path_is_closed(
_: impl Ctx,
/// The vector content whose subpaths are inspected.
content: List<Vector>,
@@ -1412,25 +1413,25 @@ 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.
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
pub async fn flatten_path<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
pub fn flatten_path<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
let graphic_list = content.into_graphic_list();
let flattened = graphic_list.clone().into_flattened_list::<Vector>();
@@ -1486,7 +1487,7 @@ pub async fn flatten_path<T: IntoGraphicList>(_: impl Ctx, #[implementations(Lis
/// Convert vector geometry into a polyline composed of evenly spaced points.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("sample_polyline_properties"), memoize)]
async fn sample_polyline(
fn sample_polyline(
_: impl Ctx,
content: List<Vector>,
spacing: PointSpacingType,
@@ -1572,7 +1573,7 @@ async fn sample_polyline(
/// Simplifies vector paths by reducing the number of curve segments while preserving the overall shape within the given tolerance.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn simplify(
fn simplify(
_: impl Ctx,
/// The vector paths to simplify.
content: List<Vector>,
@@ -1616,7 +1617,7 @@ async fn simplify(
/// Decimates vector paths into polylines by sampling any curves into line segments, then removing points that don't significantly contribute to the shape using the Ramer-Douglas-Peucker algorithm.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn decimate(
fn decimate(
_: impl Ctx,
/// The vector paths to decimate.
content: List<Vector>,
@@ -1744,7 +1745,7 @@ async fn decimate(
///
/// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it.
#[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
async fn cut_path(
fn cut_path(
_: impl Ctx,
/// The path to insert a cut into.
mut content: List<Vector>,
@@ -1795,7 +1796,7 @@ async fn cut_path(
/// Cuts path segments into separate disconnected pieces where each is a distinct subpath.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn cut_segments(_: impl Ctx, mut content: List<Vector>) -> List<Vector> {
fn cut_segments(_: impl Ctx, mut content: List<Vector>) -> List<Vector> {
// Iterate through every segment and make a copy of each of its endpoints, then reassign each segment's endpoints to its own unique point copy
for vector in content.iter_element_values_mut() {
let points_count = vector.point_domain.ids().len();
@@ -1854,7 +1855,7 @@ async fn cut_segments(_: impl Ctx, mut content: List<Vector>) -> List<Vector> {
///
/// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it.
#[node_macro::node(name("Position on Path"), category("Vector: Measure"), path(graphene_core::vector))]
async fn position_on_path(
fn position_on_path(
_: impl Ctx,
/// The path to traverse.
content: List<Vector>,
@@ -1892,7 +1893,7 @@ async fn position_on_path(
///
/// If multiple subpaths make up the path, the whole number part of the progression value selects the subpath and the decimal part determines the position along it.
#[node_macro::node(name("Tangent on Path"), category("Vector: Measure"), path(graphene_core::vector))]
async fn tangent_on_path(
fn tangent_on_path(
_: impl Ctx,
/// The path to traverse.
content: List<Vector>,
@@ -1940,7 +1941,7 @@ async fn tangent_on_path(
}
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector), memoize)]
async fn scatter_points(
fn scatter_points(
_: impl Ctx,
content: List<Vector>,
#[unit(" px")]
@@ -1990,7 +1991,7 @@ async fn scatter_points(
}
#[node_macro::node(name("Spline"), category("Vector: Modifier"), path(core_types::vector))]
async fn spline(_: impl Ctx, content: List<Vector>) -> List<Vector> {
fn spline(_: impl Ctx, content: List<Vector>) -> List<Vector> {
content
.into_iter()
.filter_map(|mut row| {
@@ -2090,7 +2091,7 @@ fn apply_point_deltas(element: &mut Vector, deltas: &[DVec2], transform: DAffine
/// Perturbs the positions of anchor points in vector geometry by random amounts and directions.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn jitter_points(
fn jitter_points(
_: impl Ctx,
/// The vector geometry with points to be jittered.
content: List<Vector>,
@@ -2140,7 +2141,7 @@ async fn jitter_points(
/// Displaces anchor points along their normal direction (perpendicular to the path) by a set distance.
/// Points with 0 or 3+ segment connections have no well-defined normal and are left in place.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn offset_points(
fn offset_points(
_: impl Ctx,
/// The vector geometry with points to be offset.
content: List<Vector>,
@@ -2177,7 +2178,7 @@ async fn offset_points(
///
/// *Progression* morphs through all objects. Interpolation is linear unless *Path* geometry is provided to control the trajectory between key objects. The **Origins to Polyline** node may be used to create a path with anchor points corresponding to each object. Other nodes can modify its path segments.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn morph<I: IntoGraphicList>(
fn morph<I: IntoGraphicList>(
_: impl Ctx,
/// The vector objects to interpolate between. Mixed graphic content is deeply flattened to keep only vector elements.
#[implementations(List<Graphic>, List<Vector>)]
@@ -3124,19 +3125,19 @@ fn point_inside(_: impl Ctx, source: List<Vector>, point: DVec2) -> bool {
// TODO: Return u32, u64, or usize instead of f64 after #1621 is resolved and has allowed us to implement automatic type conversion in the node graph for nodes with generic type inputs.
// TODO: (Currently automatic type conversion only works for concrete types, via the Graphene preprocessor and not the full Graphene type system.)
#[node_macro::node(category("General"), path(graphene_core::vector))]
async fn count_elements(_: impl Ctx, content: ListDyn) -> f64 {
fn count_elements(_: impl Ctx, content: ListDyn) -> f64 {
content.len() as f64
}
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
async fn count_points(_: impl Ctx, content: List<Vector>) -> f64 {
fn count_points(_: impl Ctx, content: List<Vector>) -> f64 {
content.iter_element_values().map(|vector| vector.point_domain.positions().len() as f64).sum()
}
/// Retrieves the vec2 position (in local space) of the anchor point at the specified index in a `List` of vector elements.
/// If no value exists at that index, the position (0, 0) is returned.
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
async fn index_points(
fn index_points(
_: impl Ctx,
/// The vector element or elements containing the anchor points to be retrieved.
content: List<Vector>,
@@ -3170,7 +3171,7 @@ async fn index_points(
}
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))]
async fn path_length(_: impl Ctx, source: List<Vector>) -> f64 {
fn path_length(_: impl Ctx, source: List<Vector>) -> f64 {
(0..source.len())
.map(|index| {
let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM, index);
@@ -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) }
}
}