From 2575b94e8f6fd62b0d1e0e2f9b8f7a76a182d26d Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Sun, 23 Aug 2026 19:50:36 +0000 Subject: [PATCH] Swap the leveled repeat_radial and repeat_on_points in for the eager variants --- .../messages/portfolio/document_migration.rs | 11 + node-graph/nodes/repeat/src/leveled.rs | 418 ---------------- node-graph/nodes/repeat/src/lib.rs | 1 - node-graph/nodes/repeat/src/repeat_nodes.rs | 459 ++++++++++++------ 4 files changed, 328 insertions(+), 561 deletions(-) delete mode 100644 node-graph/nodes/repeat/src/leveled.rs diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index 226027e4fd..5e1add6c8a 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -1403,6 +1403,17 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], } } + // The leveled-records flip moved the Repeat on Points content wire ahead of the points wire. + if reference == DefinitionIdentifier::ProtoNode(graphene_std::repeat::repeat_on_points::IDENTIFIER) { + let mut node_template = node_definition.default_node_template(); + let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; + document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[1].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[0].clone(), network_path); + for (index, input) in old_inputs.into_iter().enumerate().skip(2) { + document.network_interface.set_input(&InputConnector::node(*node_id, index), input, network_path); + } + } + // The leveled-records flip gave Mandelbrot a unit primary input. if reference == DefinitionIdentifier::ProtoNode(graphene_std::raster_nodes::std_nodes::mandelbrot::IDENTIFIER) { let mut node_template = node_definition.default_node_template(); diff --git a/node-graph/nodes/repeat/src/leveled.rs b/node-graph/nodes/repeat/src/leveled.rs deleted file mode 100644 index 1f8d2f14b5..0000000000 --- a/node-graph/nodes/repeat/src/leveled.rs +++ /dev/null @@ -1,418 +0,0 @@ -//! The rank-model repeat family: lazy leveled creators that evaluate their -//! content once per copy and compose the per-copy transform onto each row. -//! Test-category until the flip swaps them in for the eager variants. - -use core::f64::consts::TAU; -use core_types::attribute::{Attr, Transform}; -use core_types::context::{ExtractIndex, IndexLink}; -use core_types::extent::{ExtentIn, LevelIn, ListIn, ValueIn}; -use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt}; -use core_types::registry::types::{Angle, PixelSize}; -use core_types::{Ctx, DeriveCtx, InjectIndex}; -use glam::{DAffine2, DVec2}; -use graphic_types::Vector; - -/// The rank-model Repeat Array: each copy evaluates the lazy content at its -/// own index and composes the linear step onto the row's transform. -#[node_macro::node(category("Test"), extent(repeat_array_extent))] -fn repeat_array( - ctx: impl Ctx + DeriveCtx + ExtractIndex, - content: impl Node, Output = (T, Attr)>, - #[default(100., 100.)] direction: PixelSize, - angle: Angle, - #[default(5)] - #[hard(1..)] - count: u32, -) -> Result)>, Interrupt> { - let inner = content.inner_extent(ctx)?; - let (copy, rest) = ctx.split_innermost(inner); - if copy >= count as u64 { - return Err(GraphError::new("repeat addressed past its copy count").into()); - } - let mut frame = IndexLink { index: 0, outer: None }; - let (element, local) = content.eval(&ctx.push_level(&mut frame, copy, rest))?; - - // A single copy has no steps between copies, so the denominator stays 1. - let total = (count - 1).max(1) as f64; - let step = DAffine2::from_angle(copy as f64 * angle.to_radians() / total) * DAffine2::from_translation(copy as f64 * direction / total); - let local_translation = DAffine2::from_translation(local.translation); - let local_matrix = DAffine2::from_mat2(local.matrix2); - Ok(emit(element, Attr(local_translation * step * local_matrix))) -} - -/// The pushed level's extent is the copy count; inner levels forward to the -/// content, whose extent is taken uniform across copies (queried at copy 0). -fn repeat_array_extent(content: ExtentIn<'_>, _direction: ValueIn<'_, PixelSize>, _angle: ValueIn<'_, Angle>, count: ValueIn<'_, u32>, level: LevelIn) -> GPoll { - match level.pushed() { - true => count.get().map(|count| Extent::Exactly(count as usize)), - false => content.at(level), - } -} - -/// The rank-model Repeat Radial: each copy evaluates the lazy content at its -/// own index and rotates it around the center by its share of the turn. -#[node_macro::node(category("Test"), extent(repeat_radial_extent))] -fn repeat_radial( - ctx: impl Ctx + DeriveCtx + ExtractIndex, - content: impl Node, Output = (T, Attr)>, - start_angle: Angle, - #[unit(" px")] - #[default(5)] - radius: f64, - #[default(5)] - #[hard(1..)] - count: u32, -) -> Result)>, Interrupt> { - let inner = content.inner_extent(ctx)?; - let (copy, rest) = ctx.split_innermost(inner); - if copy >= count as u64 { - return Err(GraphError::new("repeat addressed past its copy count").into()); - } - let mut frame = IndexLink { index: 0, outer: None }; - let (element, local) = content.eval(&ctx.push_level(&mut frame, copy, rest))?; - - let angle = DAffine2::from_angle((TAU / count as f64) * copy as f64 + start_angle.to_radians()); - let translation = DAffine2::from_translation(radius * DVec2::Y); - let step = angle * translation; - let local_translation = DAffine2::from_translation(local.translation); - let local_matrix = DAffine2::from_mat2(local.matrix2); - Ok(emit(element, Attr(local_translation * step * local_matrix))) -} - -/// The pushed level's extent is the copy count; inner levels forward to the -/// content, whose extent is taken uniform across copies (queried at copy 0). -fn repeat_radial_extent(content: ExtentIn<'_>, _start_angle: ValueIn<'_, Angle>, _radius: ValueIn<'_, f64>, count: ValueIn<'_, u32>, level: LevelIn) -> GPoll { - match level.pushed() { - true => count.get().map(|count| Extent::Exactly(count as usize)), - false => content.at(level), - } -} - -/// The rank-model Repeat on Points: the pushed level flattens every point of -/// every points row, mirroring the eager iteration order (rows in order, a -/// row's points reversed when `reverse` is set); each copy evaluates the -/// content with its point's transformed position pushed, then lands the -/// content row's transform on that position. -#[node_macro::node(category("Test"), name("Repeat on Points"), extent(repeat_on_points_extent))] -fn repeat_on_points( - ctx: impl Ctx + DeriveCtx + ExtractIndex + InjectIndex + Copy, - content: impl Node, Output = (T, Attr)>, - points: IList, - reverse: bool, -) -> Result)>, Interrupt> { - let inner = content.inner_extent(ctx)?; - let (copy, rest) = ctx.split_innermost(inner); - - let mut remaining = copy as usize; - for row_index in 0..points.len() { - let vector = points.element_ref(row_index); - let positions = vector.point_domain.positions(); - if remaining >= positions.len() { - remaining -= positions.len(); - continue; - } - let index = match reverse { - true => positions.len() - 1 - remaining, - false => remaining, - }; - let transform: DAffine2 = points.lane(row_index).attr::(); - let transformed_point = transform.transform_point2(positions[index]); - - let scoped = ctx.push_position(transformed_point); - let mut frame = IndexLink { index: 0, outer: None }; - let (element, local) = content.eval(&scoped.ctx().push_level(&mut frame, index as u64, rest))?; - let mut composed = *local; - composed.translation = transformed_point; - return Ok(emit(element, Attr(composed))); - } - Err(GraphError::new("repeat on points addressed past its point count").into()) -} - -/// The pushed level's extent is the flattened point count across the points -/// rows; inner levels forward to the content, uniform across copies. -fn repeat_on_points_extent(content: ExtentIn<'_>, points: ListIn<'_, Vector>, _reverse: ValueIn<'_, bool>, level: LevelIn) -> GPoll { - match level.pushed() { - true => points - .get() - .map(|points| Extent::Exactly((0..points.len()).map(|row| points.element_ref(row).point_domain.positions().len()).sum())), - false => content.at(level), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use core_types::SourceId; - use core_types::arena::Arena; - use core_types::attribute::Attribute as AttributeMarker; - use core_types::context::{ContextImpl, EvalScope}; - use core_types::node::Node; - use core_types::record::{FieldWrite, Layout, Rec, RecordSource, RecordValue, element_write, stack}; - use vector_types::subpath::Subpath; - - struct ValueNode(T); - - impl Node for ValueNode { - type Output = T; - - fn eval(&self, _input: &Input) -> GPoll { - GPoll::Final(self.0.clone()) - } - } - - struct TransformSource { - layout: Layout, - element: f64, - transform: DAffine2, - } - - impl<'e> Node> for TransformSource { - type Output = RecordValue<'e>; - - fn eval(&self, _input: &ContextImpl<'e>) -> GPoll> { - let dst = stack::push(self.layout.frame_bytes()); - unsafe { - dst.cast::().write(self.element); - dst.add(self.layout.offset_of(Transform::NAME, 0).unwrap()).cast::().write(self.transform); - } - stack::pop(dst); - GPoll::Final(RecordValue::spilled(unsafe { Rec::new(dst.cast_const()) })) - } - } - - fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> { - stack::reserve(1 << 12); - EvalScope::new(Some(0.5), None, None, generations, arena) - } - - struct VectorRows { - layout: Layout, - rows: Vec<(Vector, DAffine2)>, - } - - impl<'e> Node> for VectorRows { - type Output = RecordValue<'e>; - - fn eval(&self, input: &ContextImpl<'e>) -> GPoll> { - use core_types::context::{ExtractArena, ExtractIndex}; - let (vector, transform) = &self.rows[input.innermost_index() as usize % self.rows.len()]; - let dst = stack::push(self.layout.frame_bytes()); - // SAFETY: dst is the claimed frame of this layout; offsets are the layout's own. - unsafe { - if core_types::record::write_element(dst, vector.clone(), input.arena()).is_none() { - return GPoll::Error(Box::new(core_types::gpoll::GraphError::new("arena exhausted"))); - } - core_types::record::write_field(dst, self.layout.offset_of(::NAME, 0).unwrap(), *transform); - } - stack::pop(dst); - GPoll::Final(RecordValue::spilled(unsafe { Rec::new(dst.cast_const()) })) - } - - fn extent_at(&self, _input: &ContextImpl<'e>, _level: u8) -> GPoll { - GPoll::Final(Extent::Exactly(self.rows.len())) - } - - fn layout(&self) -> &Layout { - &self.layout - } - } - - fn vector_rows_layout() -> Layout { - Layout::default().with_writes(1, element_write::(), &[FieldWrite::of::(0)]) - } - - struct PositionProbe { - layout: Layout, - } - - impl<'e> Node> for PositionProbe { - type Output = RecordValue<'e>; - - fn eval(&self, input: &ContextImpl<'e>) -> GPoll> { - use core_types::context::ExtractPosition; - let position = input.try_position().and_then(|mut positions| positions.next()).unwrap_or(DVec2::ZERO); - let dst = stack::push(self.layout.frame_bytes()); - // SAFETY: dst is the claimed frame of this layout; offsets are the layout's own. - unsafe { - dst.cast::().write(position.x); - core_types::record::write_field(dst, self.layout.offset_of(::NAME, 0).unwrap(), DAffine2::IDENTITY); - } - stack::pop(dst); - GPoll::Final(RecordValue::spilled(unsafe { Rec::new(dst.cast_const()) })) - } - - fn layout(&self) -> &Layout { - &self.layout - } - } - - fn transform_layout() -> Layout { - Layout::default().with_writes(0, element_write::(), &[FieldWrite::of::(0)]) - } - - #[test] - fn repeat_array_composes_the_step_onto_each_copys_transform() { - let arena = Arena::new(1024).unwrap(); - let generations = []; - let scope = scope_fixture(&generations, &arena); - let ctx = ContextImpl::root(&scope); - - let layout = transform_layout(); - let content = TransformSource { - layout: layout.clone(), - element: 7., - transform: DAffine2::from_translation(DVec2::new(5., 5.)), - }; - - let mut node = RepeatArrayNode::new( - RecordSource::new(content, &layout, &layout), - ValueNode(DVec2::new(10., 0.)), - ValueNode(0.0f64), - ValueNode(3u32), - &layout, - ); - Node::::set_layout(&mut node, repeat_array_layout_meta().resolve(&[Some(&layout)])); - let leveled = Node::::layout(&node).clone(); - assert_eq!(leveled.depth, 1, "the IList return pushed one rank level above the content"); - assert_eq!(node.extent_at(&ctx, 0), GPoll::Final(Extent::Exactly(3))); - - let head = ctx.index_head(); - for copy in 0..3u64 { - let mark = stack::sp(); - let lane = ctx.promoted(&head, copy); - let GPoll::Final(value) = node.eval(&lane) else { - panic!("expected a final record"); - }; - let rec = leveled.rec(&value); - assert_eq!(unsafe { rec.element::() }, 7.); - // Zero angle, direction (10, 0), count 3: copy `j` steps j * (5, 0) - // past the row's own (5, 5) translation. - let composed: DAffine2 = unsafe { rec.read(leveled.offset_of(Transform::NAME, 0).unwrap()) }; - assert_eq!(composed, DAffine2::from_translation(DVec2::new(5. + copy as f64 * 5., 5.))); - // SAFETY: the element and transform were read out above, so no borrow into this lane's frames remains. - unsafe { stack::rewind(mark) }; - } - } - - #[test] - fn repeat_radial_rotates_each_copy_around_the_center() { - let arena = Arena::new(1024).unwrap(); - let generations = []; - let scope = scope_fixture(&generations, &arena); - let ctx = ContextImpl::root(&scope); - - let layout = transform_layout(); - let local = DAffine2::from_translation(DVec2::new(1., 0.)); - let content = TransformSource { - layout: layout.clone(), - element: 7., - transform: local, - }; - - let mut node = RepeatRadialNode::new(RecordSource::new(content, &layout, &layout), ValueNode(90.0f64), ValueNode(2.0f64), ValueNode(4u32), &layout); - Node::::set_layout(&mut node, repeat_radial_layout_meta().resolve(&[Some(&layout)])); - let leveled = Node::::layout(&node).clone(); - assert_eq!(node.extent_at(&ctx, 0), GPoll::Final(Extent::Exactly(4))); - - let head = ctx.index_head(); - for copy in 0..4u64 { - let mark = stack::sp(); - let lane = ctx.promoted(&head, copy); - let GPoll::Final(value) = node.eval(&lane) else { - panic!("expected a final record"); - }; - let rec = leveled.rec(&value); - assert_eq!(unsafe { rec.element::() }, 7.); - // The kernel's own formula, so the float operations match exactly. - let step = DAffine2::from_angle((TAU / 4.) * copy as f64 + 90.0f64.to_radians()) * DAffine2::from_translation(2. * DVec2::Y); - let expected = DAffine2::from_translation(local.translation) * step * DAffine2::from_mat2(local.matrix2); - let composed: DAffine2 = unsafe { rec.read(leveled.offset_of(Transform::NAME, 0).unwrap()) }; - assert_eq!(composed, expected); - // SAFETY: the element and transform were read out above, so no borrow into this lane's frames remains. - unsafe { stack::rewind(mark) }; - } - } - #[test] - fn repeat_on_points_lands_each_copy_on_its_transformed_point() { - let arena = Arena::new(1 << 16).unwrap(); - let generations = []; - let scope = scope_fixture(&generations, &arena); - let ctx = ContextImpl::root(&scope); - - let row0: Vec = vec![DVec2::new(40., 20.), DVec2::ONE]; - let row1: Vec = vec![DVec2::new(-42., 9.), DVec2::new(10., 345.), DVec2::new(3., 4.)]; - let row0_transform = DAffine2::from_translation(DVec2::new(100., 0.)); - let points = VectorRows { - layout: vector_rows_layout(), - rows: vec![ - (Vector::from_subpath(Subpath::from_anchors(row0.clone(), false)), row0_transform), - (Vector::from_subpath(Subpath::from_anchors(row1.clone(), false)), DAffine2::IDENTITY), - ], - }; - let content_layout = transform_layout(); - let content = PositionProbe { layout: content_layout.clone() }; - - let mut node = RepeatOnPointsNode::new(RecordSource::new(content, &content_layout, &content_layout), points, ValueNode(false), &content_layout); - Node::::set_layout(&mut node, repeat_on_points_layout_meta().resolve(&[Some(&content_layout)])); - let leveled = Node::::layout(&node).clone(); - assert_eq!(leveled.depth, 1); - assert_eq!(node.extent_at(&ctx, 0), GPoll::Final(Extent::Exactly(5)), "the pushed level flattens both rows' points"); - - let expected: Vec = row0 - .iter() - .map(|&point| row0_transform.transform_point2(point)) - .chain(row1.iter().copied()) - .collect(); - - let head = ctx.index_head(); - for (flat, &point) in expected.iter().enumerate() { - let mark = stack::sp(); - let lane = ctx.promoted(&head, flat as u64); - let GPoll::Final(value) = node.eval(&lane) else { - panic!("expected a final record"); - }; - let rec = leveled.rec(&value); - // The content saw the pushed position, and the output transform lands on it. - assert_eq!(unsafe { rec.element::() }, point.x); - let composed: DAffine2 = unsafe { rec.read(leveled.offset_of(::NAME, 0).unwrap()) }; - assert_eq!(composed.translation, point); - // SAFETY: the element and transform were read out above, so no borrow into this lane's frames remains. - unsafe { stack::rewind(mark) }; - } - } - - #[test] - fn repeat_on_points_reverse_flips_each_rows_points() { - let arena = Arena::new(1 << 16).unwrap(); - let generations = []; - let scope = scope_fixture(&generations, &arena); - let ctx = ContextImpl::root(&scope); - - let positions: Vec = vec![DVec2::new(40., 20.), DVec2::ONE, DVec2::new(-42., 9.), DVec2::new(10., 345.)]; - let points = VectorRows { - layout: vector_rows_layout(), - rows: vec![(Vector::from_subpath(Subpath::from_anchors(positions.clone(), false)), DAffine2::IDENTITY)], - }; - let content_layout = transform_layout(); - let content = PositionProbe { layout: content_layout.clone() }; - - let mut node = RepeatOnPointsNode::new(RecordSource::new(content, &content_layout, &content_layout), points, ValueNode(true), &content_layout); - Node::::set_layout(&mut node, repeat_on_points_layout_meta().resolve(&[Some(&content_layout)])); - let leveled = Node::::layout(&node).clone(); - - let mut expected = positions.clone(); - expected.reverse(); - let head = ctx.index_head(); - for (flat, &point) in expected.iter().enumerate() { - let mark = stack::sp(); - let lane = ctx.promoted(&head, flat as u64); - let GPoll::Final(value) = node.eval(&lane) else { - panic!("expected a final record"); - }; - let composed: DAffine2 = unsafe { leveled.rec(&value).read(leveled.offset_of(::NAME, 0).unwrap()) }; - assert_eq!(composed.translation, point); - // SAFETY: the transform was read out above, so no borrow into this lane's frames remains. - unsafe { stack::rewind(mark) }; - } - } -} diff --git a/node-graph/nodes/repeat/src/lib.rs b/node-graph/nodes/repeat/src/lib.rs index a6b06d83f2..01d3f7aecc 100644 --- a/node-graph/nodes/repeat/src/lib.rs +++ b/node-graph/nodes/repeat/src/lib.rs @@ -1,4 +1,3 @@ -pub mod leveled; pub mod repeat_nodes; // Re-export for convenience diff --git a/node-graph/nodes/repeat/src/repeat_nodes.rs b/node-graph/nodes/repeat/src/repeat_nodes.rs index a8f4ad95eb..359aa53b04 100644 --- a/node-graph/nodes/repeat/src/repeat_nodes.rs +++ b/node-graph/nodes/repeat/src/repeat_nodes.rs @@ -1,16 +1,12 @@ -use crate::gcore::Context; use core::f64::consts::TAU; -use core_types::context::IndexLink; -use core_types::extent::{ExtentIn, LevelIn, ValueIn}; -use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt}; -use core_types::list::List; -use core_types::registry::types::{Angle, PixelSize}; use core_types::attribute::{Attr, Transform as TransformAttr}; -use core_types::{ATTR_TRANSFORM, Color, Ctx, DeriveCtx, ExtractIndex, InjectVarArgs}; +use core_types::context::IndexLink; +use core_types::extent::{ExtentIn, LevelIn, ListIn, ValueIn}; +use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt}; +use core_types::registry::types::{Angle, PixelSize}; +use core_types::{Ctx, DeriveCtx, ExtractIndex, InjectIndex}; use glam::{DAffine2, DVec2}; -use graphic_types::{Graphic, Vector}; -use raster_types::{CPU, Raster}; -use vector_types::GradientStops; +use graphic_types::Vector; /// Each copy evaluates the content within the copy's index pushed in, /// producing a level of `count` copies. @@ -90,17 +86,12 @@ fn repeat_array_extent(content: ExtentIn<'_>, _direction: ValueIn<'_, DVec2>, _a } } -#[node_macro::node(category("Repeat"))] -fn repeat_radial + Default + Send + Clone + 'static>( - ctx: impl Ctx + DeriveCtx, - #[implementations( - Context -> List, - Context -> List, - Context -> List>, - Context -> List, - Context -> List, - )] - content: impl Node, Output = List>, +/// Each copy evaluates the content within the copy's index pushed in, rotated +/// around the center by the copy's share of the turn. +#[node_macro::node(category("Repeat"), extent(repeat_radial_extent))] +fn repeat_radial( + ctx: impl Ctx + DeriveCtx + ExtractIndex, + content: impl Node, Output = (T, Attr)>, start_angle: Angle, #[unit(" px")] #[default(5)] @@ -108,94 +99,94 @@ fn repeat_radial + Default + Send + Clone + 'static>( #[default(5)] #[hard(1..)] count: u32, -) -> Result, Interrupt> { - let spilled = ctx.index_head(); - 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 mark = core_types::record::stack::sp(); - let generated_content = content.eval(&ctx.promoted(&spilled, index as u64))?; - - for row_index in 0..generated_content.len() { - let Some(mut row) = generated_content.clone_item(row_index) else { continue }; - - let local_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); - let local_translation = DAffine2::from_translation(local_transform.translation); - let local_matrix = DAffine2::from_mat2(local_transform.matrix2); - *row.attribute_mut_or_insert_default(ATTR_TRANSFORM) = local_translation * transform * local_matrix; - - result_list.push(row); - } - // SAFETY: rows are cloned into result_list and generated_content is owned, so no record borrow into this iteration's frames remains. - unsafe { core_types::record::stack::rewind(mark) }; +) -> Result)>, Interrupt> { + let inner = content.inner_extent(ctx)?; + let (copy, rest) = ctx.split_innermost(inner); + if copy >= count as u64 { + return Err(GraphError::past_end().into()); } + let mut frame = IndexLink { index: 0, outer: None }; + let (element, local) = content.eval(&ctx.push_level(&mut frame, copy, rest))?; - Ok(result_list) + let angle = DAffine2::from_angle((TAU / count as f64) * copy as f64 + start_angle.to_radians()); + let translation = DAffine2::from_translation(radius * DVec2::Y); + let step = angle * translation; + let local_translation = DAffine2::from_translation(local.translation); + let local_matrix = DAffine2::from_mat2(local.matrix2); + Ok((element, Attr(local_translation * step * local_matrix))) } -#[node_macro::node(category("Repeat"), name("Repeat on Points"))] -fn repeat_on_points + Default + Send + Clone + 'static>( - ctx: impl Ctx + DeriveCtx + InjectVarArgs, - points: List, - #[implementations( - Context -> List, - Context -> List, - Context -> List>, - Context -> List, - Context -> List, - )] - content: impl Node, Output = List>, - reverse: bool, -) -> Result, Interrupt> { - let spilled = ctx.index_head(); - 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: DAffine2 = points.attribute_cloned_or_default(ATTR_TRANSFORM, points_index); - - let positions = points_element.point_domain.positions(); - let range: Box> = match reverse { - true => Box::new(positions.iter().enumerate().rev()), - false => Box::new(positions.iter().enumerate()), - }; - - for (index, &point) in range { - let transformed_point = transform.transform_point2(point); - - let scoped = ctx.push_position(transformed_point); - let mark = core_types::record::stack::sp(); - let generated_content = content.eval(&scoped.ctx().promoted(&spilled, index as u64))?; - - for mut generated_row in generated_content.into_iter() { - generated_row.attribute_mut_or_insert_default::(ATTR_TRANSFORM).translation = transformed_point; - result_list.push(generated_row); - } - // SAFETY: generated_content is an owned list fully moved into result_list, so no record borrow into this iteration's frames remains. - unsafe { core_types::record::stack::rewind(mark) }; - } +/// The pushed level's extent is the copy count; inner levels forward to the +/// content, whose extent is taken uniform across copies (queried at copy 0). +fn repeat_radial_extent(content: ExtentIn<'_>, _start_angle: ValueIn<'_, Angle>, _radius: ValueIn<'_, f64>, count: ValueIn<'_, u32>, level: LevelIn) -> GPoll { + match level.pushed() { + true => count.get().map(|count| Extent::Exactly(count as usize)), + false => content.at(level), } +} - Ok(result_list) +/// The pushed level flattens every point of every points row, mirroring the +/// legacy iteration order (rows in order, a row's points reversed when +/// `reverse` is set); each copy evaluates the content with its point's +/// transformed position pushed, then lands the content row's transform on +/// that position. +#[node_macro::node(category("Repeat"), name("Repeat on Points"), extent(repeat_on_points_extent))] +fn repeat_on_points( + ctx: impl Ctx + DeriveCtx + ExtractIndex + InjectIndex + Copy, + content: impl Node, Output = (T, Attr)>, + points: IList, + reverse: bool, +) -> Result)>, Interrupt> { + let inner = content.inner_extent(ctx)?; + let (copy, rest) = ctx.split_innermost(inner); + + let mut remaining = copy as usize; + for row_index in 0..points.len() { + let vector = points.element_ref(row_index); + let positions = vector.point_domain.positions(); + if remaining >= positions.len() { + remaining -= positions.len(); + continue; + } + let index = match reverse { + true => positions.len() - 1 - remaining, + false => remaining, + }; + let transform: DAffine2 = points.lane(row_index).attr::(); + let transformed_point = transform.transform_point2(positions[index]); + + let scoped = ctx.push_position(transformed_point); + let mut frame = IndexLink { index: 0, outer: None }; + let (element, local) = content.eval(&scoped.ctx().push_level(&mut frame, index as u64, rest))?; + let mut composed = *local; + composed.translation = transformed_point; + return Ok((element, Attr(composed))); + } + Err(GraphError::past_end().into()) +} + +/// The pushed level's extent is the flattened point count across the points +/// rows; inner levels forward to the content, uniform across copies. +fn repeat_on_points_extent(content: ExtentIn<'_>, points: ListIn<'_, Vector>, _reverse: ValueIn<'_, bool>, level: LevelIn) -> GPoll { + match level.pushed() { + true => points + .get() + .map(|points| Extent::Exactly((0..points.len()).map(|row| points.element_ref(row).point_domain.positions().len()).sum())), + false => content.at(level), + } } #[cfg(test)] mod test { use super::*; + use core_types::SourceId; use core_types::arena::Arena; - use core_types::context::{ContextImpl, EvalScope, ExtractPosition}; - use core_types::gpoll::GPoll; - use core_types::list::Item; - use core_types::node::{Node, StatusCell}; - use core_types::record::{ElementLazyInput, RecordLift}; + use core_types::attribute::Attribute as AttributeMarker; + use core_types::context::{ContextImpl, EvalScope}; + use core_types::node::Node; + use core_types::record::{FieldWrite, Layout, Rec, RecordSource, RecordValue, element_write, stack}; use vector_types::subpath::Subpath; - const TEST_POSITION: &str = "test-position"; - struct ValueNode(T); impl Node for ValueNode { @@ -206,72 +197,256 @@ mod test { } } - /// Returns one default `Vector` recording the innermost context position under `TEST_POSITION`. - struct PositionProbe; + struct TransformSource { + layout: Layout, + element: f64, + transform: DAffine2, + } - impl Node for PositionProbe { - type Output = List; + impl<'e> Node> for TransformSource { + type Output = RecordValue<'e>; - 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 eval(&self, _input: &ContextImpl<'e>) -> GPoll> { + let dst = stack::push(self.layout.frame_bytes()); + unsafe { + dst.cast::().write(self.element); + dst.add(self.layout.offset_of(TransformAttr::NAME, 0).unwrap()).cast::().write(self.transform); + } + stack::pop(dst); + GPoll::Final(RecordValue::spilled(unsafe { Rec::new(dst.cast_const()) })) } } - fn single_default_vector() -> List { - List::new_from_element(Vector::default()) + fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> { + stack::reserve(1 << 12); + EvalScope::new(Some(0.5), None, None, generations, arena) } - fn row_translations(list: &List, key: &str) -> Vec { - (0..list.len()).map(|index| list.attribute_cloned_or_default::(key, index).translation).collect() + struct VectorRows { + layout: Layout, + rows: Vec<(Vector, DAffine2)>, } - macro_rules! test_ctx { - ($ctx:ident, $cell:ident) => { - core_types::record::stack::reserve(1 << 16); - 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(); + impl<'e> Node> for VectorRows { + type Output = RecordValue<'e>; + + fn eval(&self, input: &ContextImpl<'e>) -> GPoll> { + use core_types::context::{ExtractArena, ExtractIndex}; + let (vector, transform) = &self.rows[input.innermost_index() as usize % self.rows.len()]; + let dst = stack::push(self.layout.frame_bytes()); + // SAFETY: dst is the claimed frame of this layout; offsets are the layout's own. + unsafe { + if core_types::record::write_element(dst, vector.clone(), input.arena()).is_none() { + return GPoll::Error(Box::new(core_types::gpoll::GraphError::new("arena exhausted"))); + } + core_types::record::write_field(dst, self.layout.offset_of(::NAME, 0).unwrap(), *transform); + } + stack::pop(dst); + GPoll::Final(RecordValue::spilled(unsafe { Rec::new(dst.cast_const()) })) + } + + fn extent_at(&self, _input: &ContextImpl<'e>, _level: u8) -> GPoll { + GPoll::Final(Extent::Exactly(self.rows.len())) + } + + fn layout(&self) -> &Layout { + &self.layout + } + } + + fn vector_rows_layout() -> Layout { + Layout::default().with_writes(1, element_write::(), &[FieldWrite::of::(0)]) + } + + struct PositionProbe { + layout: Layout, + } + + impl<'e> Node> for PositionProbe { + type Output = RecordValue<'e>; + + fn eval(&self, input: &ContextImpl<'e>) -> GPoll> { + use core_types::context::ExtractPosition; + let position = input.try_position().and_then(|mut positions| positions.next()).unwrap_or(DVec2::ZERO); + let dst = stack::push(self.layout.frame_bytes()); + // SAFETY: dst is the claimed frame of this layout; offsets are the layout's own. + unsafe { + dst.cast::().write(position.x); + core_types::record::write_field(dst, self.layout.offset_of(::NAME, 0).unwrap(), DAffine2::IDENTITY); + } + stack::pop(dst); + GPoll::Final(RecordValue::spilled(unsafe { Rec::new(dst.cast_const()) })) + } + + fn layout(&self) -> &Layout { + &self.layout + } + } + + fn transform_layout() -> Layout { + Layout::default().with_writes(0, element_write::(), &[FieldWrite::of::(0)]) + } + + #[test] + fn repeat_array_composes_the_step_onto_each_copys_transform() { + let arena = Arena::new(1024).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let layout = transform_layout(); + let content = TransformSource { + layout: layout.clone(), + element: 7., + transform: DAffine2::from_translation(DVec2::new(5., 5.)), }; - } - #[test] - fn repeat_radial_rotates_copies_around_the_center() { - test_ctx!(ctx, cell); - let (radius, count) = (5., 4); + let mut node = RepeatArrayNode::new( + RecordSource::new(content, &layout, &layout), + ValueNode(DVec2::new(10., 0.)), + ValueNode(0.0f64), + ValueNode(3u32), + &layout, + ); + Node::::set_layout(&mut node, repeat_array_layout_meta().resolve(&[Some(&layout)])); + let leveled = Node::::layout(&node).clone(); + assert_eq!(leveled.depth, 1, "the IList return pushed one rank level above the content"); + assert_eq!(node.extent_at(&ctx, 0), GPoll::Final(Extent::Exactly(3))); - let lift = RecordLift::, _>::new(ValueNode(single_default_vector())); - let layout = Node::::layout(&lift).clone(); - let repeated = super::repeat_radial(&ctx, ElementLazyInput::, _>::new(&lift, &cell, 0, &layout), 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:?}"); + let head = ctx.index_head(); + for copy in 0..3u64 { + let mark = stack::sp(); + let lane = ctx.promoted(&head, copy); + let GPoll::Final(value) = node.eval(&lane) else { + panic!("expected a final record"); + }; + let rec = leveled.rec(&value); + assert_eq!(unsafe { rec.element::() }, 7.); + // Zero angle, direction (10, 0), count 3: copy `j` steps j * (5, 0) + // past the row's own (5, 5) translation. + let composed: DAffine2 = unsafe { rec.read(leveled.offset_of(TransformAttr::NAME, 0).unwrap()) }; + assert_eq!(composed, DAffine2::from_translation(DVec2::new(5. + copy as f64 * 5., 5.))); + // SAFETY: the element and transform were read out above, so no borrow into this lane's frames remains. + unsafe { stack::rewind(mark) }; } } #[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))); + fn repeat_radial_rotates_each_copy_around_the_center() { + let arena = Arena::new(1024).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); - let lift = RecordLift::, _>::new(PositionProbe); - let layout = Node::::layout(&lift).clone(); + let layout = transform_layout(); + let local = DAffine2::from_translation(DVec2::new(1., 0.)); + let content = TransformSource { + layout: layout.clone(), + element: 7., + transform: local, + }; - let generated = super::repeat_on_points(&ctx, points.clone(), ElementLazyInput::new(&lift, &cell, 0, &layout), false).unwrap(); - assert_eq!(row_translations(&generated, ATTR_TRANSFORM), positions.to_vec()); - assert_eq!(row_translations(&generated, TEST_POSITION), positions.to_vec()); + let mut node = RepeatRadialNode::new(RecordSource::new(content, &layout, &layout), ValueNode(90.0f64), ValueNode(2.0f64), ValueNode(4u32), &layout); + Node::::set_layout(&mut node, repeat_radial_layout_meta().resolve(&[Some(&layout)])); + let leveled = Node::::layout(&node).clone(); + assert_eq!(node.extent_at(&ctx, 0), GPoll::Final(Extent::Exactly(4))); - let reversed = super::repeat_on_points(&ctx, points, ElementLazyInput::new(&lift, &cell, 0, &layout), true).unwrap(); - let mut expected = positions.to_vec(); + let head = ctx.index_head(); + for copy in 0..4u64 { + let mark = stack::sp(); + let lane = ctx.promoted(&head, copy); + let GPoll::Final(value) = node.eval(&lane) else { + panic!("expected a final record"); + }; + let rec = leveled.rec(&value); + assert_eq!(unsafe { rec.element::() }, 7.); + // The kernel's own formula, so the float operations match exactly. + let step = DAffine2::from_angle((TAU / 4.) * copy as f64 + 90.0f64.to_radians()) * DAffine2::from_translation(2. * DVec2::Y); + let expected = DAffine2::from_translation(local.translation) * step * DAffine2::from_mat2(local.matrix2); + let composed: DAffine2 = unsafe { rec.read(leveled.offset_of(TransformAttr::NAME, 0).unwrap()) }; + assert_eq!(composed, expected); + // SAFETY: the element and transform were read out above, so no borrow into this lane's frames remains. + unsafe { stack::rewind(mark) }; + } + } + + #[test] + fn repeat_on_points_lands_each_copy_on_its_transformed_point() { + let arena = Arena::new(1 << 16).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let row0: Vec = vec![DVec2::new(40., 20.), DVec2::ONE]; + let row1: Vec = vec![DVec2::new(-42., 9.), DVec2::new(10., 345.), DVec2::new(3., 4.)]; + let row0_transform = DAffine2::from_translation(DVec2::new(100., 0.)); + let points = VectorRows { + layout: vector_rows_layout(), + rows: vec![ + (Vector::from_subpath(Subpath::from_anchors(row0.clone(), false)), row0_transform), + (Vector::from_subpath(Subpath::from_anchors(row1.clone(), false)), DAffine2::IDENTITY), + ], + }; + let content_layout = transform_layout(); + let content = PositionProbe { layout: content_layout.clone() }; + + let mut node = RepeatOnPointsNode::new(RecordSource::new(content, &content_layout, &content_layout), points, ValueNode(false), &content_layout); + Node::::set_layout(&mut node, repeat_on_points_layout_meta().resolve(&[Some(&content_layout)])); + let leveled = Node::::layout(&node).clone(); + assert_eq!(leveled.depth, 1); + assert_eq!(node.extent_at(&ctx, 0), GPoll::Final(Extent::Exactly(5)), "the pushed level flattens both rows' points"); + + let expected: Vec = row0.iter().map(|&point| row0_transform.transform_point2(point)).chain(row1.iter().copied()).collect(); + + let head = ctx.index_head(); + for (flat, &point) in expected.iter().enumerate() { + let mark = stack::sp(); + let lane = ctx.promoted(&head, flat as u64); + let GPoll::Final(value) = node.eval(&lane) else { + panic!("expected a final record"); + }; + let rec = leveled.rec(&value); + // The content saw the pushed position, and the output transform lands on it. + assert_eq!(unsafe { rec.element::() }, point.x); + let composed: DAffine2 = unsafe { rec.read(leveled.offset_of(::NAME, 0).unwrap()) }; + assert_eq!(composed.translation, point); + // SAFETY: the element and transform were read out above, so no borrow into this lane's frames remains. + unsafe { stack::rewind(mark) }; + } + } + + #[test] + fn repeat_on_points_reverse_flips_each_rows_points() { + let arena = Arena::new(1 << 16).unwrap(); + let generations = []; + let scope = scope_fixture(&generations, &arena); + let ctx = ContextImpl::root(&scope); + + let positions: Vec = vec![DVec2::new(40., 20.), DVec2::ONE, DVec2::new(-42., 9.), DVec2::new(10., 345.)]; + let points = VectorRows { + layout: vector_rows_layout(), + rows: vec![(Vector::from_subpath(Subpath::from_anchors(positions.clone(), false)), DAffine2::IDENTITY)], + }; + let content_layout = transform_layout(); + let content = PositionProbe { layout: content_layout.clone() }; + + let mut node = RepeatOnPointsNode::new(RecordSource::new(content, &content_layout, &content_layout), points, ValueNode(true), &content_layout); + Node::::set_layout(&mut node, repeat_on_points_layout_meta().resolve(&[Some(&content_layout)])); + let leveled = Node::::layout(&node).clone(); + + let mut expected = positions.clone(); expected.reverse(); - assert_eq!(row_translations(&reversed, ATTR_TRANSFORM), expected); + let head = ctx.index_head(); + for (flat, &point) in expected.iter().enumerate() { + let mark = stack::sp(); + let lane = ctx.promoted(&head, flat as u64); + let GPoll::Final(value) = node.eval(&lane) else { + panic!("expected a final record"); + }; + let composed: DAffine2 = unsafe { leveled.rec(&value).read(leveled.offset_of(::NAME, 0).unwrap()) }; + assert_eq!(composed.translation, point); + // SAFETY: the transform was read out above, so no borrow into this lane's frames remains. + unsafe { stack::rewind(mark) }; + } } }