mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Add test coverage for the repeat nodes
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -4880,15 +4880,11 @@ dependencies = [
|
||||
"core-types",
|
||||
"dyn-any",
|
||||
"glam",
|
||||
"graphene-core",
|
||||
"graphic-types",
|
||||
"kurbo",
|
||||
"log",
|
||||
"node-macro",
|
||||
"raster-types",
|
||||
"serde",
|
||||
"tokio",
|
||||
"vector-nodes",
|
||||
"vector-types",
|
||||
]
|
||||
|
||||
|
||||
@@ -24,9 +24,3 @@ log = { workspace = true }
|
||||
|
||||
# Optional workspace dependencies
|
||||
serde = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
graphene-core = { workspace = true }
|
||||
vector-nodes = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt"] }
|
||||
kurbo = { workspace = true }
|
||||
|
||||
@@ -179,3 +179,145 @@ fn repeat_on_points<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
|
||||
Ok(result_list)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use core_types::arena::Arena;
|
||||
use core_types::context::{ContextImpl, EvalScope, ExtractIndex, ExtractPosition};
|
||||
use core_types::gpoll::GPoll;
|
||||
use core_types::list::Item;
|
||||
use core_types::node::{LazyInput, Node, StatusCell};
|
||||
use vector_types::subpath::Subpath;
|
||||
|
||||
const TEST_POSITION: &str = "test-position";
|
||||
|
||||
struct ValueNode<T>(T);
|
||||
|
||||
impl<T: Clone, Input> Node<Input> for ValueNode<T> {
|
||||
type Output = T;
|
||||
|
||||
fn eval(&self, _input: &Input) -> GPoll<T> {
|
||||
GPoll::Final(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns one default `Vector` whose transform records the innermost context index in its x translation.
|
||||
struct IndexProbe;
|
||||
|
||||
impl<Input: ExtractIndex> Node<Input> for IndexProbe {
|
||||
type Output = List<Vector>;
|
||||
|
||||
fn eval(&self, input: &Input) -> GPoll<List<Vector>> {
|
||||
let index = input.try_index().and_then(|mut levels| levels.next()).expect("repeat must push an index level");
|
||||
let mut list = List::new();
|
||||
list.push(Item::new_from_element(Vector::default()).with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(DVec2::new(index as f64, 0.))));
|
||||
GPoll::Final(list)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns one default `Vector` recording the innermost context position under `TEST_POSITION`.
|
||||
struct PositionProbe;
|
||||
|
||||
impl<Input: ExtractPosition> Node<Input> for PositionProbe {
|
||||
type Output = List<Vector>;
|
||||
|
||||
fn eval(&self, input: &Input) -> GPoll<List<Vector>> {
|
||||
let position = input.try_position().and_then(|mut positions| positions.next()).expect("repeat_on_points must push a position level");
|
||||
let mut list = List::new();
|
||||
list.push(Item::new_from_element(Vector::default()).with_attribute(TEST_POSITION, DAffine2::from_translation(position)));
|
||||
GPoll::Final(list)
|
||||
}
|
||||
}
|
||||
|
||||
fn single_default_vector() -> List<Vector> {
|
||||
List::new_from_element(Vector::default())
|
||||
}
|
||||
|
||||
fn row_translations(list: &List<Vector>, key: &str) -> Vec<DVec2> {
|
||||
(0..list.len()).map(|index| list.attribute_cloned_or_default::<DAffine2>(key, index).translation).collect()
|
||||
}
|
||||
|
||||
macro_rules! test_ctx {
|
||||
($ctx:ident, $cell:ident) => {
|
||||
let arena = Arena::new(4096).unwrap();
|
||||
let generations = [];
|
||||
let scope = EvalScope::new(None, None, None, &generations, &arena);
|
||||
let $ctx = ContextImpl::root(&scope);
|
||||
let $cell = StatusCell::default();
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeat_pushes_the_iteration_index_in_order() {
|
||||
test_ctx!(ctx, cell);
|
||||
|
||||
let x_translations = |values: [f64; 3]| values.map(|x| DVec2::new(x, 0.)).to_vec();
|
||||
|
||||
let forward = super::repeat(&ctx, LazyInput::new(&IndexProbe, &cell, 0), 3, false).unwrap();
|
||||
assert_eq!(row_translations(&forward, ATTR_TRANSFORM), x_translations([0., 1., 2.]));
|
||||
|
||||
let reversed = super::repeat(&ctx, LazyInput::new(&IndexProbe, &cell, 0), 3, true).unwrap();
|
||||
assert_eq!(row_translations(&reversed, ATTR_TRANSFORM), x_translations([2., 1., 0.]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeat_array_spaces_copies_along_the_direction() {
|
||||
test_ctx!(ctx, cell);
|
||||
let direction = DVec2::new(1.5, 0.);
|
||||
let count = 3;
|
||||
|
||||
let content = ValueNode(single_default_vector());
|
||||
let repeated = super::repeat_array(&ctx, LazyInput::new(&content, &cell, 0), direction, 0., count).unwrap();
|
||||
|
||||
assert_eq!(repeated.len(), count as usize);
|
||||
for (index, translation) in row_translations(&repeated, ATTR_TRANSFORM).into_iter().enumerate() {
|
||||
let expected = index as f64 * direction / (count - 1) as f64;
|
||||
assert!(translation.abs_diff_eq(expected, 1e-10), "copy {index}: {translation:?} != {expected:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeat_array_single_copy_stays_finite() {
|
||||
test_ctx!(ctx, cell);
|
||||
|
||||
let content = ValueNode(single_default_vector());
|
||||
let repeated = super::repeat_array(&ctx, LazyInput::new(&content, &cell, 0), DVec2::new(12., 10.), 45., 1).unwrap();
|
||||
|
||||
assert_eq!(repeated.len(), 1);
|
||||
let transform: DAffine2 = repeated.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
||||
assert!(transform.abs_diff_eq(DAffine2::IDENTITY, 1e-10), "single copy must not divide by zero: {transform:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeat_radial_rotates_copies_around_the_center() {
|
||||
test_ctx!(ctx, cell);
|
||||
let (radius, count) = (5., 4);
|
||||
|
||||
let content = ValueNode(single_default_vector());
|
||||
let repeated = super::repeat_radial(&ctx, LazyInput::new(&content, &cell, 0), 0., radius, count).unwrap();
|
||||
|
||||
assert_eq!(repeated.len(), count as usize);
|
||||
for index in 0..count as usize {
|
||||
let transform: DAffine2 = repeated.attribute_cloned_or_default(ATTR_TRANSFORM, index);
|
||||
let expected = DAffine2::from_angle((TAU / count as f64) * index as f64) * DAffine2::from_translation(radius * DVec2::Y);
|
||||
assert!(transform.abs_diff_eq(expected, 1e-10), "copy {index}: {transform:?} != {expected:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeat_on_points_pushes_each_point_as_the_position() {
|
||||
test_ctx!(ctx, cell);
|
||||
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(&ctx, points.clone(), LazyInput::new(&PositionProbe, &cell, 0), false).unwrap();
|
||||
assert_eq!(row_translations(&generated, ATTR_TRANSFORM), positions.to_vec());
|
||||
assert_eq!(row_translations(&generated, TEST_POSITION), positions.to_vec());
|
||||
|
||||
let reversed = super::repeat_on_points(&ctx, points, LazyInput::new(&PositionProbe, &cell, 0), true).unwrap();
|
||||
let mut expected = positions.to_vec();
|
||||
expected.reverse();
|
||||
assert_eq!(row_translations(&reversed, ATTR_TRANSFORM), expected);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user