Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix shapes mouse click and double click behavior on partial shapes #6912

Merged
merged 11 commits into from
May 24, 2024

Conversation

dalthviz
Copy link
Member

@dalthviz dalthviz commented May 13, 2024

References and relevant issues

Closes #6597

Description

Enable proper handling of mouse bindings (single and double click) with unfinished/invalid shapes definitions (not enough vertices to close the shape or trying to add the same vertex multiple times. With the changes:

Preview:

invalid_shapes

@github-actions github-actions bot added the tests Something related to our tests label May 14, 2024
@dalthviz dalthviz self-assigned this May 14, 2024
@dalthviz dalthviz changed the title [WIP] Fix shapes mouse click and double click behavior on partial shape Fix shapes mouse click and double click behavior on partial shapes May 14, 2024
@dalthviz dalthviz marked this pull request as ready for review May 14, 2024 16:42
Copy link

codecov bot commented May 14, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 92.43%. Comparing base (7fc49c1) to head (7a4935d).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6912      +/-   ##
==========================================
- Coverage   92.48%   92.43%   -0.05%     
==========================================
  Files         612      612              
  Lines       55145    55169      +24     
==========================================
- Hits        51001    50997       -4     
- Misses       4144     4172      +28     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Comment on lines 448 to 450
position_diff = np.linalg.norm(event.pos - layer._last_cursor_position)
if position_diff:
# Ensure the new position is actually a new position
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason to use norm instead of just event.pos == layer._last_cursor_position? Latter seems more readable to me.

Also, using position would be more "correct" than pos since it's the world position that shoudln't change, not the canvas pos... In principle we should allow the same canvas pos if it results in a different world pos (i.e: zoom/pan between clicks).

Copy link
Member Author

@dalthviz dalthviz May 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used norm since is used for the polygon lasso tool at

if layer._mode == Mode.ADD_POLYGON_LASSO:
index = layer._moving_value[0]
position_diff = np.linalg.norm(
event.pos - layer._last_cursor_position
)
if (
position_diff
> get_settings().experimental.lasso_vertex_distance
):
add_vertex_to_path(layer, event, index, coordinates, None)

I was not sure if a straight forward comparison could be enough but if that is the case then makes sense to change it indeed 👍 Let me know if I should change it!

The logic uses the pos instead of position since, from what I checked, seems like that's the value being stored as layer._last_cursor_position. For example at

# Set last cursor position to initial position of the mouse when starting to draw the shape
layer._last_cursor_position = np.array(event.pos)
and
layer._last_cursor_position = np.array(event.pos)

Should I try to change in this PR the usage of pos for position for the layer._last_cursor_position logic?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The norm makes sense in that place because there's a threshold; in this case, you're checking only if the vertex is identical, so I would use == (probably faster as well, though I doubt it matters :P).

Ah, I see; I thought the layer._last_cursor_position was a new thing you created here. I wouldn't change the rest then.

However, this makes me wonder if there's any reason for this value to be stored on the layer and shared between different mouse callbacks? Or worse, can it be a problem if they fight over this value? Maybe there's no need to address in this PR, but we definitely need to make sure we don't end up messing up something else by touching this value.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, this makes me wonder if there's any reason for this value to be stored on the layer and shared between different mouse callbacks? Or worse, can it be a problem if they fight over this value? Maybe there's no need to address in this PR, but we definitely need to make sure we don't end up messing up something else by touching this value.

Thinking about this, I would said that the value is stored over the layer so it can be retrieved later to check the difference (in the case of the polygon lasso tool) and now the equivalence (for the polygon tool). Checking, I think that the value layer._last_cursor_position is not being set to None when finishing the drawing, so maybe it could be worthy to add such a "reset" (layer._last_cursor_position = None) as a way to mitigate possible interference between usages when drawing a shape finishes?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, good idea! Ideally this would be tracke dby the mouse callback itself... but I guess it's not that easy ^^'

@@ -2595,12 +2595,14 @@ def _finish_drawing(self, event=None) -> None:
vertices = self._data_view.shapes[index].data
if len(vertices) <= 2:
self._data_view.remove(index)
self.selected_data.clear()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an independend change right?

Copy link
Member Author

@dalthviz dalthviz May 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, is another thing related but independent indeed. While checking the fix for the initial single click issue (trying to add a vertex over the same position of an already existing vertex), I noticed that doing a double click to finish the polygons/shape drawing for an invalid set of vertices, sometimes causes a index 0 is out of bounds for axis 0 with size 0:

invalid_shape_finish

Seems like the issue in that case was caused due to the invalid shape being worked on is at the end removed while still a selection is defined. To prevent that I added the selected_data.clear() call here. Just in case, the traceback:

---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
File ~\anaconda3\envs\napari-dev\lib\site-packages\vispy\app\backends\_qt.py:516, in QtBaseCanvasBackend.mouseDoubleClickEvent(self=<vispy.app.backends._qt.CanvasBackendDesktop object>, ev=<PyQt5.QtGui.QMouseEvent object>)
    514 if self._vispy_canvas is None:
    515     return
--> 516 self._vispy_mouse_double_click(
        self = <vispy.app.backends._qt.CanvasBackendDesktop object at 0x000001ED94DC8EE0>
        ev = <PyQt5.QtGui.QMouseEvent object at 0x000001EDA34F9990>
        BUTTONMAP = {0: 0, 1: 1, 2: 2, 4: 3, 8: 4, 16: 5}
    517     native=ev,
    518     pos=_get_event_xy(ev),
    519     button=BUTTONMAP.get(ev.button(), 0),
    520     modifiers=self._modifiers(ev),
    521 )

File ~\anaconda3\envs\napari-dev\lib\site-packages\vispy\app\base.py:239, in BaseCanvasBackend._vispy_mouse_double_click(self=<vispy.app.backends._qt.CanvasBackendDesktop object>, **kwargs={'button': 1, 'buttons': [], 'last_event': <MouseEvent blocked=False button=1 buttons=[1] d...es=[] time=1715788279.9325497 type=mouse_release>, 'last_mouse_press': None, 'modifiers': (), 'native': <PyQt5.QtGui.QMouseEvent object>, 'pos': (261, 299), 'press_event': None})
    235 def _vispy_mouse_double_click(self, **kwargs):
    236     # default method for delivering double-click events to the canvas
    237     kwargs.update(self._vispy_mouse_data)
--> 239     ev = self._vispy_canvas.events.mouse_double_click(**kwargs)
        self._vispy_canvas.events.mouse_double_click = <vispy.util.event.EventEmitter object at 0x000001ED95413F70>
        kwargs = {'native': <PyQt5.QtGui.QMouseEvent object at 0x000001EDA34F9990>, 'pos': (261, 299), 'button': 1, 'modifiers': (), 'buttons': [], 'press_event': None, 'last_event': <MouseEvent blocked=False button=1 buttons=[1] delta=[0. 0.] handled=False is_dragging=True last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x000001EDA34F9990> pos=[261 299] press_event=MouseEvent source=None sources=[] time=1715788279.9325497 type=mouse_release>, 'last_mouse_press': None}
        self = <vispy.app.backends._qt.CanvasBackendDesktop object at 0x000001ED94DC8EE0>
        self._vispy_canvas.events = <vispy.util.event.EmitterGroup object at 0x000001ED95413E50>
        self._vispy_canvas = <NapariSceneCanvas (PyQt5) at 0x1ed95411cf0>
    240     self._vispy_mouse_data['last_event'] = ev
    241     return ev

File ~\anaconda3\envs\napari-dev\lib\site-packages\vispy\util\event.py:453, in EventEmitter.__call__(self=<vispy.util.event.EventEmitter object>, *args=(), **kwargs={'button': 1, 'buttons': [], 'last_event': <MouseEvent blocked=False button=1 buttons=[1] d...es=[] time=1715788279.9325497 type=mouse_release>, 'last_mouse_press': None, 'modifiers': (), 'native': <PyQt5.QtGui.QMouseEvent object>, 'pos': (261, 299), 'press_event': None})
    450 if self._emitting > 1:
    451     raise RuntimeError('EventEmitter loop detected!')
--> 453 self._invoke_callback(cb, event)
        event = <MouseEvent blocked=False button=1 buttons=[] delta=[0. 0.] handled=False is_dragging=False last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x000001EDA34F9990> pos=[261 299] press_event=None source=None sources=[] time=1715788279.9958236 type=mouse_double_click>
        self = <vispy.util.event.EventEmitter object at 0x000001ED95413F70>
        cb = <bound method VispyCanvas._on_mouse_double_click of <napari._vispy.canvas.VispyCanvas object at 0x000001ED95411CC0>>
    454 if event.blocked:
    455     break

File ~\anaconda3\envs\napari-dev\lib\site-packages\vispy\util\event.py:471, in EventEmitter._invoke_callback(self=<vispy.util.event.EventEmitter object>, cb=<bound method VispyCanvas._on_mouse_double_click of <napari._vispy.canvas.VispyCanvas object>>, event=<MouseEvent blocked=False button=1 buttons=[] de... time=1715788279.9958236 type=mouse_double_click>)
    469     cb(event)
    470 except Exception:
--> 471     _handle_exception(self.ignore_callback_errors,
        self = <vispy.util.event.EventEmitter object at 0x000001ED95413F70>
        cb = <bound method VispyCanvas._on_mouse_double_click of <napari._vispy.canvas.VispyCanvas object at 0x000001ED95411CC0>>
        event = <MouseEvent blocked=False button=1 buttons=[] delta=[0. 0.] handled=False is_dragging=False last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x000001EDA34F9990> pos=[261 299] press_event=None source=None sources=[] time=1715788279.9958236 type=mouse_double_click>
        (cb, event) = (<bound method VispyCanvas._on_mouse_double_click of <napari._vispy.canvas.VispyCanvas object at 0x000001ED95411CC0>>, <MouseEvent blocked=False button=1 buttons=[] delta=[0. 0.] handled=False is_dragging=False last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x000001EDA34F9990> pos=[261 299] press_event=None source=None sources=[] time=1715788279.9958236 type=mouse_double_click>)
    472                       self.print_callback_errors,
    473                       self, cb_event=(cb, event))

File ~\anaconda3\envs\napari-dev\lib\site-packages\vispy\util\event.py:469, in EventEmitter._invoke_callback(self=<vispy.util.event.EventEmitter object>, cb=<bound method VispyCanvas._on_mouse_double_click of <napari._vispy.canvas.VispyCanvas object>>, event=<MouseEvent blocked=False button=1 buttons=[] de... time=1715788279.9958236 type=mouse_double_click>)
    467 def _invoke_callback(self, cb, event):
    468     try:
--> 469         cb(event)
        cb = <bound method VispyCanvas._on_mouse_double_click of <napari._vispy.canvas.VispyCanvas object at 0x000001ED95411CC0>>
        event = <MouseEvent blocked=False button=1 buttons=[] delta=[0. 0.] handled=False is_dragging=False last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x000001EDA34F9990> pos=[261 299] press_event=None source=None sources=[] time=1715788279.9958236 type=mouse_double_click>
    470     except Exception:
    471         _handle_exception(self.ignore_callback_errors,
    472                           self.print_callback_errors,
    473                           self, cb_event=(cb, event))

File E:\Acer\Documentos\Quansight\Napari\napari\napari\_vispy\canvas.py:433, in VispyCanvas._on_mouse_double_click(self=<napari._vispy.canvas.VispyCanvas object>, event=<MouseEvent blocked=False button=1 buttons=[] de... time=1715788279.9958236 type=mouse_double_click>)
    410 def _on_mouse_double_click(self, event: MouseEvent) -> None:
    411     """Called whenever a mouse double-click happen on the canvas
    412
    413     Parameters
   (...)
    431          - mouse_release
    432     """
--> 433     self._process_mouse_event(mouse_double_click_callbacks, event)
        event = <MouseEvent blocked=False button=1 buttons=[] delta=[0. 0.] handled=False is_dragging=False last_event=MouseEvent modifiers=() native=<PyQt5.QtGui.QMouseEvent object at 0x000001EDA34F9990> pos=[261 299] press_event=None source=None sources=[] time=1715788279.9958236 type=mouse_double_click>
        self = <napari._vispy.canvas.VispyCanvas object at 0x000001ED95411CC0>

File E:\Acer\Documentos\Quansight\Napari\napari\napari\_vispy\canvas.py:408, in VispyCanvas._process_mouse_event(self=<napari._vispy.canvas.VispyCanvas object>, mouse_callbacks=<function mouse_double_click_callbacks>, event=<ReadOnlyWrapper at 0x000001EDA6B72240 for MouseEvent>)
    406 layer = self.viewer.layers.selection.active
    407 if layer is not None:
--> 408     mouse_callbacks(layer, event)
        event = <ReadOnlyWrapper at 0x000001EDA6B72240 for MouseEvent at 0x000001EDA6B23A00>
        layer = <Shapes layer 'Shapes' at 0x1eda652cc40>
        mouse_callbacks = <function mouse_double_click_callbacks at 0x000001ED902EFC70>

File E:\Acer\Documentos\Quansight\Napari\napari\napari\utils\interactions.py:91, in mouse_double_click_callbacks(obj=<Shapes layer 'Shapes'>, event=<ReadOnlyWrapper at 0x000001EDA6B72240 for MouseEvent>)
     85 if inspect.isgeneratorfunction(mouse_click_func):
     86     raise ValueError(
     87         trans._(
     88             "Double-click actions can't be generators.", deferred=True
     89         )
     90     )
---> 91 mouse_click_func(obj, event)
        mouse_click_func = <function finish_drawing_shape at 0x000001ED924F1AB0>
        obj = <Shapes layer 'Shapes' at 0x1eda652cc40>
        event = <ReadOnlyWrapper at 0x000001EDA6B72240 for MouseEvent at 0x000001EDA6B23A00>

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\shapes\_shapes_mouse_bindings.py:294, in finish_drawing_shape(layer=<Shapes layer 'Shapes'>, event=<ReadOnlyWrapper at 0x000001EDA6B72240 for MouseEvent>)
    280 def finish_drawing_shape(layer: Shapes, event: MouseEvent) -> None:
    281     """Finish drawing of shape.
    282
    283     Calls the finish drawing method of the shapes layer which resets all the properties used for shape drawing
   (...)
    292         double click callback of the shapes layer.
    293     """
--> 294     layer._finish_drawing()
        layer = <Shapes layer 'Shapes' at 0x1eda652cc40>

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\shapes\shapes.py:2625, in Shapes._finish_drawing(self=<Shapes layer 'Shapes'>, event=None)
   2618     self.events.data(
   2619         value=self.data,
   2620         action=ActionType.ADDED,
   2621         data_indices=(-1,),
   2622         vertex_indices=((),),
   2623     )
   2624 self._is_creating = False
-> 2625 self._update_dims()
        self = <Shapes layer 'Shapes' at 0x1eda652cc40>

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\base\base.py:841, in Layer._update_dims(self=<Shapes layer 'Shapes'>)
    837 self._slice_input = self._slice_input.with_ndim(ndim)
    839 self._ndim = ndim
--> 841 self._clear_extents_and_refresh()
        self = <Shapes layer 'Shapes' at 0x1eda652cc40>

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\base\base.py:968, in Layer._clear_extents_and_refresh(self=<Shapes layer 'Shapes'>)
    966 self._clear_extent()
    967 self._clear_extent_augmented()
--> 968 self.refresh()
        self = <Shapes layer 'Shapes' at 0x1eda652cc40>

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\base\base.py:1448, in Layer.refresh(self=<Shapes layer 'Shapes'>, event=None)
   1445     self.events.reload(layer=self)
   1446 # Otherwise, slice immediately on the calling thread.
   1447 else:
-> 1448     self._refresh_sync()
        self = <Shapes layer 'Shapes' at 0x1eda652cc40>

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\base\base.py:1456, in Layer._refresh_sync(self=<Shapes layer 'Shapes'>, event=None)
   1454 self.events.set_data()
   1455 self._update_thumbnail()
-> 1456 self._set_highlight(force=True)
        self = <Shapes layer 'Shapes' at 0x1eda652cc40>

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\shapes\shapes.py:2581, in Shapes._set_highlight(self=<Shapes layer 'Shapes'>, force=True)
   2579 self._value_stored = copy(self._value)
   2580 self._drag_box_stored = copy(self._drag_box)
-> 2581 self.events.highlight()
        self = <Shapes layer 'Shapes' at 0x1eda652cc40>
        self.events.highlight = <napari.utils.events.event.EventEmitter object at 0x000001EDA6A00070>
        self.events = <napari.utils.events.event.EmitterGroup object at 0x000001EDA261D6C0>

File E:\Acer\Documentos\Quansight\Napari\napari\napari\utils\events\event.py:764, in EventEmitter.__call__(self=<napari.utils.events.event.EventEmitter object>, *args=(), **kwargs={})
    761     self._block_counter.update([cb])
    762     continue
--> 764 self._invoke_callback(cb, event if pass_event else None)
        event = <Event blocked=False handled=False native=None source=None sources=[] type='highlight'>
        self = <napari.utils.events.event.EventEmitter object at 0x000001EDA6A00070>
        cb = <bound method VispyShapesLayer._on_highlight_change of <napari._vispy.layers.shapes.VispyShapesLayer object at 0x000001EDA6B23310>>
        pass_event = False
    765 if event.blocked:
    766     break

File E:\Acer\Documentos\Quansight\Napari\napari\napari\utils\events\event.py:802, in EventEmitter._invoke_callback(self=<napari.utils.events.event.EventEmitter object>, cb=<bound method VispyShapesLayer._on_highlight_cha...ri._vispy.layers.shapes.VispyShapesLayer object>>, event=None)
    800     self.disconnect(cb)
    801     return
--> 802 _handle_exception(
        self = <napari.utils.events.event.EventEmitter object at 0x000001EDA6A00070>
        event = None
        cb = <bound method VispyShapesLayer._on_highlight_change of <napari._vispy.layers.shapes.VispyShapesLayer object at 0x000001EDA6B23310>>
        (cb, event) = (<bound method VispyShapesLayer._on_highlight_change of <napari._vispy.layers.shapes.VispyShapesLayer object at 0x000001EDA6B23310>>, None)
    803     self.ignore_callback_errors,
    804     self.print_callback_errors,
    805     self,
    806     cb_event=(cb, event),
    807 )

File E:\Acer\Documentos\Quansight\Napari\napari\napari\utils\events\event.py:791, in EventEmitter._invoke_callback(self=<napari.utils.events.event.EventEmitter object>, cb=<bound method VispyShapesLayer._on_highlight_cha...ri._vispy.layers.shapes.VispyShapesLayer object>>, event=None)
    789         cb(event)
    790     else:
--> 791         cb()
        cb = <bound method VispyShapesLayer._on_highlight_change of <napari._vispy.layers.shapes.VispyShapesLayer object at 0x000001EDA6B23310>>
    792 except Exception as e:  # noqa: BLE001
    793     # dead Qt object with living python pointer. not importing Qt
    794     # here... but this error is consistent across backends
    795     if (
    796         isinstance(e, RuntimeError)
    797         and 'C++' in str(e)
    798         and str(e).endswith(('has been deleted', 'already deleted.'))
    799     ):

File E:\Acer\Documentos\Quansight\Napari\napari\napari\_vispy\layers\shapes.py:72, in VispyShapesLayer._on_highlight_change(self=<napari._vispy.layers.shapes.VispyShapesLayer object>)
     67 self.layer._highlight_color = (
     68     settings.appearance.highlight.highlight_color
     69 )
     71 # Compute the vertices and faces of any shape outlines
---> 72 vertices, faces = self.layer._outline_shapes()
        self.layer = <Shapes layer 'Shapes' at 0x1eda652cc40>
        self = <napari._vispy.layers.shapes.VispyShapesLayer object at 0x000001EDA6B23310>
     74 if vertices is None or len(vertices) == 0 or len(faces) == 0:
     75     vertices = np.zeros((3, self.layer._slice_input.ndisplay))

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\shapes\shapes.py:2460, in Shapes._outline_shapes(self=<Shapes layer 'Shapes'>)
   2457 else:
   2458     index = self._value[0]
-> 2460 centers, offsets, triangles = self._data_view.outline(index)
        index = [2]
        self = <Shapes layer 'Shapes' at 0x1eda652cc40>
        self._data_view = <napari.layers.shapes._shape_list.ShapeList object at 0x000001EDA6A01B40>
   2461 vertices = centers + (
   2462     self._normalized_scale_factor * self._highlight_width * offsets
   2463 )
   2464 vertices = vertices[:, ::-1]

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\shapes\_shape_list.py:1042, in ShapeList.outline(self=<napari.layers.shapes._shape_list.ShapeList object>, indices=[2])
   1040 if not isinstance(indices, Sequence):
   1041     indices = [indices]
-> 1042 return self.outlines(indices)
        indices = [2]
        self = <napari.layers.shapes._shape_list.ShapeList object at 0x000001EDA6A01B40>

File E:\Acer\Documentos\Quansight\Napari\napari\napari\layers\shapes\_shape_list.py:1084, in ShapeList.outlines(self=<napari.layers.shapes._shape_list.ShapeList object>, indices=[2])
   1082 for i, ind in enumerate(indices):
   1083     inds = t_ind == ind
-> 1084     adjust_index = starts[i] - vertices_indices[starts[i]]
        vertices_indices = []
        starts = array([], dtype=int64)
        i = 0
   1085     triangles[inds] = triangles[inds] + adjust_index
   1087 return centers, offsets, triangles

IndexError: index 0 is out of bounds for axis 0 with size 0

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any case in which we can have (or want to have) other shapes being selected while a shape is being added? If so, this will deselect everything, which might not be desired. On the other hand, probably this never happens?

Copy link
Member Author

@dalthviz dalthviz May 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for the moment the selection always resets when a polygon is initialized at

layer.selected_data = {layer.nshapes - 1}

Also, from a quick check via de GUI with latest main, seems like that is the case for all the possible shapes (when creating them any previous selection is discarded):

shape_selection_creation

So, at least for the moment, clearing the selection when removing an invalid shape/vertices should not be a problem 👍

However, just in case, should I add some sort of comment in the code pointing to this PR/these comments for tracebility?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comments never hurt, but yeah sounds fine by me then :)

Copy link
Contributor

@brisvag brisvag left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@dalthviz
Copy link
Member Author

Thanks @brisvag for all the feedback!

@psobolewskiPhD psobolewskiPhD added bugfix PR with bugfix ready to merge Last chance for comments! Will be merged in ~24h labels May 23, 2024
@psobolewskiPhD psobolewskiPhD added this to the 0.5.0 milestone May 23, 2024
@psobolewskiPhD
Copy link
Member

Thanks @dalthviz !

@psobolewskiPhD psobolewskiPhD merged commit e29593c into napari:main May 24, 2024
34 checks passed
@github-actions github-actions bot removed the ready to merge Last chance for comments! Will be merged in ~24h label May 24, 2024
andy-sweet pushed a commit to andy-sweet/napari that referenced this pull request May 28, 2024
…apari#6912)

# References and relevant issues

Closes napari#6597 

# Description

Enable proper handling of mouse bindings (single and double click) with
unfinished/invalid shapes definitions (not enough vertices to close the
shape or trying to add the same vertex multiple times. With the changes:

* Ignore addition of a vertex (mouse single click) if it has the same
position as the last vertex added (fix for napari#6597)
* Trying to finish the drawing (mouse double click) of an invalid shape
causes the in progress shape to be removed (fix for
napari#6912 (comment))

Preview:


![invalid_shapes](https://github.com/napari/napari/assets/16781833/8002862b-4cb6-4b13-80e2-366b0c4f4d03)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bugfix PR with bugfix tests Something related to our tests
Projects
None yet
3 participants