mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-23 02:08:12 +08:00
Add Table<GradientStops> gradient rendering (#3989)
* Add Table<GradientStops> gradient rendering * Add SVG and Vello renderers for Table<GradientStops> * Add thumbnail rendering for Table<GradientStops> * Use row transform to map (0,0), (1,0) unit line to document space * Set 100px width for the initially created gradient * Add support of table gradients for the gradient tool * Fix after review * Thumbnail rendering of artboard with infinite gradient layer * Hide radial gradient's reverse direction button for gradient table * Remove unused imports * Format * Fix conflict with spread method * Code review * Fix thumbnails * Connect up gradient_type and spread_method to attributes --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
8cadafa063
commit
e686ee9f42
@@ -16,7 +16,7 @@ use graphene_std::table::Table;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::misc::ManipulatorPointId;
|
||||
use graphene_std::vector::style::{Fill, Gradient};
|
||||
use graphene_std::vector::{PointId, SegmentId, VectorModificationType};
|
||||
use graphene_std::vector::{GradientStops, PointId, SegmentId, VectorModificationType};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Returns the ID of the first Spline node in the horizontal flow which is not followed by a `Path` node, or `None` if none exists.
|
||||
@@ -280,6 +280,15 @@ pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI
|
||||
Some(gradient.clone())
|
||||
}
|
||||
|
||||
/// Get the gradient table of a layer.
|
||||
pub fn get_gradient_table(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Table<GradientStops>> {
|
||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER))?;
|
||||
let TaggedValue::GradientTable(gradient_table) = inputs.get(graphene_std::math_nodes::gradient_value::GradientInput::INDEX)?.as_value()? else {
|
||||
return None;
|
||||
};
|
||||
Some(gradient_table.clone())
|
||||
}
|
||||
|
||||
/// Get the current fill of a layer from the closest "Fill" node.
|
||||
pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Color> {
|
||||
let fill_index = 1;
|
||||
|
||||
@@ -3,13 +3,17 @@ use crate::consts::{
|
||||
COLOR_OVERLAY_BLUE, DRAG_THRESHOLD, GRADIENT_MIDPOINT_DIAMOND_RADIUS, GRADIENT_MIDPOINT_MAX, GRADIENT_MIDPOINT_MIN, GRADIENT_STOP_MIN_VIEWPORT_GAP, LINE_ROTATE_SNAP_ANGLE,
|
||||
MANIPULATOR_GROUP_MARKER_SIZE, SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD,
|
||||
};
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::{GizmoEmphasis, OverlayContext};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||
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::graph_modification_utils::{self, NodeGraphLayer, get_gradient_table, is_layer_fed_by_node_of_name};
|
||||
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
|
||||
use graphene_std::raster::color::Color;
|
||||
use graphene_std::table::{Table, TableRow};
|
||||
use graphene_std::vector::style::{Fill, Gradient, GradientSpreadMethod, GradientStops, GradientType};
|
||||
use graphene_std::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD, ATTR_TRANSFORM};
|
||||
|
||||
#[derive(Default, ExtractField)]
|
||||
pub struct GradientTool {
|
||||
@@ -131,7 +135,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
|
||||
}
|
||||
|
||||
// Sync tool options with the selected layer's gradient
|
||||
if has_gradient && let Some(gradient) = get_gradient_on_selected_layer(&context.document) {
|
||||
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;
|
||||
|
||||
@@ -161,6 +165,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
|
||||
|
||||
impl LayoutHolder for GradientTool {
|
||||
fn layout(&self) -> Layout {
|
||||
let mut widgets: Vec<WidgetInstance> = Vec::new();
|
||||
|
||||
let gradient_type = RadioInput::new(vec![
|
||||
RadioEntryData::new("Linear").label("Linear").tooltip_label("Linear Gradient").on_update(move |_| {
|
||||
GradientToolMessage::UpdateOptions {
|
||||
@@ -178,6 +184,8 @@ impl LayoutHolder for GradientTool {
|
||||
.selected_index(Some((self.options.gradient_type == GradientType::Radial) as u32))
|
||||
.widget_instance();
|
||||
|
||||
widgets.extend([gradient_type, Separator::new(SeparatorStyle::Unrelated).widget_instance()]);
|
||||
|
||||
let reverse_stops = IconButton::new("Reverse", 24)
|
||||
.tooltip_label("Reverse Stops")
|
||||
.tooltip_description("Reverse the gradient color stops.")
|
||||
@@ -191,19 +199,19 @@ impl LayoutHolder for GradientTool {
|
||||
.widget_instance();
|
||||
|
||||
let spread_method = RadioInput::new(vec![
|
||||
RadioEntryData::new("Pad").label("Pad").tooltip_label("Pad").on_update(move |_| {
|
||||
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").on_update(move |_| {
|
||||
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").on_update(move |_| {
|
||||
RadioEntryData::new("Repeat").label("Repeat").tooltip_label("Repeat Spread Method").on_update(move |_| {
|
||||
GradientToolMessage::UpdateOptions {
|
||||
options: GradientOptionsUpdate::SetSpreadMethod(GradientSpreadMethod::Repeat),
|
||||
}
|
||||
@@ -213,13 +221,7 @@ impl LayoutHolder for GradientTool {
|
||||
.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,
|
||||
];
|
||||
widgets.extend([spread_method, Separator::new(SeparatorStyle::Unrelated).widget_instance(), reverse_stops]);
|
||||
|
||||
if self.options.gradient_type == GradientType::Radial {
|
||||
let orientation = self
|
||||
@@ -273,14 +275,63 @@ impl Default for GradientToolFsmState {
|
||||
|
||||
/// Computes the transform from gradient space to viewport space (where gradient space is 0..1)
|
||||
fn gradient_space_transform(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> DAffine2 {
|
||||
let bounds = document.metadata().nonzero_bounding_box(layer);
|
||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
// TODO: Drop the `is_gradient_table` branch once all gradients are `Table<GradientStops>`
|
||||
let is_gradient_table = is_layer_fed_by_node_of_name(
|
||||
layer,
|
||||
&document.network_interface,
|
||||
&DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER),
|
||||
);
|
||||
|
||||
if is_gradient_table {
|
||||
// Table<GradientStops> layers use the item's transform from gradient space to document space,
|
||||
// so we cannot use `transform_to_viewport` here as it would apply the transform twice.
|
||||
return document
|
||||
.metadata()
|
||||
.upstream_footprints
|
||||
.get(&layer.to_node())
|
||||
.map(|footprint| footprint.transform)
|
||||
.unwrap_or(document.metadata().document_to_viewport);
|
||||
}
|
||||
|
||||
let multiplied = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
let bounds = document.metadata().nonzero_bounding_box(layer);
|
||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
multiplied * bound_transform
|
||||
}
|
||||
|
||||
/// Builds the item transform that maps the unit gradient line (the +X unit vector in local space) to
|
||||
/// the segment from `start` to `end` in document space. The perpendicular column is forced to the same magnitude
|
||||
/// as the `start`..`end` direction so the matrix stays invertible (linear gradients ignore the perpendicular axis,
|
||||
/// but click detection uses the full inverse).
|
||||
// TODO: Apply a separate scale on the perpendicular axis when we support elliptical gradients
|
||||
fn gradient_item_transform(start: DVec2, end: DVec2) -> DAffine2 {
|
||||
let delta = end - start;
|
||||
let perp = DVec2::new(-delta.y, delta.x);
|
||||
DAffine2::from_cols_array(&[delta.x, delta.y, perp.x, perp.y, start.x, start.y])
|
||||
}
|
||||
|
||||
// TODO: Remove this whole function once all gradients are `Table<GradientStops>`
|
||||
fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
|
||||
match (get_gradient_table(layer, network_interface), graph_modification_utils::get_gradient(layer, network_interface)) {
|
||||
(Some(gradient_graphic), _) => {
|
||||
let stops = gradient_graphic.element(0)?.clone();
|
||||
let transform: DAffine2 = gradient_graphic.attribute_cloned_or_default(ATTR_TRANSFORM, 0);
|
||||
let spread_method: GradientSpreadMethod = gradient_graphic.attribute_cloned_or_default(ATTR_SPREAD_METHOD, 0);
|
||||
let gradient_type: GradientType = gradient_graphic.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, 0);
|
||||
let gradient = Gradient {
|
||||
stops,
|
||||
gradient_type,
|
||||
spread_method,
|
||||
start: transform.transform_point2(DVec2::ZERO),
|
||||
end: transform.transform_point2(DVec2::X),
|
||||
};
|
||||
Some(gradient)
|
||||
}
|
||||
(None, Some(gradient)) => Some(gradient),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether two adjacent stops are too closely packed in viewport space for a midpoint diamond to be shown or interacted with.
|
||||
fn midpoint_hidden_by_proximity(left_stop_pos: f64, right_stop_pos: f64, viewport_line_length: f64) -> bool {
|
||||
(right_stop_pos - left_stop_pos) * viewport_line_length < GRADIENT_STOP_MIN_VIEWPORT_GAP * 2.
|
||||
@@ -304,6 +355,8 @@ struct SelectedGradient {
|
||||
gradient: Gradient,
|
||||
dragging: GradientDragTarget,
|
||||
initial_gradient: Gradient,
|
||||
// TODO: Remove (and the matching branches in `render_gradient` / pointer-up) once `Table<GradientStops>` replaces legacy `Fill::Gradient`
|
||||
is_gradient_table: bool,
|
||||
}
|
||||
|
||||
fn calculate_insertion(start: DVec2, end: DVec2, stops: &GradientStops, mouse: DVec2) -> Option<f64> {
|
||||
@@ -347,12 +400,14 @@ fn calculate_insertion(start: DVec2, end: DVec2, stops: &GradientStops, mouse: D
|
||||
impl SelectedGradient {
|
||||
pub fn new(gradient: Gradient, layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Self {
|
||||
let transform = gradient_space_transform(layer, document);
|
||||
let is_gradient_table = get_gradient_table(layer, &document.network_interface).is_some();
|
||||
Self {
|
||||
layer: Some(layer),
|
||||
transform,
|
||||
gradient: gradient.clone(),
|
||||
dragging: GradientDragTarget::End,
|
||||
initial_gradient: gradient,
|
||||
is_gradient_table,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,10 +623,21 @@ impl SelectedGradient {
|
||||
/// Update the layer fill to the current gradient
|
||||
pub fn render_gradient(&mut self, responses: &mut VecDeque<Message>) {
|
||||
if let Some(layer) = self.layer {
|
||||
responses.add(GraphOperationMessage::FillSet {
|
||||
layer,
|
||||
fill: Fill::Gradient(self.gradient.clone()),
|
||||
});
|
||||
// TODO: Drop the `Fill::Gradient` branch when all gradients become `Table<GradientStops>`
|
||||
if self.is_gradient_table {
|
||||
let gradient_table = Table::new_from_row(
|
||||
TableRow::new_from_element(self.gradient.stops.clone())
|
||||
.with_attribute(ATTR_TRANSFORM, gradient_item_transform(self.gradient.start, self.gradient.end))
|
||||
.with_attribute(ATTR_SPREAD_METHOD, self.gradient.spread_method)
|
||||
.with_attribute(ATTR_GRADIENT_TYPE, self.gradient.gradient_type),
|
||||
);
|
||||
responses.add(GraphOperationMessage::GradientTableSet { layer, gradient_table });
|
||||
} else {
|
||||
responses.add(GraphOperationMessage::FillSet {
|
||||
layer,
|
||||
fill: Fill::Gradient(self.gradient.clone()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -952,8 +1018,11 @@ impl Fsm for GradientToolFsmState {
|
||||
};
|
||||
|
||||
// The gradient has only one point and so should become a fill
|
||||
// TODO: Drop the legacy `Fill::Solid` branch when all gradients become `Table<GradientStops>`
|
||||
if selected_gradient.gradient.stops.len() == 1 {
|
||||
if let Some(layer) = selected_gradient.layer {
|
||||
if selected_gradient.is_gradient_table {
|
||||
selected_gradient.render_gradient(responses);
|
||||
} else if let Some(layer) = selected_gradient.layer {
|
||||
responses.add(GraphOperationMessage::FillSet {
|
||||
layer,
|
||||
fill: Fill::Solid(selected_gradient.gradient.stops.color[0]),
|
||||
@@ -1047,6 +1116,7 @@ impl Fsm for GradientToolFsmState {
|
||||
for layer in document.network_interface.selected_nodes().selected_visible_layers(&document.network_interface) {
|
||||
let Some(gradient) = get_gradient(layer, &document.network_interface) else { continue };
|
||||
let transform = gradient_space_transform(layer, document);
|
||||
let is_gradient_table = get_gradient_table(layer, &document.network_interface).is_some();
|
||||
|
||||
// Check for dragging a midpoint diamond
|
||||
if drag_hint.is_none() {
|
||||
@@ -1074,6 +1144,7 @@ impl Fsm for GradientToolFsmState {
|
||||
gradient: gradient.clone(),
|
||||
dragging: GradientDragTarget::Midpoint(i),
|
||||
initial_gradient: gradient.clone(),
|
||||
is_gradient_table,
|
||||
});
|
||||
|
||||
break;
|
||||
@@ -1114,6 +1185,7 @@ impl Fsm for GradientToolFsmState {
|
||||
gradient: gradient.clone(),
|
||||
dragging: drag_target,
|
||||
initial_gradient: gradient.clone(),
|
||||
is_gradient_table,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1130,6 +1202,7 @@ impl Fsm for GradientToolFsmState {
|
||||
gradient: gradient.clone(),
|
||||
dragging: dragging_target,
|
||||
initial_gradient: gradient.clone(),
|
||||
is_gradient_table,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1538,10 +1611,24 @@ fn apply_gradient_update(
|
||||
transaction_started = true;
|
||||
}
|
||||
update(&mut gradient);
|
||||
responses.add(GraphOperationMessage::FillSet {
|
||||
layer,
|
||||
fill: Fill::Gradient(gradient),
|
||||
});
|
||||
|
||||
// Only check for the gradient table once we know we'll write back, since this is a graph traversal per layer
|
||||
// TODO: Drop the `Fill::Gradient` branch when all gradients become `Table<GradientStops>`
|
||||
if get_gradient_table(layer, &context.document.network_interface).is_some() {
|
||||
// Rebuild the item transform from the (possibly mutated) start/end so updates like `ReverseDirection` that only swap endpoints are reflected in the stored attribute
|
||||
let gradient_table = Table::new_from_row(
|
||||
TableRow::new_from_element(gradient.stops.clone())
|
||||
.with_attribute(ATTR_TRANSFORM, gradient_item_transform(gradient.start, gradient.end))
|
||||
.with_attribute(ATTR_SPREAD_METHOD, gradient.spread_method)
|
||||
.with_attribute(ATTR_GRADIENT_TYPE, gradient.gradient_type),
|
||||
);
|
||||
responses.add(GraphOperationMessage::GradientTableSet { layer, gradient_table });
|
||||
} else {
|
||||
responses.add(GraphOperationMessage::FillSet {
|
||||
layer,
|
||||
fill: Fill::Gradient(gradient),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1623,11 +1710,14 @@ mod test_gradient {
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::ScrollDelta;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector};
|
||||
pub use crate::test_utils::test_prelude::*;
|
||||
use glam::DAffine2;
|
||||
use graphene_std::vector::fill;
|
||||
use graphene_std::vector::style::Fill;
|
||||
use graphene_std::vector::style::Gradient;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graphene_std::ATTR_TRANSFORM;
|
||||
use graphene_std::table::{Table, TableRow};
|
||||
use graphene_std::vector::style::{Fill, Gradient};
|
||||
use graphene_std::vector::{GradientStop, GradientStops, fill};
|
||||
|
||||
use super::gradient_space_transform;
|
||||
|
||||
@@ -1672,6 +1762,45 @@ mod test_gradient {
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_gradient_table_layer(editor: &mut EditorTestUtils) -> LayerNodeIdentifier {
|
||||
editor.drag_tool(ToolType::Rectangle, 0., 0., 100., 100., ModifierKeys::empty()).await;
|
||||
let document = editor.active_document();
|
||||
let layer = document.metadata().all_layers().next().unwrap();
|
||||
|
||||
let gradient_node_id = editor.create_node_by_name(DefinitionIdentifier::ProtoNode(graphene_std::math_nodes::gradient_value::IDENTIFIER)).await;
|
||||
|
||||
editor
|
||||
.handle_message(NodeGraphMessage::CreateWire {
|
||||
output_connector: OutputConnector::node(gradient_node_id, 0),
|
||||
input_connector: InputConnector::node(layer.to_node(), 1),
|
||||
})
|
||||
.await;
|
||||
|
||||
editor
|
||||
.handle_message(NodeGraphMessage::SetInputValue {
|
||||
node_id: gradient_node_id,
|
||||
input_index: 1,
|
||||
value: TaggedValue::GradientTable(Table::new_from_row(
|
||||
TableRow::new_from_element(GradientStops::new([
|
||||
GradientStop {
|
||||
position: 0.,
|
||||
midpoint: 0.5,
|
||||
color: Color::RED,
|
||||
},
|
||||
GradientStop {
|
||||
position: 1.,
|
||||
midpoint: 0.5,
|
||||
color: Color::BLUE,
|
||||
},
|
||||
]))
|
||||
.with_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY),
|
||||
)),
|
||||
})
|
||||
.await;
|
||||
|
||||
layer
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ignore_artboard() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
@@ -2037,4 +2166,146 @@ mod test_gradient {
|
||||
let (gradient, _) = get_gradient(&mut editor).await;
|
||||
assert_eq!(gradient.spread_method, GradientSpreadMethod::Reflect);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gradient_table_drag_endpoint() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
let layer = create_gradient_table_layer(&mut editor).await;
|
||||
|
||||
// Create original transform for the control geometry and apply it
|
||||
let initial_start = DVec2::new(10., 50.);
|
||||
let initial_end = DVec2::new(200., 50.);
|
||||
let initial_item_transform = super::gradient_item_transform(initial_start, initial_end);
|
||||
editor
|
||||
.handle_message(GraphOperationMessage::GradientTableSet {
|
||||
layer,
|
||||
gradient_table: Table::new_from_row(
|
||||
TableRow::new_from_element(GradientStops::new([
|
||||
GradientStop {
|
||||
position: 0.,
|
||||
midpoint: 0.5,
|
||||
color: Color::RED,
|
||||
},
|
||||
GradientStop {
|
||||
position: 1.,
|
||||
midpoint: 0.5,
|
||||
color: Color::BLUE,
|
||||
},
|
||||
]))
|
||||
.with_attribute(ATTR_TRANSFORM, initial_item_transform),
|
||||
),
|
||||
})
|
||||
.await;
|
||||
|
||||
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
|
||||
|
||||
let document = editor.active_document();
|
||||
let space_transform = gradient_space_transform(layer, document);
|
||||
let gradient = super::get_gradient(layer, &document.network_interface).unwrap();
|
||||
let viewport_start = space_transform.transform_point2(gradient.start);
|
||||
let viewport_end = space_transform.transform_point2(gradient.end);
|
||||
|
||||
// Drag target of the end point, move 80px down
|
||||
let new_viewport_end = viewport_end + DVec2::new(0., 80.);
|
||||
editor.select_tool(ToolType::Gradient).await;
|
||||
editor.move_mouse(viewport_end.x, viewport_end.y, ModifierKeys::empty(), MouseKeys::empty()).await;
|
||||
editor.left_mousedown(viewport_end.x, viewport_end.y, ModifierKeys::empty()).await;
|
||||
editor.move_mouse(new_viewport_end.x, new_viewport_end.y, ModifierKeys::empty(), MouseKeys::LEFT).await;
|
||||
editor
|
||||
.mouseup(
|
||||
EditorMouseState {
|
||||
editor_position: new_viewport_end,
|
||||
mouse_keys: MouseKeys::empty(),
|
||||
scroll_delta: ScrollDelta::default(),
|
||||
},
|
||||
ModifierKeys::empty(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Verify if the gradient position is updated correctly
|
||||
let document = editor.active_document();
|
||||
let updated = super::get_gradient(layer, &document.network_interface).expect("Gradient should exist after drag");
|
||||
let updated_space_transform = gradient_space_transform(layer, document);
|
||||
let updated_viewport_start = updated_space_transform.transform_point2(updated.start);
|
||||
let updated_viewport_end = updated_space_transform.transform_point2(updated.end);
|
||||
|
||||
assert!(
|
||||
updated_viewport_start.abs_diff_eq(viewport_start, 1.),
|
||||
"Start should not move. Expected {viewport_start:?}, got {updated_viewport_start:?}"
|
||||
);
|
||||
assert!(
|
||||
updated_viewport_end.abs_diff_eq(new_viewport_end, 1.),
|
||||
"End should move to new position. Expected {new_viewport_end:?}, got {updated_viewport_end:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gradient_table_preserves_stops() {
|
||||
let mut editor = EditorTestUtils::create();
|
||||
editor.new_document().await;
|
||||
let layer = create_gradient_table_layer(&mut editor).await;
|
||||
|
||||
// Set up a 3-stop gradient with distinct colors
|
||||
let original_stops = GradientStops::new([
|
||||
GradientStop {
|
||||
position: 0.,
|
||||
midpoint: 0.5,
|
||||
color: Color::RED,
|
||||
},
|
||||
GradientStop {
|
||||
position: 0.5,
|
||||
midpoint: 0.5,
|
||||
color: Color::GREEN,
|
||||
},
|
||||
GradientStop {
|
||||
position: 1.,
|
||||
midpoint: 0.5,
|
||||
color: Color::BLUE,
|
||||
},
|
||||
]);
|
||||
let initial_start = DVec2::new(10., 50.);
|
||||
let initial_end = DVec2::new(200., 50.);
|
||||
let initial_item_transform = super::gradient_item_transform(initial_start, initial_end);
|
||||
editor
|
||||
.handle_message(GraphOperationMessage::GradientTableSet {
|
||||
layer,
|
||||
gradient_table: Table::new_from_row(TableRow::new_from_element(original_stops.clone()).with_attribute(ATTR_TRANSFORM, initial_item_transform)),
|
||||
})
|
||||
.await;
|
||||
|
||||
editor.handle_message(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }).await;
|
||||
|
||||
let document = editor.active_document();
|
||||
let space_transform = gradient_space_transform(layer, document);
|
||||
let gradient = super::get_gradient(layer, &document.network_interface).unwrap();
|
||||
let viewport_end = space_transform.transform_point2(gradient.end);
|
||||
|
||||
// Drag the end point 80px down
|
||||
let new_viewport_end = viewport_end + DVec2::new(0., 80.);
|
||||
editor.select_tool(ToolType::Gradient).await;
|
||||
editor.move_mouse(viewport_end.x, viewport_end.y, ModifierKeys::empty(), MouseKeys::empty()).await;
|
||||
editor.left_mousedown(viewport_end.x, viewport_end.y, ModifierKeys::empty()).await;
|
||||
editor.move_mouse(new_viewport_end.x, new_viewport_end.y, ModifierKeys::empty(), MouseKeys::LEFT).await;
|
||||
editor
|
||||
.mouseup(
|
||||
EditorMouseState {
|
||||
editor_position: new_viewport_end,
|
||||
mouse_keys: MouseKeys::empty(),
|
||||
scroll_delta: ScrollDelta::default(),
|
||||
},
|
||||
ModifierKeys::empty(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Verify stops are preserved after dragging
|
||||
let document = editor.active_document();
|
||||
let updated = super::get_gradient(layer, &document.network_interface).expect("Gradient should exist after drag");
|
||||
|
||||
assert_eq!(updated.stops.len(), 3, "Stop count should be preserved");
|
||||
assert_stops_at_positions(&updated.stops.position, &[0., 0.5, 1.], 1e-10);
|
||||
assert_eq!(updated.stops.color[0].to_rgba8_srgb(), Color::RED.to_rgba8_srgb(), "First stop color should be preserved");
|
||||
assert_eq!(updated.stops.color[1].to_rgba8_srgb(), Color::GREEN.to_rgba8_srgb(), "Middle stop color should be preserved");
|
||||
assert_eq!(updated.stops.color[2].to_rgba8_srgb(), Color::BLUE.to_rgba8_srgb(), "Last stop color should be preserved");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user