diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 03fc166dd4..ddf7d6cbdc 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -15,7 +15,7 @@ use graphene_std::renderer::Quad; use graphene_std::renderer::convert_usvg_path::convert_usvg_path; use graphene_std::table::Table; use graphene_std::text::{Font, TypesettingConfig}; -use graphene_std::vector::style::{Fill, Gradient, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; +use graphene_std::vector::style::{Fill, Gradient, GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; #[derive(ExtractField)] pub struct GraphOperationMessageContext<'a> { @@ -687,6 +687,14 @@ fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsCont } } +fn convert_spread_method(spread_method: usvg::SpreadMethod) -> GradientSpreadMethod { + match spread_method { + usvg::SpreadMethod::Pad => GradientSpreadMethod::Pad, + usvg::SpreadMethod::Reflect => GradientSpreadMethod::Reflect, + usvg::SpreadMethod::Repeat => GradientSpreadMethod::Repeat, + } +} + fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, bounds_transform: DAffine2, graphite_gradient_stops: &HashMap) { modify_inputs.fill_set(match &fill.paint() { usvg::Paint::Color(color) => Fill::solid(usvg_color(*color, fill.opacity().get())), @@ -709,8 +717,15 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b GradientStops::new(stops) } }; - - Fill::Gradient(Gradient { start, end, gradient_type, stops }) + let spread_method = convert_spread_method(linear.spread_method()); + + Fill::Gradient(Gradient { + start, + end, + gradient_type, + stops, + spread_method, + }) } usvg::Paint::RadialGradient(radial) => { let gradient_transform = usvg_transform(radial.transform()); @@ -732,8 +747,15 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b GradientStops::new(stops) } }; - - Fill::Gradient(Gradient { start, end, gradient_type, stops }) + let spread_method = convert_spread_method(radial.spread_method()); + + Fill::Gradient(Gradient { + start, + end, + gradient_type, + stops, + spread_method, + }) } usvg::Paint::Pattern(_) => { warn!("SVG patterns are not currently supported"); diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 5df11baa3e..6c92afae7a 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -27,7 +27,7 @@ use graphene_std::transform::{Footprint, ReferencePoint, Transform}; use graphene_std::vector::QRCodeErrorCorrectionLevel; use graphene_std::vector::misc::BooleanOperation; use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType}; -use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; +use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientSpreadMethod, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; pub(crate) fn string_properties(text: &str) -> Vec { let widget = TextLabel::new(text).widget_instance(); @@ -2004,6 +2004,55 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte ]); widgets.push(LayoutGroup::row(row)); + + let mut spread_methods_row: Vec = vec![TextLabel::new("").widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance()]; + + let spread_method_entries = [GradientSpreadMethod::Pad, GradientSpreadMethod::Reflect, GradientSpreadMethod::Repeat] + .iter() + .map(|&spread_method| { + let gradient_for_input = gradient_for_closure.clone(); + let gradient_for_backup = gradient_for_closure.clone(); + + let set_input_value = update_value( + move |_: &()| { + let mut new_gradient = gradient_for_input.clone(); + new_gradient.spread_method = spread_method; + TaggedValue::Fill(Fill::Gradient(new_gradient)) + }, + node_id, + FillInput::::INDEX, + ); + + let set_backup_value = update_value( + move |_: &()| { + let mut new_gradient = gradient_for_backup.clone(); + new_gradient.spread_method = spread_method; + TaggedValue::Gradient(new_gradient) + }, + node_id, + BackupGradientInput::INDEX, + ); + + RadioEntryData::new(format!("{:?}", spread_method)) + .label(format!("{:?}", spread_method)) + .on_update(move |_| Message::Batched { + messages: Box::new([ + set_input_value(&()), + set_backup_value(&()), + GradientToolMessage::UpdateOptions { + options: GradientOptionsUpdate::SpreadMethod(spread_method), + } + .into(), + ]), + }) + .on_commit(commit_value) + }) + .collect(); + + add_blank_assist(&mut spread_methods_row); + spread_methods_row.extend_from_slice(&[RadioInput::new(spread_method_entries).selected_index(Some(gradient.spread_method as u32)).widget_instance()]); + + widgets.push(LayoutGroup::row(spread_methods_row)); } widgets diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index f1823618e5..042c729b8e 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -9,7 +9,7 @@ use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_gradient}; use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration}; use graphene_std::raster::color::Color; -use graphene_std::vector::style::{Fill, Gradient, GradientStops, GradientType}; +use graphene_std::vector::style::{Fill, Gradient, GradientSpreadMethod, GradientStops, GradientType}; #[derive(Default, ExtractField)] pub struct GradientTool { @@ -21,6 +21,7 @@ pub struct GradientTool { #[derive(Default)] pub struct GradientOptions { gradient_type: GradientType, + spread_method: GradientSpreadMethod, } #[impl_message(Message, ToolMessage, Gradient)] @@ -53,6 +54,7 @@ pub enum GradientOptionsUpdate { Type(GradientType), ReverseStops, ReverseDirection, + SpreadMethod(GradientSpreadMethod), } impl ToolMetadata for GradientTool { @@ -84,6 +86,10 @@ impl<'a> MessageHandler> for Grad GradientOptionsUpdate::ReverseDirection => { apply_gradient_update(&mut self.data, context, responses, |_| true, |g| std::mem::swap(&mut g.start, &mut g.end)); } + GradientOptionsUpdate::SpreadMethod(spread_method) => { + self.options.spread_method = spread_method; + apply_gradient_update(&mut self.data, context, responses, |g| g.spread_method != spread_method, |g| g.spread_method = spread_method); + } }, ToolMessage::Gradient(GradientToolMessage::StartTransactionForColorStop) => { if self.data.color_picker_transaction_open { @@ -123,6 +129,22 @@ impl<'a> MessageHandler> for Grad self.data.has_selected_gradient = has_gradient; responses.add(ToolMessage::RefreshToolOptions); } + + // Sync tool options with the selected layer's gradient + if has_gradient && let Some(gradient) = get_gradient_on_selected_layer(&context.document) { + let type_differs = self.options.gradient_type != gradient.gradient_type; + let spread_method_differs = self.options.spread_method != gradient.spread_method; + + if type_differs { + self.options.gradient_type = gradient.gradient_type; + } + if spread_method_differs { + self.options.spread_method = gradient.spread_method; + } + if type_differs || spread_method_differs { + responses.add(ToolMessage::RefreshToolOptions); + } + }; } } } @@ -168,7 +190,36 @@ impl LayoutHolder for GradientTool { }) .widget_instance(); - let mut widgets = vec![gradient_type, Separator::new(SeparatorStyle::Unrelated).widget_instance(), reverse_stops]; + let spread_method = RadioInput::new(vec![ + RadioEntryData::new("Pad").label("Pad").tooltip_label("Pad").on_update(move |_| { + GradientToolMessage::UpdateOptions { + options: GradientOptionsUpdate::SpreadMethod(GradientSpreadMethod::Pad), + } + .into() + }), + RadioEntryData::new("Reflect").label("Reflect").tooltip_label("Reflect").on_update(move |_| { + GradientToolMessage::UpdateOptions { + options: GradientOptionsUpdate::SpreadMethod(GradientSpreadMethod::Reflect), + } + .into() + }), + RadioEntryData::new("Repeat").label("Repeat").tooltip_label("Repeat").on_update(move |_| { + GradientToolMessage::UpdateOptions { + options: GradientOptionsUpdate::SpreadMethod(GradientSpreadMethod::Repeat), + } + .into() + }), + ]) + .selected_index(Some(self.options.spread_method as u32)) + .widget_instance(); + + let mut widgets = vec![ + gradient_type, + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + spread_method, + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + reverse_stops, + ]; if self.options.gradient_type == GradientType::Radial { let orientation = self @@ -1149,7 +1200,14 @@ impl Fsm for GradientToolFsmState { gradient.clone() } else { // Generate a new gradient - Gradient::new(DVec2::ZERO, global_tool_data.secondary_color, DVec2::ONE, global_tool_data.primary_color, tool_options.gradient_type) + Gradient::new( + DVec2::ZERO, + global_tool_data.secondary_color, + DVec2::ONE, + global_tool_data.primary_color, + tool_options.gradient_type, + tool_options.spread_method, + ) }; let mut selected_gradient = SelectedGradient::new(gradient, layer, document); selected_gradient.dragging = GradientDragTarget::New; @@ -1501,12 +1559,16 @@ fn apply_gradient_update( responses.add(ToolMessage::RefreshToolOptions); } -fn has_gradient_on_selected_layers(document: &DocumentMessageHandler) -> bool { +fn get_gradient_on_selected_layer(document: &DocumentMessageHandler) -> Option { document .network_interface .selected_nodes() .selected_visible_layers(&document.network_interface) - .any(|layer| get_gradient(layer, &document.network_interface).is_some()) + .find_map(|layer| get_gradient(layer, &document.network_interface)) +} + +fn has_gradient_on_selected_layers(document: &DocumentMessageHandler) -> bool { + get_gradient_on_selected_layer(document).is_some() } #[inline(always)] @@ -1941,4 +2003,38 @@ mod test_gradient { // Additional verification that 0.75 stop is gone assert!(!final_positions.iter().any(|pos| (pos - 0.75).abs() < 0.05), "Stop at position 0.75 should have been deleted"); } + + #[tokio::test] + async fn change_spread_method() { + use graphene_std::vector::style::GradientSpreadMethod; + + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await; + editor.drag_tool(ToolType::Gradient, 10., 10., 90., 90., ModifierKeys::empty()).await; + + // Verify default spread method is Pad + let (gradient, _) = get_gradient(&mut editor).await; + assert_eq!(gradient.spread_method, GradientSpreadMethod::Pad); + + // Update spread method to Repeat + editor + .handle_message(GradientToolMessage::UpdateOptions { + options: GradientOptionsUpdate::SpreadMethod(GradientSpreadMethod::Repeat), + }) + .await; + + let (gradient, _) = get_gradient(&mut editor).await; + assert_eq!(gradient.spread_method, GradientSpreadMethod::Repeat); + + // Update spread method to Reflect + editor + .handle_message(GradientToolMessage::UpdateOptions { + options: GradientOptionsUpdate::SpreadMethod(GradientSpreadMethod::Reflect), + }) + .await; + + let (gradient, _) = get_gradient(&mut editor).await; + assert_eq!(gradient.spread_method, GradientSpreadMethod::Reflect); + } } diff --git a/node-graph/libraries/rendering/src/render_ext.rs b/node-graph/libraries/rendering/src/render_ext.rs index f455f719b1..471e5932ff 100644 --- a/node-graph/libraries/rendering/src/render_ext.rs +++ b/node-graph/libraries/rendering/src/render_ext.rs @@ -47,13 +47,16 @@ impl RenderExt for Gradient { format!(r#" gradientTransform="{gradient_transform}""#) }; + let spread_method = self.spread_method.svg_name(); + let spread_method = if spread_method.is_empty() { String::new() } else { format!(r#" spreadMethod="{spread_method}""#) }; + let gradient_id = generate_uuid(); match self.gradient_type { GradientType::Linear => { let _ = write!( svg_defs, - r#"{}"#, + r#"{}"#, gradient_id, start.x, start.y, end.x, end.y, stop ); } @@ -61,7 +64,7 @@ impl RenderExt for Gradient { let radius = (f64::powi(start.x - end.x, 2) + f64::powi(start.y - end.y, 2)).sqrt(); let _ = write!( svg_defs, - r#"{}"#, + r#"{}"#, gradient_id, start.x, start.y, radius, stop ); } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 7869cf6880..4c0a0cf2fc 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -25,6 +25,7 @@ use std::fmt::Write; use std::hash::{Hash, Hasher}; use std::ops::Deref; use std::sync::{Arc, LazyLock}; +use vector_types::gradient::GradientSpreadMethod; use vello::*; #[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)] @@ -1077,6 +1078,11 @@ impl Render for Table { .into() } }, + extend: match gradient.spread_method { + GradientSpreadMethod::Pad => peniko::Extend::Pad, + GradientSpreadMethod::Reflect => peniko::Extend::Reflect, + GradientSpreadMethod::Repeat => peniko::Extend::Repeat, + }, stops, interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied, ..Default::default() diff --git a/node-graph/libraries/vector-types/src/gradient.rs b/node-graph/libraries/vector-types/src/gradient.rs index 653ca6bc5d..cc2592210c 100644 --- a/node-graph/libraries/vector-types/src/gradient.rs +++ b/node-graph/libraries/vector-types/src/gradient.rs @@ -334,6 +334,27 @@ impl GradientStops { } } +#[repr(C)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, node_macro::ChoiceType)] +#[widget(Radio)] +pub enum GradientSpreadMethod { + #[default] + Pad, + Reflect, + Repeat, +} + +impl GradientSpreadMethod { + pub fn svg_name(&self) -> &'static str { + match self { + GradientSpreadMethod::Pad => "", + GradientSpreadMethod::Reflect => "reflect", + GradientSpreadMethod::Repeat => "repeat", + } + } +} + /// A gradient fill. /// /// Contains the start and end points, along with the colors at varying points along the length. @@ -345,6 +366,8 @@ pub struct Gradient { pub gradient_type: GradientType, pub start: DVec2, pub end: DVec2, + #[serde(default)] + pub spread_method: GradientSpreadMethod, } impl Default for Gradient { @@ -354,6 +377,7 @@ impl Default for Gradient { gradient_type: GradientType::Linear, start: DVec2::new(0., 0.5), end: DVec2::new(1., 0.5), + spread_method: GradientSpreadMethod::Pad, } } } @@ -369,6 +393,7 @@ impl std::hash::Hash for Gradient { .for_each(|x| x.to_bits().hash(state)); self.stops.color.iter().for_each(|color| color.hash(state)); self.gradient_type.hash(state); + self.spread_method.hash(state); } } @@ -387,7 +412,7 @@ impl std::fmt::Display for Gradient { impl Gradient { /// Constructs a new gradient with the colors at 0 and 1 specified. - pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, gradient_type: GradientType) -> Self { + pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, gradient_type: GradientType, spread_method: GradientSpreadMethod) -> Self { let stops = GradientStops::new([ GradientStop { position: 0., @@ -401,7 +426,13 @@ impl Gradient { }, ]); - Self { start, end, stops, gradient_type } + Self { + start, + end, + stops, + gradient_type, + spread_method, + } } pub fn lerp(&self, other: &Self, time: f64) -> Self { @@ -414,8 +445,15 @@ impl Gradient { }); let stops = GradientStops::new(stops); let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type }; + let spread_method = if time < 0.5 { self.spread_method } else { other.spread_method }; - Self { start, end, stops, gradient_type } + Self { + start, + end, + stops, + gradient_type, + spread_method, + } } /// Insert a stop into the gradient, the index if successful