From bd0a3452baeab3342e6f2fefe468c7aff3c9c695 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Tue, 15 Apr 2025 15:26:49 +0200 Subject: [PATCH 1/3] Refactor selector drag behavior --- .../linear_region_line_collection.py | 7 ++- .../selection_tools/linear_region_selector.py | 12 ++-- .../linear_region_selectors_match_offsets.py | 12 ++-- .../graphics/selectors/_base_selector.py | 58 ++++++++----------- fastplotlib/graphics/selectors/_linear.py | 16 ++--- .../graphics/selectors/_linear_region.py | 44 +++++++------- fastplotlib/graphics/selectors/_polygon.py | 6 +- fastplotlib/graphics/selectors/_rectangle.py | 44 ++++++++------ fastplotlib/layouts/_plot_area.py | 4 +- 9 files changed, 101 insertions(+), 102 deletions(-) diff --git a/examples/selection_tools/linear_region_line_collection.py b/examples/selection_tools/linear_region_line_collection.py index 76739d784..4b85b34dc 100644 --- a/examples/selection_tools/linear_region_line_collection.py +++ b/examples/selection_tools/linear_region_line_collection.py @@ -59,8 +59,11 @@ def update_zoomed_subplots(ev): for i in range(len(zoomed_data)): # interpolate y-vals - data = interpolate(zoomed_data[i], axis=1) - figure[i + 1, 0]["zoomed"].data[:, 1] = data + if zoomed_data[i].size == 0: + figure[i + 1, 0]["zoomed"].data[:, 1] = 0 + else: + data = interpolate(zoomed_data[i], axis=1) + figure[i + 1, 0]["zoomed"].data[:, 1] = data figure[i + 1, 0].auto_scale() diff --git a/examples/selection_tools/linear_region_selector.py b/examples/selection_tools/linear_region_selector.py index bfbf27811..272623370 100644 --- a/examples/selection_tools/linear_region_selector.py +++ b/examples/selection_tools/linear_region_selector.py @@ -79,9 +79,9 @@ def set_zoom_x(ev): if selected_data.size == 0: # no data selected zoomed_x.data[:, 1] = 0 - - # interpolate the y-values since y = f(x) - zoomed_x.data[:, 1] = interpolate(selected_data, axis=1) + else: + # interpolate the y-values since y = f(x) + zoomed_x.data[:, 1] = interpolate(selected_data, axis=1) figure[1, 0].auto_scale() @@ -92,9 +92,9 @@ def set_zoom_y(ev): if selected_data.size == 0: # no data selected zoomed_y.data[:, 1] = 0 - - # interpolate the x values since this x = f(y) - zoomed_y.data[:, 1] = -interpolate(selected_data, axis=0) + else: + # interpolate the x values since this x = f(y) + zoomed_y.data[:, 1] = -interpolate(selected_data, axis=0) figure[1, 1].auto_scale() diff --git a/examples/selection_tools/linear_region_selectors_match_offsets.py b/examples/selection_tools/linear_region_selectors_match_offsets.py index b48e30f28..a803a5e75 100644 --- a/examples/selection_tools/linear_region_selectors_match_offsets.py +++ b/examples/selection_tools/linear_region_selectors_match_offsets.py @@ -74,9 +74,9 @@ def set_zoom_x(ev): if selected_data.size == 0: # no data selected zoomed_x.data[:, 1] = 0 - - # interpolate the y-values since y = f(x) - zoomed_x.data[:, 1] = interpolate(selected_data, axis=1) + else: + # interpolate the y-values since y = f(x) + zoomed_x.data[:, 1] = interpolate(selected_data, axis=1) figure[1, 0].auto_scale() @@ -87,9 +87,9 @@ def set_zoom_y(ev): if selected_data.size == 0: # no data selected zoomed_y.data[:, 1] = 0 - - # interpolate the x values since this x = f(y) - zoomed_y.data[:, 1] = -interpolate(selected_data, axis=0) + else: + # interpolate the x values since this x = f(y) + zoomed_y.data[:, 1] = -interpolate(selected_data, axis=0) figure[1, 1].auto_scale() diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index 629c063bc..9f946f9fb 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -16,9 +16,14 @@ class MoveInfo: stores move info for a WorldObject """ - # last position for an edge, fill, or vertex in world coordinates - # can be None, such as key events - last_position: Union[np.ndarray, None] + # The initial selection. Differs per type of selector + start_selection: Any + + # The initial world position of the cursor + start_position: Union[np.ndarray, None] + + # Delta position in world coordinates + delta: np.ndarray # WorldObject or "key" event source: Union[WorldObject, str] @@ -143,9 +148,6 @@ def __init__( self._axis = axis - # current delta in world coordinates - self.delta: np.ndarray = None - self.arrow_keys_modifier = arrow_keys_modifier # if not False, moves the slider on every render cycle self._key_move_value = False @@ -273,9 +275,9 @@ def _move_start(self, event_source: WorldObject, ev): pygfx ``Event`` """ - last_position = self._plot_area.map_screen_to_world(ev) + position = self._plot_area.map_screen_to_world(ev) - self._move_info = MoveInfo(last_position=last_position, source=event_source) + self._move_info = MoveInfo(start_selection=None, start_position=position, delta=np.zeros_like(position), source=event_source) self._moving = True self._initial_controller_state = self._plot_area.controller.enabled @@ -298,21 +300,14 @@ def _move(self, ev): # disable controller during moves self._plot_area.controller.enabled = False - # get pointer current world position - world_pos = self._plot_area.map_screen_to_world(ev) + # get pointer current world position, in 'mouse capute mode' + world_pos = self._plot_area.map_screen_to_world(ev, allow_outside=True) - # outside this viewport - if world_pos is None: - return - - # compute the delta - self.delta = world_pos - self._move_info.last_position + # update the delta + self._move_info.delta = world_pos - self._move_info.start_position self._pygfx_event = ev - self._move_graphic(self.delta) - - # update last position - self._move_info.last_position = world_pos + self._move_graphic(self._move_info) # restore the initial controller state # if it was disabled, keep it disabled @@ -360,22 +355,21 @@ def _move_to_pointer(self, ev): if world_pos is None: return - self.delta = world_pos - current_pos_world + delta = world_pos - current_pos_world self._pygfx_event = ev # use fill by default as the source, such as in region selectors if len(self._fill) > 0: - self._move_info = MoveInfo( - last_position=current_pos_world, source=self._fill[0] + move_info = MoveInfo( + start_selection=None, start_position=None, delta=delta, source=self._fill[0] ) # else use an edge, such as for linear selector else: - self._move_info = MoveInfo( - last_position=current_pos_world, source=self._edges[0] + move_info = MoveInfo( + start_position=current_pos_world, last_position=current_pos_world, source=self._edges[0] ) - self._move_graphic(self.delta) - self._move_info = None + self._move_graphic(move_info) def _pointer_enter(self, ev): if self._hover_responsive is None: @@ -411,15 +405,13 @@ def _key_hold(self): # set event source # use fill by default as the source if len(self._fill) > 0: - self._move_info = MoveInfo(last_position=None, source=self._fill[0]) + move_info = MoveInfo(start_selection=None, start_position=None, delta=delta, source=self._fill[0]) # else use an edge else: - self._move_info = MoveInfo(last_position=None, source=self._edges[0]) + move_info = MoveInfo(start_selection=None, start_position=None, delta=delta, source=self._edges[0]) # move the graphic - self._move_graphic(delta=delta) - - self._move_info = None + self._move_graphic(move_info) def _key_down(self, ev): # key bind modifier must be set and must be used for the event @@ -441,8 +433,6 @@ def _key_up(self, ev): if ev.key in key_bind_direction.keys(): self._key_move_value = False - self._move_info = None - def _fpl_prepare_del(self): if hasattr(self, "_pfunc_fill"): self._plot_area.renderer.remove_event_handler( diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index 7ac0fc761..eb9e43d75 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -8,7 +8,7 @@ from .._base import Graphic from .._collection_base import GraphicCollection from ..features._selection_features import LinearSelectionFeature -from ._base_selector import BaseSelector +from ._base_selector import BaseSelector, MoveInfo class LinearSelector(BaseSelector): @@ -177,8 +177,6 @@ def __init__( world_object.add(self.line_outer) world_object.add(line_inner) - self._move_info: dict = None - if axis == "x": offset = (parent.offset[0], center + parent.offset[1], 0) elif axis == "y": @@ -276,7 +274,7 @@ def _get_selected_index(self, graphic): return min(round(index), upper_bound) - def _move_graphic(self, delta: np.ndarray): + def _move_graphic(self, move_info: MoveInfo): """ Moves the graphic @@ -287,7 +285,9 @@ def _move_graphic(self, delta: np.ndarray): """ - if self.axis == "x": - self.selection = self.selection + delta[0] - else: - self.selection = self.selection + delta[1] + # If this the first move in this drag, store initial selection + if move_info.start_selection is None: + move_info.start_selection = self.selection + + delta = move_info.delta[0] if self.axis == "x" else move_info.delta[1] + self.selection = move_info.start_selection + delta diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index 1bc3efc2c..14160b10c 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -7,7 +7,7 @@ from .._base import Graphic from .._collection_base import GraphicCollection from ..features._selection_features import LinearRegionSelectionFeature -from ._base_selector import BaseSelector +from ._base_selector import BaseSelector, MoveInfo class LinearRegionSelector(BaseSelector): @@ -288,7 +288,7 @@ def get_selected_data( # slices n_datapoints dim data_selections.append(g.data[s]) - return source.data[s] + return data_selections else: if ixs.size == 0: # empty selection @@ -368,31 +368,29 @@ def get_selected_indices( # indices map directly to grid geometry for image data buffer return np.arange(*bounds, dtype=int) - def _move_graphic(self, delta: np.ndarray): + def _move_graphic(self, move_info: MoveInfo): + + # If this the first move in this drag, store initial selection + if move_info.start_selection is None: + move_info.start_selection = self.selection + # add delta to current min, max to get new positions - if self.axis == "x": - # add x value - new_min, new_max = self.selection + delta[0] + delta = move_info.delta[0] if self.axis == "x" else move_info.delta[1] - elif self.axis == "y": - # add y value - new_min, new_max = self.selection + delta[1] + # Get original selection + cur_min, cur_max = move_info.start_selection # move entire selector if event source was fill if self._move_info.source == self.fill: - # prevent weird shrinkage of selector if one edge is already at the limit - if self.selection[0] == self.limits[0] and new_min < self.limits[0]: - # self._move_end(None) # TODO: cancel further movement to prevent weird asynchronization with pointer - return - if self.selection[1] == self.limits[1] and new_max > self.limits[1]: - # self._move_end(None) - return - - # move entire selector - self._selection.set_value(self, (new_min, new_max)) + # Limit the delta to avoid weird resizine behavior + min_delta = self.limits[0] - cur_min + max_delta = self.limits[1] - cur_max + delta = np.clip(delta, min_delta, max_delta) + # Update both bounds with equal amount + self._selection.set_value(self, (cur_min + delta, cur_max + delta)) return - # if selector is not resizable return + # if selector not resizable return if not self._resizable: return @@ -400,8 +398,10 @@ def _move_graphic(self, delta: np.ndarray): # move the edge that caused the event if self._move_info.source == self.edges[0]: # change only left or bottom bound - self._selection.set_value(self, (new_min, self._selection.value[1])) + new_min = min(cur_min + delta, cur_max) + self._selection.set_value(self, (new_min, cur_max)) elif self._move_info.source == self.edges[1]: # change only right or top bound - self._selection.set_value(self, (self.selection[0], new_max)) + new_max = max(cur_max + delta, cur_min) + self._selection.set_value(self, (cur_min, new_max)) diff --git a/fastplotlib/graphics/selectors/_polygon.py b/fastplotlib/graphics/selectors/_polygon.py index a4ecd440c..106d539df 100644 --- a/fastplotlib/graphics/selectors/_polygon.py +++ b/fastplotlib/graphics/selectors/_polygon.py @@ -62,11 +62,11 @@ def _add_segment(self, ev): """After click event, adds a new line segment""" self._current_mode = "add" - last_position = self._plot_area.map_screen_to_world(ev) - self._move_info = MoveInfo(last_position=last_position, source=None) + position = self._plot_area.map_screen_to_world(ev) + self._move_info = MoveInfo(start_selection=None, start_position=position, delta=np.zeros_like(position), source=None) # line with same position for start and end until mouse moves - data = np.array([last_position, last_position]) + data = np.array([position, position]) new_line = pygfx.Line( geometry=pygfx.Geometry(positions=data.astype(np.float32)), diff --git a/fastplotlib/graphics/selectors/_rectangle.py b/fastplotlib/graphics/selectors/_rectangle.py index 8d0af8e88..5eb570c15 100644 --- a/fastplotlib/graphics/selectors/_rectangle.py +++ b/fastplotlib/graphics/selectors/_rectangle.py @@ -8,7 +8,7 @@ from .._base import Graphic from ..features import RectangleSelectionFeature -from ._base_selector import BaseSelector +from ._base_selector import BaseSelector, MoveInfo class RectangleSelector(BaseSelector): @@ -24,7 +24,7 @@ def selection(self) -> np.ndarray[float]: """ (xmin, xmax, ymin, ymax) of the rectangle selection """ - return self._selection.value + return self._selection.value.copy() @selection.setter def selection(self, selection: Sequence[float]): @@ -479,33 +479,39 @@ def get_selected_indices( return ixs - def _move_graphic(self, delta: np.ndarray): + def _move_graphic(self, move_info: MoveInfo): - # new selection positions - xmin_new = self.selection[0] + delta[0] - xmax_new = self.selection[1] + delta[0] - ymin_new = self.selection[2] + delta[1] - ymax_new = self.selection[3] + delta[1] + # If this the first move in this drag, store initial selection + if move_info.start_selection is None: + move_info.start_selection = self.selection + + # add delta to current min, max to get new positions + deltax, deltay = move_info.delta[0], move_info.delta[1] + + # Get original selection + xmin, xmax, ymin, ymax = move_info.start_selection # move entire selector if source is fill if self._move_info.source == self.fill: - if self.selection[0] == self.limits[0] and xmin_new < self.limits[0]: - return - if self.selection[1] == self.limits[1] and xmax_new > self.limits[1]: - return - if self.selection[2] == self.limits[2] and ymin_new < self.limits[2]: - return - if self.selection[3] == self.limits[3] and ymax_new > self.limits[3]: - return - # set thew new bounds - self._selection.set_value(self, (xmin_new, xmax_new, ymin_new, ymax_new)) + # Limit the delta to avoid weird resizine behavior + min_deltax = self.limits[0] - xmin + max_deltax = self.limits[1] - xmax + min_deltay = self.limits[2] - ymin + max_deltay = self.limits[3] - ymax + deltax = np.clip(deltax, min_deltax, max_deltax) + deltay = np.clip(deltay, min_deltay, max_deltay) + # Update all bounds with equal amount + self._selection.set_value(self, (xmin + deltax, xmax+ deltax, ymin + deltay, ymax+deltay)) return # if selector not resizable return if not self._resizable: return - xmin, xmax, ymin, ymax = self.selection + xmin_new = min(xmin + deltax, xmax) + xmax_new = max(xmax + deltax, xmin) + ymin_new = min(ymin + deltay, ymax) + ymax_new = max(ymax + deltay, ymin) if self._move_info.source == self.vertices[0]: # bottom left self._selection.set_value(self, (xmin_new, xmax, ymin_new, ymax)) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index e780607ce..2934e0589 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -273,7 +273,7 @@ def background_color(self, colors: str | tuple[float]): self._background_material.set_colors(*colors) def map_screen_to_world( - self, pos: tuple[float, float] | pygfx.PointerEvent + self, pos: tuple[float, float] | pygfx.PointerEvent, allow_outside: bool = False ) -> np.ndarray | None: """ Map screen position to world position @@ -287,7 +287,7 @@ def map_screen_to_world( if isinstance(pos, pygfx.PointerEvent): pos = pos.x, pos.y - if not self.viewport.is_inside(*pos): + if not allow_outside and not self.viewport.is_inside(*pos): return None vs = self.viewport.logical_size From ba1fa768d790662f3286509e5abb54b4f0efd45f Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Tue, 15 Apr 2025 15:30:55 +0200 Subject: [PATCH 2/3] black --- .../graphics/selectors/_base_selector.py | 30 +++++++++++++++---- fastplotlib/graphics/selectors/_polygon.py | 7 ++++- fastplotlib/graphics/selectors/_rectangle.py | 8 +++-- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index 9f946f9fb..bad237357 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -277,7 +277,12 @@ def _move_start(self, event_source: WorldObject, ev): """ position = self._plot_area.map_screen_to_world(ev) - self._move_info = MoveInfo(start_selection=None, start_position=position, delta=np.zeros_like(position), source=event_source) + self._move_info = MoveInfo( + start_selection=None, + start_position=position, + delta=np.zeros_like(position), + source=event_source, + ) self._moving = True self._initial_controller_state = self._plot_area.controller.enabled @@ -361,12 +366,17 @@ def _move_to_pointer(self, ev): # use fill by default as the source, such as in region selectors if len(self._fill) > 0: move_info = MoveInfo( - start_selection=None, start_position=None, delta=delta, source=self._fill[0] + start_selection=None, + start_position=None, + delta=delta, + source=self._fill[0], ) # else use an edge, such as for linear selector else: move_info = MoveInfo( - start_position=current_pos_world, last_position=current_pos_world, source=self._edges[0] + start_position=current_pos_world, + last_position=current_pos_world, + source=self._edges[0], ) self._move_graphic(move_info) @@ -405,10 +415,20 @@ def _key_hold(self): # set event source # use fill by default as the source if len(self._fill) > 0: - move_info = MoveInfo(start_selection=None, start_position=None, delta=delta, source=self._fill[0]) + move_info = MoveInfo( + start_selection=None, + start_position=None, + delta=delta, + source=self._fill[0], + ) # else use an edge else: - move_info = MoveInfo(start_selection=None, start_position=None, delta=delta, source=self._edges[0]) + move_info = MoveInfo( + start_selection=None, + start_position=None, + delta=delta, + source=self._edges[0], + ) # move the graphic self._move_graphic(move_info) diff --git a/fastplotlib/graphics/selectors/_polygon.py b/fastplotlib/graphics/selectors/_polygon.py index 106d539df..22e42e63e 100644 --- a/fastplotlib/graphics/selectors/_polygon.py +++ b/fastplotlib/graphics/selectors/_polygon.py @@ -63,7 +63,12 @@ def _add_segment(self, ev): self._current_mode = "add" position = self._plot_area.map_screen_to_world(ev) - self._move_info = MoveInfo(start_selection=None, start_position=position, delta=np.zeros_like(position), source=None) + self._move_info = MoveInfo( + start_selection=None, + start_position=position, + delta=np.zeros_like(position), + source=None, + ) # line with same position for start and end until mouse moves data = np.array([position, position]) diff --git a/fastplotlib/graphics/selectors/_rectangle.py b/fastplotlib/graphics/selectors/_rectangle.py index 5eb570c15..fcf4467cb 100644 --- a/fastplotlib/graphics/selectors/_rectangle.py +++ b/fastplotlib/graphics/selectors/_rectangle.py @@ -479,14 +479,14 @@ def get_selected_indices( return ixs - def _move_graphic(self, move_info: MoveInfo): + def _move_graphic(self, move_info: MoveInfo): # If this the first move in this drag, store initial selection if move_info.start_selection is None: move_info.start_selection = self.selection # add delta to current min, max to get new positions - deltax, deltay = move_info.delta[0], move_info.delta[1] + deltax, deltay = move_info.delta[0], move_info.delta[1] # Get original selection xmin, xmax, ymin, ymax = move_info.start_selection @@ -501,7 +501,9 @@ def _move_graphic(self, move_info: MoveInfo): deltax = np.clip(deltax, min_deltax, max_deltax) deltay = np.clip(deltay, min_deltay, max_deltay) # Update all bounds with equal amount - self._selection.set_value(self, (xmin + deltax, xmax+ deltax, ymin + deltay, ymax+deltay)) + self._selection.set_value( + self, (xmin + deltax, xmax + deltax, ymin + deltay, ymax + deltay) + ) return # if selector not resizable return From 3d755e439431aa177c5f595db204d83c7055c05c Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Wed, 16 Apr 2025 09:42:01 +0200 Subject: [PATCH 3/3] Use | instead of Union --- fastplotlib/graphics/selectors/_base_selector.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index bad237357..cb58140a6 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -20,13 +20,13 @@ class MoveInfo: start_selection: Any # The initial world position of the cursor - start_position: Union[np.ndarray, None] + start_position: np.ndarray | None # Delta position in world coordinates delta: np.ndarray # WorldObject or "key" event - source: Union[WorldObject, str] + source: WorldObject | str # key bindings used to move the selector pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy