Push the repeat array copies onto a derived level

This commit is contained in:
Dennis Kobert
2026-08-22 17:29:51 +00:00
parent b4346fd47d
commit 4e8a44650e

View File

@@ -5,6 +5,7 @@ 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 glam::{DAffine2, DVec2};
use graphic_types::{Graphic, Vector};
@@ -45,17 +46,13 @@ fn repeat_extent(content: ExtentIn<'_>, count: ValueIn<'_, u32>, _reverse: Value
}
}
#[node_macro::node(category("Repeat"))]
pub fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl Ctx + DeriveCtx,
#[implementations(
Context -> List<Graphic>,
Context -> List<Vector>,
Context -> List<Raster<CPU>>,
Context -> List<Color>,
Context -> List<GradientStops>,
)]
content: impl Node<Context<'_>, Output = List<T>>,
/// Each copy evaluates the content within the copy's index pushed in, the
/// copy's step transform composed between the lane transform's translation
/// and matrix parts.
#[node_macro::node(category("Repeat"), extent(repeat_array_extent))]
pub fn repeat_array<T>(
ctx: impl Ctx + DeriveCtx + ExtractIndex,
content: impl Node<Context<'_>, Output = (T, Attr<TransformAttr>)>,
#[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: PixelSize,
@@ -63,37 +60,34 @@ pub fn repeat_array<T: Into<Graphic> + Default + Send + Clone + 'static>(
#[default(5)]
#[hard(1..)]
count: u32,
) -> Result<List<T>, Interrupt> {
) -> Result<IList<(T, Attr<TransformAttr>)>, Interrupt> {
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 spilled = ctx.index_head();
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 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) };
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 step_angle = copy as f64 * angle / total;
let translation = copy as f64 * direction / total;
let transform = DAffine2::from_angle(step_angle) * DAffine2::from_translation(translation);
Ok(result_list)
let mut frame = IndexLink { index: 0, outer: None };
let (element, local_transform) = content.eval(&ctx.push_level(&mut frame, copy, rest))?;
let local_translation = DAffine2::from_translation(local_transform.translation);
let local_matrix = DAffine2::from_mat2(local_transform.matrix2);
Ok((element, Attr(local_translation * transform * 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<'_, DVec2>, _angle: ValueIn<'_, f64>, count: ValueIn<'_, u32>, level: LevelIn) -> GPoll<Extent> {
match level.pushed() {
true => count.get().map(|count| Extent::Exactly(count as usize)),
false => content.at(level),
}
}
#[node_macro::node(category("Repeat"))]
@@ -259,36 +253,6 @@ mod test {
};
}
#[test]
fn repeat_array_spaces_copies_along_the_direction() {
test_ctx!(ctx, cell);
let direction = DVec2::new(1.5, 0.);
let count = 3;
let lift = RecordLift::<List<Vector>, _>::new(ValueNode(single_default_vector()));
let layout = Node::<ContextImpl>::layout(&lift).clone();
let repeated = super::repeat_array(&ctx, ElementLazyInput::new(&lift, &cell, 0, &layout), 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 lift = RecordLift::<List<Vector>, _>::new(ValueNode(single_default_vector()));
let layout = Node::<ContextImpl>::layout(&lift).clone();
let repeated = super::repeat_array(&ctx, ElementLazyInput::<List<Vector>, _>::new(&lift, &cell, 0, &layout), 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);