Convert the remaining nodes and mark genuine async sources

This commit is contained in:
Dennis Kobert
2026-07-26 22:58:40 +00:00
parent 28d37d30d0
commit 28d9e3cbe3
17 changed files with 398 additions and 114 deletions
@@ -0,0 +1,291 @@
use std::any::Any;
use std::mem::MaybeUninit;
use std::ops::Add;
use core_types::arena::{Arena, ArenaCell};
use core_types::context::{ContextImpl, Ctx, EvalScope, ExtractArena, InjectIndex};
use core_types::gnode::{BatchStatus, GNode, StatusCell};
use core_types::gpoll::{ErrorKind, Finality, GPoll, Interrupt};
fn add<A: Add<B>, B, C: Ctx>(_ctx: &C, augend: A, addend: B) -> <A as Add<B>>::Output {
augend + addend
}
struct AddNode<Node0, Node1> {
augend: Node0,
addend: Node1,
}
impl<Node0, Node1> AddNode<Node0, Node1> {
fn new(augend: Node0, addend: Node1) -> Self {
Self { augend, addend }
}
}
impl<A, B, Input, Node0, Node1> GNode<Input> for AddNode<Node0, Node1>
where
A: Add<B>,
Input: Ctx,
Node0: GNode<Input, Output = A>,
Node1: GNode<Input, Output = B>,
{
type Output = <A as Add<B>>::Output;
fn eval(&self, input: &Input) -> GPoll<Self::Output> {
let cell = StatusCell::new();
let augend = match cell.eval_input(0, &self.augend, input) {
Ok(value) => value,
Err(interrupt) => return interrupt.into(),
};
let addend = match cell.eval_input(1, &self.addend, input) {
Ok(value) => value,
Err(interrupt) => return interrupt.into(),
};
cell.finish(add(input, augend, addend))
}
}
struct ValueNode<T>(T);
impl<T: Clone, Input> GNode<Input> for ValueNode<T> {
type Output = T;
fn eval(&self, _input: &Input) -> GPoll<T> {
GPoll::Final(self.0.clone())
}
}
struct ReadIndexNode;
impl<Input: InjectIndex + Copy + ExtractIndexValue> GNode<Input> for ReadIndexNode {
type Output = f64;
fn eval(&self, input: &Input) -> GPoll<f64> {
GPoll::Final(input.index_value() as f64)
}
}
trait ExtractIndexValue {
fn index_value(&self) -> u64;
}
impl ExtractIndexValue for ContextImpl<'_> {
fn index_value(&self) -> u64 {
self.index_head().index
}
}
fn string_length<C: Ctx>(_ctx: &C, value: &String) -> f64 {
value.len() as f64
}
struct LendStringNode {
value: String,
cell: ArenaCell<String>,
}
impl LendStringNode {
fn new(value: String) -> Self {
Self {
value,
cell: ArenaCell::new(),
}
}
}
impl<'e, Input> GNode<Input> for LendStringNode
where
Input: Ctx + ExtractArena<ArenaRef = &'e Arena>,
{
type Output = &'e String;
fn eval(&self, input: &Input) -> GPoll<&'e String> {
let arena = input.arena();
if let Some(value) = self.cell.load(arena) {
return GPoll::Final(value);
}
match arena.alloc(self.value.clone()) {
Some((value, weak)) => {
self.cell.store(weak);
GPoll::Final(value)
}
None => GPoll::arena_exhausted(),
}
}
}
struct StringLengthNode<Node0> {
value: Node0,
}
impl<Node0> StringLengthNode<Node0> {
fn new(value: Node0) -> Self {
Self { value }
}
}
impl<'e, Input, Node0> GNode<Input> for StringLengthNode<Node0>
where
Input: Ctx,
Node0: GNode<Input, Output = &'e String>,
{
type Output = f64;
fn eval(&self, input: &Input) -> GPoll<f64> {
let cell = StatusCell::new();
let value = match cell.eval_input(0, &self.value, input) {
Ok(value) => value,
Err(interrupt) => return interrupt.into(),
};
cell.finish(string_length(input, value))
}
}
type ErasedGNode<T> = dyn for<'c> GNode<ContextImpl<'c>, Output = T>;
type ErasedLendEdge = dyn for<'c> GNode<ContextImpl<'c>, Output = &'c String>;
fn string_length_constructor(args: Vec<Box<dyn Any>>) -> Result<Box<ErasedGNode<f64>>, &'static str> {
let mut args = args.into_iter();
let value = *args.next().ok_or("arity")?.downcast::<Box<ErasedLendEdge>>().map_err(|_| "type")?;
Ok(Box::new(StringLengthNode::new(value)))
}
fn add_constructor_f64(args: Vec<Box<dyn Any>>) -> Result<Box<ErasedGNode<f64>>, &'static str> {
let mut args = args.into_iter();
let augend = *args.next().ok_or("arity")?.downcast::<Box<ErasedGNode<f64>>>().map_err(|_| "type")?;
let addend = *args.next().ok_or("arity")?.downcast::<Box<ErasedGNode<f64>>>().map_err(|_| "type")?;
Ok(Box::new(AddNode::new(augend, addend)))
}
fn scope_fixture<'a>(generations: &'a [(u64, u64)], arena: &'a Arena) -> EvalScope<'a> {
EvalScope::new(Some(0.5), None, None, generations, arena)
}
#[test]
fn hand_expansion_evaluates_through_typed_erased_edges() {
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let augend: Box<dyn Any> = Box::new(Box::new(ValueNode(1.0f64)) as Box<ErasedGNode<f64>>);
let addend: Box<dyn Any> = Box::new(Box::new(ValueNode(2.0f64)) as Box<ErasedGNode<f64>>);
let wired = add_constructor_f64(vec![augend, addend]).unwrap();
assert_eq!(wired.eval(&ctx), GPoll::Final(3.0));
}
#[test]
fn wiring_rejects_type_and_arity_mismatches() {
let augend: Box<dyn Any> = Box::new(Box::new(ValueNode(1.0f64)) as Box<ErasedGNode<f64>>);
let addend: Box<dyn Any> = Box::new(Box::new(ValueNode(2u32)) as Box<ErasedGNode<u32>>);
assert_eq!(add_constructor_f64(vec![augend, addend]).map(|_| ()), Err("type"));
let augend: Box<dyn Any> = Box::new(Box::new(ValueNode(1.0f64)) as Box<ErasedGNode<f64>>);
assert_eq!(add_constructor_f64(vec![augend]).map(|_| ()), Err("arity"));
}
#[test]
fn spec_loop_batches_through_the_erased_edge() {
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let graph: Box<ErasedGNode<f64>> = Box::new(AddNode::new(ReadIndexNode, ValueNode(10.0f64)));
let mut scratch = [const { MaybeUninit::uninit() }; 4];
let status = graph.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 lending_kernel_clones_once_per_generation_and_lends_after() {
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let node = LendStringNode::new("lend me".to_string());
let GPoll::Final(first) = node.eval(&ctx) else {
panic!("first eval must clone into the arena and lend");
};
let GPoll::Final(second) = node.eval(&ctx) else {
panic!("second eval must hit the cell");
};
assert_eq!(first, "lend me");
assert!(std::ptr::eq(first, second));
}
#[test]
fn exhausted_arena_reports_the_operational_error() {
let arena = Arena::new(0);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let node = LendStringNode::new("too big".to_string());
let GPoll::Error(error) = node.eval(&ctx) else {
panic!("exhaustion must surface as an operational error");
};
assert_eq!(error.kind, ErrorKind::ArenaExhausted);
}
#[test]
fn lending_edges_erase_and_wire_like_owned_edges() {
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let value: Box<dyn Any> = Box::new(Box::new(LendStringNode::new("across the boundary".to_string())) as Box<ErasedLendEdge>);
let wired = string_length_constructor(vec![value]).unwrap();
assert_eq!(wired.eval(&ctx), GPoll::Final(19.0));
}
#[test]
fn spec_loop_batches_through_the_erased_lending_edge() {
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let graph: Box<ErasedLendEdge> = Box::new(LendStringNode::new("batched".to_string()));
let mut scratch = [const { MaybeUninit::uninit() }; 3];
let status = graph.eval_batch(&ctx, 0..3, Some(&mut scratch));
let BatchStatus::Filled(lanes, finality) = status else {
panic!("expected filled, got {status:?}");
};
assert_eq!(lanes.len(), 3);
assert!(lanes.iter().all(|lane| std::ptr::eq(*lane, lanes[0])));
assert_eq!(*lanes[0], "batched");
assert_eq!(finality, Finality::AllFinal);
}
#[test]
fn fallback_input_records_partiality_invisibly() {
struct FallbackNode;
impl<Input> GNode<Input> for FallbackNode {
type Output = f64;
fn eval(&self, _input: &Input) -> GPoll<f64> {
GPoll::fallback(0.0, "upstream failed")
}
}
let arena = Arena::new(1024);
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let graph = AddNode::new(FallbackNode, ValueNode(5.0f64));
let GPoll::Fallback(boxed) = graph.eval(&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]);
}
+25 -40
View File
@@ -3,15 +3,12 @@ use crate::brush_stroke::{BrushStroke, BrushStyle};
use core_types::blending::BlendMode; use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::color::{Alpha, Color, Pixel, Sample}; use core_types::color::{Alpha, Color, Pixel, Sample};
use core_types::generic::FnNode;
use core_types::list::{Item, List}; use core_types::list::{Item, List};
use core_types::math::bbox::{AxisAlignedBbox, Bbox}; use core_types::math::bbox::{AxisAlignedBbox, Bbox};
use core_types::registry::FutureWrapperNode;
use core_types::transform::Transform; use core_types::transform::Transform;
use core_types::uuid::NodeId; 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::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM};
use core_types::{Ctx, Node}; use core_types::Ctx;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use raster_nodes::blending_nodes::blend_colors; use raster_nodes::blending_nodes::blend_colors;
use raster_nodes::std_nodes::{empty_image, extend_image_to_bounds}; 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. /// 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. /// Used internally to create the brush texture before stamping it repeatedly along a stroke path.
#[node_macro::node(category(""), skip_impl)] #[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 // Diameter
let radius = diameter / 2.; 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. /// 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)] #[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 where
BlendFn: for<'any_input> Node<'any_input, (Color, Color), Output = Color>, BlendFn: Fn(Color, Color) -> Color,
{ {
if positions.is_empty() { if positions.is_empty() {
return target; return target;
@@ -125,7 +122,7 @@ where
for x in blit_area_offset.x..blit_area_offset.x + blit_area_dimensions.x { 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 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)]; 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 target
} }
pub async fn create_brush_texture(brush_style: &BrushStyle) -> Raster<CPU> { 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 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 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.)); let image = blend_stamp_closure(stamp, blank_texture, |a, b| blend_colors(a, b, BlendMode::Normal, 1.));
image.into_element() 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. /// 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. /// If an input image is supplied, strokes are drawn on top of it, expanding bounds as needed.
#[node_macro::node(category("Raster"))] #[node_macro::node(category("Raster"))]
async fn brush( fn brush(
_: impl Ctx, _: impl Ctx,
/// Optional raster content that may be drawn onto. /// Optional raster content that may be drawn onto.
mut background: List<Raster<CPU>>, mut background: List<Raster<CPU>>,
@@ -224,7 +221,7 @@ async fn brush(
let mut brush_plan = cache.compute_brush_plan(list_item, &draw_strokes); let mut brush_plan = cache.compute_brush_plan(list_item, &draw_strokes);
// TODO: Find a way to handle more than one item // 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(); return List::new();
}; };
@@ -234,7 +231,7 @@ async fn brush(
// TODO: apply rotation from layer to stamp for non-rotationally-symmetric brushes. // TODO: apply rotation from layer to stamp for non-rotationally-symmetric brushes.
let mut brush_texture = cache.get_cached_brush(&stroke.style); let mut brush_texture = cache.get_cached_brush(&stroke.style);
if brush_texture.is_none() { 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()); cache.store_brush(stroke.style.clone(), tex.clone());
brush_texture = Some(tex); 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_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 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 blit_target = if idx == 0 {
let target = core::mem::take(&mut brush_plan.first_stroke_texture); 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 { } else {
empty_image((), stroke_to_layer, List::new_from_element(Color::TRANSPARENT)) empty_image(&(), stroke_to_layer, List::new_from_element(Color::TRANSPARENT))
// EmptyImageNode::new(CopiedNode::new(stroke_to_layer), CopiedNode::new(Color::TRANSPARENT)).eval(())
}; };
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); assert_eq!(list.len(), 1);
list.into_iter().next().unwrap_or_default() 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()) { for stroke in trace.into_iter().map(|row| row.into_element()) {
let mut brush_texture = cache.get_cached_brush(&stroke.style); let mut brush_texture = cache.get_cached_brush(&stroke.style);
if brush_texture.is_none() { 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()); cache.store_brush(stroke.style.clone(), tex.clone());
brush_texture = Some(tex); brush_texture = Some(tex);
} }
@@ -305,17 +295,13 @@ async fn brush(
_ => BlendMode::Restore, _ => BlendMode::Restore,
}; };
let blend_params = FnNode::new(move |(a, b)| blend_colors(a, b, mask_blend_mode, 1.)); 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.))
let blit_node = BlitNode::new( .into_iter()
FutureWrapperNode::new(ClonedNode::new(brush_texture)), .next()
FutureWrapperNode::new(ClonedNode::new(positions)), .unwrap_or_default();
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();
} }
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_colors(a, b, BlendMode::MultiplyAlpha, 1.));
actual_image = blend_image_closure(erase_restore_mask, actual_image, |a, b| blend_params.eval((a, b)));
} }
let transform: DAffine2 = actual_image.attribute_cloned_or_default(ATTR_TRANSFORM); let transform: DAffine2 = actual_image.attribute_cloned_or_default(ATTR_TRANSFORM);
@@ -410,16 +396,16 @@ mod test {
#[test] #[test]
fn test_brush_texture() { fn test_brush_texture() {
let size = 20.; 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.))); assert_eq!(image.transform(), DAffine2::from_scale_angle_translation(DVec2::splat(size.ceil()), 0., -DVec2::splat(size / 2.)));
// center pixel should be BLACK // center pixel should be BLACK
assert_eq!(image.sample(DVec2::splat(0.), DVec2::ONE), Some(Color::BLACK)); assert_eq!(image.sample(DVec2::splat(0.), DVec2::ONE), Some(Color::BLACK));
} }
#[tokio::test] #[test]
async fn test_brush_output_size() { fn test_brush_output_size() {
let image = brush( let image = brush(
(), &(),
&BrushCache::default(), &BrushCache::default(),
List::new_from_element(Raster::new_cpu(Image::<Color>::default())), List::new_from_element(Raster::new_cpu(Image::<Color>::default())),
List::new_from_element(BrushStroke { List::new_from_element(BrushStroke {
@@ -433,8 +419,7 @@ mod test {
blend_mode: BlendMode::Normal, blend_mode: BlendMode::Normal,
}, },
}), }),
) );
.await;
assert_eq!(image.element(0).unwrap().width, 20); assert_eq!(image.element(0).unwrap().width, 20);
} }
} }
+2 -2
View File
@@ -47,7 +47,7 @@ fn read_gradient(ctx: impl Ctx + ExtractVarArgs) -> List<GradientStops> {
} }
#[node_macro::node(category("Context"), path(core_types::vector))] #[node_macro::node(category("Context"), path(core_types::vector))]
async fn read_position( fn read_position(
ctx: impl Ctx + ExtractPosition, ctx: impl Ctx + ExtractPosition,
_primary: (), _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. /// 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. /// 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))] #[node_macro::node(category("Context"), path(core_types::vector))]
async fn read_index( fn read_index(
ctx: impl Ctx + ExtractIndex, ctx: impl Ctx + ExtractIndex,
_primary: (), _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. /// 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.
+6 -6
View File
@@ -569,7 +569,7 @@ pub fn wrap_graphic<T: Into<Graphic>>(
/// Converts a list of graphical content into a `Graphic[]` by placing it into an element of a new wrapper `Graphic[]`. /// 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. /// 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"))] #[node_macro::node(category("General"))]
pub async fn to_graphic<T: IntoGraphicList>( pub fn to_graphic<T: IntoGraphicList>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Graphic>, List<Graphic>,
@@ -587,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. /// Removes a level of nesting from a `Graphic[]`, or all nesting if "Fully Flatten" is enabled.
#[node_macro::node(category("General"))] #[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>? // 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) { 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() { for index in 0..current_graphic_list.len() {
@@ -624,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. /// 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"))] #[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 graphic_list = content.into_graphic_list();
let mut output: List<Vector> = graphic_list.clone().into_flattened_list(); let mut output: List<Vector> = graphic_list.clone().into_flattened_list();
@@ -657,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. /// 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"))] #[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() content.into_flattened_list()
} }
/// Converts a `Graphic[]` into a `Color[]` by deeply flattening any color content it contains, and discarding any non-color content. /// 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"))] #[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() content.into_flattened_list()
} }
/// Converts a `Graphic[]` into a `GradientStops[]` by deeply flattening any gradient content it contains, and discarding any non-gradient content. /// 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"))] #[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() content.into_flattened_list()
} }
@@ -137,7 +137,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. /// 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"))] #[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 placeholder = || -> Arc<[u8]> { Arc::from(Vec::<u8>::new()) };
let response = match reqwest::Client::new().get(&url).send().await { let response = match reqwest::Client::new().get(&url).send().await {
@@ -185,14 +185,14 @@ fn decode_image(_: impl Ctx, data: Arc<[u8]>) -> List<Raster<CPU>> {
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn create_canvas(_: impl Ctx) -> CanvasHandle { fn create_canvas(_: impl Ctx) -> CanvasHandle {
CanvasHandle::new() CanvasHandle::new()
} }
/// Renders a view of the input graphic within an area defined by the *Footprint*. /// Renders a view of the input graphic within an area defined by the *Footprint*.
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn rasterize<T: WasmNotSend + Clone + 'n>( fn rasterize<T: WasmNotSend + Clone>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Vector>, List<Vector>,
@@ -262,12 +262,12 @@ where
} }
#[node_macro::node(category(""), inject_scope)] #[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<'a>(_: impl Ctx, #[scope("editor-api")] editor_api: &'a PlatformEditorApi) -> &'a PlatformEditorApi {
editor_api editor_api
} }
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
pub async fn resource<'a: 'n>(_: impl Ctx, hash: ResourceHash, #[scope(editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi) -> Resource { pub async fn resource<'a>(_: 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"); 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(|| { application_io.load_resource(hash).await.unwrap_or_else(|| {
panic!("Resource {hash} not found"); panic!("Resource {hash} not found");
@@ -275,7 +275,7 @@ pub async fn resource<'a: 'n>(_: impl Ctx, hash: ResourceHash, #[scope(editor_ap
} }
#[node_macro::node(category(""), inject_scope)] #[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 { pub fn wgpu_executor<'a>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi) -> &'a ::wgpu_executor::WgpuExecutor {
editor_api editor_api
.application_io .application_io
.as_ref() .as_ref()
@@ -285,6 +285,16 @@ pub async fn wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)]
} }
#[node_macro::node(category(""), inject_scope)] #[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> { pub fn try_wgpu_executor<'a>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi) -> Option<&'a ::wgpu_executor::WgpuExecutor> {
editor_api.application_io.as_ref()?.gpu_executor() editor_api.application_io.as_ref()?.gpu_executor()
} }
#[node_macro::node(category(""), inject_scope)]
pub fn wgpu_executor_arc<'a>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi) -> std::sync::Arc<::wgpu_executor::WgpuExecutor> {
editor_api
.application_io
.as_ref()
.expect("ApplicationIo not available")
.gpu_executor_arc()
.expect("GPU executor not available")
}
@@ -12,7 +12,7 @@ use wgpu::util::DeviceExt;
use wgpu_executor::{AsyncWgpuPipeline, WgpuExecutor, WgpuPipelineCache}; use wgpu_executor::{AsyncWgpuPipeline, WgpuExecutor, WgpuPipelineCache};
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn render_background<'a: 'n>( fn render_background<'a>(
ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, ctx: impl Ctx + ExtractFootprint + ExtractVarArgs,
#[scope(composite_background_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache, #[scope(composite_background_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache,
data: RenderOutput, data: RenderOutput,
@@ -121,7 +121,7 @@ async fn render_background<'a: 'n>(
} }
#[node_macro::node(category(""), inject_scope)] #[node_macro::node(category(""), inject_scope)]
async fn composite_background_pipeline<'a: 'n>( fn composite_background_pipeline<'a>(
_ctx: impl Ctx, _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<&'a WgpuExecutor>,
#[data] pipeline: WgpuPipelineCache, #[data] pipeline: WgpuPipelineCache,
+1 -1
View File
@@ -321,7 +321,7 @@ fn flood_fill(start: &TileCoord, tile_set: &HashSet<TileCoord>, visited: &mut Ha
} }
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
pub async fn render_output_cache<'a: 'n>( pub async fn render_output_cache<'a>(
ctx: impl Ctx + ExtractAll + CloneVarArgs + ExtractRealTime + ExtractAnimationTime + ExtractPointerPosition + Sync, ctx: impl Ctx + ExtractAll + CloneVarArgs + ExtractRealTime + ExtractAnimationTime + ExtractPointerPosition + Sync,
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<&'a WgpuExecutor>, #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<&'a WgpuExecutor>,
#[scope(crate::platform_application_io::editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi, #[scope(crate::platform_application_io::editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi,
+10 -12
View File
@@ -1,7 +1,7 @@
use core_types::gpoll::Interrupt;
use core_types::list::List; use core_types::list::List;
use core_types::transform::{Footprint, Transform}; use core_types::transform::{Footprint, Transform};
use core_types::{CloneVarArgs, ExtractAll, ExtractVarArgs}; use core_types::{Color, Context, Ctx, DeriveCtx, ExtractFootprint, ExtractVarArgs, OwnedContextImpl, WasmNotSend};
use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNotSend};
use graph_craft::document::value::{RenderOutput, RenderOutputType}; use graph_craft::document::value::{RenderOutput, RenderOutputType};
use graphene_application_io::{ExportFormat, RenderConfig}; use graphene_application_io::{ExportFormat, RenderConfig};
use graphic_types::raster_types::{CPU, Raster}; use graphic_types::raster_types::{CPU, Raster};
@@ -23,8 +23,8 @@ pub struct RenderIntermediate {
} }
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + Sync>( fn render_intermediate<T: 'static + Render + WasmNotSend + Send + Sync>(
ctx: impl Ctx + ExtractVarArgs + ExtractAll + CloneVarArgs, ctx: impl Ctx + ExtractVarArgs + DeriveCtx,
#[implementations( #[implementations(
Context -> List<Artboard>, Context -> List<Artboard>,
Context -> List<Graphic>, Context -> List<Graphic>,
@@ -35,20 +35,18 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
Context -> List<String>, Context -> List<String>,
)] )]
data: impl Node<Context<'_>, Output = T>, data: impl Node<Context<'_>, Output = T>,
) -> RenderIntermediate { ) -> Result<RenderIntermediate, Interrupt> {
let data = data.eval(&ctx.derived())?;
let render_params = ctx let render_params = ctx
.vararg(0) .vararg(0)
.expect("Did not find var args") .expect("Did not find var args")
.downcast_ref::<RenderParams>() .downcast_ref::<RenderParams>()
.expect("Downcasting render params yielded invalid type"); .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 footprint = Footprint::default();
let mut metadata = RenderMetadata::default(); let mut metadata = RenderMetadata::default();
data.collect_metadata(&mut metadata, footprint, None); data.collect_metadata(&mut metadata, footprint, None);
match &render_params.render_output_type { Ok(match &render_params.render_output_type {
RenderOutputTypeRequest::Vello => { RenderOutputTypeRequest::Vello => {
let mut scene = vello::Scene::new(); let mut scene = vello::Scene::new();
@@ -70,11 +68,11 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
metadata, metadata,
} }
} }
} })
} }
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn render<'a: 'n>( fn render<'a>(
ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, 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<&'a WgpuExecutor>,
data: RenderIntermediate, data: RenderIntermediate,
@@ -144,7 +142,7 @@ async fn render<'a: 'n>(
} }
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
async fn create_context<'a: 'n>( fn create_context<'a>(
// Context injections are defined in the wrap_network_in_scope function // Context injections are defined in the wrap_network_in_scope function
render_config: RenderConfig, render_config: RenderConfig,
data: impl Node<Context<'_>, Output = RenderOutput>, data: impl Node<Context<'_>, Output = RenderOutput>,
@@ -8,7 +8,7 @@ use vector_types::vector::style::RenderMode;
use wgpu_executor::{AsyncWgpuPipeline, WgpuExecutor, WgpuPipelineCache}; use wgpu_executor::{AsyncWgpuPipeline, WgpuExecutor, WgpuPipelineCache};
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
pub async fn render_pixel_preview<'a: 'n>( pub async fn render_pixel_preview<'a>(
ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync, ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync,
#[scope(pixel_preview_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache, #[scope(pixel_preview_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache,
data: impl Node<Context<'_>, Output = RenderOutput> + Send + Sync, data: impl Node<Context<'_>, Output = RenderOutput> + Send + Sync,
@@ -75,7 +75,7 @@ pub async fn render_pixel_preview<'a: 'n>(
} }
#[node_macro::node(category(""), inject_scope)] #[node_macro::node(category(""), inject_scope)]
async fn pixel_preview_pipeline<'a: 'n>( fn pixel_preview_pipeline<'a>(
_ctx: impl Ctx, _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<&'a WgpuExecutor>,
#[data] pipeline: WgpuPipelineCache, #[data] pipeline: WgpuPipelineCache,
+1 -1
View File
@@ -23,7 +23,7 @@ pub use vector_types::vector::misc::BooleanOperation;
/// Combines the geometric forms of one or more closed paths into a new vector path that results from cutting or joining the paths by the chosen method. /// 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)] #[node_macro::node(category("Vector: Modifier"), memoize)]
async fn boolean_operation<I: graphic_types::IntoGraphicList>( fn boolean_operation<I: graphic_types::IntoGraphicList>(
_: impl Ctx, _: impl Ctx,
/// The `List` of vector paths to perform the boolean operation on. Nested `List`s are automatically flattened. /// The `List` of vector paths to perform the boolean operation on. Nested `List`s are automatically flattened.
#[implementations(List<Graphic>, List<Vector>)] #[implementations(List<Graphic>, List<Vector>)]
+1 -1
View File
@@ -8,7 +8,7 @@ use raster_types::{CPU, Raster};
use std::cmp::{max, min}; use std::cmp::{max, min};
#[node_macro::node(category("Raster: Filter"))] #[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 image_frame
.into_iter() .into_iter()
.map(|mut row| { .map(|mut row| {
+2 -2
View File
@@ -87,7 +87,7 @@ fn unpremultiply_gamma_to_linear(buffer: Image<PremultipliedGammaPixel>) -> Imag
/// Blurs the image with a Gaussian or box blur kernel filter. /// Blurs the image with a Gaussian or box blur kernel filter.
#[node_macro::node(category("Raster: Filter"))] #[node_macro::node(category("Raster: Filter"))]
async fn blur( fn blur(
_: impl Ctx, _: impl Ctx,
/// The image to be blurred. /// The image to be blurred.
image_frame: List<Raster<CPU>>, image_frame: List<Raster<CPU>>,
@@ -124,7 +124,7 @@ async fn blur(
/// Applies a median filter to reduce noise while preserving edges. /// Applies a median filter to reduce noise while preserving edges.
#[node_macro::node(category("Raster: Filter"))] #[node_macro::node(category("Raster: Filter"))]
async fn median_filter( fn median_filter(
_: impl Ctx, _: impl Ctx,
/// The image to be filtered. /// The image to be filtered.
image_frame: List<Raster<CPU>>, image_frame: List<Raster<CPU>>,
+1 -1
View File
@@ -10,7 +10,7 @@ use vector_types::GradientStops;
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27grdm%27%20%3D%20Gradient%20Map // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27grdm%27%20%3D%20Gradient%20Map
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Gradient%20settings%20(Photoshop%206.0) // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Gradient%20settings%20(Photoshop%206.0)
#[node_macro::node(category("Raster: Adjustment"))] #[node_macro::node(category("Raster: Adjustment"))]
async fn gradient_map<T: Adjust<Color>>( fn gradient_map<T: Adjust<Color>>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Raster<CPU>>, List<Raster<CPU>>,
@@ -4,7 +4,7 @@ use core_types::list::{Item, List};
use raster_types::{CPU, Raster}; use raster_types::{CPU, Raster};
#[node_macro::node(category("Color"))] #[node_macro::node(category("Color"))]
async fn image_color_palette( fn image_color_palette(
_: impl Ctx, _: impl Ctx,
image: List<Raster<CPU>>, image: List<Raster<CPU>>,
#[default(4)] #[default(4)]
@@ -108,7 +108,7 @@ fn replace_transform<T>(
// TODO: Figure out how this node should behave once #2982 is implemented. // 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. /// Obtains the transform of the first item in the input `List`, if present.
#[node_macro::node(category("Math: Transform"), path(core_types::vector))] #[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() 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. /// 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(""))] #[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; use core_types::list::Item;
if vector.is_empty() { 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. /// Applies the vector path's local transformation to its geometry and resets the transform to the identity.
#[node_macro::node(category("Vector"))] #[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); let (elements, transforms) = vector.element_and_attribute_slices_mut::<DAffine2>(ATTR_TRANSFORM);
for (element, transform) in elements.iter_mut().zip(transforms.iter_mut()) { for (element, transform) in elements.iter_mut().zip(transforms.iter_mut()) {
for (_, point) in element.point_domain.positions_mut() { for (_, point) in element.point_domain.positions_mut() {
+33 -33
View File
@@ -89,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. /// 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))] #[node_macro::node(category("Vector: Style"), path(graphene_core::vector))]
async fn assign_colors<T>( fn assign_colors<T>(
_: impl Ctx, _: impl Ctx,
/// The content with vector paths to apply the fill and/or stroke style to. /// The content with vector paths to apply the fill and/or stroke style to.
#[implementations(List<Graphic>, List<Vector>)] #[implementations(List<Graphic>, List<Vector>)]
@@ -157,7 +157,7 @@ where
/// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry. /// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry.
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))] #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))]
async fn fill<V: VectorListIterMut+ Send, F: IntoGraphicList+ Send + 'static>( fn fill<V: VectorListIterMut+ Send, F: IntoGraphicList+ Send + 'static>(
_: impl Ctx, _: impl Ctx,
/// The content with vector paths to apply the fill style to. /// The content with vector paths to apply the fill style to.
#[implementations( #[implementations(
@@ -252,7 +252,7 @@ impl IntoF64Vec for String {
/// Applies a stroke style to the vector content, giving an appearance to the area within the outline of the geometry. /// Applies a stroke style to the vector content, giving an appearance to the area within the outline of the geometry.
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))] #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))]
async fn stroke<V, L: IntoF64Vec, P: IntoGraphicList+ Send + 'static>( fn stroke<V, L: IntoF64Vec, P: IntoGraphicList+ Send + 'static>(
_: impl Ctx, _: impl Ctx,
/// The content with vector paths to apply the stroke style to. /// The content with vector paths to apply the stroke style to.
#[implementations( #[implementations(
@@ -357,7 +357,7 @@ where
} }
#[node_macro::node(name("Copy to Points"), category("Repeat"), path(core_types::vector))] #[node_macro::node(name("Copy to Points"), category("Repeat"), path(core_types::vector))]
async fn copy_to_points<I: Send + Clone>( fn copy_to_points<I: Send + Clone>(
_: impl Ctx, _: impl Ctx,
points: List<Vector>, points: List<Vector>,
/// Artwork to be copied and placed at each point. /// Artwork to be copied and placed at each point.
@@ -441,7 +441,7 @@ async fn copy_to_points<I: Send + Clone>(
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn round_corners( fn round_corners(
_: impl Ctx, _: impl Ctx,
source: List<Vector>, source: List<Vector>,
#[hard(0..)] #[hard(0..)]
@@ -778,7 +778,7 @@ pub mod extrude_algorithms {
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[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() { for vector in source.iter_element_values_mut() {
extrude_algorithms::extrude(vector, direction, joining_algorithm); extrude_algorithms::extrude(vector, direction, joining_algorithm);
} }
@@ -786,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))] #[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 Some(target) = rectangle.element(0).cloned() else { return content };
let target_transform: DAffine2 = rectangle.attribute_cloned_or_default(ATTR_TRANSFORM, 0); let target_transform: DAffine2 = rectangle.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
@@ -871,7 +871,7 @@ fn bilinear_interpolate(t: DVec2, quad: &[DVec2; 4]) -> DVec2 {
} }
#[node_macro::node(category("Vector"), path(graphene_core::vector))] #[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn pack_strips<T: Send + Clone>( fn pack_strips<T: Send + Clone>(
_: impl Ctx, _: impl Ctx,
#[implementations( #[implementations(
List<Graphic>, List<Graphic>,
@@ -992,7 +992,7 @@ where
/// Automatically constructs tangents (Bézier handles) for anchor points in a vector path. /// 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))] #[node_macro::node(category("Vector: Modifier"), name("Auto-Tangents"), path(core_types::vector))]
async fn auto_tangents( fn auto_tangents(
_: impl Ctx, _: impl Ctx,
source: List<Vector>, source: List<Vector>,
/// The amount of spread for the auto-tangents, from 0 (sharp corner) to 1 (full spread). /// The amount of spread for the auto-tangents, from 0 (sharp corner) to 1 (full spread).
@@ -1146,7 +1146,7 @@ async fn auto_tangents(
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[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 content
.into_iter() .into_iter()
.map(|mut row| { .map(|mut row| {
@@ -1171,7 +1171,7 @@ async fn bounding_box(_: impl Ctx, content: List<Vector>) -> List<Vector> {
} }
#[node_macro::node(category("Vector: Measure"), path(core_types::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()) (0..content.len())
.filter_map(|index| content.element(index).unwrap().bounding_box_with_transform(content.attribute_cloned_or_default(ATTR_TRANSFORM, index))) .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)]) .reduce(|[acc_top_left, acc_bottom_right], [top_left, bottom_right]| [acc_top_left.min(top_left), acc_bottom_right.max(bottom_right)])
@@ -1187,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. /// 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))] #[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() { for vector in points.iter_element_values_mut() {
let mut segment_domain = SegmentDomain::new(); let mut segment_domain = SegmentDomain::new();
let mut next_id = SegmentId::ZERO; let mut next_id = SegmentId::ZERO;
@@ -1215,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"))] #[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 content
.into_iter() .into_iter()
.map(|mut row| { .map(|mut row| {
@@ -1259,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))] #[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 // TODO: Make this node support stroke align, which it currently ignores
let graphic_list = content.into_graphic_list(); let graphic_list = content.into_graphic_list();
@@ -1367,7 +1367,7 @@ async fn solidify_stroke<T: IntoGraphicList>(_: impl Ctx, #[implementations(List
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[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 content
.into_iter() .into_iter()
.flat_map(|row| { .flat_map(|row| {
@@ -1398,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. /// 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))] #[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, _: impl Ctx,
/// The vector content whose subpaths are inspected. /// The vector content whose subpaths are inspected.
content: List<Vector>, content: List<Vector>,
@@ -1431,7 +1431,7 @@ fn map_points(ctx: impl Ctx + DeriveCtx, content: List<Vector>, mapped: impl Nod
// TODO: Rename to "Combine Paths" and make this happen per-element instead of flattening every element into a single path. The migration for this should then become a Flatten Vector -> Combine Paths pair of nodes. // TODO: Rename to "Combine Paths" and make this happen per-element instead of flattening every element into a single path. The migration for this should then become a Flatten Vector -> Combine Paths pair of nodes.
#[node_macro::node(category("Vector"), path(graphene_core::vector))] #[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 graphic_list = content.into_graphic_list();
let flattened = graphic_list.clone().into_flattened_list::<Vector>(); let flattened = graphic_list.clone().into_flattened_list::<Vector>();
@@ -1487,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. /// 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)] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector), properties("sample_polyline_properties"), memoize)]
async fn sample_polyline( fn sample_polyline(
_: impl Ctx, _: impl Ctx,
content: List<Vector>, content: List<Vector>,
spacing: PointSpacingType, spacing: PointSpacingType,
@@ -1573,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. /// 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))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn simplify( fn simplify(
_: impl Ctx, _: impl Ctx,
/// The vector paths to simplify. /// The vector paths to simplify.
content: List<Vector>, content: List<Vector>,
@@ -1617,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. /// 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))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn decimate( fn decimate(
_: impl Ctx, _: impl Ctx,
/// The vector paths to decimate. /// The vector paths to decimate.
content: List<Vector>, content: List<Vector>,
@@ -1745,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. /// 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))] #[node_macro::node(category("Vector: Modifier"), path(graphene_core::vector))]
async fn cut_path( fn cut_path(
_: impl Ctx, _: impl Ctx,
/// The path to insert a cut into. /// The path to insert a cut into.
mut content: List<Vector>, mut content: List<Vector>,
@@ -1796,7 +1796,7 @@ async fn cut_path(
/// Cuts path segments into separate disconnected pieces where each is a distinct subpath. /// Cuts path segments into separate disconnected pieces where each is a distinct subpath.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[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 // 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() { for vector in content.iter_element_values_mut() {
let points_count = vector.point_domain.ids().len(); let points_count = vector.point_domain.ids().len();
@@ -1855,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. /// 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))] #[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, _: impl Ctx,
/// The path to traverse. /// The path to traverse.
content: List<Vector>, content: List<Vector>,
@@ -1893,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. /// 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))] #[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, _: impl Ctx,
/// The path to traverse. /// The path to traverse.
content: List<Vector>, content: List<Vector>,
@@ -1941,7 +1941,7 @@ async fn tangent_on_path(
} }
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector), memoize)] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector), memoize)]
async fn scatter_points( fn scatter_points(
_: impl Ctx, _: impl Ctx,
content: List<Vector>, content: List<Vector>,
#[unit(" px")] #[unit(" px")]
@@ -1991,7 +1991,7 @@ async fn scatter_points(
} }
#[node_macro::node(name("Spline"), category("Vector: Modifier"), path(core_types::vector))] #[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 content
.into_iter() .into_iter()
.filter_map(|mut row| { .filter_map(|mut row| {
@@ -2091,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. /// Perturbs the positions of anchor points in vector geometry by random amounts and directions.
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn jitter_points( fn jitter_points(
_: impl Ctx, _: impl Ctx,
/// The vector geometry with points to be jittered. /// The vector geometry with points to be jittered.
content: List<Vector>, content: List<Vector>,
@@ -2141,7 +2141,7 @@ async fn jitter_points(
/// Displaces anchor points along their normal direction (perpendicular to the path) by a set distance. /// 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. /// 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))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn offset_points( fn offset_points(
_: impl Ctx, _: impl Ctx,
/// The vector geometry with points to be offset. /// The vector geometry with points to be offset.
content: List<Vector>, content: List<Vector>,
@@ -2178,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. /// *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))] #[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
async fn morph<I: IntoGraphicList>( fn morph<I: IntoGraphicList>(
_: impl Ctx, _: impl Ctx,
/// The vector objects to interpolate between. Mixed graphic content is deeply flattened to keep only vector elements. /// The vector objects to interpolate between. Mixed graphic content is deeply flattened to keep only vector elements.
#[implementations(List<Graphic>, List<Vector>)] #[implementations(List<Graphic>, List<Vector>)]
@@ -3125,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: 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.) // 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))] #[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 content.len() as f64
} }
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))] #[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() 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. /// 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. /// If no value exists at that index, the position (0, 0) is returned.
#[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))] #[node_macro::node(category("Vector: Measure"), path(graphene_core::vector))]
async fn index_points( fn index_points(
_: impl Ctx, _: impl Ctx,
/// The vector element or elements containing the anchor points to be retrieved. /// The vector element or elements containing the anchor points to be retrieved.
content: List<Vector>, content: List<Vector>,
@@ -3171,7 +3171,7 @@ async fn index_points(
} }
#[node_macro::node(category("Vector: Measure"), path(core_types::vector))] #[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()) (0..source.len())
.map(|index| { .map(|index| {
let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM, index);