Reshape the node catalog: rename the list access nodes, split Text to Vector, reinstate Upload Texture as a proto node, and delete the vestigial debug nodes

This commit is contained in:
Keavon Chambers
2026-07-20 16:37:43 -07:00
committed by Dennis Kobert
parent 194c3d0a72
commit 83cfd0225a
11 changed files with 178 additions and 243 deletions

View File

@@ -1,6 +1,5 @@
use crate::WgpuExecutorHandle;
use core_types::Color;
use core_types::Ctx;
use core_types::color::SRGBA8;
use core_types::list::{Item, List};
use core_types::ops::{Convert, ConvertAsync};
@@ -250,15 +249,3 @@ impl ConvertAsync<Raster<CPU>, WgpuExecutorHandle> for Raster<GPU> {
Box::pin(async move { converter.convert(&device).await.expect("Failed to download texture data") })
}
}
/// Uploads an raster texture from the CPU to the GPU. This is now deprecated and the Convert node should be used in the future.
///
/// Accepts either individual raster data or a `List` of raster elements and converts it to the GPU format using the WgpuExecutor's device and queue.
#[node_macro::node(category(""))]
pub fn upload_texture<T: Convert<List<Raster<GPU>>, WgpuExecutorHandle>>(
_: impl Ctx,
#[implementations(List<Raster<CPU>>, List<Raster<GPU>>)] input: T,
executor: WgpuExecutorHandle,
) -> List<Raster<GPU>> {
input.convert(Footprint::DEFAULT, executor)
}

View File

@@ -1,6 +1,5 @@
use core_types::Ctx;
use glam::{DAffine2, DVec2};
use raster_types::{CPU, Raster};
/// Meant for debugging purposes, not general use. Logs the input value to the console and passes it through unchanged.
#[node_macro::node(category("Debug"), name("Log to Console"))]
@@ -9,27 +8,3 @@ fn log_to_console<T: std::fmt::Debug>(_: impl Ctx, #[implementations(bool, f64,
log::debug!("{value:#?}");
value
}
/// Meant for debugging purposes, not general use. Returns the size of the input type in bytes.
#[node_macro::node(category("Debug"))]
fn size_of(_: impl Ctx, ty: core_types::Type) -> Option<usize> {
ty.size()
}
/// Meant for debugging purposes, not general use. Wraps the input value in the Some variant of an Option.
#[node_macro::node(category("Debug"))]
fn some<T>(_: impl Ctx, #[implementations(f64, f32, u32, u64, String)] input: T) -> Option<T> {
Some(input)
}
/// Meant for debugging purposes, not general use. Unwraps the input value from an Option, returning the default value if the input is None.
#[node_macro::node(category("Debug"))]
fn unwrap_option<T: Default>(_: impl Ctx, #[implementations(Option<f64>, Option<u32>, Option<u64>, Option<String>)] input: Option<T>) -> T {
input.unwrap_or_default()
}
/// Clones the element out of its record input.
#[node_macro::node(category("Debug"))]
fn clone<T: Clone>(_: impl Ctx, #[implementations(Raster<CPU>, f64)] value: &T) -> T {
value.clone()
}

View File

@@ -25,43 +25,10 @@ fn resolve_index(index: f64, total: u64) -> Option<u64> {
}
}
/// Returns a one-lane level holding the item at the specified index with its
/// attributes, or an empty level when the index is out of range.
#[node_macro::node(category("General"), extent(index_elements_extent))]
pub fn index_elements<T>(
ctx: impl Ctx + ModifyIndex + Copy,
/// The list of data.
list: impl Node<Context<'_>, Output = 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,
) -> 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("index elements over a non-exact extent").into()),
};
let Some(source) = resolve_index(index, total) else {
return Err(GraphError::new("index elements addressed its empty selection").into());
};
let mut shifted = *ctx;
shifted.set_index(source);
list.eval(&shifted)
}
fn index_elements_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) => Extent::Exactly(resolve_index(index, count as u64).is_some() as usize),
_ => Extent::Exactly(1),
}),
false => list.at(level),
}
}
/// Returns the list with the element at the specified index removed.
/// 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"), extent(omit_element_extent))]
pub fn omit_element<T>(
#[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>,
@@ -96,8 +63,8 @@ fn omit_element_extent(list: ExtentIn<'_>, index: ValueIn<'_, f64>, level: 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"))]
pub fn extract_element<T: Clone + Default + Send + Sync + CacheHash + 'static>(
#[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)]

View File

@@ -6,15 +6,14 @@ 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")]
use core_types::ATTR_TRANSFORM;
#[cfg(target_family = "wasm")]
use core_types::math::bbox::Bbox;
use core_types::ops::Convert;
use core_types::runtime::SourceFuture;
#[cfg(target_family = "wasm")]
use core_types::transform::Footprint;
use core_types::{Color, Ctx};
pub use graph_craft::application_io::resource::{Resource, ResourceHash};
@@ -31,7 +30,7 @@ use graphic_types::Vector;
#[cfg(target_family = "wasm")]
use graphic_types::markers::EditorMergedLayers;
use graphic_types::raster_types::Image;
use graphic_types::raster_types::{CPU, Raster};
use graphic_types::raster_types::{CPU, GPU, Raster};
#[cfg(target_family = "wasm")]
use graphic_types::vector_types::gradient::Gradient;
#[cfg(target_family = "wasm")]
@@ -308,3 +307,13 @@ pub fn wgpu_executor(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: A
pub fn try_wgpu_executor(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: Arc<PlatformEditorApi>) -> Option<::wgpu_executor::WgpuExecutorHandle> {
editor_api.application_io.as_ref()?.gpu_executor_arc().map(::wgpu_executor::WgpuExecutorHandle)
}
/// 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>> {
content.convert(Footprint::DEFAULT, executor)
}

View File

@@ -90,15 +90,24 @@ fn text(
list
}
/// Converts a styled `String[]` into vector geometry.
/// 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>,
/// Whether to split every letterform into its own vector item. Otherwise, a single vector compound path is produced.
separate_glyphs: bool,
) -> List<Vector> {
shape_text_list(&strings, separate_glyphs)
shape_text_list(&strings, false)
}
/// 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)
}

View File

@@ -2,7 +2,7 @@ use super::TypesettingConfig;
use super::text_context::TextContext;
use crate::markers::{ATTR_FONT, ATTR_TEXT_ALIGN};
use core_types::blending::BlendMode;
use core_types::list::List;
use core_types::list::{Item, List};
use core_types::uuid::NodeId;
use core_types::{
ATTR_BLEND_MODE, ATTR_EDITOR_LAYER_PATH, ATTR_FONT_SIZE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM,
@@ -23,60 +23,71 @@ pub fn lines_clipping(text: &str, font: &Resource, typesetting: TypesettingConfi
TextContext::with_thread_local(|ctx| ctx.lines_clipping(text, font, typesetting))
}
/// Shapes each string item of a styled `List<String>` into vector geometry, reading its font and typesetting
/// from the item's attributes (as set by the 'Text' node) and re-applying its transform and blending
/// attributes onto the produced paths. With `separate_glyphs`, each glyph becomes its own item.
/// Shapes a single styled string item into vector geometry, reading its font and typesetting from the item's
/// attributes (as set by the 'Text' node) and re-applying its transform and blending attributes onto the produced
/// paths. With `separate_glyphs`, each glyph becomes its own item; otherwise a single compound path is produced.
pub fn shape_text_item(item: &Item<String>, separate_glyphs: bool) -> List<Vector> {
let text = item.element();
if text.is_empty() {
return List::new();
}
// Use fallback font when none is explicitly attached.
let font: Resource = {
let font: Resource = item.attribute_cloned_or_default(ATTR_FONT);
if font.is_empty() { super::FALLBACK_FONT_RESOURCE.clone() } else { font }
};
let defaults = TypesettingConfig::default();
let typesetting = TypesettingConfig {
font_size: item.attribute_cloned_or(ATTR_FONT_SIZE, defaults.font_size),
line_height_ratio: item.attribute_cloned_or(ATTR_LINE_HEIGHT, defaults.line_height_ratio),
letter_spacing: item.attribute_cloned_or(ATTR_LETTER_SPACING, defaults.letter_spacing),
letter_tilt: item.attribute_cloned_or(ATTR_LETTER_TILT, defaults.letter_tilt),
max_width: item.attribute_cloned_or::<Option<f64>>(ATTR_MAX_WIDTH, defaults.max_width),
max_height: item.attribute_cloned_or::<Option<f64>>(ATTR_MAX_HEIGHT, defaults.max_height),
align: item.attribute_cloned_or(ATTR_TEXT_ALIGN, defaults.align),
};
let vectors = to_path(text, &font, typesetting, separate_glyphs);
let transform = item.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM);
let layer_path = item.attribute::<Vec<NodeId>>(ATTR_EDITOR_LAYER_PATH).cloned();
let blend_mode = item.attribute::<BlendMode>(ATTR_BLEND_MODE).copied();
let opacity = item.attribute::<f64>(ATTR_OPACITY).copied();
let opacity_fill = item.attribute::<f64>(ATTR_OPACITY_FILL).copied();
let mut result = List::new();
for mut produced in vectors.into_iter() {
if transform != DAffine2::IDENTITY {
let local = produced.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM);
produced.set_attribute(ATTR_TRANSFORM, transform * local);
}
if let Some(layer_path) = &layer_path {
produced.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone());
}
if let Some(blend_mode) = blend_mode {
produced.set_attribute(ATTR_BLEND_MODE, blend_mode);
}
if let Some(opacity) = opacity {
produced.set_attribute(ATTR_OPACITY, opacity);
}
if let Some(opacity_fill) = opacity_fill {
produced.set_attribute(ATTR_OPACITY_FILL, opacity_fill);
}
result.push(produced);
}
result
}
/// Shapes each string item of a styled `List<String>` into vector geometry, flattening the per-item results.
pub fn shape_text_list(strings: &List<String>, separate_glyphs: bool) -> List<Vector> {
let mut result = List::new();
for index in 0..strings.len() {
let Some(text) = strings.element(index) else { continue };
if text.is_empty() {
continue;
}
// Use fallback font when none is explicitly attached.
let font: Resource = {
let f: Resource = strings.attribute_cloned_or_default(ATTR_FONT, index);
if f.is_empty() { super::FALLBACK_FONT_RESOURCE.clone() } else { f }
};
let defaults = TypesettingConfig::default();
let typesetting = TypesettingConfig {
font_size: strings.attribute_cloned_or(ATTR_FONT_SIZE, index, defaults.font_size),
line_height_ratio: strings.attribute_cloned_or(ATTR_LINE_HEIGHT, index, defaults.line_height_ratio),
letter_spacing: strings.attribute_cloned_or(ATTR_LETTER_SPACING, index, defaults.letter_spacing),
letter_tilt: strings.attribute_cloned_or(ATTR_LETTER_TILT, index, defaults.letter_tilt),
max_width: strings.attribute_cloned_or::<Option<f64>>(ATTR_MAX_WIDTH, index, defaults.max_width),
max_height: strings.attribute_cloned_or::<Option<f64>>(ATTR_MAX_HEIGHT, index, defaults.max_height),
align: strings.attribute_cloned_or(ATTR_TEXT_ALIGN, index, defaults.align),
};
let vectors = to_path(text, &font, typesetting, separate_glyphs);
let transform = strings.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM, index);
let layer_path = strings.attribute_cloned_or_default::<Vec<NodeId>>(ATTR_EDITOR_LAYER_PATH, index);
let blend_mode = strings.attribute::<BlendMode>(ATTR_BLEND_MODE, index).copied();
let opacity = strings.attribute::<f64>(ATTR_OPACITY, index).copied();
let opacity_fill = strings.attribute::<f64>(ATTR_OPACITY_FILL, index).copied();
for mut item in vectors.into_iter() {
if transform != DAffine2::IDENTITY {
let local = item.attribute_cloned_or_default::<DAffine2>(ATTR_TRANSFORM);
item.set_attribute(ATTR_TRANSFORM, transform * local);
}
if !layer_path.is_empty() {
item.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone());
}
if let Some(blend_mode) = blend_mode {
item.set_attribute(ATTR_BLEND_MODE, blend_mode);
}
if let Some(opacity) = opacity {
item.set_attribute(ATTR_OPACITY, opacity);
}
if let Some(opacity_fill) = opacity_fill {
item.set_attribute(ATTR_OPACITY_FILL, opacity_fill);
}
result.push(item);
let Some(item) = strings.clone_item(index) else { continue };
for produced in shape_text_item(&item, separate_glyphs).into_iter() {
result.push(produced);
}
}

View File

@@ -3668,7 +3668,7 @@ fn point_inside(_: impl Ctx, source: IList<Vector>, point: DVec2) -> bool {
// TODO: Return u32, u64, or usize instead of f64 after #1621 is resolved and has allowed us to implement automatic type conversion in the node graph for nodes with generic type inputs.
// TODO: (Currently automatic type conversion only works for concrete types, via the Graphene preprocessor and not the full Graphene type system.)
#[node_macro::node(category("General"), path(graphene_core::vector))]
fn count_elements<T: Clone + Send + Sync + CacheHash + 'static>(_: impl Ctx, #[implementations(Graphic, Artboard, Vector, Raster<CPU>, Color, Gradient, String)] content: IList<T>) -> f64 {
fn list_length<T: Clone + Send + Sync + CacheHash + 'static>(_: impl Ctx, #[implementations(Graphic, Artboard, Vector, Raster<CPU>, Color, Gradient, String)] content: IList<T>) -> f64 {
content.len() as f64
}