Skip to content

Tests with pointer emulation #374

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
215 changes: 215 additions & 0 deletions examples/notebooks/linear_selector_test.ipynb
Original file line number Diff line number Diff line change
@@ -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
}
Empty file.
115 changes: 115 additions & 0 deletions fastplotlib/utils/emulation/events.py
Original file line number Diff line number Diff line change
@@ -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"]