From 3b3d0748f5999bf568331d0aa4e6bbbeecef7b49 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 20 Oct 2024 05:05:45 -0400 Subject: [PATCH 1/2] resolve merge conflict --- examples/selection_tools/image_grid_cursor.py | 59 +++++++ fastplotlib/graphics/_base.py | 16 +- fastplotlib/graphics/image.py | 6 +- fastplotlib/graphics/line.py | 6 +- fastplotlib/graphics/line_collection.py | 6 +- .../graphics/selectors/_base_selector.py | 34 ++-- fastplotlib/tools/__init__.py | 1 + fastplotlib/tools/_cursor.py | 152 ++++++++++++++++++ fastplotlib/tools/_histogram_lut.py | 10 +- 9 files changed, 251 insertions(+), 39 deletions(-) create mode 100644 examples/selection_tools/image_grid_cursor.py create mode 100644 fastplotlib/tools/_cursor.py diff --git a/examples/selection_tools/image_grid_cursor.py b/examples/selection_tools/image_grid_cursor.py new file mode 100644 index 000000000..b9a66ded8 --- /dev/null +++ b/examples/selection_tools/image_grid_cursor.py @@ -0,0 +1,59 @@ +""" +Grid of images with a cursor +============================ + +Example showing a grid of images in a single subplot and an interactive cursor +that marks the same position in each image +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import fastplotlib as fpl +import numpy as np + + +figure = fpl.Figure(size=(700, 560)) + +# make 12 images +images = np.random.rand(12, 100, 100) + +# we will display 3 rows and 4 columns +rows = 3 +columns = 4 + +# spacing between each image +spacing = 25 + +# interactive cursor +cursor = fpl.Cursor(size=15, color="magenta") + +index = 0 +for i in range(rows): + for j in range(columns): + img = images[index] + # offset is x, y, z position + offset = (j * img.shape[1] + spacing * j, i * img.shape[0] + spacing * i, 0) + img_graphic = figure[0, 0].add_image(img, cmap="viridis", offset=offset) + cursor.add(img_graphic) + + text_label = figure[0, 0].add_text( + str(index), + face_color="r", + font_size=25, + outline_color="w", + outline_thickness=0.05, + anchor="bottom-right", + offset=offset + ) + + index += 1 + +figure.show() + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.run() diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index a25bc7176..241e6f6c4 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -106,7 +106,7 @@ def __init__( # store hex id str of Graphic instance mem location self._fpl_address: HexStr = hex(id(self)) - self._plot_area = None + self._fpl_plot_area = None # event handlers self._event_handlers = defaultdict(set) @@ -362,7 +362,7 @@ def my_handler(event): feature.remove_event_handler(wrapper) def _fpl_add_plot_area_hook(self, plot_area): - self._plot_area = plot_area + self._fpl_plot_area = plot_area def __repr__(self): rval = f"{self.__class__.__name__} @ {hex(id(self))}" @@ -380,8 +380,8 @@ def _fpl_prepare_del(self): """ # remove axes if added to this graphic if self._axes is not None: - self._plot_area.scene.remove(self._axes) - self._plot_area.remove_animation(self._update_axes) + self._fpl_plot_area.scene.remove(self._axes) + self._fpl_plot_area.remove_animation(self._update_axes) self._axes.world_object.clear() # signal that a deletion has been requested @@ -402,12 +402,12 @@ def _fpl_prepare_del(self): for ev_type in PYGFX_EVENTS: try: - self._plot_area.renderer.remove_event_handler(method, ev_type) + self._fpl_plot_area.renderer.remove_event_handler(method, ev_type) except (KeyError, TypeError): pass try: - self._plot_area.remove_animation(method) + self._fpl_plot_area.remove_animation(method) except KeyError: pass @@ -452,10 +452,10 @@ def add_axes(self): if self._axes is not None: raise AttributeError("Axes already added onto this graphic") - self._axes = Axes(self._plot_area, offset=self.offset, grids=False) + self._axes = Axes(self._fpl_plot_area, offset=self.offset, grids=False) self._axes.world_object.local.rotation = self.world_object.local.rotation - self._plot_area.scene.add(self.axes.world_object) + self._fpl_plot_area.scene.add(self.axes.world_object) self._axes.update_using_bbox(self.world_object.get_world_bounding_box()) @property diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 62477d7ff..b1e74e770 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -311,7 +311,7 @@ def add_linear_selector( **kwargs, ) - self._plot_area.add_graphic(selector, center=False) + self._fpl_plot_area.add_graphic(selector, center=False) # place selector above this graphic selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) @@ -388,7 +388,7 @@ def add_linear_region_selector( **kwargs, ) - self._plot_area.add_graphic(selector, center=False) + self._fpl_plot_area.add_graphic(selector, center=False) # place above this graphic selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) @@ -430,7 +430,7 @@ def add_rectangle_selector( **kwargs, ) - self._plot_area.add_graphic(selector, center=False) + self._fpl_plot_area.add_graphic(selector, center=False) # place above this graphic selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 1574587fe..e1083411c 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -150,7 +150,7 @@ def add_linear_selector( **kwargs, ) - self._plot_area.add_graphic(selector, center=False) + self._fpl_plot_area.add_graphic(selector, center=False) # place selector above this graphic selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) @@ -207,7 +207,7 @@ def add_linear_region_selector( **kwargs, ) - self._plot_area.add_graphic(selector, center=False) + self._fpl_plot_area.add_graphic(selector, center=False) # place selector below this graphic selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] - 1) @@ -257,7 +257,7 @@ def add_rectangle_selector( **kwargs, ) - self._plot_area.add_graphic(selector, center=False) + self._fpl_plot_area.add_graphic(selector, center=False) return selector diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index c4af5dddc..a0cac9eb4 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -381,7 +381,7 @@ def add_linear_selector( **kwargs, ) - self._plot_area.add_graphic(selector, center=False) + self._fpl_plot_area.add_graphic(selector, center=False) # place selector above this graphic selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) @@ -438,7 +438,7 @@ def add_linear_region_selector( **kwargs, ) - self._plot_area.add_graphic(selector, center=False) + self._fpl_plot_area.add_graphic(selector, center=False) # place selector below this graphic selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] - 1) @@ -484,7 +484,7 @@ def add_rectangle_selector( **kwargs, ) - self._plot_area.add_graphic(selector, center=False) + self._fpl_plot_area.add_graphic(selector, center=False) return selector diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index 30643bbe4..28e42553d 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -134,7 +134,7 @@ def _get_source(self, graphic): return source def _fpl_add_plot_area_hook(self, plot_area): - self._plot_area = plot_area + self._fpl_plot_area = plot_area # when the pointer is pressed on a fill, edge or vertex for wo in self._world_objects: @@ -147,18 +147,18 @@ def _fpl_add_plot_area_hook(self, plot_area): for fill in self._fill: if fill.material.color_is_transparent: self._pfunc_fill = partial(self._check_fill_pointer_event, fill) - self._plot_area.renderer.add_event_handler( + self._fpl_plot_area.renderer.add_event_handler( self._pfunc_fill, "pointer_down" ) # when the pointer moves - self._plot_area.renderer.add_event_handler(self._move, "pointer_move") + self._fpl_plot_area.renderer.add_event_handler(self._move, "pointer_move") # when the pointer is released - self._plot_area.renderer.add_event_handler(self._move_end, "pointer_up") + self._fpl_plot_area.renderer.add_event_handler(self._move_end, "pointer_up") # move directly to location of center mouse button click - self._plot_area.renderer.add_event_handler(self._move_to_pointer, "click") + self._fpl_plot_area.renderer.add_event_handler(self._move_to_pointer, "click") # mouse hover color events for wo in self._hover_responsive: @@ -166,16 +166,16 @@ def _fpl_add_plot_area_hook(self, plot_area): wo.add_event_handler(self._pointer_leave, "pointer_leave") # arrow key bindings - self._plot_area.renderer.add_event_handler(self._key_down, "key_down") - self._plot_area.renderer.add_event_handler(self._key_up, "key_up") - self._plot_area.add_animations(self._key_hold) + self._fpl_plot_area.renderer.add_event_handler(self._key_down, "key_down") + self._fpl_plot_area.renderer.add_event_handler(self._key_up, "key_up") + self._fpl_plot_area.add_animations(self._key_hold) def _check_fill_pointer_event(self, event_source: WorldObject, ev): if self._edge_hovered: # if edge is hovered, prefer edge events, disable fill moves return - world_pos = self._plot_area.map_screen_to_world(ev) + world_pos = self._fpl_plot_area.map_screen_to_world(ev) # outside viewport, ignore # this shouldn't be possible since the event handler is registered to the fill mesh world object # but I like sanity checks anyways @@ -210,12 +210,12 @@ def _move_start(self, event_source: WorldObject, ev): pygfx ``Event`` """ - last_position = self._plot_area.map_screen_to_world(ev) + last_position = self._fpl_plot_area.map_screen_to_world(ev) self._move_info = MoveInfo(last_position=last_position, source=event_source) self._moving = True - self._initial_controller_state = self._plot_area.controller.enabled + self._initial_controller_state = self._fpl_plot_area.controller.enabled def _move(self, ev): """ @@ -233,10 +233,10 @@ def _move(self, ev): return # disable controller during moves - self._plot_area.controller.enabled = False + self._fpl_plot_area.controller.enabled = False # get pointer current world position - world_pos = self._plot_area.map_screen_to_world(ev) + world_pos = self._fpl_plot_area.map_screen_to_world(ev) # outside this viewport if world_pos is None: @@ -253,7 +253,7 @@ def _move(self, ev): # restore the initial controller state # if it was disabled, keep it disabled - self._plot_area.controller.enabled = self._initial_controller_state + self._fpl_plot_area.controller.enabled = self._initial_controller_state def _move_graphic(self, delta: np.ndarray): raise NotImplementedError("Must be implemented in subclass") @@ -265,7 +265,7 @@ def _move_end(self, ev): # restore the initial controller state # if it was disabled, keep it disabled if self._initial_controller_state is not None: - self._plot_area.controller.enabled = self._initial_controller_state + self._fpl_plot_area.controller.enabled = self._initial_controller_state def _move_to_pointer(self, ev): """ @@ -291,7 +291,7 @@ def _move_to_pointer(self, ev): current_pos_world: np.ndarray = center + offset - world_pos = self._plot_area.map_screen_to_world(ev) + world_pos = self._fpl_plot_area.map_screen_to_world(ev) # outside this viewport if world_pos is None: @@ -382,7 +382,7 @@ def _key_up(self, ev): def _fpl_prepare_del(self): if hasattr(self, "_pfunc_fill"): - self._plot_area.renderer.remove_event_handler( + self._fpl_plot_area.renderer.remove_event_handler( self._pfunc_fill, "pointer_down" ) del self._pfunc_fill diff --git a/fastplotlib/tools/__init__.py b/fastplotlib/tools/__init__.py index 80396c98d..c6eaa8fbe 100644 --- a/fastplotlib/tools/__init__.py +++ b/fastplotlib/tools/__init__.py @@ -1 +1,2 @@ from ._histogram_lut import HistogramLUTTool +from ._cursor import Cursor \ No newline at end of file diff --git a/fastplotlib/tools/_cursor.py b/fastplotlib/tools/_cursor.py new file mode 100644 index 000000000..ec631f4ee --- /dev/null +++ b/fastplotlib/tools/_cursor.py @@ -0,0 +1,152 @@ +from functools import partial + +import numpy as np + +import pygfx + + +from ..graphics._features import VertexPositions, UniformColor, Thickness +from ..layouts._subplot import Subplot +from ..graphics._base import Graphic + + +class Cursor: + def __init__( + self, + parent: Graphic = None, + children: list[Graphic] = None, + size: float = 10.0, + style: str = "+", + color: str | tuple | np.ndarray = "r", + alpha: float = 0.75, + key_toggle_moveable: str = "m", + key_add_sticky: str = "s", + ): + if children is None: + children = list() + + self._children = list() + + self._callbacks = dict() + + self._pointers: dict[Graphic, pygfx.Points] = dict() + self._sticky_pointers = dict[Graphic, list[pygfx.Points]] + + self._size = size + self._style = style + self._alpha = alpha + color = np.array(pygfx.Color(color)) + color[-1] = self._alpha + self._color = color + + # current x, y, z position of the cursor + self._position: np.ndarray = np.array([0., 0., 0.], dtype=np.float32) + + for child in children: + self.add(child) + + self._respond_pointer_move: bool = True + + def _make_pointer(self, offset) -> pygfx.WorldObject: + geo = pygfx.Geometry(positions=self.position[None, :]) + material = pygfx.PointsMarkerMaterial( + size=self.size, + size_space="world", + color=self._color, + marker=self.style + ) + wo = pygfx.Points( + geometry=geo, + material=material + ) + + # set z above + offset = offset.copy() + offset[-1] += 1 + wo.world.position = offset + + return wo + + @property + def position(self) -> np.ndarray: + return self._position + + @position.setter + def position(self, pos: tuple | list | np.ndarray): + pos = np.asarray(pos) + if not pos.shape == (3,): + raise ValueError + + self._position = pos + + for child in self.children: + wo = self._pointers[child] + pos = self.position.copy() + pos[-1] = child.offset[-1] + 1. + wo.geometry.positions.data[:] = pos + wo.geometry.positions.update_range() + + @property + def children(self) -> tuple[Graphic, ...]: + return tuple(self._children) + + @property + def size(self) -> float: + return self._size + + @property + def style(self) -> str: + return self._style + + def add(self, child: Graphic): + if child in self.children: + raise ValueError("child already registered") + + callback = partial(self._pointer_moved, child) + self._callbacks[child] = callback + + pointer = self._make_pointer(child.offset) + + self._pointers[child] = pointer + + child.add_event_handler(callback, "pointer_move") + child._fpl_plot_area.scene.add(pointer) + + self._children.append(child) + + def remove(self, child: Graphic): + if child not in self.children: + raise ValueError + + pointer = self._pointers.pop(child) + + callback = self._callbacks.pop(child) + + child.remove_event_handler(callback, "pointer_move") + + child._fpl_plot_area.scene.remove(pointer) + + self._children.remove(child) + + def _adjust_offsets(self, ev): + graphic = ev.graphic + offset = graphic.offset + offset[-1] += 1 + + self._pointers[graphic].world.position = offset + + def _pointer_moved(self, child: Graphic, ev: pygfx.PointerEvent): + if not self._respond_pointer_move: + return + + world_pos = child._fpl_plot_area.map_screen_to_world(ev) + + world_pos -= child.offset + + self.position = world_pos + + def _add_sticky_pointer(self): + pass + + def _toggle_respond_pointer_move(self): + self._respond_pointer_move = not self._respond_pointer_move diff --git a/fastplotlib/tools/_histogram_lut.py b/fastplotlib/tools/_histogram_lut.py index b8c6633a8..a87ca5df6 100644 --- a/fastplotlib/tools/_histogram_lut.py +++ b/fastplotlib/tools/_histogram_lut.py @@ -185,12 +185,12 @@ def _get_vmin_vmax_str(self) -> tuple[str, str]: return vmin_str, vmax_str def _fpl_add_plot_area_hook(self, plot_area): - self._plot_area = plot_area + self._fpl_plot_area = plot_area self._linear_region_selector._fpl_add_plot_area_hook(plot_area) self._histogram_line._fpl_add_plot_area_hook(plot_area) - self._plot_area.auto_scale() - self._plot_area.controller.enabled = True + self._fpl_plot_area.auto_scale() + self._fpl_plot_area.controller.enabled = True def _calculate_histogram(self, data): if data.ndim > 2: @@ -363,7 +363,7 @@ def set_data(self, data, reset_vmin_vmax: bool = True): self._cmap = None # reset plotarea dims - self._plot_area.auto_scale() + self._fpl_plot_area.auto_scale() @property def image_graphic(self) -> ImageGraphic: @@ -402,7 +402,7 @@ def _open_cmap_picker(self, ev): pos = ev.x, ev.y - self._plot_area.get_figure().open_popup("colormap-picker", pos, lut_tool=self) + self._fpl_plot_area.get_figure().open_popup("colormap-picker", pos, lut_tool=self) def _fpl_prepare_del(self): self._linear_region_selector._fpl_prepare_del() From 7564681c09ca31416227bf9884f6eb8283c02113 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Fri, 18 Oct 2024 06:18:38 -0400 Subject: [PATCH 2/2] forgot to commit a file --- fastplotlib/graphics/_features/_common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastplotlib/graphics/_features/_common.py b/fastplotlib/graphics/_features/_common.py index fe32a485f..1809fa3c7 100644 --- a/fastplotlib/graphics/_features/_common.py +++ b/fastplotlib/graphics/_features/_common.py @@ -18,8 +18,8 @@ def set_value(self, graphic, value: str): if not isinstance(value, str): raise TypeError("`Graphic` name must be of type ") - if graphic._plot_area is not None: - graphic._plot_area._check_graphic_name_exists(value) + if graphic._fpl_plot_area is not None: + graphic._fpl_plot_area._check_graphic_name_exists(value) self._value = value