diff --git a/examples/selection_tools/linear_selector.py b/examples/selection_tools/linear_selector.py index 65fd8f1b1..8b442db20 100644 --- a/examples/selection_tools/linear_selector.py +++ b/examples/selection_tools/linear_selector.py @@ -2,7 +2,8 @@ Linear Selectors ================ -Example showing how to use a `LinearSelector` with lines and line collections. +Example showing how to use a `LinearSelector` with lines and line collections. The linear selector is the yellow +vertical line. """ # test_example = true diff --git a/examples/selection_tools/linear_selector_image.py b/examples/selection_tools/linear_selector_image.py index 04844b568..657d5ae5e 100644 --- a/examples/selection_tools/linear_selector_image.py +++ b/examples/selection_tools/linear_selector_image.py @@ -2,8 +2,9 @@ Linear Selectors Image ====================== -Example showing how to use a `LinearSelector` to selector rows or columns of an image. The subplot on the right -displays the data for the selector row and column. +Example showing how to use a `LinearSelector` to select rows or columns of an image. The subplot on the right +displays the data for the selector row and column. Move the selectors independently or click the middle mouse +button to move both selectors to the clicked location. """ # test_example = false @@ -24,10 +25,10 @@ image = figure[0, 0].add_image(image_data) # add a row selector -image_row_selector = image.add_linear_selector(axis="y") +image_row_selector = image.add_linear_selector(axis="y", edge_color="cyan") # add column selector -image_col_selector = image.add_linear_selector() +image_col_selector = image.add_linear_selector(edge_color="cyan") # make a line to indicate row data line_image_row = figure[0, 1].add_line(image.data[0]) diff --git a/examples/selection_tools/unit_circle.py b/examples/selection_tools/unit_circle.py index 143992c62..b2ba772e4 100644 --- a/examples/selection_tools/unit_circle.py +++ b/examples/selection_tools/unit_circle.py @@ -132,6 +132,9 @@ def set_x_val(ev): sine_selector.add_event_handler(set_x_val, "selection") cosine_selector.add_event_handler(set_x_val, "selection") +# set initial position of the selector so it's not just overlapping the y-axis +sine_selector.selection = 100 + figure.show() diff --git a/fastplotlib/graphics/features/_selection_features.py b/fastplotlib/graphics/features/_selection_features.py index 3052ae3d0..ed18c8287 100644 --- a/fastplotlib/graphics/features/_selection_features.py +++ b/fastplotlib/graphics/features/_selection_features.py @@ -64,9 +64,9 @@ def set_value(self, selector, value: float): elif self._axis == "y": dim = 1 - for edge in selector._edges: - edge.geometry.positions.data[:, dim] = value - edge.geometry.positions.update_range() + edge = selector._edges[0] + edge.geometry.positions.data[:, dim] = value + edge.geometry.positions.update_range() self._value = value @@ -152,10 +152,10 @@ def set_value(self, selector, value: Sequence[float]): 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] + 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] + selector._edges[1].geometry.positions.data[:, 0] = value[1] elif self.axis == "y": # change bottom y position of the fill mesh @@ -165,18 +165,18 @@ def set_value(self, selector, value: Sequence[float]): 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] + 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] + 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() + selector._edges[0].geometry.positions.update_range() + selector._edges[1].geometry.positions.update_range() # send event if len(self._event_handlers) < 1: diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index 2d2787ac8..e4dbc890b 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -111,6 +111,7 @@ def edge_color(self, color: str | Sequence[float]): def __init__( self, edges: Tuple[Line, ...] = None, + outer_edges: Tuple[Line, ...] = None, fill: Tuple[Mesh, ...] = None, vertices: Tuple[Points, ...] = None, hover_responsive: Tuple[WorldObject, ...] = None, @@ -122,6 +123,9 @@ def __init__( if edges is None: edges = tuple() + if outer_edges is None: + outer_edges = tuple() + if fill is None: fill = tuple() @@ -129,11 +133,15 @@ def __init__( vertices = tuple() self._edges: Tuple[Line, ...] = edges + self._outer_edges: Tuple[Line, ...] = outer_edges self._fill: Tuple[Mesh, ...] = fill self._vertices: Tuple[Points, ...] = vertices self._world_objects: Tuple[WorldObject, ...] = ( - self._edges + self._fill + self._vertices + *self._edges, + *self._outer_edges, + *self._fill, + *self._vertices, ) for wo in self._world_objects: @@ -148,7 +156,7 @@ def __init__( self._hover_colors = {} if hover_responsive is not None: - for wo in self._hover_responsive: + for wo in [*self._hover_responsive, *self._outer_edges]: self._original_colors[wo] = wo.material.color self._axis = axis @@ -231,7 +239,7 @@ def _fpl_add_plot_area_hook(self, plot_area): self._plot_area.renderer.add_event_handler(self._move_to_pointer, "click") # mouse hover color events - for wo in self._hover_responsive: + for wo in [*self._hover_responsive, *self._outer_edges]: wo.add_event_handler(self._pointer_enter, "pointer_enter") wo.add_event_handler(self._pointer_leave, "pointer_leave") @@ -282,6 +290,12 @@ def _move_start(self, event_source: WorldObject, ev): """ position = self._plot_area.map_screen_to_world(ev) + # if the event source was an outer transparent line, get the + # corresponding inner line since it's just a proxy + if event_source in self._outer_edges: + index = self._outer_edges.index(event_source) + event_source = self._edges[index] + self._move_info = MoveInfo( start_selection=None, start_position=position, @@ -397,9 +411,16 @@ def _pointer_enter(self, ev): return wo = ev.pick_info["world_object"] - if wo not in self._hover_responsive: + if wo not in [*self._hover_responsive, *self._outer_edges]: return + # if it's an outer edge, highlight the corresponding inner edge instead + if wo in self._outer_edges: + # get index + index = self._outer_edges.index(wo) + # now use inner edge + wo = self._edges[index] + if wo in self._edges: self._edge_hovered = True @@ -415,7 +436,7 @@ def _pointer_leave(self, ev): self._edge_hovered = False # reset colors - for wo in self._hover_responsive: + for wo in [*self._hover_responsive, *self._outer_edges]: if self._moving: self._hover_colors[wo] = self._original_colors[wo] else: diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index 033736a5f..0364305a4 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -78,9 +78,10 @@ def __init__( limits: Sequence[float], axis: str = "x", parent: Graphic = None, - edge_color: str | Sequence[float] | np.ndarray = "w", - thickness: float = 2.5, + edge_color: str | Sequence[float] | np.ndarray = "yellow", + thickness: float = 1.0, arrow_keys_modifier: str = "Shift", + extra_width: float = 14.0, name: str = None, ): """ @@ -111,6 +112,9 @@ def __init__( edge_color: str | tuple | np.ndarray, default "w" color of the selector + extra_width: float, default 14.0 + the width around the selector which is responsive to mouse events, in logical pixels + name: str, optional name of linear selector @@ -141,8 +145,6 @@ def __init__( material = pygfx.LineInfiniteSegmentMaterial - self.colors_outer = pygfx.Color([0.3, 0.3, 0.3, 1.0]) - line_inner = pygfx.Line( # self.data.feature_data because data is a Buffer geometry=pygfx.Geometry(positions=line_data), @@ -158,11 +160,12 @@ def __init__( ), ) - self.line_outer = pygfx.Line( - geometry=pygfx.Geometry(positions=line_data), + line_outer = pygfx.Line( + geometry=line_inner.geometry, material=material( - thickness=thickness + 6, - color=self.colors_outer, + thickness=thickness + extra_width, + color=pygfx.Color([0, 0, 0]), + opacity=0, alpha_mode="blend", aa=True, render_queue=RenderQueue.selector, @@ -177,7 +180,7 @@ def __init__( world_object = pygfx.Group() - world_object.add(self.line_outer) + world_object.add(line_outer) world_object.add(line_inner) if axis == "x": @@ -188,8 +191,9 @@ def __init__( # init base selector BaseSelector.__init__( self, - edges=(line_inner, self.line_outer), - hover_responsive=(line_inner, self.line_outer), + edges=(line_inner,), + outer_edges=(line_outer,), + hover_responsive=(line_inner,), arrow_keys_modifier=arrow_keys_modifier, axis=axis, parent=parent, diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index ee6849144..9f5803c93 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -64,9 +64,10 @@ def __init__( parent: Graphic = None, resizable: bool = True, fill_color: str | Sequence[float] = (0, 0, 0.35), - edge_color: str | Sequence[float] = (0.8, 0.6, 0), - edge_thickness: float = 8, + edge_color: str | Sequence[float] = "yellow", + edge_thickness: float = 1.0, arrow_keys_modifier: str = "Shift", + extra_width: float = 14.0, name: str = None, ): """ @@ -113,6 +114,9 @@ def __init__( modifier key that must be pressed to initiate movement using arrow keys, must be one of: "Control", "Shift", "Alt" or ``None`` + extra_width: float, default 14.0 + the width around the selector lines which is responsive to mouse events, in logical pixels + name: str, optional name of this selector graphic @@ -215,6 +219,25 @@ def __init__( pick_write=True, ), ) + + line0_outer = pygfx.Line( + pygfx.Geometry( + # share buffer with inner line so they can both be managed together + positions=line0.geometry.positions + ), + pygfx.LineMaterial( + thickness=edge_thickness + extra_width, + color=pygfx.Color([0, 0, 0]), + alpha_mode="blend", + opacity=0, + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + pick_write=True, + ), + ) + line1 = pygfx.Line( pygfx.Geometry( positions=init_line_data.copy() @@ -232,8 +255,27 @@ def __init__( ), ) - self.edges: tuple[pygfx.Line, pygfx.Line] = (line0, line1) - group.add(*self.edges) + line1_outer = pygfx.Line( + pygfx.Geometry( + # share buffer with inner line so they can both be managed together + positions=line1.geometry.positions + ), + pygfx.LineMaterial( + thickness=edge_thickness + extra_width, + color=pygfx.Color([0, 0, 0]), + alpha_mode="blend", + opacity=0, + aa=True, + render_queue=RenderQueue.selector, + depth_test=False, + depth_write=False, + pick_write=True, + ), + ) + + edges: tuple[pygfx.Line, pygfx.Line] = (line0, line1) + outer_edges = (line0_outer, line1_outer) + group.add(*edges, *outer_edges) # TODO: if parent offset changes, we should set the selector offset too, use offset evented property # TODO: add check if parent is `None`, will throw error otherwise @@ -253,9 +295,10 @@ def __init__( BaseSelector.__init__( self, - edges=self.edges, + edges=edges, + outer_edges=outer_edges, fill=(self.fill,), - hover_responsive=self.edges, + hover_responsive=edges, arrow_keys_modifier=arrow_keys_modifier, axis=axis, parent=parent, @@ -423,12 +466,12 @@ def _move_graphic(self, move_info: MoveInfo): # if event source was an edge and selector is resizable, # move the edge that caused the event - if move_info.source == self.edges[0]: + if move_info.source == self._edges[0]: # change only left or bottom bound new_min = min(cur_min + delta, cur_max) self._selection.set_value(self, (new_min, cur_max)) - elif move_info.source == self.edges[1]: + elif move_info.source == self._edges[1]: # change only right or top bound new_max = max(cur_max + delta, cur_min) self._selection.set_value(self, (cur_min, new_max)) diff --git a/fastplotlib/tools/_histogram_lut.py b/fastplotlib/tools/_histogram_lut.py index 9c6b1b24d..7507a7ff2 100644 --- a/fastplotlib/tools/_histogram_lut.py +++ b/fastplotlib/tools/_histogram_lut.py @@ -106,7 +106,6 @@ def __init__( size=size, center=origin[0], axis="y", - edge_thickness=8, parent=self._histogram_line, )