mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
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:
committed by
Dennis Kobert
parent
2196d308a7
commit
26d67eb48b
2
demo-artwork/changing-seasons.graphite
generated
2
demo-artwork/changing-seasons.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/isometric-fountain.graphite
generated
2
demo-artwork/isometric-fountain.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/marbled-mandelbrot.graphite
generated
2
demo-artwork/marbled-mandelbrot.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/painted-dreams.graphite
generated
2
demo-artwork/painted-dreams.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/parametric-dunescape.graphite
generated
2
demo-artwork/parametric-dunescape.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/procedural-string-lights.graphite
generated
2
demo-artwork/procedural-string-lights.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/red-dress.graphite
generated
2
demo-artwork/red-dress.graphite
generated
File diff suppressed because one or more lines are too long
2
demo-artwork/valley-of-spires.graphite
generated
2
demo-artwork/valley-of-spires.graphite
generated
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::prelude::*;
|
||||
use graphene_std::vector::style::FillChoice;
|
||||
use graphene_std::vector::style::{FillChoice, GradientSpreadMethod};
|
||||
|
||||
/// Identifies which RGB channel a numeric input change targets.
|
||||
#[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.
|
||||
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).
|
||||
StartTransaction,
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::messages::prelude::*;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::color::SRGBA8;
|
||||
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).
|
||||
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.
|
||||
gradient: Option<Gradient>,
|
||||
spread_method: GradientSpreadMethod,
|
||||
active_marker_index: Option<u32>,
|
||||
active_marker_is_midpoint: bool,
|
||||
|
||||
@@ -50,6 +51,7 @@ impl Default for ColorPickerMessageHandler {
|
||||
old_alpha: 1.,
|
||||
old_is_none: true,
|
||||
gradient: None,
|
||||
spread_method: GradientSpreadMethod::default(),
|
||||
active_marker_index: None,
|
||||
active_marker_is_midpoint: false,
|
||||
allow_none: true,
|
||||
@@ -70,11 +72,13 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
|
||||
FillChoice::None => {
|
||||
self.set_new_hsva(0., 0., 0., 1., true);
|
||||
self.gradient = None;
|
||||
self.spread_method = GradientSpreadMethod::default();
|
||||
self.active_marker_index = None;
|
||||
self.active_marker_is_midpoint = false;
|
||||
}
|
||||
FillChoice::Solid(color) => {
|
||||
self.gradient = None;
|
||||
self.spread_method = GradientSpreadMethod::default();
|
||||
self.active_marker_index = None;
|
||||
self.active_marker_is_midpoint = false;
|
||||
self.adopt_color(color);
|
||||
@@ -82,6 +86,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
|
||||
FillChoice::Gradient(ramp) => {
|
||||
self.active_marker_index = Some(0);
|
||||
self.active_marker_is_midpoint = false;
|
||||
self.spread_method = ramp.spread_method;
|
||||
let gradient = Gradient::from(ramp);
|
||||
let first_color = gradient.color(0).unwrap_or(Color::BLACK);
|
||||
self.gradient = Some(gradient);
|
||||
@@ -187,6 +192,18 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
|
||||
self.send_layouts(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 => {
|
||||
responses.add(FrontendMessage::ColorPickerStartHistoryTransaction);
|
||||
}
|
||||
@@ -271,7 +288,10 @@ impl ColorPickerMessageHandler {
|
||||
{
|
||||
gradient.set_color(active_index as usize, color);
|
||||
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||
value: FillChoice::Gradient(GradientRamp::from(&*gradient)),
|
||||
value: FillChoice::Gradient(GradientRamp {
|
||||
spread_method: self.spread_method,
|
||||
..GradientRamp::from(&*gradient)
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||
@@ -398,7 +418,10 @@ impl ColorPickerMessageHandler {
|
||||
}
|
||||
|
||||
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.send_layouts(responses);
|
||||
@@ -598,6 +621,24 @@ impl ColorPickerMessageHandler {
|
||||
.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)
|
||||
groups.push(LayoutGroup::row(vec![
|
||||
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 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 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`).
|
||||
/// 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 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);
|
||||
|
||||
// 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(
|
||||
InputConnector::node(fill_node_id, graphene_std::vector::fill::GradientTypeInput),
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2461,13 +2461,23 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
};
|
||||
|
||||
match &fill {
|
||||
ResolvedFill::Gradient { gradient: stops, .. } => {
|
||||
ResolvedFill::Gradient { gradient: stops, spread_method, .. } => {
|
||||
let stops = stops.clone();
|
||||
let spread_method = *spread_method;
|
||||
|
||||
let reverse_button = IconButton::new("Reverse", 24)
|
||||
.tooltip_label("Reverse 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();
|
||||
widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
widgets_first_row.push(reverse_button);
|
||||
@@ -2483,7 +2493,10 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -2572,36 +2585,14 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
|
||||
if let ResolvedFill::Gradient {
|
||||
gradient_type,
|
||||
spread_method,
|
||||
transform,
|
||||
transform_is_value,
|
||||
..
|
||||
} = fill.clone()
|
||||
{
|
||||
// Linear/Radial radio: blank assist (the "Reverse Direction" button has been moved down to the spread method row)
|
||||
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
|
||||
// "Reverse Direction" button (assist) beside the Linear/Radial 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.
|
||||
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
|
||||
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)
|
||||
.tooltip_label("Reverse Direction")
|
||||
.tooltip_description(if gradient_type == GradientType::Radial {
|
||||
"Reverse which end the gradient radiates from."
|
||||
} else {
|
||||
"Swap the start and end points of the gradient line."
|
||||
})
|
||||
.tooltip_description(graph_modification_utils::reverse_direction_tooltip_description(gradient_type))
|
||||
.on_update(move |_| Message::Batched {
|
||||
messages: Box::new([
|
||||
NodeGraphMessage::SetInputValue {
|
||||
@@ -2634,28 +2621,28 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||
]),
|
||||
})
|
||||
.widget_instance();
|
||||
spread_methods_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
spread_methods_row.push(reverse_direction_button);
|
||||
row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
row.push(reverse_direction_button);
|
||||
} 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()
|
||||
.map(|&spread_method| {
|
||||
RadioEntryData::new(format!("{:?}", spread_method))
|
||||
.label(format!("{:?}", spread_method))
|
||||
.on_update(update_value(move |_| TaggedValue::GradientSpreadMethod(spread_method), node_id, SpreadMethodInput))
|
||||
.map(|&gradient_type| {
|
||||
RadioEntryData::new(format!("{:?}", gradient_type))
|
||||
.label(format!("{:?}", gradient_type))
|
||||
.on_update(update_value(move |_| TaggedValue::GradientType(gradient_type), node_id, GradientTypeInput))
|
||||
.on_commit(commit_value)
|
||||
})
|
||||
.collect();
|
||||
|
||||
spread_methods_row.extend_from_slice(&[
|
||||
row.extend_from_slice(&[
|
||||
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
|
||||
|
||||
@@ -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 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);
|
||||
assert!(
|
||||
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!(!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::uuid::NodeId;
|
||||
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::f64::consts::PI;
|
||||
use std::ops::Range;
|
||||
@@ -1413,6 +1413,14 @@ fn migrate_corner_radius_input(input: &NodeInput) -> Option<NodeInput> {
|
||||
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<()> {
|
||||
// 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.
|
||||
@@ -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
|
||||
// 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 {
|
||||
let mut node_template = resolve_document_node_type(&reference)?.default_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 {
|
||||
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::GradientRamp(gradient.stops.clone()),
|
||||
graphic_types::migrations::legacy::LegacyFill::Gradient(gradient) => TaggedValue::GradientRamp(GradientRamp {
|
||||
spread_method: gradient.spread_method,
|
||||
..gradient.stops.clone()
|
||||
}),
|
||||
};
|
||||
document
|
||||
.network_interface
|
||||
.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 {
|
||||
document.network_interface.set_input(
|
||||
&InputConnector::node_at_index(*node_id, 4),
|
||||
NodeInput::value(TaggedValue::GradientType(gradient.gradient_type), false),
|
||||
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 {
|
||||
let transform = gradient.transform * gradient.to_transform();
|
||||
document
|
||||
.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
|
||||
.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 {
|
||||
// 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
|
||||
@@ -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() {
|
||||
document.network_interface.set_input(
|
||||
&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,
|
||||
);
|
||||
|
||||
@@ -1737,36 +1749,45 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
NodeInput::value(TaggedValue::GradientType(g.gradient_type), false),
|
||||
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 {
|
||||
let transform = g.transform * g.to_transform();
|
||||
document
|
||||
.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
|
||||
.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 {
|
||||
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
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER) && inputs_count == 7 {
|
||||
// Fill split its `Option<DAffine2>` placement into a `_has_transform` bool immediately before the `_transform` matrix. The modern
|
||||
// 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 old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
|
||||
|
||||
for (index, input) in old_inputs.iter().enumerate().take(6) {
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, index), input.clone(), network_path);
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
document
|
||||
.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
|
||||
.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
|
||||
_ => {
|
||||
document
|
||||
.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));
|
||||
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)
|
||||
|
||||
@@ -381,6 +381,14 @@ pub fn gradient_space_transform(layer: LayerNodeIdentifier, network_interface: &
|
||||
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
|
||||
/// 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 {
|
||||
@@ -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 {
|
||||
return None;
|
||||
};
|
||||
let spread_method = ramp.spread_method;
|
||||
let stops = Gradient::from(ramp);
|
||||
let gradient_type = match fill_node.input(fill::GradientTypeInput).and_then(|input| input.as_value()) {
|
||||
Some(&TaggedValue::GradientType(value)) => value,
|
||||
_ => 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 transform_input = fill_node.input(fill::TransformInput).and_then(|input| input.as_value());
|
||||
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,
|
||||
_ => 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 transform = match (has_transform, fill_parameters.as_ref().and_then(|parameters| parameters.value(fill::TransformInput))) {
|
||||
(true, Some(TaggedValue::DAffine2(value))) => *value,
|
||||
@@ -838,7 +839,7 @@ pub fn set_fill_for_selected_layers(fill_choice: FillChoice, document: &Document
|
||||
layer,
|
||||
gradient: Gradient::from(ramp),
|
||||
gradient_type,
|
||||
spread_method,
|
||||
spread_method: ramp.spread_method,
|
||||
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::tool::common_functionality::auto_panning::AutoPanning;
|
||||
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 glam::DMat2;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graphene_std::color::SRGBA8;
|
||||
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)]
|
||||
pub struct GradientTool {
|
||||
@@ -53,7 +53,7 @@ pub enum GradientToolMessage {
|
||||
CommitTransactionForColorStop,
|
||||
CloseStopColorPicker,
|
||||
UpdateStopColor { color: Color },
|
||||
UpdateStops { stops: GradientStops<SRGBA8> },
|
||||
UpdateRamp { ramp: GradientRamp<SRGBA8> },
|
||||
UpdateOptions { options: GradientOptionsUpdate },
|
||||
}
|
||||
|
||||
@@ -63,7 +63,6 @@ pub enum GradientOptionsUpdate {
|
||||
Type(GradientType),
|
||||
ReverseStops,
|
||||
ReverseDirection,
|
||||
SetSpreadMethod(GradientSpreadMethod),
|
||||
}
|
||||
|
||||
impl ToolMetadata for GradientTool {
|
||||
@@ -111,16 +110,6 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
|
||||
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) => {
|
||||
if self.data.color_picker_transaction_open {
|
||||
@@ -145,8 +134,10 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
}
|
||||
}
|
||||
ToolMessage::Gradient(GradientToolMessage::UpdateStops { stops }) => {
|
||||
apply_stops_update(&mut self.data, context, responses, Gradient::from(&stops));
|
||||
ToolMessage::Gradient(GradientToolMessage::UpdateRamp { ramp }) => {
|
||||
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) => {
|
||||
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)))
|
||||
.allow_none(false)
|
||||
.narrow(true)
|
||||
.tooltip_label("Gradient Stops")
|
||||
.tooltip_description("Edit the gradient's color stops.")
|
||||
.on_update(|input: &ColorInput| {
|
||||
let stops = input.value.as_gradient().map(|ramp| ramp.stops.clone()).unwrap_or_default();
|
||||
GradientToolMessage::UpdateStops { stops }.into()
|
||||
})
|
||||
.on_commit(|_| DocumentMessage::AddTransaction.into())
|
||||
.widget_instance();
|
||||
let stops_widget = ColorInput::new(FillChoice::Gradient(GradientRamp {
|
||||
spread_method: self.options.spread_method,
|
||||
..GradientRamp::from(&stops_value)
|
||||
}))
|
||||
.allow_none(false)
|
||||
.narrow(true)
|
||||
.tooltip_label("Gradient Stops")
|
||||
.tooltip_description("Edit the gradient's color stops.")
|
||||
.on_update(|input: &ColorInput| {
|
||||
let ramp = input.value.as_gradient().cloned().unwrap_or_default();
|
||||
GradientToolMessage::UpdateRamp { ramp }.into()
|
||||
})
|
||||
.on_commit(|_| DocumentMessage::AddTransaction.into())
|
||||
.widget_instance();
|
||||
|
||||
let reverse_stops = IconButton::new("Reverse", 24)
|
||||
.tooltip_label("Reverse Stops")
|
||||
@@ -295,29 +289,6 @@ impl LayoutHolder for GradientTool {
|
||||
})
|
||||
.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 {
|
||||
"ReverseRadialGradientToRight"
|
||||
} else {
|
||||
@@ -325,7 +296,7 @@ impl LayoutHolder for GradientTool {
|
||||
};
|
||||
let reverse_direction = IconButton::new(reverse_direction_icon, 24)
|
||||
.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)
|
||||
.on_update(|_| {
|
||||
GradientToolMessage::UpdateOptions {
|
||||
@@ -341,8 +312,6 @@ impl LayoutHolder for GradientTool {
|
||||
reverse_stops,
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
gradient_type,
|
||||
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||
spread_method,
|
||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||
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
|
||||
/// 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.
|
||||
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
|
||||
.document
|
||||
.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() {
|
||||
responses.add(GraphOperationMessage::GradientStopsSet { layer, stops: new_gradient.clone() });
|
||||
responses.add(GraphOperationMessage::GradientSpreadMethodSet { layer, spread_method });
|
||||
updated_any_layer = true;
|
||||
} else if let Some((_gradient, appearance, _source)) = resolve_gradient(layer, &context.document.network_interface) {
|
||||
responses.add(GraphOperationMessage::FillGradientSet {
|
||||
layer,
|
||||
gradient: new_gradient.clone(),
|
||||
gradient_type: appearance.gradient_type,
|
||||
spread_method: appearance.spread_method,
|
||||
spread_method,
|
||||
transform: appearance.transform,
|
||||
});
|
||||
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 {
|
||||
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.
|
||||
@@ -2011,6 +1982,7 @@ mod test_gradient {
|
||||
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
|
||||
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_gradient_stops;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::get_upstream_gradient_value_node_id;
|
||||
pub use crate::test_utils::test_prelude::*;
|
||||
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 = document.network_interface.document_network().nodes.get(&fill_node_id)?;
|
||||
|
||||
let stops = match fill_node.input(fill::FillInput)?.as_value()? {
|
||||
TaggedValue::GradientRamp(ramp) => Gradient::from(ramp),
|
||||
let (stops, spread_method) = match fill_node.input(fill::FillInput)?.as_value()? {
|
||||
TaggedValue::GradientRamp(ramp) => (Gradient::from(ramp), ramp.spread_method),
|
||||
_ => 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 local_transform = match fill_node.input(fill::TransformInput).and_then(|input| input.as_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");
|
||||
}
|
||||
|
||||
/// 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]
|
||||
async fn change_spread_method() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
@@ -2619,8 +2594,8 @@ mod test_gradient {
|
||||
|
||||
// Update spread method to Repeat
|
||||
editor
|
||||
.handle_message(GradientToolMessage::UpdateOptions {
|
||||
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Repeat),
|
||||
.handle_message(GradientToolMessage::UpdateRamp {
|
||||
ramp: ramp_with_spread(&gradient.stops, GradientSpreadMethod::Repeat),
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -2629,8 +2604,8 @@ mod test_gradient {
|
||||
|
||||
// Update spread method to Reflect
|
||||
editor
|
||||
.handle_message(GradientToolMessage::UpdateOptions {
|
||||
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Reflect),
|
||||
.handle_message(GradientToolMessage::UpdateRamp {
|
||||
ramp: ramp_with_spread(&gradient.stops, GradientSpreadMethod::Reflect),
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -2652,8 +2627,8 @@ mod test_gradient {
|
||||
|
||||
// Update spread method to Repeat
|
||||
editor
|
||||
.handle_message(GradientToolMessage::UpdateOptions {
|
||||
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Repeat),
|
||||
.handle_message(GradientToolMessage::UpdateRamp {
|
||||
ramp: ramp_with_spread(&gradient.stops, GradientSpreadMethod::Repeat),
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -2662,8 +2637,8 @@ mod test_gradient {
|
||||
|
||||
// Update spread method to Reflect
|
||||
editor
|
||||
.handle_message(GradientToolMessage::UpdateOptions {
|
||||
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Reflect),
|
||||
.handle_message(GradientToolMessage::UpdateRamp {
|
||||
ramp: ramp_with_spread(&gradient.stops, GradientSpreadMethod::Reflect),
|
||||
})
|
||||
.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.
|
||||
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).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
|
||||
.handle_message(GradientToolMessage::UpdateOptions {
|
||||
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Reflect),
|
||||
.handle_message(GradientToolMessage::UpdateRamp {
|
||||
ramp: ramp_with_spread(&stops, GradientSpreadMethod::Reflect),
|
||||
})
|
||||
.await;
|
||||
|
||||
|
||||
@@ -167,7 +167,15 @@ macro_rules! tagged_value {
|
||||
Self::DashPattern(lengths) => Box::new(DashPattern::from(lengths)),
|
||||
Self::BoxCorners(values) => Box::new(BoxCorners::from(values)),
|
||||
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) => {
|
||||
let list: List<BrushStroke> = strokes.into_iter().map(core_types::list::Item::new_from_element).collect();
|
||||
Box::new(list)
|
||||
@@ -213,7 +221,15 @@ macro_rules! tagged_value {
|
||||
Self::DashPattern(lengths) => Arc::new(DashPattern::from(lengths)),
|
||||
Self::BoxCorners(values) => Arc::new(BoxCorners::from(values)),
|
||||
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) => {
|
||||
let list: List<BrushStroke> = strokes.into_iter().map(core_types::list::Item::new_from_element).collect();
|
||||
Arc::new(list)
|
||||
@@ -1086,6 +1102,8 @@ mod paint_default_parsing {
|
||||
|
||||
#[cfg(test)]
|
||||
mod gradient_shape_migration {
|
||||
use graphic_types::vector_types::GradientSpreadMethod;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn load(payload: serde_json::Value) -> TaggedValue {
|
||||
@@ -1104,7 +1122,10 @@ mod gradient_shape_migration {
|
||||
fn modern_ramp_payload_round_trips() {
|
||||
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||
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();
|
||||
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::SRGBA8;
|
||||
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
|
||||
/// like spread method can join as sibling fields opted in from their defaults.
|
||||
/// The serialized exchange form of a gradient: its stops, with whole-ramp settings as sibling fields serialized only when non-default.
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct GradientRamp<C = Color> {
|
||||
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> {
|
||||
@@ -111,13 +114,19 @@ unsafe impl<C: dyn_any::StaticTypeSized> dyn_any::StaticType for GradientRamp<C>
|
||||
|
||||
impl<C> From<GradientStops<C>> for GradientRamp<C> {
|
||||
fn from(stops: GradientStops<C>) -> Self {
|
||||
Self { stops }
|
||||
Self {
|
||||
stops,
|
||||
spread_method: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Gradient> for GradientRamp {
|
||||
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> {
|
||||
fn from(ramp: &GradientRamp) -> Self {
|
||||
Self {
|
||||
@@ -158,19 +188,28 @@ impl From<&GradientStops<SRGBA8>> for GradientRamp {
|
||||
|
||||
impl From<&GradientRamp> for GradientRamp<SRGBA8> {
|
||||
fn from(ramp: &GradientRamp) -> Self {
|
||||
Self { stops: ramp.into() }
|
||||
Self {
|
||||
stops: ramp.into(),
|
||||
spread_method: ramp.spread_method,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Gradient> for GradientRamp<SRGBA8> {
|
||||
fn from(gradient: &Gradient) -> Self {
|
||||
Self { stops: gradient.into() }
|
||||
Self {
|
||||
stops: gradient.into(),
|
||||
spread_method: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&GradientRamp<SRGBA8>> for GradientRamp {
|
||||
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))]
|
||||
#[widget(Radio)]
|
||||
pub enum GradientSpreadMethod {
|
||||
/// Extends the end colors outward.
|
||||
#[default]
|
||||
Pad,
|
||||
/// Loops the gradient by mirroring back-and-forth.
|
||||
Reflect,
|
||||
/// Loops the gradient as copies of itself.
|
||||
Repeat,
|
||||
// TODO: Add a "Clear" variant that returns transparent black outside the gradient's range
|
||||
}
|
||||
@@ -762,6 +804,10 @@ impl GradientSpreadMethod {
|
||||
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
|
||||
@@ -859,6 +905,44 @@ mod tests {
|
||||
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]
|
||||
fn gradient_ui_write_back_elides_default_attributes() {
|
||||
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE, Color::RED]);
|
||||
|
||||
@@ -27,6 +27,8 @@ use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArcle
|
||||
use rand::{Rng, SeedableRng};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
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::subpath::{BezierHandles, ManipulatorGroup};
|
||||
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::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt};
|
||||
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
|
||||
/// 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
|
||||
/// 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_spread = paint.iter_attribute_values::<GradientSpreadMethod>(ATTR_SPREAD_METHOD).is_some();
|
||||
let has_transform = paint.iter_attribute_values::<DAffine2>(ATTR_TRANSFORM).is_some();
|
||||
for index in 0..paint.len() {
|
||||
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 {
|
||||
paint.set_attribute(ATTR_GRADIENT_TYPE, index, gradient_type);
|
||||
}
|
||||
if !has_spread {
|
||||
paint.set_attribute(ATTR_SPREAD_METHOD, index, spread_method);
|
||||
}
|
||||
if !has_transform {
|
||||
let transform = transform.unwrap_or_else(|| {
|
||||
// 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>,
|
||||
#[default(Color::BLACK, Color::WHITE)] _backup_gradient: IList<Gradient>,
|
||||
_gradient_type: GradientType,
|
||||
_spread_method: GradientSpreadMethod,
|
||||
_has_transform: bool,
|
||||
_transform: DAffine2,
|
||||
) -> Result<(Vector, Attr<'e, Fill>), Interrupt> {
|
||||
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)?;
|
||||
Ok((element, Attr(Some(parked))))
|
||||
}
|
||||
@@ -347,7 +342,6 @@ fn fill_graphic_leveled<'e>(
|
||||
_backup_color: IList<Color>,
|
||||
#[default(Color::BLACK, Color::WHITE)] _backup_gradient: IList<Gradient>,
|
||||
_gradient_type: GradientType,
|
||||
_spread_method: GradientSpreadMethod,
|
||||
_has_transform: bool,
|
||||
_transform: DAffine2,
|
||||
) -> Result<(Graphic<'static>, Attr<'e, Fill>), Interrupt> {
|
||||
@@ -356,7 +350,7 @@ fn fill_graphic_leveled<'e>(
|
||||
_ => None,
|
||||
};
|
||||
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)?;
|
||||
Ok((element, Attr(Some(parked))))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user