Wrap serialized gradient stops in a new GradientRamp struct and unify FillChoice (#4400)

* Introduce the GradientRamp exchange struct as the serialized TaggedValue::Gradient payload

* Unify FillChoice and FillChoiceUI into one enum generic over color format, carrying GradientRamp stops

* Rename the TaggedValue::Gradient variant to GradientRamp to match its payload

* Move the Color variant into the tagged_value macro list since its stored and wire forms match
This commit is contained in:
Keavon Chambers
2026-09-14 12:58:19 +02:00
committed by Dennis Kobert
parent 4d8729d87c
commit 360321e0ce
32 changed files with 411 additions and 288 deletions
@@ -23,7 +23,7 @@ use graphene_std::transform::{ReferencePoint, ScaleType};
use graphene_std::vector::misc::{
ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
};
use graphene_std::vector::style::{DashPattern, FillChoice, FillChoiceUI, GradientSpreadMethod, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::style::{DashPattern, FillChoice, GradientRamp, GradientSpreadMethod, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector};
use graphene_std::{Artboard, Color, Graphic};
use std::any::Any;
@@ -726,7 +726,7 @@ impl TableItemLayout for Color {
}
fn value_widgets(&self, _target: PathStep, _data: &LayoutData) -> Vec<WidgetInstance> {
vec![
ColorInput::new(FillChoiceUI::from(&FillChoice::Solid(*self)))
ColorInput::new(FillChoice::<SRGBA8>::from(&FillChoice::Solid(*self)))
.disabled(true)
.menu_direction(Some(MenuDirection::Top))
.narrow(true)
@@ -757,7 +757,7 @@ impl TableItemLayout for Gradient {
.narrow(true)
.widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
ColorInput::new(FillChoiceUI::from(&FillChoice::Gradient(self.clone())))
ColorInput::new(FillChoice::<SRGBA8>::Gradient(GradientRamp::from(self)))
.menu_direction(Some(MenuDirection::Top))
.disabled(true)
.narrow(true)
@@ -22,6 +22,7 @@ pub enum GraphOperationMessage {
},
FillGradientSet {
layer: LayerNodeIdentifier,
#[serde(skip)]
gradient: Gradient,
gradient_type: GradientType,
spread_method: GradientSpreadMethod,
@@ -33,6 +34,7 @@ pub enum GraphOperationMessage {
},
GradientStopsSet {
layer: LayerNodeIdentifier,
#[serde(skip)]
stops: Gradient,
},
GradientPositionsSet {
@@ -18,7 +18,7 @@ use graphene_std::raster_types::Image;
use graphene_std::subpath::Subpath;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{GradientSpreadMethod, GradientType, Stroke};
use graphene_std::vector::{Gradient, PointId, Vector, VectorModification, VectorModificationType};
use graphene_std::vector::{Gradient, GradientRamp, PointId, Vector, VectorModification, VectorModificationType};
use graphene_std::{Artboard, Color, Graphic};
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
@@ -410,12 +410,13 @@ impl<'a> ModifyInputsContext<'a> {
};
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput);
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::Gradient(gradient.clone()), false), true);
let ramp = GradientRamp::from(gradient);
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp.clone()), false), true);
// Skip the rerender on all but the last input so the whole update triggers a single graph run
self.set_input_with_refresh(
InputConnector::node(fill_node_id, graphene_std::vector::fill::FillInput),
NodeInput::value(TaggedValue::Gradient(gradient), false),
NodeInput::value(TaggedValue::GradientRamp(ramp), false),
true,
);
@@ -549,7 +550,7 @@ impl<'a> ModifyInputsContext<'a> {
};
let input_connector = InputConnector::node(gradient_value_id, graphene_std::math_nodes::gradient_value::GradientInput);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Gradient(stops), false), false);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::GradientRamp(GradientRamp::from(stops)), false), false);
}
/// Update the last 'Gradient Positions' node in the chain when one exists, so on-canvas stop drags stay live even
@@ -33,7 +33,9 @@ use graphene_std::vector::misc::BooleanOperation;
use graphene_std::vector::misc::{
ArcType, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType,
};
use graphene_std::vector::style::{FillChoiceUI, Gradient, GradientSpreadMethod, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, build_transform_with_y_preservation};
use graphene_std::vector::style::{
FillChoice, Gradient, GradientRamp, GradientSpreadMethod, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, build_transform_with_y_preservation,
};
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
use graphene_std::{NodeParameter, ParameterRef};
@@ -1157,9 +1159,9 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
// Add the color input
let widget_value = match &**tagged_value {
TaggedValue::Color(color) => FillChoiceUI::Solid(SRGBA8::from(*color)),
TaggedValue::Gradient(stops) => FillChoiceUI::Gradient(GradientStops::from(stops)),
value if value.is_no_paint() => FillChoiceUI::None,
TaggedValue::Color(color) => FillChoice::<SRGBA8>::Solid(SRGBA8::from(*color)),
TaggedValue::GradientRamp(ramp) => FillChoice::<SRGBA8>::Gradient(GradientRamp::from(ramp)),
value if value.is_no_paint() => FillChoice::<SRGBA8>::None,
x => {
warn!("Color {x:?}");
return LayoutGroup::row(widgets);
@@ -1170,12 +1172,12 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
// while a plain color or gradient input always keeps its own value type
let on_update: fn(&ColorInput) -> TaggedValue = if color_button.allow_none {
|input| match &input.value {
FillChoiceUI::None => TaggedValue::no_paint(),
FillChoiceUI::Solid(srgba) => TaggedValue::Color(Color::from(*srgba)),
FillChoiceUI::Gradient(gradient_ui) => TaggedValue::Gradient(Gradient::from(gradient_ui)),
FillChoice::<SRGBA8>::None => TaggedValue::no_paint(),
FillChoice::<SRGBA8>::Solid(srgba) => TaggedValue::Color(Color::from(*srgba)),
FillChoice::<SRGBA8>::Gradient(ramp) => TaggedValue::GradientRamp(GradientRamp::from(ramp)),
}
} else if matches!(&**tagged_value, TaggedValue::Gradient(_)) {
|input| TaggedValue::Gradient(input.value.as_gradient().map(Gradient::from).unwrap_or_else(Gradient::black_to_white))
} else if matches!(&**tagged_value, TaggedValue::GradientRamp(_)) {
|input| TaggedValue::GradientRamp(input.value.as_gradient().map(GradientRamp::from).unwrap_or_else(GradientRamp::black_to_white))
} else {
|input| TaggedValue::Color(input.value.as_solid().map(Color::from).unwrap_or(Color::TRANSPARENT))
};
@@ -2424,7 +2426,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
Ok(document_node) => match document_node.input_value(FillInput) {
Some(TaggedValue::Color(color)) => ResolvedFill::Solid(Some(*color)),
Some(value) if value.is_no_paint() => ResolvedFill::Solid(None),
Some(TaggedValue::Gradient(_)) => {
Some(TaggedValue::GradientRamp(_)) => {
match graph_modification_utils::read_fill_node_gradient(document_node, || {
layer.map_or([DVec2::ZERO, DVec2::ONE], |layer| context.network_interface.document_metadata().nonzero_bounding_box(layer))
}) {
@@ -2450,12 +2452,12 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
_ => None,
};
let backup_stops = match document_node.input_value(BackupGradientInput) {
Some(TaggedValue::Gradient(stops)) => stops.clone(),
_ => Gradient::black_to_white(),
Some(TaggedValue::GradientRamp(ramp)) => ramp.clone(),
_ => GradientRamp::black_to_white(),
};
(backup_color, backup_stops)
}
Err(_) => (None, Gradient::black_to_white()),
Err(_) => (None, GradientRamp::black_to_white()),
};
match &fill {
@@ -2465,7 +2467,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
let reverse_button = IconButton::new("Reverse", 24)
.tooltip_label("Reverse Stops")
.tooltip_description("Reverse the gradient color stops.")
.on_update(update_value(move |_| TaggedValue::Gradient(stops.reversed()), node_id, FillInput))
.on_update(update_value(move |_| TaggedValue::GradientRamp(GradientRamp::from(stops.reversed())), node_id, FillInput))
.widget_instance();
widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
widgets_first_row.push(reverse_button);
@@ -2473,16 +2475,16 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
_ => add_blank_assist(&mut widgets_first_row),
}
let fill_choice_ui = match &fill {
let widget_value = match &fill {
ResolvedFill::Solid(color) => {
if let Some(color) = color {
FillChoiceUI::Solid(SRGBA8::from(*color))
FillChoice::<SRGBA8>::Solid(SRGBA8::from(*color))
} else {
FillChoiceUI::None
FillChoice::<SRGBA8>::None
}
}
ResolvedFill::Gradient { gradient: stops, .. } => FillChoiceUI::Gradient(GradientStops::from(stops)),
ResolvedFill::Other => FillChoiceUI::None,
ResolvedFill::Gradient { gradient: stops, .. } => FillChoice::<SRGBA8>::Gradient(GradientRamp::from(stops)),
ResolvedFill::Other => FillChoice::<SRGBA8>::None,
};
let solid_set_messages = move |color: Option<Color>| {
@@ -2507,18 +2509,18 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
Message::Batched { messages: messages.into() }
};
let gradient_set_messages = move |gradient: Gradient| Message::Batched {
let gradient_set_messages = move |ramp: GradientRamp| Message::Batched {
messages: Box::new([
NodeGraphMessage::SetInputValue {
node_id,
input_index: FillInput::INDEX,
value: TaggedValue::Gradient(gradient.clone()).into(),
value: Box::new(TaggedValue::GradientRamp(ramp.clone())),
}
.into(),
NodeGraphMessage::SetInputValue {
node_id,
input_index: BackupGradientInput::INDEX,
value: Box::new(TaggedValue::Gradient(gradient)),
value: Box::new(TaggedValue::GradientRamp(ramp)),
}
.into(),
]),
@@ -2527,17 +2529,14 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
widgets_first_row.push(
ColorInput::default()
.value(fill_choice_ui)
.value(widget_value)
.on_update(move |x: &ColorInput| match &x.value {
FillChoiceUI::None => solid_set_messages(None),
FillChoiceUI::Solid(srgba8) => {
FillChoice::<SRGBA8>::None => solid_set_messages(None),
FillChoice::<SRGBA8>::Solid(srgba8) => {
let color = Some(Color::from(*srgba8));
solid_set_messages(color)
}
FillChoiceUI::Gradient(gradient_stops_ui) => {
let gradient = Gradient::from(gradient_stops_ui);
gradient_set_messages(gradient)
}
FillChoice::<SRGBA8>::Gradient(ramp) => gradient_set_messages(GradientRamp::from(ramp)),
})
.on_commit(commit_value)
.widget_instance(),
@@ -2556,7 +2555,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
.on_commit(commit_value),
RadioEntryData::new("gradient")
.label("Gradient")
.on_update(update_value(move |_| TaggedValue::Gradient(backup_gradient.clone()), node_id, FillInput))
.on_update(update_value(move |_| TaggedValue::GradientRamp(backup_gradient.clone()), node_id, FillInput))
.on_commit(commit_value),
];
@@ -27,7 +27,7 @@ impl FrontendGraphDataType {
match TaggedValue::from_type_or_none(input) {
TaggedValue::U32(_) | TaggedValue::U64(_) | TaggedValue::F32(_) | TaggedValue::F64(_) | TaggedValue::DVec2(_) | TaggedValue::F64Array(_) | TaggedValue::DAffine2(_) => Self::Number,
TaggedValue::Color(_) => Self::Color,
TaggedValue::LegacyGradient(_) | TaggedValue::Gradient(_) => Self::Gradient,
TaggedValue::LegacyGradient(_) | TaggedValue::GradientRamp(_) => Self::Gradient,
TaggedValue::String(_) => Self::Typography,
// Types whose `TaggedValue` variant has been removed are routed through `TypeDefault` and identified by the descriptor's type name.
TaggedValue::TypeDefault(td) => match td.name.as_ref() {
@@ -5,7 +5,7 @@ use crate::messages::prelude::*;
use glam::DVec2;
use graphene_std::color::SRGBA8;
use graphene_std::renderer::Quad;
use graphene_std::vector::style::FillChoiceUI;
use graphene_std::vector::style::FillChoice;
fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) {
let origin = document.snapping_state.grid.origin;
@@ -274,7 +274,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
Separator::new(SeparatorStyle::Related).widget_instance(),
]);
color_widgets.push(
ColorInput::new(FillChoiceUI::Solid(SRGBA8::from_hex_str(&grid.color).unwrap_or(SRGBA8::BLACK)))
ColorInput::new(FillChoice::<SRGBA8>::Solid(SRGBA8::from_hex_str(&grid.color).unwrap_or(SRGBA8::BLACK)))
.tooltip_label("Grid Display Color")
.allow_none(false)
.on_update(update_val::<ColorInput, _>(grid, |grid, color| {
@@ -801,12 +801,13 @@ async fn legacy_four_input_fill_migrates_to_the_split_transform_shape() {
"the transform input should hold a matrix, but became {transform:?}"
);
// The Sample Gradient parameter held the tuple-form stops, which parse as the stops value with even positions elided
// The Sample Gradient parameter held the tuple-form stops, which parse as the ramp value with even positions elided
let sample_gradient_node = &network.nodes[&graph_craft::document::NodeId(2)];
let stops = sample_gradient_node.input_value(graphene_std::math_nodes::sample_gradient::GradientInput);
let Some(TaggedValue::Gradient(stops)) = stops else {
panic!("the legacy stops parameter should become a gradient stops value, but became {stops:?}");
let Some(TaggedValue::GradientRamp(ramp)) = stops else {
panic!("the legacy stops parameter should become a gradient ramp value, but became {stops:?}");
};
let stops = graphene_std::vector::Gradient::from(ramp);
assert_eq!(stops.len(), 2);
assert!(!stops.has_position_attribute(), "even legacy tuple positions should elide rather than materialize");
}
@@ -1673,7 +1673,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
let fill_value = match old_fill {
graphic_types::migrations::legacy::LegacyFill::None => TaggedValue::no_paint(),
graphic_types::migrations::legacy::LegacyFill::Solid(color) => TaggedValue::Color(*color),
graphic_types::migrations::legacy::LegacyFill::Gradient(gradient) => TaggedValue::Gradient(gradient.stops.clone()),
graphic_types::migrations::legacy::LegacyFill::Gradient(gradient) => TaggedValue::GradientRamp(gradient.stops.clone()),
};
document
.network_interface
@@ -1721,7 +1721,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
if let Some(TaggedValue::LegacyGradient(g)) = old_inputs[3].as_value() {
document.network_interface.set_input(
&InputConnector::node_at_index(*node_id, 3),
NodeInput::value(TaggedValue::Gradient(g.stops.clone()), false),
NodeInput::value(TaggedValue::GradientRamp(g.stops.clone()), false),
network_path,
);