Repository: napari/napari (Python)

Issue:
[Shapes] ValueError spam on mouseover if 4D.
### 🐛 Bug Report

If you have a 4D image layer and create a shapes layer, then draw a shape you get a spam of ValueErrors when mousing the layer:
```
ValueError                                Traceback (most recent call last)
File ~/micromamba/envs/napari-omero-060/lib/python3.12/site-packages/napari/_qt/threads/status_checker.py:126, in StatusChecker.calculate_status(self=<napari._qt.threads.status_checker.StatusChecker object>)
    122     return
    124 try:
    125     # Calculate the status change from cursor's movement
--> 126     res = viewer._calc_status_from_cursor()
        viewer = Viewer(camera=Camera(center=(0.0, 127.5, 127.5), zoom=2.195947265625, angles=(0.0, 0.0, 90.0), perspective=0.0, mouse_pan=False, mouse_zoom=True, orientation=(<DepthAxisOrientation.TOWARDS: 'towards'>, <VerticalAxisOrientation.DOWN: 'down'>, <HorizontalAxisOrientation.RIGHT: 'right'>)), cursor=Cursor(position=(0.0, 0.0, 159.6046030224299, -24.82606338221611), scaled=True, style=<CursorStyle.CROSS: 'cross'>, size=1.0), dims=Dims(ndim=4, ndisplay=2, order=(0, 1, 2, 3), axis_labels=('0', '1', '2', '3'), rollable=(True, True, True, True), range=(RangeTuple(start=0.0, stop=0.0, step=1.0), RangeTuple(start=0.0, stop=0.0, step=1.0), RangeTuple(start=0.0, stop=255.0, step=1.0), RangeTuple(start=0.0, stop=255.0, step=1.0)), margin_left=(0.0, 0.0, 0.0, 0.0), margin_right=(0.0, 0.0, 0.0, 0.0), point=(0.0, 0.0, 127.0, 127.0), last_used=0), grid=GridCanvas(stride=1, shape=(-1, -1), enabled=False, spacing=0.0), layers=[<Image layer 'data' at 0x107e0e2d0>, <Shapes layer 'Shapes' at 0x15c2b4050>], help='use <6> for move camera, use <7> for transform, use <E> for add ellipses, use <L> for add lines, use <Shift+L> for add polylines, use <T> for add path, use <P> for add polygons, use <Shift+P> for add polygons lasso, use <4> for select vertices, use <5> for select shapes, use <2> for insert vertex, use <1> for remove vertex', status={'layer_name': 'Shapes', 'layer_base': 'Shapes', 'source_type': '', 'plugin': '', 'coordinates': ' [0 0 125 125]: 0, 4', 'coords': ' [0 0 125 125]', 'value': '0, 4'}, tooltip=Tooltip(visible=False, text=''), theme='dark', title='napari', mouse_over_canvas=True, mouse_move_callbacks=[], mouse_drag_callbacks=[], mouse_double_click_callbacks=[<function double_click_to_zoom at 0x1301e4cc0>], mouse_wheel_callbacks=[<function dims_scroll at 0x1301e4c20>], _persisted_mouse_event={}, _mouse_drag_gen={}, _mouse_wheel_gen={}, _keymap={})    127 except Exception as e:  # pragma: no cover # noqa: BLE001
    128     # Our codebase is not threadsafe. It is possible that an
    129     # ViewerModel or Layer state is changed while we are trying to
   (...)    132     # from crashing the thread. The exception is logged
    133     # and a notification is sent.
    134     notification_manager.dispatch(Notification.from_exception(e))

File ~/micromamba/envs/napari-omero-060/lib/python3.12/site-packages/napari/components/viewer_model.py:739, in ViewerModel._calc_status_from_cursor(self=Viewer(camera=Camera(center=(0.0, 127.5, 127.5),...use_drag_gen={}, _mouse_wheel_gen={}, _keymap={}))
    736 # If there is an active layer and a single selection, calculate status using "the classic way".
    737 # Then return the status and the tooltip.
    738 if active is not None and active._loaded and len(selection) < 2:
--> 739     status = active.get_status(
        active = <Shapes layer 'Shapes' at 0x15c2b4050>
        self = Viewer(camera=Camera(center=(0.0, 127.5, 127.5), zoom=2.195947265625, angles=(0.0, 0.0, 90.0), perspective=0.0, mouse_pan=False, mouse_zoom=True, orientation=(<DepthAxisOrientation.TOWARDS: 'towards'>, <VerticalAxisOrientation.DOWN: 'down'>, <HorizontalAxisOrientation.RIGHT: 'right'>)), cursor=Cursor(position=(0.0, 0.0, 159.6046030224299, -24.82606338221611), scaled=True, style=<CursorStyle.CROSS: 'cross'>, size=1.0), dims=Dims(ndim=4, ndisplay=2, order=(0, 1, 2, 3), axis_labels=('0', '1', '2', '3'), rollable=(True, True, True, True), range=(RangeTuple(start=0.0, stop=0.0, step=1.0), RangeTuple(start=0.0, stop=0.0, step=1.0), RangeTuple(start=0.0, stop=255.0, step=1.0), RangeTuple(start=0.0, stop=255.0, step=1.0)), margin_left=(0.0, 0.0, 0.0, 0.0), margin_right=(0.0, 0.0, 0.0, 0.0), point=(0.0, 0.0, 127.0, 127.0), last_used=0), grid=GridCanvas(stride=1, shape=(-1, -1), enabled=False, spacing=0.0), layers=[<Image layer 'data' at 0x107e0e2d0>, <Shapes layer 'Shapes' at 0x15c2b4050>], help='use <6> for move camera, use <7> for transform, use <E> for add ellipses, use <L> for add lines, use <Shift+L> for add polylines, use <T> for add path, use <P> for add polygons, use <Shift+P> for add polygons lasso, use <4> for select vertices, use <5> for select shapes, use <2> for insert vertex, use <1> for remove vertex', status={'layer_name': 'Shapes', 'layer_base': 'Shapes', 'source_type': '', 'plugin': '', 'coordinates': ' [0 0 125 125]: 0, 4', 'coords': ' [0 0 125 125]', 'value': '0, 4'}, tooltip=Tooltip(visible=False, text=''), theme='dark', title='napari', mouse_over_canvas=True, mouse_move_callbacks=[], mouse_drag_callbacks=[], mouse_double_click_callbacks=[<function double_click_to_zoom at 0x1301e4cc0>], mouse_wheel_callbacks=[<function dims_scroll at 0x1301e4c20>], _persisted_mouse_event={}, _mouse_drag_gen={}, _mouse_wheel_gen={}, _keymap={})
        self.cursor.position = (0.0, 0.0, 159.6046030224299, -24.82606338221611)
        self.cursor = Cursor(position=(0.0, 0.0, 159.6046030224299, -24.82606338221611), scaled=True, style=<CursorStyle.CROSS: 'cross'>, size=1.0)
        self.cursor._view_direction = None
        self.dims = Dims(ndim=4, ndisplay=2, order=(0, 1, 2, 3), axis_labels=('0', '1', '2', '3'), rollable=(True, True, True, True), range=(RangeTuple(start=0.0, stop=0.0, step=1.0), RangeTuple(start=0.0, stop=0.0, step=1.0), RangeTuple(start=0.0, stop=255.0, step=1.0), RangeTuple(start=0.0, stop=255.0, step=1.0)), margin_left=(0.0, 0.0, 0.0, 0.0), margin_right=(0.0, 0.0, 0.0, 0.0), point=(0.0, 0.0, 127.0, 127.0), last_used=0)    740         self.cursor.position,
    741         view_direction=self.cursor._view_direction,
    742         dims_displayed=list(self.dims.displayed),
    743         world=True,
    744     )
    745     return status, tooltip_text
    747 # Otherwise, return the layer status of multiple selected layers
    748 # or gridded layers as well as the tooltip.

File ~/micromamba/envs/napari-omero-060/lib/python3.12/site-packages/napari/layers/base/base.py:2181, in Layer.get_status(self=<Shapes layer 'Shapes'>, position=array([  0.     ,   0.     , 159.6046 , -24.82606]), view_direction=None, dims_displayed=[2, 3], world=True, value=None)
   2179 if position is not None:
   2180     position = np.asarray(position)
-> 2181     value = self.get_value(
        value = None
        self = <Shapes layer 'Shapes' at 0x15c2b4050>
        position = array([  0.     ,   0.     , 159.6046 , -24.82606])
        view_direction = None
        dims_displayed = [2, 3]
        world = True   2182         position,
   2183         view_direction=view_direction,
   2184         dims_displayed=dims_displayed,
   2185         world=world,
   2186     )
   2187     coords_str, value_str = generate_layer_status_strings(
   2188         # position may be higher-dimensional due to other
   2189         # layers in the viewer, but self._translate_grid already
   (...)   2194         value,
   2195     )
   2196 else:

File ~/micromamba/envs/napari-omero-060/lib/python3.12/site-packages/napari/layers/base/base.py:1405, in Layer.get_value(self=<Shapes layer 'Shapes'>, position=array([  0.     ,   0.     , 159.6046 , -24.82606]), view_direction=None, dims_displayed=[2, 3], world=True)
   1399             value = self._get_value_3d(
   1400                 start_point=start_point,
   1401                 end_point=end_point,
   1402                 dims_displayed=dims_displayed,
   1403             )
   1404     else:
-> 1405         value = self._get_value(position)
        position = array([  0.     ,   0.     , 159.6046 , -24.82606])
        self = <Shapes layer 'Shapes' at 0x15c2b4050>   1407 else:
   1408     value = None

File ~/micromamba/envs/napari-omero-060/lib/python3.12/site-packages/napari/layers/shapes/shapes.py:2882, in Shapes._get_value(self=<Shapes layer 'Shapes'>, position=array([  0.     ,   0.     , 159.6046 , -24.82606]))
   2878             value = (shape, index - idx[shape_in_list])
   2880 if value is None:
   2881     # Check if mouse inside shape
-> 2882     shape = self._data_view.inside(coord)
        coord = [159.6046030224299, -24.82606338221611]
        self = <Shapes layer 'Shapes' at 0x15c2b4050>
        self._data_view = <napari.layers.shapes._shape_list.ShapeList object at 0x15f2b9220>   2883     value = (shape, None)
   2885 return value

File ~/micromamba/envs/napari-omero-060/lib/python3.12/site-packages/napari/layers/shapes/_shape_list.py:1396, in ShapeList.inside(self=<napari.layers.shapes._shape_list.ShapeList object>, coord=[159.6046030224299, -24.82606338221611])
   1394 if not self.shapes:
   1395     return None
-> 1396 bounding_boxes = self._bounding_boxes
        self = <napari.layers.shapes._shape_list.ShapeList object at 0x15f2b9220>   1397 in_bbox = np.all(
   1398     (bounding_boxes[0] <= coord) * (bounding_boxes[1] >= coord),
   1399     axis=1,
   1400 )
   1401 inside_indices = np.flatnonzero(in_bbox)

File ~/micromamba/envs/napari-omero-060/lib/python3.12/functools.py:998, in cached_property.__get__(self=<functools.cached_property object>, instance=<napari.layers.shapes._shape_list.ShapeList object>, owner=<class 'napari.layers.shapes._shape_list.ShapeList'>)
    996 val = cache.get(self.attrname, _NOT_FOUND)
    997 if val is _NOT_FOUND:
--> 998     val = self.func(instance)
        val = <object object at 0x104c382e0>
        instance = <napari.layers.shapes._shape_list.ShapeList object at 0x15f2b9220>
        self = <functools.cached_property object at 0x12ee93bc0>
        self.func = <function ShapeList._bounding_boxes at 0x12ee9f420>    999     try:
   1000         cache[self.attrname] = val

File ~/micromamba/envs/napari-omero-060/lib/python3.12/site-packages/napari/layers/shapes/_shape_list.py:1374, in ShapeList._bounding_boxes(self=<napari.layers.shapes._shape_list.ShapeList object>)
   1372 @cached_property
   1373 def _bounding_boxes(self):
-> 1374     data = np.array([s[1].bounding_box for s in self._visible_shapes])
        np = <module 'numpy' from '/Users/sobolp/micromamba/envs/napari-omero-060/lib/python3.12/site-packages/numpy/__init__.py'>
        self = <napari.layers.shapes._shape_list.ShapeList object at 0x15f2b9220>   1375     if data.size == 0:
   1376         return np.empty((0, self.ndisplay)), np.empty((0, self.ndisplay))

File ~/micromamba/envs/napari-omero-060/lib/python3.12/functools.py:998, in cached_property.__get__(self=<functools.cached_property object>, instance=<napari.layers.shapes._shape_list.ShapeList object>, owner=<class 'napari.layers.shapes._shape_list.ShapeList'>)
    996 val = cache.get(self.attrname, _NOT_FOUND)
    997 if val is _NOT_FOUND:
--> 998     val = self.func(instance)
        val = <object object at 0x104c382e0>
        instance = <napari.layers.shapes._shape_list.ShapeList object at 0x15f2b9220>
        self = <functools.cached_property object at 0x12ee93b60>
        self.func = <function ShapeList._visible_shapes at 0x12ee9f380>    999     try:
   1000         cache[self.attrname] = val

File ~/micromamba/envs/napari-omero-060/lib/python3.12/site-packages/napari/layers/shapes/_shape_list.py:1368, in ShapeList._visible_shapes(self=<napari.layers.shapes._shape_list.ShapeList object>)
   1363 slice_key = self.slice_key
   1364 if len(slice_key):
   1365     return [
   1366         (i, s)
   1367         for i, s in enumerate(self.shapes)
-> 1368         if s.slice_key[0] <= slice_key <= s.slice_key[1]
        self = <napari.layers.shapes._shape_list.ShapeList object at 0x15f2b9220>
        self.shapes = [<napari.layers.shapes._shapes_models.rectangle.Rectangle object at 0x15f44d0a0>]
        slice_key = [0.0, 0.0]   1369     ]
   1370 return list(enumerate(self.shapes))

ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
```

Crucially this happens with Images like (1, 1, y, x) such as those from the OME environment (OMERO, ome-zarr) and makes interaction with the Shapes layer really annoying, so it's pretty bad.

### 💡 Steps to Reproduce

Run:
```
import napari
import numpy as np

data = np.random.random([1, 1, 256, 256])
print(data.shape)

viewer, img_layer = napari.imshow(data)
napari.run()
```
- Use viewer button to add Shapes layer
- draw a rectangle
- mouse around

### 💡 Expected Behavior

_No response_

### 🌎 Environment

❯ napari --info                                                             
napari: 0.5.7.dev125+g8f6a23f9
Platform: macOS-14.7.5-arm64-arm-64bit-Mach-O
System: MacOS 14.7.5
Python: 3.13.2 | packaged by conda-forge | (main, Feb 17 2025, 14:02:48) [Clang 18.1.8 ]
Qt: 6.8.2
PyQt6: 6.8.1
NumPy: 2.1.3
SciPy: 1.15.2
Dask: 2025.2.0
VisPy: 0.14.3
magicgui: 0.10.0
superqt: 0.7.1
in-n-out: 0.2.1
app-model: 0.3.1
psygnal: 0.12.0
npe2: 0.7.8
pydantic: 2.10.6

OpenGL:
  - PyOpenGL: 3.1.9
  - GL version:  2.1 Metal - 88.1
  - MAX_TEXTURE_SIZE: 16384
  - GL_MAX_3D_TEXTURE_SIZE: 2048

Screens:
  - screen 1: resolution 2056x1329, scale 2.0
  - screen 2: resolution 2560x1440, scale 1.0

Optional:
  - numba: 0.61.0
  - triangle not installed
  - napari-plugin-manager: 0.1.5
  - bermuda: 0.1.4
  - PartSegCore: 0.15.11

Experimental Settings:
  - Async: True
  - Autoswap buffers: False
  - Triangulation backend: Fastest available

Settings path:
  - /Users/sobolp/Library/Application Support/napari/napari-dev_36963bf0846bfe99dcf4f566a2cc780b6a723afa/settings.yaml
Plugins:
  - napari: 0.5.7.dev104+g4f65f851 (81 contributions)
  - napari-console: 0.1.3 (0 contributions)
  - napari-skimage: 0.4.0 (61 contributions)
  - napari-svg: 0.2.1 (2 contributions)

### 💡 Additional Context

Bisect points to: 681132f908e57475d3f00d7d3482b32fcf1e13f2
#7459 
The test case in that PR was 3D, which works fine. 4D specifically is needed.


Discussion: As noted by the bisect, this was released in 0.5.6, but it's pretty bad 😬 
Suprised I hadn't hit this before, but I checked and my main omero env is 0.5.5.

We should endeavor to fix this and push a release asap.



Fix (patch):
diff --git a/napari/layers/shapes/_shape_list.py b/napari/layers/shapes/_shape_list.py
index 772d099b13d..25c66e95491 100644
--- a/napari/layers/shapes/_shape_list.py
+++ b/napari/layers/shapes/_shape_list.py
@@ -1365,7 +1365,10 @@ def _visible_shapes(self):
             return [
                 (i, s)
                 for i, s in enumerate(self.shapes)
-                if s.slice_key[0] <= slice_key <= s.slice_key[1]
+                if (
+                    np.all(s.slice_key[0] <= slice_key)
+                    and np.all(slice_key <= s.slice_key[1])
+                )
             ]
         return list(enumerate(self.shapes))