diff --git a/src/ndv/viewer/_backends/_protocols.py b/src/ndv/viewer/_backends/_protocols.py index dd51f855..d91f0886 100755 --- a/src/ndv/viewer/_backends/_protocols.py +++ b/src/ndv/viewer/_backends/_protocols.py @@ -97,6 +97,11 @@ def set_range( z: tuple[float, float] | None = None, margin: float = ..., ) -> None: ... + def zoom( + self, + factor: float | tuple, + center: tuple | None = None, + ) -> None: ... def refresh(self) -> None: ... def qwidget(self) -> QWidget: ... def add_image( diff --git a/src/ndv/viewer/_backends/_pygfx.py b/src/ndv/viewer/_backends/_pygfx.py index b6307b03..51a37cf6 100755 --- a/src/ndv/viewer/_backends/_pygfx.py +++ b/src/ndv/viewer/_backends/_pygfx.py @@ -561,6 +561,23 @@ def set_range( cam.zoom = 1 - margin self.refresh() + def zoom(self, factor: float | tuple, center: tuple | None = None): + """ + Zoom in (or out) at the given center. + + Parameters + ---------- + factor : float or tuple + Fraction by which the scene should be zoomed (e.g. a factor of 2 + causes the scene to appear twice as large). + center : tuple of 2-4 elements + The center of the view. If not given or None, use the + current center. + """ + cam = self._camera + cam.zoom *= factor + self.refresh() + def refresh(self) -> None: self._canvas.update() self._canvas.request_draw(self._animate) diff --git a/src/ndv/viewer/_backends/_vispy.py b/src/ndv/viewer/_backends/_vispy.py index e74a05e7..91860584 100755 --- a/src/ndv/viewer/_backends/_vispy.py +++ b/src/ndv/viewer/_backends/_vispy.py @@ -580,6 +580,21 @@ def set_range( max_size = max(x[1], y[1], z[1]) self._camera.scale_factor = max_size + 6 + def zoom(self, factor: float | tuple, center: tuple | None = None): + """ + Zoom in (or out) at the given center. + + Parameters + ---------- + factor : float or tuple + Fraction by which the scene should be zoomed (e.g. a factor of 2 + causes the scene to appear twice as large). + center : tuple of 2-4 elements + The center of the view. If not given or None, use the + current center. + """ + return self._camera.zoom(factor=factor, center=center) + def canvas_to_world( self, pos_xy: tuple[float, float] ) -> tuple[float, float, float]: diff --git a/src/ndv/viewer/_viewer.py b/src/ndv/viewer/_viewer.py index 8b976559..d3d97f30 100755 --- a/src/ndv/viewer/_viewer.py +++ b/src/ndv/viewer/_viewer.py @@ -153,6 +153,9 @@ def __init__( # ROI self._roi: PRoiHandle | None = None + # closest data point under the mouse + self._data_coords: tuple[int, int] = (0, 0) + # WIDGETS ---------------------------------------------------- # the button that controls the display mode of the channels @@ -731,6 +734,7 @@ def _update_hover_info(self, event: QMouseEvent) -> bool: x = int(x) y = int(y) + self._data_coords = (x, y) text = f"[{y}, {x}]" # TODO: Can we use self._canvas.elements_at? for n, handles in enumerate(self._img_handles.values()): @@ -765,6 +769,10 @@ def keyPressEvent(self, a0: QKeyEvent | None) -> None: if a0.key() == Qt.Key.Key_Delete and self._selection is not None: self._selection.remove() self._selection = None + elif a0.key() in [Qt.Key.Key_Plus, Qt.Key.Key_Equal]: + self._canvas.zoom(factor=0.667, center=self._data_coords) + elif a0.key() in [Qt.Key.Key_Minus, Qt.Key.Key_Underscore]: + self._canvas.zoom(factor=1.5, center=self._data_coords) def _update_roi_button(self, event: QMouseEvent) -> bool: if self._add_roi_btn.isChecked():