use crate::gcore::Context; use core::f64::consts::TAU; use core_types::attr; use core_types::list::{Item, List}; use core_types::registry::types::{Angle, PixelSize}; use core_types::{CloneVarArgs, Color, Ctx, ExtractAll, InjectVarArgs, OwnedContextImpl}; use glam::{DAffine2, DVec2}; use graphic_types::{Artboard, Graphic, Vector}; use raster_types::{CPU, GPU, Raster}; use vector_types::Gradient; #[node_macro::node(category("Repeat"))] async fn repeat( ctx: impl ExtractAll + CloneVarArgs + Ctx, #[implementations( Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List>, Context -> List>, Context -> List, Context -> List, Context -> List, )] content: impl Node<'n, Context<'static>, Output = List>, #[default(1)] #[hard(1..)] count: Item, reverse: Item, ) -> List { // Someday this node can have the option to generate infinitely instead of a fixed count (basically `std::iter::repeat`). let (count, reverse) = (count.into_element(), reverse.into_element()); let count = count as usize; 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; for generated_row in generated_content.into_iter() { result_list.push(generated_row); } } result_list } #[node_macro::node(category("Repeat"))] pub async fn repeat_array( ctx: impl ExtractAll + CloneVarArgs + Ctx, #[implementations( Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List>, Context -> List>, Context -> List, Context -> List, Context -> List, )] content: impl Node<'n, Context<'static>, Output = List>, #[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: Item, angle: Item, #[default(5)] #[hard(1..)] count: Item, ) -> List { let (direction, angle, count) = (direction.into_element(), angle.into_element(), count.into_element()); 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 mut result_list = List::new(); for index in 0..count { let angle = index as f64 * angle / total; 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; for row_index in 0..generated_content.len() { let Some(mut row) = generated_content.clone_item(row_index) else { continue }; let local_transform = row.attr_cloned_or_default::(); let local_translation = DAffine2::from_translation(local_transform.translation); let local_matrix = DAffine2::from_mat2(local_transform.matrix2); *row.attr_mut_or_insert_default::() = local_translation * transform * local_matrix; result_list.push(row); } } result_list } #[node_macro::node(category("Repeat"))] async fn repeat_radial( ctx: impl ExtractAll + CloneVarArgs + Ctx, #[implementations( Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List>, Context -> List>, Context -> List, Context -> List, Context -> List, )] content: impl Node<'n, Context<'static>, Output = List>, start_angle: Item, #[unit(" px")] #[default(5)] radius: Item, #[default(5)] #[hard(1..)] count: Item, ) -> List { let (start_angle, radius, count) = (start_angle.into_element(), radius.into_element(), count.into_element()); let mut result_list = List::new(); for index in 0..count { let angle = DAffine2::from_angle((TAU / count as f64) * index as f64 + start_angle.to_radians()); 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; for row_index in 0..generated_content.len() { let Some(mut row) = generated_content.clone_item(row_index) else { continue }; let local_transform = row.attr_cloned_or_default::(); let local_translation = DAffine2::from_translation(local_transform.translation); let local_matrix = DAffine2::from_mat2(local_transform.matrix2); *row.attr_mut_or_insert_default::() = local_translation * transform * local_matrix; result_list.push(row); } } result_list } #[node_macro::node(category("Repeat"), name("Repeat on Points"))] async fn repeat_on_points( ctx: impl ExtractAll + CloneVarArgs + Sync + Ctx + InjectVarArgs, points: List, #[implementations( Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List, Context -> List>, Context -> List>, Context -> List, Context -> List, Context -> List, )] content: impl Node<'n, Context<'static>, Output = List>, reverse: Item, ) -> List { let reverse = reverse.into_element(); 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 = points.attr_cloned_or_default::(points_index); let mut iteration = async |index, point| { 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; for mut generated_row in generated_content.into_iter() { generated_row.attr_mut_or_insert_default::().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::list::Item; 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; use vector_types::vector::misc::BoxCorners; fn vector_node_from_bezpath(bezpath: BezPath) -> List { List::new_from_element(Vector::from_bezpath(bezpath)) } #[derive(Clone)] pub struct FutureWrapperNode(T); impl<'i, I: Ctx, T: 'i + Clone + Send> Node<'i, I> for FutureWrapperNode { type Output = Pin + 'i + Send>>; fn eval(&'i self, _input: I) -> Self::Output { let value = self.0.clone(); Box::pin(async move { value }) } } // Raises a generator's rank-0 `Item` output to a singleton `List` for the still-list-typed Repeat on Points content connector #[derive(Clone)] pub struct RaiseToListNode(N); impl<'i, I: 'i, N> Node<'i, I> for RaiseToListNode where N: Node<'i, I, Output = Pin> + 'i + Send>>>, { type Output = Pin> + 'i + Send>>; fn eval(&'i self, input: I) -> Self::Output { let future = self.0.eval(input); Box::pin(async move { future.await.into() }) } } #[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(Item::new_from_element(0_u32))), FutureWrapperNode(Item::new_from_element(XY::Y)), ), FutureWrapperNode(Item::new_from_element(2_f64)), FutureWrapperNode(Item::new_from_element(BoxCorners::default())), FutureWrapperNode(Item::new_from_element(false)), FutureWrapperNode(Item::new_from_element(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, &RaiseToListNode(rect), Item::new_from_element(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.attr_cloned_or_default::(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))), Item::new_from_element(direction), Item::new_from_element(0.), Item::new_from_element(count), ) .await; let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::Vector(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))), Item::new_from_element(DVec2::new(12., 10.)), Item::new_from_element(45.), Item::new_from_element(1), ) .await; let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::Vector(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))), Item::new_from_element(direction), Item::new_from_element(0.), Item::new_from_element(count), ) .await; let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::Vector(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))), Item::new_from_element(45.), Item::new_from_element(4.), Item::new_from_element(8), ) .await; let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::Vector(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}"); } } }