diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index f0fa53697..f73bb1765 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -660,6 +660,9 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { bounds.height, ); self.tabs[i].scene.camera_generation += 1; + // Keep an in-progress box selection pinned to the + // drawing as the view pans under it (#234). + self.reproject_box_anchor(i, vp_size.0, vp_size.1); } self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p); return Task::none(); @@ -1242,6 +1245,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { sel.middle_down = false; sel.middle_last_pos = None; sel.box_anchor = None; + sel.box_anchor_world = None; sel.box_current = None; sel.box_crossing = false; sel.poly_active = false; @@ -2220,6 +2224,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { sel.poly_points.clear(); sel.poly_crossing = false; sel.box_anchor = None; + sel.box_anchor_world = None; sel.box_current = None; } else { if box_anchor.is_none() { @@ -2318,12 +2323,20 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { // add to it (issue #83). The box completion // (or Esc) decides what happens to the // selection. + // Pin the anchor to the world point under it + // so a zoom/pan mid-drag re-projects it + // instead of leaving it frozen in pixels + // (#234). Computed before the selection + // borrow so the &self projection can't clash. + let anchor_world = + self.cursor_model_point(i, &edit_cam, p, bounds); let mut sel = self.tabs[i].scene.selection.borrow_mut(); // Full-canvas space: ViewportMove updates // box_current in canvas coords and the overlay // draws there; release maps back into the tile. sel.box_anchor = Some(p_full); sel.box_current = Some(p_full); + sel.box_anchor_world = Some(anchor_world); sel.box_crossing = false; } } @@ -2381,6 +2394,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { sel.box_last = Some((a, p)); sel.box_last_crossing = crossing; sel.box_anchor = None; + sel.box_anchor_world = None; sel.box_current = None; sel.box_crossing = false; selection_just_completed = true; @@ -2662,10 +2676,33 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { } drop(cam); self.tabs[i].scene.camera_generation += 1; + // Keep an in-progress box selection pinned to the drawing + // as the view zooms under it (#234). + self.reproject_box_anchor(i, vw, vh); } Task::none() } + /// Re-pin an active box-selection anchor to its stored world point after the + /// model-space view moves (zoom/pan), so the selection rectangle tracks the + /// drawing instead of staying frozen at its original pixel. No-op when no + /// box is in progress. (#234) + pub(super) fn reproject_box_anchor(&mut self, i: usize, vw: f32, vh: f32) { + let world = self.tabs[i].scene.selection.borrow().box_anchor_world; + let Some(world) = world else { return }; + let tile_b = self.tabs[i].scene.active_model_tile_bounds(vw, vh); + let tile_local = iced::Rectangle { + x: 0.0, + y: 0.0, + width: tile_b.width, + height: tile_b.height, + }; + if let Some(sp) = self.tabs[i].scene.camera.borrow().project(world, tile_local) { + self.tabs[i].scene.selection.borrow_mut().box_anchor = + Some(iced::Point::new(sp.x + tile_b.x, sp.y + tile_b.y)); + } + } + pub(super) fn on_viewport_click(&mut self) -> Task { let i = self.active_tab; let rot = self.tabs[i].scene.active_view_rotation_mat(); diff --git a/src/scene/pick/selection_state.rs b/src/scene/pick/selection_state.rs index dcd031d6b..935f9e578 100644 --- a/src/scene/pick/selection_state.rs +++ b/src/scene/pick/selection_state.rs @@ -6,6 +6,11 @@ use iced::Point; pub struct SelectionState { pub vp_size: (f32, f32), pub box_anchor: Option, + /// World point under the box-selection anchor, so the anchor can be + /// re-projected to screen when the camera zooms/pans mid-drag instead of + /// staying frozen at its original pixel (which selected the wrong area). + /// (#234) + pub box_anchor_world: Option, pub box_current: Option, pub box_last: Option<(Point, Point)>, pub box_crossing: bool,