mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-17 23:38:06 +08:00
399 lines
14 KiB
Rust
399 lines
14 KiB
Rust
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<T: Send + Clone + 'static>(
|
|
ctx: impl ExtractAll + CloneVarArgs + Ctx,
|
|
#[implementations(
|
|
Context -> List<String>,
|
|
Context -> List<bool>,
|
|
Context -> List<f32>,
|
|
Context -> List<f64>,
|
|
Context -> List<u32>,
|
|
Context -> List<u64>,
|
|
Context -> List<DVec2>,
|
|
Context -> List<DAffine2>,
|
|
Context -> List<Vector>,
|
|
Context -> List<Graphic>,
|
|
Context -> List<Raster<CPU>>,
|
|
Context -> List<Raster<GPU>>,
|
|
Context -> List<Color>,
|
|
Context -> List<Gradient>,
|
|
Context -> List<Artboard>,
|
|
)]
|
|
content: impl Node<'n, Context<'static>, Output = List<T>>,
|
|
#[default(1)]
|
|
#[hard(1..)]
|
|
count: Item<u32>,
|
|
reverse: Item<bool>,
|
|
) -> List<T> {
|
|
// 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<T: Send + Clone + 'static>(
|
|
ctx: impl ExtractAll + CloneVarArgs + Ctx,
|
|
#[implementations(
|
|
Context -> List<String>,
|
|
Context -> List<bool>,
|
|
Context -> List<f32>,
|
|
Context -> List<f64>,
|
|
Context -> List<u32>,
|
|
Context -> List<u64>,
|
|
Context -> List<DVec2>,
|
|
Context -> List<DAffine2>,
|
|
Context -> List<Vector>,
|
|
Context -> List<Graphic>,
|
|
Context -> List<Raster<CPU>>,
|
|
Context -> List<Raster<GPU>>,
|
|
Context -> List<Color>,
|
|
Context -> List<Gradient>,
|
|
Context -> List<Artboard>,
|
|
)]
|
|
content: impl Node<'n, Context<'static>, 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: Item<PixelSize>,
|
|
angle: Item<Angle>,
|
|
#[default(5)]
|
|
#[hard(1..)]
|
|
count: Item<u32>,
|
|
) -> List<T> {
|
|
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::<attr::Transform>();
|
|
let local_translation = DAffine2::from_translation(local_transform.translation);
|
|
let local_matrix = DAffine2::from_mat2(local_transform.matrix2);
|
|
*row.attr_mut_or_insert_default::<attr::Transform>() = local_translation * transform * local_matrix;
|
|
|
|
result_list.push(row);
|
|
}
|
|
}
|
|
|
|
result_list
|
|
}
|
|
|
|
#[node_macro::node(category("Repeat"))]
|
|
async fn repeat_radial<T: Send + Clone + 'static>(
|
|
ctx: impl ExtractAll + CloneVarArgs + Ctx,
|
|
#[implementations(
|
|
Context -> List<String>,
|
|
Context -> List<bool>,
|
|
Context -> List<f32>,
|
|
Context -> List<f64>,
|
|
Context -> List<u32>,
|
|
Context -> List<u64>,
|
|
Context -> List<DVec2>,
|
|
Context -> List<DAffine2>,
|
|
Context -> List<Vector>,
|
|
Context -> List<Graphic>,
|
|
Context -> List<Raster<CPU>>,
|
|
Context -> List<Raster<GPU>>,
|
|
Context -> List<Color>,
|
|
Context -> List<Gradient>,
|
|
Context -> List<Artboard>,
|
|
)]
|
|
content: impl Node<'n, Context<'static>, Output = List<T>>,
|
|
start_angle: Item<Angle>,
|
|
#[unit(" px")]
|
|
#[default(5)]
|
|
radius: Item<f64>,
|
|
#[default(5)]
|
|
#[hard(1..)]
|
|
count: Item<u32>,
|
|
) -> List<T> {
|
|
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::<attr::Transform>();
|
|
let local_translation = DAffine2::from_translation(local_transform.translation);
|
|
let local_matrix = DAffine2::from_mat2(local_transform.matrix2);
|
|
*row.attr_mut_or_insert_default::<attr::Transform>() = 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<T: Send + Clone + 'static>(
|
|
ctx: impl ExtractAll + CloneVarArgs + Sync + Ctx + InjectVarArgs,
|
|
points: List<Vector>,
|
|
#[implementations(
|
|
Context -> List<String>,
|
|
Context -> List<bool>,
|
|
Context -> List<f32>,
|
|
Context -> List<f64>,
|
|
Context -> List<u32>,
|
|
Context -> List<u64>,
|
|
Context -> List<DVec2>,
|
|
Context -> List<DAffine2>,
|
|
Context -> List<Vector>,
|
|
Context -> List<Graphic>,
|
|
Context -> List<Raster<CPU>>,
|
|
Context -> List<Raster<GPU>>,
|
|
Context -> List<Color>,
|
|
Context -> List<Gradient>,
|
|
Context -> List<Artboard>,
|
|
)]
|
|
content: impl Node<'n, Context<'static>, Output = List<T>>,
|
|
reverse: Item<bool>,
|
|
) -> List<T> {
|
|
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::<attr::Transform>(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::<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::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<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 })
|
|
}
|
|
}
|
|
|
|
// Raises a generator's rank-0 `Item<Vector>` output to a singleton `List<Vector>` for the still-list-typed Repeat on Points content connector
|
|
#[derive(Clone)]
|
|
pub struct RaiseToListNode<N>(N);
|
|
|
|
impl<'i, I: 'i, N> Node<'i, I> for RaiseToListNode<N>
|
|
where
|
|
N: Node<'i, I, Output = Pin<Box<dyn Future<Output = Item<Vector>> + 'i + Send>>>,
|
|
{
|
|
type Output = Pin<Box<dyn Future<Output = List<Vector>> + '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::<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))),
|
|
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}");
|
|
}
|
|
}
|
|
}
|