From 6751bd59a401f218feb1630fc4869ac4c51df7dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=AD=E4=BA=8E=E6=96=8C?= <1931127624@qq.com> Date: Thu, 30 Apr 2020 14:09:25 +0800 Subject: [PATCH] [GUI] Better event filtering system (#801) * [GUI] better GUI filter system (gui.get_event) * [skip ci] GUI.EventFilter * [skip ci] set() * [skip ci] rename * [skip ci] revert * fix usage & add yield * [skip ci] bette example * [skip ci] refactor * [skip ci] better rgb_to_hex * [skip ci] revert * [skip ci] fix revert typo * [skip ci] enforce code format * [skip ci] revert comp * [skip ci] arrange * [skip ci] enforce code format * [skip ci] apply reviews * better OR logic * assert len(combination entry) == 2 * [skip ci] fix doc * [skip ci] update doc for gui * [skip ci] Update docs/gui.rst Co-authored-by: Taichi Gardener --- docs/gui.rst | 269 +++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + docs/utilities.rst | 25 ---- docs/vector.rst | 2 + examples/keyboard.py | 14 +- examples/mpm99.py | 2 +- examples/nbody_oscillator.py | 2 +- examples/quadtree.py | 14 +- python/taichi/misc/gui.py | 37 ++++- python/taichi/misc/image.py | 2 +- 10 files changed, 321 insertions(+), 47 deletions(-) create mode 100644 docs/gui.rst diff --git a/docs/gui.rst b/docs/gui.rst new file mode 100644 index 0000000000000..2570d68b5e2a7 --- /dev/null +++ b/docs/gui.rst @@ -0,0 +1,269 @@ +.. _gui: + +GUI system +========== + +Taichi has a built-in GUI system to help users display graphic results easier. + + +Create a window +--------------- + + +.. function:: ti.GUI(title, res, bgcolor = 0x000000) + + :parameter title: (string) the window title + :parameter res: (scalar or tuple) resolution / size of the window + :parameter bgcolor: (optional, RGB hex) background color of the window + :return: (GUI) an object represents the window + + Create a window. + If ``res`` is scalar, then width will be equal to height. + + This creates a window whose width is 1024, height is 768: + + :: + + gui = ti.GUI('Window Title', (1024, 768)) + + +.. function:: gui.show(filename = None) + + :parameter gui: (GUI) the window object + :parameter filename: (optional, string) see notes below + + Show the window on the screen. + + .. note:: + If `filename` is specified, screenshot will be saved to the file specified by the name. For example, this screenshots each frame of the window, and save it in ``.png``'s: + + :: + + for frame in range(10000): + render(img) + gui.set_image(img) + gui.show(f'{frame:06d}.png') + + +Paint a window +-------------- + + +.. function:: gui.set_image(img) + + :parameter gui: (GUI) the window object + :parameter img: (np.array or Tensor) tensor containing the image, see notes below + + Set a image to display on the window. + + The pixel, ``i`` from bottom to up, ``j`` from left to right, is set to the value of ``img[i, j]``. + + + If the window size is ``(x, y)``, then the ``img`` must be one of: + + * ``ti.var(shape=(x, y))``, a grey-scale image + + * ``ti.var(shape=(x, y, 3))``, where `3` is for `(r, g, b)` channels + + * ``ti.Vector(3, shape=(x, y))`` (see :ref:`vector`) + + * ``np.ndarray(shape=(x, y))`` + + * ``np.ndarray(shape=(x, y, 3))`` + + + The data type of ``img`` must be one of: + + * float32, clamped into [0, 1] + + * float64, clamped into [0, 1] + + * uint8, range [0, 255] + + * uint16, range [0, 65535] + + * uint32, range [0, UINT_MAX] + + +.. function:: gui.circle(pos, color = 0xFFFFFF, radius = 1) + + :parameter gui: (GUI) the window object + :parameter pos: (tuple of 2) the position of circle + :parameter color: (optional, RGB hex) color to fill the circle + :parameter radius: (optional, scalar) the radius of circle + + Draw a solid circle. + + +.. function:: gui.circles(pos, color = 0xFFFFFF, radius = 1) + + :parameter gui: (GUI) the window object + :parameter pos: (np.array) the position of circles + :parameter color: (optional, RGB hex or np.array of uint32) color(s) to fill circles + :parameter radius: (optional, scalar) the radius of circle + + Draw solid circles. + +.. note:: + + If ``color`` is a numpy array, circle at ``pos[i]`` will be colored with ``color[i]``, therefore it must have the same size with ``pos``. + + +.. function:: gui.line(begin, end, color = 0xFFFFFF, radius = 1) + + :parameter gui: (GUI) the window object + :parameter begin: (tuple of 2) the first end point position of line + :parameter end: (tuple of 2) the second end point position of line + :parameter color: (optional, RGB hex) the color of line + :parameter radius: (optional, scalar) the width of line + + Draw a line. + + +.. function:: gui.triangle(a, b, c, color = 0xFFFFFF) + + :parameter gui: (GUI) the window object + :parameter a: (tuple of 2) the first end point position of triangle + :parameter b: (tuple of 2) the second end point position of triangle + :parameter c: (tuple of 2) the third end point position of triangle + :parameter color: (optional, RGB hex) the color to fill the triangle + + Draw a solid triangle. + + +.. function:: gui.rect(topleft, bottomright, radius = 1, color = 0xFFFFFF) + + :parameter gui: (GUI) the window object + :parameter topleft: (tuple of 2) the top-left point position of rectangle + :parameter bottomright: (tuple of 2) the bottom-right point position of rectangle + :parameter color: (optional, RGB hex) the color of stroke line + :parameter radius: (optional, scalar) the width of stroke line + + Draw a hollow rectangle. + + +Event processing +---------------- + +Every event have a key and type. +*Event key* is the key that you pressed on keyboard or mouse, can be one of: + +:: + + ti.GUI.ESCAPE + ti.GUI.SHIFT + ti.GUI.LEFT + 'a' + 'b' + ... + ti.GUI.LMB + ti.GUI.RMB + +*Event type* is the type of event, for now, there are just three type of event: + +:: + + ti.GUI.RELEASE # key up + ti.GUI.PRESS # key down + ti.GUI.MOTION # mouse moved + + +A *event filter* is a list combined of *key*, *type* and *(type, key)* tuple, e.g.: + +.. code-block:: + + # if ESC pressed or released: + gui.get_event(ti.GUI.ESCAPE) + + # if any key is pressed: + gui.get_event(ti.GUI.PRESS) + + # if ESC pressed or SPACE released: + gui.get_event((ti.GUI.PRESS, ti.GUI.ESCAPE), (ti.GUI.RELEASE, ti.GUI.SPACE)) + + +.. function:: gui.get_event(a, ...) + + :parameter gui: (GUI) + :parameter a: (optional, EventFilter) filter out matched events + :return: (bool) ``False`` if there is no pending event, vise versa + + Try to pop a event from the queue, and store it in ``gui.event``. + + For example: + + :: + + while gui.get_event(): + print('Event key', gui.event.key) + + + For example, loop until ESC is pressed: + + :: + + gui = ti.GUI('Title', (640, 480)) + while not gui.get_event(ti.GUI.ESCAPE): + gui.set_image(img) + gui.show() + +.. function:: gui.get_events(a, ...) + + :parameter gui: (GUI) + :parameter a: (optional, EventFilter) filter out matched events + :return: (generator) a python generator, see below + + Basically the same as ``gui.get_event``, except for this one returns a generator of events instead of storing into ``gui.event``: + + :: + + for e in gui.get_events(): + if e.key == ti.GUI.ESCAPE: + exit() + elif e.type == ti.GUI.SPACE: + do_something() + elif e.type in ['a', ti.GUI.LEFT]: + ... + +.. function:: gui.is_pressed(key, ...) + + :parameter gui: (GUI) + :parameter key: (EventKey) keys you want to detect + :return: (bool) ``True`` if one of the keys pressed, vice versa + + .. warning:: + + Must be used together with ``gui.get_event``, or it won't be updated! + For example: + + :: + + while True: + gui.get_event() # must be called before is_pressed + if gui.is_pressed('a', ti.GUI.LEFT): + print('Go left!') + elif gui.is_pressed('d', ti.GUI.RIGHT): + print('Go right!') + +.. function:: gui.get_cursor_pos() + + :parameter gui: (GUI) + :return: (tuple of 2) current cursor position within the window + + For example: + + :: + + mouse_x, mouse_y = gui.get_cursor_pos() + + +Image I/O +--------- + +.. code-block:: python + + img = ti.imread('hello.png') + ti.imshow(img, 'Window Title') + ti.imwrite(img, 'hello2.png') + +TODO: complete here diff --git a/docs/index.rst b/docs/index.rst index 0fa966f0785b1..76ed819d0ed1f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -57,6 +57,7 @@ The Taichi Programming Language :maxdepth: 3 utilities + gui global_settings performance acknowledgments diff --git a/docs/utilities.rst b/docs/utilities.rst index 42efcf8758b65..9800905e384ab 100644 --- a/docs/utilities.rst +++ b/docs/utilities.rst @@ -1,31 +1,6 @@ Utilities ================================== -TODO: update - -GUI system ----------- - -.. code-block:: python - - gui = ti.GUI('Title', (640, 480)) - while not gui.is_pressed(ti.GUI.ESCAPE): - gui.set_image(img) - gui.show() - - -Also checkout ``examples/keyboard.py`` for more advanced event processing. - - -Image I/O ---------- - -.. code-block:: python - - img = ti.imread('hello.png') - ti.imshow(img, 'Window Title') - ti.imwrite(img, 'hello2.png') - Logging ------- diff --git a/docs/vector.rst b/docs/vector.rst index fed835049d435..f0a5a049876c4 100644 --- a/docs/vector.rst +++ b/docs/vector.rst @@ -8,6 +8,8 @@ A vector in Taichi can have two forms: - as a temporary local variable. An ``n`` component vector consists of ``n`` scalar values. - as an element of a global tensor. In this case, the tensor is an N-dimensional array of ``n`` component vectors +See :ref:`tensor_matrix` for more details. + Declaration ----------- diff --git a/examples/keyboard.py b/examples/keyboard.py index 21533bfce4bff..194d18b193cbf 100644 --- a/examples/keyboard.py +++ b/examples/keyboard.py @@ -6,14 +6,11 @@ gui = ti.GUI("Keyboard", res=(400, 400)) while True: - while gui.has_key_event(): - e = gui.get_key_event() - if e.type == ti.GUI.RELEASE: - continue - if e.key == ti.GUI.ESCAPE: + while gui.get_event(ti.GUI.PRESS): + if gui.event.key == ti.GUI.ESCAPE: exit() - elif e.key == ti.GUI.RMB: - x, y = e.pos[0], e.pos[1] + elif gui.event.key == ti.GUI.RMB: + x, y = gui.event.pos if gui.is_pressed(ti.GUI.LEFT, 'a'): x -= delta @@ -24,8 +21,7 @@ if gui.is_pressed(ti.GUI.DOWN, 's'): y -= delta if gui.is_pressed(ti.GUI.LMB): - pos = gui.get_cursor_pos() - x, y = pos[0], pos[1] + x, y = gui.get_cursor_pos() gui.circle((x, y), 0xffffff, 8) gui.show() diff --git a/examples/mpm99.py b/examples/mpm99.py index 43a1b39de4129..b3bda7c5d4df8 100644 --- a/examples/mpm99.py +++ b/examples/mpm99.py @@ -91,7 +91,7 @@ def initialize(): Jp[i] = 1 initialize() gui = ti.GUI("Taichi MLS-MPM-99", res=512, background_color=0x112F41) -for frame in range(20000): +while not gui.get_event(ti.GUI.ESCAPE): for s in range(int(2e-3 // dt)): substep() colors = np.array([0x068587, 0xED553B, 0xEEEEF0], dtype=np.uint32) diff --git a/examples/nbody_oscillator.py b/examples/nbody_oscillator.py index 6753bd05c3a4c..6589111b5fc2e 100644 --- a/examples/nbody_oscillator.py +++ b/examples/nbody_oscillator.py @@ -42,7 +42,7 @@ def advance(dt: ti.f32): gui = ti.GUI("n-body", res=(400, 400)) initialize() -while not gui.has_key_event() or gui.get_key_event().key == ti.GUI.MOTION: +while not gui.get_event(ti.GUI.ESCAPE): _pos = pos.to_numpy() gui.circles(_pos, radius=1, color=0x66ccff) gui.show() diff --git a/examples/quadtree.py b/examples/quadtree.py index 16aa398fa6218..ce617e8f0831b 100644 --- a/examples/quadtree.py +++ b/examples/quadtree.py @@ -6,7 +6,7 @@ RES = 1024 K = 2 R = 7 -N = K ** R +N = K**R Broot = ti.root B = ti.root @@ -18,12 +18,14 @@ img = ti.Vector(3, dt=ti.f32, shape=(RES, RES)) + @ti.kernel def action(p: ti.ext_arr()): a = ti.cast(p[0] * N, ti.i32) b = ti.cast(p[1] * N, ti.i32) qt[a, b] = 1 + @ti.func def draw_rect(b, i, j, s, k, dx, dy): x = i // s @@ -34,6 +36,7 @@ def draw_rect(b, i, j, s, k, dx, dy): a += ti.is_active(b, [x - dx, y - dy]) return a + @ti.kernel def paint(): for i, j in img: @@ -42,15 +45,16 @@ def paint(): for i, j in img: s = RES // N for r in ti.static(range(R)): - k = RES // K ** (R-r) - ia = draw_rect(qt.parent(r+1), i, j, s, k, 1, 0) - ja = draw_rect(qt.parent(r+1), i, j, s, k, 0, 1) - img[i, j][0] += (ia + ja) * ((R-r) / R) ** 2 + k = RES // K**(R - r) + ia = draw_rect(qt.parent(r + 1), i, j, s, k, 1, 0) + ja = draw_rect(qt.parent(r + 1), i, j, s, k, 0, 1) + img[i, j][0] += (ia + ja) * ((R - r) / R)**2 def vec2_npf32(m): return np.array([m[0], m[1]], dtype=np.float32) + gui = ti.GUI('Quadtree', (RES, RES)) while not gui.get_event(ti.GUI.PRESS): Broot.deactivate_all() diff --git a/python/taichi/misc/gui.py b/python/taichi/misc/gui.py index 88ed22b4115e1..2c3620c419190 100644 --- a/python/taichi/misc/gui.py +++ b/python/taichi/misc/gui.py @@ -139,17 +139,42 @@ def show(self, file=None): self.core.screenshot(file) self.clear(self.background_color) + class EventFilter: + def __init__(self, *filter): + self.filter = set() + for ent in filter: + if isinstance(ent, (list, tuple)): + type, key = ent + ent = (type, key) + self.filter.add(ent) + + def match(self, e): + if (e.type, e.key) in self.filter: + return True + if e.type in self.filter: + return True + if e.key in self.filter: + return True + return False + def has_key_event(self): return self.core.has_key_event() def get_event(self, *filter): + for e in self.get_events(*filter): + self.event = e + return True + return False + + def get_events(self, *filter): + filter = filter and GUI.EventFilter(*filter) or None + while True: if not self.has_key_event(): - return False - self.event = self.get_key_event() - if not len(filter) or self.event.type in filter: break - return True + e = self.get_key_event() + if filter is None or filter.match(e): + yield e def get_key_event(self): self.core.wait_key_event() @@ -157,6 +182,7 @@ def get_key_event(self): e.key = self.core.get_key_event_head_key() e.type = self.core.get_key_event_head_type() e.pos = self.core.get_key_event_head_pos() + e.pos = (e.pos[0], e.pos[1]) e.modifier = [] for mod in ['Shift', 'Alt', 'Control']: if self.is_pressed(mod): @@ -179,7 +205,8 @@ def is_pressed(self, *keys): return False def get_cursor_pos(self): - return self.core.get_cursor_pos() + pos = self.core.get_cursor_pos() + return pos[0], pos[1] def has_key_pressed(self): if self.has_key_event(): diff --git a/python/taichi/misc/image.py b/python/taichi/misc/image.py index eca7ba3449ed4..30469709e4820 100644 --- a/python/taichi/misc/image.py +++ b/python/taichi/misc/image.py @@ -37,6 +37,6 @@ def imread(filename, channels=0): def imshow(img, winname='Taichi'): img = cook_image(img) gui = ti.GUI(winname, res=img.shape[:2]) - while not gui.is_pressed(ti.GUI.ESCAPE): + while not gui.get_event(ti.GUI.ESCAPE): gui.set_image(img) gui.show()