Repository: napari/napari

Issue:
IndexError slicing Surface with higher-dimensional vertex_values
## 🐛 Bug

Surface layers have an interesting dimensionality. They can have higher-dimensional vertex points, but _also_ additional dimensions on the `vertex_values`. For those not familiar the layer data for a Surface is `(vertices, faces, vertex_values)`, where:
* `vertices` is shape `(N, D)` (N vertices of D-dimensionality)
* `faces` is shape `(M, 3)` (M triangles, 3 vertices per triangle)
* `vertex_values` is shape `(K_0, ..., K_L, N)` (N vertices, L additional dims)

So then you get a dim slider for the extra dims on the vertices themselves, then another dim slider for each additional dim on the values. However the ranges of the sliders for `vertex_values` are incorrect, and produce an `IndexError` when moved all the way to the right.

In this screenshot you can see the `vertices` slider (yellow arrow) and the `vertex_values` slider (magenta arrow). The magenta star indicates the out-of-range position on the `vertex_values` slider.

![Screenshot 2023-03-10 at 8 56 39 PM](https://user-images.githubusercontent.com/1231828/224458978-85b49637-37cb-4969-a4d3-9b44e23fad86.png)

<details>
  <summary>Full stack trace</summary>
  
```
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
File ~/mf/envs/napari-417/lib/python3.10/site-packages/napari/_qt/widgets/qt_dims_slider.py:141, in QtDimSliderWidget._value_changed(self=<napari._qt.widgets.qt_dims_slider.QtDimSliderWidget object>, value=2)
    136 def _value_changed(self, value):
    137     """Slider changed to this new value.
    138 
    139     We split this out as a separate function for perfmon.
    140     """
--> 141     self.dims.set_current_step(self.axis, value)
        self.axis = 0
        value = 2
        self.dims = Dims(ndim=4, ndisplay=3, last_used=0, range=((0.0, 3.0, 1.0), (-0.4715520143508911, 1.4715520143508911, 1.0), (-0.7367839813232422, 1.9536460041999817, 1.0), (-0.6689090132713318, 2.0490000247955322, 1.0)), current_step=(2, 0, 1, 1), order=(0, 1, 2, 3), axis_labels=('0', '1', '1', '2'))
        self = <napari._qt.widgets.qt_dims_slider.QtDimSliderWidget object at 0x29ec8c940>

File ~/mf/envs/napari-417/lib/python3.10/site-packages/napari/components/dims.py:288, in Dims.set_current_step(self=Dims(ndim=4, ndisplay=3, last_used=0, range=((0....r=(0, 1, 2, 3), axis_labels=('0', '1', '1', '2')), axis=0, value=2)
    286         full_current_step = list(self.current_step)
    287         full_current_step[axis] = step
--> 288         self.current_step = full_current_step
        full_current_step = [2, 0, 1, 1]
        self.current_step = (2, 0, 1, 1)
        self = Dims(ndim=4, ndisplay=3, last_used=0, range=((0.0, 3.0, 1.0), (-0.4715520143508911, 1.4715520143508911, 1.0), (-0.7367839813232422, 1.9536460041999817, 1.0), (-0.6689090132713318, 2.0490000247955322, 1.0)), current_step=(2, 0, 1, 1), order=(0, 1, 2, 3), axis_labels=('0', '1', '1', '2'))
    289 else:
    290     full_current_step = list(self.current_step)

File ~/mf/envs/napari-417/lib/python3.10/site-packages/napari/utils/events/evented_model.py:241, in EventedModel.__setattr__(self=Dims(ndim=4, ndisplay=3, last_used=0, range=((0....r=(0, 1, 2, 3), axis_labels=('0', '1', '1', '2')), name='current_step', value=[2, 0, 1, 1])
    239 are_equal = self.__eq_operators__.get(name, operator.eq)
    240 if not are_equal(after, before):
--> 241     getattr(self.events, name)(value=after)  # emit event
        after = (2, 0, 1, 1)
        name = 'current_step'
        self = Dims(ndim=4, ndisplay=3, last_used=0, range=((0.0, 3.0, 1.0), (-0.4715520143508911, 1.4715520143508911, 1.0), (-0.7367839813232422, 1.9536460041999817, 1.0), (-0.6689090132713318, 2.0490000247955322, 1.0)), current_step=(2, 0, 1, 1), order=(0, 1, 2, 3), axis_labels=('0', '1', '1', '2'))
    243     # emit events for any dependent computed property setters as well
    244     for dep in self.__field_dependents__.get(name, {}):

File ~/mf/envs/napari-417/lib/python3.10/site-packages/napari/utils/events/event.py:757, in EventEmitter.__call__(self=<napari.utils.events.event.EventEmitter object>, *args=(), **kwargs={'value': (2, 0, 1, 1)})
    754     self._block_counter.update([cb])
    755     continue
--> 757 self._invoke_callback(cb, event if pass_event else None)
        event = <Event blocked=False handled=False native=None source=None sources=[] type='current_step'>
        self = <napari.utils.events.event.EventEmitter object at 0x288b01c90>
        cb = <bound method ViewerModel._update_layers of Viewer(axes=Axes(visible=False, labels=True, colored=True, dashed=False, arrows=True), camera=Camera(center=(0.0, 0.10843101143836975, 0.19004550576210022), zoom=274.24974945443006, angles=(0.0, 0.0, 89.99999999999999), perspective=0.0, interactive=True), cursor=Cursor(position=(1.0, -5.425513676810831e-17, 0.9944846361386972, 0.1863991944562821), scaled=True, size=1, style=<CursorStyle.STANDARD: 'standard'>), dims=Dims(ndim=4, ndisplay=3, last_used=0, range=((0.0, 3.0, 1.0), (-0.4715520143508911, 1.4715520143508911, 1.0), (-0.7367839813232422, 1.9536460041999817, 1.0), (-0.6689090132713318, 2.0490000247955322, 1.0)), current_step=(2, 0, 1, 1), order=(0, 1, 2, 3), axis_labels=('0', '1', '1', '2')), grid=GridCanvas(stride=1, shape=(-1, -1), enabled=False), layers=[<Surface layer 'Surface' at 0x29de36b30>], scale_bar=ScaleBar(visible=False, colored=False, color=<class 'numpy.ndarray'> (4,) float32, ticks=True, position=<Position.BOTTOM_RIGHT: 'bottom_right'>, font_size=10.0, box=False, box_color=<class 'numpy.ndarray'> (4,) float32, unit=None), text_overlay=TextOverlay(visible=False, color=<class 'numpy.ndarray'> (4,) float32, font_size=10.0, position=<TextOverlayPosition.TOP_LEFT: 'top_left'>, text=''), overlays=Overlays(interaction_box=InteractionBox(points=None, show=False, show_handle=False, show_vertices=False, selection_box_drag=None, selection_box_final=None, transform_start=<napari.utils.transforms.transforms.Affine object at 0x288b03b50>, transform_drag=<napari.utils.transforms.transforms.Affine object at 0x288b03bb0>, transform_final=<napari.utils.transforms.transforms.Affine object at 0x288b03c10>, transform=<napari.utils.transforms.transforms.Affine object at 0x288b03c70>, allow_new_selection=True, selected_vertex=None)), help='', status='', tooltip=Tooltip(visible=False, text=''), theme='dark', title='napari', mouse_over_canvas=False, mouse_move_callbacks=[<function InteractionBoxMouseBindings.initialize_mouse_events.<locals>.mouse_move at 0x28da35240>], mouse_drag_callbacks=[<function InteractionBoxMouseBindings.initialize_mouse_events.<locals>.mouse_drag at 0x28da36200>], mouse_double_click_callbacks=[], mouse_wheel_callbacks=[<function dims_scroll at 0x177894af0>], _persisted_mouse_event={}, _mouse_drag_gen={}, _mouse_wheel_gen={}, keymap={'Shift': <function InteractionBoxMouseBindings.initialize_key_events.<locals>.hold_to_lock_aspect_ratio at 0x28da36050>, 'Control-Shift-R': <function InteractionBoxMouseBindings._reset_active_layer_affine at 0x28da80f70>, 'Control-Shift-A': <function InteractionBoxMouseBindings._transform_active_layer at 0x28da81ab0>})>
        pass_event = False
    758 if event.blocked:
    759     break

File ~/mf/envs/napari-417/lib/python3.10/site-packages/napari/utils/events/event.py:794, in EventEmitter._invoke_callback(self=<napari.utils.events.event.EventEmitter object>, cb=<bound method ViewerModel._update_layers of View...ndings._transform_active_layer at 0x28da81ab0>})>, event=None)
    792     self.disconnect(cb)
    793     return
--> 794 _handle_exception(
        self = <napari.utils.events.event.EventEmitter object at 0x288b01c90>
        event = None
        cb = <bound method ViewerModel._update_layers of Viewer(axes=Axes(visible=False, labels=True, colored=True, dashed=False, arrows=True), camera=Camera(center=(0.0, 0.10843101143836975, 0.19004550576210022), zoom=274.24974945443006, angles=(0.0, 0.0, 89.99999999999999), perspective=0.0, interactive=True), cursor=Cursor(position=(1.0, -5.425513676810831e-17, 0.9944846361386972, 0.1863991944562821), scaled=True, size=1, style=<CursorStyle.STANDARD: 'standard'>), dims=Dims(ndim=4, ndisplay=3, last_used=0, range=((0.0, 3.0, 1.0), (-0.4715520143508911, 1.4715520143508911, 1.0), (-0.7367839813232422, 1.9536460041999817, 1.0), (-0.6689090132713318, 2.0490000247955322, 1.0)), current_step=(2, 0, 1, 1), order=(0, 1, 2, 3), axis_labels=('0', '1', '1', '2')), grid=GridCanvas(stride=1, shape=(-1, -1), enabled=False), layers=[<Surface layer 'Surface' at 0x29de36b30>], scale_bar=ScaleBar(visible=False, colored=False, color=<class 'numpy.ndarray'> (4,) float32, ticks=True, position=<Position.BOTTOM_RIGHT: 'bottom_right'>, font_size=10.0, box=False, box_color=<class 'numpy.ndarray'> (4,) float32, unit=None), text_overlay=TextOverlay(visible=False, color=<class 'numpy.ndarray'> (4,) float32, font_size=10.0, position=<TextOverlayPosition.TOP_LEFT: 'top_left'>, text=''), overlays=Overlays(interaction_box=InteractionBox(points=None, show=False, show_handle=False, show_vertices=False, selection_box_drag=None, selection_box_final=None, transform_start=<napari.utils.transforms.transforms.Affine object at 0x288b03b50>, transform_drag=<napari.utils.transforms.transforms.Affine object at 0x288b03bb0>, transform_final=<napari.utils.transforms.transforms.Affine object at 0x288b03c10>, transform=<napari.utils.transforms.transforms.Affine object at 0x288b03c70>, allow_new_selection=True, selected_vertex=None)), help='', status='', tooltip=Tooltip(visible=False, text=''), theme='dark', title='napari', mouse_over_canvas=False, mouse_move_callbacks=[<function InteractionBoxMouseBindings.initialize_mouse_events.<locals>.mouse_move at 0x28da35240>], mouse_drag_callbacks=[<function InteractionBoxMouseBindings.initialize_mouse_events.<locals>.mouse_drag at 0x28da36200>], mouse_double_click_callbacks=[], mouse_wheel_callbacks=[<function dims_scroll at 0x177894af0>], _persisted_mouse_event={}, _mouse_drag_gen={}, _mouse_wheel_gen={}, keymap={'Shift': <function InteractionBoxMouseBindings.initialize_key_events.<locals>.hold_to_lock_aspect_ratio at 0x28da36050>, 'Control-Shift-R': <function InteractionBoxMouseBindings._reset_active_layer_affine at 0x28da80f70>, 'Control-Shift-A': <function InteractionBoxMouseBindings._transform_active_layer at 0x28da81ab0>})>
        (cb, event) = (<bound method ViewerModel._update_layers of Viewer(axes=Axes(visible=False, labels=True, colored=True, dashed=False, arrows=True), camera=Camera(center=(0.0, 0.10843101143836975, 0.19004550576210022), zoom=274.24974945443006, angles=(0.0, 0.0, 89.99999999999999), perspective=0.0, interactive=True), cursor=Cursor(position=(1.0, -5.425513676810831e-17, 0.9944846361386972, 0.1863991944562821), scaled=True, size=1, style=<CursorStyle.STANDARD: 'standard'>), dims=Dims(ndim=4, ndisplay=3, last_used=0, range=((0.0, 3.0, 1.0), (-0.4715520143508911, 1.4715520143508911, 1.0), (-0.7367839813232422, 1.9536460041999817, 1.0), (-0.6689090132713318, 2.0490000247955322, 1.0)), current_step=(2, 0, 1, 1), order=(0, 1, 2, 3), axis_labels=('0', '1', '1', '2')), grid=GridCanvas(stride=1, shape=(-1, -1), enabled=False), layers=[<Surface layer 'Surface' at 0x29de36b30>], scale_bar=ScaleBar(visible=False, colored=False, color=<class 'numpy.ndarray'> (4,) float32, ticks=True, position=<Position.BOTTOM_RIGHT: 'bottom_right'>, font_size=10.0, box=False, box_color=<class 'numpy.ndarray'> (4,) float32, unit=None), text_overlay=TextOverlay(visible=False, color=<class 'numpy.ndarray'> (4,) float32, font_size=10.0, position=<TextOverlayPosition.TOP_LEFT: 'top_left'>, text=''), overlays=Overlays(interaction_box=InteractionBox(points=None, show=False, show_handle=False, show_vertices=False, selection_box_drag=None, selection_box_final=None, transform_start=<napari.utils.transforms.transforms.Affine object at 0x288b03b50>, transform_drag=<napari.utils.transforms.transforms.Affine object at 0x288b03bb0>, transform_final=<napari.utils.transforms.transforms.Affine object at 0x288b03c10>, transform=<napari.utils.transforms.transforms.Affine object at 0x288b03c70>, allow_new_selection=True, selected_vertex=None)), help='', status='', tooltip=Tooltip(visible=False, text=''), theme='dark', title='napari', mouse_over_canvas=False, mouse_move_callbacks=[<function InteractionBoxMouseBindings.initialize_mouse_events.<locals>.mouse_move at 0x28da35240>], mouse_drag_callbacks=[<function InteractionBoxMouseBindings.initialize_mouse_events.<locals>.mouse_drag at 0x28da36200>], mouse_double_click_callbacks=[], mouse_wheel_callbacks=[<function dims_scroll at 0x177894af0>], _persisted_mouse_event={}, _mouse_drag_gen={}, _mouse_wheel_gen={}, keymap={'Shift': <function InteractionBoxMouseBindings.initialize_key_events.<locals>.hold_to_lock_aspect_ratio at 0x28da36050>, 'Control-Shift-R': <function InteractionBoxMouseBindings._reset_active_layer_affine at 0x28da80f70>, 'Control-Shift-A': <function InteractionBoxMouseBindings._transform_active_layer at 0x28da81ab0>})>, None)
    795     self.ignore_callback_errors,
    796     self.print_callback_errors,
    797     self,
    798     cb_event=(cb, event),
    799 )

File ~/mf/envs/napari-417/lib/python3.10/site-packages/napari/utils/events/event.py:783, in EventEmitter._invoke_callback(self=<napari.utils.events.event.EventEmitter object>, cb=<bound method ViewerModel._update_layers of View...ndings._transform_active_layer at 0x28da81ab0>})>, event=None)
    781         cb(event)
    782     else:
--> 783         cb()
        cb = <bound method ViewerModel._update_layers of Viewer(axes=Axes(visible=False, labels=True, colored=True, dashed=False, arrows=True), camera=Camera(center=(0.0, 0.10843101143836975, 0.19004550576210022), zoom=274.24974945443006, angles=(0.0, 0.0, 89.99999999999999), perspective=0.0, interactive=True), cursor=Cursor(position=(1.0, -5.425513676810831e-17, 0.9944846361386972, 0.1863991944562821), scaled=True, size=1, style=<CursorStyle.STANDARD: 'standard'>), dims=Dims(ndim=4, ndisplay=3, last_used=0, range=((0.0, 3.0, 1.0), (-0.4715520143508911, 1.4715520143508911, 1.0), (-0.7367839813232422, 1.9536460041999817, 1.0), (-0.6689090132713318, 2.0490000247955322, 1.0)), current_step=(2, 0, 1, 1), order=(0, 1, 2, 3), axis_labels=('0', '1', '1', '2')), grid=GridCanvas(stride=1, shape=(-1, -1), enabled=False), layers=[<Surface layer 'Surface' at 0x29de36b30>], scale_bar=ScaleBar(visible=False, colored=False, color=<class 'numpy.ndarray'> (4,) float32, ticks=True, position=<Position.BOTTOM_RIGHT: 'bottom_right'>, font_size=10.0, box=False, box_color=<class 'numpy.ndarray'> (4,) float32, unit=None), text_overlay=TextOverlay(visible=False, color=<class 'numpy.ndarray'> (4,) float32, font_size=10.0, position=<TextOverlayPosition.TOP_LEFT: 'top_left'>, text=''), overlays=Overlays(interaction_box=InteractionBox(points=None, show=False, show_handle=False, show_vertices=False, selection_box_drag=None, selection_box_final=None, transform_start=<napari.utils.transforms.transforms.Affine object at 0x288b03b50>, transform_drag=<napari.utils.transforms.transforms.Affine object at 0x288b03bb0>, transform_final=<napari.utils.transforms.transforms.Affine object at 0x288b03c10>, transform=<napari.utils.transforms.transforms.Affine object at 0x288b03c70>, allow_new_selection=True, selected_vertex=None)), help='', status='', tooltip=Tooltip(visible=False, text=''), theme='dark', title='napari', mouse_over_canvas=False, mouse_move_callbacks=[<function InteractionBoxMouseBindings.initialize_mouse_events.<locals>.mouse_move at 0x28da35240>], mouse_drag_callbacks=[<function InteractionBoxMouseBindings.initialize_mouse_events.<locals>.mouse_drag at 0x28da36200>], mouse_double_click_callbacks=[], mouse_wheel_callbacks=[<function dims_scroll at 0x177894af0>], _persisted_mouse_event={}, _mouse_drag_gen={}, _mouse_wheel_gen={}, keymap={'Shift': <function InteractionBoxMouseBindings.initialize_key_events.<locals>.hold_to_lock_aspect_ratio at 0x28da36050>, 'Control-Shift-R': <function InteractionBoxMouseBindings._reset_active_layer_affine at 0x28da80f70>, 'Control-Shift-A': <function InteractionBoxMouseBindings._transform_active_layer at 0x28da81ab0>})>
    784 except Exception as e:
    785     # dead Qt object with living python pointer. not importing Qt
    786     # here... but this error is consistent across backends
    787     if (
    788         isinstance(e, RuntimeError)
    789         and 'C++' in str(e)
    790         and str(e).endswith(('has been deleted', 'already deleted.'))
    791     ):

File ~/mf/envs/napari-417/lib/python3.10/site-packages/napari/components/viewer_model.py:348, in ViewerModel._update_layers(self=Viewer(axes=Axes(visible=False, labels=True, col...indings._transform_active_layer at 0x28da81ab0>}), layers=[<Surface layer 'Surface' at 0x29de36b30>])
    346 layers = layers or self.layers
    347 for layer in layers:
--> 348     layer._slice_dims(
        layer = <Surface layer 'Surface' at 0x29de36b30>
        self.dims.order = (0, 1, 2, 3)
        self = Viewer(axes=Axes(visible=False, labels=True, colored=True, dashed=False, arrows=True), camera=Camera(center=(0.0, 0.10843101143836975, 0.19004550576210022), zoom=274.24974945443006, angles=(0.0, 0.0, 89.99999999999999), perspective=0.0, interactive=True), cursor=Cursor(position=(1.0, -5.425513676810831e-17, 0.9944846361386972, 0.1863991944562821), scaled=True, size=1, style=<CursorStyle.STANDARD: 'standard'>), dims=Dims(ndim=4, ndisplay=3, last_used=0, range=((0.0, 3.0, 1.0), (-0.4715520143508911, 1.4715520143508911, 1.0), (-0.7367839813232422, 1.9536460041999817, 1.0), (-0.6689090132713318, 2.0490000247955322, 1.0)), current_step=(2, 0, 1, 1), order=(0, 1, 2, 3), axis_labels=('0', '1', '1', '2')), grid=GridCanvas(stride=1, shape=(-1, -1), enabled=False), layers=[<Surface layer 'Surface' at 0x29de36b30>], scale_bar=ScaleBar(visible=False, colored=False, color=<class 'numpy.ndarray'> (4,) float32, ticks=True, position=<Position.BOTTOM_RIGHT: 'bottom_right'>, font_size=10.0, box=False, box_color=<class 'numpy.ndarray'> (4,) float32, unit=None), text_overlay=TextOverlay(visible=False, color=<class 'numpy.ndarray'> (4,) float32, font_size=10.0, position=<TextOverlayPosition.TOP_LEFT: 'top_left'>, text=''), overlays=Overlays(interaction_box=InteractionBox(points=None, show=False, show_handle=False, show_vertices=False, selection_box_drag=None, selection_box_final=None, transform_start=<napari.utils.transforms.transforms.Affine object at 0x288b03b50>, transform_drag=<napari.utils.transforms.transforms.Affine object at 0x288b03bb0>, transform_final=<napari.utils.transforms.transforms.Affine object at 0x288b03c10>, transform=<napari.utils.transforms.transforms.Affine object at 0x288b03c70>, allow_new_selection=True, selected_vertex=None)), help='', status='', tooltip=Tooltip(visible=False, text=''), theme='dark', title='napari', mouse_over_canvas=False, mouse_move_callbacks=[<function InteractionBoxMouseBindings.initialize_mouse_events.<locals>.mouse_move at 0x28da35240>], mouse_drag_callbacks=[<function InteractionBoxMouseBindings.initialize_mouse_events.<locals>.mouse_drag at 0x28da36200>], mouse_double_click_callbacks=[], mouse_wheel_callbacks=[<function dims_scroll at 0x177894af0>], _persisted_mouse_event={}, _mouse_drag_gen={}, _mouse_wheel_gen={}, keymap={'Shift': <function InteractionBoxMouseBindings.initialize_key_events.<locals>.hold_to_lock_aspect_ratio at 0x28da36050>, 'Control-Shift-R': <function InteractionBoxMouseBindings._reset_active_layer_affine at 0x28da80f70>, 'Control-Shift-A': <function InteractionBoxMouseBindings._transform_active_layer at 0x28da81ab0>})
        self.dims.ndisplay = 3
        self.dims = Dims(ndim=4, ndisplay=3, last_used=0, range=((0.0, 3.0, 1.0), (-0.4715520143508911, 1.4715520143508911, 1.0), (-0.7367839813232422, 1.9536460041999817, 1.0), (-0.6689090132713318, 2.0490000247955322, 1.0)), current_step=(2, 0, 1, 1), order=(0, 1, 2, 3), axis_labels=('0', '1', '1', '2'))
    349         self.dims.point, self.dims.ndisplay, self.dims.order
    350     )
    351 position = list(self.cursor.position)
    352 for ind in self.dims.order[: -self.dims.ndisplay]:

File ~/mf/envs/napari-417/lib/python3.10/site-packages/napari/layers/base/base.py:1020, in Layer._slice_dims(self=<Surface layer 'Surface'>, point=[2.0, -0.4715520143508911, 0.2632160186767578, 0.3310909867286682], ndisplay=3, order=[0, 1, 2, 3])
   1018 # Update the point values
   1019 self._dims_point = point[offset:]
-> 1020 self._update_dims()
        self = <Surface layer 'Surface' at 0x29de36b30>
   1021 self._set_editable()

File ~/mf/envs/napari-417/lib/python3.10/site-packages/napari/layers/base/base.py:705, in Layer._update_dims(self=<Surface layer 'Surface'>, event=None)
    702 if 'extent' in self.__dict__:
    703     del self.extent
--> 705 self.refresh()
        self = <Surface layer 'Surface' at 0x29de36b30>

File ~/mf/envs/napari-417/lib/python3.10/site-packages/napari/layers/base/base.py:1209, in Layer.refresh(self=<Surface layer 'Surface'>, event=None)
   1207 """Refresh all layer data based on current view slice."""
   1208 if self.visible:
-> 1209     self.set_view_slice()
        self = <Surface layer 'Surface' at 0x29de36b30>
   1210     self.events.set_data()  # refresh is called in _update_dims which means that extent cache is invalidated. Then, base on this event extent cache in layerlist is invalidated.
   1211     self._update_thumbnail()

File ~/mf/envs/napari-417/lib/python3.10/site-packages/napari/layers/base/base.py:959, in Layer.set_view_slice(self=<Surface layer 'Surface'>)
    957 def set_view_slice(self):
    958     with self.dask_optimized_slicing():
--> 959         self._set_view_slice()
        self = <Surface layer 'Surface' at 0x29de36b30>

File ~/mf/envs/napari-417/lib/python3.10/site-packages/napari/layers/surface/surface.py:385, in Surface._set_view_slice(self=<Surface layer 'Surface'>)
    382 if values_ndim > 0:
    383     # Get indices for axes corresponding to values dimensions
    384     values_indices = self._slice_indices[:-vertex_ndim]
--> 385     values = self.vertex_values[values_indices]
        values_indices = (2,)
        self = <Surface layer 'Surface' at 0x29de36b30>
    386     if values.ndim > 1:
    387         warnings.warn(
    388             trans._(
    389                 "Assigning multiple values per vertex after slicing is not allowed. All dimensions corresponding to vertex_values must be non-displayed dimensions. Data will not be visible.",
    390                 deferred=True,
    391             )
    392         )

IndexError: index 2 is out of bounds for axis 0 with size 2
```
</details>

## To Reproduce

Run this code in the napari console, then click the second dims slider all the way to the right.
```
import numpy as np
from vispy.io import load_data_file, read_mesh
mesh_path = load_data_file('spot/spot.obj.gz')
vertices, faces, normals, _ = read_mesh(mesh_path)
values = np.ones((2, len(vertices)))
viewer.add_surface((vertices * 10, faces, values))
viewer.dims.ndisplay = 3
```

## Expected behavior

I expect the dims slider to always return values within range when slicing a given axis.

## What I think is happening
I think the issue is that extent is calculated differently for `_array_like` layers (Image), where the Surface layer is kind of a mix of `_array_like` dims on the extra `vertex_values` but non-`_array_like` for `vertices`. There is special-case code for this in both `napari/layers/base.py` (calculating extent) and in `napari/components/layerlist.py` (calculating ranges for Dims).

A simple fix looks like this, but I'm not sure it's correct with respect to how the extent then maps to the world extent:
```diff
    @property
    def _extent_data(self) -> np.ndarray:
        """Extent of layer in data coordinates.

        Returns
        -------
        extent_data : array, shape (2, D)
        """
        if len(self.vertices) == 0:
            extrema = np.full((2, self.ndim), np.nan)
        else:
            maxs = np.max(self.vertices, axis=0)
            mins = np.min(self.vertices, axis=0)

            # The full dimensionality and shape of the layer is determined by
            # the number of additional vertex value dimensions and the
            # dimensionality of the vertices themselves
            if self.vertex_values.ndim > 1:
                mins = [0] * (self.vertex_values.ndim - 1) + list(mins)
-                maxs = list(self.vertex_values.shape[:-1]) + list(maxs)
+                maxs = [n - 1 for n in self.vertex_values.shape[:-1]] + list(maxs)
            extrema = np.vstack([mins, maxs])
        return extrema

```

## Environment

napari: 0.4.17 (also observed on `main`)
Platform: macOS-13.2.1-arm64-arm-64bit
System: MacOS 13.2.1
Python: 3.10.9 | packaged by conda-forge | (main, Feb 2 2023, 20:26:08) [Clang 14.0.6 ]
Qt: 5.15.6
PyQt5: 5.15.7
NumPy: 1.24.2
SciPy: 1.10.1
Dask: 2023.3.1
VisPy: 0.11.0
magicgui: 0.7.2
superqt: unknown
in-n-out: 0.1.7
app-model: 0.1.2
npe2: 0.6.2

OpenGL:
- GL version: 2.1 Metal - 83
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 2560x1440, scale 1.0



Discussion:
I did a small test with a simple mesh and image to test centering, and it's correct (i.e: 0,0 is centered on image pixels as with points):

```py
import napari
import numpy as np
from icosphere import icosphere  # simple mesh for testing

v = napari.Viewer()
il = v.add_image(np.random.rand(5, 5, 5))

surf = icosphere(0)
vals = np.random.rand(3, len(surf[0]))

v.add_surface((*surf, vals))
```

This gives 3 steps on the slider, and works as intended. The code also makes sense to me. Do you want to open a pr with this change? :)

Resolving patch:
diff --git a/napari/layers/surface/surface.py b/napari/layers/surface/surface.py
--- a/napari/layers/surface/surface.py
+++ b/napari/layers/surface/surface.py
@@ -348,7 +348,9 @@ def _extent_data(self) -> np.ndarray:
             # dimensionality of the vertices themselves
             if self.vertex_values.ndim > 1:
                 mins = [0] * (self.vertex_values.ndim - 1) + list(mins)
-                maxs = list(self.vertex_values.shape[:-1]) + list(maxs)
+                maxs = [n - 1 for n in self.vertex_values.shape[:-1]] + list(
+                    maxs
+                )
             extrema = np.vstack([mins, maxs])
         return extrema
 


Test patch:
diff --git a/napari/layers/surface/_tests/test_surface.py b/napari/layers/surface/_tests/test_surface.py
--- a/napari/layers/surface/_tests/test_surface.py
+++ b/napari/layers/surface/_tests/test_surface.py
@@ -88,7 +88,7 @@ def test_random_3D_timeseries_surface():
     assert np.all([np.all(ld == d) for ld, d in zip(layer.data, data)])
     assert layer._data_view.shape[1] == 2
     assert layer._view_vertex_values.ndim == 1
-    assert layer.extent.data[1][0] == 22
+    assert layer.extent.data[1][0] == 21
 
     layer._slice_dims(ndisplay=3)
     assert layer._data_view.shape[1] == 3
@@ -113,8 +113,8 @@ def test_random_3D_multitimeseries_surface():
     assert np.all([np.all(ld == d) for ld, d in zip(layer.data, data)])
     assert layer._data_view.shape[1] == 2
     assert layer._view_vertex_values.ndim == 1
-    assert layer.extent.data[1][0] == 16
-    assert layer.extent.data[1][1] == 22
+    assert layer.extent.data[1][0] == 15
+    assert layer.extent.data[1][1] == 21
 
     layer._slice_dims(ndisplay=3)
     assert layer._data_view.shape[1] == 3