From e3ea5fff8aca3966cb75ac78f178241e8a1153d7 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 23 Feb 2026 00:45:58 -0800 Subject: [PATCH 1/5] Refactor GradientStops to use struct-of-arrays and include midpoint --- .../messages/layout/layout_message_handler.rs | 34 +-- .../data_panel/data_panel_message_handler.rs | 2 +- .../graph_operation_message_handler.rs | 14 +- .../tool/tool_messages/gradient_tool.rs | 61 ++-- .../floating-menus/ColorPicker.svelte | 7 +- .../widgets/inputs/ColorInput.svelte | 2 +- .../widgets/inputs/SpectrumInput.svelte | 69 +++-- frontend/src/messages.ts | 55 ++-- .../libraries/rendering/src/render_ext.rs | 12 +- .../libraries/rendering/src/renderer.rs | 16 +- .../libraries/vector-types/src/gradient.rs | 261 +++++++++++++----- node-graph/libraries/vector-types/src/lib.rs | 2 +- .../vector-types/src/vector/style.rs | 14 +- node-graph/nodes/gstd/src/lib.rs | 2 +- node-graph/nodes/raster/src/adjust.rs | 2 +- node-graph/nodes/raster/src/blending_nodes.rs | 21 +- 16 files changed, 359 insertions(+), 215 deletions(-) diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index 49a63f3d11..1af925908b 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -2,7 +2,7 @@ use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; use graphene_std::raster::color::Color; -use graphene_std::vector::style::{FillChoice, GradientStops}; +use graphene_std::vector::style::{FillChoice, GradientStop, GradientStops}; use serde_json::Value; use std::collections::HashMap; @@ -193,21 +193,23 @@ impl LayoutMessageHandler { } // Gradient - let gradient = update_value.get("stops").and_then(|x| x.as_array()); - if let Some(stops) = gradient { - let gradient_stops = stops - .iter() - .filter_map(|stop| { - stop.as_object().and_then(|stop| { - let position = stop.get("position").and_then(|x| x.as_f64()); - let color = stop.get("color").and_then(|x| x.as_object()).and_then(decode_color); - if let (Some(position), Some(color)) = (position, color) { Some((position, color)) } else { None } - }) - }) - .collect::>(); - - color_button.value = FillChoice::Gradient(GradientStops::new(gradient_stops)); - return (color_button.on_update.callback)(color_button); + let gradient = update_value.get("stops").and_then(|x| x.as_object()); + if let Some(stops_obj) = gradient { + let positions = stops_obj.get("position").and_then(|x| x.as_array()); + let midpoints = stops_obj.get("midpoint").and_then(|x| x.as_array()); + let colors = stops_obj.get("color").and_then(|x| x.as_array()); + + if let (Some(positions), Some(midpoints), Some(colors)) = (positions, midpoints, colors) { + let gradient_stops = positions.iter().zip(midpoints.iter()).zip(colors.iter()).filter_map(|((pos, mid), col)| { + let position = pos.as_f64()?; + let midpoint = mid.as_f64()?; + let color = col.as_object().and_then(decode_color)?; + Some(GradientStop { position, midpoint, color }) + }); + + color_button.value = FillChoice::Gradient(GradientStops::new(gradient_stops)); + return (color_button.on_update.callback)(color_button); + } } warn!("ColorInput update was not able to be parsed with color data: {color_button:?}"); diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index 3e5e91e9d9..36a38e8658 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -564,7 +564,7 @@ impl TableRowLayout for GradientStops { "Gradient" } fn identifier(&self) -> String { - format!("Gradient ({} stops)", self.0.len()) + format!("Gradient ({} stops)", self.len()) } fn element_widget(&self, _index: usize) -> WidgetInstance { ColorInput::new(FillChoice::Gradient(self.clone())) 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 631bc1250b..cc6eb5ec98 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 @@ -14,7 +14,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, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; +use graphene_std::vector::style::{Fill, Gradient, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; #[derive(ExtractField)] pub struct GraphOperationMessageContext<'a> { @@ -443,7 +443,11 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b let gradient_type = GradientType::Linear; - let stops = linear.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect(); + let stops = linear.stops().iter().map(|stop| GradientStop { + position: stop.offset().get() as f64, + midpoint: 0.5, + color: usvg_color(stop.color(), stop.opacity().get()), + }); let stops = GradientStops::new(stops); Fill::Gradient(Gradient { start, end, gradient_type, stops }) @@ -457,7 +461,11 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b let gradient_type = GradientType::Radial; - let stops = radial.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect(); + let stops = radial.stops().iter().map(|stop| GradientStop { + position: stop.offset().get() as f64, + midpoint: 0.5, + color: usvg_color(stop.color(), stop.opacity().get()), + }); let stops = GradientStops::new(stops); Fill::Gradient(Gradient { start, end, gradient_type, stops }) diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index fd18cc1125..9969256b6f 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -5,7 +5,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye 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::SnapManager; -use graphene_std::vector::style::{Fill, Gradient, GradientType}; +use graphene_std::vector::style::{Fill, Gradient, GradientStops, GradientType}; #[derive(Default, ExtractField)] pub struct GradientTool { @@ -182,13 +182,13 @@ struct SelectedGradient { initial_gradient: Gradient, } -fn calculate_insertion(start: DVec2, end: DVec2, stops: &[(f64, graphene_std::Color)], mouse: DVec2) -> Option { +fn calculate_insertion(start: DVec2, end: DVec2, stops: &GradientStops, mouse: DVec2) -> Option { let distance = (end - start).angle_to(mouse - start).sin() * (mouse - start).length(); let projection = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end); if distance.abs() < SEGMENT_INSERTION_DISTANCE && (0. ..=1.).contains(&projection) { - for (position, _) in stops { - let stop_pos = start.lerp(end, *position); + for stop in stops { + let stop_pos = start.lerp(end, stop.position); if stop_pos.distance_squared(mouse) < (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2) { return None; } @@ -262,11 +262,12 @@ impl SelectedGradient { // Should not go off end but can swap let clamped = new_pos.clamp(0., 1.); - self.gradient.stops.get_mut(s).unwrap().0 = clamped; - let new_pos = self.gradient.stops[s]; + self.gradient.stops.position[s] = clamped; + let new_position = self.gradient.stops.position[s]; + let new_color = self.gradient.stops.color[s]; self.gradient.stops.sort(); - self.dragging = GradientDragTarget::Step(self.gradient.stops.iter().position(|x| *x == new_pos).unwrap()); + self.dragging = GradientDragTarget::Step(self.gradient.stops.iter().position(|s| s.position == new_position && s.color == new_color).unwrap()); } } self.render_gradient(responses); @@ -357,18 +358,18 @@ impl Fsm for GradientToolFsmState { format!("#{}", color.with_alpha(1.).to_rgba_hex_srgb()) } - let start_hex = stops.first().map(|(_, c)| color_to_hex(*c)).unwrap_or(String::from(COLOR_OVERLAY_BLUE)); - let end_hex = stops.last().map(|(_, c)| color_to_hex(*c)).unwrap_or(String::from(COLOR_OVERLAY_BLUE)); + let start_hex = stops.color.first().map(|&c| color_to_hex(c)).unwrap_or(String::from(COLOR_OVERLAY_BLUE)); + let end_hex = stops.color.last().map(|&c| color_to_hex(c)).unwrap_or(String::from(COLOR_OVERLAY_BLUE)); overlay_context.line(start, end, None, None); overlay_context.gradient_color_stop(start, dragging == Some(GradientDragTarget::Start), &start_hex); overlay_context.gradient_color_stop(end, dragging == Some(GradientDragTarget::End), &end_hex); - for (index, (position, color)) in stops.clone().into_iter().enumerate() { - if position.abs() < f64::EPSILON * 1000. || (1. - position).abs() < f64::EPSILON * 1000. { + for (index, stop) in stops.iter().enumerate() { + if stop.position.abs() < f64::EPSILON * 1000. || (1. - stop.position).abs() < f64::EPSILON * 1000. { continue; } - overlay_context.gradient_color_stop(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index)), &color_to_hex(color)); + overlay_context.gradient_color_stop(start.lerp(end, stop.position), dragging == Some(GradientDragTarget::Step(index)), &color_to_hex(stop.color)); } if let (Some(projection), Some(dir)) = (calculate_insertion(start, end, stops, mouse), (end - start).try_normalize()) { @@ -415,7 +416,7 @@ impl Fsm for GradientToolFsmState { if let Some(layer) = selected_gradient.layer { responses.add(GraphOperationMessage::FillSet { layer, - fill: Fill::Solid(selected_gradient.gradient.stops[0].1), + fill: Fill::Solid(selected_gradient.gradient.stops.color[0]), }); } responses.add(DocumentMessage::CommitTransaction); @@ -424,8 +425,8 @@ impl Fsm for GradientToolFsmState { } // Find the minimum and maximum positions - let min_position = selected_gradient.gradient.stops.iter().map(|(pos, _)| *pos).reduce(f64::min).expect("No min"); - let max_position = selected_gradient.gradient.stops.iter().map(|(pos, _)| *pos).reduce(f64::max).expect("No max"); + let min_position = selected_gradient.gradient.stops.position.iter().copied().reduce(f64::min).expect("No min"); + let max_position = selected_gradient.gradient.stops.position.iter().copied().reduce(f64::max).expect("No max"); // Recompute the start and end position of the gradient (in viewport transform) let transform = selected_gradient.transform; @@ -435,7 +436,7 @@ impl Fsm for GradientToolFsmState { selected_gradient.gradient.end = transform.inverse().transform_point2(new_end); // Remap the positions - for (position, _) in selected_gradient.gradient.stops.iter_mut() { + for position in selected_gradient.gradient.stops.position.iter_mut() { *position = (*position - min_position) / (max_position - min_position); } @@ -492,8 +493,8 @@ impl Fsm for GradientToolFsmState { let Some(gradient) = get_gradient(layer, &document.network_interface) else { continue }; let transform = gradient_space_transform(layer, document); // Check for dragging step - for (index, (pos, _)) in gradient.stops.iter().enumerate() { - let pos = transform.transform_point2(gradient.start.lerp(gradient.end, *pos)); + for (index, stop) in gradient.stops.iter().enumerate() { + let pos = transform.transform_point2(gradient.start.lerp(gradient.end, stop.position)); if pos.distance_squared(mouse) < tolerance { dragging = true; tool_data.selected_gradient = Some(SelectedGradient { @@ -787,7 +788,7 @@ mod test_gradient { let (gradient, transform) = get_gradient(&mut editor).await; // Gradient goes from secondary color to primary color - let stops = gradient.stops.iter().map(|stop| (stop.0, stop.1.to_rgba8_srgb())).collect::>(); + let stops = gradient.stops.iter().map(|stop| (stop.position, stop.color.to_rgba8_srgb())).collect::>(); assert_eq!(stops, vec![(0., Color::BLUE.to_rgba8_srgb()), (1., Color::GREEN.to_rgba8_srgb())]); assert!(transform.transform_point2(gradient.start).abs_diff_eq(DVec2::new(2., 3.), 1e-10)); assert!(transform.transform_point2(gradient.end).abs_diff_eq(DVec2::new(24., 4.), 1e-10)); @@ -879,7 +880,7 @@ mod test_gradient { let (updated_gradient, _) = get_gradient(&mut editor).await; assert_eq!(updated_gradient.stops.len(), 3, "Expected 3 stops, found {}", updated_gradient.stops.len()); - let positions: Vec = updated_gradient.stops.iter().map(|(pos, _)| *pos).collect(); + let positions: Vec = updated_gradient.stops.iter().map(|stop| stop.position).collect(); assert!( positions.iter().any(|pos| (pos - 0.5).abs() < 0.1), "Expected to find a stop near position 0.5, but found: {positions:?}" @@ -975,12 +976,12 @@ mod test_gradient { // Verify initial stop positions and colors let mut stops = initial_gradient.stops.clone(); - stops.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + stops.sort(); - let positions: Vec = stops.iter().map(|(pos, _)| *pos).collect(); + let positions: Vec = stops.iter().map(|stop| stop.position).collect(); assert_stops_at_positions(&positions, &[0., 0.5, 1.], 0.1); - let middle_color = stops[1].1.to_rgba8_srgb(); + let middle_color = stops.color[1].to_rgba8_srgb(); // Simulate dragging the middle stop to position 0.8 let click_position = DVec2::new(50., 0.); @@ -1014,16 +1015,16 @@ mod test_gradient { // Verify updated stop positions and colors let mut updated_stops = updated_gradient.stops.clone(); - updated_stops.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + updated_stops.sort(); // Check positions are now correctly ordered - let updated_positions: Vec = updated_stops.iter().map(|(pos, _)| *pos).collect(); + let updated_positions: Vec = updated_stops.iter().map(|stop| stop.position).collect(); assert_stops_at_positions(&updated_positions, &[0., 0.8, 1.], 0.1); // Colors should maintain their associations with the stop points - assert_eq!(updated_stops[0].1.to_rgba8_srgb(), Color::BLUE.to_rgba8_srgb()); - assert_eq!(updated_stops[1].1.to_rgba8_srgb(), middle_color); - assert_eq!(updated_stops[2].1.to_rgba8_srgb(), Color::GREEN.to_rgba8_srgb()); + assert_eq!(updated_stops.color[0].to_rgba8_srgb(), Color::BLUE.to_rgba8_srgb()); + assert_eq!(updated_stops.color[1].to_rgba8_srgb(), middle_color); + assert_eq!(updated_stops.color[2].to_rgba8_srgb(), Color::GREEN.to_rgba8_srgb()); } #[tokio::test] @@ -1054,7 +1055,7 @@ mod test_gradient { let (updated_gradient, _) = get_gradient(&mut editor).await; assert_eq!(updated_gradient.stops.len(), 4, "Expected 4 stops, found {}", updated_gradient.stops.len()); - let positions: Vec = updated_gradient.stops.iter().map(|(pos, _)| *pos).collect(); + let positions: Vec = updated_gradient.stops.iter().map(|stop| stop.position).collect(); // Use helper function to verify positions assert_stops_at_positions(&positions, &[0., 0.25, 0.75, 1.], 0.05); @@ -1080,7 +1081,7 @@ mod test_gradient { let (final_gradient, _) = get_gradient(&mut editor).await; assert_eq!(final_gradient.stops.len(), 3, "Expected 3 stops after deletion, found {}", final_gradient.stops.len()); - let final_positions: Vec = final_gradient.stops.iter().map(|(pos, _)| *pos).collect(); + let final_positions: Vec = final_gradient.stops.iter().map(|stop| stop.position).collect(); // Verify final positions with helper function assert_stops_at_positions(&final_positions, &[0., 0.25, 1.], 0.05); diff --git a/frontend/src/components/floating-menus/ColorPicker.svelte b/frontend/src/components/floating-menus/ColorPicker.svelte index 217e2edd2e..b836414e0e 100644 --- a/frontend/src/components/floating-menus/ColorPicker.svelte +++ b/frontend/src/components/floating-menus/ColorPicker.svelte @@ -57,7 +57,7 @@ // Gradient color stops $: gradient = colorOrGradient instanceof Gradient ? colorOrGradient : undefined; let activeIndex = 0 as number | undefined; - $: selectedGradientColor = (activeIndex !== undefined && gradient?.atIndex(activeIndex)?.color) || (Color.fromCSS("black") as Color); + $: selectedGradientColor = (activeIndex !== undefined && gradient?.stops.color[activeIndex]) || (Color.fromCSS("black") as Color); // Currently viewed color $: color = colorOrGradient instanceof Color ? colorOrGradient : selectedGradientColor; // New color components @@ -286,8 +286,9 @@ function setColor(color?: Color) { const colorToEmit = color || new Color({ h: hue, s: saturation, v: value, a: alpha }); - const stop = gradientSpectrumInputWidget && activeIndex !== undefined && gradient?.atIndex(activeIndex); - if (stop) stop.color = colorToEmit; + if (gradientSpectrumInputWidget && activeIndex !== undefined && gradient?.stops.position[activeIndex] !== undefined && colorOrGradient instanceof Gradient) { + colorOrGradient.stops.color[activeIndex] = colorToEmit; + } dispatch("colorOrGradient", gradient || colorToEmit); } diff --git a/frontend/src/components/widgets/inputs/ColorInput.svelte b/frontend/src/components/widgets/inputs/ColorInput.svelte index 51fcd37bfe..c130696846 100644 --- a/frontend/src/components/widgets/inputs/ColorInput.svelte +++ b/frontend/src/components/widgets/inputs/ColorInput.svelte @@ -28,7 +28,7 @@ $: outlined = outlineFactor > 0.0001; $: chosenGradient = value instanceof Gradient ? value.toLinearGradientCSS() : `linear-gradient(${value.toHexOptionalAlpha()}, ${value.toHexOptionalAlpha()})`; $: none = value instanceof Color ? value.none : false; - $: transparency = value instanceof Gradient ? value.stops.some((stop) => stop.color.alpha < 1) : value.alpha < 1; + $: transparency = value instanceof Gradient ? value.stops.color.some((color) => color.alpha < 1) : value.alpha < 1; diff --git a/frontend/src/components/widgets/inputs/SpectrumInput.svelte b/frontend/src/components/widgets/inputs/SpectrumInput.svelte index 26a3a38810..70c3fdc11c 100644 --- a/frontend/src/components/widgets/inputs/SpectrumInput.svelte +++ b/frontend/src/components/widgets/inputs/SpectrumInput.svelte @@ -1,7 +1,8 @@ diff --git a/frontend/src/components/widgets/inputs/SpectrumInput.svelte b/frontend/src/components/widgets/inputs/SpectrumInput.svelte index 70c3fdc11c..0b39d7e564 100644 --- a/frontend/src/components/widgets/inputs/SpectrumInput.svelte +++ b/frontend/src/components/widgets/inputs/SpectrumInput.svelte @@ -1,6 +1,12 @@ + +