We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
3D camera rotation is broken
## 🐛 Bug

It seems we introduced a bug in #5701, according to `git bisect`. The 3D camera rotation is "absolute" instead of relative (i.e: the point you press on the canvas determines the starting point of that rotation).

## To Reproduce

```py
import napari
import numpy as np
v = napari.Viewer(ndisplay=3)
pl = v.add_points(np.random.rand(10, 3) * 100)
```

Then move the camera a few times.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `napari/_vispy/camera.py`
Content:
```
1 from typing import Type
2 
3 import numpy as np
4 from vispy.scene import ArcballCamera, BaseCamera, PanZoomCamera
5 
6 from napari._vispy.utils.quaternion import quaternion2euler
7 
8 
9 class VispyCamera:
10     """Vipsy camera for both 2D and 3D rendering.
11 
12     Parameters
13     ----------
14     view : vispy.scene.widgets.viewbox.ViewBox
15         Viewbox for current scene.
16     camera : napari.components.Camera
17         napari camera model.
18     dims : napari.components.Dims
19         napari dims model.
20     """
21 
22     def __init__(self, view, camera, dims) -> None:
23         self._view = view
24         self._camera = camera
25         self._dims = dims
26 
27         # Create 2D camera
28         self._2D_camera = MouseToggledPanZoomCamera(aspect=1)
29         # flip y-axis to have correct alignment
30         self._2D_camera.flip = (0, 1, 0)
31         self._2D_camera.viewbox_key_event = viewbox_key_event
32 
33         # Create 3D camera
34         self._3D_camera = MouseToggledArcballCamera(fov=0)
35         self._3D_camera.viewbox_key_event = viewbox_key_event
36 
37         # Set 2D camera by default
38         self._view.camera = self._2D_camera
39 
40         self._dims.events.ndisplay.connect(
41             self._on_ndisplay_change, position='first'
42         )
43 
44         self._camera.events.center.connect(self._on_center_change)
45         self._camera.events.zoom.connect(self._on_zoom_change)
46         self._camera.events.angles.connect(self._on_angles_change)
47         self._camera.events.perspective.connect(self._on_perspective_change)
48         self._camera.events.mouse_pan.connect(self._on_mouse_toggles_change)
49         self._camera.events.mouse_zoom.connect(self._on_mouse_toggles_change)
50 
51         self._on_ndisplay_change()
52 
53     @property
54     def angles(self):
55         """3-tuple: Euler angles of camera in 3D viewing, in degrees.
56         Note that angles might be different than the ones that might have generated the quaternion.
57         """
58 
59         if self._view.camera == self._3D_camera:
60             # Do conversion from quaternion representation to euler angles
61             angles = quaternion2euler(
62                 self._view.camera._quaternion, degrees=True
63             )
64         else:
65             angles = (0, 0, 90)
66         return angles
67 
68     @angles.setter
69     def angles(self, angles):
70         if self.angles == tuple(angles):
71             return
72 
73         # Only update angles if current camera is 3D camera
74         if self._view.camera == self._3D_camera:
75             # Create and set quaternion
76             quat = self._view.camera._quaternion.create_from_euler_angles(
77                 *angles,
78                 degrees=True,
79             )
80             self._view.camera._quaternion = quat
81             self._view.camera.view_changed()
82 
83     @property
84     def center(self):
85         """tuple: Center point of camera view for 2D or 3D viewing."""
86         if self._view.camera == self._3D_camera:
87             center = tuple(self._view.camera.center)
88         else:
89             # in 2D, we arbitrarily choose 0.0 as the center in z
90             center = (*self._view.camera.center[:2], 0.0)
91         # switch from VisPy xyz ordering to NumPy prc ordering
92         return center[::-1]
93 
94     @center.setter
95     def center(self, center):
96         if self.center == tuple(center):
97             return
98         self._view.camera.center = center[::-1]
99         self._view.camera.view_changed()
100 
101     @property
102     def zoom(self):
103         """float: Scale from canvas pixels to world pixels."""
104         canvas_size = np.array(self._view.canvas.size)
105         if self._view.camera == self._3D_camera:
106             # For fov = 0.0 normalize scale factor by canvas size to get scale factor.
107             # Note that the scaling is stored in the `_projection` property of the
108             # camera which is updated in vispy here
109             # https://github.com/vispy/vispy/blob/v0.6.5/vispy/scene/cameras/perspective.py#L301-L313
110             scale = self._view.camera.scale_factor
111         else:
112             scale = np.array(
113                 [self._view.camera.rect.width, self._view.camera.rect.height]
114             )
115             scale[np.isclose(scale, 0)] = 1  # fix for #2875
116         zoom = np.min(canvas_size / scale)
117         return zoom
118 
119     @zoom.setter
120     def zoom(self, zoom):
121         if self.zoom == zoom:
122             return
123         scale = np.array(self._view.canvas.size) / zoom
124         if self._view.camera == self._3D_camera:
125             self._view.camera.scale_factor = np.min(scale)
126         else:
127             # Set view rectangle, as left, right, width, height
128             corner = np.subtract(self._view.camera.center[:2], scale / 2)
129             self._view.camera.rect = tuple(corner) + tuple(scale)
130 
131     @property
132     def perspective(self):
133         """Field of view of camera (only visible in 3D mode)."""
134         return self._3D_camera.fov
135 
136     @perspective.setter
137     def perspective(self, perspective):
138         if self.perspective == perspective:
139             return
140         self._3D_camera.fov = perspective
141         self._view.camera.view_changed()
142 
143     @property
144     def mouse_zoom(self) -> bool:
145         return self._view.camera.mouse_zoom
146 
147     @mouse_zoom.setter
148     def mouse_zoom(self, mouse_zoom: bool):
149         self._view.camera.mouse_zoom = mouse_zoom
150 
151     @property
152     def mouse_pan(self) -> bool:
153         return self._view.camera.mouse_pan
154 
155     @mouse_pan.setter
156     def mouse_pan(self, mouse_pan: bool):
157         self._view.camera.mouse_pan = mouse_pan
158 
159     def _on_ndisplay_change(self):
160         if self._dims.ndisplay == 3:
161             self._view.camera = self._3D_camera
162         else:
163             self._view.camera = self._2D_camera
164 
165         self._on_mouse_toggles_change()
166         self._on_center_change()
167         self._on_zoom_change()
168         self._on_angles_change()
169 
170     def _on_mouse_toggles_change(self):
171         self.mouse_pan = self._camera.mouse_pan
172         self.mouse_zoom = self._camera.mouse_zoom
173 
174     def _on_center_change(self):
175         self.center = self._camera.center[-self._dims.ndisplay :]
176 
177     def _on_zoom_change(self):
178         self.zoom = self._camera.zoom
179 
180     def _on_perspective_change(self):
181         self.perspective = self._camera.perspective
182 
183     def _on_angles_change(self):
184         self.angles = self._camera.angles
185 
186     def on_draw(self, _event):
187         """Called whenever the canvas is drawn.
188 
189         Update camera model angles, center, and zoom.
190         """
191         with self._camera.events.angles.blocker(self._on_angles_change):
192             self._camera.angles = self.angles
193         with self._camera.events.center.blocker(self._on_center_change):
194             self._camera.center = self.center
195         with self._camera.events.zoom.blocker(self._on_zoom_change):
196             self._camera.zoom = self.zoom
197         with self._camera.events.perspective.blocker(
198             self._on_perspective_change
199         ):
200             self._camera.perspective = self.perspective
201 
202 
203 def viewbox_key_event(event):
204     """ViewBox key event handler.
205 
206     Parameters
207     ----------
208     event : vispy.util.event.Event
209         The vispy event that triggered this method.
210     """
211     return
212 
213 
214 def add_mouse_pan_zoom_toggles(
215     vispy_camera_cls: Type[BaseCamera],
216 ) -> Type[BaseCamera]:
217     """Add separate mouse pan and mouse zoom toggles to VisPy.
218 
219     By default, VisPy uses an ``interactive`` toggle that turns *both*
220     panning and zooming on and off. This decorator adds separate toggles,
221     ``mouse_pan`` and ``mouse_zoom``, to enable controlling them
222     separately.
223 
224     Parameters
225     ----------
226     vispy_camera_cls : Type[vispy.scene.cameras.BaseCamera]
227         A VisPy camera class to decorate.
228 
229     Returns
230     -------
231         A decorated VisPy camera class.
232     """
233 
234     class _vispy_camera_cls(vispy_camera_cls):
235         def __init__(self, **kwargs):
236             super().__init__(**kwargs)
237             self.mouse_pan = True
238             self.mouse_zoom = True
239 
240         def viewbox_mouse_event(self, event):
241             if (
242                 self.mouse_zoom
243                 and event.type == 'mouse_wheel'
244                 or self.mouse_pan
245                 and event.type in ('mouse_move', 'mouse_press')
246             ):
247                 super().viewbox_mouse_event(event)
248             else:
249                 event.handled = False
250 
251     return _vispy_camera_cls
252 
253 
254 MouseToggledPanZoomCamera = add_mouse_pan_zoom_toggles(PanZoomCamera)
255 MouseToggledArcballCamera = add_mouse_pan_zoom_toggles(ArcballCamera)
256
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/napari/_vispy/camera.py b/napari/_vispy/camera.py
--- a/napari/_vispy/camera.py
+++ b/napari/_vispy/camera.py
@@ -242,7 +242,8 @@
                 self.mouse_zoom
                 and event.type == 'mouse_wheel'
                 or self.mouse_pan
-                and event.type in ('mouse_move', 'mouse_press')
+                and event.type
+                in ('mouse_move', 'mouse_press', 'mouse_release')
             ):
                 super().viewbox_mouse_event(event)
             else:

{"golden_diff": "diff --git a/napari/_vispy/camera.py b/napari/_vispy/camera.py\n--- a/napari/_vispy/camera.py\n+++ b/napari/_vispy/camera.py\n@@ -242,7 +242,8 @@\n                 self.mouse_zoom\n                 and event.type == 'mouse_wheel'\n                 or self.mouse_pan\n-                and event.type in ('mouse_move', 'mouse_press')\n+                and event.type\n+                in ('mouse_move', 'mouse_press', 'mouse_release')\n             ):\n                 super().viewbox_mouse_event(event)\n             else:\n", "issue": "3D camera rotation is broken\n## \ud83d\udc1b Bug\r\n\r\nIt seems we introduced a bug in #5701, according to `git bisect`. The 3D camera rotation is \"absolute\" instead of relative (i.e: the point you press on the canvas determines the starting point of that rotation).\r\n\r\n## To Reproduce\r\n\r\n```py\r\nimport napari\r\nimport numpy as np\r\nv = napari.Viewer(ndisplay=3)\r\npl = v.add_points(np.random.rand(10, 3) * 100)\r\n```\r\n\r\nThen move the camera a few times.\n", "before_files": [{"content": "from typing import Type\n\nimport numpy as np\nfrom vispy.scene import ArcballCamera, BaseCamera, PanZoomCamera\n\nfrom napari._vispy.utils.quaternion import quaternion2euler\n\n\nclass VispyCamera:\n    \"\"\"Vipsy camera for both 2D and 3D rendering.\n\n    Parameters\n    ----------\n    view : vispy.scene.widgets.viewbox.ViewBox\n        Viewbox for current scene.\n    camera : napari.components.Camera\n        napari camera model.\n    dims : napari.components.Dims\n        napari dims model.\n    \"\"\"\n\n    def __init__(self, view, camera, dims) -> None:\n        self._view = view\n        self._camera = camera\n        self._dims = dims\n\n        # Create 2D camera\n        self._2D_camera = MouseToggledPanZoomCamera(aspect=1)\n        # flip y-axis to have correct alignment\n        self._2D_camera.flip = (0, 1, 0)\n        self._2D_camera.viewbox_key_event = viewbox_key_event\n\n        # Create 3D camera\n        self._3D_camera = MouseToggledArcballCamera(fov=0)\n        self._3D_camera.viewbox_key_event = viewbox_key_event\n\n        # Set 2D camera by default\n        self._view.camera = self._2D_camera\n\n        self._dims.events.ndisplay.connect(\n            self._on_ndisplay_change, position='first'\n        )\n\n        self._camera.events.center.connect(self._on_center_change)\n        self._camera.events.zoom.connect(self._on_zoom_change)\n        self._camera.events.angles.connect(self._on_angles_change)\n        self._camera.events.perspective.connect(self._on_perspective_change)\n        self._camera.events.mouse_pan.connect(self._on_mouse_toggles_change)\n        self._camera.events.mouse_zoom.connect(self._on_mouse_toggles_change)\n\n        self._on_ndisplay_change()\n\n    @property\n    def angles(self):\n        \"\"\"3-tuple: Euler angles of camera in 3D viewing, in degrees.\n        Note that angles might be different than the ones that might have generated the quaternion.\n        \"\"\"\n\n        if self._view.camera == self._3D_camera:\n            # Do conversion from quaternion representation to euler angles\n            angles = quaternion2euler(\n                self._view.camera._quaternion, degrees=True\n            )\n        else:\n            angles = (0, 0, 90)\n        return angles\n\n    @angles.setter\n    def angles(self, angles):\n        if self.angles == tuple(angles):\n            return\n\n        # Only update angles if current camera is 3D camera\n        if self._view.camera == self._3D_camera:\n            # Create and set quaternion\n            quat = self._view.camera._quaternion.create_from_euler_angles(\n                *angles,\n                degrees=True,\n            )\n            self._view.camera._quaternion = quat\n            self._view.camera.view_changed()\n\n    @property\n    def center(self):\n        \"\"\"tuple: Center point of camera view for 2D or 3D viewing.\"\"\"\n        if self._view.camera == self._3D_camera:\n            center = tuple(self._view.camera.center)\n        else:\n            # in 2D, we arbitrarily choose 0.0 as the center in z\n            center = (*self._view.camera.center[:2], 0.0)\n        # switch from VisPy xyz ordering to NumPy prc ordering\n        return center[::-1]\n\n    @center.setter\n    def center(self, center):\n        if self.center == tuple(center):\n            return\n        self._view.camera.center = center[::-1]\n        self._view.camera.view_changed()\n\n    @property\n    def zoom(self):\n        \"\"\"float: Scale from canvas pixels to world pixels.\"\"\"\n        canvas_size = np.array(self._view.canvas.size)\n        if self._view.camera == self._3D_camera:\n            # For fov = 0.0 normalize scale factor by canvas size to get scale factor.\n            # Note that the scaling is stored in the `_projection` property of the\n            # camera which is updated in vispy here\n            # https://github.com/vispy/vispy/blob/v0.6.5/vispy/scene/cameras/perspective.py#L301-L313\n            scale = self._view.camera.scale_factor\n        else:\n            scale = np.array(\n                [self._view.camera.rect.width, self._view.camera.rect.height]\n            )\n            scale[np.isclose(scale, 0)] = 1  # fix for #2875\n        zoom = np.min(canvas_size / scale)\n        return zoom\n\n    @zoom.setter\n    def zoom(self, zoom):\n        if self.zoom == zoom:\n            return\n        scale = np.array(self._view.canvas.size) / zoom\n        if self._view.camera == self._3D_camera:\n            self._view.camera.scale_factor = np.min(scale)\n        else:\n            # Set view rectangle, as left, right, width, height\n            corner = np.subtract(self._view.camera.center[:2], scale / 2)\n            self._view.camera.rect = tuple(corner) + tuple(scale)\n\n    @property\n    def perspective(self):\n        \"\"\"Field of view of camera (only visible in 3D mode).\"\"\"\n        return self._3D_camera.fov\n\n    @perspective.setter\n    def perspective(self, perspective):\n        if self.perspective == perspective:\n            return\n        self._3D_camera.fov = perspective\n        self._view.camera.view_changed()\n\n    @property\n    def mouse_zoom(self) -> bool:\n        return self._view.camera.mouse_zoom\n\n    @mouse_zoom.setter\n    def mouse_zoom(self, mouse_zoom: bool):\n        self._view.camera.mouse_zoom = mouse_zoom\n\n    @property\n    def mouse_pan(self) -> bool:\n        return self._view.camera.mouse_pan\n\n    @mouse_pan.setter\n    def mouse_pan(self, mouse_pan: bool):\n        self._view.camera.mouse_pan = mouse_pan\n\n    def _on_ndisplay_change(self):\n        if self._dims.ndisplay == 3:\n            self._view.camera = self._3D_camera\n        else:\n            self._view.camera = self._2D_camera\n\n        self._on_mouse_toggles_change()\n        self._on_center_change()\n        self._on_zoom_change()\n        self._on_angles_change()\n\n    def _on_mouse_toggles_change(self):\n        self.mouse_pan = self._camera.mouse_pan\n        self.mouse_zoom = self._camera.mouse_zoom\n\n    def _on_center_change(self):\n        self.center = self._camera.center[-self._dims.ndisplay :]\n\n    def _on_zoom_change(self):\n        self.zoom = self._camera.zoom\n\n    def _on_perspective_change(self):\n        self.perspective = self._camera.perspective\n\n    def _on_angles_change(self):\n        self.angles = self._camera.angles\n\n    def on_draw(self, _event):\n        \"\"\"Called whenever the canvas is drawn.\n\n        Update camera model angles, center, and zoom.\n        \"\"\"\n        with self._camera.events.angles.blocker(self._on_angles_change):\n            self._camera.angles = self.angles\n        with self._camera.events.center.blocker(self._on_center_change):\n            self._camera.center = self.center\n        with self._camera.events.zoom.blocker(self._on_zoom_change):\n            self._camera.zoom = self.zoom\n        with self._camera.events.perspective.blocker(\n            self._on_perspective_change\n        ):\n            self._camera.perspective = self.perspective\n\n\ndef viewbox_key_event(event):\n    \"\"\"ViewBox key event handler.\n\n    Parameters\n    ----------\n    event : vispy.util.event.Event\n        The vispy event that triggered this method.\n    \"\"\"\n    return\n\n\ndef add_mouse_pan_zoom_toggles(\n    vispy_camera_cls: Type[BaseCamera],\n) -> Type[BaseCamera]:\n    \"\"\"Add separate mouse pan and mouse zoom toggles to VisPy.\n\n    By default, VisPy uses an ``interactive`` toggle that turns *both*\n    panning and zooming on and off. This decorator adds separate toggles,\n    ``mouse_pan`` and ``mouse_zoom``, to enable controlling them\n    separately.\n\n    Parameters\n    ----------\n    vispy_camera_cls : Type[vispy.scene.cameras.BaseCamera]\n        A VisPy camera class to decorate.\n\n    Returns\n    -------\n        A decorated VisPy camera class.\n    \"\"\"\n\n    class _vispy_camera_cls(vispy_camera_cls):\n        def __init__(self, **kwargs):\n            super().__init__(**kwargs)\n            self.mouse_pan = True\n            self.mouse_zoom = True\n\n        def viewbox_mouse_event(self, event):\n            if (\n                self.mouse_zoom\n                and event.type == 'mouse_wheel'\n                or self.mouse_pan\n                and event.type in ('mouse_move', 'mouse_press')\n            ):\n                super().viewbox_mouse_event(event)\n            else:\n                event.handled = False\n\n    return _vispy_camera_cls\n\n\nMouseToggledPanZoomCamera = add_mouse_pan_zoom_toggles(PanZoomCamera)\nMouseToggledArcballCamera = add_mouse_pan_zoom_toggles(ArcballCamera)\n", "path": "napari/_vispy/camera.py"}], "after_files": [{"content": "from typing import Type\n\nimport numpy as np\nfrom vispy.scene import ArcballCamera, BaseCamera, PanZoomCamera\n\nfrom napari._vispy.utils.quaternion import quaternion2euler\n\n\nclass VispyCamera:\n    \"\"\"Vipsy camera for both 2D and 3D rendering.\n\n    Parameters\n    ----------\n    view : vispy.scene.widgets.viewbox.ViewBox\n        Viewbox for current scene.\n    camera : napari.components.Camera\n        napari camera model.\n    dims : napari.components.Dims\n        napari dims model.\n    \"\"\"\n\n    def __init__(self, view, camera, dims) -> None:\n        self._view = view\n        self._camera = camera\n        self._dims = dims\n\n        # Create 2D camera\n        self._2D_camera = MouseToggledPanZoomCamera(aspect=1)\n        # flip y-axis to have correct alignment\n        self._2D_camera.flip = (0, 1, 0)\n        self._2D_camera.viewbox_key_event = viewbox_key_event\n\n        # Create 3D camera\n        self._3D_camera = MouseToggledArcballCamera(fov=0)\n        self._3D_camera.viewbox_key_event = viewbox_key_event\n\n        # Set 2D camera by default\n        self._view.camera = self._2D_camera\n\n        self._dims.events.ndisplay.connect(\n            self._on_ndisplay_change, position='first'\n        )\n\n        self._camera.events.center.connect(self._on_center_change)\n        self._camera.events.zoom.connect(self._on_zoom_change)\n        self._camera.events.angles.connect(self._on_angles_change)\n        self._camera.events.perspective.connect(self._on_perspective_change)\n        self._camera.events.mouse_pan.connect(self._on_mouse_toggles_change)\n        self._camera.events.mouse_zoom.connect(self._on_mouse_toggles_change)\n\n        self._on_ndisplay_change()\n\n    @property\n    def angles(self):\n        \"\"\"3-tuple: Euler angles of camera in 3D viewing, in degrees.\n        Note that angles might be different than the ones that might have generated the quaternion.\n        \"\"\"\n\n        if self._view.camera == self._3D_camera:\n            # Do conversion from quaternion representation to euler angles\n            angles = quaternion2euler(\n                self._view.camera._quaternion, degrees=True\n            )\n        else:\n            angles = (0, 0, 90)\n        return angles\n\n    @angles.setter\n    def angles(self, angles):\n        if self.angles == tuple(angles):\n            return\n\n        # Only update angles if current camera is 3D camera\n        if self._view.camera == self._3D_camera:\n            # Create and set quaternion\n            quat = self._view.camera._quaternion.create_from_euler_angles(\n                *angles,\n                degrees=True,\n            )\n            self._view.camera._quaternion = quat\n            self._view.camera.view_changed()\n\n    @property\n    def center(self):\n        \"\"\"tuple: Center point of camera view for 2D or 3D viewing.\"\"\"\n        if self._view.camera == self._3D_camera:\n            center = tuple(self._view.camera.center)\n        else:\n            # in 2D, we arbitrarily choose 0.0 as the center in z\n            center = (*self._view.camera.center[:2], 0.0)\n        # switch from VisPy xyz ordering to NumPy prc ordering\n        return center[::-1]\n\n    @center.setter\n    def center(self, center):\n        if self.center == tuple(center):\n            return\n        self._view.camera.center = center[::-1]\n        self._view.camera.view_changed()\n\n    @property\n    def zoom(self):\n        \"\"\"float: Scale from canvas pixels to world pixels.\"\"\"\n        canvas_size = np.array(self._view.canvas.size)\n        if self._view.camera == self._3D_camera:\n            # For fov = 0.0 normalize scale factor by canvas size to get scale factor.\n            # Note that the scaling is stored in the `_projection` property of the\n            # camera which is updated in vispy here\n            # https://github.com/vispy/vispy/blob/v0.6.5/vispy/scene/cameras/perspective.py#L301-L313\n            scale = self._view.camera.scale_factor\n        else:\n            scale = np.array(\n                [self._view.camera.rect.width, self._view.camera.rect.height]\n            )\n            scale[np.isclose(scale, 0)] = 1  # fix for #2875\n        zoom = np.min(canvas_size / scale)\n        return zoom\n\n    @zoom.setter\n    def zoom(self, zoom):\n        if self.zoom == zoom:\n            return\n        scale = np.array(self._view.canvas.size) / zoom\n        if self._view.camera == self._3D_camera:\n            self._view.camera.scale_factor = np.min(scale)\n        else:\n            # Set view rectangle, as left, right, width, height\n            corner = np.subtract(self._view.camera.center[:2], scale / 2)\n            self._view.camera.rect = tuple(corner) + tuple(scale)\n\n    @property\n    def perspective(self):\n        \"\"\"Field of view of camera (only visible in 3D mode).\"\"\"\n        return self._3D_camera.fov\n\n    @perspective.setter\n    def perspective(self, perspective):\n        if self.perspective == perspective:\n            return\n        self._3D_camera.fov = perspective\n        self._view.camera.view_changed()\n\n    @property\n    def mouse_zoom(self) -> bool:\n        return self._view.camera.mouse_zoom\n\n    @mouse_zoom.setter\n    def mouse_zoom(self, mouse_zoom: bool):\n        self._view.camera.mouse_zoom = mouse_zoom\n\n    @property\n    def mouse_pan(self) -> bool:\n        return self._view.camera.mouse_pan\n\n    @mouse_pan.setter\n    def mouse_pan(self, mouse_pan: bool):\n        self._view.camera.mouse_pan = mouse_pan\n\n    def _on_ndisplay_change(self):\n        if self._dims.ndisplay == 3:\n            self._view.camera = self._3D_camera\n        else:\n            self._view.camera = self._2D_camera\n\n        self._on_mouse_toggles_change()\n        self._on_center_change()\n        self._on_zoom_change()\n        self._on_angles_change()\n\n    def _on_mouse_toggles_change(self):\n        self.mouse_pan = self._camera.mouse_pan\n        self.mouse_zoom = self._camera.mouse_zoom\n\n    def _on_center_change(self):\n        self.center = self._camera.center[-self._dims.ndisplay :]\n\n    def _on_zoom_change(self):\n        self.zoom = self._camera.zoom\n\n    def _on_perspective_change(self):\n        self.perspective = self._camera.perspective\n\n    def _on_angles_change(self):\n        self.angles = self._camera.angles\n\n    def on_draw(self, _event):\n        \"\"\"Called whenever the canvas is drawn.\n\n        Update camera model angles, center, and zoom.\n        \"\"\"\n        with self._camera.events.angles.blocker(self._on_angles_change):\n            self._camera.angles = self.angles\n        with self._camera.events.center.blocker(self._on_center_change):\n            self._camera.center = self.center\n        with self._camera.events.zoom.blocker(self._on_zoom_change):\n            self._camera.zoom = self.zoom\n        with self._camera.events.perspective.blocker(\n            self._on_perspective_change\n        ):\n            self._camera.perspective = self.perspective\n\n\ndef viewbox_key_event(event):\n    \"\"\"ViewBox key event handler.\n\n    Parameters\n    ----------\n    event : vispy.util.event.Event\n        The vispy event that triggered this method.\n    \"\"\"\n    return\n\n\ndef add_mouse_pan_zoom_toggles(\n    vispy_camera_cls: Type[BaseCamera],\n) -> Type[BaseCamera]:\n    \"\"\"Add separate mouse pan and mouse zoom toggles to VisPy.\n\n    By default, VisPy uses an ``interactive`` toggle that turns *both*\n    panning and zooming on and off. This decorator adds separate toggles,\n    ``mouse_pan`` and ``mouse_zoom``, to enable controlling them\n    separately.\n\n    Parameters\n    ----------\n    vispy_camera_cls : Type[vispy.scene.cameras.BaseCamera]\n        A VisPy camera class to decorate.\n\n    Returns\n    -------\n        A decorated VisPy camera class.\n    \"\"\"\n\n    class _vispy_camera_cls(vispy_camera_cls):\n        def __init__(self, **kwargs):\n            super().__init__(**kwargs)\n            self.mouse_pan = True\n            self.mouse_zoom = True\n\n        def viewbox_mouse_event(self, event):\n            if (\n                self.mouse_zoom\n                and event.type == 'mouse_wheel'\n                or self.mouse_pan\n                and event.type\n                in ('mouse_move', 'mouse_press', 'mouse_release')\n            ):\n                super().viewbox_mouse_event(event)\n            else:\n                event.handled = False\n\n    return _vispy_camera_cls\n\n\nMouseToggledPanZoomCamera = add_mouse_pan_zoom_toggles(PanZoomCamera)\nMouseToggledArcballCamera = add_mouse_pan_zoom_toggles(ArcballCamera)\n", "path": "napari/_vispy/camera.py"}]}