vispy 0.14.3__cp39-cp39-macosx_11_0_arm64.whl → 0.15.1__cp39-cp39-macosx_11_0_arm64.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/_qt.py +46 -11
- vispy/color/colormap.py +106 -27
- vispy/color/tests/test_color.py +27 -1
- vispy/ext/cocoapy.py +1 -21
- vispy/geometry/meshdata.py +3 -1
- vispy/geometry/tests/test_triangulation.py +88 -0
- vispy/geometry/triangulation.py +11 -13
- vispy/gloo/program.py +2 -2
- vispy/scene/cameras/arcball.py +1 -2
- vispy/scene/cameras/base_camera.py +63 -50
- vispy/scene/cameras/panzoom.py +4 -1
- vispy/scene/cameras/perspective.py +6 -1
- vispy/scene/cameras/turntable.py +11 -1
- vispy/version.py +9 -4
- vispy/visuals/_scalable_textures.py +5 -4
- vispy/visuals/filters/mesh.py +6 -1
- vispy/visuals/gridlines.py +61 -5
- vispy/visuals/image.py +19 -7
- vispy/visuals/surface_plot.py +1 -1
- vispy/visuals/tests/test_gridlines.py +30 -0
- vispy/visuals/tests/test_image.py +17 -15
- vispy/visuals/tests/test_scalable_textures.py +16 -0
- vispy/visuals/tests/test_surface_plot.py +8 -3
- vispy/visuals/text/_sdf_cpu.cpython-39-darwin.so +0 -0
- {vispy-0.14.3.dist-info → vispy-0.15.1.dist-info}/METADATA +50 -27
- {vispy-0.14.3.dist-info → vispy-0.15.1.dist-info}/RECORD +29 -28
- {vispy-0.14.3.dist-info → vispy-0.15.1.dist-info}/WHEEL +2 -1
- {vispy-0.14.3.dist-info → vispy-0.15.1.dist-info/licenses}/LICENSE.txt +1 -1
- {vispy-0.14.3.dist-info → vispy-0.15.1.dist-info}/top_level.txt +0 -0
vispy/gloo/program.py
CHANGED
|
@@ -362,7 +362,7 @@ class Program(GLObject):
|
|
|
362
362
|
data = TextureCube(data)
|
|
363
363
|
else:
|
|
364
364
|
# This should not happen
|
|
365
|
-
raise RuntimeError('Unknown type %s' % type_)
|
|
365
|
+
raise RuntimeError('Unknown type %s for %s' % (type_, name))
|
|
366
366
|
# Store and send GLIR command
|
|
367
367
|
self._user_variables[name] = data
|
|
368
368
|
self.glir.associate(data.glir)
|
|
@@ -442,7 +442,7 @@ class Program(GLObject):
|
|
|
442
442
|
self._glir.command('ATTRIBUTE', self._id,
|
|
443
443
|
name, type_, value, divisor)
|
|
444
444
|
else:
|
|
445
|
-
raise KeyError('Cannot set data for a %s.' % kind)
|
|
445
|
+
raise KeyError('Cannot set data for a %s (%s).' % (kind, name))
|
|
446
446
|
else:
|
|
447
447
|
# This variable is not defined in the current source code,
|
|
448
448
|
# so we cannot establish whether this is a uniform or
|
vispy/scene/cameras/arcball.py
CHANGED
|
@@ -6,7 +6,6 @@ from __future__ import division
|
|
|
6
6
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
|
|
9
|
-
|
|
10
9
|
from ...util import transforms
|
|
11
10
|
from ...util.quaternion import Quaternion
|
|
12
11
|
from ...visuals.transforms import MatrixTransform
|
|
@@ -57,7 +56,7 @@ class ArcballCamera(Base3DRotationCamera):
|
|
|
57
56
|
|
|
58
57
|
def _update_rotation(self, event):
|
|
59
58
|
"""Update rotation parmeters based on mouse movement"""
|
|
60
|
-
p2 = event.
|
|
59
|
+
p2 = event.pos[:2]
|
|
61
60
|
if self._event_value is None:
|
|
62
61
|
self._event_value = p2
|
|
63
62
|
wh = self._viewbox.size
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# Distributed under the (new) BSD License. See LICENSE.txt for more info.
|
|
4
4
|
|
|
5
5
|
from __future__ import division
|
|
6
|
+
from contextlib import contextmanager
|
|
6
7
|
|
|
7
8
|
from ...util import keys
|
|
8
9
|
from ..node import Node
|
|
@@ -211,11 +212,14 @@ class BaseCamera(Node):
|
|
|
211
212
|
@center.setter
|
|
212
213
|
def center(self, val):
|
|
213
214
|
if len(val) == 2:
|
|
214
|
-
|
|
215
|
+
center = float(val[0]), float(val[1]), 0.0
|
|
215
216
|
elif len(val) == 3:
|
|
216
|
-
|
|
217
|
+
center = float(val[0]), float(val[1]), float(val[2])
|
|
217
218
|
else:
|
|
218
219
|
raise ValueError('Center must be a 2 or 3 element tuple')
|
|
220
|
+
if center == self._center:
|
|
221
|
+
return
|
|
222
|
+
self._center = center
|
|
219
223
|
self.view_changed()
|
|
220
224
|
|
|
221
225
|
@property
|
|
@@ -233,6 +237,12 @@ class BaseCamera(Node):
|
|
|
233
237
|
self._fov = fov
|
|
234
238
|
self.view_changed()
|
|
235
239
|
|
|
240
|
+
@contextmanager
|
|
241
|
+
def _block_updates(self):
|
|
242
|
+
prev, self._resetting = self._resetting, True
|
|
243
|
+
yield
|
|
244
|
+
self._resetting = prev
|
|
245
|
+
|
|
236
246
|
# Camera methods
|
|
237
247
|
|
|
238
248
|
def set_range(self, x=None, y=None, z=None, margin=0.05):
|
|
@@ -276,31 +286,30 @@ class BaseCamera(Node):
|
|
|
276
286
|
return
|
|
277
287
|
|
|
278
288
|
# There is a viewbox, we're going to set the range for real
|
|
279
|
-
self.
|
|
289
|
+
with self._block_updates():
|
|
280
290
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
291
|
+
# Get bounds from viewbox if not given
|
|
292
|
+
if all([(b is None) for b in bounds]):
|
|
293
|
+
bounds = self._viewbox.get_scene_bounds()
|
|
294
|
+
else:
|
|
295
|
+
for i in range(3):
|
|
296
|
+
if bounds[i] is None:
|
|
297
|
+
bounds[i] = self._viewbox.get_scene_bounds(i)
|
|
298
|
+
|
|
299
|
+
# Calculate ranges and margins
|
|
300
|
+
ranges = [b[1] - b[0] for b in bounds]
|
|
301
|
+
margins = [(r*margin or 0.1) for r in ranges]
|
|
302
|
+
# Assign limits for this camera
|
|
303
|
+
bounds_margins = [(b[0]-m, b[1]+m) for b, m in zip(bounds, margins)]
|
|
304
|
+
self._xlim, self._ylim, self._zlim = bounds_margins
|
|
305
|
+
# Store center location
|
|
306
|
+
if (not init) or (self._center is None):
|
|
307
|
+
self._center = [(b[0] + r / 2) for b, r in zip(bounds, ranges)]
|
|
308
|
+
|
|
309
|
+
# Let specific camera handle it
|
|
310
|
+
self._set_range(init)
|
|
301
311
|
|
|
302
312
|
# Finish
|
|
303
|
-
self._resetting = False
|
|
304
313
|
self.view_changed()
|
|
305
314
|
|
|
306
315
|
def _set_range(self, init):
|
|
@@ -357,32 +366,34 @@ class BaseCamera(Node):
|
|
|
357
366
|
state = state or {}
|
|
358
367
|
state.update(kwargs)
|
|
359
368
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
369
|
+
with self._block_updates():
|
|
370
|
+
# In first pass, process tuple keys which select subproperties. This
|
|
371
|
+
# is an undocumented feature used for selective linking of camera state.
|
|
372
|
+
#
|
|
373
|
+
# Subproperties are handled by first copying old value of the root
|
|
374
|
+
# property, then setting the subproperty on this copy, and finally
|
|
375
|
+
# assigning the copied object back to the camera property. There needs
|
|
376
|
+
# to be an assignment of the root property so setters are called and
|
|
377
|
+
# update is triggered.
|
|
378
|
+
for key in list(state.keys()):
|
|
379
|
+
if isinstance(key, tuple):
|
|
380
|
+
key1 = key[0]
|
|
381
|
+
if key1 not in state:
|
|
382
|
+
root_prop = getattr(self, key1)
|
|
383
|
+
# We make copies by passing the old object to the type's
|
|
384
|
+
# constructor. This needs to be supported as is the case in
|
|
385
|
+
# e.g. the geometry.Rect class.
|
|
386
|
+
state[key1] = root_prop.__class__(root_prop)
|
|
387
|
+
nested_setattr(state[key1], key[1:], state[key])
|
|
388
|
+
|
|
389
|
+
# In second pass, assign the new root properties.
|
|
390
|
+
for key, val in state.items():
|
|
391
|
+
if isinstance(key, tuple):
|
|
392
|
+
continue
|
|
393
|
+
if key not in self._state_props:
|
|
394
|
+
raise KeyError('Not a valid camera state property %r' % key)
|
|
395
|
+
setattr(self, key, val)
|
|
396
|
+
self.view_changed()
|
|
386
397
|
|
|
387
398
|
def link(self, camera, props=None, axis=None):
|
|
388
399
|
"""Link this camera with another camera of the same type
|
|
@@ -508,6 +519,8 @@ class BaseCamera(Node):
|
|
|
508
519
|
|
|
509
520
|
def _set_scene_transform(self, tr):
|
|
510
521
|
"""Called by subclasses to configure the viewbox scene transform."""
|
|
522
|
+
if self._resetting:
|
|
523
|
+
return
|
|
511
524
|
# todo: check whether transform has changed, connect to
|
|
512
525
|
# transform.changed event
|
|
513
526
|
pre_tr = self.pre_transform
|
vispy/scene/cameras/panzoom.py
CHANGED
|
@@ -164,7 +164,7 @@ class PanZoomCamera(BaseCamera):
|
|
|
164
164
|
def center(self, center):
|
|
165
165
|
if not (isinstance(center, (tuple, list)) and len(center) in (2, 3)):
|
|
166
166
|
raise ValueError('center must be a 2 or 3 element tuple')
|
|
167
|
-
rect = Rect(self.rect) or Rect(*DEFAULT_RECT_TUPLE)
|
|
167
|
+
rect = Rect(self.rect) or Rect(*DEFAULT_RECT_TUPLE) # make a copy of self.rect
|
|
168
168
|
rect.center = center[:2]
|
|
169
169
|
self.rect = rect
|
|
170
170
|
|
|
@@ -246,6 +246,9 @@ class PanZoomCamera(BaseCamera):
|
|
|
246
246
|
event.handled = False
|
|
247
247
|
|
|
248
248
|
def _update_transform(self):
|
|
249
|
+
if self._resetting: # base camera linking operation
|
|
250
|
+
return
|
|
251
|
+
|
|
249
252
|
rect = self.rect
|
|
250
253
|
self._real_rect = Rect(rect)
|
|
251
254
|
vbr = self._viewbox.rect.flipped(x=self.flip[0], y=(not self.flip[1]))
|
|
@@ -80,7 +80,10 @@ class PerspectiveCamera(BaseCamera):
|
|
|
80
80
|
|
|
81
81
|
@scale_factor.setter
|
|
82
82
|
def scale_factor(self, value):
|
|
83
|
-
|
|
83
|
+
value = abs(float(value))
|
|
84
|
+
if value == self._scale_factor:
|
|
85
|
+
return
|
|
86
|
+
self._scale_factor = value
|
|
84
87
|
self.view_changed()
|
|
85
88
|
|
|
86
89
|
@property
|
|
@@ -137,6 +140,8 @@ class PerspectiveCamera(BaseCamera):
|
|
|
137
140
|
# Do we have a viewbox
|
|
138
141
|
if self._viewbox is None:
|
|
139
142
|
return
|
|
143
|
+
if self._resetting: # base camera linking operation
|
|
144
|
+
return
|
|
140
145
|
|
|
141
146
|
# Calculate viewing range for x and y
|
|
142
147
|
fx = fy = self._scale_factor
|
vispy/scene/cameras/turntable.py
CHANGED
|
@@ -73,6 +73,9 @@ class TurntableCamera(Base3DRotationCamera):
|
|
|
73
73
|
super(TurntableCamera, self).__init__(fov=fov, **kwargs)
|
|
74
74
|
|
|
75
75
|
# Set camera attributes
|
|
76
|
+
self._azimuth = azimuth
|
|
77
|
+
self._elevation = elevation
|
|
78
|
+
self._roll = roll
|
|
76
79
|
self.azimuth = azimuth
|
|
77
80
|
self.elevation = elevation
|
|
78
81
|
self.roll = roll
|
|
@@ -90,7 +93,10 @@ class TurntableCamera(Base3DRotationCamera):
|
|
|
90
93
|
@elevation.setter
|
|
91
94
|
def elevation(self, elev):
|
|
92
95
|
elev = float(elev)
|
|
93
|
-
|
|
96
|
+
elev = min(90, max(-90, elev))
|
|
97
|
+
if elev == self._elevation:
|
|
98
|
+
return
|
|
99
|
+
self._elevation = elev
|
|
94
100
|
self.view_changed()
|
|
95
101
|
|
|
96
102
|
@property
|
|
@@ -108,6 +114,8 @@ class TurntableCamera(Base3DRotationCamera):
|
|
|
108
114
|
azim += 360
|
|
109
115
|
while azim > 180:
|
|
110
116
|
azim -= 360
|
|
117
|
+
if azim == self._azimuth:
|
|
118
|
+
return
|
|
111
119
|
self._azimuth = azim
|
|
112
120
|
self.view_changed()
|
|
113
121
|
|
|
@@ -123,6 +131,8 @@ class TurntableCamera(Base3DRotationCamera):
|
|
|
123
131
|
roll += 360
|
|
124
132
|
while roll > 180:
|
|
125
133
|
roll -= 360
|
|
134
|
+
if roll == self._roll:
|
|
135
|
+
return
|
|
126
136
|
self._roll = roll
|
|
127
137
|
self.view_changed()
|
|
128
138
|
|
vispy/version.py
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
# file generated by
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
+
|
|
3
6
|
TYPE_CHECKING = False
|
|
4
7
|
if TYPE_CHECKING:
|
|
5
|
-
from typing import Tuple
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
6
11
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
12
|
else:
|
|
8
13
|
VERSION_TUPLE = object
|
|
@@ -12,5 +17,5 @@ __version__: str
|
|
|
12
17
|
__version_tuple__: VERSION_TUPLE
|
|
13
18
|
version_tuple: VERSION_TUPLE
|
|
14
19
|
|
|
15
|
-
__version__ = version = '0.
|
|
16
|
-
__version_tuple__ = version_tuple = (0,
|
|
20
|
+
__version__ = version = '0.15.1'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 15, 1)
|
|
@@ -291,10 +291,11 @@ class CPUScaledTextureMixin(_ScaledTextureMixin):
|
|
|
291
291
|
|
|
292
292
|
range_min, range_max = self._data_limits
|
|
293
293
|
clim_min, clim_max = self.clim
|
|
294
|
-
|
|
294
|
+
full_range = range_max - range_min
|
|
295
|
+
if clim_min == clim_max or full_range == 0:
|
|
295
296
|
return 0, np.inf
|
|
296
|
-
clim_min = (clim_min - range_min) /
|
|
297
|
-
clim_max = (clim_max - range_min) /
|
|
297
|
+
clim_min = (clim_min - range_min) / full_range
|
|
298
|
+
clim_max = (clim_max - range_min) / full_range
|
|
298
299
|
return clim_min, clim_max
|
|
299
300
|
|
|
300
301
|
@staticmethod
|
|
@@ -312,7 +313,7 @@ class CPUScaledTextureMixin(_ScaledTextureMixin):
|
|
|
312
313
|
def scale_and_set_data(self, data, offset=None, copy=True):
|
|
313
314
|
"""Upload new data to the GPU, scaling if necessary."""
|
|
314
315
|
if self._data_dtype is None:
|
|
315
|
-
|
|
316
|
+
self._data_dtype = data.dtype
|
|
316
317
|
|
|
317
318
|
# ensure dtype is the same as it was before, or funny things happen
|
|
318
319
|
# no copy is performed unless asked for or necessary
|
vispy/visuals/filters/mesh.py
CHANGED
|
@@ -421,6 +421,7 @@ class ShadingFilter(Filter):
|
|
|
421
421
|
ffunc = Function(self._shaders['fragment'])
|
|
422
422
|
|
|
423
423
|
self._normals = VertexBuffer(np.zeros((0, 3), dtype=np.float32))
|
|
424
|
+
self._normals_cache = None
|
|
424
425
|
vfunc['normal'] = self._normals
|
|
425
426
|
|
|
426
427
|
super().__init__(vcode=vfunc, fcode=ffunc)
|
|
@@ -550,7 +551,11 @@ class ShadingFilter(Filter):
|
|
|
550
551
|
)
|
|
551
552
|
|
|
552
553
|
normals = self._visual.mesh_data.get_vertex_normals(indexed='faces')
|
|
553
|
-
self.
|
|
554
|
+
if normals is not self._normals_cache:
|
|
555
|
+
# limit how often we upload new normal arrays
|
|
556
|
+
# gotcha: if normals are changed in place then this won't invalidate this cache
|
|
557
|
+
self._normals_cache = normals
|
|
558
|
+
self._normals.set_data(self._normals_cache, convert=True)
|
|
554
559
|
|
|
555
560
|
def on_mesh_data_updated(self, event):
|
|
556
561
|
self._update_data()
|
vispy/visuals/gridlines.py
CHANGED
|
@@ -4,27 +4,31 @@
|
|
|
4
4
|
|
|
5
5
|
from __future__ import division
|
|
6
6
|
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
7
9
|
from .image import ImageVisual
|
|
8
10
|
from ..color import Color
|
|
9
11
|
from .shaders import Function
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
_GRID_COLOR = """
|
|
15
|
+
uniform vec4 u_gridlines_bounds;
|
|
16
|
+
uniform float u_border_width;
|
|
17
|
+
|
|
13
18
|
vec4 grid_color(vec2 pos) {
|
|
14
19
|
vec4 px_pos = $map_to_doc(vec4(pos, 0, 1));
|
|
15
20
|
px_pos /= px_pos.w;
|
|
16
21
|
|
|
17
22
|
// Compute vectors representing width, height of pixel in local coords
|
|
18
|
-
float s = 1.;
|
|
19
23
|
vec4 local_pos = $map_doc_to_local(px_pos);
|
|
20
|
-
vec4 dx = $map_doc_to_local(px_pos + vec4(1.0
|
|
21
|
-
vec4 dy = $map_doc_to_local(px_pos + vec4(0, 1.0
|
|
24
|
+
vec4 dx = $map_doc_to_local(px_pos + vec4(1.0, 0, 0, 0));
|
|
25
|
+
vec4 dy = $map_doc_to_local(px_pos + vec4(0, 1.0, 0, 0));
|
|
22
26
|
local_pos /= local_pos.w;
|
|
23
27
|
dx = dx / dx.w - local_pos;
|
|
24
28
|
dy = dy / dy.w - local_pos;
|
|
25
29
|
|
|
26
30
|
// Pixel length along each axis, rounded to the nearest power of 10
|
|
27
|
-
vec2 px =
|
|
31
|
+
vec2 px = vec2(abs(dx.x) + abs(dy.x), abs(dx.y) + abs(dy.y));
|
|
28
32
|
float log10 = log(10.0);
|
|
29
33
|
float sx = pow(10.0, floor(log(px.x) / log10) + 1.) * $scale.x;
|
|
30
34
|
float sy = pow(10.0, floor(log(px.y) / log10) + 1.) * $scale.y;
|
|
@@ -57,6 +61,17 @@ vec4 grid_color(vec2 pos) {
|
|
|
57
61
|
if (alpha == 0.) {
|
|
58
62
|
discard;
|
|
59
63
|
}
|
|
64
|
+
|
|
65
|
+
if (any(lessThan(local_pos.xy + u_border_width / 2, u_gridlines_bounds.xz)) ||
|
|
66
|
+
any(greaterThan(local_pos.xy - u_border_width / 2, u_gridlines_bounds.yw))) {
|
|
67
|
+
discard;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (any(lessThan(local_pos.xy - u_gridlines_bounds.xz, vec2(u_border_width / 2))) ||
|
|
71
|
+
any(lessThan(u_gridlines_bounds.yw - local_pos.xy, vec2(u_border_width / 2)))) {
|
|
72
|
+
alpha = 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
60
75
|
return vec4($color.rgb, $color.a * alpha);
|
|
61
76
|
}
|
|
62
77
|
"""
|
|
@@ -73,9 +88,16 @@ class GridLinesVisual(ImageVisual):
|
|
|
73
88
|
color : Color
|
|
74
89
|
The base color for grid lines. The final color may have its alpha
|
|
75
90
|
channel modified.
|
|
91
|
+
grid_bounds : tuple or None
|
|
92
|
+
The lower and upper bound for each axis beyond which no grid is rendered.
|
|
93
|
+
In the form of (minx, maxx, miny, maxy).
|
|
94
|
+
border_width : float
|
|
95
|
+
Tickness of the border rendered at the bounds of the grid.
|
|
76
96
|
"""
|
|
77
97
|
|
|
78
|
-
def __init__(self, scale=(1, 1), color='w'
|
|
98
|
+
def __init__(self, scale=(1, 1), color='w',
|
|
99
|
+
grid_bounds=None,
|
|
100
|
+
border_width=2):
|
|
79
101
|
# todo: PlaneVisual should support subdivide/impostor methods from
|
|
80
102
|
# image and gridlines should inherit from plane instead.
|
|
81
103
|
self._grid_color_fn = Function(_GRID_COLOR)
|
|
@@ -86,6 +108,40 @@ class GridLinesVisual(ImageVisual):
|
|
|
86
108
|
self.shared_program.frag['get_data'] = self._grid_color_fn
|
|
87
109
|
cfun = Function('vec4 null(vec4 x) { return x; }')
|
|
88
110
|
self.shared_program.frag['color_transform'] = cfun
|
|
111
|
+
self.unfreeze()
|
|
112
|
+
self.grid_bounds = grid_bounds
|
|
113
|
+
self.border_width = border_width
|
|
114
|
+
self.freeze()
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def grid_bounds(self):
|
|
118
|
+
return self._grid_bounds
|
|
119
|
+
|
|
120
|
+
@grid_bounds.setter
|
|
121
|
+
def grid_bounds(self, value):
|
|
122
|
+
if value is None:
|
|
123
|
+
value = (None,) * 4
|
|
124
|
+
grid_bounds = []
|
|
125
|
+
for i, v in enumerate(value):
|
|
126
|
+
if v is None:
|
|
127
|
+
if i % 2:
|
|
128
|
+
v = -np.inf
|
|
129
|
+
else:
|
|
130
|
+
v = np.inf
|
|
131
|
+
grid_bounds.append(v)
|
|
132
|
+
self.shared_program['u_gridlines_bounds'] = value
|
|
133
|
+
self._grid_bounds = grid_bounds
|
|
134
|
+
self.update()
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def border_width(self):
|
|
138
|
+
return self._border_width
|
|
139
|
+
|
|
140
|
+
@border_width.setter
|
|
141
|
+
def border_width(self, value):
|
|
142
|
+
self.shared_program['u_border_width'] = value
|
|
143
|
+
self._border_width = value
|
|
144
|
+
self.update()
|
|
89
145
|
|
|
90
146
|
@property
|
|
91
147
|
def size(self):
|
vispy/visuals/image.py
CHANGED
|
@@ -91,11 +91,9 @@ _TEXTURE_LOOKUP = """
|
|
|
91
91
|
|
|
92
92
|
_APPLY_CLIM_FLOAT = """
|
|
93
93
|
float apply_clim(float data) {
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
discard;
|
|
98
|
-
}
|
|
94
|
+
// pass through NaN values to get handled by the colormap
|
|
95
|
+
if (!(data <= 0.0 || 0.0 <= data)) return data;
|
|
96
|
+
|
|
99
97
|
data = clamp(data, min($clim.x, $clim.y), max($clim.x, $clim.y));
|
|
100
98
|
data = (data - $clim.x) / ($clim.y - $clim.x);
|
|
101
99
|
return data;
|
|
@@ -103,7 +101,7 @@ _APPLY_CLIM_FLOAT = """
|
|
|
103
101
|
|
|
104
102
|
_APPLY_CLIM = """
|
|
105
103
|
vec4 apply_clim(vec4 color) {
|
|
106
|
-
// Handle NaN values
|
|
104
|
+
// Handle NaN values (clamp them to the minimum value)
|
|
107
105
|
// http://stackoverflow.com/questions/11810158/how-to-deal-with-nan-or-inf-in-opengl-es-2-0-shaders
|
|
108
106
|
color.r = !(color.r <= 0.0 || 0.0 <= color.r) ? min($clim.x, $clim.y) : color.r;
|
|
109
107
|
color.g = !(color.g <= 0.0 || 0.0 <= color.g) ? min($clim.x, $clim.y) : color.g;
|
|
@@ -117,6 +115,9 @@ _APPLY_CLIM = """
|
|
|
117
115
|
|
|
118
116
|
_APPLY_GAMMA_FLOAT = """
|
|
119
117
|
float apply_gamma(float data) {
|
|
118
|
+
// pass through NaN values to get handled by the colormap
|
|
119
|
+
if (!(data <= 0.0 || 0.0 <= data)) return data;
|
|
120
|
+
|
|
120
121
|
return pow(data, $gamma);
|
|
121
122
|
}"""
|
|
122
123
|
|
|
@@ -129,7 +130,7 @@ _APPLY_GAMMA = """
|
|
|
129
130
|
|
|
130
131
|
_NULL_COLOR_TRANSFORM = 'vec4 pass(vec4 color) { return color; }'
|
|
131
132
|
|
|
132
|
-
_C2L_RED = 'float
|
|
133
|
+
_C2L_RED = 'float color_to_luminance(vec4 color) { return color.r; }'
|
|
133
134
|
|
|
134
135
|
_CUSTOM_FILTER = """
|
|
135
136
|
vec4 texture_lookup(vec2 texcoord) {
|
|
@@ -456,6 +457,17 @@ class ImageVisual(Visual):
|
|
|
456
457
|
self.shared_program.frag['color_transform'][2]['gamma'] = self._gamma
|
|
457
458
|
self.update()
|
|
458
459
|
|
|
460
|
+
@property
|
|
461
|
+
def bad_color(self):
|
|
462
|
+
"""Color used to render NaN values."""
|
|
463
|
+
return self._cmap.get_bad_color()
|
|
464
|
+
|
|
465
|
+
@bad_color.setter
|
|
466
|
+
def bad_color(self, color):
|
|
467
|
+
self._cmap.set_bad_color(color)
|
|
468
|
+
self._need_colortransform_update = True
|
|
469
|
+
self.update()
|
|
470
|
+
|
|
459
471
|
@property
|
|
460
472
|
def method(self):
|
|
461
473
|
"""Get rendering method name."""
|
vispy/visuals/surface_plot.py
CHANGED
|
@@ -126,7 +126,7 @@ class SurfacePlotVisual(MeshVisual):
|
|
|
126
126
|
# convert (width, height, 4) to (num_verts, 4)
|
|
127
127
|
vert_shape = self.__vertices.shape
|
|
128
128
|
num_vertices = vert_shape[0] * vert_shape[1]
|
|
129
|
-
colors = colors.reshape(num_vertices,
|
|
129
|
+
colors = colors.reshape(num_vertices, colors.shape[-1])
|
|
130
130
|
return colors
|
|
131
131
|
|
|
132
132
|
def set_data(self, x=None, y=None, z=None, colors=None):
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
|
|
5
|
+
"""
|
|
6
|
+
Tests for GridLines
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from vispy.scene import GridLines, STTransform
|
|
12
|
+
from vispy.testing import (requires_application, TestingCanvas,
|
|
13
|
+
run_tests_if_main)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@requires_application()
|
|
17
|
+
def test_gridlines():
|
|
18
|
+
with TestingCanvas(size=(80, 80)) as c:
|
|
19
|
+
grid = GridLines(parent=c.scene)
|
|
20
|
+
grid.transform = STTransform(translate=(40, 40))
|
|
21
|
+
render = c.render()
|
|
22
|
+
np.testing.assert_array_equal(render[40, 40], (151, 151, 151, 255))
|
|
23
|
+
np.testing.assert_array_equal(render[50, 50], (0, 0, 0, 255))
|
|
24
|
+
|
|
25
|
+
grid.grid_bounds = (-10, 10, -10, 10)
|
|
26
|
+
render = c.render()
|
|
27
|
+
np.testing.assert_array_equal(render[50, 50], (255, 255, 255, 255))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
run_tests_if_main()
|
|
@@ -128,28 +128,30 @@ def test_image_clims_and_gamma(input_dtype, texture_format, num_channels,
|
|
|
128
128
|
@pytest.mark.xfail(IS_CI, reason="CI environments sometimes treat NaN as 0")
|
|
129
129
|
@requires_application()
|
|
130
130
|
@pytest.mark.parametrize('texture_format', [None, 'auto'])
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
data
|
|
135
|
-
data[:
|
|
131
|
+
@pytest.mark.parametrize('bad_color', [None, (1, 0, 0, 1)])
|
|
132
|
+
def test_image_nan(texture_format, bad_color):
|
|
133
|
+
size = (80, 80)
|
|
134
|
+
data = np.ones(size)
|
|
135
|
+
data[:20, :20] = np.nan
|
|
136
|
+
data[-20: -20:] = 0
|
|
136
137
|
|
|
137
|
-
expected = (np.ones(
|
|
138
|
-
|
|
139
|
-
expected[:5, -5:, :3] = 0
|
|
138
|
+
expected = (np.ones(size + (4,)) * 255).astype(np.uint8)
|
|
139
|
+
expected[-20: -20:] = (0, 0, 0, 225)
|
|
140
140
|
if texture_format is None:
|
|
141
141
|
# CPU scaling's NaNs get converted to 0s
|
|
142
|
-
expected[:
|
|
142
|
+
expected[:20, :20] = (0, 0, 0, 255)
|
|
143
143
|
else:
|
|
144
144
|
# GPU receives NaNs
|
|
145
|
-
# nan -
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
# nan - mapped to bad color
|
|
146
|
+
if bad_color is None:
|
|
147
|
+
# no bad color means transparent, so we should see the green canvas
|
|
148
|
+
bad_color = (0, 1, 0, 1)
|
|
149
|
+
expected[:20, :20] = np.array(bad_color) * 255
|
|
149
150
|
|
|
150
151
|
with TestingCanvas(size=size[::-1], bgcolor=(0, 1, 0)) as c:
|
|
151
|
-
Image(data, cmap='grays',
|
|
152
|
+
image = Image(data, cmap='grays', clim=(0, 1),
|
|
152
153
|
texture_format=texture_format, parent=c.scene)
|
|
154
|
+
image.bad_color = bad_color
|
|
153
155
|
rendered = c.render()
|
|
154
156
|
np.testing.assert_allclose(rendered, expected)
|
|
155
157
|
|
|
@@ -327,7 +329,7 @@ def test_image_interpolation():
|
|
|
327
329
|
assert np.allclose(render[center_left], black)
|
|
328
330
|
assert np.allclose(render[center_right], white)
|
|
329
331
|
|
|
330
|
-
image.interpolation = '
|
|
332
|
+
image.interpolation = 'linear'
|
|
331
333
|
render = c.render()
|
|
332
334
|
assert np.allclose(render[left], black)
|
|
333
335
|
assert np.allclose(render[right], white)
|
|
@@ -133,6 +133,22 @@ def test_clim_handling_cpu():
|
|
|
133
133
|
assert st.clim == (5, 25)
|
|
134
134
|
assert st.clim_normalized == (0, 1)
|
|
135
135
|
|
|
136
|
+
# u8 auto -> f32 auto
|
|
137
|
+
st = CPUScaledStub()
|
|
138
|
+
st.set_clim("auto")
|
|
139
|
+
assert st.clim == "auto"
|
|
140
|
+
st.scale_and_set_data(ref_data.astype(np.uint8))
|
|
141
|
+
assert st.clim == (5, 25)
|
|
142
|
+
assert st.clim_normalized == (0, 1)
|
|
143
|
+
# set new data with an out-of-range value
|
|
144
|
+
# it should clip at the limits of the original data type
|
|
145
|
+
st.set_clim("auto")
|
|
146
|
+
assert st.clim == "auto"
|
|
147
|
+
new_data = np.array([[10, 10, 5], [15, 2048, 15]], dtype=np.float32)
|
|
148
|
+
st.scale_and_set_data(new_data)
|
|
149
|
+
assert st.clim == (5, 255)
|
|
150
|
+
assert st.clim_normalized == (0, 1)
|
|
151
|
+
|
|
136
152
|
|
|
137
153
|
def test_clim_handling_gpu():
|
|
138
154
|
ref_data = np.array([[10, 10, 5], [15, 25, 15]])
|