Skip to content

Commit 637ab21

Browse files
committed
Fix PTZ coordinate drift during drag operations
This shifts all dragging offset calculations in TransformLayer, SelectTool, and PathTool to document space, removing any dependency on cached viewport projections for state logic. Both BoundingBoxManager's interactions and custom Path selection behaviors now properly invalidate and lazily reload viewport layouts on CanvasTransformed updates, resolving issues with geometry moving opposite the viewport during pan/tilt/zoom.
1 parent 203910a commit 637ab21

6 files changed

Lines changed: 354 additions & 130 deletions

File tree

editor/src/messages/tool/common_functionality/transformation_cage.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,52 @@ pub fn snap_drag(start: DVec2, current: DVec2, snap_to_axis: bool, axis: Axis, s
368368
document.metadata().document_to_viewport.transform_vector2(offset)
369369
}
370370

371+
/// Snaps a dragging event using document-space drag state so PTZ changes do not invalidate the drag anchor.
372+
pub fn snap_drag_from_document(start: DVec2, current: DVec2, snap_to_axis: bool, axis: Axis, snap_data: SnapData, snap_manager: &mut SnapManager, candidates: &[SnapCandidatePoint]) -> DVec2 {
373+
let document = snap_data.document;
374+
let document_to_viewport = document.metadata().document_to_viewport;
375+
let start_viewport = document_to_viewport.transform_point2(start);
376+
let mouse_position = axis_align_drag(snap_to_axis, axis, snap_data.input.mouse.position, start_viewport);
377+
let aligned_document = document_to_viewport.inverse().transform_point2(mouse_position);
378+
let total_mouse_delta_document = aligned_document - start;
379+
let mut offset = aligned_document - current;
380+
let mut best_snap = SnappedPoint::infinite_snap(aligned_document);
381+
382+
let bbox = Rect::point_iter(candidates.iter().map(|candidate| candidate.document_point + total_mouse_delta_document));
383+
384+
for (index, point) in candidates.iter().enumerate() {
385+
let config = SnapTypeConfiguration {
386+
bbox,
387+
accept_distribution: true,
388+
use_existing_candidates: index != 0,
389+
..Default::default()
390+
};
391+
392+
let mut point = point.clone();
393+
point.document_point += total_mouse_delta_document;
394+
395+
let constrained_along_axis = snap_to_axis || axis.is_constraint();
396+
let snapped = if constrained_along_axis {
397+
let constraint = SnapConstraint::Line {
398+
origin: point.document_point,
399+
direction: total_mouse_delta_document.try_normalize().unwrap_or(DVec2::X),
400+
};
401+
snap_manager.constrained_snap(&snap_data, &point, constraint, config)
402+
} else {
403+
snap_manager.free_snap(&snap_data, &point, config)
404+
};
405+
406+
if best_snap.other_snap_better(&snapped) {
407+
offset = snapped.snapped_point_document - point.document_point + (aligned_document - current);
408+
best_snap = snapped;
409+
}
410+
}
411+
412+
snap_manager.update_indicator(best_snap);
413+
414+
offset
415+
}
416+
371417
/// Contains info on the overlays for the bounding box and transform handles
372418
#[derive(Clone, Debug, Default)]
373419
pub struct BoundingBoxManager {
@@ -379,10 +425,12 @@ pub struct BoundingBoxManager {
379425
pub transform_tampered: bool,
380426
/// The transform to viewport space for the bounds co-ordinates when the transformation was started.
381427
pub original_bound_transform: DAffine2,
428+
pub original_bounds_to_document: DAffine2,
382429
pub selected_edges: Option<SelectedEdges>,
383430
pub original_transforms: OriginalTransforms,
384431
pub opposite_pivot: DVec2,
385432
pub center_of_transformation: DVec2,
433+
pub center_of_transformation_doc: DVec2,
386434
}
387435

388436
impl BoundingBoxManager {

editor/src/messages/tool/tool_messages/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub mod tool_prelude {
1717
pub use crate::messages::input_mapper::utility_types::input_keyboard::{Key, MouseMotion};
1818
pub use crate::messages::layout::utility_types::widget_prelude::*;
1919
pub use crate::messages::prelude::*;
20-
pub use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionMessageContext, ToolMetadata, ToolTransition, ToolType};
20+
pub use crate::messages::tool::utility_types::{DragState, EventToMessageMap, Fsm, ToolActionMessageContext, ToolMetadata, ToolTransition, ToolType};
2121
pub use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
2222
pub use glam::{DAffine2, DVec2};
2323
}

editor/src/messages/tool/tool_messages/path_tool.rs

Lines changed: 58 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ pub struct PathToolOptions {
5555
pub enum PathToolMessage {
5656
// Standard messages
5757
Abort,
58+
CanvasTransformed,
5859
SelectionChanged,
5960
Overlays {
6061
context: OverlayContext,
@@ -511,6 +512,7 @@ impl ToolTransition for PathTool {
511512
fn event_to_message_map(&self) -> EventToMessageMap {
512513
EventToMessageMap {
513514
tool_abort: Some(PathToolMessage::Abort.into()),
515+
canvas_transformed: Some(PathToolMessage::CanvasTransformed.into()),
514516
selection_changed: Some(PathToolMessage::SelectionChanged.into()),
515517
overlay_provider: Some(|context| PathToolMessage::Overlays { context }.into()),
516518
..Default::default()
@@ -561,7 +563,7 @@ struct PathToolData {
561563
snap_manager: SnapManager,
562564
lasso_polygon: Vec<DVec2>,
563565
selection_mode: Option<SelectionMode>,
564-
drag_start_pos: DVec2,
566+
drag_start_doc: DVec2,
565567
previous_mouse_position: DVec2,
566568
toggle_colinear_debounce: bool,
567569
opposing_handle_lengths: Option<OpposingHandleLengths>,
@@ -611,6 +613,7 @@ struct PathToolData {
611613
hovered_layers: Vec<LayerNodeIdentifier>,
612614
ghost_outline: Vec<(Vec<ClickTargetType>, LayerNodeIdentifier)>,
613615
make_path_editable_is_allowed: bool,
616+
last_pointer_move: Option<PathToolMessage>,
614617
}
615618

616619
impl PathToolData {
@@ -645,14 +648,19 @@ impl PathToolData {
645648
// Convert previous mouse position to viewport space first
646649
let document_to_viewport = metadata.document_to_viewport;
647650
let previous_mouse = document_to_viewport.transform_point2(self.previous_mouse_position);
648-
if previous_mouse == self.drag_start_pos {
651+
let drag_start_vp = document_to_viewport.transform_point2(self.drag_start_doc);
652+
if previous_mouse == drag_start_vp {
649653
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
650-
[self.drag_start_pos - tolerance, self.drag_start_pos + tolerance]
654+
[drag_start_vp - tolerance, drag_start_vp + tolerance]
651655
} else {
652-
[self.drag_start_pos, previous_mouse]
656+
[drag_start_vp, previous_mouse]
653657
}
654658
}
655659

660+
fn drag_start_viewport(&self, document: &DocumentMessageHandler) -> DVec2 {
661+
document.metadata().document_to_viewport.transform_point2(self.drag_start_doc)
662+
}
663+
656664
fn update_selection_status(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler) {
657665
let selection_status = get_selection_status(&document.network_interface, shape_editor);
658666

@@ -736,7 +744,7 @@ impl PathToolData {
736744
self.double_click_handled = false;
737745
self.opposing_handle_lengths = None;
738746

739-
self.drag_start_pos = input.mouse.position;
747+
self.drag_start_doc = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
740748

741749
if input.time - self.last_click_time > DOUBLE_CLICK_MILLISECONDS {
742750
self.saved_points_before_anchor_convert_smooth_sharp.clear();
@@ -784,7 +792,7 @@ impl PathToolData {
784792
}
785793

786794
if let Some(selected_points) = selection_info {
787-
self.drag_start_pos = input.mouse.position;
795+
self.drag_start_doc = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
788796

789797
// If selected points contain only handles and there was some selection before, then it is stored and becomes restored upon release
790798
let mut dragging_only_handles = true;
@@ -871,7 +879,7 @@ impl PathToolData {
871879
// TODO: If the segment connected to one of the endpoints is also selected then select that point
872880
}
873881

874-
self.drag_start_pos = input.mouse.position;
882+
self.drag_start_doc = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
875883
let viewport_to_document = document.metadata().document_to_viewport.inverse();
876884
self.previous_mouse_position = viewport_to_document.transform_point2(input.mouse.position);
877885

@@ -900,15 +908,15 @@ impl PathToolData {
900908

901909
self.started_drawing_from_inside = true;
902910

903-
self.drag_start_pos = input.mouse.position;
911+
self.drag_start_doc = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
904912
self.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
905913

906914
let selection_shape = if lasso_select { SelectionShapeType::Lasso } else { SelectionShapeType::Box };
907915
PathToolFsmState::Drawing { selection_shape }
908916
}
909917
// Start drawing
910918
else {
911-
self.drag_start_pos = input.mouse.position;
919+
self.drag_start_doc = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
912920
self.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
913921

914922
let selection_shape = if lasso_select { SelectionShapeType::Lasso } else { SelectionShapeType::Box };
@@ -1153,7 +1161,7 @@ impl PathToolData {
11531161
fn start_snap_along_axis(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
11541162
// Find the negative delta to take the point to the drag start position
11551163
let current_mouse = input.mouse.position;
1156-
let drag_start = self.drag_start_pos;
1164+
let drag_start = self.drag_start_viewport(document);
11571165
let opposite_delta = drag_start - current_mouse;
11581166

11591167
shape_editor.move_selected_points_and_segments(None, document, opposite_delta, false, true, false, None, false, responses);
@@ -1174,7 +1182,7 @@ impl PathToolData {
11741182
fn stop_snap_along_axis(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
11751183
// Calculate the negative delta of the selection and move it back to the drag start
11761184
let current_mouse = input.mouse.position;
1177-
let drag_start = self.drag_start_pos;
1185+
let drag_start = self.drag_start_viewport(document);
11781186

11791187
let opposite_delta = drag_start - current_mouse;
11801188
let Some(axis) = self.snapping_axis else { return };
@@ -1463,7 +1471,7 @@ impl PathToolData {
14631471
let mut was_alt_dragging = false;
14641472

14651473
if self.snapping_axis.is_none() {
1466-
if self.alt_clicked_on_anchor && !self.alt_dragging_from_anchor && self.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
1474+
if self.alt_clicked_on_anchor && !self.alt_dragging_from_anchor && self.drag_start_viewport(document).distance(input.mouse.position) > DRAG_THRESHOLD {
14671475
// Checking which direction the dragging begins
14681476
self.alt_dragging_from_anchor = true;
14691477
let Some(layer) = document.network_interface.selected_nodes().selected_layers(document.metadata()).next() else {
@@ -1485,7 +1493,7 @@ impl PathToolData {
14851493
return;
14861494
};
14871495

1488-
let delta = input.mouse.position - self.drag_start_pos;
1496+
let delta = input.mouse.position - self.drag_start_viewport(document);
14891497
let handle = if delta.dot(tangent1) >= delta.dot(tangent2) {
14901498
segment1.to_manipulator_point()
14911499
} else {
@@ -1528,7 +1536,7 @@ impl PathToolData {
15281536
// Constantly checking and changing the snapping axis based on current mouse position
15291537
if snap_axis && self.snapping_axis.is_some() {
15301538
let Some(current_axis) = self.snapping_axis else { return };
1531-
let total_delta = self.drag_start_pos - input.mouse.position;
1539+
let total_delta = self.drag_start_viewport(document) - input.mouse.position;
15321540

15331541
if (total_delta.x.abs() > total_delta.y.abs() && current_axis == Axis::Y) || (total_delta.y.abs() > total_delta.x.abs() && current_axis == Axis::X) {
15341542
self.stop_snap_along_axis(shape_editor, document, input, responses);
@@ -1706,7 +1714,7 @@ impl Fsm for PathToolFsmState {
17061714
}
17071715
(_, PathToolMessage::Overlays { context: mut overlay_context }) => {
17081716
// Set this to show ghost line only if drag actually happened
1709-
if matches!(self, Self::Dragging(_)) && tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
1717+
if matches!(self, Self::Dragging(_)) && tool_data.drag_start_viewport(document).distance(input.mouse.position) > DRAG_THRESHOLD {
17101718
for (outline, layer) in &tool_data.ghost_outline {
17111719
let transform = document.metadata().transform_to_viewport(*layer);
17121720
overlay_context.outline(outline.iter(), transform, Some(COLOR_OVERLAY_GRAY));
@@ -1904,7 +1912,8 @@ impl Fsm for PathToolFsmState {
19041912
let (points_inside, segments_inside) = match selection_shape {
19051913
SelectionShapeType::Box => {
19061914
let previous_mouse = document.metadata().document_to_viewport.transform_point2(tool_data.previous_mouse_position);
1907-
let bbox = Rect::new(tool_data.drag_start_pos.x, tool_data.drag_start_pos.y, previous_mouse.x, previous_mouse.y).abs();
1915+
let drag_start_vp = tool_data.drag_start_viewport(document);
1916+
let bbox = Rect::new(drag_start_vp.x, drag_start_vp.y, previous_mouse.x, previous_mouse.y).abs();
19081917
shape_editor.get_inside_points_and_segments(
19091918
&document.network_interface,
19101919
SelectionShape::Box(bbox),
@@ -1971,7 +1980,7 @@ impl Fsm for PathToolFsmState {
19711980
// Draw the snapping axis lines
19721981
if tool_data.snapping_axis.is_some() {
19731982
let Some(axis) = tool_data.snapping_axis else { return self };
1974-
let origin = tool_data.drag_start_pos;
1983+
let origin = tool_data.drag_start_viewport(document);
19751984
let viewport_diagonal = viewport.size().into_dvec2().length();
19761985

19771986
match axis {
@@ -2096,14 +2105,24 @@ impl Fsm for PathToolFsmState {
20962105
segment_editing_modifier,
20972106
},
20982107
) => {
2108+
tool_data.last_pointer_move = Some(PathToolMessage::PointerMove {
2109+
equidistant,
2110+
toggle_colinear,
2111+
move_anchor_with_handles,
2112+
snap_angle,
2113+
lock_angle,
2114+
delete_segment,
2115+
break_colinear_molding,
2116+
segment_editing_modifier,
2117+
});
20992118
let selected_only_handles = !shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_)));
21002119
tool_data.stored_selection = None;
21012120

2102-
if !tool_data.saved_selection_before_handle_drag.is_empty() && (tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD) && (selected_only_handles) {
2121+
if !tool_data.saved_selection_before_handle_drag.is_empty() && (tool_data.drag_start_viewport(document).distance(input.mouse.position) > DRAG_THRESHOLD) && (selected_only_handles) {
21032122
tool_data.handle_drag_toggle = true;
21042123
}
21052124

2106-
if tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
2125+
if tool_data.drag_start_viewport(document).distance(input.mouse.position) > DRAG_THRESHOLD {
21072126
tool_data.molding_segment = true;
21082127
}
21092128

@@ -2243,19 +2262,11 @@ impl Fsm for PathToolFsmState {
22432262
self
22442263
}
22452264
(PathToolFsmState::Drawing { selection_shape: selection_type }, PathToolMessage::PointerOutsideViewport { .. }) => {
2246-
// Auto-panning
2247-
if let Some(offset) = tool_data.auto_panning.shift_viewport(input, viewport, responses) {
2248-
tool_data.drag_start_pos += offset;
2249-
}
2250-
2265+
let _ = tool_data.auto_panning.shift_viewport(input, viewport, responses);
22512266
PathToolFsmState::Drawing { selection_shape: selection_type }
22522267
}
22532268
(PathToolFsmState::Dragging(dragging_state), PathToolMessage::PointerOutsideViewport { .. }) => {
2254-
// Auto-panning
2255-
if let Some(offset) = tool_data.auto_panning.shift_viewport(input, viewport, responses) {
2256-
tool_data.drag_start_pos += offset;
2257-
}
2258-
2269+
let _ = tool_data.auto_panning.shift_viewport(input, viewport, responses);
22592270
PathToolFsmState::Dragging(dragging_state)
22602271
}
22612272
(
@@ -2314,7 +2325,7 @@ impl Fsm for PathToolFsmState {
23142325

23152326
let document_to_viewport = document.metadata().document_to_viewport;
23162327
let previous_mouse = document_to_viewport.transform_point2(tool_data.previous_mouse_position);
2317-
if tool_data.drag_start_pos == previous_mouse {
2328+
if tool_data.drag_start_viewport(document) == previous_mouse {
23182329
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
23192330
} else {
23202331
let selection_mode = match tool_action_data.preferences.get_selection_mode() {
@@ -2324,7 +2335,8 @@ impl Fsm for PathToolFsmState {
23242335

23252336
match selection_shape {
23262337
SelectionShapeType::Box => {
2327-
let bbox = Rect::new(tool_data.drag_start_pos.x, tool_data.drag_start_pos.y, previous_mouse.x, previous_mouse.y).abs();
2338+
let drag_start_vp = tool_data.drag_start_viewport(document);
2339+
let bbox = Rect::new(drag_start_vp.x, drag_start_vp.y, previous_mouse.x, previous_mouse.y).abs();
23282340

23292341
shape_editor.select_all_in_shape(
23302342
&document.network_interface,
@@ -2355,7 +2367,7 @@ impl Fsm for PathToolFsmState {
23552367
PathToolFsmState::Ready
23562368
}
23572369
(PathToolFsmState::Dragging { .. }, PathToolMessage::Escape | PathToolMessage::RightClick) => {
2358-
if tool_data.handle_drag_toggle && tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
2370+
if tool_data.handle_drag_toggle && tool_data.drag_start_viewport(document).distance(input.mouse.position) > DRAG_THRESHOLD {
23592371
shape_editor.deselect_all_points();
23602372
shape_editor.deselect_all_segments();
23612373

@@ -2410,7 +2422,7 @@ impl Fsm for PathToolFsmState {
24102422
};
24112423
tool_data.started_drawing_from_inside = false;
24122424

2413-
if tool_data.drag_start_pos.distance(previous_mouse) < 1e-8 {
2425+
if tool_data.drag_start_viewport(document).distance(previous_mouse) < 1e-8 {
24142426
// Clicked inside or outside the shape then deselect all of the points/segments
24152427
if document.click(input, viewport).is_some() && tool_data.stored_selection.is_none() {
24162428
tool_data.stored_selection = Some(shape_editor.selected_shape_state.clone());
@@ -2421,7 +2433,8 @@ impl Fsm for PathToolFsmState {
24212433
} else {
24222434
match selection_shape {
24232435
SelectionShapeType::Box => {
2424-
let bbox = Rect::new(tool_data.drag_start_pos.x, tool_data.drag_start_pos.y, previous_mouse.x, previous_mouse.y).abs();
2436+
let drag_start_vp = tool_data.drag_start_viewport(document);
2437+
let bbox = Rect::new(drag_start_vp.x, drag_start_vp.y, previous_mouse.x, previous_mouse.y).abs();
24252438

24262439
shape_editor.select_all_in_shape(
24272440
&document.network_interface,
@@ -2454,7 +2467,7 @@ impl Fsm for PathToolFsmState {
24542467
(_, PathToolMessage::DragStop { extend_selection, .. }) => {
24552468
tool_data.ghost_outline.clear();
24562469
let extend_selection = input.keyboard.get(extend_selection as usize);
2457-
let drag_occurred = tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD;
2470+
let drag_occurred = tool_data.drag_start_viewport(document).distance(input.mouse.position) > DRAG_THRESHOLD;
24582471
let mut segment_dissolved = false;
24592472
let mut point_inserted = false;
24602473

@@ -2585,7 +2598,7 @@ impl Fsm for PathToolFsmState {
25852598
}
25862599
}
25872600
// Deselect all points if the user clicks the filled region of the shape
2588-
else if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD {
2601+
else if tool_data.drag_start_viewport(document).distance(input.mouse.position) <= DRAG_THRESHOLD {
25892602
shape_editor.deselect_all_points();
25902603
shape_editor.deselect_all_segments();
25912604
}
@@ -3037,7 +3050,7 @@ impl Fsm for PathToolFsmState {
30373050

30383051
if nearest_point.is_some() {
30393052
// Flip the selected point between smooth and sharp
3040-
if !tool_data.double_click_handled && tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD {
3053+
if !tool_data.double_click_handled && tool_data.drag_start_viewport(document).distance(input.mouse.position) <= DRAG_THRESHOLD {
30413054
responses.add(DocumentMessage::StartTransaction);
30423055

30433056
shape_editor.select_points_by_layer_and_id(&tool_data.saved_points_before_anchor_convert_smooth_sharp);
@@ -3119,6 +3132,14 @@ impl Fsm for PathToolFsmState {
31193132

31203133
PathToolFsmState::Ready
31213134
}
3135+
(PathToolFsmState::Dragging(_) | PathToolFsmState::Drawing { .. } | PathToolFsmState::SlidingPoint, PathToolMessage::CanvasTransformed) => {
3136+
if let Some(msg) = tool_data.last_pointer_move.clone() {
3137+
responses.add(msg);
3138+
}
3139+
responses.add(OverlaysMessage::Draw);
3140+
self
3141+
}
3142+
(_, PathToolMessage::CanvasTransformed) => self,
31223143
(_, PathToolMessage::Abort) => {
31233144
responses.add(OverlaysMessage::Draw);
31243145
PathToolFsmState::Ready

0 commit comments

Comments
 (0)