Add test coverage for the repeat nodes

This commit is contained in:
Dennis Kobert
2026-08-02 16:18:07 +00:00
parent d385f3c69b
commit ee499be31f
3 changed files with 142 additions and 10 deletions

View File

@@ -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 }

View File

@@ -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);
}
}