from typing import Sequence
import numpy as np
import pygfx as gfx
from ...utils import mesh_masks
from ._base import GraphicFeature, GraphicFeatureEvent, block_reentrance
from ...utils.triangulation import triangulate
[docs]
class LinearSelectionFeature(GraphicFeature):
event_info_spec = [
{
"dict key": "value",
"type": "float",
"description": "new x or y value of selection",
},
]
event_extra_attrs = [
{
"attribute": "get_selected_index",
"type": "callable",
"description": "returns index under the selector",
}
]
def __init__(self, axis: str, value: float, limits: tuple[float, float]):
"""
Parameters
----------
axis: "x" | "y"
axis the selector is restricted to
value: float
position of the slider in world space, NOT data space
limits: (float, float)
min, max limits of the selector
"""
super().__init__()
self._axis = axis
self._limits = limits
self._value = value
@property
def value(self) -> np.float32:
"""
selection, data x or y value
"""
return self._value
@block_reentrance
def set_value(self, selector, value: float):
# clip value between limits
value = np.clip(value, self._limits[0], self._limits[1], dtype=np.float32)
# set position
if self._axis == "x":
dim = 0
elif self._axis == "y":
dim = 1
for edge in selector._edges:
edge.geometry.positions.data[:, dim] = value
edge.geometry.positions.update_range()
self._value = value
event = GraphicFeatureEvent("selection", {"value": value})
event.get_selected_index = selector.get_selected_index
self._call_event_handlers(event)
[docs]
class LinearRegionSelectionFeature(GraphicFeature):
event_info_spec = [
{
"dict key": "value",
"type": "np.ndarray",
"description": "new [min, max] of selection",
},
]
event_extra_attrs = [
{
"attribute": "get_selected_indices",
"type": "callable",
"description": "returns indices under the selector",
},
{
"attribute": "get_selected_data",
"type": "callable",
"description": "returns data under the selector",
},
]
def __init__(self, value: tuple[int, int], axis: str, limits: tuple[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]:
"""
(min, max) of the selection, in data space
"""
return self._value
@property
def axis(self) -> str:
"""one of "x" | "y" """
return self._axis
@block_reentrance
def set_value(self, selector, value: Sequence[float]):
"""
Set start, stop range of selector
Parameters
----------
selector: LinearRegionSelector
value: (float, float)
(min, max) values in data space
"""
if not len(value) == 2:
raise TypeError(
"selection must be a array, tuple, list, or sequence in the form of `(min, max)`, "
"where `min` and `max` are numeric values."
)
# convert to array, clip values if they are beyond the limits
value = np.asarray(value, dtype=np.float32).clip(*self._limits)
# make sure `selector width >= 2`, left edge must not move past right edge!
# or bottom edge must not move past top edge!
if not (value[1] - value[0]) >= 0:
return
if self.axis == "x":
# change left x position of the fill mesh
selector.fill.geometry.positions.data[mesh_masks.x_left] = value[0]
# change right x position of the fill mesh
selector.fill.geometry.positions.data[mesh_masks.x_right] = value[1]
# change x position of the left edge line
selector.edges[0].geometry.positions.data[:, 0] = value[0]
# change x position of the right edge line
selector.edges[1].geometry.positions.data[:, 0] = value[1]
elif self.axis == "y":
# change bottom y position of the fill mesh
selector.fill.geometry.positions.data[mesh_masks.y_bottom] = value[0]
# change top position of the fill mesh
selector.fill.geometry.positions.data[mesh_masks.y_top] = value[1]
# change y position of the bottom edge line
selector.edges[0].geometry.positions.data[:, 1] = value[0]
# change y position of the top edge line
selector.edges[1].geometry.positions.data[:, 1] = value[1]
self._value = value
# send changes to GPU
selector.fill.geometry.positions.update_range()
selector.edges[0].geometry.positions.update_range()
selector.edges[1].geometry.positions.update_range()
# send event
if len(self._event_handlers) < 1:
return
event = GraphicFeatureEvent("selection", {"value": self.value})
event.get_selected_indices = selector.get_selected_indices
event.get_selected_data = selector.get_selected_data
self._call_event_handlers(event)
# 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
[docs]
class RectangleSelectionFeature(GraphicFeature):
event_info_spec = [
{
"dict key": "value",
"type": "np.ndarray",
"description": "new [xmin, xmax, ymin, ymax] of selection",
},
]
event_extra_attrs = [
{
"attribute": "get_selected_indices",
"type": "callable",
"description": "returns indices under the selector",
},
{
"attribute": "get_selected_data",
"type": "callable",
"description": "returns data under the selector",
},
]
def __init__(
self,
value: tuple[float, float, float, float],
limits: tuple[float, float, float, float],
):
super().__init__()
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
@block_reentrance
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
value = np.asarray(value, dtype=np.float32)
# clip values if they are beyond the limits
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]]
)
# change the vertex positions
# bottom left
selector.vertices[0].geometry.positions.data[:] = np.array([[xmin, ymin, 1]])
# bottom right
selector.vertices[1].geometry.positions.data[:] = np.array([[xmax, ymin, 1]])
# top left
selector.vertices[2].geometry.positions.data[:] = np.array([[xmin, ymax, 1]])
# top right
selector.vertices[3].geometry.positions.data[:] = np.array([[xmax, ymax, 1]])
self._value = value
# send changes to GPU
selector.fill.geometry.positions.update_range()
for edge in selector.edges:
edge.geometry.positions.update_range()
for vertex in selector.vertices:
vertex.geometry.positions.update_range()
# send event
if len(self._event_handlers) < 1:
return
event = GraphicFeatureEvent("selection", {"value": self.value})
event.get_selected_indices = selector.get_selected_indices
event.get_selected_data = selector.get_selected_data
# calls any events
self._call_event_handlers(event)
class PolygonSelectionFeature(GraphicFeature):
event_info_spec = [
{
"dict key": "value",
"type": "np.ndarray",
"description": "new array of points that represents the polygon selection",
},
]
event_extra_attrs = [
{
"attribute": "get_selected_indices",
"type": "callable",
"description": "returns indices under the selector",
},
{
"attribute": "get_selected_data",
"type": "callable",
"description": "returns data under the selector",
},
]
def __init__(
self,
value: Sequence[tuple[float]],
limits: tuple[float, float, float, float],
):
super().__init__()
self._limits = limits
self._value = np.asarray(value).reshape(-1, 3).astype(float)
@property
def value(self) -> np.ndarray[float]:
"""
The array of the polygon, in data space
"""
return self._value
@block_reentrance
def set_value(self, selector, value: Sequence[tuple[float]]):
"""
Set the selection of the rectangle selector.
Parameters
----------
selector: PolygonSelector
value: array
new values (3D points) of the selection
"""
value = np.asarray(value, dtype=np.float32)
if not value.shape[1] == 3:
raise TypeError(
"Selection must be an array, tuple, list, or sequence of the shape Nx3."
)
# clip values if they are beyond the limits
value[:, 0] = value[:, 0].clip(self._limits[0], self._limits[1])
value[:, 1] = value[:, 1].clip(self._limits[2], self._limits[3])
self._value = value
if len(value) >= 3:
indices = triangulate(value)
else:
indices = np.zeros((0, 3), np.int32)
geometry = selector.geometry
# Need larger buffer?
if len(value) > geometry.positions.nitems:
arr = np.zeros((geometry.positions.nitems * 2, 3), np.float32)
geometry.positions = gfx.Buffer(arr)
if len(indices) > geometry.indices.nitems:
arr = np.zeros((geometry.indices.nitems * 2, 3), np.int32)
geometry.indices = gfx.Buffer(arr)
geometry.positions.data[: len(value)] = value
geometry.positions.data[len(value) :] = value[-1] if len(value) else (0, 0, 0)
geometry.positions.draw_range = 0, len(value)
geometry.positions.update_full()
geometry.indices.data[: len(indices)] = indices
geometry.indices.data[len(indices) :] = 0
geometry.indices.draw_range = 0, len(indices)
geometry.indices.update_full()
# send event
if len(self._event_handlers) < 1:
return
event = GraphicFeatureEvent("selection", {"value": self.value})
event.get_selected_indices = selector.get_selected_indices
event.get_selected_data = selector.get_selected_data
# calls any events
self._call_event_handlers(event)