vispy 0.9.5__cp38-cp38-win_amd64.whl → 0.14.0__cp38-cp38-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of vispy might be problematic. Click here for more details.

Files changed (103) hide show
  1. vispy/app/backends/_glfw.py +2 -2
  2. vispy/app/backends/_pyglet.py +8 -2
  3. vispy/app/backends/_qt.py +88 -63
  4. vispy/app/backends/_wx.py +6 -1
  5. vispy/app/canvas.py +4 -2
  6. vispy/app/tests/test_canvas.py +52 -1
  7. vispy/app/tests/test_context.py +5 -3
  8. vispy/color/color_array.py +8 -1
  9. vispy/color/colormap.py +5 -25
  10. vispy/geometry/meshdata.py +76 -38
  11. vispy/geometry/rect.py +6 -0
  12. vispy/geometry/tests/test_meshdata.py +72 -0
  13. vispy/gloo/buffer.py +12 -0
  14. vispy/gloo/gl/_constants.py +9 -5
  15. vispy/gloo/gl/_es2.py +8 -4
  16. vispy/gloo/gl/_gl2.py +2 -3
  17. vispy/gloo/gl/_proxy.py +1 -1
  18. vispy/gloo/gl/_pyopengl2.py +12 -7
  19. vispy/gloo/gl/tests/test_names.py +3 -0
  20. vispy/gloo/glir.py +26 -13
  21. vispy/gloo/program.py +39 -22
  22. vispy/gloo/tests/test_program.py +9 -2
  23. vispy/gloo/tests/test_texture.py +19 -2
  24. vispy/gloo/texture.py +46 -16
  25. vispy/gloo/wrappers.py +4 -2
  26. vispy/glsl/build_spatial_filters.py +241 -293
  27. vispy/glsl/misc/spatial-filters.frag +1299 -254
  28. vispy/io/_data/spatial-filters.npy +0 -0
  29. vispy/io/datasets.py +2 -2
  30. vispy/io/image.py +1 -1
  31. vispy/io/stl.py +3 -3
  32. vispy/scene/cameras/base_camera.py +6 -2
  33. vispy/scene/cameras/panzoom.py +10 -14
  34. vispy/scene/cameras/perspective.py +6 -0
  35. vispy/scene/cameras/tests/test_cameras.py +27 -0
  36. vispy/scene/cameras/tests/test_perspective.py +37 -0
  37. vispy/scene/cameras/turntable.py +39 -23
  38. vispy/scene/canvas.py +9 -5
  39. vispy/scene/events.py +9 -0
  40. vispy/scene/node.py +19 -2
  41. vispy/scene/tests/test_canvas.py +30 -1
  42. vispy/scene/tests/test_visuals.py +113 -0
  43. vispy/scene/visuals.py +6 -1
  44. vispy/scene/widgets/viewbox.py +3 -2
  45. vispy/testing/_runners.py +6 -12
  46. vispy/testing/_testing.py +3 -4
  47. vispy/util/check_environment.py +4 -4
  48. vispy/util/gallery_scraper.py +50 -32
  49. vispy/util/tests/test_gallery_scraper.py +2 -0
  50. vispy/util/transforms.py +1 -1
  51. vispy/util/wrappers.py +1 -1
  52. vispy/version.py +2 -3
  53. vispy/visuals/__init__.py +2 -0
  54. vispy/visuals/_scalable_textures.py +20 -17
  55. vispy/visuals/collections/array_list.py +3 -3
  56. vispy/visuals/collections/base_collection.py +1 -1
  57. vispy/visuals/ellipse.py +1 -1
  58. vispy/visuals/filters/__init__.py +3 -2
  59. vispy/visuals/filters/base_filter.py +120 -0
  60. vispy/visuals/filters/clipping_planes.py +24 -12
  61. vispy/visuals/filters/markers.py +28 -0
  62. vispy/visuals/filters/mesh.py +61 -6
  63. vispy/visuals/filters/tests/test_primitive_picking_filters.py +70 -0
  64. vispy/visuals/graphs/graph.py +1 -1
  65. vispy/visuals/image.py +114 -26
  66. vispy/visuals/image_complex.py +130 -0
  67. vispy/visuals/instanced_mesh.py +152 -0
  68. vispy/visuals/isocurve.py +1 -1
  69. vispy/visuals/line/dash_atlas.py +46 -41
  70. vispy/visuals/line/line.py +2 -5
  71. vispy/visuals/markers.py +310 -384
  72. vispy/visuals/mesh.py +2 -2
  73. vispy/visuals/shaders/function.py +3 -0
  74. vispy/visuals/shaders/tests/test_function.py +6 -0
  75. vispy/visuals/tests/test_axis.py +2 -2
  76. vispy/visuals/tests/test_image.py +92 -2
  77. vispy/visuals/tests/test_image_complex.py +36 -0
  78. vispy/visuals/tests/test_instanced_mesh.py +50 -0
  79. vispy/visuals/tests/test_markers.py +6 -0
  80. vispy/visuals/tests/test_mesh.py +17 -0
  81. vispy/visuals/tests/test_text.py +11 -0
  82. vispy/visuals/tests/test_volume.py +218 -12
  83. vispy/visuals/text/_sdf_cpu.cp38-win_amd64.pyd +0 -0
  84. vispy/visuals/text/_sdf_cpu.pyx +21 -23
  85. vispy/visuals/text/text.py +9 -3
  86. vispy/visuals/tube.py +2 -2
  87. vispy/visuals/visual.py +144 -3
  88. vispy/visuals/volume.py +300 -131
  89. {vispy-0.9.5.dist-info → vispy-0.14.0.dist-info}/LICENSE.txt +1 -1
  90. {vispy-0.9.5.dist-info → vispy-0.14.0.dist-info}/METADATA +218 -198
  91. {vispy-0.9.5.dist-info → vispy-0.14.0.dist-info}/RECORD +93 -96
  92. {vispy-0.9.5.dist-info → vispy-0.14.0.dist-info}/WHEEL +1 -1
  93. vispy/glsl/antialias/__init__.py +0 -0
  94. vispy/glsl/arrowheads/__init__.py +0 -0
  95. vispy/glsl/arrows/__init__.py +0 -0
  96. vispy/glsl/collections/__init__.py +0 -0
  97. vispy/glsl/colormaps/__init__.py +0 -0
  98. vispy/glsl/lines/__init__.py +0 -0
  99. vispy/glsl/markers/__init__.py +0 -0
  100. vispy/glsl/math/__init__.py +0 -0
  101. vispy/glsl/misc/__init__.py +0 -0
  102. vispy/glsl/transforms/__init__.py +0 -0
  103. {vispy-0.9.5.dist-info → vispy-0.14.0.dist-info}/top_level.txt +0 -0
vispy/testing/_testing.py CHANGED
@@ -14,7 +14,7 @@ import gc
14
14
  import pytest
15
15
  import functools
16
16
 
17
- from distutils.version import LooseVersion
17
+ from packaging.version import Version
18
18
 
19
19
  from ..util.check_environment import has_backend
20
20
 
@@ -251,7 +251,7 @@ def has_ipython(version='3.0'):
251
251
  except Exception:
252
252
  return False, "IPython library not found"
253
253
  else:
254
- if LooseVersion(IPython.__version__) >= LooseVersion(version):
254
+ if Version(IPython.__version__) >= Version(version):
255
255
  return True, "IPython present"
256
256
  else:
257
257
  message = (
@@ -284,8 +284,7 @@ def _has_scipy(min_version):
284
284
  try:
285
285
  assert isinstance(min_version, str)
286
286
  import scipy # noqa, analysis:ignore
287
- from distutils.version import LooseVersion
288
- this_version = LooseVersion(scipy.__version__)
287
+ this_version = Version(scipy.__version__)
289
288
  if this_version < min_version:
290
289
  return False
291
290
  except Exception:
@@ -2,7 +2,7 @@
2
2
  # Copyright (c) Vispy Development Team. All Rights Reserved.
3
3
  # Distributed under the (new) BSD License. See LICENSE.txt for more info.
4
4
  import os
5
- from distutils.version import LooseVersion
5
+ from packaging.version import Version
6
6
 
7
7
  from vispy.util import use_log_level
8
8
 
@@ -14,7 +14,7 @@ def has_matplotlib(version='1.2'):
14
14
  except Exception:
15
15
  has_mpl = False
16
16
  else:
17
- if LooseVersion(matplotlib.__version__) >= LooseVersion(version):
17
+ if Version(matplotlib.__version__) >= Version(version):
18
18
  has_mpl = True
19
19
  else:
20
20
  has_mpl = False
@@ -27,8 +27,8 @@ def has_skimage(version='0.11'):
27
27
  import skimage
28
28
  except ImportError:
29
29
  return False
30
- sk_version = LooseVersion(skimage.__version__)
31
- return sk_version >= LooseVersion(version)
30
+ sk_version = Version(skimage.__version__)
31
+ return sk_version >= Version(version)
32
32
 
33
33
 
34
34
  def has_backend(backend, has=(), capable=(), out=()):
@@ -60,7 +60,7 @@ class VisPyGalleryScraper:
60
60
  example_fn = block_vars["src_file"]
61
61
  frame_num_list = self._get_frame_list_from_source(example_fn)
62
62
  image_path_iterator = block_vars['image_path_iterator']
63
- canvas_or_widget = self._get_canvaslike_from_globals(block_vars["example_globals"])
63
+ canvas_or_widget = get_canvaslike_from_globals(block_vars["example_globals"])
64
64
  if not frame_num_list:
65
65
  image_paths = []
66
66
  elif isinstance(frame_num_list[0], str):
@@ -89,6 +89,7 @@ class VisPyGalleryScraper:
89
89
  frame_grabber.save_animation(image_path)
90
90
  else:
91
91
  frame_grabber.save_frame(image_path)
92
+ frame_grabber.cleanup()
92
93
  if 'images' in gallery_conf['compress_images']:
93
94
  optipng(image_path, gallery_conf['compress_images_args'])
94
95
  return [image_path]
@@ -133,36 +134,38 @@ class VisPyGalleryScraper:
133
134
  frame_paths.append(frame_fn)
134
135
  return frame_paths
135
136
 
136
- def _get_canvaslike_from_globals(self, globals_dict):
137
- qt_widget = self._get_qt_top_parent(globals_dict)
138
- if qt_widget is not None:
139
- return qt_widget
140
-
141
- # Get canvas
142
- if "canvas" in globals_dict:
143
- return globals_dict["canvas"]
144
- if "Canvas" in globals_dict:
145
- return globals_dict["Canvas"]()
146
- if "fig" in globals_dict:
147
- return globals_dict["fig"]
148
- return None
149
137
 
150
- @staticmethod
151
- def _get_qt_top_parent(globals_dict):
152
- if "QWidget" not in globals_dict and "QMainWindow" not in globals_dict:
153
- return None
154
-
155
- qmainwindow = globals_dict.get("QMainWindow")
156
- qwidget = globals_dict.get("QWidget", qmainwindow)
157
- all_qt_widgets = [widget for widget in globals_dict.values()
158
- if isinstance(widget, qwidget) and widget is not None]
159
- all_qt_mains = [widget for widget in all_qt_widgets if isinstance(widget, qmainwindow)]
160
- if all_qt_mains:
161
- return all_qt_mains[0]
162
- if all_qt_widgets:
163
- return all_qt_widgets[0]
138
+ def get_canvaslike_from_globals(globals_dict):
139
+ qt_widget = _get_qt_top_parent(globals_dict)
140
+ if qt_widget is not None:
141
+ return qt_widget
142
+
143
+ # Get canvas
144
+ if "canvas" in globals_dict:
145
+ return globals_dict["canvas"]
146
+ if "Canvas" in globals_dict:
147
+ return globals_dict["Canvas"]()
148
+ if "fig" in globals_dict:
149
+ return globals_dict["fig"]
150
+ return None
151
+
152
+
153
+ def _get_qt_top_parent(globals_dict):
154
+ if "QWidget" not in globals_dict and "QMainWindow" not in globals_dict and "QtWidgets" not in globals_dict:
164
155
  return None
165
156
 
157
+ qtwidgets = globals_dict.get("QtWidgets")
158
+ qmainwindow = globals_dict.get("QMainWindow", getattr(qtwidgets, "QMainWindow", None))
159
+ qwidget = globals_dict.get("QWidget", getattr(qtwidgets, "QWidget", qmainwindow))
160
+ all_qt_widgets = [widget for widget in globals_dict.values()
161
+ if isinstance(widget, qwidget) and widget is not None]
162
+ all_qt_mains = [widget for widget in all_qt_widgets if isinstance(widget, qmainwindow)]
163
+ if all_qt_mains:
164
+ return all_qt_mains[0]
165
+ if all_qt_widgets:
166
+ return all_qt_widgets[0]
167
+ return None
168
+
166
169
 
167
170
  class FrameGrabber:
168
171
  """Helper to grab a series of screenshots from the current Canvas-like object."""
@@ -174,6 +177,13 @@ class FrameGrabber:
174
177
  self._collected_images = []
175
178
  self._frames_to_grab = frame_grab_list[:] # copy so original list is preserved
176
179
 
180
+ def cleanup(self):
181
+ from PyQt5.QtWidgets import QApplication
182
+ for child_widget in QApplication.allWidgets():
183
+ if hasattr(child_widget, 'close'):
184
+ child_widget.close()
185
+ QApplication.processEvents()
186
+
177
187
  def on_draw(self, _):
178
188
  if self._done:
179
189
  return # Grab only once
@@ -204,13 +214,19 @@ class FrameGrabber:
204
214
 
205
215
  def _grab_qt_screenshot(self):
206
216
  from PyQt5.QtWidgets import QApplication
217
+ from PyQt5.QtCore import QTimer
207
218
  self._canvas.show()
208
219
  # Qt is going to grab from the screen so we need the window on top
209
220
  self._canvas.raise_()
210
221
  # We need to give the GUI event loop and OS time to draw everything
222
+ time.sleep(1.5)
211
223
  QApplication.processEvents()
224
+ QTimer.singleShot(1000, self._grab_widget_screenshot)
212
225
  time.sleep(1.5)
213
226
  QApplication.processEvents()
227
+
228
+ def _grab_widget_screenshot(self):
229
+ from PyQt5.QtWidgets import QApplication
214
230
  screen = QApplication.screenAt(self._canvas.pos())
215
231
  screenshot = screen.grabWindow(int(self._canvas.windowHandle().winId()))
216
232
  arr = self._qpixmap_to_ndarray(screenshot)
@@ -218,14 +234,16 @@ class FrameGrabber:
218
234
 
219
235
  @staticmethod
220
236
  def _qpixmap_to_ndarray(pixmap):
237
+ from PyQt5 import QtGui
221
238
  import numpy as np
222
- im = pixmap.toImage()
239
+ im = pixmap.toImage().convertToFormat(QtGui.QImage.Format.Format_RGB32)
223
240
  size = pixmap.size()
224
241
  width = size.width()
225
242
  height = size.height()
226
- im_bits = im.bits()
227
- im_bits.setsize(height * width * 4) # RGBA
228
- return np.frombuffer(im_bits, np.uint8).reshape((height, width, 4))
243
+ im_bits = im.constBits()
244
+ im_bits.setsize(height * width * 4)
245
+ # Convert 0xffRRGGBB buffer -> (B, G, R, 0xff) -> (R, G, B)
246
+ return np.array(im_bits).reshape((height, width, 4))[:, :, 2::-1]
229
247
 
230
248
  def _grab_vispy_screenshots(self):
231
249
  os.environ['VISPY_IGNORE_OLD_VERSION'] = 'true'
@@ -16,6 +16,8 @@ except ImportError:
16
16
 
17
17
  from ..gallery_scraper import VisPyGalleryScraper
18
18
 
19
+ pytest.importorskip("PyQt5", reason="Gallery scraper only supports PyQt5")
20
+
19
21
 
20
22
  def _create_fake_block_vars(canvas):
21
23
  block_vars = {
vispy/util/transforms.py CHANGED
@@ -54,7 +54,7 @@ def scale(s, dtype=None):
54
54
 
55
55
 
56
56
  def rotate(angle, axis, dtype=None):
57
- """The 3x3 rotation matrix for rotation about a vector.
57
+ """The 4x4 rotation matrix for rotation about a vector.
58
58
 
59
59
  Parameters
60
60
  ----------
vispy/util/wrappers.py CHANGED
@@ -9,7 +9,7 @@ and vispy.gloo.gl can be used independently, they are not complely
9
9
  independent for some configureation. E.g. when using real ES 2.0,
10
10
  the app backend should use EGL and not a desktop OpenGL context. Also,
11
11
  we probably want it to be easy to configure vispy to use the ipython
12
- notebook backend, which requires specifc config of both app and gl.
12
+ notebook backend, which requires specific config of both app and gl.
13
13
 
14
14
  This module does not have to be aware of the available app and gl
15
15
  backends, but it should be(come) aware of (in)compatibilities between
vispy/version.py CHANGED
@@ -1,5 +1,4 @@
1
- # coding: utf-8
2
1
  # file generated by setuptools_scm
3
2
  # don't change, don't track in version control
4
- version = '0.9.5'
5
- version_tuple = (0, 9, 5)
3
+ __version__ = version = '0.14.0'
4
+ __version_tuple__ = version_tuple = (0, 14, 0)
vispy/visuals/__init__.py CHANGED
@@ -17,9 +17,11 @@ from .cube import CubeVisual # noqa
17
17
  from .ellipse import EllipseVisual # noqa
18
18
  from .gridlines import GridLinesVisual # noqa
19
19
  from .image import ImageVisual # noqa
20
+ from .image_complex import ComplexImageVisual # noqa
20
21
  from .gridmesh import GridMeshVisual # noqa
21
22
  from .histogram import HistogramVisual # noqa
22
23
  from .infinite_line import InfiniteLineVisual # noqa
24
+ from .instanced_mesh import InstancedMeshVisual # noqa
23
25
  from .isocurve import IsocurveVisual # noqa
24
26
  from .isoline import IsolineVisual # noqa
25
27
  from .isosurface import IsosurfaceVisual # noqa
@@ -5,8 +5,7 @@ import warnings
5
5
 
6
6
  import numpy as np
7
7
 
8
- from vispy.gloo import Texture2D, Texture3D
9
- from vispy.gloo.texture import should_cast_to_f32
8
+ from vispy.gloo.texture import Texture2D, Texture3D, convert_dtype_and_clip
10
9
 
11
10
 
12
11
  def get_default_clim_from_dtype(dtype):
@@ -210,7 +209,11 @@ class _ScaledTextureMixin:
210
209
 
211
210
  def scale_and_set_data(self, data, offset=None, copy=False):
212
211
  """Upload new data to the GPU."""
213
- return self.set_data(data, offset=offset, copy=copy)
212
+ # we need to call super here or we get infinite recursion
213
+ return super().set_data(data, offset=offset, copy=copy)
214
+
215
+ def set_data(self, data, offset=None, copy=False):
216
+ self.scale_and_set_data(data, offset=offset, copy=copy)
214
217
 
215
218
 
216
219
  class CPUScaledTextureMixin(_ScaledTextureMixin):
@@ -295,29 +298,31 @@ class CPUScaledTextureMixin(_ScaledTextureMixin):
295
298
 
296
299
  @staticmethod
297
300
  def _scale_data_on_cpu(data, clim, copy=True):
298
- if copy:
299
- should_cast_to_f32(data.dtype)
300
- data = np.array(data, dtype=np.float32, copy=copy)
301
- elif not copy and not np.issubdtype(data.dtype, np.floating):
302
- raise ValueError("Data must be of floating type for no copying to occur.")
303
-
301
+ data = np.array(data, dtype=np.float32, copy=copy)
304
302
  if clim[0] != clim[1]:
303
+ # we always must copy the data if we change it here, otherwise it might change
304
+ # unexpectedly the data held outside of here
305
+ if not copy:
306
+ data = data.copy()
305
307
  data -= clim[0]
306
- data *= 1.0 / (clim[1] - clim[0])
307
- if should_cast_to_f32(data.dtype):
308
- data = data.astype(np.float32)
308
+ data *= 1 / (clim[1] - clim[0])
309
309
  return data
310
310
 
311
311
  def scale_and_set_data(self, data, offset=None, copy=True):
312
312
  """Upload new data to the GPU, scaling if necessary."""
313
- self._data_dtype = data.dtype
313
+ if self._data_dtype is None:
314
+ data.dtype == self._data_dtype
315
+
316
+ # ensure dtype is the same as it was before, or funny things happen
317
+ # no copy is performed unless asked for or necessary
318
+ data = convert_dtype_and_clip(data, self._data_dtype, copy=copy)
314
319
 
315
320
  clim = self._clim
316
321
  is_auto = isinstance(clim, str) and clim == 'auto'
317
322
  if data.ndim == self._ndim or data.shape[self._ndim] == 1:
318
323
  if is_auto:
319
324
  clim = get_default_clim_from_data(data)
320
- data = self._scale_data_on_cpu(data, clim, copy=copy)
325
+ data = self._scale_data_on_cpu(data, clim, copy=False)
321
326
  data_limits = clim
322
327
  else:
323
328
  data_limits = get_default_clim_from_dtype(data.dtype)
@@ -326,7 +331,7 @@ class CPUScaledTextureMixin(_ScaledTextureMixin):
326
331
 
327
332
  self._clim = float(clim[0]), float(clim[1])
328
333
  self._data_limits = data_limits
329
- return super().scale_and_set_data(data, offset=offset, copy=copy)
334
+ return super().scale_and_set_data(data, offset=offset, copy=False)
330
335
 
331
336
 
332
337
  class GPUScaledTextureMixin(_ScaledTextureMixin):
@@ -346,7 +351,6 @@ class GPUScaledTextureMixin(_ScaledTextureMixin):
346
351
  also give the texture permission to change formats in the future if
347
352
  new data is provided with a different data type.
348
353
 
349
-
350
354
  This class should only be used internally. For similar features where
351
355
  scaling occurs on the CPU see
352
356
  :class:`vispy.visuals._scalable_textures.CPUScaledTextureMixin`.
@@ -390,7 +394,6 @@ class GPUScaledTextureMixin(_ScaledTextureMixin):
390
394
  texture_format = np.dtype(texture_format).type
391
395
  if texture_format not in self._texture_dtype_format:
392
396
  raise ValueError("Can't determine internal texture format for '{}'".format(texture_format))
393
- should_cast_to_f32(texture_format)
394
397
  texture_format = self._texture_dtype_format[texture_format]
395
398
  # adjust internalformat for format of data (RGBA vs L)
396
399
  texture_format = texture_format.replace('r', 'rgba'[:num_channels])
@@ -52,7 +52,7 @@ class ArrayList(object):
52
52
  an error is raised.
53
53
 
54
54
  If `itemsize` is 1-D array, the array will be divided into
55
- elements whose succesive sizes will be picked from itemsize.
55
+ elements whose successive sizes will be picked from itemsize.
56
56
  If the sum of itemsize values is different from array size,
57
57
  an error is raised.
58
58
 
@@ -291,7 +291,7 @@ class ArrayList(object):
291
291
  an error is raised.
292
292
 
293
293
  If `itemsize` is 1-D array, the array will be divided into
294
- elements whose succesive sizes will be picked from itemsize.
294
+ elements whose successive sizes will be picked from itemsize.
295
295
  If the sum of itemsize values is different from array size,
296
296
  an error is raised.
297
297
  """
@@ -394,7 +394,7 @@ class ArrayList(object):
394
394
  an error is raised.
395
395
 
396
396
  If `itemsize` is 1-D array, the array will be divided into
397
- elements whose succesive sizes will be picked from itemsize.
397
+ elements whose successive sizes will be picked from itemsize.
398
398
  If the sum of itemsize values is different from array size,
399
399
  an error is raised.
400
400
  """
@@ -237,7 +237,7 @@ class BaseCollection(object):
237
237
  an error is raised.
238
238
 
239
239
  If `itemsize` is 1-D array, the array will be divided into
240
- elements whose succesive sizes will be picked from itemsize.
240
+ elements whose successive sizes will be picked from itemsize.
241
241
  If the sum of itemsize values is different from array size,
242
242
  an error is raised.
243
243
  """
vispy/visuals/ellipse.py CHANGED
@@ -79,7 +79,7 @@ class EllipseVisual(PolygonVisual):
79
79
 
80
80
  vertices = np.empty([num_segments + 2, 2], dtype=np.float32)
81
81
 
82
- # split the total angle into num_segments intances
82
+ # split the total angle into num_segments instances
83
83
  theta = np.linspace(start_angle,
84
84
  start_angle + np.deg2rad(span_angle),
85
85
  num_segments + 1)
@@ -2,8 +2,9 @@
2
2
  # Copyright (c) Vispy Development Team. All Rights Reserved.
3
3
  # Distributed under the (new) BSD License. See LICENSE.txt for more info.
4
4
 
5
- from .base_filter import Filter # noqa
5
+ from .base_filter import Filter, PrimitivePickingFilter # noqa
6
6
  from .clipper import Clipper # noqa
7
7
  from .color import Alpha, ColorFilter, IsolineFilter, ZColormapFilter # noqa
8
8
  from .picking import PickingFilter # noqa
9
- from .mesh import TextureFilter, ShadingFilter, WireframeFilter # noqa
9
+ from .markers import MarkerPickingFilter # noqa
10
+ from .mesh import TextureFilter, ShadingFilter, InstancedShadingFilter, WireframeFilter, FacePickingFilter # noqa
@@ -2,6 +2,11 @@
2
2
  # Copyright (c) Vispy Development Team. All Rights Reserved.
3
3
  # Distributed under the (new) BSD License. See LICENSE.txt for more info.
4
4
 
5
+ from abc import ABCMeta, abstractmethod
6
+
7
+ import numpy as np
8
+
9
+ from vispy.gloo import VertexBuffer
5
10
  from ..shaders import Function
6
11
 
7
12
 
@@ -120,3 +125,118 @@ class Filter(BaseFilter):
120
125
 
121
126
  self._attached = False
122
127
  self._visual = None
128
+
129
+
130
+ class PrimitivePickingFilter(Filter, metaclass=ABCMeta):
131
+ """Abstract base class for Visual-specific filters to implement a
132
+ primitive-picking mode.
133
+
134
+ Subclasses must (and usually only need to) implement
135
+ :py:meth:`_get_picking_ids`.
136
+ """
137
+
138
+ def __init__(self, fpos=9, *, discard_transparent=False):
139
+ # fpos is set to 9 by default to put it near the end, but before the
140
+ # default PickingFilter
141
+ vfunc = Function("""\
142
+ varying vec4 v_marker_picking_color;
143
+ void prepare_marker_picking() {
144
+ v_marker_picking_color = $ids;
145
+ }
146
+ """)
147
+ ffunc = Function("""\
148
+ varying vec4 v_marker_picking_color;
149
+ void marker_picking_filter() {
150
+ if ( $enabled != 1 ) {
151
+ return;
152
+ }
153
+ if ( $discard_transparent == 1 && gl_FragColor.a == 0.0 ) {
154
+ discard;
155
+ }
156
+ gl_FragColor = v_marker_picking_color;
157
+ }
158
+ """)
159
+
160
+ self._id_colors = VertexBuffer(np.zeros((0, 4), dtype=np.float32))
161
+ vfunc['ids'] = self._id_colors
162
+ self._n_primitives = 0
163
+ super().__init__(vcode=vfunc, fcode=ffunc, fpos=fpos)
164
+ self.enabled = False
165
+ self.discard_transparent = discard_transparent
166
+
167
+ @abstractmethod
168
+ def _get_picking_ids(self):
169
+ """Return a 1D array of picking IDs for the vertices in the visual.
170
+
171
+ Generally, this method should be implemented to:
172
+ 1. Calculate the number of primitives in the visual (may be
173
+ persisted in `self._n_primitives`).
174
+ 2. Calculate a range of picking ids for each primitive in the
175
+ visual. IDs should start from 1, reserving 0 for the background. If
176
+ primitives comprise multiple vertices (triangles), ids may need to
177
+ be repeated.
178
+
179
+ The return value should be an array of uint32 with shape
180
+ (num_vertices,).
181
+
182
+ If no change to the picking IDs is needed (for example, the number of
183
+ primitives has not changed), this method should return `None`.
184
+ """
185
+ raise NotImplementedError(self)
186
+
187
+ def _update_id_colors(self):
188
+ """Calculate the colors encoding the picking IDs for the visual.
189
+
190
+ For performance, this method will not update the id colors VertexBuffer
191
+ if :py:meth:`_get_picking_ids` returns `None`.
192
+ """
193
+ # this should remain untouched
194
+ ids = self._get_picking_ids()
195
+ if ids is not None:
196
+ id_colors = self._pack_ids_into_rgba(ids)
197
+ self._id_colors.set_data(id_colors)
198
+
199
+ @staticmethod
200
+ def _pack_ids_into_rgba(ids):
201
+ """Pack an array of uint32 primitive ids into float32 RGBA colors."""
202
+ if ids.dtype != np.uint32:
203
+ raise ValueError(f"ids must be uint32, got {ids.dtype}")
204
+
205
+ return np.divide(
206
+ ids.view(np.uint8).reshape(-1, 4),
207
+ 255,
208
+ dtype=np.float32
209
+ )
210
+
211
+ def _on_data_updated(self, event=None):
212
+ if not self.attached:
213
+ return
214
+ self._update_id_colors()
215
+
216
+ @property
217
+ def enabled(self):
218
+ return self._enabled
219
+
220
+ @enabled.setter
221
+ def enabled(self, e):
222
+ self._enabled = bool(e)
223
+ self.fshader['enabled'] = int(self._enabled)
224
+ self._on_data_updated()
225
+
226
+ @property
227
+ def discard_transparent(self):
228
+ return self._discard_transparent
229
+
230
+ @discard_transparent.setter
231
+ def discard_transparent(self, d):
232
+ self._discard_transparent = bool(d)
233
+ self.fshader['discard_transparent'] = int(self._discard_transparent)
234
+
235
+ def _attach(self, visual):
236
+ super()._attach(visual)
237
+ visual.events.data_updated.connect(self._on_data_updated)
238
+ self._on_data_updated()
239
+
240
+ def _detach(self, visual):
241
+ visual.events.data_updated.disconnect(self._on_data_updated)
242
+ super()._detach(visual)
@@ -2,6 +2,10 @@
2
2
  # Copyright (c) Vispy Development Team. All Rights Reserved.
3
3
  # Distributed under the (new) BSD License. See LICENSE.txt for more info.
4
4
 
5
+ from __future__ import annotations
6
+
7
+ from typing import Optional
8
+
5
9
  from functools import lru_cache
6
10
 
7
11
  import numpy as np
@@ -37,7 +41,7 @@ class PlanesClipper(Filter):
37
41
  }
38
42
  """
39
43
 
40
- def __init__(self, clipping_planes=None, coord_system='scene'):
44
+ def __init__(self, clipping_planes: Optional[np.ndarray] = None, coord_system: str = 'scene'):
41
45
  tr = ['visual', 'scene', 'document', 'canvas', 'framebuffer', 'render']
42
46
  if coord_system not in tr:
43
47
  raise ValueError(f'Invalid coordinate system {coord_system}. Must be one of {tr}.')
@@ -48,6 +52,11 @@ class PlanesClipper(Filter):
48
52
  fcode=Function(self.FRAG_CODE), fhook='pre', fpos=1,
49
53
  )
50
54
 
55
+ # initialize clipping planes
56
+ self._clipping_planes = np.empty((0, 2, 3), dtype=np.float32)
57
+ self._clipping_planes_func = Function(self._build_clipping_planes_glsl(0))
58
+ self.fshader['clip_with_planes'] = self._clipping_planes_func
59
+
51
60
  v_position = Varying('v_position', 'vec4')
52
61
  self.vshader['v_position'] = v_position
53
62
  self.fshader['v_position'] = v_position
@@ -55,7 +64,7 @@ class PlanesClipper(Filter):
55
64
  self.clipping_planes = clipping_planes
56
65
 
57
66
  @property
58
- def coord_system(self):
67
+ def coord_system(self) -> str:
59
68
  """
60
69
  Coordinate system used by the clipping planes (see visuals.transforms.transform_system.py)
61
70
  """
@@ -68,7 +77,7 @@ class PlanesClipper(Filter):
68
77
 
69
78
  @staticmethod
70
79
  @lru_cache(maxsize=10)
71
- def _build_clipping_planes_func(n_planes):
80
+ def _build_clipping_planes_glsl(n_planes: int) -> str:
72
81
  """Build the code snippet used to clip the volume based on self.clipping_planes."""
73
82
  func_template = '''
74
83
  float clip_planes(vec3 loc) {{
@@ -87,24 +96,27 @@ class PlanesClipper(Filter):
87
96
  for idx in range(n_planes):
88
97
  all_clips.append(clip_template.format(idx=idx))
89
98
  formatted_code = func_template.format(clips=''.join(all_clips))
90
- return Function(formatted_code)
99
+ return formatted_code
91
100
 
92
101
  @property
93
- def clipping_planes(self):
102
+ def clipping_planes(self) -> np.ndarray:
94
103
  """Get the set of planes used to clip the mesh.
95
104
  Each plane is defined by a position and a normal vector (magnitude is irrelevant). Shape: (n_planes, 2, 3)
96
105
  """
97
106
  return self._clipping_planes
98
107
 
99
108
  @clipping_planes.setter
100
- def clipping_planes(self, value):
109
+ def clipping_planes(self, value: Optional[np.ndarray]):
101
110
  if value is None:
102
- value = np.empty([0, 2, 3])
103
- self._clipping_planes = value
111
+ value = np.empty((0, 2, 3), dtype=np.float32)
112
+
113
+ # only recreate function if amount of clipping planes changes
114
+ if len(value) != len(self._clipping_planes):
115
+ self._clipping_planes_func = Function(self._build_clipping_planes_glsl(len(value)))
116
+ self.fshader['clip_with_planes'] = self._clipping_planes_func
104
117
 
105
- clip_func = self._build_clipping_planes_func(len(value))
106
- self.fshader['clip_with_planes'] = clip_func
118
+ self._clipping_planes = value
107
119
 
108
120
  for idx, plane in enumerate(value):
109
- clip_func[f'clipping_plane_pos{idx}'] = tuple(plane[0])
110
- clip_func[f'clipping_plane_norm{idx}'] = tuple(plane[1])
121
+ self._clipping_planes_func[f'clipping_plane_pos{idx}'] = tuple(plane[0])
122
+ self._clipping_planes_func[f'clipping_plane_norm{idx}'] = tuple(plane[1])
@@ -0,0 +1,28 @@
1
+ import numpy as np
2
+
3
+ from vispy.visuals.filters import PrimitivePickingFilter
4
+
5
+
6
+ class MarkerPickingFilter(PrimitivePickingFilter):
7
+ """Filter used to color markers by a picking ID.
8
+
9
+ Note that the ID color uses the alpha channel, so this may not be used
10
+ with blending enabled.
11
+
12
+ Examples
13
+ --------
14
+ :ref:`sphx_glr_gallery_scene_marker_picking.py`
15
+ """
16
+
17
+ def _get_picking_ids(self):
18
+ if self._visual._data is None:
19
+ n_markers = 0
20
+ else:
21
+ n_markers = len(self._visual._data['a_position'])
22
+
23
+ # we only care about the number of markers changing
24
+ if self._n_primitives == n_markers:
25
+ return
26
+ self._n_primitives = n_markers
27
+
28
+ return np.arange(1, n_markers + 1, dtype=np.uint32)