vispy 0.12.0__cp310-cp310-win_amd64.whl → 0.14.0__cp310-cp310-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.
- vispy/app/backends/_glfw.py +1 -1
- vispy/app/backends/_qt.py +78 -53
- vispy/color/color_array.py +8 -1
- vispy/color/colormap.py +4 -24
- vispy/geometry/meshdata.py +76 -38
- vispy/geometry/tests/test_meshdata.py +72 -0
- vispy/gloo/gl/_constants.py +3 -3
- vispy/gloo/program.py +1 -1
- vispy/gloo/texture.py +9 -4
- vispy/scene/cameras/base_camera.py +4 -0
- vispy/scene/cameras/panzoom.py +4 -1
- vispy/scene/cameras/perspective.py +6 -0
- vispy/scene/cameras/tests/test_perspective.py +37 -0
- vispy/scene/canvas.py +9 -5
- vispy/scene/events.py +9 -0
- vispy/scene/node.py +18 -1
- vispy/scene/tests/test_visuals.py +113 -0
- vispy/scene/visuals.py +5 -1
- vispy/util/gallery_scraper.py +8 -0
- vispy/util/tests/test_gallery_scraper.py +2 -0
- vispy/version.py +2 -3
- vispy/visuals/__init__.py +1 -0
- vispy/visuals/filters/__init__.py +3 -2
- vispy/visuals/filters/base_filter.py +120 -0
- vispy/visuals/filters/markers.py +28 -0
- vispy/visuals/filters/mesh.py +61 -6
- vispy/visuals/filters/tests/test_primitive_picking_filters.py +70 -0
- vispy/visuals/instanced_mesh.py +152 -0
- vispy/visuals/line/dash_atlas.py +46 -41
- vispy/visuals/markers.py +63 -33
- vispy/visuals/mesh.py +2 -2
- vispy/visuals/tests/test_instanced_mesh.py +50 -0
- vispy/visuals/tests/test_markers.py +6 -0
- vispy/visuals/tests/test_mesh.py +17 -0
- vispy/visuals/text/_sdf_cpu.cp310-win_amd64.pyd +0 -0
- vispy/visuals/text/_sdf_cpu.pyx +21 -23
- vispy/visuals/tube.py +1 -1
- vispy/visuals/visual.py +142 -1
- vispy/visuals/volume.py +19 -10
- {vispy-0.12.0.dist-info → vispy-0.14.0.dist-info}/LICENSE.txt +1 -1
- {vispy-0.12.0.dist-info → vispy-0.14.0.dist-info}/METADATA +7 -6
- {vispy-0.12.0.dist-info → vispy-0.14.0.dist-info}/RECORD +44 -40
- {vispy-0.12.0.dist-info → vispy-0.14.0.dist-info}/WHEEL +1 -1
- {vispy-0.12.0.dist-info → vispy-0.14.0.dist-info}/top_level.txt +0 -0
vispy/scene/canvas.py
CHANGED
|
@@ -140,6 +140,7 @@ class SceneCanvas(app.Canvas, Frozen):
|
|
|
140
140
|
self.events.mouse_move.connect(self._process_mouse_event)
|
|
141
141
|
self.events.mouse_release.connect(self._process_mouse_event)
|
|
142
142
|
self.events.mouse_wheel.connect(self._process_mouse_event)
|
|
143
|
+
self.events.touch.connect(self._process_mouse_event)
|
|
143
144
|
|
|
144
145
|
self.scene = SubScene()
|
|
145
146
|
self.freeze()
|
|
@@ -344,7 +345,12 @@ class SceneCanvas(app.Canvas, Frozen):
|
|
|
344
345
|
|
|
345
346
|
def _process_mouse_event(self, event):
|
|
346
347
|
prof = Profiler() # noqa
|
|
347
|
-
deliver_types = [
|
|
348
|
+
deliver_types = [
|
|
349
|
+
'mouse_press',
|
|
350
|
+
'mouse_wheel',
|
|
351
|
+
'gesture_zoom',
|
|
352
|
+
'gesture_rotate',
|
|
353
|
+
]
|
|
348
354
|
if self._send_hover_events:
|
|
349
355
|
deliver_types += ['mouse_move']
|
|
350
356
|
|
|
@@ -487,11 +493,8 @@ class SceneCanvas(app.Canvas, Frozen):
|
|
|
487
493
|
than triggering transform updates across the scene with every
|
|
488
494
|
click.
|
|
489
495
|
"""
|
|
490
|
-
|
|
491
|
-
self._scene.picking = True
|
|
496
|
+
with self._scene.set_picking():
|
|
492
497
|
img = self.render(bgcolor=(0, 0, 0, 0), crop=crop)
|
|
493
|
-
finally:
|
|
494
|
-
self._scene.picking = False
|
|
495
498
|
img = img.astype('int32') * [2**0, 2**8, 2**16, 2**24]
|
|
496
499
|
id_ = img.sum(axis=2).astype('int32')
|
|
497
500
|
return id_
|
|
@@ -524,6 +527,7 @@ class SceneCanvas(app.Canvas, Frozen):
|
|
|
524
527
|
self.events.mouse_move.disconnect(self._process_mouse_event)
|
|
525
528
|
self.events.mouse_release.disconnect(self._process_mouse_event)
|
|
526
529
|
self.events.mouse_wheel.disconnect(self._process_mouse_event)
|
|
530
|
+
self.events.touch.disconnect(self._process_mouse_event)
|
|
527
531
|
|
|
528
532
|
# -------------------------------------------------- transform handling ---
|
|
529
533
|
def push_viewport(self, viewport):
|
vispy/scene/events.py
CHANGED
|
@@ -71,6 +71,15 @@ class SceneMouseEvent(Event):
|
|
|
71
71
|
"""The increment by which the mouse wheel has moved."""
|
|
72
72
|
return self.mouse_event.delta
|
|
73
73
|
|
|
74
|
+
@property
|
|
75
|
+
def scale(self):
|
|
76
|
+
"""The scale of a gesture_zoom event"""
|
|
77
|
+
try:
|
|
78
|
+
return self.mouse_event.scale
|
|
79
|
+
except AttributeError:
|
|
80
|
+
errmsg = f"SceneMouseEvent type '{self.type}' has no scale"
|
|
81
|
+
raise TypeError(errmsg)
|
|
82
|
+
|
|
74
83
|
def copy(self):
|
|
75
84
|
ev = self.__class__(self.mouse_event, self.visual)
|
|
76
85
|
return ev
|
vispy/scene/node.py
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
from __future__ import division
|
|
6
6
|
|
|
7
7
|
import weakref
|
|
8
|
+
from contextlib import contextmanager
|
|
8
9
|
|
|
9
10
|
from ..util.event import Event, EmitterGroup
|
|
10
11
|
from ..visuals.transforms import (NullTransform, BaseTransform,
|
|
@@ -63,7 +64,8 @@ class Node(object):
|
|
|
63
64
|
# Add some events to the emitter groups:
|
|
64
65
|
events = ['canvas_change', 'parent_change', 'children_change',
|
|
65
66
|
'transform_change', 'mouse_press', 'mouse_move',
|
|
66
|
-
'mouse_release', 'mouse_wheel', 'key_press', 'key_release'
|
|
67
|
+
'mouse_release', 'mouse_wheel', 'key_press', 'key_release',
|
|
68
|
+
'gesture_zoom', 'gesture_rotate']
|
|
67
69
|
# Create event emitter if needed (in subclasses that inherit from
|
|
68
70
|
# Visual, we already have an emitter to share)
|
|
69
71
|
if not hasattr(self, 'events'):
|
|
@@ -625,3 +627,18 @@ class Node(object):
|
|
|
625
627
|
for c in self.children:
|
|
626
628
|
c.picking = p
|
|
627
629
|
self._picking = p
|
|
630
|
+
|
|
631
|
+
@contextmanager
|
|
632
|
+
def set_picking(self, *, picking=True):
|
|
633
|
+
"""Context manager to temporarily set picking for this node and its children.
|
|
634
|
+
|
|
635
|
+
Note that this function will not alter the picking mode unless/until
|
|
636
|
+
the context manager is entered (using the `with` statement). Use
|
|
637
|
+
:py:attr:`~.picking` for setting the picking mode directly.
|
|
638
|
+
"""
|
|
639
|
+
old_picking = self.picking
|
|
640
|
+
try:
|
|
641
|
+
self.picking = picking
|
|
642
|
+
yield self.picking
|
|
643
|
+
finally:
|
|
644
|
+
self.picking = old_picking
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
1
3
|
from vispy.scene import visuals, Node
|
|
2
4
|
from vispy.scene.visuals import VisualNode
|
|
3
5
|
import vispy.visuals
|
|
@@ -26,3 +28,114 @@ def test_visual_node_generation():
|
|
|
26
28
|
vis_node = getattr(visuals, name[:-6])
|
|
27
29
|
assert issubclass(vis_node, Node)
|
|
28
30
|
assert issubclass(vis_node, obj)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_push_gl_state():
|
|
34
|
+
node = vispy.visuals.MeshVisual()
|
|
35
|
+
og_gl_state = node._vshare.gl_state.copy()
|
|
36
|
+
node.push_gl_state(blend=False, depth_test=False)
|
|
37
|
+
assert node._vshare.gl_state != og_gl_state
|
|
38
|
+
# preset is always set, unset kwargs should be absent
|
|
39
|
+
assert node._vshare.gl_state == {
|
|
40
|
+
"preset": None,
|
|
41
|
+
"blend": False,
|
|
42
|
+
"depth_test": False,
|
|
43
|
+
}
|
|
44
|
+
node.pop_gl_state()
|
|
45
|
+
assert node._vshare.gl_state == og_gl_state
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_push_gl_state_update():
|
|
49
|
+
node = vispy.visuals.MeshVisual()
|
|
50
|
+
og_gl_state = node._vshare.gl_state.copy()
|
|
51
|
+
assert "blend" not in og_gl_state
|
|
52
|
+
assert node._vshare.gl_state["depth_test"]
|
|
53
|
+
|
|
54
|
+
node.push_gl_state_update(blend=False, depth_test=False)
|
|
55
|
+
assert node._vshare.gl_state != og_gl_state
|
|
56
|
+
assert not node._vshare.gl_state["blend"]
|
|
57
|
+
assert not node._vshare.gl_state["depth_test"]
|
|
58
|
+
|
|
59
|
+
node.pop_gl_state()
|
|
60
|
+
assert node._vshare.gl_state == og_gl_state
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_pop_empty_gl_state():
|
|
64
|
+
node = vispy.visuals.MeshVisual()
|
|
65
|
+
assert node._prev_gl_state == []
|
|
66
|
+
og_gl_state = node._vshare.gl_state.copy()
|
|
67
|
+
node.pop_gl_state()
|
|
68
|
+
assert node._vshare.gl_state == og_gl_state
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_update_gl_state():
|
|
72
|
+
node = vispy.visuals.MeshVisual()
|
|
73
|
+
|
|
74
|
+
og_gl_state = node._vshare.gl_state.copy()
|
|
75
|
+
assert og_gl_state
|
|
76
|
+
og_gl_state["blend"] = False
|
|
77
|
+
|
|
78
|
+
node.update_gl_state(blend=True)
|
|
79
|
+
|
|
80
|
+
# check that the state was updated
|
|
81
|
+
assert node._vshare.gl_state.pop("blend") != og_gl_state.pop("blend")
|
|
82
|
+
# the rest of the state should be unchanged
|
|
83
|
+
assert node._vshare.gl_state == og_gl_state
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_update_gl_state_context_manager():
|
|
87
|
+
node = vispy.visuals.MeshVisual()
|
|
88
|
+
|
|
89
|
+
node.set_gl_state(blend=False)
|
|
90
|
+
og_gl_state = node._vshare.gl_state.copy()
|
|
91
|
+
|
|
92
|
+
with node.update_gl_state(blend=True):
|
|
93
|
+
# check that the state was updated
|
|
94
|
+
assert node._vshare.gl_state == {**og_gl_state, "blend": True}
|
|
95
|
+
|
|
96
|
+
# the update should be reverted once out of the context
|
|
97
|
+
assert node._vshare.gl_state == og_gl_state
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_set_gl_state():
|
|
101
|
+
node = vispy.visuals.MeshVisual()
|
|
102
|
+
|
|
103
|
+
node.set_gl_state(blend=False, depth_test=False)
|
|
104
|
+
# preset is always set, unset kwargs should be absent
|
|
105
|
+
assert node._vshare.gl_state == {
|
|
106
|
+
"preset": None,
|
|
107
|
+
"blend": False,
|
|
108
|
+
"depth_test": False,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
node.set_gl_state(blend=False)
|
|
112
|
+
# preset is always set, unset kwargs should be absent
|
|
113
|
+
assert node._vshare.gl_state == {"preset": None, "blend": False}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_set_gl_state_context_manager():
|
|
117
|
+
node = vispy.visuals.MeshVisual()
|
|
118
|
+
|
|
119
|
+
node.set_gl_state(blend=False)
|
|
120
|
+
og_gl_state = node._vshare.gl_state.copy()
|
|
121
|
+
|
|
122
|
+
with node.set_gl_state(blend=True):
|
|
123
|
+
# preset is always set, unset kwargs should be absent
|
|
124
|
+
assert node._vshare.gl_state == {"preset": None, "blend": True}
|
|
125
|
+
|
|
126
|
+
# the update should be reverted once out of the context
|
|
127
|
+
assert node._vshare.gl_state == og_gl_state
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@pytest.mark.parametrize("enable_picking", [True, False])
|
|
131
|
+
def test_picking_context(enable_picking):
|
|
132
|
+
mesh = visuals.Mesh()
|
|
133
|
+
mesh.picking = not enable_picking
|
|
134
|
+
|
|
135
|
+
assert mesh.picking != enable_picking
|
|
136
|
+
|
|
137
|
+
with mesh.set_picking(picking=enable_picking) as p:
|
|
138
|
+
assert p == enable_picking
|
|
139
|
+
assert mesh.picking == enable_picking
|
|
140
|
+
|
|
141
|
+
assert mesh.picking != enable_picking
|
vispy/scene/visuals.py
CHANGED
|
@@ -71,7 +71,10 @@ class VisualNode(Node):
|
|
|
71
71
|
return
|
|
72
72
|
self._picking = p
|
|
73
73
|
self._picking_filter.enabled = p
|
|
74
|
-
|
|
74
|
+
if p:
|
|
75
|
+
self.push_gl_state_update(blend=False)
|
|
76
|
+
else:
|
|
77
|
+
self.pop_gl_state()
|
|
75
78
|
|
|
76
79
|
def _update_trsys(self, event):
|
|
77
80
|
"""Transform object(s) have changed for this Node; assign these to the
|
|
@@ -244,6 +247,7 @@ Histogram = create_visual_node(visuals.HistogramVisual)
|
|
|
244
247
|
Image = create_visual_node(visuals.ImageVisual)
|
|
245
248
|
ComplexImage = create_visual_node(visuals.ComplexImageVisual)
|
|
246
249
|
InfiniteLine = create_visual_node(visuals.InfiniteLineVisual)
|
|
250
|
+
InstancedMesh = create_visual_node(visuals.InstancedMeshVisual)
|
|
247
251
|
Isocurve = create_visual_node(visuals.IsocurveVisual)
|
|
248
252
|
Isoline = create_visual_node(visuals.IsolineVisual)
|
|
249
253
|
Isosurface = create_visual_node(visuals.IsosurfaceVisual)
|
vispy/util/gallery_scraper.py
CHANGED
|
@@ -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]
|
|
@@ -176,6 +177,13 @@ class FrameGrabber:
|
|
|
176
177
|
self._collected_images = []
|
|
177
178
|
self._frames_to_grab = frame_grab_list[:] # copy so original list is preserved
|
|
178
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
|
+
|
|
179
187
|
def on_draw(self, _):
|
|
180
188
|
if self._done:
|
|
181
189
|
return # Grab only once
|
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__ = version = '0.
|
|
5
|
-
__version_tuple__ = version_tuple = (0,
|
|
3
|
+
__version__ = version = '0.14.0'
|
|
4
|
+
__version_tuple__ = version_tuple = (0, 14, 0)
|
vispy/visuals/__init__.py
CHANGED
|
@@ -21,6 +21,7 @@ from .image_complex import ComplexImageVisual # noqa
|
|
|
21
21
|
from .gridmesh import GridMeshVisual # noqa
|
|
22
22
|
from .histogram import HistogramVisual # noqa
|
|
23
23
|
from .infinite_line import InfiniteLineVisual # noqa
|
|
24
|
+
from .instanced_mesh import InstancedMeshVisual # noqa
|
|
24
25
|
from .isocurve import IsocurveVisual # noqa
|
|
25
26
|
from .isoline import IsolineVisual # noqa
|
|
26
27
|
from .isosurface import IsosurfaceVisual # noqa
|
|
@@ -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 .
|
|
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)
|
|
@@ -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)
|
vispy/visuals/filters/mesh.py
CHANGED
|
@@ -7,7 +7,7 @@ import numpy as np
|
|
|
7
7
|
|
|
8
8
|
from vispy.gloo import Texture2D, VertexBuffer
|
|
9
9
|
from vispy.visuals.shaders import Function, Varying
|
|
10
|
-
from vispy.visuals.filters import Filter
|
|
10
|
+
from vispy.visuals.filters import Filter, PrimitivePickingFilter
|
|
11
11
|
from ...color import Color
|
|
12
12
|
|
|
13
13
|
|
|
@@ -177,6 +177,7 @@ void shade() {
|
|
|
177
177
|
vec3 u = dFdx(v_pos_scene.xyz);
|
|
178
178
|
vec3 v = dFdy(v_pos_scene.xyz);
|
|
179
179
|
normal = cross(u, v);
|
|
180
|
+
} else {
|
|
180
181
|
// Note(asnt): The normal calculated above always points in the
|
|
181
182
|
// direction of the camera. Reintroduce the original orientation of the
|
|
182
183
|
// face.
|
|
@@ -387,6 +388,10 @@ class ShadingFilter(Filter):
|
|
|
387
388
|
<https://github.com/vispy/vispy/blob/main/examples/basics/scene/mesh_shading.py>`_
|
|
388
389
|
example script.
|
|
389
390
|
"""
|
|
391
|
+
_shaders = {
|
|
392
|
+
'vertex': shading_vertex_template,
|
|
393
|
+
'fragment': shading_fragment_template,
|
|
394
|
+
}
|
|
390
395
|
|
|
391
396
|
def __init__(self, shading='flat',
|
|
392
397
|
ambient_coefficient=(1, 1, 1, 1),
|
|
@@ -412,8 +417,8 @@ class ShadingFilter(Filter):
|
|
|
412
417
|
|
|
413
418
|
self._enabled = enabled
|
|
414
419
|
|
|
415
|
-
vfunc = Function(
|
|
416
|
-
ffunc = Function(
|
|
420
|
+
vfunc = Function(self._shaders['vertex'])
|
|
421
|
+
ffunc = Function(self._shaders['fragment'])
|
|
417
422
|
|
|
418
423
|
self._normals = VertexBuffer(np.zeros((0, 3), dtype=np.float32))
|
|
419
424
|
vfunc['normal'] = self._normals
|
|
@@ -572,6 +577,29 @@ class ShadingFilter(Filter):
|
|
|
572
577
|
super()._detach(visual)
|
|
573
578
|
|
|
574
579
|
|
|
580
|
+
instanced_shading_vertex_template = shading_vertex_template.replace(
|
|
581
|
+
"$normal",
|
|
582
|
+
"mat3($instance_transform_x, $instance_transform_y, $instance_transform_z) * $normal"
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
class InstancedShadingFilter(ShadingFilter):
|
|
587
|
+
"""Shading filter modified for use with :class:`~vispy.visuals.InstancedMeshVisual`.
|
|
588
|
+
|
|
589
|
+
See :class:`ShadingFilter` for details and usage.
|
|
590
|
+
"""
|
|
591
|
+
_shaders = {
|
|
592
|
+
'vertex': instanced_shading_vertex_template,
|
|
593
|
+
'fragment': ShadingFilter._shaders['fragment'],
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
def _attach(self, visual):
|
|
597
|
+
super()._attach(visual)
|
|
598
|
+
self.vshader['instance_transform_x'] = visual._instance_transforms_vbos[0]
|
|
599
|
+
self.vshader['instance_transform_y'] = visual._instance_transforms_vbos[1]
|
|
600
|
+
self.vshader['instance_transform_z'] = visual._instance_transforms_vbos[2]
|
|
601
|
+
|
|
602
|
+
|
|
575
603
|
wireframe_vertex_template = """
|
|
576
604
|
varying vec3 v_bc;
|
|
577
605
|
|
|
@@ -729,13 +757,40 @@ class WireframeFilter(Filter):
|
|
|
729
757
|
bc = np.tile(bc[None, ...], (n_faces, 1, 1))
|
|
730
758
|
self._bc.set_data(bc, convert=True)
|
|
731
759
|
|
|
732
|
-
def
|
|
760
|
+
def on_data_updated(self, event):
|
|
733
761
|
self._update_data()
|
|
734
762
|
|
|
735
763
|
def _attach(self, visual):
|
|
736
764
|
super()._attach(visual)
|
|
737
|
-
visual.events.data_updated.connect(self.
|
|
765
|
+
visual.events.data_updated.connect(self.on_data_updated)
|
|
738
766
|
|
|
739
767
|
def _detach(self, visual):
|
|
740
|
-
visual.events.data_updated.disconnect(self.
|
|
768
|
+
visual.events.data_updated.disconnect(self.on_data_updated)
|
|
741
769
|
super()._detach(visual)
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
class FacePickingFilter(PrimitivePickingFilter):
|
|
773
|
+
"""Filter used to color mesh faces by a picking ID.
|
|
774
|
+
|
|
775
|
+
Note that the ID color uses the alpha channel, so this may not be used
|
|
776
|
+
with blending enabled.
|
|
777
|
+
|
|
778
|
+
Examples
|
|
779
|
+
--------
|
|
780
|
+
:ref:`sphx_glr_gallery_scene_face_picking.py`
|
|
781
|
+
"""
|
|
782
|
+
|
|
783
|
+
def _get_picking_ids(self):
|
|
784
|
+
if self._visual.mesh_data.is_empty():
|
|
785
|
+
n_faces = 0
|
|
786
|
+
else:
|
|
787
|
+
n_faces = len(self._visual.mesh_data.get_faces())
|
|
788
|
+
|
|
789
|
+
# we only care about the number of faces changing
|
|
790
|
+
if self._n_primitives == n_faces:
|
|
791
|
+
return None
|
|
792
|
+
self._n_primitives = n_faces
|
|
793
|
+
|
|
794
|
+
ids = np.arange(1, n_faces + 1, dtype=np.uint32)
|
|
795
|
+
ids = np.repeat(ids, 3, axis=0) # repeat id for each vertex
|
|
796
|
+
return ids
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright (c) Vispy Development Team. All Rights Reserved.
|
|
3
|
+
# Distributed under the (new) BSD License. See LICENSE.txt for more info.
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from vispy.geometry import create_plane
|
|
7
|
+
from vispy.scene.visuals import Markers, Mesh
|
|
8
|
+
from vispy.testing import requires_application, TestingCanvas
|
|
9
|
+
from vispy.visuals.filters import FacePickingFilter, MarkerPickingFilter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_empty_mesh_face_picking():
|
|
13
|
+
mesh = Mesh()
|
|
14
|
+
filter = FacePickingFilter()
|
|
15
|
+
mesh.attach(filter)
|
|
16
|
+
filter.enabled = True
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@requires_application()
|
|
20
|
+
def test_mesh_face_picking():
|
|
21
|
+
vertices, faces, _ = create_plane(125, 125)
|
|
22
|
+
vertices = vertices["position"]
|
|
23
|
+
vertices[:, :2] += 125 / 2
|
|
24
|
+
mesh = Mesh(vertices=vertices, faces=faces)
|
|
25
|
+
filter = FacePickingFilter()
|
|
26
|
+
mesh.attach(filter)
|
|
27
|
+
|
|
28
|
+
with TestingCanvas(size=(125, 125)) as c:
|
|
29
|
+
view = c.central_widget.add_view()
|
|
30
|
+
view.add(mesh)
|
|
31
|
+
filter.enabled = True
|
|
32
|
+
mesh.update_gl_state(blend=False)
|
|
33
|
+
picking_render = c.render(bgcolor=(0, 0, 0, 0), alpha=True)
|
|
34
|
+
|
|
35
|
+
# unpack the IDs
|
|
36
|
+
ids = picking_render.view(np.uint32)
|
|
37
|
+
# the plane is made up of two triangles and nearly fills the view
|
|
38
|
+
# pick one point on each triangle
|
|
39
|
+
assert ids[125 // 2, 125 // 4] == 1
|
|
40
|
+
assert ids[125 // 2, 3 * 125 // 4] == 2
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_empty_markers_picking():
|
|
44
|
+
markers = Markers()
|
|
45
|
+
filter = MarkerPickingFilter()
|
|
46
|
+
markers.attach(filter)
|
|
47
|
+
filter.enabled = True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@requires_application()
|
|
51
|
+
def test_markers_picking():
|
|
52
|
+
markers = Markers(
|
|
53
|
+
pos=np.array([[-0.5, -0.5], [0.5, 0.5]]),
|
|
54
|
+
size=5,
|
|
55
|
+
)
|
|
56
|
+
filter = MarkerPickingFilter()
|
|
57
|
+
markers.attach(filter)
|
|
58
|
+
|
|
59
|
+
with TestingCanvas(size=(125, 125)) as c:
|
|
60
|
+
view = c.central_widget.add_view(camera="panzoom")
|
|
61
|
+
view.camera.rect = (-1, -1, 2, 2)
|
|
62
|
+
view.add(markers)
|
|
63
|
+
|
|
64
|
+
filter.enabled = True
|
|
65
|
+
markers.update_gl_state(blend=False)
|
|
66
|
+
picking_render = c.render(bgcolor=(0, 0, 0, 0), alpha=True)
|
|
67
|
+
ids = picking_render.view(np.uint32)
|
|
68
|
+
|
|
69
|
+
assert ids[3 * 125 // 4, 125 // 4] == 1
|
|
70
|
+
assert ids[125 // 4, 3 * 125 // 4] == 2
|