mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
New nodes: Sum, Average, Minimum, Maximum, Any, All (#4344)
Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
committed by
Dennis Kobert
parent
04d6c0d5cf
commit
20eb5ccb8a
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -2095,12 +2095,17 @@ dependencies = [
|
||||
"dyn-any",
|
||||
"glam",
|
||||
"graphene-hash",
|
||||
"graphic-nodes",
|
||||
"graphic-types",
|
||||
"log",
|
||||
"math-nodes",
|
||||
"node-macro",
|
||||
"rand",
|
||||
"raster-types",
|
||||
"repeat-nodes",
|
||||
"serde",
|
||||
"tsify",
|
||||
"vector-types",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
@@ -2180,6 +2185,7 @@ dependencies = [
|
||||
"core-types",
|
||||
"dyn-any",
|
||||
"glam",
|
||||
"graphene-core",
|
||||
"graphic-types",
|
||||
"node-macro",
|
||||
"raster-types",
|
||||
|
||||
@@ -2758,7 +2758,7 @@ impl DocumentMessageHandler {
|
||||
}
|
||||
|
||||
let solidify_stroke_definition = document_node_definitions::resolve_proto_node_type(graphene_std::vector::solidify_stroke::IDENTIFIER).expect("Solidify Stroke node should exist");
|
||||
let item_at_index_definition = document_node_definitions::resolve_proto_node_type(graphene_std::graphic::item_at_index::IDENTIFIER).expect("Item at Index node should exist");
|
||||
let item_at_index_definition = document_node_definitions::resolve_proto_node_type(graphene_std::list::item_at_index::IDENTIFIER).expect("Item at Index node should exist");
|
||||
|
||||
let mut resulting_layers: Vec<NodeId> = Vec::new();
|
||||
|
||||
@@ -4339,13 +4339,13 @@ mod document_message_handler_tests {
|
||||
// A base that wrongly carried a phantom element would therefore show up as a recorded row, which this catches.
|
||||
// The `news` guard below is what keeps both assertions honest, since a wrong `Output` type empties every record.
|
||||
let base_lengths: Vec<usize> = instrumented
|
||||
.grab_all_input_as::<graphene_std::graphic::extend::BaseInput<graphene_std::Graphic>, graphene_std::list::List<graphene_std::Graphic>>(&editor.runtime)
|
||||
.grab_all_input_as::<graphene_std::list::extend::BaseInput<graphene_std::Graphic>, graphene_std::list::List<graphene_std::Graphic>>(&editor.runtime)
|
||||
.map(|base| base.len())
|
||||
.collect();
|
||||
assert!(base_lengths.iter().all(|&len| len == 0), "Every stack base should be empty, found lengths {base_lengths:?}");
|
||||
|
||||
let news: Vec<graphene_std::list::List<graphene_std::Graphic>> = instrumented
|
||||
.grab_all_input_as::<graphene_std::graphic::extend::NewInput<graphene_std::Graphic>, graphene_std::list::List<graphene_std::Graphic>>(&editor.runtime)
|
||||
.grab_all_input_as::<graphene_std::list::extend::NewInput<graphene_std::Graphic>, graphene_std::list::List<graphene_std::Graphic>>(&editor.runtime)
|
||||
.collect();
|
||||
assert!(!news.is_empty(), "Instrumentation should have recorded at least one stacked element list");
|
||||
let phantom_count = news
|
||||
|
||||
@@ -189,7 +189,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
||||
DocumentNode {
|
||||
call_argument: generic!(T),
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(4), 0)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(list::extend::IDENTIFIER),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
@@ -318,7 +318,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
||||
NodeInput::import(graphene_std::Type::Fn(Box::new(concrete!(Context)), Box::new(concrete!(List<Artboard>))), 0),
|
||||
NodeInput::node(NodeId(3), 0),
|
||||
],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphic::extend::IDENTIFIER),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(list::extend::IDENTIFIER),
|
||||
..Default::default()
|
||||
},
|
||||
// Content coercion into a graphic level, evaluated within the artboard's footprint
|
||||
@@ -836,7 +836,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
||||
},
|
||||
// 5: Map
|
||||
DocumentNode {
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphic::map::IDENTIFIER),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(list::map::IDENTIFIER),
|
||||
inputs: vec![NodeInput::node(NodeId(4), 0), NodeInput::node(NodeId(3), 0)],
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1335,13 +1335,13 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
|
||||
// Node 1: item_at_index at index 0, extracts the whole match as a bare String (drops the item's start/end/name attributes since the unwrapped String can't carry them)
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::value(TaggedValue::F64(0.), false)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphic::item_at_index::IDENTIFIER),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(list::item_at_index::IDENTIFIER),
|
||||
..Default::default()
|
||||
},
|
||||
// Node 2: remove_at_index at index 0, returns the capture group items as a List<String>, preserving each item's start/end/name attributes
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::value(TaggedValue::F64(0.), false)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(graphic::remove_at_index::IDENTIFIER),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(list::remove_at_index::IDENTIFIER),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
|
||||
@@ -104,7 +104,7 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
||||
"graphene_core::transform_nodes::FreezeRealTimeNode",
|
||||
"graphene_core::vector::SubpathSegmentLengthsNode",
|
||||
"core_types::vector::SubpathSegmentLengthsNode",
|
||||
// The deleted debug Option trio degrades to a passthrough of its single input (audit resolution 8)
|
||||
// The deleted debug Option trio degrades to a passthrough of its single input
|
||||
"graphene_core::ops::SizeOfNode",
|
||||
"graphene_core::debug::SizeOfNode",
|
||||
"graphene_core::ops::SomeNode",
|
||||
@@ -139,8 +139,12 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
||||
],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::graphic::extend::IDENTIFIER,
|
||||
aliases: &["graphene_core::graphic::graphic::ExtendNode", "graphene_core::graphic::ExtendNode"],
|
||||
node: graphene_std::list::extend::IDENTIFIER,
|
||||
aliases: &[
|
||||
"graphene_core::graphic::graphic::ExtendNode",
|
||||
"graphene_core::graphic::ExtendNode",
|
||||
"graphic_nodes::graphic::ExtendNode",
|
||||
],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::graphic::flatten_graphic::IDENTIFIER,
|
||||
@@ -155,18 +159,19 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
||||
aliases: &["graphene_core::graphic::FlattenVectorNode", "graphene_core::graphic_element::FlattenVectorNode"],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::graphic::item_at_index::IDENTIFIER,
|
||||
node: graphene_std::list::item_at_index::IDENTIFIER,
|
||||
aliases: &[
|
||||
"graphene_core::graphic_element::IndexNode",
|
||||
"graphene_core::graphic::IndexNode",
|
||||
"graphene_core::graphic::IndexElementsNode",
|
||||
"graphic_nodes::graphic::IndexElementsNode",
|
||||
"graphic_nodes::graphic::ExtractElementNode",
|
||||
"graphic_nodes::graphic::ItemAtIndexNode",
|
||||
],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::graphic::remove_at_index::IDENTIFIER,
|
||||
aliases: &["graphic_nodes::graphic::OmitElementNode"],
|
||||
node: graphene_std::list::remove_at_index::IDENTIFIER,
|
||||
aliases: &["graphic_nodes::graphic::OmitElementNode", "graphic_nodes::graphic::RemoveAtIndexNode"],
|
||||
},
|
||||
// The legacy layer extend no longer exists as a node; the aliases still land on its identifier so the
|
||||
// subgraph rebuild below recognizes and replaces the networks that carried it.
|
||||
@@ -850,8 +855,8 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
||||
},
|
||||
// The string map folded into the general Map, and its reader into the vararg readers.
|
||||
NodeReplacement {
|
||||
node: graphene_std::graphic::map::IDENTIFIER,
|
||||
aliases: &["graphene_core::vector::InstanceMapNode", "text_nodes::MapStringNode"],
|
||||
node: graphene_std::list::map::IDENTIFIER,
|
||||
aliases: &["graphene_core::vector::InstanceMapNode", "text_nodes::MapStringNode", "graphic_nodes::graphic::MapNode"],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::context::read_position::IDENTIFIER,
|
||||
@@ -2800,7 +2805,7 @@ fn migrate_removed_catalog_definitions(node_id: &NodeId, node: &DocumentNode, ne
|
||||
}
|
||||
}
|
||||
|
||||
// The removed Attach Attribute node (merged into Write Attribute per audit resolution 6) degrades to a passthrough of its
|
||||
// The removed Attach Attribute node degrades to a passthrough of its
|
||||
// content: its eager whole-list source input cannot be mechanically rewired as Write Attribute's lazy per-item value producer.
|
||||
if let Some(DefinitionIdentifier::ProtoNode(identifier)) = document.network_interface.reference(node_id, network_path)
|
||||
&& identifier.as_str().ends_with("::AttachAttributeNode")
|
||||
|
||||
@@ -582,7 +582,7 @@ mod test_artboard {
|
||||
Err(e) => panic!("Failed to evaluate graph: {e}"),
|
||||
};
|
||||
let mut artboards = List::new();
|
||||
for list in instrumented.grab_all_input_level::<graphene_std::graphic::extend::NewInput<Artboard>, Artboard>(&editor.runtime) {
|
||||
for list in instrumented.grab_all_input_level::<graphene_std::list::extend::NewInput<Artboard>, Artboard>(&editor.runtime) {
|
||||
for index in 0..list.len() {
|
||||
if let Some(item) = list.clone_item(index) {
|
||||
artboards.push(item);
|
||||
|
||||
@@ -33,4 +33,4 @@ pub use promote::{Promotion, assert_promoted, register_element_promote, register
|
||||
pub use route::{RecordSource, SourcePlan};
|
||||
pub use run::{Group, GroupItem, RunBuilder, RunColumn, RunView, run_to_owned_list};
|
||||
pub use serve::{FrameClaim, MaterializedSpan, Served, SlotRun, serve_input};
|
||||
pub use testkit::{LiftedSource, ServedRecord, capture, test_frames};
|
||||
pub use testkit::{LiftedSource, ServedRecord, capture, fixtures as test_fixtures, test_frames};
|
||||
|
||||
@@ -180,3 +180,330 @@ mod tests {
|
||||
assert_eq!(served.attr::<Transform>(), DAffine2::from_translation(DVec2::new(3., 4.)));
|
||||
}
|
||||
}
|
||||
|
||||
/// The node-test fixtures: hand-wired record sources and layout installers,
|
||||
/// so a node crate's tests drive a generated node without the compiler pass.
|
||||
pub mod fixtures {
|
||||
use super::super::frames::Frames;
|
||||
use super::super::layout::{FieldWrite, Layout, LayoutMeta, RecordLayout, element_write};
|
||||
use super::super::serve::{FrameClaim, Served};
|
||||
use super::test_frames;
|
||||
use crate::SourceId;
|
||||
use crate::arena::Arena;
|
||||
use crate::attribute::{Attribute, Opacity, Transform};
|
||||
use crate::context::{ContextImpl, EvalScope, ExtractArena, ExtractIndex, ExtractIndices};
|
||||
use crate::gpoll::{Extent, GPoll};
|
||||
use crate::node::Node;
|
||||
use crate::value::ValueSource;
|
||||
use glam::DAffine2;
|
||||
|
||||
/// A one-record source with fixed `f64` fields, optionally served partial.
|
||||
pub struct RecordSourceNode<E> {
|
||||
pub layout: Layout,
|
||||
pub element: E,
|
||||
pub fields: Vec<(&'static str, f64)>,
|
||||
pub partial: bool,
|
||||
}
|
||||
|
||||
impl<C, E: Copy + Send + Sync + dyn_any::StaticTypeSized + 'static> Node<C> for RecordSourceNode<E> {
|
||||
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'e Arena>,
|
||||
{
|
||||
let mut frame = slot;
|
||||
let arena = ExtractArena::arena(input);
|
||||
if frame.element(self.element, arena).is_none() {
|
||||
return GPoll::arena_exhausted();
|
||||
}
|
||||
for (name, field) in &self.fields {
|
||||
write_field_at(&mut frame, &self.layout, name, 0, *field);
|
||||
}
|
||||
// SAFETY: the writes above complete the record of this layout.
|
||||
let served = unsafe { frame.finish_served() };
|
||||
match self.partial {
|
||||
true => GPoll::Partial(served),
|
||||
false => GPoll::Final(served),
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(&self) -> &Layout {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
/// A level of `f64` lanes, each optionally carrying one fixed field.
|
||||
pub struct LeveledSourceNode {
|
||||
pub layout: Layout,
|
||||
pub elements: Vec<f64>,
|
||||
pub field: Option<(&'static str, f64)>,
|
||||
}
|
||||
|
||||
impl<C: ExtractIndex> Node<C> for LeveledSourceNode {
|
||||
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'e Arena>,
|
||||
{
|
||||
let element = self.elements[input.innermost_index() as usize % self.elements.len()];
|
||||
let mut frame = slot;
|
||||
let arena = ExtractArena::arena(input);
|
||||
if frame.element(element, arena).is_none() {
|
||||
return GPoll::arena_exhausted();
|
||||
}
|
||||
if let Some((name, value)) = self.field {
|
||||
write_field_at(&mut frame, &self.layout, name, 0, value);
|
||||
}
|
||||
// SAFETY: the writes above complete the record of this layout.
|
||||
GPoll::Final(unsafe { frame.finish_served() })
|
||||
}
|
||||
|
||||
fn extent_at<'x>(&self, _input: &C, _level: u8, _frames: &Frames<'x>) -> GPoll<Extent>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'x Arena>,
|
||||
{
|
||||
GPoll::Final(Extent::Exactly(self.elements.len()))
|
||||
}
|
||||
|
||||
fn layout(&self) -> &Layout {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
/// A level of `f64` lanes each carrying a transform.
|
||||
pub struct LeveledTransformSource {
|
||||
pub layout: Layout,
|
||||
pub rows: Vec<(f64, DAffine2)>,
|
||||
}
|
||||
|
||||
impl<C: ExtractIndex> Node<C> for LeveledTransformSource {
|
||||
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'e Arena>,
|
||||
{
|
||||
let (element, transform) = self.rows[input.innermost_index() as usize % self.rows.len()];
|
||||
let mut frame = slot;
|
||||
let arena = ExtractArena::arena(input);
|
||||
if frame.element(element, arena).is_none() {
|
||||
return GPoll::arena_exhausted();
|
||||
}
|
||||
write_attr_at::<Transform>(&mut frame, &self.layout, transform);
|
||||
// SAFETY: the writes above complete the record of this layout.
|
||||
GPoll::Final(unsafe { frame.finish_served() })
|
||||
}
|
||||
|
||||
fn extent_at<'x>(&self, _input: &C, _level: u8, _frames: &Frames<'x>) -> GPoll<Extent>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'x Arena>,
|
||||
{
|
||||
GPoll::Final(Extent::Exactly(self.rows.len()))
|
||||
}
|
||||
|
||||
fn layout(&self) -> &Layout {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
/// Serves lanes carrying both a Transform no gather kernel declares and an
|
||||
/// Opacity one does, so a carried column can be told apart from a written one.
|
||||
pub struct LeveledCarriedSource {
|
||||
pub layout: Layout,
|
||||
pub rows: Vec<(f64, DAffine2, f64)>,
|
||||
}
|
||||
|
||||
impl<C: ExtractIndex> Node<C> for LeveledCarriedSource {
|
||||
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'e Arena>,
|
||||
{
|
||||
let (element, transform, opacity) = self.rows[input.innermost_index() as usize % self.rows.len()];
|
||||
let mut frame = slot;
|
||||
let arena = ExtractArena::arena(input);
|
||||
if frame.element(element, arena).is_none() {
|
||||
return GPoll::arena_exhausted();
|
||||
}
|
||||
write_attr_at::<Transform>(&mut frame, &self.layout, transform);
|
||||
write_attr_at::<Opacity>(&mut frame, &self.layout, opacity);
|
||||
// SAFETY: the writes above complete the record of this layout.
|
||||
GPoll::Final(unsafe { frame.finish_served() })
|
||||
}
|
||||
|
||||
fn extent_at<'x>(&self, _input: &C, _level: u8, _frames: &Frames<'x>) -> GPoll<Extent>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'x Arena>,
|
||||
{
|
||||
GPoll::Final(Extent::Exactly(self.rows.len()))
|
||||
}
|
||||
|
||||
fn layout(&self) -> &Layout {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
/// A leveled source that keeps its count to itself: the extent is a lower
|
||||
/// bound and lanes past the data answer the past-end signal.
|
||||
pub struct DrainSourceNode {
|
||||
pub layout: Layout,
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
impl<C: ExtractIndex> Node<C> for DrainSourceNode {
|
||||
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'e Arena>,
|
||||
{
|
||||
let lane = input.innermost_index();
|
||||
if lane >= self.count as u64 {
|
||||
return GPoll::past_end();
|
||||
}
|
||||
let mut frame = slot;
|
||||
let arena = ExtractArena::arena(input);
|
||||
if frame.element(lane as f64, arena).is_none() {
|
||||
return GPoll::arena_exhausted();
|
||||
}
|
||||
// SAFETY: the writes above complete the record of this layout.
|
||||
GPoll::Final(unsafe { frame.finish_served() })
|
||||
}
|
||||
|
||||
fn extent_at<'x>(&self, _input: &C, _level: u8, _frames: &Frames<'x>) -> GPoll<Extent>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'x Arena>,
|
||||
{
|
||||
GPoll::Final(Extent::AtLeast(0))
|
||||
}
|
||||
|
||||
fn layout(&self) -> &Layout {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
/// Depth-0 content varying per copy: serves the enclosing (pushed) level's
|
||||
/// index, which sits one link above the content's own innermost lane.
|
||||
pub struct IndexSourceNode {
|
||||
pub layout: Layout,
|
||||
}
|
||||
|
||||
impl<C: ExtractIndex + ExtractIndices> Node<C> for IndexSourceNode {
|
||||
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'e Arena>,
|
||||
{
|
||||
let element = input.try_index().and_then(|mut indices| indices.nth(1)).unwrap_or(0) as f64;
|
||||
let mut frame = slot;
|
||||
let arena = ExtractArena::arena(input);
|
||||
if frame.element(element, arena).is_none() {
|
||||
return GPoll::arena_exhausted();
|
||||
}
|
||||
// SAFETY: the writes above complete the record of this layout.
|
||||
GPoll::Final(unsafe { frame.finish_served() })
|
||||
}
|
||||
|
||||
fn layout(&self) -> &Layout {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes a field at the layout's resolved offset, the wiring-proven pairing
|
||||
/// a generated node performs.
|
||||
pub fn write_field_at<T: Copy + 'static>(frame: &mut FrameClaim<'_, '_>, layout: &Layout, name: &str, level: u8, value: T) {
|
||||
let field = layout
|
||||
.fields
|
||||
.iter()
|
||||
.find(|field| field.name == name && field.level == level)
|
||||
.expect("the layout carries the written field");
|
||||
assert_eq!(field.type_id, std::any::TypeId::of::<T>(), "the field was declared at this value type");
|
||||
// SAFETY: the offset is this layout's own, at the field's declared type.
|
||||
unsafe { frame.attr_at(field.offset, value) };
|
||||
}
|
||||
|
||||
/// [`write_field_at`] for a census marker at level 0.
|
||||
pub fn write_attr_at<A: Attribute>(frame: &mut FrameClaim<'_, '_>, layout: &Layout, value: A::Value<'static>)
|
||||
where
|
||||
A::Value<'static>: Copy + 'static,
|
||||
{
|
||||
write_field_at(frame, layout, A::NAME, 0, value);
|
||||
}
|
||||
|
||||
pub fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
|
||||
EvalScope::new(Some(0.5), None, None, generations, arena)
|
||||
}
|
||||
|
||||
fn f64_fields(names: &[&'static str]) -> Vec<FieldWrite> {
|
||||
names
|
||||
.iter()
|
||||
.map(|name| FieldWrite {
|
||||
name,
|
||||
level: 0,
|
||||
size: 8,
|
||||
align: 8,
|
||||
type_id: std::any::TypeId::of::<f64>(),
|
||||
read_erased: <Opacity as Attribute>::read_erased,
|
||||
repark: None,
|
||||
content_hash: None,
|
||||
content_eq: None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A depth-0 `f64` record layout carrying the named `f64` fields.
|
||||
pub fn f64_layout(names: &[&'static str]) -> Layout {
|
||||
Layout::default().with_writes(0, element_write::<f64>(), &f64_fields(names))
|
||||
}
|
||||
|
||||
/// A one-level `f64` layout carrying the named `f64` fields.
|
||||
pub fn leveled_f64_layout(names: &[&'static str]) -> Layout {
|
||||
Layout::default().with_writes(1, element_write::<f64>(), &f64_fields(names))
|
||||
}
|
||||
|
||||
/// Frame space sized for the layouts, with a floor for small fixtures.
|
||||
pub fn frames_for(layouts: &[&Layout]) -> Frames<'static> {
|
||||
test_frames(layouts.iter().map(|layout| layout.frame_bytes()).sum::<usize>().max(1 << 12))
|
||||
}
|
||||
|
||||
/// Installs the layout the compiler pass would resolve for `node` over the
|
||||
/// given input layouts. The fixtures wire constants into every eager input,
|
||||
/// which the pass records as lane-invariant.
|
||||
pub fn install<N: Node<ContextImpl<'static>>>(mut node: N, meta: LayoutMeta, inputs: &[Option<&Layout>]) -> N {
|
||||
let resolved = RecordLayout {
|
||||
named_writes: Vec::new(),
|
||||
named_reads: Vec::new(),
|
||||
named_read_defaults: Vec::new(),
|
||||
lane_invariant: u32::MAX,
|
||||
..meta.resolve(inputs)
|
||||
};
|
||||
<N as Node<ContextImpl<'static>>>::set_layout(&mut node, resolved);
|
||||
node
|
||||
}
|
||||
|
||||
/// Installs a flipped node's output layout directly.
|
||||
pub fn install_flip<N: Node<ContextImpl<'static>>>(mut node: N, layout: &Layout) -> N {
|
||||
let bundle = RecordLayout {
|
||||
named_writes: Vec::new(),
|
||||
named_reads: Vec::new(),
|
||||
named_read_defaults: Vec::new(),
|
||||
frame_bytes: layout.frame_bytes(),
|
||||
plan: Vec::new(),
|
||||
layout: layout.clone(),
|
||||
lane_invariant: u32::MAX,
|
||||
};
|
||||
<N as Node<ContextImpl<'static>>>::set_layout(&mut node, bundle);
|
||||
node
|
||||
}
|
||||
|
||||
/// A constant as a value source with its layout.
|
||||
pub fn lifted_value<T: Clone + Send + Sync + dyn_any::StaticTypeSized + 'static>(value: T) -> (ValueSource<T>, Layout)
|
||||
where
|
||||
T::Static: Clone + Send + Sync,
|
||||
{
|
||||
let lift = ValueSource::new(value);
|
||||
let layout = Node::<ContextImpl>::layout(&lift).clone();
|
||||
(lift, layout)
|
||||
}
|
||||
|
||||
pub fn bare_source(layout: &Layout, element: f64) -> RecordSourceNode<f64> {
|
||||
RecordSourceNode {
|
||||
layout: layout.clone(),
|
||||
element,
|
||||
fields: vec![],
|
||||
partial: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,14 @@ pub fn cache_key<C: CacheHash + ?Sized>(ctx: &C) -> u64 {
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
/// The lane-normalized cache key and arena generation of one evaluation, so a
|
||||
/// node can keep per-evaluation scratch across its lanes.
|
||||
pub fn eval_key<'e, C: CacheHash + crate::context::InjectIndex + crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena> + Copy>(ctx: &C) -> (u64, u64) {
|
||||
let mut keyed = *ctx;
|
||||
keyed.set_index(0);
|
||||
(cache_key(&keyed), ctx.arena().generation())
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ConstructionError {
|
||||
Arity { expected: usize, got: usize },
|
||||
|
||||
@@ -23,14 +23,22 @@ core-types = { workspace = true }
|
||||
graphene-hash = { workspace = true }
|
||||
raster-types = { workspace = true }
|
||||
graphic-types = { workspace = true }
|
||||
vector-types = { workspace = true }
|
||||
node-macro = { workspace = true }
|
||||
|
||||
# Workspace dependencies
|
||||
dyn-any = { workspace = true }
|
||||
glam = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
# Optional workspace dependencies
|
||||
serde = { workspace = true, optional = true }
|
||||
tsify = { workspace = true, optional = true }
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# The level tests drive the production catalog's kernels
|
||||
graphic-nodes = { workspace = true }
|
||||
repeat-nodes = { workspace = true }
|
||||
math-nodes = { workspace = true }
|
||||
|
||||
@@ -53,6 +53,7 @@ vararg_readers! {
|
||||
read_color / read_color_extent / ReadColorNode: Color;
|
||||
read_gradient / read_gradient_extent / ReadGradientNode: Gradient;
|
||||
read_string / read_string_extent / ReadStringNode: String;
|
||||
read_number / read_number_extent / ReadNumberNode: f64;
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Context"), path(core_types::vector))]
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod context;
|
||||
pub mod context_modification;
|
||||
pub mod debug;
|
||||
pub mod extract_xy;
|
||||
pub mod list;
|
||||
pub mod memo;
|
||||
pub mod ops;
|
||||
#[cfg(test)]
|
||||
|
||||
725
node-graph/nodes/gcore/src/list.rs
Normal file
725
node-graph/nodes/gcore/src/list.rs
Normal file
@@ -0,0 +1,725 @@
|
||||
//! The generic level kernels: the nodes that reorder, select, expand or
|
||||
//! collapse a level regardless of its element type.
|
||||
|
||||
use core_types::context::IndexLink;
|
||||
use core_types::extent::{ExtentIn, LevelIn, ListIn, ValueIn};
|
||||
use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt, Level};
|
||||
use core_types::list::List;
|
||||
use core_types::node::Lane;
|
||||
use core_types::registry::types::{SeedValue, SignedInteger};
|
||||
use core_types::uuid::NodeId;
|
||||
use core_types::{CacheHash, Color, Ctx, DeriveCtx, ExtractIndex, InjectIndex, ModifyIndex};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphic_types::vector_types::Gradient;
|
||||
use graphic_types::{Artboard, Graphic, Vector};
|
||||
use rand::SeedableRng;
|
||||
use rand::seq::SliceRandom;
|
||||
use raster_types::{CPU, GPU, Raster};
|
||||
use std::cmp::Ordering;
|
||||
|
||||
/// Resolves a signed index over `total` lanes: negatives count from the end,
|
||||
/// out of range resolves to nothing.
|
||||
fn resolve_index(index: f64, total: u64) -> Option<u64> {
|
||||
let index = index as i64;
|
||||
match index < 0 {
|
||||
true => total.checked_sub(index.unsigned_abs()),
|
||||
false => ((index as u64) < total).then_some(index as u64),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the list with the item at the specified index removed.
|
||||
/// If no value exists at that index, the list is returned unchanged.
|
||||
#[node_macro::node(category("General"), name("Remove at Index"), extent(omit_element_extent))]
|
||||
pub fn remove_at_index<T>(
|
||||
ctx: impl Ctx + ModifyIndex + Copy,
|
||||
/// The list of data.
|
||||
list: impl Node<Context<'_>, Output = T>,
|
||||
/// The index of the item to remove, starting from 0 for the first item. Negative indices count backwards from the end of the list, starting from -1 for the last item.
|
||||
index: SignedInteger,
|
||||
) -> Result<T, Interrupt> {
|
||||
let total = match list.extent(ctx, Level::Total) {
|
||||
GPoll::Final(Extent::Exactly(count)) => count as u64,
|
||||
GPoll::Pending => return Err(Interrupt::Pending),
|
||||
_ => return Err(GraphError::new("omit over a non-exact extent").into()),
|
||||
};
|
||||
let lane = ctx.index();
|
||||
let source = match resolve_index(index, total) {
|
||||
Some(omitted) if lane >= omitted => lane + 1,
|
||||
_ => lane,
|
||||
};
|
||||
let mut shifted = *ctx;
|
||||
shifted.set_index(source);
|
||||
list.eval(&shifted)
|
||||
}
|
||||
|
||||
fn omit_element_extent(list: ExtentIn<'_>, index: ValueIn<'_, f64>, level: LevelIn) -> GPoll<Extent> {
|
||||
match level.top() {
|
||||
true => index.get().zip(list.at(level)).map(|(index, extent)| match extent {
|
||||
Extent::Exactly(count) if resolve_index(index, count as u64).is_some() => Extent::Exactly(count - 1),
|
||||
extent => extent,
|
||||
}),
|
||||
false => list.at(level),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the bare element (without the item's attributes) at the specified index in a `List`.
|
||||
/// Use this when downstream nodes want just the inner value rather than a `List` containing a single item.
|
||||
/// If no value exists at that index, the element type's default is returned.
|
||||
#[node_macro::node(category("General"), name("Item at Index"))]
|
||||
pub fn item_at_index<T: Clone + Default + Send + Sync + CacheHash + 'static>(
|
||||
_: impl Ctx,
|
||||
/// The `List` of data to extract from.
|
||||
#[implementations(String, f64, NodeId, Color, Gradient, Vector, Raster<CPU>, Graphic, Artboard)]
|
||||
list: IList<T>,
|
||||
/// The index of the item to retrieve, starting from 0 for the first item. Negative indices count backwards from the end of the list, starting from -1 for the last item.
|
||||
index: SignedInteger,
|
||||
) -> T {
|
||||
resolve_index(index, list.len() as u64).map(|resolved| list.element_ref(resolved as usize).clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// A lane order computed once per evaluation, for the nodes whose lane
|
||||
/// mapping needs the whole level (sort, shuffle).
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LaneOrder {
|
||||
key: u64,
|
||||
generation: u64,
|
||||
order: Vec<usize>,
|
||||
}
|
||||
|
||||
type LaneOrderCache = std::sync::Arc<std::sync::Mutex<Option<LaneOrder>>>;
|
||||
|
||||
/// The source lane of output `lane` under the order `build` produces, built
|
||||
/// once per `(key, generation)`.
|
||||
fn ordered_source(cache: &LaneOrderCache, (key, generation): (u64, u64), lane: usize, build: impl FnOnce() -> Vec<usize>) -> Option<usize> {
|
||||
let mut cached = cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if !matches!(cached.as_ref(), Some(entry) if entry.key == key && entry.generation == generation) {
|
||||
*cached = Some(LaneOrder { key, generation, order: build() });
|
||||
}
|
||||
cached.as_ref().and_then(|entry| entry.order.get(lane).copied())
|
||||
}
|
||||
|
||||
/// The lanes the pattern keeps: a period's kept positions and how many lanes
|
||||
/// there are in total, so a lane maps to its source in constant time.
|
||||
fn kept_source(pattern: &[bool], lane: usize) -> usize {
|
||||
if pattern.is_empty() {
|
||||
return lane;
|
||||
}
|
||||
let kept: Vec<usize> = pattern.iter().enumerate().filter_map(|(position, keep)| keep.then_some(position)).collect();
|
||||
(lane / kept.len()) * pattern.len() + kept[lane % kept.len()]
|
||||
}
|
||||
|
||||
fn kept_count(pattern: &[bool], total: usize) -> usize {
|
||||
if pattern.is_empty() {
|
||||
return total;
|
||||
}
|
||||
let kept_per_period = pattern.iter().filter(|keep| **keep).count();
|
||||
let full_periods = total / pattern.len();
|
||||
let tail_kept = pattern[..total % pattern.len()].iter().filter(|keep| **keep).count();
|
||||
full_periods * kept_per_period + tail_kept
|
||||
}
|
||||
|
||||
/// Keeps chosen items from a list (those corresponding to `true` values) and discards the others (those corresponding to `false` values) based on the *Keep Pattern* bool list. A short pattern is repeated over the remainder of the filtered list, allowing a pattern like `[true, false]` to keep every other item starting from the first. An empty pattern keeps all items.
|
||||
#[node_macro::node(category("General"), extent(filter_extent))]
|
||||
fn filter<T: Clone + Send + Sync + CacheHash + 'static>(
|
||||
ctx: impl Ctx + ExtractIndex + InjectIndex + Copy,
|
||||
/// The list of data to filter.
|
||||
#[implementations(String, bool, f32, f64, u32, u64, DVec2, DAffine2, Vector, Graphic, Raster<CPU>, Raster<GPU>, Color, Gradient, Artboard)]
|
||||
list: IList<T>,
|
||||
/// The list of true and false values that determines which corresponding items are kept (`true`) and discarded (`false`). The pattern may repeat if it is shorter than the list of data.
|
||||
keep_pattern: IList<bool>,
|
||||
) -> Result<IList<Lane<T>>, Interrupt> {
|
||||
let pattern: Vec<bool> = keep_pattern.iter().collect();
|
||||
let source = kept_source(&pattern, ctx.index() as usize);
|
||||
if source >= list.len() {
|
||||
return Err(GraphError::past_end().into());
|
||||
}
|
||||
Ok(list.lane(source))
|
||||
}
|
||||
|
||||
fn filter_extent<T>(list: ListIn<'_, T>, keep_pattern: ListIn<'_, bool>, level: LevelIn) -> GPoll<Extent> {
|
||||
match level.top() {
|
||||
true => list.get().zip(keep_pattern.get()).map(|(list, keep_pattern)| {
|
||||
let pattern: Vec<bool> = keep_pattern.iter().collect();
|
||||
Extent::Exactly(kept_count(&pattern, list.len()))
|
||||
}),
|
||||
false => GPoll::Final(Extent::Exactly(1)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reverses the order of the items in a list, so the last item comes first and the first comes last.
|
||||
#[node_macro::node(category("General"), extent(same_count_extent))]
|
||||
fn reverse<T: Clone + Send + Sync + CacheHash + 'static>(
|
||||
ctx: impl Ctx + ExtractIndex + InjectIndex + Copy,
|
||||
/// The list of data to reverse.
|
||||
#[implementations(String, bool, f32, f64, u32, u64, DVec2, DAffine2, Vector, Graphic, Raster<CPU>, Raster<GPU>, Color, Gradient, Artboard)]
|
||||
list: IList<T>,
|
||||
) -> Result<IList<Lane<T>>, Interrupt> {
|
||||
let lane = ctx.index() as usize;
|
||||
if lane >= list.len() {
|
||||
return Err(GraphError::past_end().into());
|
||||
}
|
||||
Ok(list.lane(list.len() - 1 - lane))
|
||||
}
|
||||
|
||||
/// The level keeps the list's count; the lanes only change places.
|
||||
fn same_count_extent<T>(list: ListIn<'_, T>, level: LevelIn) -> GPoll<Extent> {
|
||||
match level.top() {
|
||||
true => list.total(),
|
||||
false => GPoll::Final(Extent::Exactly(1)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Shifts the items in a list by a number of positions. With wrapping, items pushed off one end reappear at the other. Otherwise they are dropped, shortening the list.
|
||||
#[node_macro::node(category("General"), extent(shift_extent))]
|
||||
fn shift<T: Clone + Send + Sync + CacheHash + 'static>(
|
||||
ctx: impl Ctx + ExtractIndex + InjectIndex + Copy,
|
||||
/// The list of data to shift.
|
||||
#[implementations(String, bool, f32, f64, u32, u64, DVec2, DAffine2, Vector, Graphic, Raster<CPU>, Raster<GPU>, Color, Gradient, Artboard)]
|
||||
list: IList<T>,
|
||||
/// How many positions to shift each item. Positive values shift items toward the start of the list, negative toward the end.
|
||||
amount: SignedInteger,
|
||||
/// Whether items shifted off one end wrap around to the other. When off, they are dropped and the list gets shorter.
|
||||
#[default(true)]
|
||||
wrap: bool,
|
||||
) -> Result<IList<Lane<T>>, Interrupt> {
|
||||
let lane = ctx.index() as i64;
|
||||
let amount = amount as i64;
|
||||
let len = list.len() as i64;
|
||||
let source = match (wrap, len) {
|
||||
(_, 0) => return Err(GraphError::past_end().into()),
|
||||
(true, len) => (lane + amount).rem_euclid(len),
|
||||
// Dropping from the front reads ahead; dropping from the back only shortens the level.
|
||||
(false, _) => lane + amount.max(0),
|
||||
};
|
||||
if source >= len {
|
||||
return Err(GraphError::past_end().into());
|
||||
}
|
||||
Ok(list.lane(source as usize))
|
||||
}
|
||||
|
||||
fn shift_extent<T>(list: ListIn<'_, T>, amount: ValueIn<'_, f64>, wrap: ValueIn<'_, bool>, level: LevelIn) -> GPoll<Extent> {
|
||||
match level.top() {
|
||||
true => list.total().zip(amount.get()).zip(wrap.get()).map(|((total, amount), wrap)| match (total, wrap) {
|
||||
(Extent::Exactly(count), false) => Extent::Exactly(count.saturating_sub(amount.abs() as usize)),
|
||||
(total, _) => total,
|
||||
}),
|
||||
false => GPoll::Final(Extent::Exactly(1)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Randomly reorders the items in a list. The same seed always produces the same ordering.
|
||||
#[node_macro::node(category("General"), extent(shuffle_extent))]
|
||||
fn shuffle<'e, T: Clone + Send + Sync + CacheHash + 'static>(
|
||||
ctx: impl Ctx + CacheHash + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
|
||||
/// The list to have its items randomly reordered.
|
||||
#[implementations(String, bool, f32, f64, u32, u64, DVec2, DAffine2, Vector, Graphic, Raster<CPU>, Raster<GPU>, Color, Gradient, Artboard)]
|
||||
list: IList<T>,
|
||||
/// Seed to determine the unique variation of the random shuffle ordering. The same seed always produces the same ordering.
|
||||
seed: SeedValue,
|
||||
#[data] order: LaneOrderCache,
|
||||
) -> Result<IList<Lane<T>>, Interrupt> {
|
||||
let source = ordered_source(order, core_types::registry::eval_key(ctx), ctx.index() as usize, || {
|
||||
let mut order: Vec<usize> = (0..list.len()).collect();
|
||||
order.shuffle(&mut rand::rngs::StdRng::seed_from_u64(seed.into()));
|
||||
order
|
||||
});
|
||||
source.map(|source| list.lane(source)).ok_or_else(|| GraphError::past_end().into())
|
||||
}
|
||||
|
||||
fn shuffle_extent<T>(list: ListIn<'_, T>, _seed: ValueIn<'_, SeedValue>, level: LevelIn) -> GPoll<Extent> {
|
||||
same_count_extent(list, level)
|
||||
}
|
||||
|
||||
/// Generates a list of evenly spaced numbers, starting at a value and progressing by a step (which may be positive, negative, or zero) for a given count.
|
||||
#[node_macro::node(category("General"), name("Number Sequence"), extent(number_sequence_extent))]
|
||||
fn number_sequence(
|
||||
ctx: impl Ctx + ExtractIndex + InjectIndex + Copy,
|
||||
_primary: (),
|
||||
/// The first number in the sequence.
|
||||
start: f64,
|
||||
/// The amount added to reach each successive number.
|
||||
#[default(1.)]
|
||||
step: f64,
|
||||
/// How many numbers to generate.
|
||||
#[default(10)]
|
||||
count: u32,
|
||||
) -> Result<IList<f64>, Interrupt> {
|
||||
let lane = ctx.index();
|
||||
if lane >= count as u64 {
|
||||
return Err(GraphError::past_end().into());
|
||||
}
|
||||
Ok(start + step * lane as f64)
|
||||
}
|
||||
|
||||
fn number_sequence_extent(_primary: ValueIn<'_, ()>, _start: ValueIn<'_, f64>, _step: ValueIn<'_, f64>, count: ValueIn<'_, u32>, level: LevelIn) -> GPoll<Extent> {
|
||||
match level.top() {
|
||||
true => count.get().map(|count| Extent::Exactly(count as usize)),
|
||||
false => GPoll::Final(Extent::Exactly(1)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Counts out the index of each item in a list (0, 1, 2, and so on), producing a list of numbers with one for each item.
|
||||
#[node_macro::node(category("General"), extent(list_indices_extent))]
|
||||
fn list_indices<T: Clone + Send + Sync + CacheHash + 'static>(
|
||||
ctx: impl Ctx + ExtractIndex + InjectIndex + Copy,
|
||||
/// The list whose items are counted.
|
||||
#[implementations(String, bool, f32, f64, u32, u64, DVec2, DAffine2, Vector, Graphic, Raster<CPU>, Raster<GPU>, Color, Gradient, Artboard)]
|
||||
list: IList<T>,
|
||||
/// The number that the count begins from for the first item.
|
||||
start_index: SignedInteger,
|
||||
) -> Result<IList<Lane<f64>>, Interrupt> {
|
||||
let lane = ctx.index() as usize;
|
||||
if lane >= list.len() {
|
||||
return Err(GraphError::past_end().into());
|
||||
}
|
||||
Ok(list.lane(lane).map_element(start_index + lane as f64))
|
||||
}
|
||||
|
||||
fn list_indices_extent<T>(list: ListIn<'_, T>, _start_index: ValueIn<'_, f64>, level: LevelIn) -> GPoll<Extent> {
|
||||
same_count_extent(list, level)
|
||||
}
|
||||
|
||||
/// The half-open lane range a slice keeps: negative starts count from the
|
||||
/// end, and an end at or below zero counts from the end too.
|
||||
fn slice_bounds(total: usize, start: f64, end: f64) -> (usize, usize) {
|
||||
let start = match start < 0. {
|
||||
true => total.saturating_sub(start.abs() as usize),
|
||||
false => (start as usize).min(total),
|
||||
};
|
||||
let end = match end <= 0. {
|
||||
true => total.saturating_sub(end.abs() as usize),
|
||||
false => (end as usize).min(total),
|
||||
};
|
||||
(start, end.max(start))
|
||||
}
|
||||
|
||||
/// Extracts a portion of a list, starting at "Start" and ending before "End".
|
||||
///
|
||||
/// Negative indices count from the end of the list. If the index of "Start" equals or exceeds "End", the result is an empty list.
|
||||
#[node_macro::node(category("General"), extent(list_slice_extent))]
|
||||
fn list_slice<T: Clone + Send + Sync + CacheHash + 'static>(
|
||||
ctx: impl Ctx + ExtractIndex + InjectIndex + Copy,
|
||||
/// The list of data to take a portion of.
|
||||
#[implementations(String, bool, f32, f64, u32, u64, DVec2, DAffine2, Vector, Graphic, Raster<CPU>, Raster<GPU>, Color, Gradient, Artboard)]
|
||||
list: IList<T>,
|
||||
/// The index of the first item in the portion. Negative indices count from the end of the list.
|
||||
start: SignedInteger,
|
||||
/// The index the portion ends before, which is not included. Zero or negative indices count from the end of the list.
|
||||
end: SignedInteger,
|
||||
) -> Result<IList<Lane<T>>, Interrupt> {
|
||||
let (first, end) = slice_bounds(list.len(), start, end);
|
||||
let source = first + ctx.index() as usize;
|
||||
if source >= end {
|
||||
return Err(GraphError::past_end().into());
|
||||
}
|
||||
Ok(list.lane(source))
|
||||
}
|
||||
|
||||
fn list_slice_extent<T>(list: ListIn<'_, T>, start: ValueIn<'_, f64>, end: ValueIn<'_, f64>, level: LevelIn) -> GPoll<Extent> {
|
||||
match level.top() {
|
||||
true => list.total().zip(start.get()).zip(end.get()).map(|((total, start), end)| match total {
|
||||
Extent::Exactly(count) => {
|
||||
let (first, end) = slice_bounds(count, start, end);
|
||||
Extent::Exactly(end - first)
|
||||
}
|
||||
total => total,
|
||||
}),
|
||||
false => GPoll::Final(Extent::Exactly(1)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pairwise ordering used by the Sort node for element values. Types without a natural
|
||||
/// order compare as equal, so the stable sort leaves their items in their original relative positions.
|
||||
pub trait ElementOrder {
|
||||
fn element_order(&self, _other: &Self) -> Ordering {
|
||||
Ordering::Equal
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! element_order {
|
||||
(ordered: $($ordered:ty),*; unordered: $($unordered:ty),*;) => {
|
||||
$(
|
||||
impl ElementOrder for $ordered {
|
||||
fn element_order(&self, other: &Self) -> Ordering {
|
||||
self.cmp(other)
|
||||
}
|
||||
}
|
||||
)*
|
||||
$(impl ElementOrder for $unordered {})*
|
||||
};
|
||||
}
|
||||
|
||||
element_order! {
|
||||
ordered: String, bool, u32, u64;
|
||||
unordered: DVec2, DAffine2, Vector, Graphic<'_>, Raster<CPU>, Raster<GPU>, Color, Gradient, Artboard<'_>;
|
||||
}
|
||||
|
||||
impl ElementOrder for f32 {
|
||||
fn element_order(&self, other: &Self) -> Ordering {
|
||||
self.total_cmp(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl ElementOrder for f64 {
|
||||
fn element_order(&self, other: &Self) -> Ordering {
|
||||
self.total_cmp(other)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reorders a list's items from smallest to largest, either by each item's own value or by a parallel list of sortable values in the *Sort Order* input. The sort is stable, so items with the same sort order retain their relative positions.
|
||||
#[node_macro::node(category("General"), extent(sort_extent))]
|
||||
fn sort<'e, T: ElementOrder + Clone + Send + Sync + CacheHash + 'static>(
|
||||
ctx: impl Ctx + CacheHash + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
|
||||
/// The list of data to reorder.
|
||||
#[implementations(String, bool, f32, f64, u32, u64, DVec2, DAffine2, Vector, Graphic, Raster<CPU>, Raster<GPU>, Color, Gradient, Artboard)]
|
||||
list: IList<T>,
|
||||
/// The optional list of orderable values, corresponding item-to-item with the input list, to sort by instead of the items' own values.
|
||||
// The two-generic grid master authors here (f64/String/bool key lists) needs multi-generic implementations support our macro does not have; narrowed to f64 keys.
|
||||
#[expose]
|
||||
sort_order: IList<f64>,
|
||||
/// Reverses the sorted list order, following descending order instead of ascending (numbers largest-to-smallest, strings Z-to-A, etc.).
|
||||
reverse: bool,
|
||||
#[data] order: LaneOrderCache,
|
||||
) -> Result<IList<Lane<T>>, Interrupt> {
|
||||
let source = ordered_source(order, core_types::registry::eval_key(ctx), ctx.index() as usize, || {
|
||||
// Order by the parallel keys when provided (repeating the last if there are fewer keys than items), otherwise by the element values themselves
|
||||
let keys: Vec<f64> = sort_order.iter().collect();
|
||||
let mut order: Vec<usize> = (0..list.len()).collect();
|
||||
order.sort_by(|&a, &b| {
|
||||
let ordering = match keys.as_slice() {
|
||||
[] => list.element_ref(a).element_order(list.element_ref(b)),
|
||||
keys => keys[a.min(keys.len() - 1)].element_order(&keys[b.min(keys.len() - 1)]),
|
||||
};
|
||||
if reverse { ordering.reverse() } else { ordering }
|
||||
});
|
||||
order
|
||||
});
|
||||
source.map(|source| list.lane(source)).ok_or_else(|| GraphError::past_end().into())
|
||||
}
|
||||
|
||||
fn sort_extent<T>(list: ListIn<'_, T>, _sort_order: ListIn<'_, f64>, _reverse: ValueIn<'_, bool>, level: LevelIn) -> GPoll<Extent> {
|
||||
same_count_extent(list, level)
|
||||
}
|
||||
|
||||
/// One content row as the vararg shape the readers expect: a single-item
|
||||
/// legacy list carrying the row's element only, so the list's dyn-hash is a
|
||||
/// complete cache key over the observables.
|
||||
fn vararg_row<Row: Clone + Send + Sync + 'static>(content: core_types::node::List<'_, Row>, row: usize) -> List<Row> {
|
||||
List::new_from_element(content.element_ref(row).clone())
|
||||
}
|
||||
|
||||
/// One subgraph invocation per content row, the row riding as a vararg, with
|
||||
/// the subgraph's lanes concatenated into one flat level. The level reports a
|
||||
/// lower bound; consumers drain to the past-end signal.
|
||||
#[node_macro::node(category("General"))]
|
||||
pub fn map<Row: Clone + Send + Sync + CacheHash + 'static, T>(
|
||||
ctx: impl Ctx + DeriveCtx + ExtractIndex + InjectIndex + Copy,
|
||||
#[implementations(Graphic, Vector, Raster<CPU>, Color, Gradient, String)] content: IList<Row>,
|
||||
mapped: impl Node<Context<'_>, Output = IList<T>>,
|
||||
) -> Result<IList<T>, Interrupt> {
|
||||
let mut remaining = ctx.index();
|
||||
for row in 0..content.len() {
|
||||
let item = vararg_row(content, row);
|
||||
let scoped = ctx.push_vararg(&item);
|
||||
let lanes = mapped.inner_extent_at(&scoped.ctx(), row as u64)?;
|
||||
if remaining >= lanes {
|
||||
remaining -= lanes;
|
||||
continue;
|
||||
}
|
||||
let mut frame = IndexLink { index: 0, outer: None };
|
||||
return mapped.eval(&scoped.ctx().push_level(&mut frame, row as u64, remaining));
|
||||
}
|
||||
Err(GraphError::past_end().into())
|
||||
}
|
||||
|
||||
/// Joins two levels of the same type, the base's lanes followed by the new's.
|
||||
#[node_macro::node(category("General"), extent(extend_extent))]
|
||||
pub fn extend<T>(
|
||||
ctx: impl Ctx + ExtractIndex + InjectIndex + Copy,
|
||||
/// The input whose lanes appear at the start of the extended level.
|
||||
base: impl Node<Context<'_>, Output = T>,
|
||||
/// The input whose lanes appear at the end of the extended level.
|
||||
#[expose]
|
||||
new: impl Node<Context<'_>, Output = T>,
|
||||
) -> Result<T, Interrupt> {
|
||||
let split = match base.extent(ctx, Level::Total) {
|
||||
GPoll::Final(Extent::Exactly(count)) => count as u64,
|
||||
// A scalar side joins the concat as a single lane, per `Extent::sum`.
|
||||
GPoll::Final(Extent::Free) => 1,
|
||||
GPoll::Pending => return Err(Interrupt::Pending),
|
||||
_ => return Err(GraphError::new("extend over a non-exact base extent").into()),
|
||||
};
|
||||
let lane = ctx.index();
|
||||
match lane < split {
|
||||
true => base.eval(ctx),
|
||||
false => {
|
||||
let mut shifted = *ctx;
|
||||
shifted.set_index(lane - split);
|
||||
new.eval(&shifted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The top level sums both sides; inner levels must agree (rectangular), a
|
||||
/// free side or a side with no top-level lanes defers to the other.
|
||||
fn extend_extent(base: ExtentIn<'_>, new: ExtentIn<'_>, level: LevelIn) -> GPoll<Extent> {
|
||||
match level.top() {
|
||||
true => Extent::sum(base.at(level), new.at(level)),
|
||||
false => base.at(level).zip(new.at(level)).and_then(|extents| match extents {
|
||||
(Extent::Free, other) | (other, Extent::Free) => GPoll::Final(other),
|
||||
(base_inner, new_inner) if base_inner == new_inner => GPoll::Final(base_inner),
|
||||
(base_inner, new_inner) => {
|
||||
let top = LevelIn {
|
||||
level: level.depth - 1,
|
||||
depth: level.depth,
|
||||
};
|
||||
match (base.at(top), new.at(top)) {
|
||||
(GPoll::Final(Extent::Exactly(0)), _) => GPoll::Final(new_inner),
|
||||
(_, GPoll::Final(Extent::Exactly(0))) => GPoll::Final(base_inner),
|
||||
_ => GPoll::error("extend inner extents differ"),
|
||||
}
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub use _map_mod::map_entries;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use core_types::arena::Arena;
|
||||
use core_types::context::ContextImpl;
|
||||
use core_types::node::Node;
|
||||
use core_types::record::test_fixtures::*;
|
||||
use core_types::record::{Frames, Layout, capture};
|
||||
use core_types::value::{LeveledValueSource, ValueSource};
|
||||
|
||||
/// A level of `elements` as a record source with its layout.
|
||||
fn level<T: Clone + Send + Sync + CacheHash + PartialEq + dyn_any::StaticTypeSized + 'static>(elements: Vec<T>) -> (LeveledValueSource<T>, Layout)
|
||||
where
|
||||
T::Static: Clone + Send + Sync,
|
||||
{
|
||||
let source = LeveledValueSource::new(elements);
|
||||
let layout = Node::<ContextImpl>::layout(&source).clone();
|
||||
(source, layout)
|
||||
}
|
||||
|
||||
/// The level's elements in lane order, read through the extent.
|
||||
fn elements<'a, T: Clone + 'static, N: for<'c> Node<ContextImpl<'c>>>(node: &N, ctx: &ContextImpl<'a>, frames: &Frames<'a>) -> Vec<T> {
|
||||
let GPoll::Final(Extent::Exactly(count)) = node.extent_at(ctx, 0, &frames.reborrow()) else {
|
||||
panic!("expected an exact level");
|
||||
};
|
||||
let head = ctx.index_head();
|
||||
let mut elements = Vec::with_capacity(count);
|
||||
for lane in 0..count {
|
||||
let GPoll::Final(served) = capture(node, &ctx.promoted(&head, lane as u64), frames) else {
|
||||
panic!("expected a final record at lane {lane}");
|
||||
};
|
||||
elements.push(served.element::<T>());
|
||||
}
|
||||
elements
|
||||
}
|
||||
|
||||
macro_rules! fixture {
|
||||
($arena:ident, $scope:ident, $ctx:ident) => {
|
||||
let $arena = Arena::new(1 << 16).unwrap();
|
||||
let generations = [];
|
||||
let $scope = scope_fixture(&generations, &$arena);
|
||||
let $ctx = ContextImpl::root(&$scope);
|
||||
};
|
||||
}
|
||||
|
||||
fn sorted<T: Clone + Send + Sync + CacheHash + PartialEq + ElementOrder + dyn_any::StaticTypeSized + 'static>(items: Vec<T>, keys: Vec<f64>, reverse: bool) -> Vec<T>
|
||||
where
|
||||
T::Static: Clone + Send + Sync,
|
||||
{
|
||||
fixture!(arena, scope, ctx);
|
||||
let (list, list_layout) = level(items);
|
||||
let (keys, keys_layout) = level(keys);
|
||||
let frames = frames_for(&[&list_layout, &keys_layout]);
|
||||
let node = install(
|
||||
SortNode::<_, _, _, T>::new(list, keys, ValueSource::new(reverse)),
|
||||
sort_layout_meta(),
|
||||
&[Some(&list_layout), Some(&keys_layout)],
|
||||
);
|
||||
elements(&node, &ctx, &frames)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sorts_elements_by_their_natural_order() {
|
||||
assert_eq!(
|
||||
sorted(vec!["banana".to_string(), "apple".to_string(), "cherry".to_string()], vec![], false),
|
||||
["apple", "banana", "cherry"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sorts_elements_in_reverse() {
|
||||
assert_eq!(sorted(vec![3., 1., 2.], vec![], true), [3., 2., 1.]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort_order_keys_override_element_order() {
|
||||
assert_eq!(
|
||||
sorted(vec!["apple".to_string(), "banana".to_string(), "cherry".to_string()], vec![2., 0., 1.], false),
|
||||
["banana", "cherry", "apple"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_sort_order_repeats_its_last_key() {
|
||||
assert_eq!(sorted(vec!["a".to_string(), "b".to_string(), "c".to_string()], vec![2., 1.], false), ["b", "c", "a"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_sort_order_ignores_its_extra_keys() {
|
||||
assert_eq!(sorted(vec![1., 2.], vec![3., 1., 0., 5.], false), [2., 1.]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsortable_elements_keep_their_original_order() {
|
||||
let points = vec![DVec2::new(3., 3.), DVec2::new(1., 1.), DVec2::new(2., 2.)];
|
||||
assert_eq!(sorted(points.clone(), vec![], false), points);
|
||||
}
|
||||
|
||||
fn shifted(items: Vec<f64>, amount: f64, wrap: bool) -> Vec<f64> {
|
||||
fixture!(arena, scope, ctx);
|
||||
let (list, layout) = level(items);
|
||||
let frames = frames_for(&[&layout]);
|
||||
let node = install(
|
||||
ShiftNode::<_, _, _, f64>::new(list, ValueSource::new(amount), ValueSource::new(wrap)),
|
||||
shift_layout_meta(),
|
||||
&[Some(&layout)],
|
||||
);
|
||||
elements(&node, &ctx, &frames)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_wraps_items_around() {
|
||||
assert_eq!(shifted(vec![1., 2., 3., 4.], 1., true), [2., 3., 4., 1.]);
|
||||
assert_eq!(shifted(vec![1., 2., 3., 4.], -1., true), [4., 1., 2., 3.]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_without_wrapping_drops_items() {
|
||||
assert_eq!(shifted(vec![1., 2., 3., 4.], 1., false), [2., 3., 4.]);
|
||||
assert_eq!(shifted(vec![1., 2., 3., 4.], -1., false), [1., 2., 3.]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shuffle_is_deterministic_and_preserves_elements() {
|
||||
let original = vec![1., 2., 3., 4., 5., 6., 7., 8.];
|
||||
let shuffled = |seed: u32| {
|
||||
fixture!(arena, scope, ctx);
|
||||
let (list, layout) = level(original.clone());
|
||||
let frames = frames_for(&[&layout]);
|
||||
let node = install(ShuffleNode::<_, _, f64>::new(list, ValueSource::new(SeedValue::from(seed))), shuffle_layout_meta(), &[Some(&layout)]);
|
||||
elements::<f64, _>(&node, &ctx, &frames)
|
||||
};
|
||||
let first = shuffled(42);
|
||||
assert_eq!(first, shuffled(42), "the same seed should always produce the same ordering");
|
||||
|
||||
let mut recovered = first;
|
||||
recovered.sort_by(|a, b| a.total_cmp(b));
|
||||
assert_eq!(recovered, original, "shuffling should preserve all the elements");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reverse_flips_the_lane_order() {
|
||||
fixture!(arena, scope, ctx);
|
||||
let (list, layout) = level(vec![1., 2., 3.]);
|
||||
let frames = frames_for(&[&layout]);
|
||||
let node = install(ReverseNode::<_, f64>::new(list), reverse_layout_meta(), &[Some(&layout)]);
|
||||
assert_eq!(elements::<f64, _>(&node, &ctx, &frames), [3., 2., 1.]);
|
||||
}
|
||||
|
||||
fn filtered(items: Vec<f64>, pattern: Vec<bool>) -> Vec<f64> {
|
||||
fixture!(arena, scope, ctx);
|
||||
let (list, layout) = level(items);
|
||||
let (pattern, pattern_layout) = level(pattern);
|
||||
let frames = frames_for(&[&layout, &pattern_layout]);
|
||||
let node = install(FilterNode::<_, _, f64>::new(list, pattern), filter_layout_meta(), &[Some(&layout), Some(&pattern_layout)]);
|
||||
elements(&node, &ctx, &frames)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_tiles_the_keep_pattern_over_the_items() {
|
||||
assert_eq!(filtered(vec![1., 2., 3., 4., 5.], vec![true, false]), [1., 3., 5.]);
|
||||
assert_eq!(filtered(vec![1., 2., 3., 4., 5.], vec![false, true, true]), [2., 3., 5.]);
|
||||
assert_eq!(filtered(vec![1., 2., 3.], vec![]), [1., 2., 3.], "an empty pattern keeps everything");
|
||||
assert!(filtered(vec![1., 2., 3.], vec![false]).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_sequence_generates_evenly_spaced_numbers() {
|
||||
fixture!(arena, scope, ctx);
|
||||
let (unit, unit_layout) = lifted_value(());
|
||||
let (start, start_layout) = lifted_value(0.);
|
||||
let (step, step_layout) = lifted_value(2.);
|
||||
let (count, count_layout) = lifted_value(4_u32);
|
||||
let frames = frames_for(&[&unit_layout]);
|
||||
let node = install(
|
||||
NumberSequenceNode::new(unit, start, step, count, &unit_layout, &start_layout, &step_layout, &count_layout),
|
||||
number_sequence_layout_meta(),
|
||||
&[Some(&unit_layout), Some(&start_layout), Some(&step_layout), Some(&count_layout)],
|
||||
);
|
||||
assert_eq!(elements::<f64, _>(&node, &ctx, &frames), [0., 2., 4., 6.]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_indices_counts_each_item() {
|
||||
let indices = |start: f64| {
|
||||
fixture!(arena, scope, ctx);
|
||||
let (list, layout) = level(vec!["a".to_string(), "b".to_string(), "c".to_string()]);
|
||||
let frames = frames_for(&[&layout]);
|
||||
let node = install(ListIndicesNode::<_, _, String>::new(list, ValueSource::new(start)), list_indices_layout_meta(), &[Some(&layout)]);
|
||||
elements::<f64, _>(&node, &ctx, &frames)
|
||||
};
|
||||
assert_eq!(indices(0.), [0., 1., 2.]);
|
||||
assert_eq!(indices(1.), [1., 2., 3.]);
|
||||
}
|
||||
|
||||
fn sliced(items: Vec<f64>, start: f64, end: f64) -> Vec<f64> {
|
||||
fixture!(arena, scope, ctx);
|
||||
let (list, layout) = level(items);
|
||||
let frames = frames_for(&[&layout]);
|
||||
let node = install(
|
||||
ListSliceNode::<_, _, _, f64>::new(list, ValueSource::new(start), ValueSource::new(end)),
|
||||
list_slice_layout_meta(),
|
||||
&[Some(&layout)],
|
||||
);
|
||||
elements(&node, &ctx, &frames)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_slice_takes_the_portion_between_start_and_end() {
|
||||
assert_eq!(sliced(vec![1., 2., 3., 4., 5.], 1., 3.), [2., 3.]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_slice_resolves_negative_indices_from_the_end() {
|
||||
assert_eq!(sliced(vec![1., 2., 3., 4., 5.], -2., 0.), [4., 5.], "an end of zero reaches through the end of the list");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_slice_yields_nothing_when_start_reaches_end() {
|
||||
assert!(sliced(vec![1., 2., 3., 4., 5.], 3., 3.).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod registry_tests {
|
||||
use super::*;
|
||||
|
||||
/// A carried subject with implementations registers one row per element
|
||||
/// type, each naming its concrete carrier rather than the erased generic.
|
||||
#[test]
|
||||
fn a_carried_subject_registers_concrete_rows() {
|
||||
let entries = _sort_mod::sort_entries();
|
||||
assert_eq!(entries.len(), 15, "one row per implementation");
|
||||
assert_eq!(entries[0].io.inputs[0], core_types::registry::record_source_type::<String>());
|
||||
assert_eq!(entries[3].io.inputs[0], core_types::registry::record_source_type::<f64>());
|
||||
assert_eq!(entries[3].io.return_value, core_types::registry::record_type::<f64>());
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
//! wiring is by hand until the compiler pass constructs layouts.
|
||||
|
||||
use core_types::Ctx;
|
||||
use core_types::attribute::{Attr, EditorLayerPath, Opacity, OwnedAttr, RemoveAttr, Transform};
|
||||
use core_types::attribute::{Attr, Opacity, OwnedAttr, RemoveAttr, Transform};
|
||||
use core_types::context::{DeriveCtx, ExtractIndex, IndexLink, InjectIndex, ModifyIndex};
|
||||
use core_types::extent::{ExtentIn, LevelIn, ListIn, ValueIn};
|
||||
use core_types::gpoll::{ErrorKind, Extent, GPoll, GraphError, Interrupt, Level};
|
||||
@@ -62,11 +62,6 @@ fn repeat_opacity(ctx: impl Ctx + ExtractIndex, element: f64, count: u32) -> ILi
|
||||
emit(element, Attr(ctx.index() as f64))
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Test"))]
|
||||
fn sum(_: impl Ctx, items: IList<f64>) -> f64 {
|
||||
items.into_iter().sum()
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Test"))]
|
||||
fn sum_nested(_: impl Ctx, items: IList<IList<f64>>) -> f64 {
|
||||
items.into_iter().sum()
|
||||
@@ -80,39 +75,6 @@ fn repeat_opacity_extent(element: ExtentIn<'_>, count: ValueIn<'_, u32>, level:
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic structure creator: evaluates the lazy content once per copy with the
|
||||
/// copy's index pushed in, producing a rank level of `count` copies.
|
||||
#[node_macro::node(category("Test"), extent(repeat_extent))]
|
||||
fn repeat<T>(
|
||||
ctx: impl Ctx + DeriveCtx + ExtractIndex,
|
||||
content: impl Node<Context<'_>, Output = T>,
|
||||
#[default(1)]
|
||||
#[hard(1..)]
|
||||
count: u32,
|
||||
reverse: bool,
|
||||
) -> Result<IList<T>, 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 copy = match reverse {
|
||||
true => count as u64 - 1 - copy,
|
||||
false => copy,
|
||||
};
|
||||
let mut frame = IndexLink { index: 0, outer: None };
|
||||
content.eval(&ctx.push_level(&mut frame, copy, rest))
|
||||
}
|
||||
|
||||
/// 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_extent(content: ExtentIn<'_>, count: ValueIn<'_, u32>, _reverse: ValueIn<'_, bool>, level: LevelIn) -> GPoll<Extent> {
|
||||
match level.pushed() {
|
||||
true => count.get().map(|count| Extent::Exactly(count as usize)),
|
||||
false => content.at(level),
|
||||
}
|
||||
}
|
||||
|
||||
/// Test-only lazy-carrier creator: each copy evaluates the content at its own
|
||||
/// index and re-scales the row's opacity by the copy number.
|
||||
#[node_macro::node(category("Test"), extent(repeat_faded_extent))]
|
||||
@@ -136,51 +98,6 @@ fn repeat_faded_extent(content: ExtentIn<'_>, count: ValueIn<'_, u32>, level: Le
|
||||
}
|
||||
}
|
||||
|
||||
/// Rank-model Extend: the output's top level is `base`'s lanes followed by
|
||||
/// `new`'s, each side evaluated within its own index range.
|
||||
#[node_macro::node(category("Test"), extent(extend_extent))]
|
||||
fn extend<T>(ctx: impl Ctx + ExtractIndex + InjectIndex + Copy, base: impl Node<Context<'_>, Output = T>, new: impl Node<Context<'_>, Output = T>) -> Result<T, Interrupt> {
|
||||
let split = match base.extent(ctx, Level::Total) {
|
||||
GPoll::Final(Extent::Exactly(count)) => count as u64,
|
||||
// A scalar side joins the concat as a single lane, per `Extent::sum`.
|
||||
GPoll::Final(Extent::Free) => 1,
|
||||
GPoll::Pending => return Err(Interrupt::Pending),
|
||||
_ => return Err(GraphError::new("extend over a non-exact base extent").into()),
|
||||
};
|
||||
let lane = ctx.index();
|
||||
match lane < split {
|
||||
true => base.eval(ctx),
|
||||
false => {
|
||||
let mut shifted = *ctx;
|
||||
shifted.set_index(lane - split);
|
||||
new.eval(&shifted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The top level sums both sides; inner levels must agree (rectangular), a
|
||||
/// free side defers to the other.
|
||||
fn extend_extent(base: ExtentIn<'_>, new: ExtentIn<'_>, level: LevelIn) -> GPoll<Extent> {
|
||||
match level.top() {
|
||||
true => Extent::sum(base.at(level), new.at(level)),
|
||||
false => base.at(level).zip(new.at(level)).and_then(|extents| match extents {
|
||||
(Extent::Free, other) | (other, Extent::Free) => GPoll::Final(other),
|
||||
(base, new) if base == new => GPoll::Final(base),
|
||||
_ => GPoll::error("extend inner extents differ"),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// The layer-path stamp: writes the owning layer's document node path on each lane.
|
||||
#[node_macro::node(category("Test"))]
|
||||
fn stamp_layer_path<'e, T>(ctx: impl Ctx + ExtractArena<'e>, element: T, path: Vec<NodeId>) -> Result<(T, Attr<'e, EditorLayerPath>), Interrupt> {
|
||||
let (parked, _) = ctx.arena().alloc(path).ok_or(GraphError {
|
||||
kind: ErrorKind::ArenaExhausted,
|
||||
trace: Vec::new(),
|
||||
})?;
|
||||
Ok((element, Attr(parked.as_slice())))
|
||||
}
|
||||
|
||||
/// Resolves a signed index over `total` lanes: negatives count from the end,
|
||||
/// out of range resolves to nothing.
|
||||
fn resolve_index(index: f64, total: u64) -> Option<u64> {
|
||||
@@ -191,36 +108,6 @@ fn resolve_index(index: f64, total: u64) -> Option<u64> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rank-model Omit Element: the top level shrinks by one; lanes at or past
|
||||
/// the omitted index read one lane further. An out-of-range index passes the
|
||||
/// level through unchanged.
|
||||
#[node_macro::node(category("Test"), extent(omit_element_extent))]
|
||||
fn omit_element<T>(ctx: impl Ctx + ModifyIndex + Copy, content: impl Node<Context<'_>, Output = T>, index: f64) -> Result<T, Interrupt> {
|
||||
let total = match content.extent(ctx, Level::Total) {
|
||||
GPoll::Final(Extent::Exactly(count)) => count as u64,
|
||||
GPoll::Pending => return Err(Interrupt::Pending),
|
||||
_ => return Err(GraphError::new("omit over a non-exact extent").into()),
|
||||
};
|
||||
let lane = ctx.index();
|
||||
let source = match resolve_index(index, total) {
|
||||
Some(omitted) if lane >= omitted => lane + 1,
|
||||
_ => lane,
|
||||
};
|
||||
let mut shifted = *ctx;
|
||||
shifted.set_index(source);
|
||||
content.eval(&shifted)
|
||||
}
|
||||
|
||||
fn omit_element_extent(content: ExtentIn<'_>, index: ValueIn<'_, f64>, level: LevelIn) -> GPoll<Extent> {
|
||||
match level.top() {
|
||||
true => index.get().zip(content.at(level)).map(|(index, extent)| match extent {
|
||||
Extent::Exactly(count) if resolve_index(index, count as u64).is_some() => Extent::Exactly(count - 1),
|
||||
extent => extent,
|
||||
}),
|
||||
false => content.at(level),
|
||||
}
|
||||
}
|
||||
|
||||
/// Rank-model Index Elements: a one-lane level holding the item at the index
|
||||
/// with its attributes, or an empty level when the index is out of range.
|
||||
#[node_macro::node(category("Test"), extent(index_elements_extent))]
|
||||
@@ -248,13 +135,6 @@ fn index_elements_extent(content: ExtentIn<'_>, index: ValueIn<'_, f64>, level:
|
||||
}
|
||||
}
|
||||
|
||||
/// Rank-model Extract Element: the bare element at the index, or the element
|
||||
/// type's default when the index is out of range.
|
||||
#[node_macro::node(category("Test"))]
|
||||
fn extract_element(_: impl Ctx + InjectIndex + Copy, list: IList<f64>, index: f64) -> f64 {
|
||||
resolve_index(index, list.len() as u64).map(|resolved| list.get(resolved as usize)).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Rank-model Mirror kernel: the level holds the content's lanes followed by
|
||||
/// reflected copies (or the reflected copies alone), each reflected transform
|
||||
/// mirrored about the level's horizontal center.
|
||||
@@ -455,325 +335,19 @@ fn forward_record<T>(_: impl Ctx, element: T) -> T {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::list::{ExtendNode, ItemAtIndexNode, RemoveAtIndexNode};
|
||||
use core_types::SourceId;
|
||||
use core_types::arena::Arena;
|
||||
use core_types::attribute::Attribute as AttributeMarker;
|
||||
use core_types::context::{ContextImpl, EvalScope, ExtractArena};
|
||||
use core_types::context::{ContextImpl, ExtractArena};
|
||||
use core_types::gpoll::GPoll;
|
||||
use core_types::node::Node;
|
||||
use core_types::record::test_fixtures::*;
|
||||
use core_types::record::{FrameClaim, Layout, LiftedSource, Rec, RecordSource, Served};
|
||||
use core_types::value::ValueSource;
|
||||
|
||||
struct RecordSourceNode<E> {
|
||||
layout: Layout,
|
||||
element: E,
|
||||
fields: Vec<(&'static str, f64)>,
|
||||
partial: bool,
|
||||
}
|
||||
|
||||
impl<C, E: Copy + Send + Sync + dyn_any::StaticTypeSized + 'static> Node<C> for RecordSourceNode<E> {
|
||||
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'e Arena>,
|
||||
{
|
||||
let mut frame = slot;
|
||||
let arena = ExtractArena::arena(input);
|
||||
if frame.element(self.element, arena).is_none() {
|
||||
return GPoll::arena_exhausted();
|
||||
}
|
||||
for (name, field) in &self.fields {
|
||||
write_field_at(&mut frame, &self.layout, name, 0, *field);
|
||||
}
|
||||
// SAFETY: the writes above complete the record of this layout.
|
||||
let served = unsafe { frame.finish_served() };
|
||||
match self.partial {
|
||||
true => GPoll::Partial(served),
|
||||
false => GPoll::Final(served),
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(&self) -> &Layout {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
struct LeveledSourceNode {
|
||||
layout: Layout,
|
||||
elements: Vec<f64>,
|
||||
field: Option<(&'static str, f64)>,
|
||||
}
|
||||
|
||||
impl<C: ExtractIndex> Node<C> for LeveledSourceNode {
|
||||
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'e Arena>,
|
||||
{
|
||||
let element = self.elements[input.innermost_index() as usize % self.elements.len()];
|
||||
let mut frame = slot;
|
||||
let arena = ExtractArena::arena(input);
|
||||
if frame.element(element, arena).is_none() {
|
||||
return GPoll::arena_exhausted();
|
||||
}
|
||||
if let Some((name, value)) = self.field {
|
||||
write_field_at(&mut frame, &self.layout, name, 0, value);
|
||||
}
|
||||
// SAFETY: the writes above complete the record of this layout.
|
||||
GPoll::Final(unsafe { frame.finish_served() })
|
||||
}
|
||||
|
||||
fn extent_at<'x>(&self, _input: &C, _level: u8, _frames: &core_types::record::Frames<'x>) -> GPoll<Extent>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'x Arena>,
|
||||
{
|
||||
GPoll::Final(Extent::Exactly(self.elements.len()))
|
||||
}
|
||||
|
||||
fn layout(&self) -> &Layout {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
struct LeveledTransformSource {
|
||||
layout: Layout,
|
||||
rows: Vec<(f64, DAffine2)>,
|
||||
}
|
||||
|
||||
impl<C: ExtractIndex> Node<C> for LeveledTransformSource {
|
||||
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'e Arena>,
|
||||
{
|
||||
let (element, transform) = self.rows[input.innermost_index() as usize % self.rows.len()];
|
||||
let mut frame = slot;
|
||||
let arena = ExtractArena::arena(input);
|
||||
if frame.element(element, arena).is_none() {
|
||||
return GPoll::arena_exhausted();
|
||||
}
|
||||
write_attr_at::<Transform>(&mut frame, &self.layout, transform);
|
||||
// SAFETY: the writes above complete the record of this layout.
|
||||
GPoll::Final(unsafe { frame.finish_served() })
|
||||
}
|
||||
|
||||
fn extent_at<'x>(&self, _input: &C, _level: u8, _frames: &core_types::record::Frames<'x>) -> GPoll<Extent>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'x Arena>,
|
||||
{
|
||||
GPoll::Final(Extent::Exactly(self.rows.len()))
|
||||
}
|
||||
|
||||
fn layout(&self) -> &Layout {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
/// Serves lanes carrying both a Transform no gather kernel declares and an
|
||||
/// Opacity one does, so a carried column can be told apart from a written one.
|
||||
struct LeveledCarriedSource {
|
||||
layout: Layout,
|
||||
rows: Vec<(f64, DAffine2, f64)>,
|
||||
}
|
||||
|
||||
impl<C: ExtractIndex> Node<C> for LeveledCarriedSource {
|
||||
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'e Arena>,
|
||||
{
|
||||
let (element, transform, opacity) = self.rows[input.innermost_index() as usize % self.rows.len()];
|
||||
let mut frame = slot;
|
||||
let arena = ExtractArena::arena(input);
|
||||
if frame.element(element, arena).is_none() {
|
||||
return GPoll::arena_exhausted();
|
||||
}
|
||||
write_attr_at::<Transform>(&mut frame, &self.layout, transform);
|
||||
write_attr_at::<Opacity>(&mut frame, &self.layout, opacity);
|
||||
// SAFETY: the writes above complete the record of this layout.
|
||||
GPoll::Final(unsafe { frame.finish_served() })
|
||||
}
|
||||
|
||||
fn extent_at<'x>(&self, _input: &C, _level: u8, _frames: &core_types::record::Frames<'x>) -> GPoll<Extent>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'x Arena>,
|
||||
{
|
||||
GPoll::Final(Extent::Exactly(self.rows.len()))
|
||||
}
|
||||
|
||||
fn layout(&self) -> &Layout {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
/// A leveled source that keeps its count to itself: the extent is a lower
|
||||
/// bound and lanes past the data answer the past-end signal.
|
||||
struct DrainSourceNode {
|
||||
layout: Layout,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl<C: ExtractIndex> Node<C> for DrainSourceNode {
|
||||
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'e Arena>,
|
||||
{
|
||||
let lane = input.innermost_index();
|
||||
if lane >= self.count as u64 {
|
||||
return GPoll::past_end();
|
||||
}
|
||||
let mut frame = slot;
|
||||
let arena = ExtractArena::arena(input);
|
||||
if frame.element(lane as f64, arena).is_none() {
|
||||
return GPoll::arena_exhausted();
|
||||
}
|
||||
// SAFETY: the writes above complete the record of this layout.
|
||||
GPoll::Final(unsafe { frame.finish_served() })
|
||||
}
|
||||
|
||||
fn extent_at<'x>(&self, _input: &C, _level: u8, _frames: &core_types::record::Frames<'x>) -> GPoll<Extent>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'x Arena>,
|
||||
{
|
||||
GPoll::Final(Extent::AtLeast(0))
|
||||
}
|
||||
|
||||
fn layout(&self) -> &Layout {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
struct IndexSourceNode {
|
||||
layout: Layout,
|
||||
}
|
||||
|
||||
impl<C: ExtractIndex + core_types::context::ExtractIndices> Node<C> for IndexSourceNode {
|
||||
fn serve<'e, 'l>(&self, input: &C, slot: FrameClaim<'e, 'l>) -> GPoll<Served<'e>>
|
||||
where
|
||||
C: ExtractArena<ArenaRef = &'e Arena>,
|
||||
{
|
||||
// Depth-0 content varying per copy: the enclosing (pushed) level's
|
||||
// index sits one link above the content's own innermost lane.
|
||||
let element = input.try_index().and_then(|mut indices| indices.nth(1)).unwrap_or(0) as f64;
|
||||
let mut frame = slot;
|
||||
let arena = ExtractArena::arena(input);
|
||||
if frame.element(element, arena).is_none() {
|
||||
return GPoll::arena_exhausted();
|
||||
}
|
||||
// SAFETY: the writes above complete the record of this layout.
|
||||
GPoll::Final(unsafe { frame.finish_served() })
|
||||
}
|
||||
|
||||
fn layout(&self) -> &Layout {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes a field at the layout's resolved offset, the wiring-proven pairing
|
||||
/// a generated node performs.
|
||||
fn write_field_at<T: Copy + 'static>(frame: &mut FrameClaim<'_, '_>, layout: &Layout, name: &str, level: u8, value: T) {
|
||||
let field = layout
|
||||
.fields
|
||||
.iter()
|
||||
.find(|field| field.name == name && field.level == level)
|
||||
.expect("the layout carries the written field");
|
||||
assert_eq!(field.type_id, std::any::TypeId::of::<T>(), "the field was declared at this value type");
|
||||
// SAFETY: the offset is this layout's own, at the field's declared type.
|
||||
unsafe { frame.attr_at(field.offset, value) };
|
||||
}
|
||||
|
||||
/// [`write_field_at`] for a census marker at level 0.
|
||||
fn write_attr_at<A: core_types::attribute::Attribute>(frame: &mut FrameClaim<'_, '_>, layout: &Layout, value: A::Value<'static>)
|
||||
where
|
||||
A::Value<'static>: Copy + 'static,
|
||||
{
|
||||
write_field_at(frame, layout, A::NAME, 0, value);
|
||||
}
|
||||
fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
|
||||
EvalScope::new(Some(0.5), None, None, generations, arena)
|
||||
}
|
||||
|
||||
fn f64_layout(names: &[&'static str]) -> Layout {
|
||||
let writes: Vec<core_types::record::FieldWrite> = names
|
||||
.iter()
|
||||
.map(|name| core_types::record::FieldWrite {
|
||||
name,
|
||||
level: 0,
|
||||
size: 8,
|
||||
align: 8,
|
||||
type_id: std::any::TypeId::of::<f64>(),
|
||||
read_erased: <Opacity as AttributeMarker>::read_erased,
|
||||
repark: None,
|
||||
content_hash: None,
|
||||
content_eq: None,
|
||||
})
|
||||
.collect();
|
||||
Layout::default().with_writes(0, core_types::record::element_write::<f64>(), &writes)
|
||||
}
|
||||
|
||||
fn leveled_f64_layout(names: &[&'static str]) -> Layout {
|
||||
let writes: Vec<core_types::record::FieldWrite> = names
|
||||
.iter()
|
||||
.map(|name| core_types::record::FieldWrite {
|
||||
name,
|
||||
level: 0,
|
||||
size: 8,
|
||||
align: 8,
|
||||
type_id: std::any::TypeId::of::<f64>(),
|
||||
read_erased: <Opacity as AttributeMarker>::read_erased,
|
||||
repark: None,
|
||||
content_hash: None,
|
||||
content_eq: None,
|
||||
})
|
||||
.collect();
|
||||
Layout::default().with_writes(1, core_types::record::element_write::<f64>(), &writes)
|
||||
}
|
||||
|
||||
fn frames_for(layouts: &[&Layout]) -> core_types::record::Frames<'static> {
|
||||
core_types::record::test_frames(layouts.iter().map(|layout| layout.frame_bytes()).sum::<usize>().max(1 << 12))
|
||||
}
|
||||
|
||||
fn install<N: Node<ContextImpl<'static>>>(mut node: N, meta: core_types::record::LayoutMeta, inputs: &[Option<&Layout>]) -> N {
|
||||
// The fixtures wire constants into every eager input, which the compiler
|
||||
// pass records as lane-invariant.
|
||||
let resolved = core_types::record::RecordLayout {
|
||||
named_writes: Vec::new(),
|
||||
named_reads: Vec::new(),
|
||||
named_read_defaults: Vec::new(),
|
||||
lane_invariant: u32::MAX,
|
||||
..meta.resolve(inputs)
|
||||
};
|
||||
<N as Node<ContextImpl<'static>>>::set_layout(&mut node, resolved);
|
||||
node
|
||||
}
|
||||
|
||||
fn install_flip<N: Node<ContextImpl<'static>>>(mut node: N, layout: &Layout) -> N {
|
||||
let bundle = core_types::record::RecordLayout {
|
||||
named_writes: Vec::new(),
|
||||
named_reads: Vec::new(),
|
||||
named_read_defaults: Vec::new(),
|
||||
frame_bytes: layout.frame_bytes(),
|
||||
plan: Vec::new(),
|
||||
layout: layout.clone(),
|
||||
lane_invariant: u32::MAX,
|
||||
};
|
||||
<N as Node<ContextImpl<'static>>>::set_layout(&mut node, bundle);
|
||||
node
|
||||
}
|
||||
|
||||
fn lifted_value<T: Clone + Send + Sync + core_types::StaticTypeSized + 'static>(value: T) -> (ValueSource<T>, Layout)
|
||||
where
|
||||
T::Static: Clone + Send + Sync,
|
||||
{
|
||||
let lift = ValueSource::new(value);
|
||||
let layout = Node::<ContextImpl>::layout(&lift).clone();
|
||||
(lift, layout)
|
||||
}
|
||||
|
||||
fn bare_source(layout: &Layout, element: f64) -> RecordSourceNode<f64> {
|
||||
RecordSourceNode {
|
||||
layout: layout.clone(),
|
||||
element,
|
||||
fields: vec![],
|
||||
partial: false,
|
||||
}
|
||||
}
|
||||
use graphic_nodes::graphic::{StampLayerPathNode, stamp_layer_path_layout_meta};
|
||||
use math_nodes::{SumNode, sum_layout_meta};
|
||||
use repeat_nodes::repeat_nodes::RepeatNode;
|
||||
|
||||
#[test]
|
||||
fn creator_pushes_a_rank_level() {
|
||||
@@ -1405,7 +979,7 @@ mod tests {
|
||||
};
|
||||
let (index_edge, index_layout) = lifted_value(index);
|
||||
install(
|
||||
OmitElementNode::new(RecordSource::new(content, &layout, &layout), index_edge, &layout, &index_layout),
|
||||
RemoveAtIndexNode::new(RecordSource::new(content, &layout, &layout), index_edge, &layout, &index_layout),
|
||||
meta(),
|
||||
&[Some(&layout)],
|
||||
)
|
||||
@@ -1495,7 +1069,10 @@ mod tests {
|
||||
field: None,
|
||||
};
|
||||
let (index_edge, index_layout) = lifted_value(index);
|
||||
let node = install_flip(ExtractElementNode::new(RecordSource::new(content, &layout, &layout), index_edge, &layout, &index_layout), &out);
|
||||
let node = install_flip(
|
||||
ItemAtIndexNode::<_, _, f64>::new(RecordSource::new(content, &layout, &layout), index_edge, &layout, &index_layout),
|
||||
&out,
|
||||
);
|
||||
let GPoll::Final(served) = core_types::record::capture(&node, &ctx, &frames) else {
|
||||
panic!("expected a final record");
|
||||
};
|
||||
|
||||
@@ -17,3 +17,7 @@ dyn-any = { workspace = true }
|
||||
glam = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
node-macro = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# The map tests drive the generic Map over graphic rows
|
||||
graphene-core = { workspace = true }
|
||||
|
||||
@@ -2,17 +2,17 @@ use crate::record::Inherited;
|
||||
use core_types::arena::Arena;
|
||||
use core_types::attribute::{Attr, EditorLayerPath, Name0, Named, Opacity, OpacityFill, Transform as TransformAttr, WireValue};
|
||||
use core_types::bounds::{BoundingBox, RenderBoundingBox};
|
||||
use core_types::extent::{ExtentIn, LevelIn, ListIn, ValueIn};
|
||||
use core_types::gpoll::{ErrorKind, Extent, GPoll, GraphError, Interrupt, Level};
|
||||
use core_types::extent::{LevelIn, ListIn, ValueIn};
|
||||
use core_types::gpoll::{ErrorKind, Extent, GPoll, GraphError, Interrupt};
|
||||
use core_types::list::List;
|
||||
use core_types::node::Lane;
|
||||
use core_types::registry::types::{Angle, SignedInteger};
|
||||
use core_types::registry::types::Angle;
|
||||
use core_types::uuid::NodeId;
|
||||
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, CacheHash, Color, Ctx, DeriveCtx, ExtractIndex, InjectIndex, ModifyIndex};
|
||||
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color, Ctx, ExtractIndex, InjectIndex};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphic_types::graphic::{Graphic, GraphicLevel, RowStep, TryFromGraphic, walk_vector_rows};
|
||||
use graphic_types::markers::{EditorMergedLayers, Fill, Stroke as StrokeAttr};
|
||||
use graphic_types::{ATTR_FILL, ATTR_STROKE, Artboard, Vector};
|
||||
use graphic_types::{ATTR_FILL, ATTR_STROKE, Vector};
|
||||
use raster_types::{CPU, GPU, Raster};
|
||||
use vector_types::gradient::{GradientSpreadMethod, GradientType as GradientTypeValue};
|
||||
use vector_types::{Gradient, GradientStop, ReferencePoint};
|
||||
@@ -25,90 +25,6 @@ fn arena_exhausted() -> Interrupt {
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Resolves a signed index over `total` lanes: negatives count from the end,
|
||||
/// out of range resolves to nothing.
|
||||
fn resolve_index(index: f64, total: u64) -> Option<u64> {
|
||||
let index = index as i64;
|
||||
match index < 0 {
|
||||
true => total.checked_sub(index.unsigned_abs()),
|
||||
false => ((index as u64) < total).then_some(index as u64),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the list with the item at the specified index removed.
|
||||
/// If no value exists at that index, the list is returned unchanged.
|
||||
#[node_macro::node(category("General"), name("Remove at Index"), extent(omit_element_extent))]
|
||||
pub fn remove_at_index<T>(
|
||||
ctx: impl Ctx + ModifyIndex + Copy,
|
||||
/// The list of data.
|
||||
list: impl Node<Context<'_>, Output = T>,
|
||||
/// The index of the item to remove, starting from 0 for the first item. Negative indices count backwards from the end of the list, starting from -1 for the last item.
|
||||
index: SignedInteger,
|
||||
) -> Result<T, Interrupt> {
|
||||
let total = match list.extent(ctx, Level::Total) {
|
||||
GPoll::Final(Extent::Exactly(count)) => count as u64,
|
||||
GPoll::Pending => return Err(Interrupt::Pending),
|
||||
_ => return Err(GraphError::new("omit over a non-exact extent").into()),
|
||||
};
|
||||
let lane = ctx.index();
|
||||
let source = match resolve_index(index, total) {
|
||||
Some(omitted) if lane >= omitted => lane + 1,
|
||||
_ => lane,
|
||||
};
|
||||
let mut shifted = *ctx;
|
||||
shifted.set_index(source);
|
||||
list.eval(&shifted)
|
||||
}
|
||||
|
||||
fn omit_element_extent(list: ExtentIn<'_>, index: ValueIn<'_, f64>, level: LevelIn) -> GPoll<Extent> {
|
||||
match level.top() {
|
||||
true => index.get().zip(list.at(level)).map(|(index, extent)| match extent {
|
||||
Extent::Exactly(count) if resolve_index(index, count as u64).is_some() => Extent::Exactly(count - 1),
|
||||
extent => extent,
|
||||
}),
|
||||
false => list.at(level),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the bare element (without the item's attributes) at the specified index in a `List`.
|
||||
/// Use this when downstream nodes want just the inner value rather than a `List` containing a single item.
|
||||
/// If no value exists at that index, the element type's default is returned.
|
||||
#[node_macro::node(category("General"), name("Item at Index"))]
|
||||
pub fn item_at_index<T: Clone + Default + Send + Sync + CacheHash + 'static>(
|
||||
_: impl Ctx,
|
||||
/// The `List` of data to extract from.
|
||||
#[implementations(String, f64, NodeId, Color, Gradient, Vector, Raster<CPU>, Graphic, Artboard)]
|
||||
list: IList<T>,
|
||||
/// The index of the item to retrieve, starting from 0 for the first item. Negative indices count backwards from the end of the list, starting from -1 for the last item.
|
||||
index: SignedInteger,
|
||||
) -> T {
|
||||
resolve_index(index, list.len() as u64).map(|resolved| list.element_ref(resolved as usize).clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// One subgraph invocation per content row, the row riding as a vararg, with
|
||||
/// the subgraph's lanes concatenated into one flat level. The level reports a
|
||||
/// lower bound; consumers drain to the past-end signal.
|
||||
#[node_macro::node(category("General"))]
|
||||
pub fn map<Row: Clone + Send + Sync + CacheHash + 'static, T>(
|
||||
ctx: impl Ctx + DeriveCtx + ExtractIndex + InjectIndex + Copy,
|
||||
#[implementations(Graphic, Vector, Raster<CPU>, Color, Gradient, String)] content: IList<Row>,
|
||||
mapped: impl Node<Context<'_>, Output = IList<T>>,
|
||||
) -> Result<IList<T>, Interrupt> {
|
||||
let mut remaining = ctx.index();
|
||||
for row in 0..content.len() {
|
||||
let item = crate::record::vararg_row(content, row);
|
||||
let scoped = ctx.push_vararg(&item);
|
||||
let lanes = mapped.inner_extent_at(&scoped.ctx(), row as u64)?;
|
||||
if remaining >= lanes {
|
||||
remaining -= lanes;
|
||||
continue;
|
||||
}
|
||||
let mut frame = core_types::context::IndexLink { index: 0, outer: None };
|
||||
return mapped.eval(&scoped.ctx().push_level(&mut frame, row as u64, remaining));
|
||||
}
|
||||
Err(GraphError::past_end().into())
|
||||
}
|
||||
|
||||
/// The reflection transform the mirror applies, or nothing when the content
|
||||
/// has no rectangular bounds (the legacy passthrough case).
|
||||
fn mirror_reflection<T>(legacy: &List<T>, relative_to_bounds: ReferencePoint, offset: f64, angle: f64) -> Option<DAffine2>
|
||||
@@ -261,7 +177,6 @@ fn mirror_vector_extent(
|
||||
}
|
||||
}
|
||||
|
||||
pub use _map_mod::map_entries;
|
||||
pub use _mirror_vector_mod::mirror_vector_entries;
|
||||
|
||||
/// `node_path` with its trailing entry dropped: the containing network's path, which is also a unique
|
||||
@@ -344,57 +259,6 @@ attribute_reads! {
|
||||
read_spread_method_attribute: GradientSpreadMethod => GradientSpreadMethod;
|
||||
}
|
||||
|
||||
/// Joins two levels of the same type, the base's lanes followed by the new's.
|
||||
#[node_macro::node(category("General"), extent(extend_extent))]
|
||||
pub fn extend<T>(
|
||||
ctx: impl Ctx + ExtractIndex + InjectIndex + Copy,
|
||||
/// The input whose lanes appear at the start of the extended level.
|
||||
base: impl Node<Context<'_>, Output = T>,
|
||||
/// The input whose lanes appear at the end of the extended level.
|
||||
#[expose]
|
||||
new: impl Node<Context<'_>, Output = T>,
|
||||
) -> Result<T, Interrupt> {
|
||||
let split = match base.extent(ctx, Level::Total) {
|
||||
GPoll::Final(Extent::Exactly(count)) => count as u64,
|
||||
// A scalar side joins the concat as a single lane, per `Extent::sum`.
|
||||
GPoll::Final(Extent::Free) => 1,
|
||||
GPoll::Pending => return Err(Interrupt::Pending),
|
||||
_ => return Err(GraphError::new("extend over a non-exact base extent").into()),
|
||||
};
|
||||
let lane = ctx.index();
|
||||
match lane < split {
|
||||
true => base.eval(ctx),
|
||||
false => {
|
||||
let mut shifted = *ctx;
|
||||
shifted.set_index(lane - split);
|
||||
new.eval(&shifted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The top level sums both sides; inner levels must agree (rectangular), a
|
||||
/// free side or a side with no top-level lanes defers to the other.
|
||||
fn extend_extent(base: ExtentIn<'_>, new: ExtentIn<'_>, level: LevelIn) -> GPoll<Extent> {
|
||||
match level.top() {
|
||||
true => Extent::sum(base.at(level), new.at(level)),
|
||||
false => base.at(level).zip(new.at(level)).and_then(|extents| match extents {
|
||||
(Extent::Free, other) | (other, Extent::Free) => GPoll::Final(other),
|
||||
(base_inner, new_inner) if base_inner == new_inner => GPoll::Final(base_inner),
|
||||
(base_inner, new_inner) => {
|
||||
let top = LevelIn {
|
||||
level: level.depth - 1,
|
||||
depth: level.depth,
|
||||
};
|
||||
match (base.at(top), new.at(top)) {
|
||||
(GPoll::Final(Extent::Exactly(0)), _) => GPoll::Final(new_inner),
|
||||
(_, GPoll::Final(Extent::Exactly(0))) => GPoll::Final(base_inner),
|
||||
_ => GPoll::error("extend inner extents differ"),
|
||||
}
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Nests the input graphical content in a wrapper graphic. This essentially "groups" the input.
|
||||
/// The wrapped run keeps the level's element type, so the legacy boundary can
|
||||
/// lower a wrapped vector level to the bare typed graphic the pre-flip wrap made.
|
||||
|
||||
@@ -223,15 +223,16 @@ fn flatten_levels_extent(content: ExtentIn<'_>, level: LevelIn) -> GPoll<Extent>
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::graphic::{ColorsToGradientNode, FlattenColorNode, FlattenGraphicNode, MapNode, WrapGraphicNode, flatten_color_layout_meta, flatten_graphic_layout_meta, wrap_graphic_layout_meta};
|
||||
use core_types::SourceId;
|
||||
use crate::graphic::{ColorsToGradientNode, FlattenColorNode, FlattenGraphicNode, WrapGraphicNode, flatten_color_layout_meta, flatten_graphic_layout_meta, wrap_graphic_layout_meta};
|
||||
use core_types::arena::Arena;
|
||||
use core_types::attribute::Attribute as AttributeMarker;
|
||||
use core_types::context::{ContextImpl, EvalScope, ExtractArena};
|
||||
use core_types::context::{ContextImpl, ExtractArena};
|
||||
use core_types::list::{Item, List};
|
||||
use core_types::node::Node;
|
||||
use core_types::record::test_fixtures::*;
|
||||
use core_types::record::{self, FrameClaim, Layout, RecordSource, Served};
|
||||
use core_types::value::ValueSource;
|
||||
use graphene_core::list::{MapNode, map_entries};
|
||||
|
||||
struct GraphicSource {
|
||||
layout: Layout,
|
||||
@@ -265,59 +266,6 @@ mod tests {
|
||||
&self.layout
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes a field at the layout's resolved offset, the wiring-proven pairing
|
||||
/// a generated node performs.
|
||||
fn write_field_at<T: Copy + 'static>(frame: &mut FrameClaim<'_, '_>, layout: &Layout, name: &str, level: u8, value: T) {
|
||||
let field = layout
|
||||
.fields
|
||||
.iter()
|
||||
.find(|field| field.name == name && field.level == level)
|
||||
.expect("the layout carries the written field");
|
||||
assert_eq!(field.type_id, std::any::TypeId::of::<T>(), "the field was declared at this value type");
|
||||
// SAFETY: the offset is this layout's own, at the field's declared type.
|
||||
unsafe { frame.attr_at(field.offset, value) };
|
||||
}
|
||||
|
||||
/// [`write_field_at`] for a census marker at level 0.
|
||||
fn write_attr_at<A: core_types::attribute::Attribute>(frame: &mut FrameClaim<'_, '_>, layout: &Layout, value: A::Value<'static>)
|
||||
where
|
||||
A::Value<'static>: Copy + 'static,
|
||||
{
|
||||
write_field_at(frame, layout, A::NAME, 0, value);
|
||||
}
|
||||
fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
|
||||
EvalScope::new(Some(0.5), None, None, generations, arena)
|
||||
}
|
||||
|
||||
fn install<N: Node<ContextImpl<'static>>>(mut node: N, meta: record::LayoutMeta, inputs: &[Option<&Layout>]) -> N {
|
||||
// The fixtures wire constants into every eager input, which the compiler
|
||||
// pass records as lane-invariant.
|
||||
let resolved = record::RecordLayout {
|
||||
named_writes: Vec::new(),
|
||||
named_reads: Vec::new(),
|
||||
named_read_defaults: Vec::new(),
|
||||
lane_invariant: u32::MAX,
|
||||
..meta.resolve(inputs)
|
||||
};
|
||||
<N as Node<ContextImpl<'static>>>::set_layout(&mut node, resolved);
|
||||
node
|
||||
}
|
||||
|
||||
fn install_flip<N: Node<ContextImpl<'static>>>(mut node: N, layout: &Layout) -> N {
|
||||
let bundle = record::RecordLayout {
|
||||
named_writes: Vec::new(),
|
||||
named_reads: Vec::new(),
|
||||
named_read_defaults: Vec::new(),
|
||||
frame_bytes: layout.frame_bytes(),
|
||||
plan: Vec::new(),
|
||||
layout: layout.clone(),
|
||||
lane_invariant: u32::MAX,
|
||||
};
|
||||
<N as Node<ContextImpl<'static>>>::set_layout(&mut node, bundle);
|
||||
node
|
||||
}
|
||||
|
||||
fn graphic_layout() -> Layout {
|
||||
Layout::default().with_writes(1, record::element_write_hashed::<Graphic>(), &[record::FieldWrite::of::<Transform>(0)])
|
||||
}
|
||||
@@ -568,7 +516,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn flat_map_registers_one_row_per_content_type() {
|
||||
let entries = crate::graphic::map_entries();
|
||||
let entries = map_entries();
|
||||
assert_eq!(entries.len(), 6, "one registry row per content implementation");
|
||||
let content_types: Vec<core_types::Type> = entries.iter().map(|entry| entry.io.inputs[0].clone()).collect();
|
||||
assert_eq!(content_types[0], core_types::registry::record_source_type::<Graphic>());
|
||||
|
||||
@@ -82,6 +82,12 @@ pub mod context {
|
||||
pub use graphene_core::context::*;
|
||||
}
|
||||
|
||||
// The list types beside the generic level kernels over them.
|
||||
pub mod list {
|
||||
pub use core_types::list::*;
|
||||
pub use graphene_core::list::*;
|
||||
}
|
||||
|
||||
// Re-export graphene_core modules for backward compatibility
|
||||
pub mod ops {
|
||||
pub use core_types::ops::*;
|
||||
|
||||
@@ -127,15 +127,6 @@ pub struct ShapedRows {
|
||||
|
||||
type ShapedCache = std::sync::Arc<std::sync::Mutex<Option<ShapedRows>>>;
|
||||
|
||||
/// The lane-normalized cache key and arena generation of one evaluation.
|
||||
macro_rules! eval_key {
|
||||
($ctx:expr) => {{
|
||||
let mut keyed = *$ctx;
|
||||
InjectIndex::set_index(&mut keyed, 0);
|
||||
(core_types::registry::cache_key(&keyed), $ctx.arena().generation())
|
||||
}};
|
||||
}
|
||||
|
||||
/// The `lane`-th path over all the strings' shaped rows, carrying its
|
||||
/// string's columns with the composed transform overriding. `key` and
|
||||
/// `generation` scope the cache to one evaluation.
|
||||
@@ -186,7 +177,7 @@ fn text_to_vector<'e>(
|
||||
strings: IList<String>,
|
||||
#[data] shaped: ShapedCache,
|
||||
) -> Result<IList<(Lane<Vector>, Attr<'e, TransformAttr>)>, Interrupt> {
|
||||
shaped_lane(strings, ctx.index() as usize, eval_key!(ctx), shaped, false)
|
||||
shaped_lane(strings, ctx.index() as usize, core_types::registry::eval_key(ctx), shaped, false)
|
||||
}
|
||||
|
||||
fn text_to_vector_extent(strings: ListIn<'_, String>, level: LevelIn) -> GPoll<Extent> {
|
||||
@@ -201,7 +192,7 @@ fn text_to_vector_glyphs<'e>(
|
||||
strings: IList<String>,
|
||||
#[data] shaped: ShapedCache,
|
||||
) -> Result<IList<(Lane<Vector>, Attr<'e, TransformAttr>)>, Interrupt> {
|
||||
shaped_lane(strings, ctx.index() as usize, eval_key!(ctx), shaped, true)
|
||||
shaped_lane(strings, ctx.index() as usize, core_types::registry::eval_key(ctx), shaped, true)
|
||||
}
|
||||
|
||||
fn text_to_vector_glyphs_extent(strings: ListIn<'_, String>, level: LevelIn) -> GPoll<Extent> {
|
||||
|
||||
@@ -639,6 +639,45 @@ fn binary_gcd<T: num_traits::int::PrimInt + std::ops::ShrAssign<i32> + std::ops:
|
||||
a << shift
|
||||
}
|
||||
|
||||
/// Adds together all the numbers in the input list, producing their total.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
pub fn sum(_: impl Ctx, values: IList<f64>) -> f64 {
|
||||
values.iter().sum()
|
||||
}
|
||||
|
||||
/// Averages all the numbers in the input list. An empty list gives 0.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn average(_: impl Ctx, values: IList<f64>) -> f64 {
|
||||
let count = values.len();
|
||||
let average = if count == 0 { 0. } else { values.iter().sum::<f64>() / count as f64 };
|
||||
|
||||
average
|
||||
}
|
||||
|
||||
/// Gives the smallest number in the input list. An empty list gives 0.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn minimum(_: impl Ctx, values: IList<f64>) -> f64 {
|
||||
values.iter().reduce(f64::min).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Gives the largest number in the input list. An empty list gives 0.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn maximum(_: impl Ctx, values: IList<f64>) -> f64 {
|
||||
values.iter().reduce(f64::max).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Outputs true if at least one value in the input list is true. An empty list gives false.
|
||||
#[node_macro::node(category("Math: Logic"))]
|
||||
fn any(_: impl Ctx, values: IList<bool>) -> bool {
|
||||
values.iter().any(|value| value)
|
||||
}
|
||||
|
||||
/// Outputs true only if every value in the input list is true. An empty list gives true.
|
||||
#[node_macro::node(category("Math: Logic"))]
|
||||
fn all(_: impl Ctx, values: IList<bool>) -> bool {
|
||||
values.iter().all(|value| value)
|
||||
}
|
||||
|
||||
/// The less-than operation (`<`) compares two values and returns true if the first value is less than the second, or false if it is not.
|
||||
/// If enabled with *Or Equal*, the less-than-or-equal operation (`<=`) is used instead.
|
||||
#[node_macro::node(category("Math: Logic"))]
|
||||
|
||||
@@ -12,7 +12,7 @@ use graphic_types::Vector;
|
||||
/// producing a level of `count` copies.
|
||||
// Someday this node can have the option to generate infinitely instead of a fixed count (basically `std::iter::repeat`).
|
||||
#[node_macro::node(category("Repeat"), extent(repeat_extent))]
|
||||
fn repeat<T>(
|
||||
pub fn repeat<T>(
|
||||
ctx: impl Ctx + DeriveCtx + ExtractIndex,
|
||||
content: impl Node<Context<'_>, Output = T>,
|
||||
#[default(1)]
|
||||
|
||||
Reference in New Issue
Block a user