From d22dc898d9d05229a200c2fcef2d060f4d9b7147 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sat, 7 Feb 2026 07:09:50 +0000 Subject: [PATCH 1/5] snapping --- .../portfolio/document/utility_types/misc.rs | 15 +++++++++++++++ .../tool/common_functionality/snapping.rs | 5 +++++ .../snapping/layer_snapper.rs | 4 ++++ .../tool/tool_messages/gradient_tool.rs | 18 ++++++++++++++++-- 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index c474bf9665..2394dc73b0 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -307,6 +307,19 @@ pub enum PathSnapSource { 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 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -347,6 +360,7 @@ pub enum SnapSource { Artboard(ArtboardSnapSource), Path(PathSnapSource), Alignment(AlignmentSnapSource), + Gradient(GradientSnapSource), } impl SnapSource { @@ -377,6 +391,7 @@ impl fmt::Display for SnapSource { SnapSource::Artboard(artboard_snap_source) => write!(f, "{artboard_snap_source}"), SnapSource::Path(path_snap_source) => write!(f, "{path_snap_source}"), SnapSource::Alignment(alignment_snap_source) => write!(f, "{alignment_snap_source}"), + SnapSource::Gradient(gradient_snap_source) => write!(f, "{gradient_snap_source}"), } } } diff --git a/editor/src/messages/tool/common_functionality/snapping.rs b/editor/src/messages/tool/common_functionality/snapping.rs index 9b53cce626..6089e4dad9 100644 --- a/editor/src/messages/tool/common_functionality/snapping.rs +++ b/editor/src/messages/tool/common_functionality/snapping.rs @@ -259,6 +259,11 @@ impl SnapManager { let snapped = self.free_snap(snap_data, &point, SnapTypeConfiguration::default()); self.update_indicator(snapped); } + pub fn gradient_preview_draw(&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 { self.indicator.as_ref().map(|point| point.snapped_point_document) diff --git a/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs b/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs index 9919855716..18a190ac20 100644 --- a/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs +++ b/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs @@ -453,6 +453,10 @@ impl SnapCandidatePoint { 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>) -> Self { let mut point = Self::new_source(document_point, SnapSource::Path(PathSnapSource::AnchorPointWithFreeHandles)); point.neighbors = neighbors.into(); diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 9969256b6f..125c396d2d 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -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::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 crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapData, SnapManager, SnapTypeConfiguration}; use graphene_std::vector::style::{Fill, Gradient, GradientStops, GradientType}; #[derive(Default, ExtractField)] @@ -379,6 +379,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 } (GradientToolFsmState::Ready { .. }, GradientToolMessage::SelectionChanged) => { @@ -592,7 +595,15 @@ impl Fsm for GradientToolFsmState { } (GradientToolFsmState::Drawing, GradientToolMessage::PointerMove { constrain_axis }) => { 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 mut mouse = input.mouse.position; + let snap_data = SnapData::new(document, input, viewport); + let point = SnapCandidatePoint::gradient_handle(document.metadata().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.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document); + } + tool_data.snap_manager.update_indicator(snapped); + selected_gradient.update_gradient( mouse, responses, @@ -662,6 +673,9 @@ impl Fsm for GradientToolFsmState { } } + let snap_data = SnapData::new(document, input, viewport); + tool_data.snap_manager.gradient_preview_draw(&snap_data, mouse); + responses.add(OverlaysMessage::Draw); GradientToolFsmState::Ready { hover_insertion } } From 037621668579356ef4617ffe7156c3cc2a1fea5d Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sun, 22 Feb 2026 08:31:31 +0000 Subject: [PATCH 2/5] Cleanup --- .../tool/tool_messages/gradient_tool.rs | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 125c396d2d..03d7632268 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -531,21 +531,19 @@ impl Fsm for GradientToolFsmState { 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) - && let Some(index) = gradient.clone().insert_stop(mouse, transform) - { - responses.add(DocumentMessage::StartTransaction); - transaction_started = true; + if distance.abs() < SEGMENT_INSERTION_DISTANCE && (0. ..=1.).contains(&projection) { let mut new_gradient = gradient.clone(); - new_gradient.insert_stop(mouse, transform); - - let mut selected_gradient = SelectedGradient::new(new_gradient, layer, document); - selected_gradient.dragging = GradientDragTarget::Step(index); - // No offset when inserting a new stop, it should be exactly under the mouse - selected_gradient.render_gradient(responses); - tool_data.selected_gradient = Some(selected_gradient); - dragging = true; + 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); + selected_gradient.dragging = GradientDragTarget::Step(index); + // No offset when inserting a new stop, it should be exactly under the mouse + selected_gradient.render_gradient(responses); + tool_data.selected_gradient = Some(selected_gradient); + dragging = true; + } } } } From 4ff43994c388ccd0505fc8288c3935cada148793 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sun, 22 Feb 2026 10:26:21 +0000 Subject: [PATCH 3/5] fix --- .../document/overlays/utility_types_web.rs | 2 + .../tool/tool_messages/gradient_tool.rs | 76 +++++++++++++++---- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_web.rs b/editor/src/messages/portfolio/document/overlays/utility_types_web.rs index 4fbaa5878d..86c9a5b244 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_web.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_web.rs @@ -496,6 +496,8 @@ impl OverlayContext { // Stroke (outer) draw_circle(radius, Some(stroke_width), COLOR_OVERLAY_WHITE); + self.render_context.set_line_width(1.); + self.end_dpi_aware_transform(); } diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 03d7632268..564a7b08f3 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -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::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, SnapData, SnapManager, SnapTypeConfiguration}; +use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration}; use graphene_std::vector::style::{Fill, Gradient, GradientStops, GradientType}; #[derive(Default, ExtractField)] @@ -215,7 +215,16 @@ impl SelectedGradient { } } - pub fn update_gradient(&mut self, mut mouse: DVec2, responses: &mut VecDeque, snap_rotate: bool, gradient_type: GradientType, drag_start: DVec2) { + pub fn update_gradient( + &mut self, + mut mouse: DVec2, + responses: &mut VecDeque, + snap_rotate: bool, + gradient_type: GradientType, + drag_start: DVec2, + snap_data: SnapData, + snap_manager: &mut SnapManager, + ) { if mouse.distance(drag_start) < DRAG_THRESHOLD { self.gradient = self.initial_gradient.clone(); self.render_gradient(responses); @@ -243,22 +252,59 @@ impl SelectedGradient { let rotated = DVec2::new(length * angle.cos(), length * angle.sin()); 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); match self.dragging { - GradientDragTarget::Start => self.gradient.start = transformed_mouse, - GradientDragTarget::End => self.gradient.end = transformed_mouse, + GradientDragTarget::Start => { + self.gradient.start = transformed_mouse; + } + GradientDragTarget::End => { + self.gradient.end = transformed_mouse; + } GradientDragTarget::New => { self.gradient.start = self.transform.inverse().transform_point2(drag_start); self.gradient.end = transformed_mouse; } 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 - 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 let clamped = new_pos.clamp(0., 1.); @@ -486,7 +532,13 @@ impl Fsm for GradientToolFsmState { self } (GradientToolFsmState::Ready { .. }, GradientToolMessage::PointerDown) => { - let mouse = input.mouse.position; + let mut mouse = input.mouse.position; + let snap_data = SnapData::new(document, input, viewport); + let point = SnapCandidatePoint::gradient_handle(document.metadata().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.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document); + } tool_data.drag_start = mouse; let tolerance = (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2); @@ -593,14 +645,8 @@ impl Fsm for GradientToolFsmState { } (GradientToolFsmState::Drawing, GradientToolMessage::PointerMove { constrain_axis }) => { if let Some(selected_gradient) = &mut tool_data.selected_gradient { - let mut mouse = input.mouse.position; + let mouse = input.mouse.position; let snap_data = SnapData::new(document, input, viewport); - let point = SnapCandidatePoint::gradient_handle(document.metadata().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.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document); - } - tool_data.snap_manager.update_indicator(snapped); selected_gradient.update_gradient( mouse, @@ -608,6 +654,8 @@ impl Fsm for GradientToolFsmState { input.keyboard.get(constrain_axis as usize), selected_gradient.gradient.gradient_type, tool_data.drag_start, + snap_data, + &mut tool_data.snap_manager, ); } From 093c67084dde5203bc381040004bee0161b38ca7 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 23 Feb 2026 20:11:31 -0800 Subject: [PATCH 4/5] Fix snapping failing sometimes on newly drawn gradient lines --- editor/src/messages/tool/tool_messages/gradient_tool.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 564a7b08f3..0f5fbf8dc3 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -603,7 +603,8 @@ impl Fsm for GradientToolFsmState { let gradient_state = if dragging { GradientToolFsmState::Drawing } 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 if let Some(layer) = selected_layer { From 5b68d5eec4f0ef61614cc472b55fb2b9de921137 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 23 Feb 2026 20:37:17 -0800 Subject: [PATCH 5/5] Code cleanup --- .../document/overlays/utility_types_web.rs | 3 +-- .../messages/tool/common_functionality/snapping.rs | 5 ++++- .../messages/tool/tool_messages/gradient_tool.rs | 13 ++++++++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_web.rs b/editor/src/messages/portfolio/document/overlays/utility_types_web.rs index 86c9a5b244..a80166f787 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_web.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_web.rs @@ -484,6 +484,7 @@ impl OverlayContext { self.render_context.set_line_width(width); self.render_context.set_stroke_style_str(color); self.render_context.stroke(); + self.render_context.set_line_width(1.); } else { self.render_context.set_fill_style_str(color); self.render_context.fill(); @@ -496,8 +497,6 @@ impl OverlayContext { // Stroke (outer) draw_circle(radius, Some(stroke_width), COLOR_OVERLAY_WHITE); - self.render_context.set_line_width(1.); - self.end_dpi_aware_transform(); } diff --git a/editor/src/messages/tool/common_functionality/snapping.rs b/editor/src/messages/tool/common_functionality/snapping.rs index 6089e4dad9..ad732978c3 100644 --- a/editor/src/messages/tool/common_functionality/snapping.rs +++ b/editor/src/messages/tool/common_functionality/snapping.rs @@ -251,15 +251,18 @@ impl SnapManager { pub fn update_indicator(&mut self, snapped_point: SnappedPoint) { self.indicator = snapped_point.is_snapped().then_some(snapped_point); } + pub fn clear_indicator(&mut self) { self.indicator = None; } + 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 snapped = self.free_snap(snap_data, &point, SnapTypeConfiguration::default()); self.update_indicator(snapped); } - pub fn gradient_preview_draw(&mut self, snap_data: &SnapData, mouse: DVec2) { + + 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); diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 0f5fbf8dc3..607caafcaf 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -215,6 +215,7 @@ impl SelectedGradient { } } + #[allow(clippy::too_many_arguments)] pub fn update_gradient( &mut self, mut mouse: DVec2, @@ -279,6 +280,7 @@ impl SelectedGradient { } GradientDragTarget::Step(s) => { 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), @@ -532,13 +534,18 @@ impl Fsm for GradientToolFsmState { self } (GradientToolFsmState::Ready { .. }, GradientToolMessage::PointerDown) => { + 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.metadata().document_to_viewport.inverse().transform_point2(mouse)); + 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.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document); + mouse = document_to_viewport.transform_point2(snapped.snapped_point_document); } + tool_data.drag_start = mouse; let tolerance = (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2); @@ -721,7 +728,7 @@ impl Fsm for GradientToolFsmState { } let snap_data = SnapData::new(document, input, viewport); - tool_data.snap_manager.gradient_preview_draw(&snap_data, mouse); + tool_data.snap_manager.preview_draw_gradient(&snap_data, mouse); responses.add(OverlaysMessage::Draw); GradientToolFsmState::Ready { hover_insertion }