Make the node graph use Table<GradientStops> instead of GradientStops (#3837)

* Switch from GradientStops to Table<GradientStops> in all nodes

* Remove TaggedValue::ColorNotInTable

* Fix bug

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Add migrations

* Fix default gradient on empty table

* Update demo artwork

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Keavon Chambers
2026-02-26 14:07:31 -08:00
committed by GitHub
parent 81c73d11ff
commit f1cbc4b396
26 changed files with 132 additions and 111 deletions

View File

@@ -26,6 +26,7 @@ use std::hash::Hash;
use std::marker::PhantomData;
use std::str::FromStr;
pub use std::sync::Arc;
use text_nodes::vector_types::GradientStop;
pub struct TaggedValueTypeError;
@@ -182,7 +183,6 @@ tagged_value! {
U64(u64),
Bool(bool),
String(String),
ColorNotInTable(Color),
// ========================
// LISTS OF PRIMITIVE TYPES
// ========================
@@ -209,8 +209,10 @@ tagged_value! {
#[serde(alias = "ArtboardGroup")]
Artboard(Table<Artboard>),
#[serde(deserialize_with = "core_types::misc::migrate_color")] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "ColorTable", alias = "OptionalColor")]
#[serde(alias = "ColorTable", alias = "OptionalColor", alias = "ColorNotInTable")]
Color(Table<Color>),
#[serde(deserialize_with = "graphic_types::vector_types::gradient::migrate_gradient_stops")] // TODO: Eventually remove this migration document upgrade code
#[serde(alias = "GradientPositions", alias = "GradientStops")]
GradientTable(Table<GradientStops>),
// ============
// STRUCT TYPES
@@ -222,8 +224,6 @@ tagged_value! {
DAffine2(DAffine2),
Stroke(graphic_types::vector_types::vector::style::Stroke),
Gradient(graphic_types::vector_types::vector::style::Gradient),
#[serde(alias = "GradientPositions")] // TODO: Eventually remove this alias document upgrade code
GradientStops(GradientStops),
Font(text_nodes::Font),
BrushStrokes(Vec<BrushStroke>),
BrushCache(BrushCache),
@@ -333,6 +333,35 @@ impl TaggedValue {
None
}
fn to_gradient(input: &str) -> Option<GradientStops> {
// String syntax: (e.g. "000000ff, ff0000ff")
let stops = input.split(',').filter_map(|s| to_color(s.trim())).collect::<Vec<_>>();
if stops.len() == 1 {
Some(GradientStops::new(vec![
GradientStop {
position: 0.,
midpoint: 0.5,
color: stops[0],
},
GradientStop {
position: 1.,
midpoint: 0.5,
color: stops[0],
},
]))
} else if stops.len() >= 2 {
let step = 1. / (stops.len() - 1) as f64;
Some(GradientStops::new(stops.into_iter().enumerate().map(|(i, color)| GradientStop {
position: i as f64 * step,
midpoint: 0.5,
color,
})))
} else {
log::error!("Invalid default value gradient string: {input}");
None
}
}
fn to_reference_point(input: &str) -> Option<ReferencePoint> {
let mut choices = input.split("::");
let (first, second) = (choices.next()?.trim(), choices.next()?.trim());
@@ -375,9 +404,8 @@ impl TaggedValue {
() if ty == TypeId::of::<u32>() => FromStr::from_str(string).map(TaggedValue::U32).ok()?,
() if ty == TypeId::of::<DVec2>() => to_dvec2(string).map(TaggedValue::DVec2)?,
() if ty == TypeId::of::<bool>() => FromStr::from_str(string).map(TaggedValue::Bool).ok()?,
() if ty == TypeId::of::<Color>() => to_color(string).map(TaggedValue::ColorNotInTable)?,
() if ty == TypeId::of::<Option<Color>>() => TaggedValue::ColorNotInTable(to_color(string)?),
() if ty == TypeId::of::<Table<Color>>() => to_color(string).map(|color| TaggedValue::Color(Table::new_from_element(color)))?,
() if ty == TypeId::of::<Table<GradientStops>>() => to_gradient(string).map(|color| TaggedValue::GradientTable(Table::new_from_element(color)))?,
() if ty == TypeId::of::<Fill>() => to_color(string).map(|color| TaggedValue::Fill(Fill::solid(color)))?,
() if ty == TypeId::of::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?,
_ => return None,
@@ -537,8 +565,3 @@ mod fake_hash {
}
}
}
#[test]
fn can_construct_color() {
assert_eq!(TaggedValue::from_type(&concrete!(Color)).unwrap(), TaggedValue::ColorNotInTable(Color::default()));
}

View File

@@ -94,7 +94,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::StrokeAlign]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Stroke]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Gradient]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => GradientStops]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<graphene_std::uuid::NodeId>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Box<graphene_std::vector::VectorModification>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::CentroidType]),
@@ -157,7 +156,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<Color>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Image<Color>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<GradientStops>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => GradientStops]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<DVec2>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<NodeId>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<f64>]),
@@ -183,7 +181,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
#[cfg(feature = "gpu")]
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<Raster<GPU>>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option<f64>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Color]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option<Color>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => [f64; 4]]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Graphic]),

View File

@@ -12,11 +12,8 @@ use core_types::uuid::{NodeId, generate_uuid};
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use graphic_types::Vector;
use graphic_types::raster_types::BitmapMut;
use graphic_types::raster_types::Image;
use graphic_types::raster_types::{CPU, GPU, Raster};
use graphic_types::vector_types::gradient::GradientStops;
use graphic_types::vector_types::gradient::GradientType;
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster};
use graphic_types::vector_types::gradient::{GradientStops, GradientType};
use graphic_types::vector_types::subpath::Subpath;
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
use graphic_types::vector_types::vector::style::{Fill, PaintOrder, RenderMode, Stroke, StrokeAlign};

View File

@@ -447,6 +447,24 @@ impl Gradient {
}
}
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_gradient_stops<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<core_types::table::Table<GradientStops>, D::Error> {
use core_types::table::Table;
use serde::Deserialize;
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum GradientStopsFormat {
GradientStops(GradientStops),
GradientTable(Table<GradientStops>),
}
Ok(match GradientStopsFormat::deserialize(deserializer)? {
GradientStopsFormat::GradientStops(stops) => Table::new_from_element(stops),
GradientStopsFormat::GradientTable(table) => table,
})
}
impl core_types::bounds::BoundingBox for GradientStops {
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> core_types::bounds::RenderBoundingBox {
core_types::bounds::RenderBoundingBox::Infinite

View File

@@ -1455,11 +1455,9 @@ mod tests {
() -> Table<Raster<CPU>>,
() -> Table<Color>,
() -> Table<GradientStops>,
() -> GradientStops,
Footprint -> Table<Raster<CPU>>,
Footprint -> Table<Color>,
Footprint -> Table<GradientStops>,
Footprint -> GradientStops,
)]
image: impl Node<F, Output = T>,
) -> T {

View File

@@ -3,7 +3,8 @@ use core_types::transform::Footprint;
use core_types::uuid::NodeId;
use core_types::{CloneVarArgs, Color, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl};
use glam::{DAffine2, DVec2};
use graphic_types::{Artboard, Graphic, Vector, vector_types::GradientStops};
use graphic_types::vector_types::GradientStops;
use graphic_types::{Artboard, Graphic, Vector};
use raster_types::{CPU, GPU, Raster};
const DAY: f64 = 1000. * 3600. * 24.;
@@ -84,7 +85,6 @@ async fn quantize_real_time<T>(
Context -> Table<Color>,
Context -> Table<Artboard>,
Context -> Table<GradientStops>,
Context -> GradientStops,
Context -> (),
)]
value: impl Node<'n, Context<'static>, Output = T>,
@@ -128,7 +128,6 @@ async fn quantize_animation_time<T>(
Context -> Table<Color>,
Context -> Table<Artboard>,
Context -> Table<GradientStops>,
Context -> GradientStops,
Context -> (),
)]
value: impl Node<'n, Context<'static>, Output = T>,

View File

@@ -5,7 +5,8 @@ use core_types::transform::Footprint;
use core_types::uuid::NodeId;
use core_types::{Color, OwnedContextImpl};
use glam::{DAffine2, DVec2};
use graphic_types::{Artboard, Graphic, Vector, vector_types::GradientStops};
use graphic_types::vector_types::GradientStops;
use graphic_types::{Artboard, Graphic, Vector};
use raster_types::{CPU, GPU, Raster};
/// Filters out what should be unused components of the context based on the specified requirements.
@@ -37,7 +38,6 @@ async fn context_modification<T>(
Context -> Table<Color>,
Context -> Table<Artboard>,
Context -> Table<GradientStops>,
Context -> GradientStops,
)]
value: impl Node<Context<'static>, Output = T>,
/// The parts of the context to keep when evaluating the input value. All other parts are nullified.

View File

@@ -3,7 +3,8 @@ use core_types::registry::types::TextArea;
use core_types::table::Table;
use core_types::{Context, Ctx};
use glam::{DAffine2, DVec2};
use graphic_types::{Artboard, Graphic, Vector, vector_types::GradientStops};
use graphic_types::vector_types::GradientStops;
use graphic_types::{Artboard, Graphic, Vector};
use raster_types::{CPU, GPU, Raster};
/// Type-asserts a value to be a string.
@@ -152,7 +153,7 @@ async fn switch<T, C: Send + 'n + Clone>(
Context -> Table<Raster<CPU>>,
Context -> Table<Raster<GPU>>,
Context -> Table<Color>,
Context -> GradientStops,
Context -> Table<GradientStops>,
)]
if_true: impl Node<C, Output = T>,
#[expose]
@@ -171,7 +172,7 @@ async fn switch<T, C: Send + 'n + Clone>(
Context -> Table<Raster<CPU>>,
Context -> Table<Raster<GPU>>,
Context -> Table<Color>,
Context -> GradientStops,
Context -> Table<GradientStops>,
)]
if_false: impl Node<C, Output = T>,
) -> T {

View File

@@ -318,22 +318,47 @@ pub async fn flatten_gradient<T: IntoGraphicTable + 'n + Send + Clone>(_: impl C
/// Constructs a gradient from a table of colors, 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<T: IntoGraphicTable + 'n + Send + Clone>(_: impl Ctx, #[implementations(Table<Graphic>, Table<Color>)] colors: T) -> GradientStops {
fn colors_to_gradient<T: IntoGraphicTable + 'n + Send + Clone>(_: impl Ctx, #[implementations(Table<Graphic>, Table<Color>)] colors: T) -> Table<GradientStops> {
let colors = colors.into_flattened_table::<Color>();
let total_colors = colors.len();
if total_colors == 0 {
return GradientStops::new(vec![GradientStop {
position: 0.,
midpoint: 0.5,
color: Color::BLACK,
}]);
return Table::new_from_element(GradientStops::new(vec![
GradientStop {
position: 0.,
midpoint: 0.5,
color: Color::BLACK,
},
GradientStop {
position: 1.,
midpoint: 0.5,
color: Color::BLACK,
},
]));
}
if let (Some(color), None) = {
let mut colors_iter = colors.iter();
(colors_iter.next(), colors_iter.next())
} {
return Table::new_from_element(GradientStops::new(vec![
GradientStop {
position: 0.,
midpoint: 0.5,
color: *color.element,
},
GradientStop {
position: 1.,
midpoint: 0.5,
color: *color.element,
},
]));
}
let colors = colors.into_iter().enumerate().map(|(index, row)| GradientStop {
position: index as f64 / (total_colors - 1).max(1) as f64,
position: index as f64 / (total_colors - 1) as f64,
midpoint: 0.5,
color: row.element,
});
GradientStops::new(colors)
Table::new_from_element(GradientStops::new(colors))
}

View File

@@ -767,21 +767,17 @@ fn color_value(_: impl Ctx, _primary: (), #[default(Color::BLACK)] color: Table<
/// Constructs a gradient value which may be set to any sequence of color stops to represent the transition between colors.
#[node_macro::node(category("Value"))]
fn gradient_value(_: impl Ctx, _primary: (), gradient: GradientStops) -> GradientStops {
fn gradient_value(_: impl Ctx, _primary: (), gradient: Table<GradientStops>) -> Table<GradientStops> {
gradient
}
/// Constructs a gradient value which may be set to any sequence of color stops to represent the transition between colors.
#[node_macro::node(category("Value"))]
fn gradient_table_value(_: impl Ctx, _primary: (), gradient: GradientStops) -> Table<GradientStops> {
Table::new_from_element(gradient)
}
/// Gets the color at the specified position along the gradient, given a position from 0 (left) to 1 (right).
#[node_macro::node(category("Color"))]
fn sample_gradient(_: impl Ctx, _primary: (), gradient: GradientStops, position: Fraction) -> Table<Color> {
fn sample_gradient(_: impl Ctx, _primary: (), gradient: Table<GradientStops>, position: Fraction) -> Table<Color> {
let Some(row) = gradient.get(0) else { return Table::new() };
let position = position.clamp(0., 1.);
let color = gradient.evaluate(position);
let color = row.element.evaluate(position);
Table::new_from_element(color)
}

View File

@@ -54,7 +54,6 @@ fn luminance<T: Adjust<Color>>(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
#[gpu_image]
mut input: T,
@@ -80,7 +79,6 @@ fn gamma_correction<T: Adjust<Color>>(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
#[gpu_image]
mut input: T,
@@ -102,7 +100,6 @@ fn extract_channel<T: Adjust<Color>>(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
#[gpu_image]
mut input: T,
@@ -127,7 +124,6 @@ fn make_opaque<T: Adjust<Color>>(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
#[gpu_image]
mut input: T,
@@ -154,7 +150,6 @@ fn brightness_contrast_classic<T: Adjust<Color>>(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
#[gpu_image]
mut input: T,
@@ -186,7 +181,6 @@ fn brightness_contrast<T: Adjust<Color>>(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
#[gpu_image]
mut input: T,
@@ -268,7 +262,6 @@ fn levels<T: Adjust<Color>>(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
#[gpu_image]
mut image: T,
@@ -337,7 +330,6 @@ fn black_and_white<T: Adjust<Color>>(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
#[gpu_image]
mut image: T,
@@ -411,7 +403,6 @@ fn hue_saturation<T: Adjust<Color>>(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
#[gpu_image]
mut input: T,
@@ -447,7 +438,6 @@ fn invert<T: Adjust<Color>>(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
#[gpu_image]
mut input: T,
@@ -471,7 +461,6 @@ fn threshold<T: Adjust<Color>>(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
#[gpu_image]
mut image: T,
@@ -518,7 +507,6 @@ fn vibrance<T: Adjust<Color>>(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
#[gpu_image]
mut image: T,
@@ -684,7 +672,6 @@ fn channel_mixer<T: Adjust<Color>>(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
#[gpu_image]
mut image: T,
@@ -815,7 +802,6 @@ fn selective_color<T: Adjust<Color>>(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
#[gpu_image]
mut image: T,
@@ -962,7 +948,6 @@ fn posterize<T: Adjust<Color>>(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
#[gpu_image]
mut input: T,
@@ -997,7 +982,6 @@ fn exposure<T: Adjust<Color>>(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
#[gpu_image]
mut input: T,

View File

@@ -136,7 +136,6 @@ fn blend<T: Blend<Color> + Send>(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
#[gpu_image]
over: T,
@@ -145,7 +144,6 @@ fn blend<T: Blend<Color> + Send>(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
#[gpu_image]
under: T,
@@ -162,7 +160,6 @@ fn color_overlay<T: Adjust<Color>>(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
#[gpu_image]
mut image: T,

View File

@@ -16,16 +16,17 @@ async fn gradient_map<T: Adjust<Color>>(
Table<Raster<CPU>>,
Table<Color>,
Table<GradientStops>,
GradientStops,
)]
mut image: T,
gradient: GradientStops,
gradient: Table<GradientStops>,
reverse: bool,
) -> T {
let Some(row) = gradient.get(0) else { return image };
image.adjust(|color| {
let intensity = color.luminance_srgb();
let intensity = if reverse { 1. - intensity } else { intensity };
gradient.evaluate(intensity as f64).to_linear_srgb()
row.element.evaluate(intensity as f64).to_linear_srgb()
});
image

View File

@@ -60,7 +60,7 @@ async fn assign_colors<T>(
stroke: bool,
/// The range of colors to select from.
#[widget(ParsedWidgetOverride::Custom = "assign_colors_gradient")]
gradient: GradientStops,
gradient: Table<GradientStops>,
/// Whether to reverse the gradient.
reverse: bool,
/// Whether to randomize the color selection for each element from throughout the gradient.
@@ -76,8 +76,10 @@ async fn assign_colors<T>(
where
T: VectorTableIterMut + 'n + Send,
{
let Some(row) = gradient.into_iter().next() else { return content };
let length = content.vector_iter_mut().count();
let gradient = if reverse { gradient.reversed() } else { gradient };
let gradient = if reverse { row.element.reversed() } else { row.element };
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());