Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
3 changes: 2 additions & 1 deletion fastplotlib/graphics/_features/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
TextOutlineThickness,
)

from ._selection_features import LinearSelectionFeature, LinearRegionSelectionFeature
from ._selection_features import LinearSelectionFeature, LinearRegionSelectionFeature, RectangleSelectionFeature
from ._common import Name, Offset, Rotation, Visible, Deleted


Expand All @@ -56,6 +56,7 @@
"TextOutlineThickness",
"LinearSelectionFeature",
"LinearRegionSelectionFeature",
"RectangleRegionSelectionFeature"
"Name",
"Offset",
"Rotation",
Expand Down
137 changes: 136 additions & 1 deletion fastplotlib/graphics/_features/_selection_features.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Sequence
from typing import Sequence, Tuple

import numpy as np

Expand Down Expand Up @@ -190,3 +190,138 @@ def set_value(self, selector, value: Sequence[float]):
# TODO: user's selector event handlers can call event.graphic.get_selected_indices() to get the data index,
# and event.graphic.get_selected_data() to get the data under the selection
# this is probably a good idea so that the data isn't sliced until it's actually necessary


class RectangleSelectionFeature(GraphicFeature):
"""
**additional event attributes:**

+----------------------+----------+------------------------------------+
| attribute | type | description |
+======================+==========+====================================+
| get_selected_indices | callable | returns indices under the selector |
+----------------------+----------+------------------------------------+
| get_selected_data | callable | returns data under the selector |
+----------------------+----------+------------------------------------+

**info dict:**

+----------+------------+-------------------------------------------+
| dict key | value type | value description |
+==========+============+===========================================+
| value | np.ndarray | new [xmin, xmax, ymin, ymax] of selection |
+----------+------------+-------------------------------------------+

"""
def __init__(
self,
value: tuple[float, float, float, float],
axis: str | None,
limits: tuple[float, float, float, float]
):
super().__init__()

self._axis = axis
self._limits = limits
self._value = tuple(int(v) for v in value)

@property
def value(self) -> np.ndarray[float]:
"""
(xmin, xmax, ymin, ymax) of the selection, in data space
"""
return self._value

@property
def axis(self) -> str:
"""one of "x" | "y" """
return self._axis
Comment thread
clewis7 marked this conversation as resolved.
Outdated

def set_value(self, selector, value: Sequence[float]):
"""
Set the selection of the rectangle selector.

Parameters
----------
selector: RectangleSelector

value: (float, float, float, float)
new values (xmin, xmax, ymin, ymax) of the selection
"""
if not len(value) == 4:
raise TypeError(
"Selection must be an array, tuple, list, or sequence in the form of `(xmin, xmax, ymin, ymax)`, "
"where `xmin`, `xmax`, `ymin`, `ymax` are numeric values."
)

# convert to array, clip values if they are beyond the limits
# clip x
value = np.asarray(value, dtype=np.float32)
value[:2] = value[:2].clip(self._limits[0], self._limits[1])
# clip y
value[2:] = value[2:].clip(self._limits[2], self._limits[3])

xmin, xmax, ymin, ymax = value

# make sure `selector width >= 2` and selector height >=2 , left edge must not move past right edge!
# or bottom edge must not move past top edge!
if not (xmax - xmin) >= 0 or not (ymax - ymin) >= 0:
return

# change fill mesh
# change left x position of the fill mesh
selector.fill.geometry.positions.data[mesh_masks.x_left] = xmin

# change right x position of the fill mesh
selector.fill.geometry.positions.data[mesh_masks.x_right] = xmax

# change bottom y position of the fill mesh
selector.fill.geometry.positions.data[mesh_masks.y_bottom] = ymin

# change top position of the fill mesh
selector.fill.geometry.positions.data[mesh_masks.y_top] = ymax

# change the edge lines

# each edge line is defined by two end points which are stored in the
# geometry.positions
# [x0, y0, z0]
# [x1, y1, z0]

# left line
z = selector.edges[0].geometry.positions.data[:, -1][0]
selector.edges[0].geometry.positions.data[:] = np.array(
[[xmin, ymin, z], [xmin, ymax, z]]
)

# right line
selector.edges[1].geometry.positions.data[:] = np.array(
[[xmax, ymin, z], [xmax, ymax, z]]
)

# bottom line
selector.edges[2].geometry.positions.data[:] = np.array(
[[xmin, ymin, z], [xmax, ymin, z]]
)

# top line
selector.edges[3].geometry.positions.data[:] = np.array(
[[xmin, ymax, z], [xmax, ymax, z]]
)
#
self._value = value
#
# send changes to GPU
selector.fill.geometry.positions.update_range()
#
for edge in selector.edges:
edge.geometry.positions.update_range()

# send event
if len(self._event_handlers) < 1:
return

event = FeatureEvent("selection", {"value": self.value})

# calls any events
self._call_event_handlers(event)
3 changes: 2 additions & 1 deletion fastplotlib/graphics/selectors/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from ._linear import LinearSelector
from ._linear_region import LinearRegionSelector
from ._polygon import PolygonSelector
from ._rectangle_region import RectangleSelector


__all__ = ["LinearSelector", "LinearRegionSelector"]
__all__ = ["LinearSelector", "LinearRegionSelector", "RectangleSelector"]
3 changes: 3 additions & 0 deletions fastplotlib/graphics/selectors/_base_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ def __init__(
self._edges + self._fill + self._vertices
)

for wo in self._world_objects:
wo.material.pick_write = True

self._hover_responsive: Tuple[WorldObject, ...] = hover_responsive

if hover_responsive is not None:
Expand Down
Loading