diff --git a/Cargo.lock b/Cargo.lock index da51dd98b8..cd6e5d9689 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/node-graph/nodes/repeat/Cargo.toml b/node-graph/nodes/repeat/Cargo.toml index ec3c38e495..9f2d9e261c 100644 --- a/node-graph/nodes/repeat/Cargo.toml +++ b/node-graph/nodes/repeat/Cargo.toml @@ -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 } diff --git a/node-graph/nodes/repeat/src/repeat_nodes.rs b/node-graph/nodes/repeat/src/repeat_nodes.rs index c2fa1b94de..a277dc53d7 100644 --- a/node-graph/nodes/repeat/src/repeat_nodes.rs +++ b/node-graph/nodes/repeat/src/repeat_nodes.rs @@ -179,3 +179,145 @@ fn repeat_on_points + 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); + + impl Node for ValueNode { + type Output = T; + + fn eval(&self, _input: &Input) -> GPoll { + GPoll::Final(self.0.clone()) + } + } + + /// Returns one default `Vector` whose transform records the innermost context index in its x translation. + struct IndexProbe; + + impl Node for IndexProbe { + type Output = List; + + fn eval(&self, input: &Input) -> GPoll> { + 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 Node for PositionProbe { + type Output = List; + + fn eval(&self, input: &Input) -> GPoll> { + 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 { + List::new_from_element(Vector::default()) + } + + fn row_translations(list: &List, key: &str) -> Vec { + (0..list.len()).map(|index| list.attribute_cloned_or_default::(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); + } +}