mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-21 03:28:11 +08:00
Cut over to the graphene execution model
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(®ion_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(
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>)]
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -241,7 +241,7 @@ pub fn extend_image_to_bounds(_: impl Ctx, image: List<Raster<CPU>>, bounds: DAf
|
||||
let image_data = &row.element().data;
|
||||
let (image_width, image_height) = (row.element().width, row.element().height);
|
||||
if image_width == 0 || image_height == 0 {
|
||||
return empty_image((), bounds, List::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap();
|
||||
return empty_image(&(), bounds, List::new_from_element(Color::TRANSPARENT)).into_iter().next().unwrap();
|
||||
}
|
||||
|
||||
let orig_image_scale = DVec2::new(image_width as f64, image_height as f64);
|
||||
@@ -290,7 +290,7 @@ pub fn empty_image(_: impl Ctx, transform: DAffine2, color: List<Color>) -> List
|
||||
}
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
pub fn image<'a: 'n>(_: impl Ctx, resource: Resource) -> List<Raster<CPU>> {
|
||||
pub fn image(_: impl Ctx, resource: Resource) -> List<Raster<CPU>> {
|
||||
let image_data = resource.as_ref();
|
||||
|
||||
let Some(image) = ::image::load_from_memory(image_data).ok() else {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
use crate::gcore::Context;
|
||||
use core::f64::consts::TAU;
|
||||
use core_types::gpoll::Interrupt;
|
||||
use core_types::list::List;
|
||||
use core_types::registry::types::{Angle, PixelSize};
|
||||
use core_types::{ATTR_TRANSFORM, CloneVarArgs, Color, Ctx, ExtractAll, InjectVarArgs, OwnedContextImpl};
|
||||
use core_types::{ATTR_TRANSFORM, Color, Ctx, DeriveCtx, InjectVarArgs};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphic_types::{Graphic, Vector};
|
||||
use raster_types::{CPU, Raster};
|
||||
use vector_types::GradientStops;
|
||||
|
||||
#[node_macro::node(category("Repeat"))]
|
||||
async fn repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
ctx: impl ExtractAll + CloneVarArgs + Ctx,
|
||||
fn repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
ctx: impl Ctx + DeriveCtx,
|
||||
#[implementations(
|
||||
Context -> List<Graphic>,
|
||||
Context -> List<Vector>,
|
||||
@@ -18,35 +19,35 @@ async fn repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
Context -> List<Color>,
|
||||
Context -> List<GradientStops>,
|
||||
)]
|
||||
content: impl Node<'n, Context<'static>, Output = List<T>>,
|
||||
content: impl Node<Context<'_>, Output = List<T>>,
|
||||
#[default(1)]
|
||||
#[hard(1..)]
|
||||
count: u32,
|
||||
reverse: bool,
|
||||
) -> List<T> {
|
||||
) -> Result<List<T>, Interrupt> {
|
||||
// Someday this node can have the option to generate infinitely instead of a fixed count (basically `std::iter::repeat`).
|
||||
|
||||
let count = count as usize;
|
||||
let count = count as u64;
|
||||
let spilled = ctx.index_head();
|
||||
|
||||
let mut result_list = List::new();
|
||||
|
||||
for index in 0..count {
|
||||
let index = if reverse { count - index - 1 } else { index };
|
||||
|
||||
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index);
|
||||
let generated_content = content.eval(new_ctx.into_context()).await;
|
||||
let generated_content = content.eval(&ctx.promoted(&spilled, index))?;
|
||||
|
||||
for generated_row in generated_content.into_iter() {
|
||||
result_list.push(generated_row);
|
||||
}
|
||||
}
|
||||
|
||||
result_list
|
||||
Ok(result_list)
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Repeat"))]
|
||||
pub async fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
ctx: impl ExtractAll + CloneVarArgs + Ctx,
|
||||
pub fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
ctx: impl Ctx + DeriveCtx,
|
||||
#[implementations(
|
||||
Context -> List<Graphic>,
|
||||
Context -> List<Vector>,
|
||||
@@ -54,7 +55,7 @@ pub async fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
Context -> List<Color>,
|
||||
Context -> List<GradientStops>,
|
||||
)]
|
||||
content: impl Node<'n, Context<'static>, Output = List<T>>,
|
||||
content: impl Node<Context<'_>, Output = List<T>>,
|
||||
#[default(100., 100.)]
|
||||
// TODO: When using a custom Properties panel layout in document_node_definitions.rs and this default is set, the widget weirdly doesn't show up in the Properties panel. Investigation is needed.
|
||||
direction: PixelSize,
|
||||
@@ -62,10 +63,11 @@ pub async fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
#[default(5)]
|
||||
#[hard(1..)]
|
||||
count: u32,
|
||||
) -> List<T> {
|
||||
) -> Result<List<T>, Interrupt> {
|
||||
let angle = angle.to_radians();
|
||||
// A single copy has no steps between copies, so the denominator is kept at 1 to avoid `0. / 0.` producing a NaN transform
|
||||
let total = (count - 1).max(1) as f64;
|
||||
let spilled = ctx.index_head();
|
||||
|
||||
let mut result_list = List::new();
|
||||
|
||||
@@ -74,8 +76,7 @@ pub async fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
let translation = index as f64 * direction / total;
|
||||
let transform = DAffine2::from_angle(angle) * DAffine2::from_translation(translation);
|
||||
|
||||
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index as usize);
|
||||
let generated_content = content.eval(new_ctx.into_context()).await;
|
||||
let generated_content = content.eval(&ctx.promoted(&spilled, index as u64))?;
|
||||
|
||||
for row_index in 0..generated_content.len() {
|
||||
let Some(mut row) = generated_content.clone_item(row_index) else { continue };
|
||||
@@ -89,12 +90,12 @@ pub async fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
}
|
||||
}
|
||||
|
||||
result_list
|
||||
Ok(result_list)
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Repeat"))]
|
||||
async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
ctx: impl ExtractAll + CloneVarArgs + Ctx,
|
||||
fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
ctx: impl Ctx + DeriveCtx,
|
||||
#[implementations(
|
||||
Context -> List<Graphic>,
|
||||
Context -> List<Vector>,
|
||||
@@ -102,7 +103,7 @@ async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
Context -> List<Color>,
|
||||
Context -> List<GradientStops>,
|
||||
)]
|
||||
content: impl Node<'n, Context<'static>, Output = List<T>>,
|
||||
content: impl Node<Context<'_>, Output = List<T>>,
|
||||
start_angle: Angle,
|
||||
#[unit(" px")]
|
||||
#[default(5)]
|
||||
@@ -110,7 +111,8 @@ async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
#[default(5)]
|
||||
#[hard(1..)]
|
||||
count: u32,
|
||||
) -> List<T> {
|
||||
) -> Result<List<T>, Interrupt> {
|
||||
let spilled = ctx.index_head();
|
||||
let mut result_list = List::new();
|
||||
|
||||
for index in 0..count {
|
||||
@@ -118,8 +120,7 @@ async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
let translation = DAffine2::from_translation(radius * DVec2::Y);
|
||||
let transform = angle * translation;
|
||||
|
||||
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index as usize);
|
||||
let generated_content = content.eval(new_ctx.into_context()).await;
|
||||
let generated_content = content.eval(&ctx.promoted(&spilled, index as u64))?;
|
||||
|
||||
for row_index in 0..generated_content.len() {
|
||||
let Some(mut row) = generated_content.clone_item(row_index) else { continue };
|
||||
@@ -133,12 +134,12 @@ async fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
}
|
||||
}
|
||||
|
||||
result_list
|
||||
Ok(result_list)
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Repeat"), name("Repeat on Points"))]
|
||||
async fn repeat_on_points<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
ctx: impl ExtractAll + CloneVarArgs + Sync + Ctx + InjectVarArgs,
|
||||
fn repeat_on_points<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
ctx: impl Ctx + DeriveCtx + InjectVarArgs,
|
||||
points: List<Vector>,
|
||||
#[implementations(
|
||||
Context -> List<Graphic>,
|
||||
@@ -147,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)
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user