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

@@ -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)
}