Repository: napari/napari

Issue:
Paint not showing until 3d mode activated
## 🐛 Bug

After updating to 0.37 (dev56) the paint tool isn't working. Paint doesn't show up when painting onto an image volume (99,256,256). However, if the '3d mode' (number of displayed dimensions) is activated, the paint is there.

## To Reproduce

Steps to reproduce the behavior:

1. Run examples/nD_labels.py
2. Select label layer, select paint brush, paint. Try to switch number of displayed dimensions.


## Expected behavior

The paint should appear under the brush as it is painted, with the expected paint colour and brush size.

## Environment

napari: 0.3.7.dev56+g4e954d3
Platform: Windows-10-10.0.17763-SP0
Python: 3.7.6 | packaged by conda-forge | (default, Jun 1 2020, 18:11:50) [MSC v.1916 64 bit (AMD64)]
Qt: 5.12.5
PyQt5: 5.12.3
NumPy: 1.18.5
SciPy: 1.4.1
Dask: 2.18.1
VisPy: 0.6.4

GL version: 4.4.0 - Build 21.20.16.4574
MAX_TEXTURE_SIZE: 16384

Plugins:
- napari-plugin-engine: 0.1.6
- svg: 0.1.4
 - Any other relevant information:




Discussion:
I can reproduce this behaviour in master. Dang! Thanks for reporting @penningav! This is a bad regression so I hope we can fix it before 0.3.8!
Not a problem, glad you guys are looking at it. That would be great. Cheers
Looks like this is caused by #1607, based on git bisect.
Yikes! I should be able to look into this tomorrow, thanks for reporting!! Sounds like it's not in the `0.3.7` release itself, just the latest version on master.
Ok, I looked into this a little more. It is caused by these lines in #1607 https://github.com/napari/napari/pull/1607/files#diff-3ff6125a4ff917291bee7185ab645021R91-R94, 

```python
        if self.current_key is not None and self.current_key == request.key:
            # We are already showing this slice, or its being loaded
            # asynchronously.
            return None
```
Where we no longer update the image if the key hasn't changed. Unfortunately this update scheme doesn't take into consideration that the data itself may have changed. There maybe other ways to trigger the update in this case, and I'll leave it to @pwinston to suggest a fix and a test to prevent this from happening again.

Resolving patch:
diff --git a/napari/layers/image/image.py b/napari/layers/image/image.py
--- a/napari/layers/image/image.py
+++ b/napari/layers/image/image.py
@@ -219,11 +219,7 @@ def __init__(
             self._thumbnail_level = 0
         self.corner_pixels[1] = self.level_shapes[self._data_level]
 
-        # Initialize the current slice to an empty image.
-        self._slice = ImageSlice(
-            self._get_empty_image(), self._raw_to_displayed, self.rgb
-        )
-        self._empty = True
+        self._new_empty_slice()
 
         # Set contrast_limits and colormaps
         self._gamma = gamma
@@ -250,6 +246,14 @@ def __init__(
         # Trigger generation of view slice and thumbnail
         self._update_dims()
 
+    def _new_empty_slice(self):
+        """Initialize the current slice to an empty image.
+        """
+        self._slice = ImageSlice(
+            self._get_empty_image(), self._raw_to_displayed, self.rgb
+        )
+        self._empty = True
+
     def _get_empty_image(self):
         """Get empty image to use as the default before data is loaded.
         """
@@ -495,6 +499,7 @@ def _raw_to_displayed(self, raw):
 
     def _set_view_slice(self):
         """Set the view given the indices to slice with."""
+        self._new_empty_slice()
         not_disp = self.dims.not_displayed
 
         # Check if requested slice outside of data range
@@ -511,9 +516,6 @@ def _set_view_slice(self):
                 [extent[1, ax] - 1 for ax in not_disp],
             )
         ):
-            self._slice.image.raw = self._get_empty_image()
-            self._slice.thumbnail.raw = self._get_empty_image()
-            self._empty = True
             return
         self._empty = False
 


Test patch:
diff --git a/napari/_tests/test_with_screenshot.py b/napari/_tests/test_with_screenshot.py
--- a/napari/_tests/test_with_screenshot.py
+++ b/napari/_tests/test_with_screenshot.py
@@ -1,9 +1,17 @@
+import collections
 import os
 import sys
 
 import numpy as np
 import pytest
 
+from napari.utils.interactions import (
+    ReadOnlyWrapper,
+    mouse_move_callbacks,
+    mouse_press_callbacks,
+    mouse_release_callbacks,
+)
+
 
 @pytest.mark.skipif(
     sys.platform.startswith('win') or not os.getenv("CI"),
@@ -355,3 +363,54 @@ def test_changing_image_attenuation(make_test_viewer):
     center = tuple(np.round(np.divide(screenshot.shape[:2], 2)).astype(int))
     # Check that rendering has been attenuated
     assert screenshot[center + (0,)] < 60
+
+
+@pytest.mark.skipif(
+    sys.platform.startswith('win') or not os.getenv("CI"),
+    reason='Screenshot tests are not supported on napari windows CI.',
+)
+def test_labels_painting(make_test_viewer):
+    """Test painting labels updates image."""
+    data = np.zeros((100, 100))
+
+    viewer = make_test_viewer(show=True)
+    viewer.add_labels(data)
+    layer = viewer.layers[0]
+
+    screenshot = viewer.screenshot(canvas_only=True)
+
+    # Check that no painting has occurred
+    assert layer.data.max() == 0
+    assert screenshot[:, :, :2].max() == 0
+
+    # Enter paint mode
+    layer.position = (0, 0)
+    layer.mode = 'paint'
+    layer.selected_label = 3
+
+    # Simulate click
+    Event = collections.namedtuple(
+        'Event', field_names=['type', 'is_dragging']
+    )
+
+    # Simulate click
+    event = ReadOnlyWrapper(Event(type='mouse_press', is_dragging=False))
+    mouse_press_callbacks(layer, event)
+
+    layer.position = (100, 100)
+
+    # Simulate drag
+    event = ReadOnlyWrapper(Event(type='mouse_move', is_dragging=True))
+    mouse_move_callbacks(layer, event)
+
+    # Simulate release
+    event = ReadOnlyWrapper(Event(type='mouse_release', is_dragging=False))
+    mouse_release_callbacks(layer, event)
+
+    event = ReadOnlyWrapper(Event(type='mouse_press', is_dragging=False))
+    mouse_press_callbacks(layer, event)
+
+    screenshot = viewer.screenshot(canvas_only=True)
+    # Check that painting has now occurred
+    assert layer.data.max() > 0
+    assert screenshot[:, :, :2].max() > 0