Skip to content

Refactor selector drag behavior #800

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

Merged
merged 3 commits into from
Apr 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions examples/selection_tools/linear_region_line_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand Down
12 changes: 6 additions & 6 deletions examples/selection_tools/linear_region_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand All @@ -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()


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand All @@ -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()


Expand Down
80 changes: 45 additions & 35 deletions fastplotlib/graphics/selectors/_base_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@ 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: 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -273,9 +275,14 @@ 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
Expand All @@ -298,21 +305,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)

# outside this viewport
if world_pos is None:
return
# get pointer current world position, in 'mouse capute mode'
world_pos = self._plot_area.map_screen_to_world(ev, allow_outside=True)

# 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
Expand Down Expand Up @@ -360,22 +360,26 @@ 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:
Expand Down Expand Up @@ -411,15 +415,23 @@ 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
Expand All @@ -441,8 +453,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(
Expand Down
16 changes: 8 additions & 8 deletions fastplotlib/graphics/selectors/_linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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

Expand All @@ -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
44 changes: 22 additions & 22 deletions fastplotlib/graphics/selectors/_linear_region.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -368,40 +368,40 @@ 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

# if event source was an edge and selector is resizable,
# 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))
11 changes: 8 additions & 3 deletions fastplotlib/graphics/selectors/_polygon.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,16 @@ 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)),
Expand Down
Loading
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