mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-23 03:18:12 +08:00
Add support for gradients with midpoints and add draggable diamonds to the color picker dialog (#3813)
* Refactor GradientStops to use struct-of-arrays and include midpoint * Implement interaction and rendering * Make color picker saturation-value color picking snap to original position and show both axis lines Make color picker saturation-value color picking snap to original position and show both axis lines * Add graphite:midpoint attribute to SVG exports * Add graphite:midpoint parsing to SVG importer
This commit is contained in:
@@ -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,18 +193,17 @@ 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::<Vec<_>>();
|
||||
let positions = update_value.get("position").and_then(|x| x.as_array());
|
||||
let midpoints = update_value.get("midpoint").and_then(|x| x.as_array());
|
||||
let colors = update_value.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);
|
||||
|
||||
@@ -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()))
|
||||
|
||||
+116
-10
@@ -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> {
|
||||
@@ -337,7 +337,17 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
let offset_to_center = DVec2::new(size.width() as f64, size.height() as f64) / -2.;
|
||||
let transform = transform * DAffine2::from_translation(offset_to_center);
|
||||
|
||||
import_usvg_node(&mut modify_inputs, &usvg::Node::Group(Box::new(tree.root().clone())), transform, id, parent, insert_index);
|
||||
let graphite_gradient_stops = extract_graphite_gradient_stops(&svg);
|
||||
|
||||
import_usvg_node(
|
||||
&mut modify_inputs,
|
||||
&usvg::Node::Group(Box::new(tree.root().clone())),
|
||||
transform,
|
||||
id,
|
||||
parent,
|
||||
insert_index,
|
||||
&graphite_gradient_stops,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -362,7 +372,85 @@ fn usvg_transform(c: usvg::Transform) -> DAffine2 {
|
||||
DAffine2::from_cols_array(&[c.sx as f64, c.ky as f64, c.kx as f64, c.sy as f64, c.tx as f64, c.ty as f64])
|
||||
}
|
||||
|
||||
fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, transform: DAffine2, id: NodeId, parent: LayerNodeIdentifier, insert_index: usize) {
|
||||
const GRAPHITE_NAMESPACE: &str = "https://graphite.art";
|
||||
|
||||
/// Pre-parses the raw SVG XML to extract gradient stops that have `graphite:midpoint` attributes.
|
||||
/// Graphite exports gradients with midpoint curve data by writing interpolated approximation stops
|
||||
/// alongside the real stops. Real stops are tagged with `graphite:midpoint` attributes.
|
||||
/// Returns a map from gradient element `id` to `GradientStops` containing only the real stops.
|
||||
fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops> {
|
||||
let mut result = HashMap::new();
|
||||
|
||||
// Quick check: if the SVG doesn't reference `graphite:midpoint` at all, skip parsing
|
||||
if !svg.contains("graphite:midpoint") {
|
||||
return result;
|
||||
}
|
||||
|
||||
let doc = match usvg::roxmltree::Document::parse(svg) {
|
||||
Ok(doc) => doc,
|
||||
Err(_) => return result,
|
||||
};
|
||||
|
||||
for node in doc.descendants() {
|
||||
match node.tag_name().name() {
|
||||
"linearGradient" | "radialGradient" => {}
|
||||
_ => continue,
|
||||
}
|
||||
|
||||
let gradient_id = match node.attribute("id") {
|
||||
Some(id) => id.to_string(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let mut real_stops = Vec::new();
|
||||
let mut has_any_midpoint = false;
|
||||
|
||||
for child in node.children() {
|
||||
if child.tag_name().name() != "stop" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let midpoint = child.attribute((GRAPHITE_NAMESPACE, "midpoint")).and_then(|v| v.parse::<f64>().ok());
|
||||
|
||||
if let Some(midpoint) = midpoint {
|
||||
has_any_midpoint = true;
|
||||
|
||||
let offset = child.attribute("offset").and_then(|v| v.parse::<f64>().ok()).unwrap_or(0.);
|
||||
let opacity = child.attribute("stop-opacity").and_then(|v| v.parse::<f32>().ok()).unwrap_or(1.);
|
||||
let color = child.attribute("stop-color").and_then(|hex| parse_hex_stop_color(hex, opacity)).unwrap_or(Color::BLACK);
|
||||
|
||||
real_stops.push(GradientStop { position: offset, midpoint, color });
|
||||
}
|
||||
}
|
||||
|
||||
if has_any_midpoint && !real_stops.is_empty() {
|
||||
result.insert(gradient_id, GradientStops::new(real_stops));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option<Color> {
|
||||
let hex = hex.strip_prefix('#')?;
|
||||
if hex.len() != 6 {
|
||||
return None;
|
||||
}
|
||||
let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.;
|
||||
let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.;
|
||||
let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.;
|
||||
Some(Color::from_rgbaf32_unchecked(r, g, b, opacity))
|
||||
}
|
||||
|
||||
fn import_usvg_node(
|
||||
modify_inputs: &mut ModifyInputsContext,
|
||||
node: &usvg::Node,
|
||||
transform: DAffine2,
|
||||
id: NodeId,
|
||||
parent: LayerNodeIdentifier,
|
||||
insert_index: usize,
|
||||
graphite_gradient_stops: &HashMap<String, GradientStops>,
|
||||
) {
|
||||
let layer = modify_inputs.create_layer(id);
|
||||
modify_inputs.network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
|
||||
modify_inputs.layer_node = Some(layer);
|
||||
@@ -372,7 +460,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
|
||||
match node {
|
||||
usvg::Node::Group(group) => {
|
||||
for child in group.children() {
|
||||
import_usvg_node(modify_inputs, child, transform, NodeId::new(), layer, 0);
|
||||
import_usvg_node(modify_inputs, child, transform, NodeId::new(), layer, 0, graphite_gradient_stops);
|
||||
}
|
||||
modify_inputs.layer_node = Some(layer);
|
||||
}
|
||||
@@ -388,7 +476,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
|
||||
|
||||
if let Some(fill) = path.fill() {
|
||||
let bounds_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
apply_usvg_fill(fill, modify_inputs, bounds_transform);
|
||||
apply_usvg_fill(fill, modify_inputs, bounds_transform, graphite_gradient_stops);
|
||||
}
|
||||
if let Some(stroke) = path.stroke() {
|
||||
apply_usvg_stroke(stroke, modify_inputs, transform * usvg_transform(node.abs_transform()));
|
||||
@@ -432,7 +520,7 @@ fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsCont
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, bounds_transform: DAffine2) {
|
||||
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, bounds_transform: DAffine2, graphite_gradient_stops: &HashMap<String, GradientStops>) {
|
||||
modify_inputs.fill_set(match &fill.paint() {
|
||||
usvg::Paint::Color(color) => Fill::solid(usvg_color(*color, fill.opacity().get())),
|
||||
usvg::Paint::LinearGradient(linear) => {
|
||||
@@ -443,8 +531,17 @@ 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 = GradientStops::new(stops);
|
||||
let stops = match graphite_gradient_stops.get(linear.id()) {
|
||||
Some(graphite_stops) => graphite_stops.clone(),
|
||||
None => {
|
||||
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()),
|
||||
});
|
||||
GradientStops::new(stops)
|
||||
}
|
||||
};
|
||||
|
||||
Fill::Gradient(Gradient { start, end, gradient_type, stops })
|
||||
}
|
||||
@@ -457,8 +554,17 @@ 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 = GradientStops::new(stops);
|
||||
let stops = match graphite_gradient_stops.get(radial.id()) {
|
||||
Some(graphite_stops) => graphite_stops.clone(),
|
||||
None => {
|
||||
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()),
|
||||
});
|
||||
GradientStops::new(stops)
|
||||
}
|
||||
};
|
||||
|
||||
Fill::Gradient(Gradient { start, end, gradient_type, stops })
|
||||
}
|
||||
|
||||
@@ -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<f64> {
|
||||
fn calculate_insertion(start: DVec2, end: DVec2, stops: &GradientStops, mouse: DVec2) -> Option<f64> {
|
||||
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::<Vec<_>>();
|
||||
let stops = gradient.stops.iter().map(|stop| (stop.position, stop.color.to_rgba8_srgb())).collect::<Vec<_>>();
|
||||
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<f64> = updated_gradient.stops.iter().map(|(pos, _)| *pos).collect();
|
||||
let positions: Vec<f64> = 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<f64> = stops.iter().map(|(pos, _)| *pos).collect();
|
||||
let positions: Vec<f64> = 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<f64> = updated_stops.iter().map(|(pos, _)| *pos).collect();
|
||||
let updated_positions: Vec<f64> = 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<f64> = updated_gradient.stops.iter().map(|(pos, _)| *pos).collect();
|
||||
let positions: Vec<f64> = 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<f64> = final_gradient.stops.iter().map(|(pos, _)| *pos).collect();
|
||||
let final_positions: Vec<f64> = 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);
|
||||
|
||||
Reference in New Issue
Block a user