Move the gradient spread method into GradientRamp and the color picker popover (#4402)

* Add a spread method field to GradientRamp, carried at runtime as the gradient item's attribute

* Retire the Fill node's spread method input, folding its value into the gradient ramps on document upgrade

* Add an Ends spread method radio to the color picker popover, replacing the Gradient tool's control bar radio

* Update the demo art
This commit is contained in:
Keavon Chambers
2026-09-14 12:58:22 +02:00
committed by Dennis Kobert
parent 6480de274e
commit c5cb466e28
19 changed files with 400 additions and 193 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
use crate::messages::layout::utility_types::widgets::input_widgets::{SpectrumInputUpdate, VisualColorPickersInputUpdate}; use crate::messages::layout::utility_types::widgets::input_widgets::{SpectrumInputUpdate, VisualColorPickersInputUpdate};
use crate::messages::prelude::*; use crate::messages::prelude::*;
use graphene_std::vector::style::FillChoice; use graphene_std::vector::style::{FillChoice, GradientSpreadMethod};
/// Identifies which RGB channel a numeric input change targets. /// Identifies which RGB channel a numeric input change targets.
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
@@ -47,6 +47,8 @@ pub enum ColorPickerMessage {
/// `SpectrumInput` change: marker move/insert/delete, midpoint move/reset, or active marker selection changed. /// `SpectrumInput` change: marker move/insert/delete, midpoint move/reset, or active marker selection changed.
GradientUpdate { update: SpectrumInputUpdate }, GradientUpdate { update: SpectrumInputUpdate },
/// Spread method choice from the gradient "Ends" selection.
SetSpreadMethod { spread_method: GradientSpreadMethod },
/// Tell the frontend to start an undo transaction (forwarded as a `FrontendMessage` it bridges out to the picker's parent). /// Tell the frontend to start an undo transaction (forwarded as a `FrontendMessage` it bridges out to the picker's parent).
StartTransaction, StartTransaction,
@@ -5,7 +5,7 @@ use crate::messages::prelude::*;
use graphene_std::Color; use graphene_std::Color;
use graphene_std::color::SRGBA8; use graphene_std::color::SRGBA8;
use graphene_std::core_types::misc::parse_css_color; use graphene_std::core_types::misc::parse_css_color;
use graphene_std::vector::style::{FillChoice, Gradient, GradientRamp, GradientStops}; use graphene_std::vector::style::{FillChoice, Gradient, GradientRamp, GradientSpreadMethod, GradientStops};
/// Bounds for a midpoint position (relative to the interval between two adjacent gradient stops). /// Bounds for a midpoint position (relative to the interval between two adjacent gradient stops).
const MIN_MIDPOINT: f64 = 0.01; const MIN_MIDPOINT: f64 = 0.01;
@@ -29,6 +29,7 @@ pub struct ColorPickerMessageHandler {
// When set, the picker is editing a gradient: the visual pickers and inputs target the active stop's color. // When set, the picker is editing a gradient: the visual pickers and inputs target the active stop's color.
gradient: Option<Gradient>, gradient: Option<Gradient>,
spread_method: GradientSpreadMethod,
active_marker_index: Option<u32>, active_marker_index: Option<u32>,
active_marker_is_midpoint: bool, active_marker_is_midpoint: bool,
@@ -50,6 +51,7 @@ impl Default for ColorPickerMessageHandler {
old_alpha: 1., old_alpha: 1.,
old_is_none: true, old_is_none: true,
gradient: None, gradient: None,
spread_method: GradientSpreadMethod::default(),
active_marker_index: None, active_marker_index: None,
active_marker_is_midpoint: false, active_marker_is_midpoint: false,
allow_none: true, allow_none: true,
@@ -70,11 +72,13 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
FillChoice::None => { FillChoice::None => {
self.set_new_hsva(0., 0., 0., 1., true); self.set_new_hsva(0., 0., 0., 1., true);
self.gradient = None; self.gradient = None;
self.spread_method = GradientSpreadMethod::default();
self.active_marker_index = None; self.active_marker_index = None;
self.active_marker_is_midpoint = false; self.active_marker_is_midpoint = false;
} }
FillChoice::Solid(color) => { FillChoice::Solid(color) => {
self.gradient = None; self.gradient = None;
self.spread_method = GradientSpreadMethod::default();
self.active_marker_index = None; self.active_marker_index = None;
self.active_marker_is_midpoint = false; self.active_marker_is_midpoint = false;
self.adopt_color(color); self.adopt_color(color);
@@ -82,6 +86,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
FillChoice::Gradient(ramp) => { FillChoice::Gradient(ramp) => {
self.active_marker_index = Some(0); self.active_marker_index = Some(0);
self.active_marker_is_midpoint = false; self.active_marker_is_midpoint = false;
self.spread_method = ramp.spread_method;
let gradient = Gradient::from(ramp); let gradient = Gradient::from(ramp);
let first_color = gradient.color(0).unwrap_or(Color::BLACK); let first_color = gradient.color(0).unwrap_or(Color::BLACK);
self.gradient = Some(gradient); self.gradient = Some(gradient);
@@ -187,6 +192,18 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
self.send_layouts(responses); self.send_layouts(responses);
} }
ColorPickerMessage::GradientUpdate { update } => self.apply_gradient_update(update, responses), ColorPickerMessage::GradientUpdate { update } => self.apply_gradient_update(update, responses),
ColorPickerMessage::SetSpreadMethod { spread_method } => {
let Some(gradient) = &self.gradient else { return };
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
self.spread_method = spread_method;
responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp {
spread_method,
..GradientRamp::from(gradient)
}),
});
self.send_layouts(responses);
}
ColorPickerMessage::StartTransaction => { ColorPickerMessage::StartTransaction => {
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction); responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
} }
@@ -271,7 +288,10 @@ impl ColorPickerMessageHandler {
{ {
gradient.set_color(active_index as usize, color); gradient.set_color(active_index as usize, color);
responses.add(FrontendMessage::ColorPickerColorChanged { responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp::from(&*gradient)), value: FillChoice::Gradient(GradientRamp {
spread_method: self.spread_method,
..GradientRamp::from(&*gradient)
}),
}); });
} else { } else {
responses.add(FrontendMessage::ColorPickerColorChanged { responses.add(FrontendMessage::ColorPickerColorChanged {
@@ -398,7 +418,10 @@ impl ColorPickerMessageHandler {
} }
responses.add(FrontendMessage::ColorPickerColorChanged { responses.add(FrontendMessage::ColorPickerColorChanged {
value: FillChoice::Gradient(GradientRamp::from(&gradient)), value: FillChoice::Gradient(GradientRamp {
spread_method: self.spread_method,
..GradientRamp::from(&gradient)
}),
}); });
self.gradient = Some(gradient); self.gradient = Some(gradient);
self.send_layouts(responses); self.send_layouts(responses);
@@ -598,6 +621,24 @@ impl ColorPickerMessageHandler {
.widget_instance(), .widget_instance(),
])); ]));
// Gradient ends spread method (only present when the picker is in gradient mode)
if self.gradient.is_some() {
let entries = [GradientSpreadMethod::Pad, GradientSpreadMethod::Reflect, GradientSpreadMethod::Repeat]
.into_iter()
.map(|spread_method| {
RadioEntryData::new(format!("{spread_method:?}"))
.label(spread_method.to_string())
.on_update(move |_| ColorPickerMessage::SetSpreadMethod { spread_method }.into())
})
.collect();
groups.push(LayoutGroup::row(vec![
TextLabel::new("Ends").tooltip_label("Spread Method").tooltip_description(ENDS_DESCRIPTION).widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
RadioInput::new(entries).selected_index(Some(self.spread_method as u32)).disabled(self.disabled).widget_instance(),
]));
}
// Color presets (None / Black / White / pure colors / eyedropper) // Color presets (None / Black / White / pure colors / eyedropper)
groups.push(LayoutGroup::row(vec![ groups.push(LayoutGroup::row(vec![
ColorPresetsInput::default() ColorPresetsInput::default()
@@ -651,6 +692,12 @@ const HUE_DESCRIPTION: &str = "The shade along the spectrum of the rainbow.";
const SATURATION_DESCRIPTION: &str = "The vividness from grayscale to full color."; const SATURATION_DESCRIPTION: &str = "The vividness from grayscale to full color.";
const VALUE_DESCRIPTION: &str = "The brightness from black to full color."; const VALUE_DESCRIPTION: &str = "The brightness from black to full color.";
const ALPHA_DESCRIPTION: &str = "The level of translucency, from transparent (0%) to opaque (100%)."; const ALPHA_DESCRIPTION: &str = "The level of translucency, from transparent (0%) to opaque (100%).";
const ENDS_DESCRIPTION: &str = "\
How the gradient continues beyond its ends:\n\
**Pad** extends the end colors outward.\n\
**Reflect** loops the gradient by mirroring back-and-forth.\n\
**Repeat** loops the gradient as copies of itself.\
";
/// The popover's background color as sRGB gamma-encoded channels (the `--color-2-mildblack` design token, `#222`). /// The popover's background color as sRGB gamma-encoded channels (the `--color-2-mildblack` design token, `#222`).
/// Used by the comparison swatch's outline computation to brighten the inset border for colors close to this background. /// Used by the comparison swatch's outline computation to brighten the inset border for colors close to this background.
@@ -411,6 +411,7 @@ impl<'a> ModifyInputsContext<'a> {
let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput); let backup_input_connector = InputConnector::node(fill_node_id, graphene_std::vector::fill::BackupGradientInput);
let ramp = GradientRamp::from(gradient); let ramp = GradientRamp::from(gradient);
let ramp = GradientRamp { spread_method, ..ramp };
self.set_input_with_refresh(backup_input_connector, NodeInput::value(TaggedValue::GradientRamp(ramp.clone()), false), true); 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 // Skip the rerender on all but the last input so the whole update triggers a single graph run
@@ -444,12 +445,6 @@ impl<'a> ModifyInputsContext<'a> {
self.set_input_with_refresh( self.set_input_with_refresh(
InputConnector::node(fill_node_id, graphene_std::vector::fill::GradientTypeInput), InputConnector::node(fill_node_id, graphene_std::vector::fill::GradientTypeInput),
NodeInput::value(TaggedValue::GradientType(gradient_type), false), NodeInput::value(TaggedValue::GradientType(gradient_type), false),
true,
);
self.set_input_with_refresh(
InputConnector::node(fill_node_id, graphene_std::vector::fill::SpreadMethodInput),
NodeInput::value(TaggedValue::GradientSpreadMethod(spread_method), false),
false, false,
); );
} }
@@ -2461,13 +2461,23 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
}; };
match &fill { match &fill {
ResolvedFill::Gradient { gradient: stops, .. } => { ResolvedFill::Gradient { gradient: stops, spread_method, .. } => {
let stops = stops.clone(); let stops = stops.clone();
let spread_method = *spread_method;
let reverse_button = IconButton::new("Reverse", 24) let reverse_button = IconButton::new("Reverse", 24)
.tooltip_label("Reverse Stops") .tooltip_label("Reverse Stops")
.tooltip_description("Reverse the gradient color stops.") .tooltip_description("Reverse the gradient color stops.")
.on_update(update_value(move |_| TaggedValue::GradientRamp(GradientRamp::from(stops.reversed())), node_id, FillInput)) .on_update(update_value(
move |_| {
TaggedValue::GradientRamp(GradientRamp {
spread_method,
..GradientRamp::from(stops.reversed())
})
},
node_id,
FillInput,
))
.widget_instance(); .widget_instance();
widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance()); widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
widgets_first_row.push(reverse_button); widgets_first_row.push(reverse_button);
@@ -2483,7 +2493,10 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
FillChoice::<SRGBA8>::None FillChoice::<SRGBA8>::None
} }
} }
ResolvedFill::Gradient { gradient: stops, .. } => FillChoice::<SRGBA8>::Gradient(GradientRamp::from(stops)), ResolvedFill::Gradient { gradient: stops, spread_method, .. } => FillChoice::<SRGBA8>::Gradient(GradientRamp {
spread_method: *spread_method,
..GradientRamp::from(stops)
}),
ResolvedFill::Other => FillChoice::<SRGBA8>::None, ResolvedFill::Other => FillChoice::<SRGBA8>::None,
}; };
@@ -2572,36 +2585,14 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
if let ResolvedFill::Gradient { if let ResolvedFill::Gradient {
gradient_type, gradient_type,
spread_method,
transform, transform,
transform_is_value, transform_is_value,
.. ..
} = fill.clone() } = fill.clone()
{ {
// Linear/Radial radio: blank assist (the "Reverse Direction" button has been moved down to the spread method row) // "Reverse Direction" button (assist) beside the Linear/Radial radio. Icon orientation is resolved in viewport
let mut row = vec![TextLabel::new("").widget_instance()];
add_blank_assist(&mut row);
let entries = [GradientType::Linear, GradientType::Radial]
.iter()
.map(|&grad_type| {
RadioEntryData::new(format!("{:?}", grad_type))
.label(format!("{:?}", grad_type))
.on_update(update_value(move |_| TaggedValue::GradientType(grad_type), node_id, GradientTypeInput))
.on_commit(commit_value)
})
.collect();
row.extend_from_slice(&[
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
RadioInput::new(entries).selected_index(Some(gradient_type as u32)).widget_instance(),
]);
widgets.push(LayoutGroup::row(row));
// "Reverse Direction" button (assist) plus the Pad/Reflect/Repeat radio. Icon orientation is resolved in viewport
// space so canvas tilt and layer transforms behave the same as in the Gradient tool's control bar. // space so canvas tilt and layer transforms behave the same as in the Gradient tool's control bar.
let mut spread_methods_row = vec![TextLabel::new("").widget_instance()]; let mut row = vec![TextLabel::new("").widget_instance()];
// The button writes a value into the transform input, so only offer it when the input isn't wired // The button writes a value into the transform input, so only offer it when the input isn't wired
if transform_is_value { if transform_is_value {
@@ -2612,11 +2603,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
let reverse_direction_button = IconButton::new(if orientation_rightward { "ReverseRadialGradientToRight" } else { "ReverseRadialGradientToLeft" }, 24) let reverse_direction_button = IconButton::new(if orientation_rightward { "ReverseRadialGradientToRight" } else { "ReverseRadialGradientToLeft" }, 24)
.tooltip_label("Reverse Direction") .tooltip_label("Reverse Direction")
.tooltip_description(if gradient_type == GradientType::Radial { .tooltip_description(graph_modification_utils::reverse_direction_tooltip_description(gradient_type))
"Reverse which end the gradient radiates from."
} else {
"Swap the start and end points of the gradient line."
})
.on_update(move |_| Message::Batched { .on_update(move |_| Message::Batched {
messages: Box::new([ messages: Box::new([
NodeGraphMessage::SetInputValue { NodeGraphMessage::SetInputValue {
@@ -2634,28 +2621,28 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
]), ]),
}) })
.widget_instance(); .widget_instance();
spread_methods_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance()); row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
spread_methods_row.push(reverse_direction_button); row.push(reverse_direction_button);
} else { } else {
add_blank_assist(&mut spread_methods_row); add_blank_assist(&mut row);
} }
let spread_method_entries = [GradientSpreadMethod::Pad, GradientSpreadMethod::Reflect, GradientSpreadMethod::Repeat] let entries = [GradientType::Linear, GradientType::Radial]
.iter() .iter()
.map(|&spread_method| { .map(|&gradient_type| {
RadioEntryData::new(format!("{:?}", spread_method)) RadioEntryData::new(format!("{:?}", gradient_type))
.label(format!("{:?}", spread_method)) .label(format!("{:?}", gradient_type))
.on_update(update_value(move |_| TaggedValue::GradientSpreadMethod(spread_method), node_id, SpreadMethodInput)) .on_update(update_value(move |_| TaggedValue::GradientType(gradient_type), node_id, GradientTypeInput))
.on_commit(commit_value) .on_commit(commit_value)
}) })
.collect(); .collect();
spread_methods_row.extend_from_slice(&[ row.extend_from_slice(&[
Separator::new(SeparatorStyle::Unrelated).widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance(),
RadioInput::new(spread_method_entries).selected_index(Some(spread_method as u32)).widget_instance(), RadioInput::new(entries).selected_index(Some(gradient_type as u32)).widget_instance(),
]); ]);
widgets.push(LayoutGroup::row(spread_methods_row)); widgets.push(LayoutGroup::row(row));
} }
widgets widgets
@@ -784,7 +784,7 @@ async fn legacy_four_input_fill_migrates_to_the_split_transform_shape() {
let network = document.network_interface.nested_network(&network_path).expect("the found network path should resolve"); let network = document.network_interface.nested_network(&network_path).expect("the found network path should resolve");
let fill_node = &network.nodes[&node_id]; let fill_node = &network.nodes[&node_id];
assert_eq!(fill_node.inputs.len(), 8, "the legacy Fill should upgrade to the 8-input shape"); assert_eq!(fill_node.inputs.len(), 7, "the legacy Fill should upgrade to the 7-input shape");
let paint = fill_node.input(graphene_std::vector::fill::FillInput); let paint = fill_node.input(graphene_std::vector::fill::FillInput);
assert!( assert!(
matches!(paint, Some(graph_craft::document::NodeInput::Node { .. })), matches!(paint, Some(graph_craft::document::NodeInput::Node { .. })),
@@ -811,3 +811,58 @@ async fn legacy_four_input_fill_migrates_to_the_split_transform_shape() {
assert_eq!(stops.len(), 2); assert_eq!(stops.len(), 2);
assert!(!stops.has_position_attribute(), "even legacy tuple positions should elide rather than materialize"); assert!(!stops.has_position_attribute(), "even legacy tuple positions should elide rather than materialize");
} }
#[tokio::test]
async fn eight_input_fill_migrates_spread_method_into_the_ramp() {
use graph_craft::document::value::TaggedValue;
use graphene_std::vector::style::GradientSpreadMethod;
// A minimal document from the era when spread method was the Fill node's own sixth input, here set to Repeat
const EIGHT_INPUT_DOCUMENT: &str = r#"{"network_interface":{"network":{"exports":[{"Node":{"node_id":1,"output_index":0,"lambda":false}}],"nodes":[[1,{"inputs":[{"Value":{"tagged_value":{"GraphicGroup":{"instance":[],"transform":[],"alpha_blending":[],"source_node_id":[]}},"exposed":true}},{"Value":{"tagged_value":{"GradientRamp":{"stops":{"color":[{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0},{"red":1.0,"green":1.0,"blue":1.0,"alpha":1.0}]}}},"exposed":false}},{"Value":{"tagged_value":{"Color":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}},"exposed":false}},{"Value":{"tagged_value":{"GradientRamp":{"stops":{"color":[{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0},{"red":1.0,"green":1.0,"blue":1.0,"alpha":1.0}]}}},"exposed":false}},{"Value":{"tagged_value":{"GradientType":"Linear"},"exposed":false}},{"Value":{"tagged_value":{"GradientSpreadMethod":"Repeat"},"exposed":false}},{"Value":{"tagged_value":{"Bool":false},"exposed":false}},{"Value":{"tagged_value":{"DAffine2":[1.0,0.0,0.0,1.0,0.0,0.0]},"exposed":false}}],"manual_composition":{"Concrete":{"name":"core::option::Option<alloc::sync::Arc<graphene_core::context::OwnedContextImpl>>","alias":null}},"implementation":{"ProtoNode":{"name":"graphene_core::vector::FillNode"}},"visible":true,"skip_deduplication":false}]],"scope_injections":[]},"network_metadata":{"persistent_metadata":{"node_metadata":[[1,{"persistent_metadata":{"reference":"Fill","display_name":"","input_properties":[{"input_data":{"input_name":"Content"},"widget_override":null},{"input_data":{"input_name":"Fill"},"widget_override":null},{"input_data":{"input_name":"Backup Color"},"widget_override":null},{"input_data":{"input_name":"Backup Gradient"},"widget_override":null},{"input_data":{"input_name":"Gradient Type"},"widget_override":null},{"input_data":{"input_name":"Spread Method"},"widget_override":null},{"input_data":{"input_name":"Has Transform"},"widget_override":null},{"input_data":{"input_name":"Transform"},"widget_override":null}],"output_names":[""],"has_primary_output":true,"locked":false,"pinned":false,"node_type_metadata":{"Node":{"position":{"Absolute":[0,0]}}},"network_metadata":null}}]],"previewing":"No","navigation_metadata":{"node_graph_ptz":{"pan":[0.0,0.0],"tilt":0.0,"zoom":1.0,"flip":false},"node_graph_to_viewport":[1.0,0.0,0.0,1.0,0.0,0.0],"node_graph_top_right":[0.0,0.0]},"selection_undo_history":[],"selection_redo_history":[]}}},"collapsed":[],"name":"eight_input_fill.graphite","commit_hash":"0000000000000000000000000000000000000000","document_ptz":{"pan":[0.0,0.0],"tilt":0.0,"zoom":1.0,"flip":false},"document_mode":"DesignMode","view_mode":"Normal","overlays_visibility_settings":{"all":true,"artboard_name":true,"compass_rose":true,"quick_measurement":true,"transform_measurement":true,"transform_cage":true,"hover_outline":true,"selection_outline":true,"pivot":true,"path":true,"anchors":true,"handles":true},"rulers_visible":true,"snapping_state":{"snapping_enabled":true,"grid_snapping":false,"artboards":true,"tolerance":8.0,"bounding_box":{"center_point":true,"corner_point":true,"edge_midpoint":true,"align_with_edges":true,"distribute_evenly":true},"path":{"anchor_point":true,"line_midpoint":true,"along_path":true,"normal_to_path":true,"tangent_to_path":true,"path_intersection_point":true,"align_with_anchor_point":true,"perpendicular_from_endpoint":true},"grid":{"origin":[0.0,0.0],"grid_type":{"Rectangular":{"spacing":[1.0,1.0]}},"grid_color":{"red":0.6,"green":0.6,"blue":0.6,"alpha":1.0},"dot_display":false}},"graph_view_overlay_open":false,"graph_fade_artwork_percentage":80.0}"#;
// Deserializing alone must succeed, so a failure below is attributable to the migrations
DocumentMessageHandler::deserialize_document(EIGHT_INPUT_DOCUMENT).expect("the eight-input document should deserialize");
let mut editor = EditorTestUtils::create();
editor
.handle_message(PortfolioMessage::OpenDocumentFile {
document_name: None,
document_path: None,
document_serialized_content: EIGHT_INPUT_DOCUMENT.to_string(),
})
.await;
let document = editor.active_document();
let (network_path, node_id) = find_fill_node(document);
let network = document.network_interface.nested_network(&network_path).expect("the found network path should resolve");
let fill_node = &network.nodes[&node_id];
assert_eq!(fill_node.inputs.len(), 7, "the eight-input Fill should fold down to the 7-input shape");
let paint = fill_node.input_value(graphene_std::vector::fill::FillInput);
let Some(TaggedValue::GradientRamp(ramp)) = paint else {
panic!("the fill input should keep its gradient ramp value, but became {paint:?}");
};
assert_eq!(ramp.spread_method, GradientSpreadMethod::Repeat, "the spread method input's value should fold into the fill ramp");
let backup = fill_node.input_value(graphene_std::vector::fill::BackupGradientInput);
let Some(TaggedValue::GradientRamp(backup_ramp)) = backup else {
panic!("the backup gradient input should keep its gradient ramp value, but became {backup:?}");
};
assert_eq!(
backup_ramp.spread_method,
GradientSpreadMethod::Repeat,
"the spread method input's value should fold into the backup ramp"
);
let has_transform = fill_node.input_value(graphene_std::vector::fill::HasTransformInput);
assert!(
matches!(has_transform, Some(TaggedValue::Bool(false))),
"the has-transform input should shift down intact, but became {has_transform:?}"
);
let transform = fill_node.input_value(graphene_std::vector::fill::TransformInput);
assert!(
matches!(transform, Some(TaggedValue::DAffine2(_))),
"the transform input should shift down intact, but became {transform:?}"
);
}
@@ -17,7 +17,7 @@ use graphene_std::text::{TextAlign, TypesettingConfig};
use graphene_std::transform::ScaleType; use graphene_std::transform::ScaleType;
use graphene_std::uuid::NodeId; use graphene_std::uuid::NodeId;
use graphene_std::vector::graphic_types; use graphene_std::vector::graphic_types;
use graphene_std::vector::style::{PaintOrder, StrokeAlign}; use graphene_std::vector::style::{GradientRamp, GradientSpreadMethod, PaintOrder, StrokeAlign};
use std::collections::HashMap; use std::collections::HashMap;
use std::f64::consts::PI; use std::f64::consts::PI;
use std::ops::Range; use std::ops::Range;
@@ -1413,6 +1413,14 @@ fn migrate_corner_radius_input(input: &NodeInput) -> Option<NodeInput> {
Some(NodeInput::value(TaggedValue::BoxCorners(values), *exposed)) Some(NodeInput::value(TaggedValue::BoxCorners(values), *exposed))
} }
/// Rewrites a gradient ramp value input to carry the given spread method, which used to live in the Fill node's retired `_spread_method` input.
fn fold_spread_method_into_ramp_input(input: &NodeInput, spread_method: GradientSpreadMethod) -> NodeInput {
match input.as_value() {
Some(TaggedValue::GradientRamp(ramp)) => NodeInput::value(TaggedValue::GradientRamp(GradientRamp { spread_method, ..ramp.clone() }), input.is_exposed()),
_ => input.clone(),
}
}
fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], document: &mut DocumentMessageHandler, reset_node_definitions_on_open: bool) -> Option<()> { fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], document: &mut DocumentMessageHandler, reset_node_definitions_on_open: bool) -> Option<()> {
// Must run before the reset block below: a node referencing a removed catalog entry would otherwise abort // Must run before the reset block below: a node referencing a removed catalog entry would otherwise abort
// `migrate_node` via the `?` on `resolve_document_node_type`, preventing subsequent migration blocks from running. // `migrate_node` via the `?` on `resolve_document_node_type`, preventing subsequent migration blocks from running.
@@ -1657,7 +1665,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
} }
// Upgrade the legacy 4-input Fill node (content, fill: Fill, _backup_color, _backup_gradient: Gradient) to the value-model // Upgrade the legacy 4-input Fill node (content, fill: Fill, _backup_color, _backup_gradient: Gradient) to the value-model
// 8-input shape (content, fill: generic paint list, _backup_color, _backup_gradient, _gradient_type, _spread_method, _has_transform, _transform). // 7-input shape (content, fill: generic paint list, _backup_color, _backup_gradient, _gradient_type, _has_transform, _transform).
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::fill::IDENTIFIER) && inputs_count == 4 { if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::fill::IDENTIFIER) && inputs_count == 4 {
let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template); document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
@@ -1673,33 +1681,31 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
let fill_value = match old_fill { let fill_value = match old_fill {
graphic_types::migrations::legacy::LegacyFill::None => TaggedValue::no_paint(), graphic_types::migrations::legacy::LegacyFill::None => TaggedValue::no_paint(),
graphic_types::migrations::legacy::LegacyFill::Solid(color) => TaggedValue::Color(*color), graphic_types::migrations::legacy::LegacyFill::Solid(color) => TaggedValue::Color(*color),
graphic_types::migrations::legacy::LegacyFill::Gradient(gradient) => TaggedValue::GradientRamp(gradient.stops.clone()), graphic_types::migrations::legacy::LegacyFill::Gradient(gradient) => TaggedValue::GradientRamp(GradientRamp {
spread_method: gradient.spread_method,
..gradient.stops.clone()
}),
}; };
document document
.network_interface .network_interface
.set_input(&InputConnector::node_at_index(*node_id, 1), NodeInput::value(fill_value, exposed), network_path); .set_input(&InputConnector::node_at_index(*node_id, 1), NodeInput::value(fill_value, exposed), network_path);
// Gradient metadata (4, 5, 6, 7): applies only to a literal gradient, solids/none keep the template defaults // Gradient metadata (4, 5, 6): applies only to a literal gradient, solids/none keep the template defaults
if let graphic_types::migrations::legacy::LegacyFill::Gradient(gradient) = old_fill { if let graphic_types::migrations::legacy::LegacyFill::Gradient(gradient) = old_fill {
document.network_interface.set_input( document.network_interface.set_input(
&InputConnector::node_at_index(*node_id, 4), &InputConnector::node_at_index(*node_id, 4),
NodeInput::value(TaggedValue::GradientType(gradient.gradient_type), false), NodeInput::value(TaggedValue::GradientType(gradient.gradient_type), false),
network_path, network_path,
); );
document.network_interface.set_input(
&InputConnector::node_at_index(*node_id, 5),
NodeInput::value(TaggedValue::GradientSpreadMethod(gradient.spread_method), false),
network_path,
);
if gradient.absolute { if gradient.absolute {
let transform = gradient.transform * gradient.to_transform(); let transform = gradient.transform * gradient.to_transform();
document document
.network_interface .network_interface
.set_input(&InputConnector::node_at_index(*node_id, 6), NodeInput::value(TaggedValue::Bool(true), false), network_path); .set_input(&InputConnector::node_at_index(*node_id, 5), NodeInput::value(TaggedValue::Bool(true), false), network_path);
document document
.network_interface .network_interface
.set_input(&InputConnector::node_at_index(*node_id, 7), NodeInput::value(TaggedValue::DAffine2(transform), false), network_path); .set_input(&InputConnector::node_at_index(*node_id, 6), NodeInput::value(TaggedValue::DAffine2(transform), false), network_path);
} else { } else {
// Baking a legacy bounding-box-relative gradient is deferred until the measurement pre-pass can supply the paint // Baking a legacy bounding-box-relative gradient is deferred until the measurement pre-pass can supply the paint
// target's bounds, so the template's unbaked `_has_transform = false` stands until the bake lands // target's bounds, so the template's unbaked `_has_transform = false` stands until the bake lands
@@ -1721,7 +1727,13 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
if let Some(TaggedValue::LegacyGradient(g)) = old_inputs[3].as_value() { if let Some(TaggedValue::LegacyGradient(g)) = old_inputs[3].as_value() {
document.network_interface.set_input( document.network_interface.set_input(
&InputConnector::node_at_index(*node_id, 3), &InputConnector::node_at_index(*node_id, 3),
NodeInput::value(TaggedValue::GradientRamp(g.stops.clone()), false), NodeInput::value(
TaggedValue::GradientRamp(GradientRamp {
spread_method: g.spread_method,
..g.stops.clone()
}),
false,
),
network_path, network_path,
); );
@@ -1737,36 +1749,45 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
NodeInput::value(TaggedValue::GradientType(g.gradient_type), false), NodeInput::value(TaggedValue::GradientType(g.gradient_type), false),
network_path, network_path,
); );
document.network_interface.set_input(
&InputConnector::node_at_index(*node_id, 5),
NodeInput::value(TaggedValue::GradientSpreadMethod(g.spread_method), false),
network_path,
);
if g.absolute { if g.absolute {
let transform = g.transform * g.to_transform(); let transform = g.transform * g.to_transform();
document document
.network_interface .network_interface
.set_input(&InputConnector::node_at_index(*node_id, 6), NodeInput::value(TaggedValue::Bool(true), false), network_path); .set_input(&InputConnector::node_at_index(*node_id, 5), NodeInput::value(TaggedValue::Bool(true), false), network_path);
document document
.network_interface .network_interface
.set_input(&InputConnector::node_at_index(*node_id, 7), NodeInput::value(TaggedValue::DAffine2(transform), false), network_path); .set_input(&InputConnector::node_at_index(*node_id, 6), NodeInput::value(TaggedValue::DAffine2(transform), false), network_path);
} else { } else {
document.pending_gradient_bbox_bake.push((network_path.to_vec(), *node_id, g.clone())); document.pending_gradient_bbox_bake.push((network_path.to_vec(), *node_id, g.clone()));
} }
} }
} }
inputs_count = 8; inputs_count = 7;
} }
// Fill split its `Option<DAffine2>` placement into a `_has_transform` bool immediately before the `_transform` matrix // Fill split its `Option<DAffine2>` placement into a `_has_transform` bool immediately before the `_transform` matrix. The modern
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER) && inputs_count == 7 { // shape is also 7 inputs, so this era is identified by its `_spread_method` input at 5 or its optional transform at 6.
let is_pre_transform_split_fill = inputs_count == 7
&& (matches!(node.inputs.get(5).and_then(|input| input.as_value()), Some(TaggedValue::GradientSpreadMethod(_)))
|| matches!(node.inputs.get(6).and_then(|input| input.as_value()), Some(TaggedValue::LegacyOptionalDAffine2(_))));
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER) && is_pre_transform_split_fill {
let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?; let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
for (index, input) in old_inputs.iter().enumerate().take(6) { let spread_method = match old_inputs.get(5).and_then(|input| input.as_value()) {
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, index), input.clone(), network_path); Some(&TaggedValue::GradientSpreadMethod(value)) => value,
_ => GradientSpreadMethod::default(),
};
for (index, input) in old_inputs.iter().enumerate().take(5) {
let input = if index == 1 || index == 3 {
fold_spread_method_into_ramp_input(input, spread_method)
} else {
input.clone()
};
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, index), input, network_path);
} }
match old_inputs.get(6).and_then(|input| input.as_value()) { match old_inputs.get(6).and_then(|input| input.as_value()) {
@@ -1775,22 +1796,46 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
let transform = value.unwrap_or(glam::DAffine2::IDENTITY); let transform = value.unwrap_or(glam::DAffine2::IDENTITY);
document document
.network_interface .network_interface
.set_input(&InputConnector::node_at_index(*node_id, 6), NodeInput::value(TaggedValue::Bool(has_transform), false), network_path); .set_input(&InputConnector::node_at_index(*node_id, 5), NodeInput::value(TaggedValue::Bool(has_transform), false), network_path);
document document
.network_interface .network_interface
.set_input(&InputConnector::node_at_index(*node_id, 7), NodeInput::value(TaggedValue::DAffine2(transform), false), network_path); .set_input(&InputConnector::node_at_index(*node_id, 6), NodeInput::value(TaggedValue::DAffine2(transform), false), network_path);
} }
// A wired (or otherwise non-value) transform keeps its connection and is treated as present // A wired (or otherwise non-value) transform keeps its connection and is treated as present
_ => { _ => {
document document
.network_interface .network_interface
.set_input(&InputConnector::node_at_index(*node_id, 6), NodeInput::value(TaggedValue::Bool(true), false), network_path); .set_input(&InputConnector::node_at_index(*node_id, 5), NodeInput::value(TaggedValue::Bool(true), false), network_path);
let transform_input = old_inputs.get(6).cloned().unwrap_or_else(|| NodeInput::value(TaggedValue::DAffine2(glam::DAffine2::IDENTITY), false)); let transform_input = old_inputs.get(6).cloned().unwrap_or_else(|| NodeInput::value(TaggedValue::DAffine2(glam::DAffine2::IDENTITY), false));
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 7), transform_input, network_path); document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 6), transform_input, network_path);
} }
} }
inputs_count = 8; inputs_count = 7;
}
// The Fill node's `_spread_method` input moved into the `GradientRamp` value's own `spread_method` field
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER) && inputs_count == 8 {
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
let spread_method = match old_inputs.get(5).and_then(|input| input.as_value()) {
Some(&TaggedValue::GradientSpreadMethod(value)) => value,
_ => GradientSpreadMethod::default(),
};
for (index, input) in old_inputs.iter().enumerate().take(5) {
let input = if index == 1 || index == 3 {
fold_spread_method_into_ramp_input(input, spread_method)
} else {
input.clone()
};
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, index), input, network_path);
}
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 5), old_inputs[6].clone(), network_path);
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 6), old_inputs[7].clone(), network_path);
inputs_count = 7;
} }
// Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644) // Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644)
@@ -381,6 +381,14 @@ pub fn gradient_space_transform(layer: LayerNodeIdentifier, network_interface: &
metadata.transform_to_viewport(layer) metadata.transform_to_viewport(layer)
} }
/// Tooltip description for a "Reverse Direction" gradient button, phrased for the given gradient type.
pub fn reverse_direction_tooltip_description(gradient_type: GradientType) -> &'static str {
match gradient_type {
GradientType::Radial => "Reverse which end the gradient radiates from.",
GradientType::Linear => "Swap the start and end points of the gradient line.",
}
}
/// True when start→end (mapped through `transform` into viewport space) points predominantly rightward. For purely /// True when start→end (mapped through `transform` into viewport space) points predominantly rightward. For purely
/// vertical lines we fall back to a stable tiebreaker on (x + y) so the choice doesn't flicker between equal alternatives. /// vertical lines we fall back to a stable tiebreaker on (x + y) so the choice doesn't flicker between equal alternatives.
pub fn gradient_orientation_rightward(transform: glam::DAffine2) -> bool { pub fn gradient_orientation_rightward(transform: glam::DAffine2) -> bool {
@@ -665,15 +673,12 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn
let TaggedValue::GradientRamp(ramp) = fill_node.input(fill::FillInput)?.as_value()? else { let TaggedValue::GradientRamp(ramp) = fill_node.input(fill::FillInput)?.as_value()? else {
return None; return None;
}; };
let spread_method = ramp.spread_method;
let stops = Gradient::from(ramp); let stops = Gradient::from(ramp);
let gradient_type = match fill_node.input(fill::GradientTypeInput).and_then(|input| input.as_value()) { let gradient_type = match fill_node.input(fill::GradientTypeInput).and_then(|input| input.as_value()) {
Some(&TaggedValue::GradientType(value)) => value, Some(&TaggedValue::GradientType(value)) => value,
_ => GradientType::default(), _ => GradientType::default(),
}; };
let spread_method = match fill_node.input(fill::SpreadMethodInput).and_then(|input| input.as_value()) {
Some(&TaggedValue::GradientSpreadMethod(value)) => value,
_ => GradientSpreadMethod::default(),
};
let has_transform = matches!(fill_node.input(fill::HasTransformInput).and_then(|input| input.as_value()), Some(&TaggedValue::Bool(true))); let has_transform = matches!(fill_node.input(fill::HasTransformInput).and_then(|input| input.as_value()), Some(&TaggedValue::Bool(true)));
let transform_input = fill_node.input(fill::TransformInput).and_then(|input| input.as_value()); let transform_input = fill_node.input(fill::TransformInput).and_then(|input| input.as_value());
let transform = match (has_transform, transform_input) { let transform = match (has_transform, transform_input) {
@@ -823,10 +828,6 @@ pub fn set_fill_for_selected_layers(fill_choice: FillChoice, document: &Document
Some(TaggedValue::GradientType(value)) => *value, Some(TaggedValue::GradientType(value)) => *value,
_ => GradientType::default(), _ => GradientType::default(),
}; };
let spread_method = match fill_parameters.as_ref().and_then(|parameters| parameters.value(fill::SpreadMethodInput)) {
Some(TaggedValue::GradientSpreadMethod(value)) => *value,
_ => GradientSpreadMethod::default(),
};
let has_transform = matches!(fill_parameters.as_ref().and_then(|parameters| parameters.value(fill::HasTransformInput)), Some(TaggedValue::Bool(true))); let has_transform = matches!(fill_parameters.as_ref().and_then(|parameters| parameters.value(fill::HasTransformInput)), Some(TaggedValue::Bool(true)));
let transform = match (has_transform, fill_parameters.as_ref().and_then(|parameters| parameters.value(fill::TransformInput))) { let transform = match (has_transform, fill_parameters.as_ref().and_then(|parameters| parameters.value(fill::TransformInput))) {
(true, Some(TaggedValue::DAffine2(value))) => *value, (true, Some(TaggedValue::DAffine2(value))) => *value,
@@ -838,7 +839,7 @@ pub fn set_fill_for_selected_layers(fill_choice: FillChoice, document: &Document
layer, layer,
gradient: Gradient::from(ramp), gradient: Gradient::from(ramp),
gradient_type, gradient_type,
spread_method, spread_method: ramp.spread_method,
transform, transform,
}); });
} }
@@ -9,14 +9,14 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface};
use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::graph_modification_utils::{ use crate::messages::tool::common_functionality::graph_modification_utils::{
self, NodeGraphLayer, get_fill_node_id_with_direct_fill_input, get_gradient_stops, get_upstream_gradient_value_node_id, gradient_chain_target_input, self, NodeGraphLayer, get_fill_node_id_with_direct_fill_input, get_gradient_stops, get_upstream_gradient_value_node_id, gradient_chain_target_input, reverse_direction_tooltip_description,
}; };
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration}; use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
use glam::DMat2; use glam::DMat2;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graphene_std::color::SRGBA8; use graphene_std::color::SRGBA8;
use graphene_std::raster::color::Color; use graphene_std::raster::color::Color;
use graphene_std::vector::style::{FillChoice, Gradient, GradientRamp, GradientSpreadMethod, GradientStop, GradientStops, GradientType, build_transform_with_y_preservation}; use graphene_std::vector::style::{FillChoice, Gradient, GradientRamp, GradientSpreadMethod, GradientStop, GradientType, build_transform_with_y_preservation};
#[derive(Default, ExtractField)] #[derive(Default, ExtractField)]
pub struct GradientTool { pub struct GradientTool {
@@ -53,7 +53,7 @@ pub enum GradientToolMessage {
CommitTransactionForColorStop, CommitTransactionForColorStop,
CloseStopColorPicker, CloseStopColorPicker,
UpdateStopColor { color: Color }, UpdateStopColor { color: Color },
UpdateStops { stops: GradientStops<SRGBA8> }, UpdateRamp { ramp: GradientRamp<SRGBA8> },
UpdateOptions { options: GradientOptionsUpdate }, UpdateOptions { options: GradientOptionsUpdate },
} }
@@ -63,7 +63,6 @@ pub enum GradientOptionsUpdate {
Type(GradientType), Type(GradientType),
ReverseStops, ReverseStops,
ReverseDirection, ReverseDirection,
SetSpreadMethod(GradientSpreadMethod),
} }
impl ToolMetadata for GradientTool { impl ToolMetadata for GradientTool {
@@ -111,16 +110,6 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
appearance.transform *= reverse; appearance.transform *= reverse;
}, },
), ),
GradientOptionsUpdate::SetSpreadMethod(spread_method) => {
self.options.spread_method = spread_method;
apply_gradient_update(
&mut self.data,
context,
responses,
|(_gradient, appearance)| appearance.spread_method != spread_method,
|(_gradient, appearance)| appearance.spread_method = spread_method,
);
}
}, },
ToolMessage::Gradient(GradientToolMessage::StartTransactionForColorStop) => { ToolMessage::Gradient(GradientToolMessage::StartTransactionForColorStop) => {
if self.data.color_picker_transaction_open { if self.data.color_picker_transaction_open {
@@ -145,8 +134,10 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
responses.add(PropertiesPanelMessage::Refresh); responses.add(PropertiesPanelMessage::Refresh);
} }
} }
ToolMessage::Gradient(GradientToolMessage::UpdateStops { stops }) => { ToolMessage::Gradient(GradientToolMessage::UpdateRamp { ramp }) => {
apply_stops_update(&mut self.data, context, responses, Gradient::from(&stops)); let ramp = GradientRamp::from(&ramp);
self.options.spread_method = ramp.spread_method;
apply_stops_update(&mut self.data, context, responses, Gradient::from(&ramp), ramp.spread_method);
} }
ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => { ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => {
if self.data.color_picker_transaction_open { if self.data.color_picker_transaction_open {
@@ -271,17 +262,20 @@ impl LayoutHolder for GradientTool {
}, },
]) ])
}); });
let stops_widget = ColorInput::new(FillChoice::Gradient(GradientRamp::from(&stops_value))) let stops_widget = ColorInput::new(FillChoice::Gradient(GradientRamp {
.allow_none(false) spread_method: self.options.spread_method,
.narrow(true) ..GradientRamp::from(&stops_value)
.tooltip_label("Gradient Stops") }))
.tooltip_description("Edit the gradient's color stops.") .allow_none(false)
.on_update(|input: &ColorInput| { .narrow(true)
let stops = input.value.as_gradient().map(|ramp| ramp.stops.clone()).unwrap_or_default(); .tooltip_label("Gradient Stops")
GradientToolMessage::UpdateStops { stops }.into() .tooltip_description("Edit the gradient's color stops.")
}) .on_update(|input: &ColorInput| {
.on_commit(|_| DocumentMessage::AddTransaction.into()) let ramp = input.value.as_gradient().cloned().unwrap_or_default();
.widget_instance(); GradientToolMessage::UpdateRamp { ramp }.into()
})
.on_commit(|_| DocumentMessage::AddTransaction.into())
.widget_instance();
let reverse_stops = IconButton::new("Reverse", 24) let reverse_stops = IconButton::new("Reverse", 24)
.tooltip_label("Reverse Stops") .tooltip_label("Reverse Stops")
@@ -295,29 +289,6 @@ impl LayoutHolder for GradientTool {
}) })
.widget_instance(); .widget_instance();
let spread_method = RadioInput::new(vec![
RadioEntryData::new("Pad").label("Pad").tooltip_label("Pad Spread Method").on_update(move |_| {
GradientToolMessage::UpdateOptions {
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Pad),
}
.into()
}),
RadioEntryData::new("Reflect").label("Reflect").tooltip_label("Reflect Spread Method").on_update(move |_| {
GradientToolMessage::UpdateOptions {
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Reflect),
}
.into()
}),
RadioEntryData::new("Repeat").label("Repeat").tooltip_label("Repeat Spread Method").on_update(move |_| {
GradientToolMessage::UpdateOptions {
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Repeat),
}
.into()
}),
])
.selected_index(Some(self.options.spread_method as u32))
.widget_instance();
let reverse_direction_icon = if self.data.gradient_orientation_rightward { let reverse_direction_icon = if self.data.gradient_orientation_rightward {
"ReverseRadialGradientToRight" "ReverseRadialGradientToRight"
} else { } else {
@@ -325,7 +296,7 @@ impl LayoutHolder for GradientTool {
}; };
let reverse_direction = IconButton::new(reverse_direction_icon, 24) let reverse_direction = IconButton::new(reverse_direction_icon, 24)
.tooltip_label("Reverse Direction") .tooltip_label("Reverse Direction")
.tooltip_description("Reverse which end the gradient radiates from.") .tooltip_description(reverse_direction_tooltip_description(self.options.gradient_type))
.disabled(!self.data.has_selected_gradient) .disabled(!self.data.has_selected_gradient)
.on_update(|_| { .on_update(|_| {
GradientToolMessage::UpdateOptions { GradientToolMessage::UpdateOptions {
@@ -341,8 +312,6 @@ impl LayoutHolder for GradientTool {
reverse_stops, reverse_stops,
Separator::new(SeparatorStyle::Unrelated).widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance(),
gradient_type, gradient_type,
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
spread_method,
Separator::new(SeparatorStyle::Related).widget_instance(), Separator::new(SeparatorStyle::Related).widget_instance(),
reverse_direction, reverse_direction,
]); ]);
@@ -1891,7 +1860,7 @@ fn apply_gradient_update(
/// Set new gradient stops on every selected layer's gradient. Unlike `apply_gradient_update`, this doesn't open its own /// Set new gradient stops on every selected layer's gradient. Unlike `apply_gradient_update`, this doesn't open its own
/// transaction so it can be called repeatedly during a color picker drag and have all the changes coalesced into a /// transaction so it can be called repeatedly during a color picker drag and have all the changes coalesced into a
/// single undo entry by the surrounding 'on_commit' callback. /// single undo entry by the surrounding 'on_commit' callback.
fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessageContext, responses: &mut VecDeque<Message>, new_gradient: Gradient) { fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessageContext, responses: &mut VecDeque<Message>, new_gradient: Gradient, spread_method: GradientSpreadMethod) {
let selected_layers: Vec<_> = context let selected_layers: Vec<_> = context
.document .document
.network_interface .network_interface
@@ -1907,13 +1876,14 @@ fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessa
if get_upstream_gradient_value_node_id(layer, &context.document.network_interface).is_some() { if get_upstream_gradient_value_node_id(layer, &context.document.network_interface).is_some() {
responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: new_gradient.clone() }); responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: new_gradient.clone() });
responses.add(GraphOperationMessage::GradientSpreadMethodSet { layer, spread_method });
updated_any_layer = true; updated_any_layer = true;
} else if let Some((_gradient, appearance, _source)) = resolve_gradient(layer, &context.document.network_interface) { } else if let Some((_gradient, appearance, _source)) = resolve_gradient(layer, &context.document.network_interface) {
responses.add(GraphOperationMessage::FillGradientSet { responses.add(GraphOperationMessage::FillGradientSet {
layer, layer,
gradient: new_gradient.clone(), gradient: new_gradient.clone(),
gradient_type: appearance.gradient_type, gradient_type: appearance.gradient_type,
spread_method: appearance.spread_method, spread_method,
transform: appearance.transform, transform: appearance.transform,
}); });
updated_any_layer = true; updated_any_layer = true;
@@ -1922,6 +1892,7 @@ fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessa
if let Some(selected_gradient) = &mut data.selected_gradient { if let Some(selected_gradient) = &mut data.selected_gradient {
selected_gradient.gradient = new_gradient.clone(); selected_gradient.gradient = new_gradient.clone();
selected_gradient.appearance.spread_method = spread_method;
} }
// When no selected layer had a gradient to update, the user is editing the tool's default gradient instead. // When no selected layer had a gradient to update, the user is editing the tool's default gradient instead.
@@ -2011,6 +1982,7 @@ mod test_gradient {
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector}; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector};
use crate::messages::tool::common_functionality::graph_modification_utils::get_fill_node_id_with_direct_fill_input; use crate::messages::tool::common_functionality::graph_modification_utils::get_fill_node_id_with_direct_fill_input;
use crate::messages::tool::common_functionality::graph_modification_utils::get_gradient_stops;
use crate::messages::tool::common_functionality::graph_modification_utils::get_upstream_gradient_value_node_id; use crate::messages::tool::common_functionality::graph_modification_utils::get_upstream_gradient_value_node_id;
pub use crate::test_utils::test_prelude::*; pub use crate::test_utils::test_prelude::*;
use glam::DAffine2; use glam::DAffine2;
@@ -2060,16 +2032,11 @@ mod test_gradient {
let fill_node_id = get_fill_node_id_with_direct_fill_input(layer, &document.network_interface)?; let fill_node_id = get_fill_node_id_with_direct_fill_input(layer, &document.network_interface)?;
let fill_node = document.network_interface.document_network().nodes.get(&fill_node_id)?; let fill_node = document.network_interface.document_network().nodes.get(&fill_node_id)?;
let stops = match fill_node.input(fill::FillInput)?.as_value()? { let (stops, spread_method) = match fill_node.input(fill::FillInput)?.as_value()? {
TaggedValue::GradientRamp(ramp) => Gradient::from(ramp), TaggedValue::GradientRamp(ramp) => (Gradient::from(ramp), ramp.spread_method),
_ => return None, _ => return None,
}; };
let spread_method = match fill_node.input(fill::SpreadMethodInput).and_then(|input| input.as_value()) {
Some(&TaggedValue::GradientSpreadMethod(value)) => value,
_ => GradientSpreadMethod::default(),
};
let has_transform = matches!(fill_node.input(fill::HasTransformInput).and_then(|input| input.as_value()), Some(&TaggedValue::Bool(true))); let has_transform = matches!(fill_node.input(fill::HasTransformInput).and_then(|input| input.as_value()), Some(&TaggedValue::Bool(true)));
let local_transform = match fill_node.input(fill::TransformInput).and_then(|input| input.as_value()) { let local_transform = match fill_node.input(fill::TransformInput).and_then(|input| input.as_value()) {
Some(&TaggedValue::DAffine2(value)) if has_transform => value, Some(&TaggedValue::DAffine2(value)) if has_transform => value,
@@ -2606,6 +2573,14 @@ mod test_gradient {
assert_eq!(editor.active_document().metadata().all_layers().count(), 0, "Expected the layer to be deleted after drawing a gradient"); assert_eq!(editor.active_document().metadata().all_layers().count(), 0, "Expected the layer to be deleted after drawing a gradient");
} }
/// Build the JS-boundary ramp the stops swatch's picker would send when choosing a new spread method.
fn ramp_with_spread(stops: &Gradient, spread_method: GradientSpreadMethod) -> GradientRamp<SRGBA8> {
GradientRamp::<SRGBA8>::from(&GradientRamp {
spread_method,
..GradientRamp::from(stops)
})
}
#[tokio::test] #[tokio::test]
async fn change_spread_method() { async fn change_spread_method() {
let mut editor = EditorTestUtils::create(); let mut editor = EditorTestUtils::create();
@@ -2619,8 +2594,8 @@ mod test_gradient {
// Update spread method to Repeat // Update spread method to Repeat
editor editor
.handle_message(GradientToolMessage::UpdateOptions { .handle_message(GradientToolMessage::UpdateRamp {
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Repeat), ramp: ramp_with_spread(&gradient.stops, GradientSpreadMethod::Repeat),
}) })
.await; .await;
@@ -2629,8 +2604,8 @@ mod test_gradient {
// Update spread method to Reflect // Update spread method to Reflect
editor editor
.handle_message(GradientToolMessage::UpdateOptions { .handle_message(GradientToolMessage::UpdateRamp {
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Reflect), ramp: ramp_with_spread(&gradient.stops, GradientSpreadMethod::Reflect),
}) })
.await; .await;
@@ -2652,8 +2627,8 @@ mod test_gradient {
// Update spread method to Repeat // Update spread method to Repeat
editor editor
.handle_message(GradientToolMessage::UpdateOptions { .handle_message(GradientToolMessage::UpdateRamp {
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Repeat), ramp: ramp_with_spread(&gradient.stops, GradientSpreadMethod::Repeat),
}) })
.await; .await;
@@ -2662,8 +2637,8 @@ mod test_gradient {
// Update spread method to Reflect // Update spread method to Reflect
editor editor
.handle_message(GradientToolMessage::UpdateOptions { .handle_message(GradientToolMessage::UpdateRamp {
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Reflect), ramp: ramp_with_spread(&gradient.stops, GradientSpreadMethod::Reflect),
}) })
.await; .await;
@@ -2880,9 +2855,10 @@ mod test_gradient {
// Set the spread method through the tool, which splices a 'Spread Method' node onto the Fill's fill input wire. // Set the spread method through the tool, which splices a 'Spread Method' node onto the Fill's fill input wire.
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await; editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
editor.select_tool(ToolType::Gradient).await; editor.select_tool(ToolType::Gradient).await;
let stops = get_gradient_stops(layer, &editor.active_document().network_interface).expect("the chain layer should resolve its gradient stops");
editor editor
.handle_message(GradientToolMessage::UpdateOptions { .handle_message(GradientToolMessage::UpdateRamp {
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Reflect), ramp: ramp_with_spread(&stops, GradientSpreadMethod::Reflect),
}) })
.await; .await;
+24 -3
View File
@@ -167,7 +167,15 @@ macro_rules! tagged_value {
Self::DashPattern(lengths) => Box::new(DashPattern::from(lengths)), Self::DashPattern(lengths) => Box::new(DashPattern::from(lengths)),
Self::BoxCorners(values) => Box::new(BoxCorners::from(values)), Self::BoxCorners(values) => Box::new(BoxCorners::from(values)),
Self::Color(color) => Box::new(List::<Color>::new_from_element(color)), Self::Color(color) => Box::new(List::<Color>::new_from_element(color)),
Self::GradientRamp(ramp) => Box::new(List::<Gradient>::new_from_element(Gradient::from(ramp))), Self::GradientRamp(ramp) => {
// The ramp's spread method rides the served list as its attribute, as `Item<Gradient>::from` does on master.
let spread_method = ramp.spread_method;
let mut list = List::<Gradient>::new_from_element(Gradient::from(ramp));
if !spread_method.is_default() {
list.set_attribute(graphic_types::vector_types::ATTR_SPREAD_METHOD, 0, spread_method);
}
Box::new(list)
}
Self::BrushStrokes(strokes) => { Self::BrushStrokes(strokes) => {
let list: List<BrushStroke> = strokes.into_iter().map(core_types::list::Item::new_from_element).collect(); let list: List<BrushStroke> = strokes.into_iter().map(core_types::list::Item::new_from_element).collect();
Box::new(list) Box::new(list)
@@ -213,7 +221,15 @@ macro_rules! tagged_value {
Self::DashPattern(lengths) => Arc::new(DashPattern::from(lengths)), Self::DashPattern(lengths) => Arc::new(DashPattern::from(lengths)),
Self::BoxCorners(values) => Arc::new(BoxCorners::from(values)), Self::BoxCorners(values) => Arc::new(BoxCorners::from(values)),
Self::Color(color) => Arc::new(List::<Color>::new_from_element(color)), Self::Color(color) => Arc::new(List::<Color>::new_from_element(color)),
Self::GradientRamp(ramp) => Arc::new(List::<Gradient>::new_from_element(Gradient::from(ramp))), Self::GradientRamp(ramp) => {
// The ramp's spread method rides the served list as its attribute, as `Item<Gradient>::from` does on master.
let spread_method = ramp.spread_method;
let mut list = List::<Gradient>::new_from_element(Gradient::from(ramp));
if !spread_method.is_default() {
list.set_attribute(graphic_types::vector_types::ATTR_SPREAD_METHOD, 0, spread_method);
}
Arc::new(list)
}
Self::BrushStrokes(strokes) => { Self::BrushStrokes(strokes) => {
let list: List<BrushStroke> = strokes.into_iter().map(core_types::list::Item::new_from_element).collect(); let list: List<BrushStroke> = strokes.into_iter().map(core_types::list::Item::new_from_element).collect();
Arc::new(list) Arc::new(list)
@@ -1086,6 +1102,8 @@ mod paint_default_parsing {
#[cfg(test)] #[cfg(test)]
mod gradient_shape_migration { mod gradient_shape_migration {
use graphic_types::vector_types::GradientSpreadMethod;
use super::*; use super::*;
fn load(payload: serde_json::Value) -> TaggedValue { fn load(payload: serde_json::Value) -> TaggedValue {
@@ -1104,7 +1122,10 @@ mod gradient_shape_migration {
fn modern_ramp_payload_round_trips() { fn modern_ramp_payload_round_trips() {
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]); let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
gradient.set_positions(&[0.2, 0.9]); gradient.set_positions(&[0.2, 0.9]);
let value = TaggedValue::GradientRamp(GradientRamp::from(gradient)); let value = TaggedValue::GradientRamp(GradientRamp {
spread_method: GradientSpreadMethod::Reflect,
..GradientRamp::from(gradient)
});
let json = serde_json::to_value(&value).unwrap(); let json = serde_json::to_value(&value).unwrap();
assert!(json.get("GradientRamp").and_then(|payload| payload.get("stops")).is_some(), "the payload should nest its stops: {json}"); assert!(json.get("GradientRamp").and_then(|payload| payload.get("stops")).is_some(), "the payload should nest its stops: {json}");
@@ -1,3 +1,4 @@
use crate::ATTR_SPREAD_METHOD;
use core_types::Color; use core_types::Color;
use core_types::color::SRGBA8; use core_types::color::SRGBA8;
use core_types::list::{ATTR_MIDPOINT, ATTR_POSITION, Item, List}; use core_types::list::{ATTR_MIDPOINT, ATTR_POSITION, Item, List};
@@ -96,13 +97,15 @@ impl GradientStops<SRGBA8> {
} }
} }
/// The serialized exchange form of a gradient: its stops, nested so that whole-ramp settings /// The serialized exchange form of a gradient: its stops, with whole-ramp settings as sibling fields serialized only when non-default.
/// like spread method can join as sibling fields opted in from their defaults.
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash)] #[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GradientRamp<C = Color> { pub struct GradientRamp<C = Color> {
pub stops: GradientStops<C>, pub stops: GradientStops<C>,
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "GradientSpreadMethod::is_default"))]
#[cfg_attr(feature = "wasm", tsify(optional))]
pub spread_method: GradientSpreadMethod,
} }
unsafe impl<C: dyn_any::StaticTypeSized> dyn_any::StaticType for GradientRamp<C> { unsafe impl<C: dyn_any::StaticTypeSized> dyn_any::StaticType for GradientRamp<C> {
@@ -111,13 +114,19 @@ unsafe impl<C: dyn_any::StaticTypeSized> dyn_any::StaticType for GradientRamp<C>
impl<C> From<GradientStops<C>> for GradientRamp<C> { impl<C> From<GradientStops<C>> for GradientRamp<C> {
fn from(stops: GradientStops<C>) -> Self { fn from(stops: GradientStops<C>) -> Self {
Self { stops } Self {
stops,
spread_method: Default::default(),
}
} }
} }
impl From<&Gradient> for GradientRamp { impl From<&Gradient> for GradientRamp {
fn from(gradient: &Gradient) -> Self { fn from(gradient: &Gradient) -> Self {
Self { stops: gradient.into() } Self {
stops: gradient.into(),
spread_method: Default::default(),
}
} }
} }
@@ -139,6 +148,27 @@ impl From<&GradientRamp> for Gradient {
} }
} }
// The runtime wire form: whole-ramp settings ride as the gradient item's attributes in its containing list,
// where the Fill kernel, chain setter nodes, and renderers read and write them
impl From<GradientRamp> for Item<Gradient> {
fn from(ramp: GradientRamp) -> Self {
let mut item = Item::new_from_element(Gradient::from(ramp.stops));
if !ramp.spread_method.is_default() {
item.set_attribute(ATTR_SPREAD_METHOD, ramp.spread_method);
}
item
}
}
impl From<&Item<Gradient>> for GradientRamp {
fn from(item: &Item<Gradient>) -> Self {
Self {
stops: item.element().into(),
spread_method: item.attribute_cloned_or_default(ATTR_SPREAD_METHOD),
}
}
}
impl From<&GradientRamp> for GradientStops<SRGBA8> { impl From<&GradientRamp> for GradientStops<SRGBA8> {
fn from(ramp: &GradientRamp) -> Self { fn from(ramp: &GradientRamp) -> Self {
Self { Self {
@@ -158,19 +188,28 @@ impl From<&GradientStops<SRGBA8>> for GradientRamp {
impl From<&GradientRamp> for GradientRamp<SRGBA8> { impl From<&GradientRamp> for GradientRamp<SRGBA8> {
fn from(ramp: &GradientRamp) -> Self { fn from(ramp: &GradientRamp) -> Self {
Self { stops: ramp.into() } Self {
stops: ramp.into(),
spread_method: ramp.spread_method,
}
} }
} }
impl From<&Gradient> for GradientRamp<SRGBA8> { impl From<&Gradient> for GradientRamp<SRGBA8> {
fn from(gradient: &Gradient) -> Self { fn from(gradient: &Gradient) -> Self {
Self { stops: gradient.into() } Self {
stops: gradient.into(),
spread_method: Default::default(),
}
} }
} }
impl From<&GradientRamp<SRGBA8>> for GradientRamp { impl From<&GradientRamp<SRGBA8>> for GradientRamp {
fn from(ramp: &GradientRamp<SRGBA8>) -> Self { fn from(ramp: &GradientRamp<SRGBA8>) -> Self {
Self::from(&ramp.stops) Self {
spread_method: ramp.spread_method,
..Self::from(&ramp.stops)
}
} }
} }
@@ -747,9 +786,12 @@ impl Gradient {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[widget(Radio)] #[widget(Radio)]
pub enum GradientSpreadMethod { pub enum GradientSpreadMethod {
/// Extends the end colors outward.
#[default] #[default]
Pad, Pad,
/// Loops the gradient by mirroring back-and-forth.
Reflect, Reflect,
/// Loops the gradient as copies of itself.
Repeat, Repeat,
// TODO: Add a "Clear" variant that returns transparent black outside the gradient's range // TODO: Add a "Clear" variant that returns transparent black outside the gradient's range
} }
@@ -762,6 +804,10 @@ impl GradientSpreadMethod {
GradientSpreadMethod::Repeat => "repeat", GradientSpreadMethod::Repeat => "repeat",
} }
} }
pub fn is_default(&self) -> bool {
*self == Self::default()
}
} }
/// Rebuild the y-axis so its (parallel, perpendicular) components in the x-axis-aligned frame stay constant, both /// Rebuild the y-axis so its (parallel, perpendicular) components in the x-axis-aligned frame stay constant, both
@@ -859,6 +905,44 @@ mod tests {
assert_eq!(serde_json::from_str::<GradientRamp>(&json).unwrap(), explicit); assert_eq!(serde_json::from_str::<GradientRamp>(&json).unwrap(), explicit);
} }
#[test]
fn spread_method_serializes_only_when_not_default() {
let default_spread = GradientRamp::from(Gradient::from(vec![Color::BLACK, Color::WHITE]));
let json = serde_json::to_string(&default_spread).unwrap();
assert!(!json.contains("spread_method"), "the default Pad spread method must not serialize: {json}");
assert_eq!(serde_json::from_str::<GradientRamp>(&json).unwrap(), default_spread);
let repeating = GradientRamp {
spread_method: GradientSpreadMethod::Repeat,
..default_spread.clone()
};
let json = serde_json::to_string(&repeating).unwrap();
assert!(json.contains(r#""spread_method":"Repeat""#), "a non-default spread method must serialize: {json}");
assert_eq!(serde_json::from_str::<GradientRamp>(&json).unwrap(), repeating);
}
#[test]
fn spread_method_round_trips_through_the_item_attribute() {
let ramp = GradientRamp {
spread_method: GradientSpreadMethod::Repeat,
..GradientRamp::from(Gradient::from(vec![Color::BLACK, Color::WHITE]))
};
let item = Item::<Gradient>::from(ramp.clone());
assert_eq!(
item.attribute_cloned_or_default::<GradientSpreadMethod>(ATTR_SPREAD_METHOD),
GradientSpreadMethod::Repeat,
"the runtime item should carry the spread method as its attribute"
);
assert_eq!(GradientRamp::from(&item), ramp);
let padded = Item::<Gradient>::from(GradientRamp::from(Gradient::from(vec![Color::BLACK, Color::WHITE])));
assert!(
padded.attribute::<GradientSpreadMethod>(ATTR_SPREAD_METHOD).is_none(),
"the default Pad must stay absent rather than materialize"
);
}
#[test] #[test]
fn gradient_ui_write_back_elides_default_attributes() { fn gradient_ui_write_back_elides_default_attributes() {
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE, Color::RED]); let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE, Color::RED]);
+5 -11
View File
@@ -27,6 +27,8 @@ use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArcle
use rand::{Rng, SeedableRng}; use rand::{Rng, SeedableRng};
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use vector_types::ATTR_GRADIENT_TYPE;
use vector_types::GradientType;
use vector_types::gradient::{build_transform_with_y_preservation, initial_gradient_transform_for_bounding_box}; use vector_types::gradient::{build_transform_with_y_preservation, initial_gradient_transform_for_bounding_box};
use vector_types::subpath::{BezierHandles, ManipulatorGroup}; use vector_types::subpath::{BezierHandles, ManipulatorGroup};
use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, eval_pathseg_euclidean, evaluate_bezpath, split_bezpath, tangent_on_bezpath}; use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, eval_pathseg_euclidean, evaluate_bezpath, split_bezpath, tangent_on_bezpath};
@@ -40,8 +42,6 @@ use vector_types::vector::misc::{
use vector_types::vector::style::{DashPattern, Gradient, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use vector_types::vector::style::{DashPattern, Gradient, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt}; use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt};
use vector_types::vector::{PointDomain, RegionDomain}; use vector_types::vector::{PointDomain, RegionDomain};
use vector_types::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD};
use vector_types::{GradientSpreadMethod, GradientType};
/// The gradient color for one assign-colors position, replaying the /// The gradient color for one assign-colors position, replaying the
/// randomized draws up to it. /// randomized draws up to it.
@@ -276,9 +276,8 @@ fn park_paint<'e>(arena: &'e core_types::arena::Arena, paint: List<Graphic<'stat
/// The gradient defaulting the legacy fill performed, applied to the nested /// The gradient defaulting the legacy fill performed, applied to the nested
/// stops list the paint table wraps. /// stops list the paint table wraps.
fn default_gradient_paint(paint: &mut List<Graphic>, bounds: Option<[DVec2; 2]>, gradient_type: GradientType, spread_method: GradientSpreadMethod, transform: Option<DAffine2>) { fn default_gradient_paint(paint: &mut List<Graphic>, bounds: Option<[DVec2; 2]>, gradient_type: GradientType, transform: Option<DAffine2>) {
let has_type = paint.iter_attribute_values::<GradientType>(ATTR_GRADIENT_TYPE).is_some(); let has_type = paint.iter_attribute_values::<GradientType>(ATTR_GRADIENT_TYPE).is_some();
let has_spread = paint.iter_attribute_values::<GradientSpreadMethod>(ATTR_SPREAD_METHOD).is_some();
let has_transform = paint.iter_attribute_values::<DAffine2>(ATTR_TRANSFORM).is_some(); let has_transform = paint.iter_attribute_values::<DAffine2>(ATTR_TRANSFORM).is_some();
for index in 0..paint.len() { for index in 0..paint.len() {
if !matches!(paint.element(index), Some(Graphic::Gradient(_))) { if !matches!(paint.element(index), Some(Graphic::Gradient(_))) {
@@ -287,9 +286,6 @@ fn default_gradient_paint(paint: &mut List<Graphic>, bounds: Option<[DVec2; 2]>,
if !has_type { if !has_type {
paint.set_attribute(ATTR_GRADIENT_TYPE, index, gradient_type); paint.set_attribute(ATTR_GRADIENT_TYPE, index, gradient_type);
} }
if !has_spread {
paint.set_attribute(ATTR_SPREAD_METHOD, index, spread_method);
}
if !has_transform { if !has_transform {
let transform = transform.unwrap_or_else(|| { let transform = transform.unwrap_or_else(|| {
// Nudge a degenerate axis so the gradient transform stays invertible, matching the editor's `nonzero_bounding_box` // Nudge a degenerate axis so the gradient transform stays invertible, matching the editor's `nonzero_bounding_box`
@@ -326,12 +322,11 @@ fn fill<'e>(
_backup_color: IList<Color>, _backup_color: IList<Color>,
#[default(Color::BLACK, Color::WHITE)] _backup_gradient: IList<Gradient>, #[default(Color::BLACK, Color::WHITE)] _backup_gradient: IList<Gradient>,
_gradient_type: GradientType, _gradient_type: GradientType,
_spread_method: GradientSpreadMethod,
_has_transform: bool, _has_transform: bool,
_transform: DAffine2, _transform: DAffine2,
) -> Result<(Vector, Attr<'e, Fill>), Interrupt> { ) -> Result<(Vector, Attr<'e, Fill>), Interrupt> {
let mut paint = paint_table(fill); let mut paint = paint_table(fill);
default_gradient_paint(&mut paint, element.bounding_box(), _gradient_type, _spread_method, _has_transform.then_some(_transform)); default_gradient_paint(&mut paint, element.bounding_box(), _gradient_type, _has_transform.then_some(_transform));
let parked = park_paint(ctx.arena(), paint)?; let parked = park_paint(ctx.arena(), paint)?;
Ok((element, Attr(Some(parked)))) Ok((element, Attr(Some(parked))))
} }
@@ -347,7 +342,6 @@ fn fill_graphic_leveled<'e>(
_backup_color: IList<Color>, _backup_color: IList<Color>,
#[default(Color::BLACK, Color::WHITE)] _backup_gradient: IList<Gradient>, #[default(Color::BLACK, Color::WHITE)] _backup_gradient: IList<Gradient>,
_gradient_type: GradientType, _gradient_type: GradientType,
_spread_method: GradientSpreadMethod,
_has_transform: bool, _has_transform: bool,
_transform: DAffine2, _transform: DAffine2,
) -> Result<(Graphic<'static>, Attr<'e, Fill>), Interrupt> { ) -> Result<(Graphic<'static>, Attr<'e, Fill>), Interrupt> {
@@ -356,7 +350,7 @@ fn fill_graphic_leveled<'e>(
_ => None, _ => None,
}; };
let mut paint = paint_table(fill); let mut paint = paint_table(fill);
default_gradient_paint(&mut paint, bounds, _gradient_type, _spread_method, _has_transform.then_some(_transform)); default_gradient_paint(&mut paint, bounds, _gradient_type, _has_transform.then_some(_transform));
let parked = park_paint(ctx.arena(), paint)?; let parked = park_paint(ctx.arena(), paint)?;
Ok((element, Attr(Some(parked)))) Ok((element, Attr(Some(parked))))
} }