Add snapping to endpoints and stops in the Gradient tool (#3732)

* snapping

* Cleanup

* fix

* Fix snapping failing sometimes on newly drawn gradient lines

* Code cleanup

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Kulcode 2026-02-24 10:18:28 +05:30 committed by GitHub
parent 691d965bcf
commit 3b91d02fff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 118 additions and 22 deletions

View File

@ -484,6 +484,7 @@ impl OverlayContext {
self.render_context.set_line_width(width); self.render_context.set_line_width(width);
self.render_context.set_stroke_style_str(color); self.render_context.set_stroke_style_str(color);
self.render_context.stroke(); self.render_context.stroke();
self.render_context.set_line_width(1.);
} else { } else {
self.render_context.set_fill_style_str(color); self.render_context.set_fill_style_str(color);
self.render_context.fill(); self.render_context.fill();

View File

@ -307,6 +307,19 @@ pub enum PathSnapSource {
IntersectionPoint, IntersectionPoint,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GradientSnapSource {
Endpoint,
}
impl fmt::Display for GradientSnapSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GradientSnapSource::Endpoint => write!(f, "Gradient: Endpoint"),
}
}
}
impl fmt::Display for PathSnapSource { impl fmt::Display for PathSnapSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
@ -347,6 +360,7 @@ pub enum SnapSource {
Artboard(ArtboardSnapSource), Artboard(ArtboardSnapSource),
Path(PathSnapSource), Path(PathSnapSource),
Alignment(AlignmentSnapSource), Alignment(AlignmentSnapSource),
Gradient(GradientSnapSource),
} }
impl SnapSource { impl SnapSource {
@ -377,6 +391,7 @@ impl fmt::Display for SnapSource {
SnapSource::Artboard(artboard_snap_source) => write!(f, "{artboard_snap_source}"), SnapSource::Artboard(artboard_snap_source) => write!(f, "{artboard_snap_source}"),
SnapSource::Path(path_snap_source) => write!(f, "{path_snap_source}"), SnapSource::Path(path_snap_source) => write!(f, "{path_snap_source}"),
SnapSource::Alignment(alignment_snap_source) => write!(f, "{alignment_snap_source}"), SnapSource::Alignment(alignment_snap_source) => write!(f, "{alignment_snap_source}"),
SnapSource::Gradient(gradient_snap_source) => write!(f, "{gradient_snap_source}"),
} }
} }
} }

View File

@ -251,15 +251,23 @@ impl SnapManager {
pub fn update_indicator(&mut self, snapped_point: SnappedPoint) { pub fn update_indicator(&mut self, snapped_point: SnappedPoint) {
self.indicator = snapped_point.is_snapped().then_some(snapped_point); self.indicator = snapped_point.is_snapped().then_some(snapped_point);
} }
pub fn clear_indicator(&mut self) { pub fn clear_indicator(&mut self) {
self.indicator = None; self.indicator = None;
} }
pub fn preview_draw(&mut self, snap_data: &SnapData, mouse: DVec2) { pub fn preview_draw(&mut self, snap_data: &SnapData, mouse: DVec2) {
let point = SnapCandidatePoint::handle(snap_data.document.metadata().document_to_viewport.inverse().transform_point2(mouse)); let point = SnapCandidatePoint::handle(snap_data.document.metadata().document_to_viewport.inverse().transform_point2(mouse));
let snapped = self.free_snap(snap_data, &point, SnapTypeConfiguration::default()); let snapped = self.free_snap(snap_data, &point, SnapTypeConfiguration::default());
self.update_indicator(snapped); self.update_indicator(snapped);
} }
pub fn preview_draw_gradient(&mut self, snap_data: &SnapData, mouse: DVec2) {
let point = SnapCandidatePoint::gradient_handle(snap_data.document.metadata().document_to_viewport.inverse().transform_point2(mouse));
let snapped = self.free_snap(snap_data, &point, SnapTypeConfiguration::default());
self.update_indicator(snapped);
}
pub fn indicator_pos(&self) -> Option<DVec2> { pub fn indicator_pos(&self) -> Option<DVec2> {
self.indicator.as_ref().map(|point| point.snapped_point_document) self.indicator.as_ref().map(|point| point.snapped_point_document)
} }

View File

@ -453,6 +453,10 @@ impl SnapCandidatePoint {
Self::new_source(document_point, SnapSource::Path(PathSnapSource::AnchorPointWithFreeHandles)) Self::new_source(document_point, SnapSource::Path(PathSnapSource::AnchorPointWithFreeHandles))
} }
pub fn gradient_handle(document_point: DVec2) -> Self {
Self::new_source(document_point, SnapSource::Gradient(GradientSnapSource::Endpoint))
}
pub fn handle_neighbors(document_point: DVec2, neighbors: impl Into<Vec<DVec2>>) -> Self { pub fn handle_neighbors(document_point: DVec2, neighbors: impl Into<Vec<DVec2>>) -> Self {
let mut point = Self::new_source(document_point, SnapSource::Path(PathSnapSource::AnchorPointWithFreeHandles)); let mut point = Self::new_source(document_point, SnapSource::Path(PathSnapSource::AnchorPointWithFreeHandles));
point.neighbors = neighbors.into(); point.neighbors = neighbors.into();

View File

@ -4,7 +4,7 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayContex
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning; 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::{NodeGraphLayer, get_gradient};
use crate::messages::tool::common_functionality::snapping::SnapManager; use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
use graphene_std::vector::style::{Fill, Gradient, GradientStops, GradientType}; use graphene_std::vector::style::{Fill, Gradient, GradientStops, GradientType};
#[derive(Default, ExtractField)] #[derive(Default, ExtractField)]
@ -215,7 +215,17 @@ impl SelectedGradient {
} }
} }
pub fn update_gradient(&mut self, mut mouse: DVec2, responses: &mut VecDeque<Message>, snap_rotate: bool, gradient_type: GradientType, drag_start: DVec2) { #[allow(clippy::too_many_arguments)]
pub fn update_gradient(
&mut self,
mut mouse: DVec2,
responses: &mut VecDeque<Message>,
snap_rotate: bool,
gradient_type: GradientType,
drag_start: DVec2,
snap_data: SnapData,
snap_manager: &mut SnapManager,
) {
if mouse.distance(drag_start) < DRAG_THRESHOLD { if mouse.distance(drag_start) < DRAG_THRESHOLD {
self.gradient = self.initial_gradient.clone(); self.gradient = self.initial_gradient.clone();
self.render_gradient(responses); self.render_gradient(responses);
@ -243,22 +253,60 @@ impl SelectedGradient {
let rotated = DVec2::new(length * angle.cos(), length * angle.sin()); let rotated = DVec2::new(length * angle.cos(), length * angle.sin());
mouse = point - rotated; mouse = point - rotated;
} else {
// Basic point snapping when not angle-constraining
let document_to_viewport = snap_data.document.metadata().document_to_viewport;
let document_mouse = document_to_viewport.inverse().transform_point2(mouse);
let point_candidate = SnapCandidatePoint::gradient_handle(document_mouse);
let snapped = snap_manager.free_snap(&snap_data, &point_candidate, SnapTypeConfiguration::default());
if snapped.is_snapped() {
mouse = document_to_viewport.transform_point2(snapped.snapped_point_document);
}
snap_manager.update_indicator(snapped);
} }
let transformed_mouse = self.transform.inverse().transform_point2(mouse); let transformed_mouse = self.transform.inverse().transform_point2(mouse);
match self.dragging { match self.dragging {
GradientDragTarget::Start => self.gradient.start = transformed_mouse, GradientDragTarget::Start => {
GradientDragTarget::End => self.gradient.end = transformed_mouse, self.gradient.start = transformed_mouse;
}
GradientDragTarget::End => {
self.gradient.end = transformed_mouse;
}
GradientDragTarget::New => { GradientDragTarget::New => {
self.gradient.start = self.transform.inverse().transform_point2(drag_start); self.gradient.start = self.transform.inverse().transform_point2(drag_start);
self.gradient.end = transformed_mouse; self.gradient.end = transformed_mouse;
} }
GradientDragTarget::Step(s) => { GradientDragTarget::Step(s) => {
let (start, end) = (self.transform.transform_point2(self.gradient.start), self.transform.transform_point2(self.gradient.end)); let document_to_viewport = snap_data.document.metadata().document_to_viewport;
let (viewport_start, viewport_end) = (self.transform.transform_point2(self.gradient.start), self.transform.transform_point2(self.gradient.end));
let (document_start, document_end) = (
document_to_viewport.inverse().transform_point2(viewport_start),
document_to_viewport.inverse().transform_point2(viewport_end),
);
let constraint = SnapConstraint::Line {
origin: document_start,
direction: document_end - document_start,
};
let document_mouse = document_to_viewport.inverse().transform_point2(mouse);
let point_candidate = SnapCandidatePoint::gradient_handle(document_mouse);
let snapped = snap_manager.constrained_snap(&snap_data, &point_candidate, constraint, SnapTypeConfiguration::default());
let projected_mouse_document = if snapped.is_snapped() {
snapped.snapped_point_document
} else {
constraint.projection(document_mouse)
};
let projected_mouse = document_to_viewport.transform_point2(projected_mouse_document);
snap_manager.update_indicator(snapped);
// Calculate the new position by finding the closest point on the line // Calculate the new position by finding the closest point on the line
let new_pos = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end); let new_pos = ((viewport_end - viewport_start).angle_to(projected_mouse - viewport_start)).cos() * viewport_start.distance(projected_mouse) / viewport_start.distance(viewport_end);
// Should not go off end but can swap // Should not go off end but can swap
let clamped = new_pos.clamp(0., 1.); let clamped = new_pos.clamp(0., 1.);
@ -379,6 +427,9 @@ impl Fsm for GradientToolFsmState {
} }
} }
let snap_data = SnapData::new(document, input, viewport);
tool_data.snap_manager.draw_overlays(snap_data, &mut overlay_context);
self self
} }
(GradientToolFsmState::Ready { .. }, GradientToolMessage::SelectionChanged) => { (GradientToolFsmState::Ready { .. }, GradientToolMessage::SelectionChanged) => {
@ -483,7 +534,18 @@ impl Fsm for GradientToolFsmState {
self self
} }
(GradientToolFsmState::Ready { .. }, GradientToolMessage::PointerDown) => { (GradientToolFsmState::Ready { .. }, GradientToolMessage::PointerDown) => {
let mouse = input.mouse.position; let document_to_viewport = document.metadata().document_to_viewport;
let mut mouse = input.mouse.position;
let snap_data = SnapData::new(document, input, viewport);
let point = SnapCandidatePoint::gradient_handle(document_to_viewport.inverse().transform_point2(mouse));
let snapped = tool_data.snap_manager.free_snap(&snap_data, &point, SnapTypeConfiguration::default());
if snapped.is_snapped() {
mouse = document_to_viewport.transform_point2(snapped.snapped_point_document);
}
tool_data.drag_start = mouse; tool_data.drag_start = mouse;
let tolerance = (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2); let tolerance = (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2);
@ -528,21 +590,19 @@ impl Fsm for GradientToolFsmState {
let distance = (end - start).angle_to(mouse - start).sin() * (mouse - start).length(); 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); let projection = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end);
if distance.abs() < SEGMENT_INSERTION_DISTANCE if distance.abs() < SEGMENT_INSERTION_DISTANCE && (0. ..=1.).contains(&projection) {
&& (0. ..=1.).contains(&projection)
&& let Some(index) = gradient.clone().insert_stop(mouse, transform)
{
responses.add(DocumentMessage::StartTransaction);
transaction_started = true;
let mut new_gradient = gradient.clone(); let mut new_gradient = gradient.clone();
new_gradient.insert_stop(mouse, transform); if let Some(index) = new_gradient.insert_stop(mouse, transform) {
responses.add(DocumentMessage::StartTransaction);
transaction_started = true;
let mut selected_gradient = SelectedGradient::new(new_gradient, layer, document); let mut selected_gradient = SelectedGradient::new(new_gradient, layer, document);
selected_gradient.dragging = GradientDragTarget::Step(index); selected_gradient.dragging = GradientDragTarget::Step(index);
// No offset when inserting a new stop, it should be exactly under the mouse // No offset when inserting a new stop, it should be exactly under the mouse
selected_gradient.render_gradient(responses); selected_gradient.render_gradient(responses);
tool_data.selected_gradient = Some(selected_gradient); tool_data.selected_gradient = Some(selected_gradient);
dragging = true; dragging = true;
}
} }
} }
} }
@ -550,7 +610,8 @@ impl Fsm for GradientToolFsmState {
let gradient_state = if dragging { let gradient_state = if dragging {
GradientToolFsmState::Drawing GradientToolFsmState::Drawing
} else { } else {
let selected_layer = document.click(input, viewport); let document_mouse = document.metadata().document_to_viewport.inverse().transform_point2(mouse);
let selected_layer = document.click_based_on_position(document_mouse);
// Apply the gradient to the selected layer // Apply the gradient to the selected layer
if let Some(layer) = selected_layer { if let Some(layer) = selected_layer {
@ -592,13 +653,17 @@ impl Fsm for GradientToolFsmState {
} }
(GradientToolFsmState::Drawing, GradientToolMessage::PointerMove { constrain_axis }) => { (GradientToolFsmState::Drawing, GradientToolMessage::PointerMove { constrain_axis }) => {
if let Some(selected_gradient) = &mut tool_data.selected_gradient { if let Some(selected_gradient) = &mut tool_data.selected_gradient {
let mouse = input.mouse.position; // tool_data.snap_manager.snap_position(responses, document, input.mouse.position); let mouse = input.mouse.position;
let snap_data = SnapData::new(document, input, viewport);
selected_gradient.update_gradient( selected_gradient.update_gradient(
mouse, mouse,
responses, responses,
input.keyboard.get(constrain_axis as usize), input.keyboard.get(constrain_axis as usize),
selected_gradient.gradient.gradient_type, selected_gradient.gradient.gradient_type,
tool_data.drag_start, tool_data.drag_start,
snap_data,
&mut tool_data.snap_manager,
); );
} }
@ -662,6 +727,9 @@ impl Fsm for GradientToolFsmState {
} }
} }
let snap_data = SnapData::new(document, input, viewport);
tool_data.snap_manager.preview_draw_gradient(&snap_data, mouse);
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
GradientToolFsmState::Ready { hover_insertion } GradientToolFsmState::Ready { hover_insertion }
} }