mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-21 02:38:12 +08:00
New nodes: Sum, Average, Minimum, Maximum, Any, All (#4344)
Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
co-authored by
Dennis Kobert
parent
04d6c0d5cf
commit
20eb5ccb8a
@@ -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>());
|
||||
|
||||
Reference in New Issue
Block a user