From 0a300050f5ff15277e78cec76289eb14a7b72b06 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 16 Nov 2023 21:38:19 -0500 Subject: [PATCH] basic emulation of pointer movement, use test for linear_selector --- examples/notebooks/linear_selector_test.ipynb | 215 ++++++++++++++++++ fastplotlib/utils/emulation/__init__.py | 0 fastplotlib/utils/emulation/events.py | 115 ++++++++++ 3 files changed, 330 insertions(+) create mode 100644 examples/notebooks/linear_selector_test.ipynb create mode 100644 fastplotlib/utils/emulation/__init__.py create mode 100644 fastplotlib/utils/emulation/events.py diff --git a/examples/notebooks/linear_selector_test.ipynb b/examples/notebooks/linear_selector_test.ipynb new file mode 100644 index 000000000..18c48ee27 --- /dev/null +++ b/examples/notebooks/linear_selector_test.ipynb @@ -0,0 +1,215 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a06e1fd9-47df-42a3-a76c-19e23d7b89fd", + "metadata": {}, + "source": [ + "## `LinearSelector`, draggable selector that can optionally associated with an ipywidget." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb95ba19-14b5-4bf4-93d9-05182fa500cb", + "metadata": {}, + "outputs": [], + "source": [ + "import fastplotlib as fpl\n", + "from fastplotlib.graphics.selectors import Synchronizer\n", + "\n", + "import numpy as np\n", + "from ipywidgets import VBox, IntSlider, FloatSlider\n", + "\n", + "plot = fpl.Plot()\n", + "\n", + "# data to plot\n", + "xs = np.linspace(0, 100, 1000)\n", + "sine = np.sin(xs) * 20\n", + "\n", + "# make sine along x axis\n", + "sine_graphic = plot.add_line(np.column_stack([xs, sine]).astype(np.float32))\n", + "\n", + "# make some selectors\n", + "selector = sine_graphic.add_linear_selector()\n", + "selector2 = sine_graphic.add_linear_selector(20)\n", + "selector3 = sine_graphic.add_linear_selector(40)\n", + "\n", + "ss = Synchronizer(selector, selector2, selector3)\n", + "\n", + "def set_color_at_index(ev):\n", + " # changes the color at the index where the slider is\n", + " ix = ev.pick_info[\"selected_index\"]\n", + " g = ev.pick_info[\"graphic\"].parent\n", + " g.colors[ix] = \"green\"\n", + "\n", + "selector.selection.add_event_handler(set_color_at_index)\n", + "\n", + "# fastplotlib LineSelector can make an ipywidget slider and return it :D \n", + "ipywidget_slider = selector.make_ipywidget_slider()\n", + "ipywidget_slider.description = \"slider1\"\n", + "\n", + "# or you can make your own ipywidget sliders and connect them to the linear selector\n", + "ipywidget_slider2 = IntSlider(min=0, max=100, description=\"slider2\")\n", + "ipywidget_slider3 = FloatSlider(min=0, max=100, description=\"slider3\")\n", + "\n", + "selector2.add_ipywidget_handler(ipywidget_slider2, step=5)\n", + "selector3.add_ipywidget_handler(ipywidget_slider3, step=0.1)\n", + "\n", + "plot.auto_scale()\n", + "plot.show(sidecar=True, add_widgets=[ipywidget_slider])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d718fd9f-8795-40a3-a3aa-4869673d5036", + "metadata": {}, + "outputs": [], + "source": [ + "from pylinalg import vec_transform\n", + "from fastplotlib.utils.emulation.events import emulate_pointer_movement" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b962db7-a791-41cc-9d1b-ff9bd09b43b2", + "metadata": {}, + "outputs": [], + "source": [ + "# get ndc position of selector\n", + "ndc = vec_transform(selector.position, plot.camera.camera_matrix)\n", + "ndc" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ddb3370d-9782-427a-8d2b-efaef0ad9b27", + "metadata": {}, + "outputs": [], + "source": [ + "x, y = plot.canvas.get_logical_size()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "985b45aa-06a2-452d-98c5-255a7b3117ff", + "metadata": {}, + "outputs": [], + "source": [ + "screen_matrix = np.array([\n", + " [x / 2, 0, 0, (x - 1) /2],\n", + " [0, y / 2, 0, (y - 1) / 2],\n", + " [0, 0, 1, 0],\n", + " [0, 0, 0, 1]\n", + "])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1f5ca3d-d325-43c0-96ee-b5e862d8f344", + "metadata": {}, + "outputs": [], + "source": [ + "# get screen position of selector\n", + "screen_pos = vec_transform(ndc, screen_matrix)[:2]\n", + "screen_pos\n", + "\n", + "# set an end point for the selector\n", + "end_pos = screen_pos.copy()\n", + "end_pos[0] += 200 # +100 pixels in x for end position" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0aa7c696-d8d2-4d41-8cda-719ed8c0b582", + "metadata": {}, + "outputs": [], + "source": [ + "emulate_pointer_movement(\n", + " plot.renderer,\n", + " start_position=screen_pos,\n", + " end_position=end_pos,\n", + " button=1,\n", + " modifiers=\"Shift\",\n", + " n_steps=100\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "3b0f448f-bbe4-4b87-98e3-093f561c216c", + "metadata": {}, + "source": [ + "### Drag linear selectors with the mouse, hold \"Shift\" to synchronize movement of all the selectors" + ] + }, + { + "cell_type": "markdown", + "id": "c6f041b7-8779-46f1-8454-13cec66f53fd", + "metadata": {}, + "source": [ + "## Also works for line collections" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e36da217-f82a-4dfa-9556-1f4a2c7c4f1c", + "metadata": {}, + "outputs": [], + "source": [ + "sines = [sine] * 10\n", + "\n", + "plot = fpl.Plot()\n", + "\n", + "sine_stack = plot.add_line_stack(sines)\n", + "\n", + "colors = \"y\", \"blue\", \"red\", \"green\"\n", + "\n", + "selectors = list()\n", + "for i, c in enumerate(colors):\n", + " sel = sine_stack.add_linear_selector(i * 100, color=c, name=str(i))\n", + " selectors.append(sel)\n", + " \n", + "ss = Synchronizer(*selectors)\n", + "\n", + "plot.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "71ae4fca-f644-4d4f-8f32-f9d069bbc2f1", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/fastplotlib/utils/emulation/__init__.py b/fastplotlib/utils/emulation/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/fastplotlib/utils/emulation/events.py b/fastplotlib/utils/emulation/events.py new file mode 100644 index 000000000..0fd9e3049 --- /dev/null +++ b/fastplotlib/utils/emulation/events.py @@ -0,0 +1,115 @@ +from copy import deepcopy +from typing import * + +import numpy as np +import pygfx + + +def emulate_pointer_movement( + renderer: pygfx.WgpuRenderer, + start_position: np.ndarray, # in screen coordinates + end_position: np.ndarray, # in screen coordinates + n_steps: int = 100, + button: int = 1, + modifiers: List[str] = None, + check_target: pygfx.WorldObject = None +) -> pygfx.WorldObject: + """ + Emulate a pointer_down -> pointer_move -> pointer_up event series. + + Parameters + ---------- + renderer: pygfx.WgpuRenderer + the renderer + + start_position: np.ndarray + start position of pointer event, [x, y, z] in screen coordinates + + end_position: np.ndarray + end position of pointer event, [x, y, z] in screen coordinates + + n_steps: int, default 100 + number of pointer_move events between pointer_down and pointer_up + + button: int + pointer button to emulate + 1: left + 2: + modifiers: List[str] + modifiers, "Shift", "Alt", etc. + + check_target: pygfx.WorldObject, optional + if provided, asserts that the pointer_down event targets the provided `check_target` + + Returns + ------- + Union[pygfx.WorldObject, None] + The world object that was interacted with the pointer events. + ``None`` if no world object was interacted with. + + """ + if modifiers is None: + modifiers = list() + + # instead of manually defining the target + # use get_pick_info to make sure the target is pickable! + pick_info = renderer.get_pick_info(start_position) + + if check_target is not None: + assert pick_info["world_object"] is check_target + + # start and stop positions + x1, y1 = start_position[:2] + xn, yn = end_position[:2] + + # create event info dict + event_info = { + "x": x1, + "y": y1, + "modifiers": modifiers, + "button": button, + "buttons": [button], + } + + # pointer down event + pointer_down = pygfx.PointerEvent( + type="pointer_down", + target=pick_info["world_object"], + **event_info + ) + + # pointer move event, xy will be set per step + pointer_move = pygfx.PointerEvent( + type="pointer_move", + **event_info + ) + + # create event info for pointer_up event to end the emulation + event_info = deepcopy(event_info) + event_info["x"], event_info["y"] = end_position + + event_info = deepcopy(event_info) + + pointer_up = pygfx.PointerEvent( + type="pointer_up", + **event_info + ) + + # start emulating the event series + renderer.dispatch_event(pointer_down) + + x_steps = np.linspace(x1, xn, n_steps) + y_steps = np.linspace(y1, yn, n_steps) + + # the pointer move events, move uniformly between start_position and end_position + for dx, dy in zip(x_steps, y_steps): + pointer_move.x = dx + pointer_move.dy = dy + + # move the pointer by dx, dy + renderer.dispatch_event(pointer_move) + + # end event emulation + renderer.dispatch_event(pointer_up) + + return pick_info["world_object"]