Skip to content

Commit 95995d8

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 95995d8

6 files changed

Lines changed: 367 additions & 123 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: 75 additions & 29 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>,
@@ -645,14 +647,19 @@ impl PathToolData {
645647
// Convert previous mouse position to viewport space first
646648
let document_to_viewport = metadata.document_to_viewport;
647649
let previous_mouse = document_to_viewport.transform_point2(self.previous_mouse_position);
648-
if previous_mouse == self.drag_start_pos {
650+
let drag_start_vp = document_to_viewport.transform_point2(self.drag_start_doc);
651+
if previous_mouse == drag_start_vp {
649652
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
650-
[self.drag_start_pos - tolerance, self.drag_start_pos + tolerance]
653+
[drag_start_vp - tolerance, drag_start_vp + tolerance]
651654
} else {
652-
[self.drag_start_pos, previous_mouse]
655+
[drag_start_vp, previous_mouse]
653656
}
654657
}
655658

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

@@ -736,7 +743,7 @@ impl PathToolData {
736743
self.double_click_handled = false;
737744
self.opposing_handle_lengths = None;
738745

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

741748
if input.time - self.last_click_time > DOUBLE_CLICK_MILLISECONDS {
742749
self.saved_points_before_anchor_convert_smooth_sharp.clear();
@@ -784,7 +791,7 @@ impl PathToolData {
784791
}
785792

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

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

874-
self.drag_start_pos = input.mouse.position;
881+
self.drag_start_doc = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
875882
let viewport_to_document = document.metadata().document_to_viewport.inverse();
876883
self.previous_mouse_position = viewport_to_document.transform_point2(input.mouse.position);
877884

@@ -900,15 +907,15 @@ impl PathToolData {
900907

901908
self.started_drawing_from_inside = true;
902909

903-
self.drag_start_pos = input.mouse.position;
910+
self.drag_start_doc = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
904911
self.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
905912

906913
let selection_shape = if lasso_select { SelectionShapeType::Lasso } else { SelectionShapeType::Box };
907914
PathToolFsmState::Drawing { selection_shape }
908915
}
909916
// Start drawing
910917
else {
911-
self.drag_start_pos = input.mouse.position;
918+
self.drag_start_doc = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
912919
self.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
913920

914921
let selection_shape = if lasso_select { SelectionShapeType::Lasso } else { SelectionShapeType::Box };
@@ -1153,7 +1160,7 @@ impl PathToolData {
11531160
fn start_snap_along_axis(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
11541161
// Find the negative delta to take the point to the drag start position
11551162
let current_mouse = input.mouse.position;
1156-
let drag_start = self.drag_start_pos;
1163+
let drag_start = self.drag_start_viewport(document);
11571164
let opposite_delta = drag_start - current_mouse;
11581165

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

11791186
let opposite_delta = drag_start - current_mouse;
11801187
let Some(axis) = self.snapping_axis else { return };
@@ -1463,7 +1470,7 @@ impl PathToolData {
14631470
let mut was_alt_dragging = false;
14641471

14651472
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 {
1473+
if self.alt_clicked_on_anchor && !self.alt_dragging_from_anchor && self.drag_start_viewport(document).distance(input.mouse.position) > DRAG_THRESHOLD {
14671474
// Checking which direction the dragging begins
14681475
self.alt_dragging_from_anchor = true;
14691476
let Some(layer) = document.network_interface.selected_nodes().selected_layers(document.metadata()).next() else {
@@ -1485,7 +1492,7 @@ impl PathToolData {
14851492
return;
14861493
};
14871494

1488-
let delta = input.mouse.position - self.drag_start_pos;
1495+
let delta = input.mouse.position - self.drag_start_viewport(document);
14891496
let handle = if delta.dot(tangent1) >= delta.dot(tangent2) {
14901497
segment1.to_manipulator_point()
14911498
} else {
@@ -1528,7 +1535,7 @@ impl PathToolData {
15281535
// Constantly checking and changing the snapping axis based on current mouse position
15291536
if snap_axis && self.snapping_axis.is_some() {
15301537
let Some(current_axis) = self.snapping_axis else { return };
1531-
let total_delta = self.drag_start_pos - input.mouse.position;
1538+
let total_delta = self.drag_start_viewport(document) - input.mouse.position;
15321539

15331540
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) {
15341541
self.stop_snap_along_axis(shape_editor, document, input, responses);
@@ -1706,7 +1713,7 @@ impl Fsm for PathToolFsmState {
17061713
}
17071714
(_, PathToolMessage::Overlays { context: mut overlay_context }) => {
17081715
// 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 {
1716+
if matches!(self, Self::Dragging(_)) && tool_data.drag_start_viewport(document).distance(input.mouse.position) > DRAG_THRESHOLD {
17101717
for (outline, layer) in &tool_data.ghost_outline {
17111718
let transform = document.metadata().transform_to_viewport(*layer);
17121719
overlay_context.outline(outline.iter(), transform, Some(COLOR_OVERLAY_GRAY));
@@ -1904,7 +1911,8 @@ impl Fsm for PathToolFsmState {
19041911
let (points_inside, segments_inside) = match selection_shape {
19051912
SelectionShapeType::Box => {
19061913
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();
1914+
let drag_start_vp = tool_data.drag_start_viewport(document);
1915+
let bbox = Rect::new(drag_start_vp.x, drag_start_vp.y, previous_mouse.x, previous_mouse.y).abs();
19081916
shape_editor.get_inside_points_and_segments(
19091917
&document.network_interface,
19101918
SelectionShape::Box(bbox),
@@ -1971,7 +1979,7 @@ impl Fsm for PathToolFsmState {
19711979
// Draw the snapping axis lines
19721980
if tool_data.snapping_axis.is_some() {
19731981
let Some(axis) = tool_data.snapping_axis else { return self };
1974-
let origin = tool_data.drag_start_pos;
1982+
let origin = tool_data.drag_start_viewport(document);
19751983
let viewport_diagonal = viewport.size().into_dvec2().length();
19761984

19771985
match axis {
@@ -2099,11 +2107,11 @@ impl Fsm for PathToolFsmState {
20992107
let selected_only_handles = !shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_)));
21002108
tool_data.stored_selection = None;
21012109

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) {
2110+
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) {
21032111
tool_data.handle_drag_toggle = true;
21042112
}
21052113

2106-
if tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
2114+
if tool_data.drag_start_viewport(document).distance(input.mouse.position) > DRAG_THRESHOLD {
21072115
tool_data.molding_segment = true;
21082116
}
21092117

@@ -2245,15 +2253,15 @@ impl Fsm for PathToolFsmState {
22452253
(PathToolFsmState::Drawing { selection_shape: selection_type }, PathToolMessage::PointerOutsideViewport { .. }) => {
22462254
// Auto-panning
22472255
if let Some(offset) = tool_data.auto_panning.shift_viewport(input, viewport, responses) {
2248-
tool_data.drag_start_pos += offset;
2256+
tool_data.drag_start_doc += document.metadata().document_to_viewport.inverse().transform_vector2(offset);
22492257
}
22502258

22512259
PathToolFsmState::Drawing { selection_shape: selection_type }
22522260
}
22532261
(PathToolFsmState::Dragging(dragging_state), PathToolMessage::PointerOutsideViewport { .. }) => {
22542262
// Auto-panning
22552263
if let Some(offset) = tool_data.auto_panning.shift_viewport(input, viewport, responses) {
2256-
tool_data.drag_start_pos += offset;
2264+
tool_data.drag_start_doc += document.metadata().document_to_viewport.inverse().transform_vector2(offset);
22572265
}
22582266

22592267
PathToolFsmState::Dragging(dragging_state)
@@ -2314,7 +2322,7 @@ impl Fsm for PathToolFsmState {
23142322

23152323
let document_to_viewport = document.metadata().document_to_viewport;
23162324
let previous_mouse = document_to_viewport.transform_point2(tool_data.previous_mouse_position);
2317-
if tool_data.drag_start_pos == previous_mouse {
2325+
if tool_data.drag_start_viewport(document) == previous_mouse {
23182326
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
23192327
} else {
23202328
let selection_mode = match tool_action_data.preferences.get_selection_mode() {
@@ -2324,7 +2332,8 @@ impl Fsm for PathToolFsmState {
23242332

23252333
match selection_shape {
23262334
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();
2335+
let drag_start_vp = tool_data.drag_start_viewport(document);
2336+
let bbox = Rect::new(drag_start_vp.x, drag_start_vp.y, previous_mouse.x, previous_mouse.y).abs();
23282337

23292338
shape_editor.select_all_in_shape(
23302339
&document.network_interface,
@@ -2355,7 +2364,7 @@ impl Fsm for PathToolFsmState {
23552364
PathToolFsmState::Ready
23562365
}
23572366
(PathToolFsmState::Dragging { .. }, PathToolMessage::Escape | PathToolMessage::RightClick) => {
2358-
if tool_data.handle_drag_toggle && tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
2367+
if tool_data.handle_drag_toggle && tool_data.drag_start_viewport(document).distance(input.mouse.position) > DRAG_THRESHOLD {
23592368
shape_editor.deselect_all_points();
23602369
shape_editor.deselect_all_segments();
23612370

@@ -2410,7 +2419,7 @@ impl Fsm for PathToolFsmState {
24102419
};
24112420
tool_data.started_drawing_from_inside = false;
24122421

2413-
if tool_data.drag_start_pos.distance(previous_mouse) < 1e-8 {
2422+
if tool_data.drag_start_viewport(document).distance(previous_mouse) < 1e-8 {
24142423
// Clicked inside or outside the shape then deselect all of the points/segments
24152424
if document.click(input, viewport).is_some() && tool_data.stored_selection.is_none() {
24162425
tool_data.stored_selection = Some(shape_editor.selected_shape_state.clone());
@@ -2421,7 +2430,8 @@ impl Fsm for PathToolFsmState {
24212430
} else {
24222431
match selection_shape {
24232432
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();
2433+
let drag_start_vp = tool_data.drag_start_viewport(document);
2434+
let bbox = Rect::new(drag_start_vp.x, drag_start_vp.y, previous_mouse.x, previous_mouse.y).abs();
24252435

24262436
shape_editor.select_all_in_shape(
24272437
&document.network_interface,
@@ -2454,7 +2464,7 @@ impl Fsm for PathToolFsmState {
24542464
(_, PathToolMessage::DragStop { extend_selection, .. }) => {
24552465
tool_data.ghost_outline.clear();
24562466
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;
2467+
let drag_occurred = tool_data.drag_start_viewport(document).distance(input.mouse.position) > DRAG_THRESHOLD;
24582468
let mut segment_dissolved = false;
24592469
let mut point_inserted = false;
24602470

@@ -2585,7 +2595,7 @@ impl Fsm for PathToolFsmState {
25852595
}
25862596
}
25872597
// 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 {
2598+
else if tool_data.drag_start_viewport(document).distance(input.mouse.position) <= DRAG_THRESHOLD {
25892599
shape_editor.deselect_all_points();
25902600
shape_editor.deselect_all_segments();
25912601
}
@@ -3037,7 +3047,7 @@ impl Fsm for PathToolFsmState {
30373047

30383048
if nearest_point.is_some() {
30393049
// 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 {
3050+
if !tool_data.double_click_handled && tool_data.drag_start_viewport(document).distance(input.mouse.position) <= DRAG_THRESHOLD {
30413051
responses.add(DocumentMessage::StartTransaction);
30423052

30433053
shape_editor.select_points_by_layer_and_id(&tool_data.saved_points_before_anchor_convert_smooth_sharp);
@@ -3119,6 +3129,42 @@ impl Fsm for PathToolFsmState {
31193129

31203130
PathToolFsmState::Ready
31213131
}
3132+
// PTZ handling: re-emit PointerMove to recompute positions with updated document_to_viewport
3133+
(PathToolFsmState::Dragging(dragging_state), PathToolMessage::CanvasTransformed) => {
3134+
let modifier_keys = PathToolMessage::PointerMove {
3135+
equidistant: Key::Alt,
3136+
toggle_colinear: Key::KeyC,
3137+
move_anchor_with_handles: Key::Space,
3138+
snap_angle: Key::Control,
3139+
lock_angle: Key::Shift,
3140+
delete_segment: Key::Backspace,
3141+
break_colinear_molding: Key::Tab,
3142+
segment_editing_modifier: Key::Alt,
3143+
};
3144+
responses.add(modifier_keys);
3145+
responses.add(OverlaysMessage::Draw);
3146+
PathToolFsmState::Dragging(dragging_state)
3147+
}
3148+
(PathToolFsmState::Drawing { selection_shape }, PathToolMessage::CanvasTransformed) => {
3149+
let modifier_keys = PathToolMessage::PointerMove {
3150+
equidistant: Key::Alt,
3151+
toggle_colinear: Key::KeyC,
3152+
move_anchor_with_handles: Key::Space,
3153+
snap_angle: Key::Control,
3154+
lock_angle: Key::Shift,
3155+
delete_segment: Key::Backspace,
3156+
break_colinear_molding: Key::Tab,
3157+
segment_editing_modifier: Key::Alt,
3158+
};
3159+
responses.add(modifier_keys);
3160+
responses.add(OverlaysMessage::Draw);
3161+
PathToolFsmState::Drawing { selection_shape }
3162+
}
3163+
(PathToolFsmState::SlidingPoint, PathToolMessage::CanvasTransformed) => {
3164+
responses.add(OverlaysMessage::Draw);
3165+
PathToolFsmState::SlidingPoint
3166+
}
3167+
(_, PathToolMessage::CanvasTransformed) => self,
31223168
(_, PathToolMessage::Abort) => {
31233169
responses.add(OverlaysMessage::Draw);
31243170
PathToolFsmState::Ready

0 commit comments

Comments
 (0)