Convert the node catalog to rank-polymorphic kernels, materialize stored values as ranked wires, and display wire rank in the graph

Co-authored-by:    Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
Keavon Chambers
2026-07-20 21:27:13 -07:00
committed by Dennis Kobert
parent 83cfd0225a
commit 04d6c0d5cf
55 changed files with 1781 additions and 995 deletions

View File

@@ -82,8 +82,7 @@ fn brush_stamp_generator(_: impl Ctx, #[unit(" px")] diameter: f64, color: Color
}
/// Used to efficiently paint brush strokes. Applies the same texture repeatedly at different positions with proper blending and boundary handling.
#[node_macro::node(category(""), skip_impl)]
fn blit<BlendFn: Fn(Color, Color) -> Color>(_: impl Ctx, mut target: List<Raster<CPU>>, texture: Raster<CPU>, positions: Vec<DVec2>, blend_mode: BlendFn) -> List<Raster<CPU>> {
fn blit<BlendFn: Fn(Color, Color) -> Color>(mut target: List<Raster<CPU>>, texture: Raster<CPU>, positions: Vec<DVec2>, blend_mode: BlendFn) -> List<Raster<CPU>> {
if positions.is_empty() {
return target;
}
@@ -340,7 +339,7 @@ fn brush_core(list_item: Item<Raster<CPU>>, strokes: Vec<BrushStroke>, cache: &B
List::new_from_item(item)
};
let list = blit(&(), blit_target, brush_texture, positions, |a, b| blend_colors(a, b, BlendMode::Normal, 1.));
let list = blit(blit_target, brush_texture, positions, |a, b| blend_colors(a, b, BlendMode::Normal, 1.));
assert_eq!(list.len(), 1);
list.into_iter().next().unwrap_or_default()
};
@@ -376,12 +375,10 @@ fn brush_core(list_item: Item<Raster<CPU>>, strokes: Vec<BrushStroke>, cache: &B
_ => BlendMode::Restore,
};
erase_restore_mask = blit(&(), List::new_from_item(erase_restore_mask), brush_texture, positions, move |a, b| {
blend_colors(a, b, mask_blend_mode, 1.)
})
.into_iter()
.next()
.unwrap_or_default();
erase_restore_mask = blit(List::new_from_item(erase_restore_mask), brush_texture, positions, move |a, b| blend_colors(a, b, mask_blend_mode, 1.))
.into_iter()
.next()
.unwrap_or_default();
}
actual_image = blend_image_closure(erase_restore_mask, actual_image, |a, b| blend_colors(a, b, BlendMode::MultiplyAlpha, 1.));
@@ -458,6 +455,7 @@ pub fn blend_stamp_closure(foreground: BrushStampGenerator<Color>, mut backgroun
#[cfg(test)]
mod test {
use super::*;
use crate::brush_stroke::BrushStroke;
use core_types::transform::Transform;
use glam::DAffine2;

View File

@@ -1,5 +1,4 @@
use core_types::gpoll::GPoll;
use core_types::list::List;
use core_types::transform::Footprint;
use core_types::{CacheHash, Color, Context, Ctx, DeriveCtx, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime};
use glam::{DAffine2, DVec2};
@@ -74,15 +73,13 @@ fn quantize_real_time<T>(
Context -> DAffine2,
Context -> Footprint,
Context -> DVec2,
Context -> List<Vector>,
Context -> List<Graphic>,
Context -> List<Raster<CPU>>,
Context -> List<Raster<GPU>>,
Context -> List<Color>,
Context -> List<Artboard>,
Context -> List<Gradient>,
Context -> List<String>,
Context -> List<f64>,
Context -> Vector,
Context -> Graphic,
Context -> Raster<CPU>,
Context -> Raster<GPU>,
Context -> Color,
Context -> Gradient,
Context -> Artboard,
Context -> (),
)]
value: impl Node<Context<'_>, Output = T>,
@@ -114,15 +111,13 @@ fn quantize_animation_time<T>(
Context -> DAffine2,
Context -> Footprint,
Context -> DVec2,
Context -> List<Vector>,
Context -> List<Graphic>,
Context -> List<Raster<CPU>>,
Context -> List<Raster<GPU>>,
Context -> List<Color>,
Context -> List<Artboard>,
Context -> List<Gradient>,
Context -> List<String>,
Context -> List<f64>,
Context -> Vector,
Context -> Graphic,
Context -> Raster<CPU>,
Context -> Raster<GPU>,
Context -> Color,
Context -> Gradient,
Context -> Artboard,
Context -> (),
)]
value: impl Node<Context<'_>, Output = T>,

View File

@@ -7,46 +7,6 @@ use graphic_types::vector_types::Gradient;
use graphic_types::{Graphic, Vector};
use raster_types::{CPU, Raster};
#[node_macro::node(category("Context"), path(graphene_core::vector))]
fn read_graphic(ctx: impl Ctx + ExtractVarArgs) -> List<Graphic<'static>> {
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
let var_arg = var_arg as &dyn std::any::Any;
var_arg.downcast_ref().cloned().unwrap_or_default()
}
#[node_macro::node(category("Context"), path(graphene_core::vector))]
fn read_vector(ctx: impl Ctx + ExtractVarArgs) -> List<Vector> {
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
let var_arg = var_arg as &dyn std::any::Any;
var_arg.downcast_ref().cloned().unwrap_or_default()
}
#[node_macro::node(category("Context"), path(graphene_core::vector))]
fn read_raster(ctx: impl Ctx + ExtractVarArgs) -> List<Raster<CPU>> {
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
let var_arg = var_arg as &dyn std::any::Any;
var_arg.downcast_ref().cloned().unwrap_or_default()
}
#[node_macro::node(category("Context"), path(graphene_core::vector))]
fn read_color(ctx: impl Ctx + ExtractVarArgs) -> List<Color> {
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
let var_arg = var_arg as &dyn std::any::Any;
var_arg.downcast_ref().cloned().unwrap_or_default()
}
#[node_macro::node(category("Context"), path(graphene_core::vector))]
fn read_gradient(ctx: impl Ctx + ExtractVarArgs) -> List<Gradient> {
let Ok(var_arg) = ctx.vararg(0) else { return Default::default() };
let var_arg = var_arg as &dyn std::any::Any;
var_arg.downcast_ref().cloned().unwrap_or_default()
}
/// The mapped row riding as vararg 0, in the production single-item shape.
fn vararg_list<T: 'static>(ctx: &impl ExtractVarArgs) -> Option<&List<T>> {
let arg = ctx.vararg(0).ok()?;
@@ -69,54 +29,30 @@ fn vararg_element<T: Clone + 'static>(ctx: &(impl ExtractVarArgs + ExtractIndex)
.ok_or_else(|| GraphError::new("vararg row addressed past its items").into())
}
/// Rank-model vararg source: the mapped row's items as lanes, elements only.
#[node_macro::node(category("Test"), extent_raw(read_graphic_row_extent))]
pub fn read_graphic_row(ctx: impl Ctx + ExtractVarArgs + ExtractIndex) -> Result<IList<Graphic<'static>>, Interrupt> {
vararg_element(ctx)
// The vararg readers: the mapped row's items as the lanes of a level, elements
// only, one node per element type since a reader names its type.
macro_rules! vararg_readers {
($($node:ident / $extent:ident / $node_type:ident: $element:ty;)*) => {
$(
#[node_macro::node(category("Context"), path(graphene_core::vector), extent_raw($extent))]
pub fn $node(ctx: impl Ctx + ExtractVarArgs + ExtractIndex, _primary: ()) -> Result<IList<$element>, Interrupt> {
vararg_element(ctx)
}
fn $extent<C: Ctx + ExtractVarArgs, N>(_: &$node_type<N>, ctx: &C, level: u8) -> GPoll<Extent> {
vararg_lanes::<$element>(ctx, level)
}
)*
};
}
fn read_graphic_row_extent<C: Ctx + ExtractVarArgs>(_: &ReadGraphicRowNode, ctx: &C, level: u8) -> GPoll<Extent> {
vararg_lanes::<Graphic>(ctx, level)
}
/// Rank-model vararg source: the mapped row's items as lanes, elements only.
#[node_macro::node(category("Test"), extent_raw(read_vector_row_extent))]
pub fn read_vector_row(ctx: impl Ctx + ExtractVarArgs + ExtractIndex) -> Result<IList<Vector>, Interrupt> {
vararg_element(ctx)
}
fn read_vector_row_extent<C: Ctx + ExtractVarArgs>(_: &ReadVectorRowNode, ctx: &C, level: u8) -> GPoll<Extent> {
vararg_lanes::<Vector>(ctx, level)
}
/// Rank-model vararg source: the mapped row's items as lanes, elements only.
#[node_macro::node(category("Test"), extent_raw(read_raster_row_extent))]
pub fn read_raster_row(ctx: impl Ctx + ExtractVarArgs + ExtractIndex) -> Result<IList<Raster<CPU>>, Interrupt> {
vararg_element(ctx)
}
fn read_raster_row_extent<C: Ctx + ExtractVarArgs>(_: &ReadRasterRowNode, ctx: &C, level: u8) -> GPoll<Extent> {
vararg_lanes::<Raster<CPU>>(ctx, level)
}
/// Rank-model vararg source: the mapped row's items as lanes, elements only.
#[node_macro::node(category("Test"), extent_raw(read_color_row_extent))]
pub fn read_color_row(ctx: impl Ctx + ExtractVarArgs + ExtractIndex) -> Result<IList<Color>, Interrupt> {
vararg_element(ctx)
}
fn read_color_row_extent<C: Ctx + ExtractVarArgs>(_: &ReadColorRowNode, ctx: &C, level: u8) -> GPoll<Extent> {
vararg_lanes::<Color>(ctx, level)
}
/// Rank-model vararg source: the mapped row's items as lanes, elements only.
#[node_macro::node(category("Test"), extent_raw(read_gradient_row_extent))]
pub fn read_gradient_row(ctx: impl Ctx + ExtractVarArgs + ExtractIndex) -> Result<IList<Gradient>, Interrupt> {
vararg_element(ctx)
}
fn read_gradient_row_extent<C: Ctx + ExtractVarArgs>(_: &ReadGradientRowNode, ctx: &C, level: u8) -> GPoll<Extent> {
vararg_lanes::<Gradient>(ctx, level)
vararg_readers! {
read_graphic / read_graphic_extent / ReadGraphicNode: Graphic<'static>;
read_vector / read_vector_extent / ReadVectorNode: Vector;
read_raster / read_raster_extent / ReadRasterNode: Raster<CPU>;
read_color / read_color_extent / ReadColorNode: Color;
read_gradient / read_gradient_extent / ReadGradientNode: Gradient;
read_string / read_string_extent / ReadStringNode: String;
}
#[node_macro::node(category("Context"), path(core_types::vector))]

View File

@@ -9,6 +9,7 @@ fn passthrough<T: Send>(_: impl Ctx, content: T) -> T {
content
}
/// Shifts a whole wire value onto a connector's type through the std `Into` trait, serving the whole-`List` erasure onto `ListDyn` under the input adapter identifier.
#[node_macro::node(category(""), skip_impl)]
fn into<T: Send + Into<O>, O: Send>(_: impl Ctx, value: T, #[data] _out_ty: PhantomData<O>) -> O {
value.into()

View File

@@ -1944,7 +1944,12 @@ mod tests {
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let node = install(crate::context::ReadColorRowNode::new(), crate::context::read_color_row_layout_meta(), &[]);
let unit = Layout::default().with_writes(0, core_types::record::element_write::<()>(), &[]);
let node = install(
crate::context::ReadColorNode::new(ValueSource::new(()), &unit),
crate::context::read_color_layout_meta(),
&[Some(&unit)],
);
let out = Node::<ContextImpl>::layout(&node).clone();
assert_eq!(out.depth, 1);
// No row pushed: an empty level, matching the legacy empty-list return.

View File

@@ -1,20 +1,30 @@
use core_types::attribute::{Attr, EditorLayerPath, Name0, Named, Transform as TransformAttr, WireValue};
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::{Extent, GPoll, GraphError, Interrupt, Level};
use core_types::gpoll::{ErrorKind, Extent, GPoll, GraphError, Interrupt, Level};
use core_types::list::List;
use core_types::node::Lane;
use core_types::registry::types::{Angle, SignedInteger};
use core_types::uuid::NodeId;
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_TRANSFORM, CacheHash, Color, Ctx, DeriveCtx, ExtractIndex, InjectIndex, ModifyIndex};
use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, CacheHash, Color, Ctx, DeriveCtx, ExtractIndex, InjectIndex, ModifyIndex};
use glam::{DAffine2, DVec2};
use graphic_types::graphic::{Graphic, IntoGraphicList};
use graphic_types::{ATTR_EDITOR_MERGED_LAYERS, Artboard, Vector};
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 raster_types::{CPU, GPU, Raster};
use vector_types::gradient::{GradientSpreadMethod, GradientType as GradientTypeValue};
use vector_types::{Gradient, GradientStop, ReferencePoint};
fn arena_exhausted() -> Interrupt {
GraphError {
kind: ErrorKind::ArenaExhausted,
trace: Vec::new(),
}
.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> {
@@ -79,7 +89,7 @@ pub fn item_at_index<T: Clone + Default + Send + Sync + CacheHash + 'static>(
/// 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"))]
fn map<Row: Clone + Send + Sync + CacheHash + 'static, T>(
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>>,
@@ -181,7 +191,7 @@ where
#[node_macro::node(category("General"), extent(mirror_extent))]
fn mirror<'e>(
ctx: impl Ctx + core_types::context::ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
content: IList<Graphic<'static>>,
#[default(ReferencePoint::Center)] relative_to_bounds: ReferencePoint,
#[unit(" px")] offset: f64,
@@ -219,7 +229,7 @@ fn mirror_extent(
/// mirror identifier.
#[node_macro::node(category(""), extent(mirror_vector_extent))]
fn mirror_vector<'e>(
ctx: impl Ctx + core_types::context::ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
content: IList<Vector>,
#[default(ReferencePoint::Center)] relative_to_bounds: ReferencePoint,
#[unit(" px")] offset: f64,
@@ -251,6 +261,7 @@ 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
@@ -265,10 +276,7 @@ pub fn path_of_subgraph(_: impl Ctx, node_path: Vec<NodeId>) -> Vec<NodeId> {
/// each lane, which lets editor tools trace data back to its layer.
#[node_macro::node(category(""))]
pub 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: core_types::gpoll::ErrorKind::ArenaExhausted,
trace: Vec::new(),
})?;
let (parked, _) = ctx.arena().alloc(path).ok_or_else(arena_exhausted)?;
Ok((element, Attr(parked.as_slice())))
}
@@ -284,10 +292,7 @@ pub fn write_attribute<'e, T, V: WireValue>(
name: Named<Name0>,
#[implementations(f64, u32, u64, bool, DVec2, DAffine2, Color, Vec<NodeId>, String)] value: V,
) -> Result<(T, Attr<'e, Named<Name0, V::Row>>), Interrupt> {
let parked = value.park(ctx.arena()).ok_or(GraphError {
kind: core_types::gpoll::ErrorKind::ArenaExhausted,
trace: Vec::new(),
})?;
let parked = value.park(ctx.arena()).ok_or_else(arena_exhausted)?;
Ok((content, Attr(parked)))
}
@@ -390,34 +395,6 @@ fn extend_extent(base: ExtentIn<'_>, new: ExtentIn<'_>, level: LevelIn) -> GPoll
}
}
// TODO: Eventually remove this document upgrade code
/// Performs an obsolete function as part of a migration from an older document format.
/// Users are advised to delete this node and replace it with a new one.
#[node_macro::node(category(""))]
pub fn legacy_layer_extend<T: Send + Clone>(
_: impl Ctx,
#[implementations(List<Artboard>, List<Graphic>, List<Vector>, List<String>, List<Raster<CPU>>, List<Raster<GPU>>, List<Color>, List<Gradient>)] base: List<T>,
#[expose]
#[implementations(List<Artboard>, List<Graphic>, List<Vector>, List<String>, List<Raster<CPU>>, List<Raster<GPU>>, List<Color>, List<Gradient>)]
new: List<T>,
nested_node_path: List<NodeId>,
) -> List<T> {
// Get the penultimate element of the node path, or None if the path is too short
// This is used to get the ID of the user-facing parent layer-style node (which encapsulates this internal node).
let layer = {
let index = nested_node_path.len().wrapping_sub(2);
nested_node_path.element(index).copied()
};
let mut base = base;
for mut row in new.into_iter() {
row.set_attribute(ATTR_EDITOR_LAYER_PATH, layer);
base.push(row);
}
base
}
/// 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.
@@ -438,23 +415,9 @@ fn wrap_graphic_extent<T>(_content: ListIn<'_, T>, _level: LevelIn) -> GPoll<Ext
/// Converts graphical content into a `Graphic` level. A `Graphic` level passes through
/// unchanged; a typed level nests as one graphic lane, keeping the pre-flip list
/// collapse (`to_graphic_typed` serves those rows). The legacy list rows accept an
/// unconverted producer's list value as one element, built as a native group.
/// collapse (`to_graphic_typed` serves those rows).
#[node_macro::node(category("General"))]
pub fn to_graphic<'e, T: graphic_types::graphic::IntoGraphicElement>(
ctx: impl Ctx + core_types::context::ExtractArena<'e>,
#[implementations(
Graphic,
List<Graphic>,
List<Vector>,
List<Raster<CPU>>,
List<Raster<GPU>>,
List<Color>,
List<Gradient>,
List<String>,
)]
content: T,
) -> Result<Graphic<'e>, Interrupt> {
pub fn to_graphic<'e, T: graphic_types::graphic::IntoGraphicElement>(ctx: impl Ctx + ExtractArena<'e>, #[implementations(Graphic)] content: T) -> Result<Graphic<'e>, Interrupt> {
content.into_graphic_element(ctx.arena()).ok_or_else(|| GraphError::new("the arena is exhausted").into())
}
@@ -463,24 +426,8 @@ pub fn to_graphic<'e, T: graphic_types::graphic::IntoGraphicElement>(
/// without changing the level's shape. Registered under the convert identifier.
#[node_macro::node(category(""))]
pub fn to_graphic_element<'e, T: graphic_types::graphic::IntoGraphicElement>(
ctx: impl Ctx + core_types::context::ExtractArena<'e>,
#[implementations(
Graphic,
Vector,
Raster<CPU>,
Raster<GPU>,
Color,
Gradient,
String,
List<Graphic>,
List<Vector>,
List<Raster<CPU>>,
List<Raster<GPU>>,
List<Color>,
List<Gradient>,
List<String>,
)]
content: T,
ctx: impl Ctx + ExtractArena<'e>,
#[implementations(Graphic, Vector, Raster<CPU>, Raster<GPU>, Color, Gradient, String)] content: T,
) -> Result<Graphic<'e>, Interrupt> {
content.into_graphic_element(ctx.arena()).ok_or_else(|| GraphError::new("the arena is exhausted").into())
}
@@ -508,21 +455,6 @@ fn to_graphic_unit_extent(_content: core_types::extent::ValueIn<'_, ()>, _level:
GPoll::Final(Extent::Exactly(0))
}
/// The transitional level bridge: the input's records as the legacy list an
/// unconverted consumer expects, attributes copied through their erased
/// reads and content kept in its native form. Registered under the legacy
/// convert identifiers.
#[node_macro::node(category(""))]
pub fn level_to_list<T: Clone + Send + Sync + CacheHash + dyn_any::StaticTypeSized>(
_: impl Ctx,
#[implementations(Graphic, Vector, Raster<CPU>, Raster<GPU>, Color, Gradient, String)] value: IList<T>,
_converter: (),
) -> List<T> {
let item = value.as_group_item();
graphic_types::graphic::run_to_list::<T>(&item).expect("the run holds the row's element type")
}
pub use _level_to_list_mod::level_to_list_entries;
pub use _to_graphic_element_mod::to_graphic_element_entries;
pub use _to_graphic_typed_mod::to_graphic_typed_entries;
pub use _to_graphic_unit_mod::to_graphic_unit_entries;
@@ -539,7 +471,7 @@ pub use _to_graphic_unit_mod::to_graphic_unit_entries;
/// not declare are truncated.
#[node_macro::node(category("General"), extent(flatten_graphic_extent))]
pub fn flatten_graphic<'e>(
ctx: impl Ctx + core_types::context::ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
content: IList<Graphic<'static>>,
fully_flatten: bool,
) -> Result<IList<(Lane<Graphic<'static>>, Attr<'e, TransformAttr>)>, Interrupt> {
@@ -572,64 +504,222 @@ fn flatten_graphic_extent(content: ListIn<'_, Graphic>, fully_flatten: ValueIn<'
}
}
/// Converts a `Graphic[]` into a `Vector[]` by deeply flattening any vector content it contains, and discarding any non-vector content.
#[node_macro::node(category("Vector"))]
pub fn flatten_vector<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>)] content: T) -> List<Vector> {
let graphic_list = content.into_graphic_list();
let mut output: List<Vector> = graphic_list.clone().into_flattened_list();
// TODO: Replace this snapshot hack with per-layer metadata driven by each layer's Monitor node.
// TODO: Flattening here erases the upstream `List<Graphic>` hierarchy that editor metadata collection walks
// TODO: to populate `upstream_footprints` / `local_transforms` / `click_targets` per child layer. As a workaround
// TODO: we stash the pre-flattened list on the output so `List<Vector>::collect_metadata` can recurse into it,
// TODO: which conflates render output with editor metadata and forces the pre-compensation dance below.
// TODO: The cleaner fix is to drive each layer's metadata from its own Monitor's captured `(Context, List<Graphic>)`,
// TODO: at which point this attribute (and the equivalents in Boolean Operation, Solidify Stroke, Flatten Path,
// TODO: Morph, Rasterize) become unnecessary.
if !output.is_empty() {
// Item 0 carries a composed transform inherited from the flattened input, but the merged_layers
// already holds the original transforms; pre-compensate by item 0's inverse so the renderer's
// `upstream_footprint *= item_0_transform` recursion cancels out and leaves the originals intact.
let mut graphic_list = graphic_list;
let item_0_transform: DAffine2 = output.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
if item_0_transform.matrix2.determinant().abs() > f64::EPSILON {
let inverse = item_0_transform.inverse();
for transform in graphic_list.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
*transform = inverse * *transform;
}
/// The `lane`-th flattened vector row of `level` as a one-item list, with the
/// top-level lane it descends from.
fn locate_vector_row(level: GraphicLevel<'_>, lane: usize) -> Option<(List<Vector>, usize)> {
let mut remaining = lane;
let mut located = None;
walk_vector_rows(level, &mut |row| {
if remaining > 0 {
remaining -= 1;
return RowStep::Continue;
}
let mut one = List::new();
row.build_into(&mut one);
located = Some((one, row.top_lane()));
RowStep::Stop
});
located
}
output.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, Some(graphic_list));
fn vector_row_count(level: GraphicLevel<'_>) -> usize {
let mut count = 0;
walk_vector_rows(level, &mut |_| {
count += 1;
RowStep::Continue
});
count
}
// TODO: Replace this snapshot hack with per-layer metadata driven by each layer's Monitor node.
// TODO: Flattening erases the upstream `Graphic` hierarchy that editor metadata collection walks to populate
// TODO: `upstream_footprints` / `local_transforms` / `click_targets` per child layer, so the pre-flattened list
// TODO: is stashed on row 0 for `collect_metadata` to recurse into (as Boolean Operation, Solidify Stroke,
// TODO: Flatten Path, Morph and Rasterize do). Driving each layer's metadata from its own Monitor's captured
// TODO: `(Context, List<Graphic>)` would make this attribute unnecessary.
/// The parked merged-layers snapshot for row 0. Row 0 carries a composed
/// transform the snapshot's own transforms already include, so the snapshot is
/// pre-compensated by its inverse to cancel the renderer's
/// `upstream_footprint *= row_0_transform` recursion.
fn merged_layers_snapshot<'e>(arena: &'e Arena, mut snapshot: List<Graphic<'static>>, row_0_transform: DAffine2) -> Result<&'e List<Graphic<'static>>, Interrupt> {
if row_0_transform.matrix2.determinant().abs() > f64::EPSILON {
let inverse = row_0_transform.inverse();
for transform in snapshot.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
*transform = inverse * *transform;
}
}
arena.alloc_sized_keyed(snapshot, 0).map(|(parked, _)| parked).ok_or_else(arena_exhausted)
}
output
type FlattenedVectorRow<'a, 'e> = (
Lane<'a, Vector>,
Attr<'e, TransformAttr>,
Attr<'e, Fill>,
Attr<'e, StrokeAttr>,
Attr<'e, Opacity>,
Attr<'e, OpacityFill>,
Attr<'e, EditorLayerPath>,
Attr<'e, EditorMergedLayers>,
);
/// A built vector row as the flatten's output: `carrier`'s columns with the
/// walk's composition, paint and layer path overriding, and `snapshot` parked
/// as the merged layers where given.
fn emit_vector_row<'a, 'e>(arena: &'e Arena, carrier: Lane<'a, Graphic<'static>>, row: List<Vector>, snapshot: Option<List<Graphic<'static>>>) -> Result<FlattenedVectorRow<'a, 'e>, Interrupt> {
let park_paint = |paint: Option<&Option<List<Graphic<'static>>>>| {
paint
.and_then(|paint| paint.as_ref())
.map(|paint| arena.alloc_sized_keyed(paint.clone(), 0).map(|(parked, _)| parked).ok_or_else(arena_exhausted))
.transpose()
};
let fill = park_paint(row.attribute(ATTR_FILL, 0))?;
let stroke = park_paint(row.attribute(ATTR_STROKE, 0))?;
let layer_path: Vec<NodeId> = row.attribute(ATTR_EDITOR_LAYER_PATH, 0).cloned().unwrap_or_default();
let (layer_path, _) = arena.alloc(layer_path).ok_or_else(arena_exhausted)?;
let transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
let merged_layers = snapshot.map(|snapshot| merged_layers_snapshot(arena, snapshot, transform)).transpose()?;
let element = row.element(0).cloned().unwrap_or_default();
Ok((
carrier.map_element(element),
Attr(transform),
Attr(fill),
Attr(stroke),
Attr(row.attribute_cloned_or(ATTR_OPACITY, 0, 1.)),
Attr(row.attribute_cloned_or(ATTR_OPACITY_FILL, 0, 1.)),
Attr(layer_path.as_slice()),
Attr(merged_layers),
))
}
/// Converts a `Graphic[]` into a `Vector[]` by deeply flattening any vector content it contains, and discarding any non-vector content.
/// Each row carries the columns of the top-level row it descends from, with the
/// path's composed transform and opacities, the reaching paint and the layer path overriding.
#[node_macro::node(category("Vector"), extent(flatten_vector_extent))]
pub fn flatten_vector<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
content: IList<Graphic<'static>>,
) -> Result<
IList<(
Lane<Vector>,
Attr<'e, TransformAttr>,
Attr<'e, Fill>,
Attr<'e, StrokeAttr>,
Attr<'e, Opacity>,
Attr<'e, OpacityFill>,
Attr<'e, EditorLayerPath>,
Attr<'e, EditorMergedLayers>,
)>,
Interrupt,
> {
let lane = ctx.index() as usize;
let item = content.as_group_item();
let Some((row, top)) = locate_vector_row(GraphicLevel::Run(&item), lane) else {
return Err(GraphError::past_end().into());
};
let snapshot = (lane == 0).then(|| legacy_render_list_of(content));
emit_vector_row(ctx.arena(), content.lane(top), row, snapshot)
}
/// The level holds one row per vector leaf of the walk.
fn flatten_vector_extent(content: ListIn<'_, Graphic>, level: LevelIn) -> GPoll<Extent> {
match level.top() {
true => content.get().map(|content| Extent::Exactly(vector_row_count(GraphicLevel::Run(&content.as_group_item())))),
false => GPoll::Final(Extent::Exactly(1)),
}
}
/// The `lane`-th `T` leaf under the content, carrying the columns of the
/// top-level row it descends from with the path's composition overriding.
type FlattenedLeafRow<'a, 'e, T> = (Lane<'a, T>, Attr<'e, TransformAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>);
fn flatten_leaf_lane<'a, 'e, T: TryFromGraphic + dyn_any::StaticTypeSized>(content: core_types::node::List<'a, Graphic<'static>>, lane: usize) -> Result<FlattenedLeafRow<'a, 'e, T>, Interrupt> {
let mut remaining = lane;
for row in 0..content.len() {
let carrier = content.lane(row);
let mut located = None;
crate::record::walk_typed_leaves(content.element_ref(row), Inherited::of(&carrier), &mut |leaf: &T, inherited| {
if remaining > 0 {
remaining -= 1;
return RowStep::Continue;
}
located = Some((leaf.clone(), inherited));
RowStep::Stop
});
if let Some((leaf, inherited)) = located {
return Ok((carrier.map_element(leaf), Attr(inherited.transform), Attr(inherited.opacity), Attr(inherited.fill_opacity)));
}
}
Err(GraphError::past_end().into())
}
/// The level holds one row per `T` leaf under the content.
fn flatten_leaves_extent<T: TryFromGraphic + dyn_any::StaticTypeSized>(content: ListIn<'_, Graphic>, level: LevelIn) -> GPoll<Extent> {
match level.top() {
true => content
.get()
.map(|content| Extent::Exactly((0..content.len()).map(|row| crate::record::typed_leaf_count::<T>(content.element_ref(row))).sum())),
false => GPoll::Final(Extent::Exactly(1)),
}
}
/// Converts a `Graphic[]` into a `Raster[]` by deeply flattening any raster content it contains, and discarding any non-raster content.
#[node_macro::node(category("Raster"))]
pub fn flatten_raster<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Raster<CPU>>)] content: T) -> List<Raster<CPU>> {
content.into_flattened_list()
#[node_macro::node(category("Raster"), extent(flatten_leaves_extent::<Raster<CPU>>))]
pub fn flatten_raster<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
content: IList<Graphic<'static>>,
) -> Result<IList<(Lane<Raster<CPU>>, Attr<'e, TransformAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>)>, Interrupt> {
flatten_leaf_lane(content, ctx.index() as usize)
}
/// Converts a `Graphic[]` into a `Color[]` by deeply flattening any color content it contains, and discarding any non-color content.
#[node_macro::node(category("General"))]
pub fn flatten_color<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Color>)] content: T) -> List<Color> {
content.into_flattened_list()
#[node_macro::node(category("General"), extent(flatten_leaves_extent::<Color>))]
pub fn flatten_color<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
content: IList<Graphic<'static>>,
) -> Result<IList<(Lane<Color>, Attr<'e, TransformAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>)>, Interrupt> {
flatten_leaf_lane(content, ctx.index() as usize)
}
/// Converts a `Graphic[]` into a `Gradient[]` by deeply flattening any gradient content it contains, and discarding any non-gradient content.
#[node_macro::node(category("General"))]
pub fn flatten_gradient<T: IntoGraphicList>(_: impl Ctx, #[implementations(List<Graphic>, List<Gradient>)] content: T) -> List<Gradient> {
content.into_flattened_list()
#[node_macro::node(category("General"), extent(flatten_leaves_extent::<Gradient>))]
pub fn flatten_gradient<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
content: IList<Graphic<'static>>,
) -> Result<IList<(Lane<Gradient>, Attr<'e, TransformAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>)>, Interrupt> {
flatten_leaf_lane(content, ctx.index() as usize)
}
/// A gradient with `colors` as evenly spaced stops from 0 to 1; none makes a
/// black gradient and one repeats at both ends.
fn evenly_spaced_gradient(colors: &[Color]) -> Gradient {
let stop = |position: f64, color: Color| GradientStop { position, midpoint: 0.5, color };
match colors {
[] => Gradient::new(vec![stop(0., Color::BLACK), stop(1., Color::BLACK)]),
[color] => Gradient::new(vec![stop(0., *color), stop(1., *color)]),
colors => Gradient::new(colors.iter().enumerate().map(|(index, color)| stop(index as f64 / (colors.len() - 1) as f64, *color))),
}
}
/// Constructs a gradient from a `Color[]`, where the colors are evenly distributed as gradient stops across the range from 0 to 1.
#[node_macro::node(category("Color"))]
fn colors_to_gradient(_: impl Ctx, colors: IList<Color>) -> Gradient {
let stop = |position: f64, color: Color| GradientStop { position, midpoint: 0.5, color };
match colors.len() {
0 => Gradient::new(vec![stop(0., Color::BLACK), stop(1., Color::BLACK)]),
1 => Gradient::new(vec![stop(0., colors.get(0)), stop(1., colors.get(0))]),
total => Gradient::new((0..total).map(|index| stop(index as f64 / (total - 1) as f64, colors.get(index)))),
}
#[node_macro::node(category("Color"), name("Colors to Gradient"))]
pub fn colors_to_gradient(_: impl Ctx, colors: IList<Color>) -> Gradient {
evenly_spaced_gradient(&colors.iter().collect::<Vec<_>>())
}
/// The gradient over a graphic level's color leaves, as [`colors_to_gradient`].
/// Registered under the colors to gradient identifier.
#[node_macro::node(category(""))]
pub fn colors_to_gradient_graphic(_: impl Ctx, colors: IList<Graphic<'static>>) -> Gradient {
let mut leaves = Vec::new();
for row in 0..colors.len() {
crate::record::walk_typed_leaves::<Color>(colors.element_ref(row), Inherited::IDENTITY, &mut |color, _| {
leaves.push(*color);
RowStep::Continue
});
}
evenly_spaced_gradient(&leaves)
}
pub use _colors_to_gradient_graphic_mod::colors_to_gradient_graphic_entries;

View File

@@ -1,17 +1,21 @@
//! Pilot record nodes over the production graphic types: element-space
//! expanders whose ragged nesting lives inside `Graphic` values, ahead of the
//! flip. Wiring is by hand until the compiler pass constructs layouts.
//! The element-space walks the production flatten nodes share, plus the
//! level-nesting pilots (`nested_map`, `flatten_levels`) that have no
//! production counterpart yet. The tests here drive the production nodes in
//! `graphic.rs` with hand-wired layouts.
use core_types::attribute::{Attr, Transform};
use core_types::attribute::{Opacity, OpacityFill, Transform};
use core_types::context::{DeriveCtx, ExtractIndex, IndexLink, InjectIndex};
use core_types::extent::{ExtentIn, LevelIn, ListIn, ValueIn};
use core_types::extent::{ExtentIn, LevelIn};
use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt};
use core_types::lane::LaneSource;
use core_types::node::RecordLane;
use core_types::record::RunView;
use core_types::{ATTR_TRANSFORM, Color, Ctx};
use glam::DAffine2;
use graphic_types::Vector;
use graphic_types::graphic::Graphic;
use graphic_types::graphic::{Graphic, RowStep, TryFromGraphic};
use raster_types::{CPU, Raster};
use vector_types::{Gradient, GradientStop};
use vector_types::Gradient;
/// Whether the walk can descend into a group: the run holds `Graphic`
/// elements.
@@ -65,63 +69,90 @@ pub(crate) fn locate<'e>(graphic: &Graphic<'e>, transform: DAffine2, fully_flatt
}
}
/// Rank-model Flatten: one flat level holding the content's leaves, each with
/// the transforms along its path composed; a group beyond the walk's depth
/// rides as a leaf with its embedded transforms untouched.
#[node_macro::node(category("Test"), extent(flatten_extent))]
fn flatten(ctx: impl Ctx + ExtractIndex + InjectIndex + Copy, content: IList<Graphic<'static>>, fully_flatten: bool) -> Result<IList<(Graphic<'static>, Attr<Transform>)>, Interrupt> {
let mut remaining = ctx.index() as usize;
for row in 0..content.len() {
let graphic = content.element_ref(row);
let count = leaf_count(graphic, fully_flatten, 0);
if remaining >= count {
remaining -= count;
continue;
}
let transform: DAffine2 = content.lane(row).attr::<Transform>();
if let Some((leaf, composed)) = locate(graphic, transform, fully_flatten, 0, &mut remaining) {
return Ok((leaf, Attr(composed)));
/// The ancestor composition a typed flatten's leaf inherits: transform,
/// opacity and fill opacity multiply down the path, as the legacy flatten did.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Inherited {
pub transform: DAffine2,
pub opacity: f64,
pub fill_opacity: f64,
}
impl Inherited {
pub(crate) const IDENTITY: Self = Self {
transform: DAffine2::IDENTITY,
opacity: 1.,
fill_opacity: 1.,
};
/// The composition a top-level row starts from: the row's own columns.
pub(crate) fn of(lane: &RecordLane<'_>) -> Self {
Self {
transform: lane.attr::<Transform>(),
opacity: lane.attr::<Opacity>(),
fill_opacity: lane.attr::<OpacityFill>(),
}
}
Err(GraphError::new("flatten addressed past its leaf count").into())
}
/// The level holds one row per leaf of the walk.
fn flatten_extent(content: ListIn<'_, Graphic>, fully_flatten: ValueIn<'_, bool>, level: LevelIn) -> GPoll<Extent> {
match level.top() {
true => fully_flatten
.get()
.zip(content.get())
.map(|(fully_flatten, content)| Extent::Exactly((0..content.len()).map(|row| leaf_count(content.element_ref(row), fully_flatten, 0)).sum())),
false => GPoll::Final(Extent::Exactly(1)),
fn composed<S: LaneSource>(self, source: &S, lane: usize) -> Self {
Self {
transform: self.transform * source.attr::<Transform>(lane),
opacity: self.opacity * source.attr::<Opacity>(lane),
fill_opacity: self.fill_opacity * source.attr::<OpacityFill>(lane),
}
}
}
/// Rank-model Wrap: the content level as one group element on a one-lane
/// level, the inverse of flatten's one-level descent.
#[node_macro::node(category("Test"), extent(wrap_extent))]
fn wrap<'e>(_: impl Ctx, content: IList<Graphic<'e>>) -> Result<IList<Graphic<'e>>, Interrupt> {
let item = content.as_group_item();
Ok(Graphic::Group(core_types::record::Group { row: None, content: item }))
}
/// The collected group is the level's single lane.
fn wrap_extent(_content: ListIn<'_, Graphic>, _level: LevelIn) -> GPoll<Extent> {
GPoll::Final(Extent::Exactly(1))
}
/// Rank-model colors-to-gradient: the color level folds into one gradient
/// with evenly spaced stops.
#[node_macro::node(category("Test"))]
fn to_gradient(_: impl Ctx, colors: IList<Color>) -> Gradient {
let stop = |position: f64, color: Color| GradientStop { position, midpoint: 0.5, color };
match colors.len() {
0 => Gradient::new(vec![stop(0., Color::BLACK), stop(1., Color::BLACK)]),
1 => Gradient::new(vec![stop(0., colors.get(0)), stop(1., colors.get(0))]),
total => Gradient::new((0..total).map(|index| stop(index as f64 / (total - 1) as f64, colors.get(index)))),
/// Visits every `T` leaf under `graphic`, at any depth, with the composition
/// along its path. A group run typed `T` contributes its lanes directly; runs
/// of other element types contribute nothing.
pub(crate) fn walk_typed_leaves<T: TryFromGraphic + dyn_any::StaticTypeSized>(graphic: &Graphic, inherited: Inherited, visit: &mut dyn FnMut(&T, Inherited) -> RowStep) -> RowStep {
match graphic {
Graphic::Graphic(children) => {
for index in 0..children.len() {
let Some(child) = children.element(index) else { continue };
if let RowStep::Stop = walk_typed_leaves(child, inherited.composed(children, index), visit) {
return RowStep::Stop;
}
}
RowStep::Continue
}
Graphic::Group(group) => {
let item = &group.content;
if let Some(run) = RunView::<Graphic>::new(item) {
for lane in 0..item.len() {
let Some(child) = run.element(lane) else { continue };
if let RowStep::Stop = walk_typed_leaves(child, inherited.composed(&run, lane), visit) {
return RowStep::Stop;
}
}
} else if let Some(run) = RunView::<T>::new(item) {
for lane in 0..item.len() {
let Some(leaf) = run.element(lane) else { continue };
if let RowStep::Stop = visit(leaf, inherited.composed(&run, lane)) {
return RowStep::Stop;
}
}
}
RowStep::Continue
}
leaf => match T::leaf_of(leaf) {
Some(leaf) => visit(leaf, inherited),
None => RowStep::Continue,
},
}
}
/// The `T` leaves under `graphic`, at any depth.
pub(crate) fn typed_leaf_count<T: TryFromGraphic + dyn_any::StaticTypeSized>(graphic: &Graphic) -> usize {
let mut count = 0;
walk_typed_leaves::<T>(graphic, Inherited::IDENTITY, &mut |_, _| {
count += 1;
RowStep::Continue
});
count
}
/// One content row as the production vararg shape: 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.
@@ -129,11 +160,12 @@ pub(crate) fn vararg_row<Row: Clone + Send + Sync + 'static>(content: core_types
core_types::list::List::new_from_element(content.element_ref(row).clone())
}
/// Rank-model Map: one subgraph invocation per content row, the row riding as
/// a vararg; the subgraph's own level nests under the content level. The
/// levels report a lower bound; consumers drain to the past-end signal.
/// Rank-model nested Map: one subgraph invocation per content row, the row
/// riding as a vararg; the subgraph's own level nests under the content level.
/// The levels report a lower bound; consumers drain to the past-end signal.
/// The production `Map` is this walk with the levels concatenated.
#[node_macro::node(category("Test"))]
fn map<Row: Clone + Send + Sync + core_types::CacheHash + 'static, T>(
fn nested_map<Row: Clone + Send + Sync + core_types::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>>,
@@ -153,30 +185,6 @@ fn map<Row: Clone + Send + Sync + core_types::CacheHash + 'static, T>(
Err(GraphError::past_end().into())
}
/// Rank-model flat-map (the production Map): map's walk 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("Test"))]
fn flat_map<Row: Clone + Send + Sync + core_types::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())
}
/// Rank-model level collapse: two nested levels become one flat level. The
/// flat index already spans the input's depth, so the eval forwards it.
#[node_macro::node(category("Test"), extent(flatten_levels_extent))]
@@ -215,6 +223,7 @@ 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 core_types::arena::Arena;
use core_types::attribute::Attribute as AttributeMarker;
@@ -352,7 +361,7 @@ mod tests {
macro_rules! build {
($layout:ident, $rows:expr, $fully:expr) => {
install(
FlattenNode::new(
FlattenGraphicNode::new(
RecordSource::new(
GraphicSource {
layout: $layout.clone(),
@@ -363,7 +372,7 @@ mod tests {
),
ValueSource::new($fully),
),
flatten_layout_meta(),
flatten_graphic_layout_meta(),
&[Some(&$layout)],
)
};
@@ -453,7 +462,7 @@ mod tests {
let layout = graphic_layout();
let node = install(
MapNode::<_, _, Graphic>::new(
NestedMapNode::<_, _, Graphic>::new(
RecordSource::new(
GraphicSource {
layout: layout.clone(),
@@ -496,7 +505,7 @@ mod tests {
let layout = graphic_layout();
let flat = install(
FlatMapNode::<_, _, Graphic>::new(
MapNode::<_, _, Graphic>::new(
RecordSource::new(
GraphicSource {
layout: layout.clone(),
@@ -512,7 +521,7 @@ mod tests {
&[Some(&layout), Some(&layout)],
);
let mapped = install(
MapNode::<_, _, Graphic>::new(
NestedMapNode::<_, _, Graphic>::new(
RecordSource::new(
GraphicSource {
layout: layout.clone(),
@@ -559,7 +568,7 @@ mod tests {
#[test]
fn flat_map_registers_one_row_per_content_type() {
let entries = _flat_map_mod::flat_map_entries();
let entries = crate::graphic::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>());
@@ -580,7 +589,7 @@ mod tests {
let layout = graphic_layout();
let node = install(
FlatMapNode::<_, _, Graphic>::new(
MapNode::<_, _, Graphic>::new(
RecordSource::new(
GraphicSource {
layout: layout.clone(),
@@ -710,8 +719,8 @@ mod tests {
let layout = graphic_layout();
let rows = vec![(text("a"), translation(1.)), (text("b"), translation(2.))];
let node = install(
WrapNode::new(RecordSource::new(GraphicSource { layout: layout.clone(), rows }, &layout, &layout), &layout),
wrap_layout_meta(),
WrapGraphicNode::<_, Graphic>::new(RecordSource::new(GraphicSource { layout: layout.clone(), rows }, &layout, &layout), &layout),
wrap_graphic_layout_meta(),
&[Some(&layout)],
);
let out = Node::<ContextImpl>::layout(&node).clone();
@@ -748,8 +757,8 @@ mod tests {
let layout = graphic_layout();
let rows = vec![(text("a"), translation(1.)), (text("b"), translation(2.))];
let node = install(
WrapNode::new(RecordSource::new(GraphicSource { layout: layout.clone(), rows }, &layout, &layout), &layout),
wrap_layout_meta(),
WrapGraphicNode::<_, Graphic>::new(RecordSource::new(GraphicSource { layout: layout.clone(), rows }, &layout, &layout), &layout),
wrap_graphic_layout_meta(),
&[Some(&layout)],
);
let out = Node::<ContextImpl>::layout(&node).clone();
@@ -821,7 +830,12 @@ mod tests {
let layout = Layout::default().with_writes(1, record::element_write_hashed::<Color>(), &[]);
let out = Layout::default().with_writes(0, record::element_write_hashed::<Gradient>(), &[]);
let build = |colors: Vec<Color>| install_flip(ToGradientNode::new(RecordSource::new(ColorSource { layout: layout.clone(), colors }, &layout, &layout), &layout), &out);
let build = |colors: Vec<Color>| {
install_flip(
ColorsToGradientNode::new(RecordSource::new(ColorSource { layout: layout.clone(), colors }, &layout, &layout), &layout),
&out,
)
};
let stops_of = |colors: Vec<Color>| {
let node = build(colors);
let GPoll::Final(record) = record::capture(&node, &ctx, &frames) else {
@@ -852,8 +866,8 @@ mod tests {
let layout = graphic_layout();
let rows = vec![(text("a"), translation(1.)), (text("b"), translation(2.))];
let node = install(
WrapNode::new(RecordSource::new(GraphicSource { layout: layout.clone(), rows }, &layout, &layout), &layout),
wrap_layout_meta(),
WrapGraphicNode::<_, Graphic>::new(RecordSource::new(GraphicSource { layout: layout.clone(), rows }, &layout, &layout), &layout),
wrap_graphic_layout_meta(),
&[Some(&layout)],
);
let out = Node::<ContextImpl>::layout(&node).clone();
@@ -884,8 +898,8 @@ mod tests {
let layout = graphic_layout();
let rows = vec![(text("a"), translation(1.)), (text("b"), translation(2.))];
let wrapped = install(
WrapNode::new(RecordSource::new(GraphicSource { layout: layout.clone(), rows }, &layout, &layout), &layout),
wrap_layout_meta(),
WrapGraphicNode::<_, Graphic>::new(RecordSource::new(GraphicSource { layout: layout.clone(), rows }, &layout, &layout), &layout),
wrap_graphic_layout_meta(),
&[Some(&layout)],
);
let wrap_out = Node::<ContextImpl>::layout(&wrapped).clone();
@@ -920,6 +934,45 @@ mod tests {
}
}
/// [Color a, G[Color b (opacity 0.5), Text], Text]: two color leaves, the
/// nested one composing G's transform and its own opacity; the texts drop.
#[test]
fn flatten_color_keeps_only_color_leaves_and_composes_the_path() {
let frames = core_types::record::test_frames(1 << 16);
let arena = Arena::new(1 << 16).unwrap();
let generations = [];
let scope = scope_fixture(&generations, &arena);
let ctx = ContextImpl::root(&scope);
let nested = {
let Graphic::Graphic(mut children) = group(vec![(Graphic::Color(Color::WHITE), translation(20.)), (text("x"), translation(300.))]) else {
unreachable!("group builds a legacy graphic list");
};
children.set_attribute(core_types::ATTR_OPACITY, 0, 0.5);
Graphic::Graphic(children)
};
let rows = vec![(Graphic::Color(Color::BLACK), translation(1.)), (nested, translation(0.5)), (text("y"), translation(9.))];
let layout = graphic_layout();
let node = install(
FlattenColorNode::new(RecordSource::new(GraphicSource { layout: layout.clone(), rows }, &layout, &layout)),
flatten_color_layout_meta(),
&[Some(&layout)],
);
assert_eq!(node.extent_at(&ctx, 0, &frames.reborrow()), GPoll::Final(Extent::Exactly(2)));
let head = ctx.index_head();
let expected = [(Color::BLACK, 1., 1.), (Color::WHITE, 20.5, 0.5)];
for (lane, &(color, x, opacity)) in expected.iter().enumerate() {
let GPoll::Final(record) = record::capture(&node, &ctx.promoted(&head, lane as u64), &frames) else {
panic!("expected a final record");
};
assert_eq!(record.element::<Color>(), color, "lane {lane}");
let transform: DAffine2 = record.attr::<Transform>();
assert_eq!(transform.translation.x, x, "lane {lane}");
assert_eq!(record.attr::<Opacity>(), opacity, "lane {lane}");
}
}
#[test]
fn flatten_fully_composes_the_path() {
let frames = core_types::record::test_frames(1 << 16);

View File

@@ -6,6 +6,7 @@ use canvas_utils::{Canvas, CanvasHandle};
use core_types::attribute::{Attr, OwnedAttr, Transform};
use core_types::color::SRGBA8;
use core_types::gpoll::GPoll;
#[cfg(target_family = "wasm")]
use core_types::list::List;
#[cfg(target_family = "wasm")]
@@ -218,7 +219,7 @@ async fn rasterize<T: Clone + Send + Sync + dyn_any::StaticTypeSized>(
mut canvas: CanvasHandle,
) -> (Raster<CPU>, Attr<Transform>, OwnedAttr<EditorMergedLayers>)
where
List<T>: Render + Clone + graphic_types::IntoGraphicList,
List<T>: Render + Clone + IntoGraphicList,
{
use glam::{DAffine2, DVec2};
@@ -310,10 +311,6 @@ pub fn try_wgpu_executor(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_ap
/// Uploads image data from CPU memory into a GPU texture so that GPU-based nodes can process it.
#[node_macro::node(category("Debug"), memoize)]
pub fn upload_texture<T: Convert<List<Raster<GPU>>, ::wgpu_executor::WgpuExecutorHandle>>(
_: impl Ctx,
#[implementations(List<Raster<CPU>>)] content: T,
#[scope(wgpu_executor::IDENTIFIER)] executor: ::wgpu_executor::WgpuExecutorHandle,
) -> List<Raster<GPU>> {
pub fn upload_texture(_: impl Ctx, content: Raster<CPU>, #[scope(wgpu_executor::IDENTIFIER)] executor: ::wgpu_executor::WgpuExecutorHandle) -> Raster<GPU> {
content.convert(Footprint::DEFAULT, executor)
}

View File

@@ -1,7 +1,6 @@
use core_types::gpoll::Interrupt;
use core_types::list::List;
use core_types::transform::{Footprint, Transform};
use core_types::{Color, Context, Ctx, DeriveCtx, ExtractFootprint, ExtractIndex, ExtractVarArgs, InjectIndex, VarArgLink, VarArgSlots, WasmNotSend};
use core_types::{Color, Ctx, DeriveCtx, ExtractFootprint, ExtractIndex, ExtractVarArgs, InjectIndex, VarArgLink, VarArgSlots};
use graph_craft::document::value::{RenderOutput, RenderOutputType};
use graphene_application_io::{ExportFormat, RenderConfig};
use graphic_types::raster_types::{CPU, Raster};
@@ -51,34 +50,9 @@ fn intermediate_of<R: Render>(data: &R, render_params: &RenderParams) -> RenderI
}
}
/// The input's records materialize into a run, which renders directly.
#[node_macro::node(category(""))]
fn render_intermediate<T: dyn_any::StaticTypeSized + 'static + Render + WasmNotSend + Send + Sync>(
ctx: impl Ctx + ExtractVarArgs + DeriveCtx,
#[implementations(
Context -> List<Artboard>,
Context -> List<Graphic>,
Context -> List<Vector>,
Context -> List<Raster<CPU>>,
Context -> List<Color>,
Context -> List<Gradient>,
Context -> List<String>,
)]
data: impl Node<Context<'_>, Output = T>,
) -> Result<RenderIntermediate, Interrupt> {
let data = data.eval(&ctx.derived())?;
let render_params = ctx
.vararg(0)
.expect("Did not find var args")
.downcast_ref::<RenderParams>()
.expect("Downcasting render params yielded invalid type");
Ok(intermediate_of(&data, render_params))
}
/// The leveled form of `render_intermediate`: the input's records materialize
/// into a run, which renders directly.
#[node_macro::node(category(""))]
fn render_intermediate_leveled<T: Clone + Send + Sync + core_types::CacheHash + dyn_any::StaticTypeSized + 'static>(
fn render_intermediate<T: Clone + Send + Sync + core_types::CacheHash + dyn_any::StaticTypeSized + 'static>(
ctx: impl Ctx + ExtractVarArgs + ExtractIndex + InjectIndex + Copy,
#[implementations(Artboard, Graphic, Vector, Raster<CPU>, Color, Gradient, String)] data: IList<T>,
) -> Result<RenderIntermediate, Interrupt>

View File

@@ -1,16 +1,21 @@
use core_types::consts::{DEFAULT_FONT_SIZE, DEFAULT_LINE_HEIGHT};
use core_types::attribute::{Attr, FontSize, LetterSpacing, LetterTilt, LineHeight, MaxHeight, MaxWidth, Transform as TransformAttr};
use core_types::extent::{LevelIn, ListIn};
use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt};
use core_types::list::List;
use core_types::{ATTR_FONT_SIZE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, Ctx};
use core_types::node::{Lane, RecordLane};
use core_types::{ATTR_TRANSFORM, Ctx, ExtractIndex, InjectIndex};
use glam::DAffine2;
use graph_craft::application_io::resource::Resource;
use graphic_types::Vector;
use text_nodes::markers::{Font as FontAttr, TextAlign as TextAlignAttr};
pub use text_nodes::*;
/// Produces a styled `String[]` carrying all typographic attributes.
/// Produces a styled `String` carrying all typographic attributes.
///
/// Use the **Text to Vector** node to convert this into vector geometry if desired.
#[node_macro::node(category("Text"))]
fn text(
_: impl Ctx,
fn text<'e>(
ctx: impl Ctx + ExtractArena<'e>,
_primary: (),
/// The text content to be drawn.
#[widget(ParsedWidgetOverride::Custom = "text_area")]
@@ -59,55 +64,146 @@ fn text(
/// The horizontal alignment of each line of text within its surrounding box. To have an effect on a single line of text, *Max Width* must be set.
#[widget(ParsedWidgetOverride::Custom = "text_align")]
align: TextAlign,
) -> List<String> {
let mut list = List::new_from_element(text);
if font != Resource::default() {
list.set_attribute(ATTR_FONT, 0, font);
}
if (size - DEFAULT_FONT_SIZE).abs() > f64::EPSILON {
list.set_attribute(ATTR_FONT_SIZE, 0, size);
}
if (line_height - DEFAULT_LINE_HEIGHT).abs() > f64::EPSILON {
list.set_attribute(ATTR_LINE_HEIGHT, 0, line_height);
}
if letter_spacing != 0. {
list.set_attribute(ATTR_LETTER_SPACING, 0, letter_spacing);
}
if letter_tilt != 0. {
list.set_attribute(ATTR_LETTER_TILT, 0, letter_tilt);
}
if has_max_width {
list.set_attribute(ATTR_MAX_WIDTH, 0, Some(max_width));
}
if has_max_height {
list.set_attribute(ATTR_MAX_HEIGHT, 0, Some(max_height));
}
if align != TextAlign::default() {
list.set_attribute(ATTR_TEXT_ALIGN, 0, align);
}
list
) -> Result<
(
String,
Attr<'e, FontAttr>,
Attr<'e, FontSize>,
Attr<'e, LineHeight>,
Attr<'e, LetterSpacing>,
Attr<'e, LetterTilt>,
Attr<'e, MaxWidth>,
Attr<'e, MaxHeight>,
Attr<'e, TextAlignAttr>,
),
Interrupt,
> {
let (font, _) = ctx.arena().alloc(font).ok_or_else(|| Interrupt::from(GraphError::new("the arena is exhausted")))?;
Ok((
text,
Attr(font),
Attr(size),
Attr(line_height),
Attr(letter_spacing),
Attr(letter_tilt),
Attr(has_max_width.then_some(max_width)),
Attr(has_max_height.then_some(max_height)),
Attr(align),
))
}
/// Converts styled text into vector compound paths.
#[node_macro::node(category("Text"), name("Text to Vector"))]
fn text_to_vector(
_: impl Ctx,
/// A styled list of text strings produced by the **Text** node (or any other `String[]` source).
#[implementations(List<String>)]
strings: List<String>,
) -> List<Vector> {
shape_text_list(&strings, false)
/// Shapes one styled string lane into vector geometry, the font and
/// typesetting read from the lane's columns. The paths keep their glyph-local
/// transforms; the lane's own transform composes on at emit.
fn shape_lane(lane: &RecordLane<'_>, text: &str, separate_glyphs: bool) -> List<Vector> {
if text.is_empty() {
return List::new();
}
let font = lane.attr::<FontAttr>();
let font = match font.is_empty() {
true => &FALLBACK_FONT_RESOURCE,
false => font,
};
let typesetting = TypesettingConfig {
font_size: lane.attr::<FontSize>(),
line_height_ratio: lane.attr::<LineHeight>(),
letter_spacing: lane.attr::<LetterSpacing>(),
letter_tilt: lane.attr::<LetterTilt>(),
max_width: lane.attr::<MaxWidth>(),
max_height: lane.attr::<MaxHeight>(),
align: lane.attr::<TextAlignAttr>(),
};
to_path(text, font, typesetting, separate_glyphs)
}
/// Splits styled text into a separate vector item for each of its glyphs (letterforms).
#[node_macro::node(category("Text"), name("Text to Vector Glyphs"))]
fn text_to_vector_glyphs(
_: impl Ctx,
/// A styled list of text strings produced by the **Text** node (or any other `String[]` source).
#[implementations(List<String>)]
strings: List<String>,
) -> List<Vector> {
shape_text_list(&strings, true)
/// The shaped paths of every string lane, valid for one key and generation,
/// so addressing the level's lanes shapes each string once.
#[derive(Debug, Default)]
pub struct ShapedRows {
key: u64,
generation: u64,
rows: Vec<List<Vector>>,
}
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.
fn shaped_lane<'a, 'e>(
strings: core_types::node::List<'a, String>,
lane: usize,
(key, generation): (u64, u64),
cache: &ShapedCache,
separate_glyphs: bool,
) -> Result<(Lane<'a, Vector>, Attr<'e, TransformAttr>), Interrupt> {
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) {
let rows = (0..strings.len()).map(|row| shape_lane(&strings.lane(row), strings.element_ref(row), separate_glyphs)).collect();
*cached = Some(ShapedRows { key, generation, rows });
}
let rows = &cached.as_ref().expect("populated above").rows;
let mut remaining = lane;
for (row, shaped) in rows.iter().enumerate() {
if remaining >= shaped.len() {
remaining -= shaped.len();
continue;
}
let element = shaped.element(remaining).cloned().unwrap_or_default();
let local: DAffine2 = shaped.attribute_cloned_or_default(ATTR_TRANSFORM, remaining);
let carrier = strings.lane(row);
let transform = carrier.attr::<TransformAttr>() * local;
return Ok((carrier.map_element(element), Attr(transform)));
}
Err(GraphError::past_end().into())
}
/// The level holds every string's shaped paths in order.
fn shaped_extent(strings: ListIn<'_, String>, level: LevelIn, separate_glyphs: bool) -> GPoll<Extent> {
match level.top() {
true => strings
.get()
.map(|strings| Extent::Exactly((0..strings.len()).map(|row| shape_lane(&strings.lane(row), strings.element_ref(row), separate_glyphs).len()).sum())),
false => GPoll::Final(Extent::Exactly(1)),
}
}
/// Converts styled text into vector compound paths, one per string.
#[node_macro::node(category("Text"), name("Text to Vector"), extent(text_to_vector_extent))]
fn text_to_vector<'e>(
ctx: impl Ctx + core_types::CacheHash + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
/// Styled strings produced by the **Text** node (or any other `String` source).
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)
}
fn text_to_vector_extent(strings: ListIn<'_, String>, level: LevelIn) -> GPoll<Extent> {
shaped_extent(strings, level, false)
}
/// Splits styled text into a separate vector path for each of its glyphs (letterforms).
#[node_macro::node(category("Text"), name("Text to Vector Glyphs"), extent(text_to_vector_glyphs_extent))]
fn text_to_vector_glyphs<'e>(
ctx: impl Ctx + core_types::CacheHash + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
/// Styled strings produced by the **Text** node (or any other `String` source).
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)
}
fn text_to_vector_glyphs_extent(strings: ListIn<'_, String>, level: LevelIn) -> GPoll<Extent> {
shaped_extent(strings, level, true)
}

View File

@@ -41,7 +41,7 @@ fn math<T: num_traits::float::Float>(
#[implementations(f64, f32)]
operand_a: T,
/// A math expression that may incorporate "A" and/or "B", such as `sqrt(A + B) - B^2`.
#[default(A + B)]
#[default("A + B")]
expression: String,
/// The value of "B" when calculating the expression.
#[implementations(f64, f32)]
@@ -517,10 +517,10 @@ fn absolute_value<T: AbsoluteValue>(
fn min<T: std::cmp::PartialOrd>(
_: impl Ctx,
/// One of the two numbers, of which the lesser is returned.
#[implementations(f64, f32, u32, &str)]
#[implementations(f64, f32, u32, String)]
value: T,
/// The other of the two numbers, of which the lesser is returned.
#[implementations(f64, f32, u32, &str)]
#[implementations(f64, f32, u32, String)]
other_value: T,
) -> T {
if value < other_value { value } else { other_value }
@@ -531,10 +531,10 @@ fn min<T: std::cmp::PartialOrd>(
fn max<T: std::cmp::PartialOrd>(
_: impl Ctx,
/// One of the two numbers, of which the greater is returned.
#[implementations(f64, f32, u32, &str)]
#[implementations(f64, f32, u32, String)]
value: T,
/// The other of the two numbers, of which the greater is returned.
#[implementations(f64, f32, u32, &str)]
#[implementations(f64, f32, u32, String)]
other_value: T,
) -> T {
if value > other_value { value } else { other_value }
@@ -545,13 +545,13 @@ fn max<T: std::cmp::PartialOrd>(
fn clamp<T: std::cmp::PartialOrd>(
_: impl Ctx,
/// The number to be clamped, which is restricted to the range between the minimum and maximum values.
#[implementations(f64, f32, u32, &str)]
#[implementations(f64, f32, u32, String)]
value: T,
/// The left (smaller) side of the range. The output is never less than this number.
#[implementations(f64, f32, u32, &str)]
#[implementations(f64, f32, u32, String)]
min: T,
/// The right (greater) side of the range. The output is never greater than this number.
#[implementations(f64, f32, u32, &str)]
#[implementations(f64, f32, u32, String)]
#[default(1)]
max: T,
) -> T {
@@ -678,10 +678,10 @@ fn greater_than<T: std::cmp::PartialOrd<T>>(
fn equals<T: std::cmp::PartialEq<T>>(
_: impl Ctx,
/// One of the two values to compare for equality.
#[implementations(f64, f32, u32, DVec2, bool, &str, String)]
#[implementations(f64, f32, u32, DVec2, bool, String)]
value: T,
/// The other of the two values to compare for equality.
#[implementations(f64, f32, u32, DVec2, bool, &str, String)]
#[implementations(f64, f32, u32, DVec2, bool, String)]
other_value: T,
) -> bool {
other_value == value
@@ -692,10 +692,10 @@ fn equals<T: std::cmp::PartialEq<T>>(
fn not_equals<T: std::cmp::PartialEq<T>>(
_: impl Ctx,
/// One of the two values to compare for inequality.
#[implementations(f64, f32, u32, DVec2, bool, &str)]
#[implementations(f64, f32, u32, DVec2, bool, String)]
value: T,
/// The other of the two values to compare for inequality.
#[implementations(f64, f32, u32, DVec2, bool, &str)]
#[implementations(f64, f32, u32, DVec2, bool, String)]
other_value: T,
) -> bool {
other_value != value
@@ -767,7 +767,7 @@ fn vec2_value(_: impl Ctx, _primary: (), x: f64, y: f64) -> DVec2 {
DVec2::new(x, y)
}
/// Constructs a color value which may be set to any color, or no color.
/// Constructs a color value which may be set to any color.
#[node_macro::node(category("Value"))]
fn color_value(_: impl Ctx, _primary: (), #[default(Color::BLACK)] color: Color) -> Color {
color

View File

@@ -1,9 +1,11 @@
use core_types::list::{Item, List};
use core_types::{ATTR_TYPE, Ctx};
use crate::{expanded_count, locate_expanded, unescape_string};
use core_types::attribute::{Attr, Type};
use core_types::extent::{LevelIn, ListIn, ValueIn};
use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt};
use core_types::node::Lane;
use core_types::{Ctx, ExtractIndex, InjectIndex};
use serde_json::Value;
use crate::unescape_string;
// ===========
// Format JSON
// ===========
@@ -221,12 +223,12 @@ fn query_json(
/// • **Index Elements**: access the `N`th query result.
/// • **String to Number**: convert numeric query results to numbers.
/// • **String Value** → **Equals**: convert "true", "false", or "null" query results to bools.
#[node_macro::node(name("Query JSON All"), category("Text: JSON"))]
fn query_json_all(
_: impl Ctx,
/// The JSON string to extract values from.
#[node_macro::node(name("Query JSON All"), category("Text: JSON"), extent(query_json_all_extent))]
fn query_json_all<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
/// The JSON strings to extract values from.
#[name("JSON")]
json: String,
json: IList<String>,
/// Determines which contained values to extract from within the JSON.
///
/// The path syntax is like JavaScript's accessor syntax that follows an array/object value. It also supports negative indexing to count backwards from the end. Additionally, `[]` accesses all array and object values instead of just one.
@@ -240,15 +242,33 @@ fn query_json_all(
/// Strips the surrounding double quotes from string values, returning the raw text. Other types are never wrapped in quotes.
#[default(true)]
unquote_strings: bool,
) -> List<String> {
let cleaned = strip_trailing_commas(&json);
let Ok(value): Result<Value, _> = serde_json::from_str(&cleaned) else { return List::new() };
let Some(segments) = parse_json_path(path.trim()) else { return List::new() };
) -> Result<IList<(Lane<String>, Attr<'e, Type>)>, Interrupt> {
let (row, (text, ty)) = locate_expanded(json, ctx.index() as usize, |json| query_all(json, &path, unquote_strings)).ok_or_else(|| Interrupt::from(GraphError::past_end()))?;
Ok((json.lane(row).map_element(text), Attr(ty)))
}
/// Every value `path` matches in `json` with its JSON type, none for invalid
/// JSON or an invalid path.
fn query_all(json: &str, path: &str, unquote_strings: bool) -> Vec<(String, &'static str)> {
let cleaned = strip_trailing_commas(json);
let Ok(value): Result<Value, _> = serde_json::from_str(&cleaned) else { return Vec::new() };
let Some(segments) = parse_json_path(path.trim()) else { return Vec::new() };
let mut results = Vec::new();
resolve_all(&value, &segments, !unquote_strings, &mut results);
results
}
results.into_iter().map(|(text, ty)| Item::new_from_element(text).with_attribute(ATTR_TYPE, ty.to_string())).collect()
/// The level holds every string's matched values in order.
fn query_json_all_extent(json: ListIn<'_, String>, path: ValueIn<'_, String>, unquote_strings: ValueIn<'_, bool>, level: LevelIn) -> GPoll<Extent> {
match level.top() {
true => json
.get()
.zip(path.get())
.zip(unquote_strings.get())
.map(|((json, path), unquote_strings)| expanded_count(json, |json| query_all(json, &path, unquote_strings).len())),
false => GPoll::Final(Extent::Exactly(1)),
}
}
/// A parsed segment of a JSON access path.

View File

@@ -8,11 +8,12 @@ mod text_context;
mod to_path;
use convert_case::{Boundary, Converter, pattern};
use core_types::gpoll::Interrupt;
use core_types::extent::{LevelIn, ListIn, ValueIn};
use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt};
use core_types::graphene_hash::CacheHash;
use core_types::list::{Item, List};
use core_types::node::Lane;
use core_types::registry::types::{SignedInteger, TextArea};
use core_types::{Context, Ctx, DeriveCtx, ExtractVarArgs};
use core_types::{Ctx, ExtractIndex, InjectIndex};
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use unicode_segmentation::UnicodeSegmentation;
@@ -361,7 +362,7 @@ fn format_number(
}
/// Parses a string into a number. Falls back to the chosen value if the string is not a valid number.
#[node_macro::node(category("Text"))]
#[node_macro::node(category("Text"), name("String to Number"))]
fn string_to_number(
_: impl Ctx,
/// The string containing a number. Surrounding whitespace is ignored, a decimal point (.) may be included, sign prefixes (+/-) are respected, and scientific notation (e.g. "1e-3") is supported.
@@ -727,14 +728,43 @@ fn string_length(_: impl Ctx, string: String) -> f64 {
string.graphemes(true).count() as f64
}
/// Splits a string into a list of substrings based on the specified delimiter. This is the inverse of the **String Join** node.
/// The `lane`-th row of the level made by expanding every string in order,
/// with the row of the string it came from.
pub(crate) fn locate_expanded<R>(strings: core_types::node::List<'_, String>, lane: usize, expand: impl Fn(&str) -> Vec<R>) -> Option<(usize, R)> {
let mut remaining = lane;
for row in 0..strings.len() {
let mut expanded = expand(strings.element_ref(row));
if remaining >= expanded.len() {
remaining -= expanded.len();
continue;
}
return Some((row, expanded.swap_remove(remaining)));
}
None
}
/// The rows every string expands to, summed.
pub(crate) fn expanded_count(strings: core_types::node::List<'_, String>, expand: impl Fn(&str) -> usize) -> Extent {
Extent::Exactly((0..strings.len()).map(|row| expand(strings.element_ref(row))).sum())
}
/// The parts of `string` around `delimiter`, unescaped when asked.
fn split_parts(string: &str, delimiter: &str, delimiter_escaping: bool) -> Vec<String> {
let delimiter = match delimiter_escaping {
true => unescape_string(delimiter.to_string()),
false => delimiter.to_string(),
};
string.split(&delimiter).map(str::to_string).collect()
}
/// Splits each string into substrings based on the specified delimiter, producing one flat list of all the substrings. This is the inverse of the **String Join** node.
///
/// For example, splitting "a, b, c" with delimiter ", " produces `["a", "b", "c"]`.
#[node_macro::node(category("Text"))]
#[node_macro::node(category("Text"), extent(string_split_extent))]
fn string_split(
_: impl Ctx,
/// The string to split into substrings.
string: String,
ctx: impl Ctx + ExtractIndex + InjectIndex + Copy,
/// The strings to split into substrings.
strings: IList<String>,
/// The character(s) that separate the substrings. These are not included in the outputs.
#[default("\\n")]
delimiter: String,
@@ -742,10 +772,21 @@ fn string_split(
/// "\n" (newline), "\r" (carriage return), "\t" (tab), "\0" (null), and "\\" (backslash).
#[default(true)]
delimiter_escaping: bool,
) -> List<String> {
let delimiter = if delimiter_escaping { unescape_string(delimiter) } else { delimiter };
) -> Result<IList<Lane<String>>, Interrupt> {
let (row, part) = locate_expanded(strings, ctx.index() as usize, |string| split_parts(string, &delimiter, delimiter_escaping)).ok_or_else(|| Interrupt::from(GraphError::past_end()))?;
Ok(strings.lane(row).map_element(part))
}
string.split(&delimiter).map(str::to_string).map(Item::new_from_element).collect()
/// The level holds every string's parts in order.
fn string_split_extent(strings: ListIn<'_, String>, delimiter: ValueIn<'_, String>, delimiter_escaping: ValueIn<'_, bool>, level: LevelIn) -> GPoll<Extent> {
match level.top() {
true => strings
.get()
.zip(delimiter.get())
.zip(delimiter_escaping.get())
.map(|((strings, delimiter), escaping)| expanded_count(strings, |string| split_parts(string, &delimiter, escaping).len())),
false => GPoll::Final(Extent::Exactly(1)),
}
}
/// Joins a list of strings together with a separator between each pair. This is the inverse of the **String Split** node.
@@ -755,7 +796,7 @@ fn string_split(
fn string_join(
_: impl Ctx,
/// The list of strings to join together.
strings: List<String>,
strings: IList<String>,
/// The text placed between each pair of strings.
#[default(", ")]
separator: String,
@@ -766,39 +807,7 @@ fn string_join(
) -> String {
let separator = if separator_escaping { unescape_string(separator) } else { separator };
strings.iter_element_values().map(|s| s.as_str()).collect::<Vec<_>>().join(&separator)
}
/// Iterates over a list of strings, evaluating the mapped operation for each one. Use the **Read String** node to access the current string inside the loop.
#[node_macro::node(category("Text"))]
fn map_string(
ctx: impl Ctx + DeriveCtx,
strings: List<String>,
#[expose]
#[implementations(Context -> String)]
mapped: impl Node<Context<'_>, Output = String>,
) -> Result<List<String>, Interrupt> {
let spilled = ctx.index_head();
let mut result = List::new();
for (i, row) in strings.into_iter().enumerate() {
let string = row.into_element();
let scoped = ctx.push_vararg(&string);
let mapped_string = mapped.eval(&scoped.ctx().promoted(&spilled, i as u64))?;
result.push(Item::new_from_element(mapped_string));
}
Ok(result)
}
/// Reads the current string from within a **Map String** node's loop.
#[node_macro::node(category("Context"))]
fn read_string(ctx: impl Ctx + ExtractVarArgs) -> String {
let Ok(var_arg) = ctx.vararg(0) else { return String::new() };
let var_arg = var_arg as &dyn std::any::Any;
var_arg.downcast_ref::<String>().cloned().unwrap_or_default()
(0..strings.len()).map(|row| strings.element_ref(row).as_str()).collect::<Vec<_>>().join(&separator)
}
/// Converts a value to a JSON string representation.

View File

@@ -202,7 +202,7 @@ impl PathBuilder {
}
}
// "Separate Glyphs" off: widen the accumulated AABBs and bundle as one override `Vector`
// Glyph separation off: widen the accumulated AABBs and bundle as one override `Vector`
if !self.merged_click_target_bboxes.is_empty() {
let mut bboxes = self.merged_click_target_bboxes;
widen_horizontal_gaps(&mut bboxes, &self.merged_click_target_baselines);

View File

@@ -1,6 +1,10 @@
use core_types::list::{Item, List};
use crate::{expanded_count, locate_expanded};
use core_types::attribute::{Attr, End, Name, Start};
use core_types::extent::{LevelIn, ListIn, ValueIn};
use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt};
use core_types::node::Lane;
use core_types::registry::types::SignedInteger;
use core_types::{ATTR_END, ATTR_NAME, ATTR_START, Ctx};
use core_types::{Ctx, ExtractIndex, InjectIndex};
/// Checks whether the string contains a match for the given regular expression pattern. Optionally restricts the match to only the start and/or end of the string.
#[node_macro::node(category("Text: Regex"))]
@@ -77,17 +81,101 @@ fn regex_replace(
}
}
/// Finds a regex match in the string and returns its components. The result is a list where the first item is the whole match (`$0`) and subsequent items are the capture groups (`$1`, `$2`, etc., if any).
/// The pattern with its flag prefix compiled, or nothing for an empty or
/// invalid pattern (the latter logged).
fn compile_regex(pattern: &str, case_insensitive: bool, multiline: bool) -> Option<fancy_regex::Regex> {
if pattern.is_empty() {
return None;
}
let flags = match (case_insensitive, multiline) {
(false, false) => "",
(true, false) => "(?i)",
(false, true) => "(?m)",
(true, true) => "(?im)",
};
match fancy_regex::Regex::new(&format!("{flags}{pattern}")) {
Ok(regex) => Some(regex),
Err(_) => {
log::error!("Invalid regex pattern: {pattern}");
None
}
}
}
/// One matched substring with its byte range in the searched string and, for
/// a capture, the group's name.
struct Span {
text: String,
start: u64,
end: u64,
name: String,
}
/// The whole match then each capture group of the `match_index`-th match,
/// empty where the index resolves to no match.
fn capture_spans(regex: &fancy_regex::Regex, string: &str, match_index: f64) -> Vec<Span> {
// Capture group names indexed positionally; index 0 (the whole match) is always None.
let capture_names: Vec<Option<String>> = regex.capture_names().map(|name| name.map(str::to_string)).collect();
// Collect all matches since we need to support negative indexing
let matches: Vec<_> = regex.captures_iter(string).filter_map(|c| c.ok()).collect();
let match_index = match_index as i32;
let resolved_index = match match_index < 0 {
true => match matches.len().checked_sub((-match_index) as usize) {
Some(index) => index,
None => return Vec::new(),
},
false => match_index as usize,
};
let Some(captures) = matches.get(resolved_index) else {
return Vec::new();
};
(0..captures.len())
.map(|i| {
let captured = captures.get(i);
Span {
text: captured.map_or(String::new(), |m| m.as_str().to_string()),
start: captured.map_or(0, |m| m.start() as u64),
end: captured.map_or(0, |m| m.end() as u64),
name: capture_names.get(i).cloned().flatten().unwrap_or_default(),
}
})
.collect()
}
fn match_spans(regex: &fancy_regex::Regex, string: &str) -> Vec<Span> {
regex
.find_iter(string)
.filter_map(|m| m.ok())
.map(|m| Span {
text: m.as_str().to_string(),
start: m.start() as u64,
end: m.end() as u64,
name: String::new(),
})
.collect()
}
/// The parts of `string` between matches, the whole string without a usable pattern.
fn split_parts(regex: Option<&fancy_regex::Regex>, string: &str) -> Vec<String> {
match regex {
Some(regex) => regex.split(string).filter_map(|s| s.ok()).map(str::to_string).collect(),
None => vec![string.to_string()],
}
}
/// Finds a regex match in each string and returns its components, as one flat list where a match contributes the whole match (`$0`) followed by its capture groups (`$1`, `$2`, etc., if any).
///
/// The match index selects which non-overlapping occurrence to return (0 for the first match). Returns an empty list if no match is found at the given index.
/// The match index selects which non-overlapping occurrence to return (0 for the first match). A string contributes nothing if no match is found at the given index.
///
/// Each item carries `start` and `end` byte-offset attributes pointing into the original string, plus a `name` attribute holding
/// Each item carries `start` and `end` byte-offset attributes pointing into its original string, plus a `name` attribute holding
/// the capture group's name (empty for unnamed groups, and for index 0 which is the whole match).
#[node_macro::node(category(""))]
fn regex_find(
_: impl Ctx,
/// The string to search within.
string: String,
#[node_macro::node(category(""), extent(regex_find_extent))]
fn regex_find<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
/// The strings to search within.
strings: IList<String>,
/// The regular expression pattern to search for.
pattern: String,
/// Which non-overlapping occurrence of the pattern to return, starting from 0 for the first match. Negative indices count backwards from the last match.
@@ -96,135 +184,109 @@ fn regex_find(
case_insensitive: bool,
/// Make `^` and `$` match the start and end of each line, not just the whole string.
multiline: bool,
) -> List<String> {
if pattern.is_empty() {
return List::new();
}
let flags = match (case_insensitive, multiline) {
(false, false) => "",
(true, false) => "(?i)",
(false, true) => "(?m)",
(true, true) => "(?im)",
};
let full_pattern = format!("{flags}{pattern}");
let Ok(regex) = fancy_regex::Regex::new(&full_pattern) else {
log::error!("Invalid regex pattern: {pattern}");
return List::new();
};
// Capture group names indexed positionally; index 0 (the whole match) is always None.
let capture_names: Vec<Option<String>> = regex.capture_names().map(|name| name.map(str::to_string)).collect();
// Collect all matches since we need to support negative indexing
let matches: Vec<_> = regex.captures_iter(&string).filter_map(|c| c.ok()).collect();
let match_index = match_index as i32;
let resolved_index = if match_index < 0 {
let from_end = (-match_index) as usize;
if from_end > matches.len() {
return List::new();
}
matches.len() - from_end
} else {
match_index as usize
};
let Some(captures) = matches.get(resolved_index) else {
return List::new();
};
// Index 0 is the whole match, 1+ are capture groups
(0..captures.len())
.map(|i| {
let captured = captures.get(i);
let text = captured.map_or(String::new(), |m| m.as_str().to_string());
let start = captured.map_or(0_u64, |m| m.start() as u64);
let end = captured.map_or(0_u64, |m| m.end() as u64);
let name = capture_names.get(i).cloned().flatten().unwrap_or_default();
Item::new_from_element(text)
.with_attribute(ATTR_START, start)
.with_attribute(ATTR_END, end)
.with_attribute(ATTR_NAME, name)
})
.collect()
) -> Result<IList<(Lane<String>, Attr<'e, Start>, Attr<'e, End>, Attr<'e, Name>)>, Interrupt> {
let regex = compile_regex(&pattern, case_insensitive, multiline);
let (row, span) = locate_expanded(strings, ctx.index() as usize, |string| {
regex.as_ref().map_or_else(Vec::new, |regex| capture_spans(regex, string, match_index))
})
.ok_or_else(|| Interrupt::from(GraphError::past_end()))?;
let (name, _) = ctx.arena().alloc(span.name).ok_or_else(|| Interrupt::from(GraphError::new("the arena is exhausted")))?;
Ok((strings.lane(row).map_element(span.text), Attr(span.start), Attr(span.end), Attr(name.as_str())))
}
/// Finds all non-overlapping matches of a regular expression pattern in the string, returning a list of the matched substrings.
/// The level holds every string's captures in order.
fn regex_find_extent(
strings: ListIn<'_, String>,
pattern: ValueIn<'_, String>,
match_index: ValueIn<'_, f64>,
case_insensitive: ValueIn<'_, bool>,
multiline: ValueIn<'_, bool>,
level: LevelIn,
) -> GPoll<Extent> {
match level.top() {
true => strings
.get()
.zip(pattern.get())
.zip(match_index.get())
.zip(case_insensitive.get())
.zip(multiline.get())
.map(|((((strings, pattern), match_index), case_insensitive), multiline)| {
let regex = compile_regex(&pattern, case_insensitive, multiline);
expanded_count(strings, |string| regex.as_ref().map_or(0, |regex| capture_spans(regex, string, match_index).len()))
}),
false => GPoll::Final(Extent::Exactly(1)),
}
}
/// Finds all non-overlapping matches of a regular expression pattern in each string, returning one flat list of the matched substrings.
///
/// Each item carries `start` and `end` byte-offset attributes pointing into the original string.
#[node_macro::node(category("Text: Regex"))]
fn regex_find_all(
_: impl Ctx,
/// The string to search within.
string: String,
/// Each item carries `start` and `end` byte-offset attributes pointing into its original string.
#[node_macro::node(category("Text: Regex"), extent(regex_find_all_extent))]
fn regex_find_all<'e>(
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
/// The strings to search within.
strings: IList<String>,
/// The regular expression pattern to search for.
pattern: String,
/// Match letters regardless of case.
case_insensitive: bool,
/// Make `^` and `$` match the start and end of each line, not just the whole string.
multiline: bool,
) -> List<String> {
if pattern.is_empty() {
return List::new();
}
let flags = match (case_insensitive, multiline) {
(false, false) => "",
(true, false) => "(?i)",
(false, true) => "(?m)",
(true, true) => "(?im)",
};
let full_pattern = format!("{flags}{pattern}");
let Ok(regex) = fancy_regex::Regex::new(&full_pattern) else {
log::error!("Invalid regex pattern: {pattern}");
return List::new();
};
regex
.find_iter(&string)
.filter_map(|m| m.ok())
.map(|m| {
Item::new_from_element(m.as_str().to_string())
.with_attribute(ATTR_START, m.start() as u64)
.with_attribute(ATTR_END, m.end() as u64)
})
.collect()
) -> Result<IList<(Lane<String>, Attr<'e, Start>, Attr<'e, End>)>, Interrupt> {
let regex = compile_regex(&pattern, case_insensitive, multiline);
let (row, span) =
locate_expanded(strings, ctx.index() as usize, |string| regex.as_ref().map_or_else(Vec::new, |regex| match_spans(regex, string))).ok_or_else(|| Interrupt::from(GraphError::past_end()))?;
Ok((strings.lane(row).map_element(span.text), Attr(span.start), Attr(span.end)))
}
/// Splits a string into a list of substrings pulled from between separator characters as matched by a regular expression.
/// The level holds every string's matches in order.
fn regex_find_all_extent(strings: ListIn<'_, String>, pattern: ValueIn<'_, String>, case_insensitive: ValueIn<'_, bool>, multiline: ValueIn<'_, bool>, level: LevelIn) -> GPoll<Extent> {
match level.top() {
true => strings
.get()
.zip(pattern.get())
.zip(case_insensitive.get())
.zip(multiline.get())
.map(|(((strings, pattern), case_insensitive), multiline)| {
let regex = compile_regex(&pattern, case_insensitive, multiline);
expanded_count(strings, |string| regex.as_ref().map_or(0, |regex| match_spans(regex, string).len()))
}),
false => GPoll::Final(Extent::Exactly(1)),
}
}
/// Splits each string into substrings pulled from between separator characters as matched by a regular expression, producing one flat list of all the substrings.
///
/// For example, splitting "Three, two, one... LIFTOFF" with pattern `\W+` (non-word characters) produces `["Three", "two", "one", "LIFTOFF"]`.
#[node_macro::node(category("Text: Regex"))]
#[node_macro::node(category("Text: Regex"), extent(regex_split_extent))]
fn regex_split(
_: impl Ctx,
/// The string to split into substrings.
string: String,
ctx: impl Ctx + ExtractIndex + InjectIndex + Copy,
/// The strings to split into substrings.
strings: IList<String>,
/// The regular expression pattern to split on. Matches are consumed and not included in the output.
pattern: String,
/// Match letters regardless of case.
case_insensitive: bool,
/// Make `^` and `$` match the start and end of each line, not just the whole string.
multiline: bool,
) -> List<String> {
if pattern.is_empty() {
return List::new_from_element(string);
}
let flags = match (case_insensitive, multiline) {
(false, false) => "",
(true, false) => "(?i)",
(false, true) => "(?m)",
(true, true) => "(?im)",
};
let full_pattern = format!("{flags}{pattern}");
let Ok(regex) = fancy_regex::Regex::new(&full_pattern) else {
log::error!("Invalid regex pattern: {pattern}");
return List::new_from_element(string);
};
regex.split(&string).filter_map(|s| s.ok()).map(|s| s.to_string()).map(Item::new_from_element).collect()
) -> Result<IList<Lane<String>>, Interrupt> {
let regex = compile_regex(&pattern, case_insensitive, multiline);
let (row, part) = locate_expanded(strings, ctx.index() as usize, |string| split_parts(regex.as_ref(), string)).ok_or_else(|| Interrupt::from(GraphError::past_end()))?;
Ok(strings.lane(row).map_element(part))
}
/// The level holds every string's parts in order.
fn regex_split_extent(strings: ListIn<'_, String>, pattern: ValueIn<'_, String>, case_insensitive: ValueIn<'_, bool>, multiline: ValueIn<'_, bool>, level: LevelIn) -> GPoll<Extent> {
match level.top() {
true => strings
.get()
.zip(pattern.get())
.zip(case_insensitive.get())
.zip(multiline.get())
.map(|(((strings, pattern), case_insensitive), multiline)| {
let regex = compile_regex(&pattern, case_insensitive, multiline);
expanded_count(strings, |string| split_parts(regex.as_ref(), string).len())
}),
false => GPoll::Final(Extent::Exactly(1)),
}
}

View File

@@ -6,9 +6,8 @@ use core_types::gpoll::{Extent, GPoll, Interrupt};
use core_types::transform::{ApplyTransform, ScaleType, Transform};
use core_types::{CacheHash, Context, Ctx, DeriveCtx, InjectFootprint, ModifyFootprint};
use glam::{DAffine2, DMat2, DVec2};
use graphic_types::Graphic;
use graphic_types::Vector;
use graphic_types::raster_types::{CPU, GPU, Raster};
use graphic_types::{Artboard, Graphic, Vector};
use vector_types::Gradient;
/// Applies the specified transform to each lane of the input, composing onto the lane's transform attribute.
@@ -97,7 +96,10 @@ fn replace_transform<T>(_: impl Ctx + InjectFootprint, (element, _content_transf
// TODO: Figure out how this node should behave once #2982 is implemented.
/// Obtains the transform of the first lane of the input, if present.
#[node_macro::node(category("Math: Transform"), path(core_types::vector))]
fn extract_transform<T: Clone + Send + Sync + CacheHash + 'static>(_: impl Ctx, #[implementations(Graphic, Vector, Raster<CPU>, Raster<GPU>, Color, Gradient)] content: IList<T>) -> DAffine2 {
fn extract_transform<T: Clone + Send + Sync + CacheHash + 'static>(
_: impl Ctx,
#[implementations(Graphic, Vector, Raster<CPU>, Raster<GPU>, Color, Gradient, String, Artboard)] content: IList<T>,
) -> DAffine2 {
match content.len() {
0 => DAffine2::default(),
_ => content.lane(0).attr::<TransformAttr>(),

View File

@@ -1,5 +1,6 @@
use core_types::attribute::{Attr, EditorLayerPath, RemoveAttr, Transform as TransformAttr};
use core_types::gpoll::{GraphError, Interrupt};
use core_types::transform::BakeTransform;
use core_types::uuid::NodeId;
use core_types::{Ctx, ExtractIndex, InjectIndex};
use glam::DAffine2;
@@ -38,14 +39,12 @@ fn path_modify<'e>(
Ok((element, Attr(parked.as_slice()), RemoveAttr::new()))
}
/// Applies the vector path's local transformation to its geometry and resets the transform to the identity.
/// Bakes the content's transform attribute into its underlying value, resetting the attribute to the identity.
#[node_macro::node(category("Vector"))]
fn apply_transform(_ctx: impl Ctx, (mut vector, transform): (Vector, Attr<TransformAttr>)) -> (Vector, Attr<TransformAttr>) {
// Monomorphic on Vector: our macro cannot yet read a record element through an open generic, so master's DAffine2 and DVec2 rows have no node here.
fn bake_transform(_ctx: impl Ctx, (mut content, transform): (Vector, Attr<TransformAttr>)) -> (Vector, Attr<TransformAttr>) {
let transform: DAffine2 = *transform;
for (_, point) in vector.point_domain.positions_mut() {
*point = transform.transform_point2(*point);
}
vector.segment_domain.transform(transform);
content.bake_transform(&transform);
(vector, Attr(DAffine2::IDENTITY))
(content, Attr(DAffine2::IDENTITY))
}

View File

@@ -1278,10 +1278,10 @@ fn dimensions(_: impl Ctx, content: IList<Vector>) -> DVec2 {
.unwrap_or_default()
}
/// Type-asserts a value to be vector data.
/// Type-asserts a value to be vector data. A position becomes a single-anchor vector.
#[node_macro::node(category("Vector"), name("As Vector"), path(core_types::vector))]
fn as_vector(_: impl Ctx, value: Vector) -> Vector {
value
fn as_vector<T: Into<Vector>>(_: impl Ctx, #[implementations(Vector, DVec2)] value: T) -> Vector {
value.into()
}
/// Creates a polyline from a series of vector points, replacing any existing segments and regions that may already exist.