mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Swap the leveled repeat_radial and repeat_on_points in for the eager variants
This commit is contained in:
@@ -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<T>(
|
||||
ctx: impl Ctx + DeriveCtx + ExtractIndex,
|
||||
content: impl Node<Context<'_>, Output = (T, Attr<Transform>)>,
|
||||
#[default(100., 100.)] direction: PixelSize,
|
||||
angle: Angle,
|
||||
#[default(5)]
|
||||
#[hard(1..)]
|
||||
count: u32,
|
||||
) -> Result<IList<(T, Attr<Transform>)>, 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<Extent> {
|
||||
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<T>(
|
||||
ctx: impl Ctx + DeriveCtx + ExtractIndex,
|
||||
content: impl Node<Context<'_>, Output = (T, Attr<Transform>)>,
|
||||
start_angle: Angle,
|
||||
#[unit(" px")]
|
||||
#[default(5)]
|
||||
radius: f64,
|
||||
#[default(5)]
|
||||
#[hard(1..)]
|
||||
count: u32,
|
||||
) -> Result<IList<(T, Attr<Transform>)>, 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<Extent> {
|
||||
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<T>(
|
||||
ctx: impl Ctx + DeriveCtx + ExtractIndex + InjectIndex + Copy,
|
||||
content: impl Node<Context<'_>, Output = (T, Attr<Transform>)>,
|
||||
points: IList<Vector>,
|
||||
reverse: bool,
|
||||
) -> Result<IList<(T, Attr<Transform>)>, 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::<Transform>();
|
||||
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<Extent> {
|
||||
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>(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())
|
||||
}
|
||||
}
|
||||
|
||||
struct TransformSource {
|
||||
layout: Layout,
|
||||
element: f64,
|
||||
transform: DAffine2,
|
||||
}
|
||||
|
||||
impl<'e> Node<ContextImpl<'e>> for TransformSource {
|
||||
type Output = RecordValue<'e>;
|
||||
|
||||
fn eval(&self, _input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
|
||||
let dst = stack::push(self.layout.frame_bytes());
|
||||
unsafe {
|
||||
dst.cast::<f64>().write(self.element);
|
||||
dst.add(self.layout.offset_of(Transform::NAME, 0).unwrap()).cast::<DAffine2>().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<ContextImpl<'e>> for VectorRows {
|
||||
type Output = RecordValue<'e>;
|
||||
|
||||
fn eval(&self, input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
|
||||
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(<Transform as AttributeMarker>::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<Extent> {
|
||||
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::<Vector>(), &[FieldWrite::of::<Transform>(0)])
|
||||
}
|
||||
|
||||
struct PositionProbe {
|
||||
layout: Layout,
|
||||
}
|
||||
|
||||
impl<'e> Node<ContextImpl<'e>> for PositionProbe {
|
||||
type Output = RecordValue<'e>;
|
||||
|
||||
fn eval(&self, input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
|
||||
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::<f64>().write(position.x);
|
||||
core_types::record::write_field(dst, self.layout.offset_of(<Transform as AttributeMarker>::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::<f64>(), &[FieldWrite::of::<Transform>(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::<ContextImpl>::set_layout(&mut node, repeat_array_layout_meta().resolve(&[Some(&layout)]));
|
||||
let leveled = Node::<ContextImpl>::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::<f64>() }, 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::<ContextImpl>::set_layout(&mut node, repeat_radial_layout_meta().resolve(&[Some(&layout)]));
|
||||
let leveled = Node::<ContextImpl>::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::<f64>() }, 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<DVec2> = vec![DVec2::new(40., 20.), DVec2::ONE];
|
||||
let row1: Vec<DVec2> = 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::<ContextImpl>::set_layout(&mut node, repeat_on_points_layout_meta().resolve(&[Some(&content_layout)]));
|
||||
let leveled = Node::<ContextImpl>::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<DVec2> = 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::<f64>() }, point.x);
|
||||
let composed: DAffine2 = unsafe { rec.read(leveled.offset_of(<Transform as AttributeMarker>::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<DVec2> = 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::<ContextImpl>::set_layout(&mut node, repeat_on_points_layout_meta().resolve(&[Some(&content_layout)]));
|
||||
let leveled = Node::<ContextImpl>::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(<Transform as AttributeMarker>::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) };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod leveled;
|
||||
pub mod repeat_nodes;
|
||||
|
||||
// Re-export for convenience
|
||||
|
||||
@@ -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<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, rotated
|
||||
/// around the center by the copy's share of the turn.
|
||||
#[node_macro::node(category("Repeat"), extent(repeat_radial_extent))]
|
||||
fn repeat_radial<T>(
|
||||
ctx: impl Ctx + DeriveCtx + ExtractIndex,
|
||||
content: impl Node<Context<'_>, Output = (T, Attr<TransformAttr>)>,
|
||||
start_angle: Angle,
|
||||
#[unit(" px")]
|
||||
#[default(5)]
|
||||
@@ -108,94 +99,94 @@ fn repeat_radial<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
#[default(5)]
|
||||
#[hard(1..)]
|
||||
count: u32,
|
||||
) -> Result<List<T>, 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<IList<(T, Attr<TransformAttr>)>, 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<T: Into<Graphic> + Default + Send + Clone + 'static>(
|
||||
ctx: impl Ctx + DeriveCtx + InjectVarArgs,
|
||||
points: List<Vector>,
|
||||
#[implementations(
|
||||
Context -> List<Graphic>,
|
||||
Context -> List<Vector>,
|
||||
Context -> List<Raster<CPU>>,
|
||||
Context -> List<Color>,
|
||||
Context -> List<GradientStops>,
|
||||
)]
|
||||
content: impl Node<Context<'_>, Output = List<T>>,
|
||||
reverse: bool,
|
||||
) -> Result<List<T>, 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<dyn Iterator<Item = (usize, &DVec2)>> = 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::<DAffine2>(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<Extent> {
|
||||
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<T>(
|
||||
ctx: impl Ctx + DeriveCtx + ExtractIndex + InjectIndex + Copy,
|
||||
content: impl Node<Context<'_>, Output = (T, Attr<TransformAttr>)>,
|
||||
points: IList<Vector>,
|
||||
reverse: bool,
|
||||
) -> Result<IList<(T, Attr<TransformAttr>)>, 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::<TransformAttr>();
|
||||
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<Extent> {
|
||||
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>(T);
|
||||
|
||||
impl<T: Clone, Input> Node<Input> for ValueNode<T> {
|
||||
@@ -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<Input: ExtractPosition> Node<Input> for PositionProbe {
|
||||
type Output = List<Vector>;
|
||||
impl<'e> Node<ContextImpl<'e>> for TransformSource {
|
||||
type Output = RecordValue<'e>;
|
||||
|
||||
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 eval(&self, _input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
|
||||
let dst = stack::push(self.layout.frame_bytes());
|
||||
unsafe {
|
||||
dst.cast::<f64>().write(self.element);
|
||||
dst.add(self.layout.offset_of(TransformAttr::NAME, 0).unwrap()).cast::<DAffine2>().write(self.transform);
|
||||
}
|
||||
stack::pop(dst);
|
||||
GPoll::Final(RecordValue::spilled(unsafe { Rec::new(dst.cast_const()) }))
|
||||
}
|
||||
}
|
||||
|
||||
fn single_default_vector() -> List<Vector> {
|
||||
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<Vector>, key: &str) -> Vec<DVec2> {
|
||||
(0..list.len()).map(|index| list.attribute_cloned_or_default::<DAffine2>(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<ContextImpl<'e>> for VectorRows {
|
||||
type Output = RecordValue<'e>;
|
||||
|
||||
fn eval(&self, input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
|
||||
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(<TransformAttr as AttributeMarker>::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<Extent> {
|
||||
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::<Vector>(), &[FieldWrite::of::<TransformAttr>(0)])
|
||||
}
|
||||
|
||||
struct PositionProbe {
|
||||
layout: Layout,
|
||||
}
|
||||
|
||||
impl<'e> Node<ContextImpl<'e>> for PositionProbe {
|
||||
type Output = RecordValue<'e>;
|
||||
|
||||
fn eval(&self, input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
|
||||
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::<f64>().write(position.x);
|
||||
core_types::record::write_field(dst, self.layout.offset_of(<TransformAttr as AttributeMarker>::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::<f64>(), &[FieldWrite::of::<TransformAttr>(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::<ContextImpl>::set_layout(&mut node, repeat_array_layout_meta().resolve(&[Some(&layout)]));
|
||||
let leveled = Node::<ContextImpl>::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::<List<Vector>, _>::new(ValueNode(single_default_vector()));
|
||||
let layout = Node::<ContextImpl>::layout(&lift).clone();
|
||||
let repeated = super::repeat_radial(&ctx, ElementLazyInput::<List<Vector>, _>::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::<f64>() }, 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::<List<Vector>, _>::new(PositionProbe);
|
||||
let layout = Node::<ContextImpl>::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::<ContextImpl>::set_layout(&mut node, repeat_radial_layout_meta().resolve(&[Some(&layout)]));
|
||||
let leveled = Node::<ContextImpl>::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::<f64>() }, 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<DVec2> = vec![DVec2::new(40., 20.), DVec2::ONE];
|
||||
let row1: Vec<DVec2> = 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::<ContextImpl>::set_layout(&mut node, repeat_on_points_layout_meta().resolve(&[Some(&content_layout)]));
|
||||
let leveled = Node::<ContextImpl>::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<DVec2> = 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::<f64>() }, point.x);
|
||||
let composed: DAffine2 = unsafe { rec.read(leveled.offset_of(<TransformAttr as AttributeMarker>::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<DVec2> = 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::<ContextImpl>::set_layout(&mut node, repeat_on_points_layout_meta().resolve(&[Some(&content_layout)]));
|
||||
let leveled = Node::<ContextImpl>::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(<TransformAttr as AttributeMarker>::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) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user