From fca3b23374f56c5740a02d019f6dc9eb8f387980 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Fri, 23 Jan 2026 12:32:32 +0000 Subject: [PATCH 1/3] piviot snapping --- .../tool/tool_messages/select_tool.rs | 81 ++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index e5738e7770..558b96a5cf 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -7,7 +7,7 @@ use crate::messages::portfolio::document::graph_operation::utility_types::Transf use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; -use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType}; +use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, BoundingBoxSnapTarget, FlipAxis, GroupFolderType, PathSnapSource, SnapSource, SnapTarget}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface, NodeTemplate}; use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes; use crate::messages::preferences::SelectionMode; @@ -592,6 +592,68 @@ pub fn create_bounding_box_transform(document: &DocumentMessageHandler) -> DAffi .unwrap_or_default() } +fn snap_pivot_to_bounds(document: &DocumentMessageHandler, mouse_position: DVec2, selection_bounds: &Option) -> Option { + let tolerance = snapping::snap_tolerance(document); + let mut best_distance = f64::INFINITY; + let mut best_snap_point: Option = None; + let mut best_quad: Option = None; + let mut best_point_type: u8 = 0; // 0=corner, 1=midpoint, 2=center + + let mut check_snap = |snap_doc: DVec2, quad: Quad, point_type: u8| { + let snap_viewport = document.metadata().document_to_viewport.transform_point2(snap_doc); + let distance = mouse_position.distance(snap_viewport); + if distance < tolerance && distance < best_distance { + best_distance = distance; + best_snap_point = Some(snap_doc); + best_quad = Some(quad); + best_point_type = point_type; + } + }; + + // Snap to selection's combined bounding box + if let Some(bounds) = selection_bounds { + let quad = document.metadata().document_to_viewport.inverse() * bounds.transform * Quad::from_box(bounds.bounds); + for i in 0..4 { + check_snap(quad.0[i], quad, 0); + check_snap((quad.0[i] + quad.0[(i + 1) % 4]) / 2.0, quad, 1); + } + check_snap(quad.center(), quad, 2); + } + + let selected: Vec<_> = document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface).collect(); + for layer in document.metadata().all_layers() { + if selected.contains(&layer) || !document.network_interface.is_visible(&layer.to_node(), &[]) { + continue; + } + let Some(bounds) = document.metadata().bounding_box_with_transform(layer, document.metadata().transform_to_document(layer)) else { + continue; + }; + let quad = Quad::from_box(bounds); + for i in 0..4 { + check_snap(quad.0[i], quad, 0); + check_snap((quad.0[i] + quad.0[(i + 1) % 4]) / 2.0, quad, 1); + } + check_snap(quad.center(), quad, 2); + } + + best_snap_point.zip(best_quad).map(|(snap_point, quad)| { + let target = match best_point_type { + 0 => SnapTarget::BoundingBox(BoundingBoxSnapTarget::CornerPoint), + 1 => SnapTarget::BoundingBox(BoundingBoxSnapTarget::EdgeMidpoint), + _ => SnapTarget::BoundingBox(BoundingBoxSnapTarget::CenterPoint), + }; + snapping::SnappedPoint { + snapped_point_document: snap_point, + source: SnapSource::Path(PathSnapSource::HandlePoint), + target, + distance: best_distance, + tolerance, + target_bounds: Some(quad), + ..Default::default() + } + }) +} + impl Fsm for SelectToolFsmState { type ToolData = SelectToolData; type ToolOptions = (); @@ -1149,6 +1211,7 @@ impl Fsm for SelectToolFsmState { } (SelectToolFsmState::DraggingPivot, SelectToolMessage::Abort) => { responses.add(DocumentMessage::AbortTransaction); + tool_data.snap_manager.cleanup(responses); let selection = tool_data.nested_selection_behavior; SelectToolFsmState::Ready { selection } @@ -1274,10 +1337,24 @@ impl Fsm for SelectToolFsmState { } (SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerMove { modifier_keys }) => { let mouse_position = input.mouse.position; - let snapped_mouse_position = mouse_position; + let document_mouse = document.metadata().document_to_viewport.inverse().transform_point2(mouse_position); + let snap_data = SnapData::new(document, input, viewport); + let point = SnapCandidatePoint::handle(document_mouse); + let mut snapped = tool_data.snap_manager.free_snap(&snap_data, &point, snapping::SnapTypeConfiguration::default()); + + if let Some(pivot_snap) = snap_pivot_to_bounds(document, mouse_position, &tool_data.bounding_box_manager) + && pivot_snap.distance < snapped.distance + { + snapped = pivot_snap; + } + + tool_data.snap_manager.update_indicator(snapped.clone()); + + let snapped_mouse_position = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document); tool_data.pivot_gizmo.pivot.set_viewport_position(snapped_mouse_position); + responses.add(OverlaysMessage::Draw); responses.add(NodeGraphMessage::RunDocumentGraph); // Auto-panning From e315fd0387e4b3facb80a94d6eed3c01b84260df Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Mon, 16 Feb 2026 21:17:52 +0000 Subject: [PATCH 2/3] Cleanup-1 --- .../tool/tool_messages/select_tool.rs | 49 +++++++------------ 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 558b96a5cf..fbf0d09792 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -594,19 +594,13 @@ pub fn create_bounding_box_transform(document: &DocumentMessageHandler) -> DAffi fn snap_pivot_to_bounds(document: &DocumentMessageHandler, mouse_position: DVec2, selection_bounds: &Option) -> Option { let tolerance = snapping::snap_tolerance(document); - let mut best_distance = f64::INFINITY; - let mut best_snap_point: Option = None; - let mut best_quad: Option = None; - let mut best_point_type: u8 = 0; // 0=corner, 1=midpoint, 2=center + let mut best_snap: Option<(DVec2, Quad, BoundingBoxSnapTarget, f64)> = None; - let mut check_snap = |snap_doc: DVec2, quad: Quad, point_type: u8| { + let mut check_snap = |snap_doc: DVec2, quad: Quad, point_type: BoundingBoxSnapTarget| { let snap_viewport = document.metadata().document_to_viewport.transform_point2(snap_doc); let distance = mouse_position.distance(snap_viewport); - if distance < tolerance && distance < best_distance { - best_distance = distance; - best_snap_point = Some(snap_doc); - best_quad = Some(quad); - best_point_type = point_type; + if distance < tolerance && best_snap.map_or(true, |(_, _, _, best_distance)| distance < best_distance) { + best_snap = Some((snap_doc, quad, point_type, distance)); } }; @@ -614,10 +608,10 @@ fn snap_pivot_to_bounds(document: &DocumentMessageHandler, mouse_position: DVec2 if let Some(bounds) = selection_bounds { let quad = document.metadata().document_to_viewport.inverse() * bounds.transform * Quad::from_box(bounds.bounds); for i in 0..4 { - check_snap(quad.0[i], quad, 0); - check_snap((quad.0[i] + quad.0[(i + 1) % 4]) / 2.0, quad, 1); + check_snap(quad.0[i], quad, BoundingBoxSnapTarget::CornerPoint); + check_snap((quad.0[i] + quad.0[(i + 1) % 4]) / 2.0, quad, BoundingBoxSnapTarget::EdgeMidpoint); } - check_snap(quad.center(), quad, 2); + check_snap(quad.center(), quad, BoundingBoxSnapTarget::CenterPoint); } let selected: Vec<_> = document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface).collect(); @@ -630,27 +624,20 @@ fn snap_pivot_to_bounds(document: &DocumentMessageHandler, mouse_position: DVec2 }; let quad = Quad::from_box(bounds); for i in 0..4 { - check_snap(quad.0[i], quad, 0); - check_snap((quad.0[i] + quad.0[(i + 1) % 4]) / 2.0, quad, 1); + check_snap(quad.0[i], quad, BoundingBoxSnapTarget::CornerPoint); + check_snap((quad.0[i] + quad.0[(i + 1) % 4]) / 2.0, quad, BoundingBoxSnapTarget::EdgeMidpoint); } - check_snap(quad.center(), quad, 2); + check_snap(quad.center(), quad, BoundingBoxSnapTarget::CenterPoint); } - best_snap_point.zip(best_quad).map(|(snap_point, quad)| { - let target = match best_point_type { - 0 => SnapTarget::BoundingBox(BoundingBoxSnapTarget::CornerPoint), - 1 => SnapTarget::BoundingBox(BoundingBoxSnapTarget::EdgeMidpoint), - _ => SnapTarget::BoundingBox(BoundingBoxSnapTarget::CenterPoint), - }; - snapping::SnappedPoint { - snapped_point_document: snap_point, - source: SnapSource::Path(PathSnapSource::HandlePoint), - target, - distance: best_distance, - tolerance, - target_bounds: Some(quad), - ..Default::default() - } + best_snap.map(|(snap_point, quad, point_type, distance)| snapping::SnappedPoint { + snapped_point_document: snap_point, + source: SnapSource::Path(PathSnapSource::HandlePoint), + target: SnapTarget::BoundingBox(point_type), + distance, + tolerance, + target_bounds: Some(quad), + ..Default::default() }) } From 40f4f1929684e43820e7b4caadcd5a597d7ce10b Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Mon, 16 Feb 2026 21:30:05 +0000 Subject: [PATCH 3/3] Cleanup --- .../tool/tool_messages/select_tool.rs | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 611befe771..d0c3b11dee 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -596,22 +596,24 @@ fn snap_pivot_to_bounds(document: &DocumentMessageHandler, mouse_position: DVec2 let tolerance = snapping::snap_tolerance(document); let mut best_snap: Option<(DVec2, Quad, BoundingBoxSnapTarget, f64)> = None; - let mut check_snap = |snap_doc: DVec2, quad: Quad, point_type: BoundingBoxSnapTarget| { - let snap_viewport = document.metadata().document_to_viewport.transform_point2(snap_doc); - let distance = mouse_position.distance(snap_viewport); - if distance < tolerance && best_snap.map_or(true, |(_, _, _, best_distance)| distance < best_distance) { - best_snap = Some((snap_doc, quad, point_type, distance)); + let mut check_quad = |quad: Quad| { + let check = |snap_doc: DVec2, point_type: BoundingBoxSnapTarget, best: &mut Option<(DVec2, Quad, BoundingBoxSnapTarget, f64)>| { + let snap_viewport = document.metadata().document_to_viewport.transform_point2(snap_doc); + let distance = mouse_position.distance(snap_viewport); + if distance < tolerance && best.map_or(true, |(_, _, _, d)| distance < d) { + *best = Some((snap_doc, quad, point_type, distance)); + } + }; + for i in 0..4 { + check(quad.0[i], BoundingBoxSnapTarget::CornerPoint, &mut best_snap); + check((quad.0[i] + quad.0[(i + 1) % 4]) / 2.0, BoundingBoxSnapTarget::EdgeMidpoint, &mut best_snap); } + check(quad.center(), BoundingBoxSnapTarget::CenterPoint, &mut best_snap); }; // Snap to selection's combined bounding box if let Some(bounds) = selection_bounds { - let quad = document.metadata().document_to_viewport.inverse() * bounds.transform * Quad::from_box(bounds.bounds); - for i in 0..4 { - check_snap(quad.0[i], quad, BoundingBoxSnapTarget::CornerPoint); - check_snap((quad.0[i] + quad.0[(i + 1) % 4]) / 2.0, quad, BoundingBoxSnapTarget::EdgeMidpoint); - } - check_snap(quad.center(), quad, BoundingBoxSnapTarget::CenterPoint); + check_quad(document.metadata().document_to_viewport.inverse() * bounds.transform * Quad::from_box(bounds.bounds)); } let selected: Vec<_> = document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface).collect(); @@ -622,12 +624,7 @@ fn snap_pivot_to_bounds(document: &DocumentMessageHandler, mouse_position: DVec2 let Some(bounds) = document.metadata().bounding_box_with_transform(layer, document.metadata().transform_to_document(layer)) else { continue; }; - let quad = Quad::from_box(bounds); - for i in 0..4 { - check_snap(quad.0[i], quad, BoundingBoxSnapTarget::CornerPoint); - check_snap((quad.0[i] + quad.0[(i + 1) % 4]) / 2.0, quad, BoundingBoxSnapTarget::EdgeMidpoint); - } - check_snap(quad.center(), quad, BoundingBoxSnapTarget::CenterPoint); + check_quad(Quad::from_box(bounds)); } best_snap.map(|(snap_point, quad, point_type, distance)| snapping::SnappedPoint {