From 9a2d278da13c50d9d01acaf756eb765f68482eca Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sat, 7 Feb 2026 07:09:50 +0000 Subject: [PATCH 1/3] 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 fd18cc1125..14dc8dba73 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, GradientType}; #[derive(Default, ExtractField)] @@ -378,6 +378,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) => { @@ -591,7 +594,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, @@ -661,6 +672,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 8e63209ee6ea2cbdd53e62615c0167b9aa40cf93 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sun, 22 Feb 2026 08:31:31 +0000 Subject: [PATCH 2/3] 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 14dc8dba73..175947604f 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -530,21 +530,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 584ef7ae09385763eac3343e201b77c53cc6abc9 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sun, 22 Feb 2026 10:26:21 +0000 Subject: [PATCH 3/3] 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 175947604f..5683bc11c3 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, 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.); @@ -485,7 +531,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); @@ -592,14 +644,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, @@ -607,6 +653,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, ); }