vispy 0.14.3__cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl → 0.15.2__cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.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 CHANGED
@@ -493,41 +493,57 @@ class QtBaseCanvasBackend(BaseCanvasBackend):
493
493
  def mousePressEvent(self, ev):
494
494
  if self._vispy_canvas is None:
495
495
  return
496
- self._vispy_mouse_press(
496
+ vispy_event = self._vispy_mouse_press(
497
497
  native=ev,
498
498
  pos=_get_event_xy(ev),
499
499
  button=BUTTONMAP.get(ev.button(), 0),
500
500
  modifiers=self._modifiers(ev),
501
501
  )
502
+ # If vispy did not handle the event, clear the accept parameter of the qt event
503
+ if not vispy_event.handled:
504
+ ev.ignore()
502
505
 
503
506
  def mouseReleaseEvent(self, ev):
504
507
  if self._vispy_canvas is None:
505
508
  return
506
- self._vispy_mouse_release(
509
+ vispy_event = self._vispy_mouse_release(
507
510
  native=ev,
508
511
  pos=_get_event_xy(ev),
509
512
  button=BUTTONMAP[ev.button()],
510
513
  modifiers=self._modifiers(ev),
511
514
  )
515
+ # If vispy did not handle the event, clear the accept parameter of the qt event
516
+ if not vispy_event.handled:
517
+ ev.ignore()
512
518
 
513
519
  def mouseDoubleClickEvent(self, ev):
514
520
  if self._vispy_canvas is None:
515
521
  return
516
- self._vispy_mouse_double_click(
522
+ vispy_event = self._vispy_mouse_double_click(
517
523
  native=ev,
518
524
  pos=_get_event_xy(ev),
519
525
  button=BUTTONMAP.get(ev.button(), 0),
520
526
  modifiers=self._modifiers(ev),
521
527
  )
528
+ # If vispy did not handle the event, clear the accept parameter of the qt event
529
+ if not vispy_event.handled:
530
+ ev.ignore()
522
531
 
523
532
  def mouseMoveEvent(self, ev):
524
533
  if self._vispy_canvas is None:
525
534
  return
526
- self._vispy_mouse_move(
535
+ # NB ignores events, returns None for events in quick succession
536
+ vispy_event = self._vispy_mouse_move(
527
537
  native=ev,
528
538
  pos=_get_event_xy(ev),
529
539
  modifiers=self._modifiers(ev),
530
540
  )
541
+ # If vispy did not handle the event, clear the accept parameter of the qt event
542
+ # Note that the handler can return None, this is equivalent to not handling the event
543
+ if vispy_event is None or not vispy_event.handled:
544
+ # Theoretically, a parent widget might want to listen to all of
545
+ # the mouse move events, including those that VisPy ignores
546
+ ev.ignore()
531
547
 
532
548
  def wheelEvent(self, ev):
533
549
  if self._vispy_canvas is None:
@@ -544,12 +560,15 @@ class QtBaseCanvasBackend(BaseCanvasBackend):
544
560
  delta = ev.angleDelta()
545
561
  deltax, deltay = delta.x() / 120.0, delta.y() / 120.0
546
562
  # Emit event
547
- self._vispy_canvas.events.mouse_wheel(
563
+ vispy_event = self._vispy_canvas.events.mouse_wheel(
548
564
  native=ev,
549
565
  delta=(deltax, deltay),
550
566
  pos=_get_event_xy(ev),
551
567
  modifiers=self._modifiers(ev),
552
568
  )
569
+ # If vispy did not handle the event, clear the accept parameter of the qt event
570
+ if not vispy_event.handled:
571
+ ev.ignore()
553
572
 
554
573
  def keyPressEvent(self, ev):
555
574
  self._keyEvent(self._vispy_canvas.events.key_press, ev)
@@ -571,17 +590,20 @@ class QtBaseCanvasBackend(BaseCanvasBackend):
571
590
  pos = self.mapFromGlobal(ev.globalPos())
572
591
  pos = pos.x(), pos.y()
573
592
 
593
+ vispy_event = None
574
594
  if t == QtCore.Qt.NativeGestureType.BeginNativeGesture:
575
- self._vispy_canvas.events.touch(
595
+ vispy_event = self._vispy_canvas.events.touch(
576
596
  type='gesture_begin',
577
597
  pos=_get_event_xy(ev),
598
+ modifiers=self._modifiers(ev),
578
599
  )
579
600
  elif t == QtCore.Qt.NativeGestureType.EndNativeGesture:
580
601
  self._native_touch_total_rotation = []
581
602
  self._native_touch_total_scale = []
582
- self._vispy_canvas.events.touch(
603
+ vispy_event = self._vispy_canvas.events.touch(
583
604
  type='gesture_end',
584
605
  pos=_get_event_xy(ev),
606
+ modifiers=self._modifiers(ev),
585
607
  )
586
608
  elif t == QtCore.Qt.NativeGestureType.RotateNativeGesture:
587
609
  angle = ev.value()
@@ -592,12 +614,13 @@ class QtBaseCanvasBackend(BaseCanvasBackend):
592
614
  )
593
615
  self._native_gesture_rotation_values.append(angle)
594
616
  total_rotation_angle = math.fsum(self._native_gesture_rotation_values)
595
- self._vispy_canvas.events.touch(
617
+ vispy_event = self._vispy_canvas.events.touch(
596
618
  type="gesture_rotate",
597
619
  pos=pos,
598
620
  rotation=angle,
599
621
  last_rotation=last_angle,
600
622
  total_rotation_angle=total_rotation_angle,
623
+ modifiers=self._modifiers(ev),
601
624
  )
602
625
  elif t == QtCore.Qt.NativeGestureType.ZoomNativeGesture:
603
626
  scale = ev.value()
@@ -608,12 +631,13 @@ class QtBaseCanvasBackend(BaseCanvasBackend):
608
631
  )
609
632
  self._native_gesture_scale_values.append(scale)
610
633
  total_scale_factor = math.fsum(self._native_gesture_scale_values)
611
- self._vispy_canvas.events.touch(
634
+ vispy_event = self._vispy_canvas.events.touch(
612
635
  type="gesture_zoom",
613
636
  pos=pos,
614
637
  last_scale=last_scale,
615
638
  scale=scale,
616
639
  total_scale_factor=total_scale_factor,
640
+ modifiers=self._modifiers(ev),
617
641
  )
618
642
  # QtCore.Qt.NativeGestureType.PanNativeGesture
619
643
  # Qt6 docs seem to imply this is only supported on Wayland but I have
@@ -622,6 +646,11 @@ class QtBaseCanvasBackend(BaseCanvasBackend):
622
646
  # On macOS, more fingers are usually swallowed by the OS (by spaces,
623
647
  # mission control, etc.).
624
648
 
649
+ # If vispy did not handle the event, clear the accept parameter of the qt event
650
+ # Note that some handlers return None, this is equivalent to not handling the event
651
+ if vispy_event is None or not vispy_event.handled:
652
+ ev.ignore()
653
+
625
654
  def event(self, ev):
626
655
  out = super(QtBaseCanvasBackend, self).event(ev)
627
656
 
@@ -644,7 +673,10 @@ class QtBaseCanvasBackend(BaseCanvasBackend):
644
673
  else:
645
674
  key = None
646
675
  mod = self._modifiers(ev)
647
- func(native=ev, key=key, text=str(ev.text()), modifiers=mod)
676
+ vispy_event = func(native=ev, key=key, text=str(ev.text()), modifiers=mod)
677
+ # If vispy did not handle the event, clear the accept parameter of the qt event
678
+ if not vispy_event.handled:
679
+ ev.ignore()
648
680
 
649
681
  def _modifiers(self, event):
650
682
  # Convert the QT modifier state into a tuple of active modifier keys.
@@ -771,7 +803,10 @@ class CanvasBackendEgl(QtBaseCanvasBackend, QWidget):
771
803
 
772
804
  def resizeEvent(self, event):
773
805
  w, h = event.size().width(), event.size().height()
774
- self._vispy_canvas.events.resize(size=(w, h))
806
+ vispy_event = self._vispy_canvas.events.resize(size=(w, h))
807
+ # If vispy did not handle the event, clear the accept parameter of the qt event
808
+ if not vispy_event.handled:
809
+ event.ignore()
775
810
 
776
811
  def paintEvent(self, event):
777
812
  self._vispy_canvas.events.draw(region=None)
vispy/color/colormap.py CHANGED
@@ -5,9 +5,10 @@
5
5
  from __future__ import division # just to be safe...
6
6
  import warnings
7
7
 
8
+ import re
8
9
  import numpy as np
9
10
 
10
- from .color_array import ColorArray
11
+ from .color_array import ColorArray, Color
11
12
  from ..ext.cubehelix import cubehelix
12
13
  from hsluv import hsluv_to_rgb
13
14
  from ..util.check_environment import has_matplotlib
@@ -158,11 +159,12 @@ def _glsl_mix(controls=None, colors=None, texture_map_data=None):
158
159
  LUT[:, 0, 2] = np.interp(x, controls, c_rgba[:, 2])
159
160
  LUT[:, 0, 3] = np.interp(x, controls, c_rgba[:, 3])
160
161
 
161
- s2 = "uniform sampler2D texture2D_LUT;"
162
- s = "{\n return texture2D(texture2D_LUT, \
163
- vec2(0.0, clamp(t, 0.0, 1.0)));\n} "
164
-
165
- return "%s\nvec4 colormap(float t) {\n%s\n}" % (s2, s)
162
+ return """
163
+ uniform sampler2D texture2D_LUT;
164
+ vec4 colormap(float t) {
165
+ return texture2D(texture2D_LUT, vec2(0.0, clamp(t, 0.0, 1.0)));
166
+ }
167
+ """
166
168
 
167
169
 
168
170
  def _glsl_step(controls=None, colors=None, texture_map_data=None):
@@ -192,11 +194,12 @@ def _glsl_step(controls=None, colors=None, texture_map_data=None):
192
194
  colors_rgba = ColorArray(colors[:])._rgba
193
195
  LUT[:, 0, :] = colors_rgba[j]
194
196
 
195
- s2 = "uniform sampler2D texture2D_LUT;"
196
- s = "{\n return texture2D(texture2D_LUT, \
197
- vec2(0.0, clamp(t, 0.0, 1.0)));\n} "
198
-
199
- return "%s\nvec4 colormap(float t) {\n%s\n}" % (s2, s)
197
+ return """
198
+ uniform sampler2D texture2D_LUT;
199
+ vec4 colormap(float t) {
200
+ return texture2D(texture2D_LUT, vec2(0.0, clamp(t, 0.0, 1.0)));
201
+ }
202
+ """
200
203
 
201
204
 
202
205
  # Mini GLSL template system for colors.
@@ -219,6 +222,12 @@ class BaseColormap(object):
219
222
  ----------
220
223
  colors : list of lists, tuples, or ndarrays
221
224
  The control colors used by the colormap (shape = (ncolors, 4)).
225
+ bad_color : None | array-like
226
+ The color mapping for NaN values.
227
+ high_color : None | array-like
228
+ The color mapping for values greater than or equal to 1.
229
+ low_color : None | array-like
230
+ The color mapping for values less than or equal to 0.
222
231
 
223
232
  Notes
224
233
  -----
@@ -234,6 +243,9 @@ class BaseColormap(object):
234
243
 
235
244
  # Control colors used by the colormap.
236
245
  colors = None
246
+ bad_color = None
247
+ high_color = None
248
+ low_color = None
237
249
 
238
250
  # GLSL string with a function implementing the color map.
239
251
  glsl_map = None
@@ -242,7 +254,7 @@ class BaseColormap(object):
242
254
  # for luminance to RGBA conversion.
243
255
  texture_map_data = None
244
256
 
245
- def __init__(self, colors=None):
257
+ def __init__(self, colors=None, *, bad_color=None, low_color=None, high_color=None):
246
258
  # Ensure the colors are arrays.
247
259
  if colors is not None:
248
260
  self.colors = colors
@@ -252,6 +264,51 @@ class BaseColormap(object):
252
264
  if len(self.colors) > 0:
253
265
  self.glsl_map = _process_glsl_template(self.glsl_map,
254
266
  self.colors.rgba)
267
+ if high_color is not None:
268
+ self.high_color = Color(high_color)
269
+ self._set_high_color_glsl()
270
+ if low_color is not None:
271
+ self.low_color = Color(low_color)
272
+ self._set_low_color_glsl()
273
+
274
+ self.bad_color = Color((0, 0, 0, 0) if bad_color is None else bad_color)
275
+ self._set_bad_color_glsl()
276
+
277
+ def _set_bad_color_glsl(self):
278
+ """Set the color mapping for NaN values."""
279
+ r, g, b, a = self.bad_color.rgba
280
+
281
+ bad_color_glsl = f"""
282
+ // Map NaN to bad_color
283
+ if (!(t <= 0.0 || 0.0 <= t)) {{
284
+ return vec4({r:.3f}, {g:.3f}, {b:.3f}, {a:.3f});
285
+ }}"""
286
+
287
+ self.glsl_map = re.sub(r'float t\) \{', f'float t) {{{bad_color_glsl}', self.glsl_map)
288
+
289
+ def _set_high_color_glsl(self):
290
+ """Set the color mapping for values greater than or equal to max clim."""
291
+ r, g, b, a = self.high_color.rgba
292
+
293
+ high_color_glsl = f"""
294
+ // Map high_color
295
+ if (1 - t <= 1e-12) {{ // use epsilon to work around numerical imprecision
296
+ return vec4({r:.3f}, {g:.3f}, {b:.3f}, {a:.3f});
297
+ }}"""
298
+
299
+ self.glsl_map = re.sub(r'float t\) \{', f'float t) {{{high_color_glsl}', self.glsl_map)
300
+
301
+ def _set_low_color_glsl(self):
302
+ """Set the color mapping for values less than or equal to min clim."""
303
+ r, g, b, a = self.low_color.rgba
304
+
305
+ low_color_glsl = f"""
306
+ // Map low_color
307
+ if (t <= 1e-12) {{ // use epsilon to work around numerical imprecision
308
+ return vec4({r:.3f}, {g:.3f}, {b:.3f}, {a:.3f});
309
+ }}"""
310
+
311
+ self.glsl_map = re.sub(r'float t\) \{', f'float t) {{{low_color_glsl}', self.glsl_map)
255
312
 
256
313
  def map(self, item):
257
314
  """Return a rgba array for the requested items.
@@ -281,6 +338,15 @@ class BaseColormap(object):
281
338
  """
282
339
  raise NotImplementedError()
283
340
 
341
+ def _map_edge_case_colors(self, param, colors):
342
+ """Apply special mapping to edge cases (NaN and max/min clim)."""
343
+ colors = np.where(np.isnan(param.reshape(-1, 1)), self.bad_color.rgba, colors)
344
+ if self.high_color is not None:
345
+ colors = np.where((param == 1).reshape(-1, 1), self.high_color.rgba, colors)
346
+ if self.low_color is not None:
347
+ colors = np.where((param == 0).reshape(-1, 1), self.low_color.rgba, colors)
348
+ return colors
349
+
284
350
  def texture_lut(self):
285
351
  """Return a texture2D object for LUT after its value is set. Can be None."""
286
352
  return None
@@ -368,6 +434,12 @@ class Colormap(BaseColormap):
368
434
  be 'zero'.
369
435
  If 'linear', ncontrols = ncolors (one color per control point).
370
436
  If 'zero', ncontrols = ncolors+1 (one color per bin).
437
+ bad_color : None | array-like
438
+ The color mapping for NaN values.
439
+ high_color : None | array-like
440
+ The color mapping for values greater than or equal to 1.
441
+ low_color : None | array-like
442
+ The color mapping for values less than or equal to 0.
371
443
 
372
444
  Examples
373
445
  --------
@@ -379,7 +451,8 @@ class Colormap(BaseColormap):
379
451
 
380
452
  """
381
453
 
382
- def __init__(self, colors, controls=None, interpolation='linear'):
454
+ def __init__(self, colors, controls=None, interpolation='linear', *,
455
+ bad_color=None, low_color=None, high_color=None):
383
456
  self.interpolation = interpolation
384
457
  ncontrols = self._ncontrols(len(colors))
385
458
  # Default controls.
@@ -391,7 +464,8 @@ class Colormap(BaseColormap):
391
464
  self.texture_map_data = np.zeros((LUT_len, 1, 4), dtype=np.float32)
392
465
  self.glsl_map = self._glsl_map_generator(self._controls, colors,
393
466
  self.texture_map_data)
394
- super(Colormap, self).__init__(colors)
467
+ super(Colormap, self).__init__(colors, bad_color=bad_color,
468
+ high_color=high_color, low_color=low_color)
395
469
 
396
470
  @property
397
471
  def interpolation(self):
@@ -428,7 +502,8 @@ class Colormap(BaseColormap):
428
502
  colors : list
429
503
  List of rgba colors.
430
504
  """
431
- return self._map_function(self.colors.rgba, x, self._controls)
505
+ colors = self._map_function(self.colors.rgba, x, self._controls)
506
+ return self._map_edge_case_colors(x, colors)
432
507
 
433
508
  def texture_lut(self):
434
509
  """Return a texture2D object for LUT after its value is set. Can be None."""
@@ -540,7 +615,8 @@ class _Fire(BaseColormap):
540
615
  a, b, d = self.colors.rgba
541
616
  c = _mix_simple(a, b, t)
542
617
  e = _mix_simple(b, d, t**2)
543
- return _mix_simple(c, e, t)
618
+ colors = np.atleast_2d(_mix_simple(c, e, t))
619
+ return self._map_edge_case_colors(t, colors)
544
620
 
545
621
 
546
622
  class _Grays(BaseColormap):
@@ -551,10 +627,8 @@ class _Grays(BaseColormap):
551
627
  """
552
628
 
553
629
  def map(self, t):
554
- if isinstance(t, np.ndarray):
555
- return np.hstack([t, t, t, np.ones(t.shape)]).astype(np.float32)
556
- else:
557
- return np.array([t, t, t, 1.0], dtype=np.float32)
630
+ colors = np.c_[t, t, t, np.ones(t.shape)]
631
+ return self._map_edge_case_colors(t, colors)
558
632
 
559
633
 
560
634
  class _Ice(BaseColormap):
@@ -565,11 +639,8 @@ class _Ice(BaseColormap):
565
639
  """
566
640
 
567
641
  def map(self, t):
568
- if isinstance(t, np.ndarray):
569
- return np.hstack([t, t, np.ones(t.shape),
570
- np.ones(t.shape)]).astype(np.float32)
571
- else:
572
- return np.array([t, t, 1.0, 1.0], dtype=np.float32)
642
+ colors = np.c_[t, t, np.ones(t.shape), np.ones(t.shape)]
643
+ return self._map_edge_case_colors(t, colors)
573
644
 
574
645
 
575
646
  class _Hot(BaseColormap):
@@ -586,7 +657,8 @@ class _Hot(BaseColormap):
586
657
  def map(self, t):
587
658
  rgba = self.colors.rgba
588
659
  smoothed = smoothstep(rgba[0, :3], rgba[1, :3], t)
589
- return np.hstack((smoothed, np.ones((len(t), 1))))
660
+ colors = np.hstack((smoothed, np.ones((len(t), 1))))
661
+ return self._map_edge_case_colors(t, colors)
590
662
 
591
663
 
592
664
  class _Winter(BaseColormap):
@@ -600,9 +672,15 @@ class _Winter(BaseColormap):
600
672
  """
601
673
 
602
674
  def map(self, t):
603
- return _mix_simple(self.colors.rgba[0],
675
+ colors = _mix_simple(self.colors.rgba[0],
604
676
  self.colors.rgba[1],
605
677
  np.sqrt(t))
678
+ return self._map_edge_case_colors(t, colors)
679
+
680
+
681
+ class _HiLo(_Grays):
682
+ def __init__(self, *args, **kwargs):
683
+ super().__init__(*args, **kwargs, low_color='blue', high_color='red')
606
684
 
607
685
 
608
686
  class SingleHue(Colormap):
@@ -1089,6 +1167,7 @@ _colormaps = dict(
1089
1167
  husl=HSLuv(),
1090
1168
  diverging=Diverging(),
1091
1169
  RdYeBuCy=RedYellowBlueCyan(),
1170
+ HiLo=_HiLo(),
1092
1171
  )
1093
1172
 
1094
1173
 
@@ -3,7 +3,7 @@
3
3
  # Distributed under the (new) BSD License. See LICENSE.txt for more info.
4
4
 
5
5
  import numpy as np
6
- from numpy.testing import assert_array_equal, assert_allclose
6
+ from numpy.testing import assert_array_equal, assert_array_almost_equal, assert_allclose
7
7
 
8
8
  from vispy.color import (Color, ColorArray, get_color_names,
9
9
  Colormap,
@@ -349,4 +349,30 @@ def test_normalize():
349
349
  assert_allclose([y.min(), y.max()], [0.2975, 1-0.2975], 1e-1, 1e-1)
350
350
 
351
351
 
352
+ def test_colormap_bad_color():
353
+ """Test NaN handling."""
354
+ red = (1, 0, 0, 1)
355
+ white = (1, 1, 1, 1)
356
+ green = (0, 1, 0, 1)
357
+ cm = Colormap([white, green], bad_color=red)
358
+ assert_array_equal(cm[0].rgba, [white])
359
+ assert_array_equal(cm[1].rgba, [green])
360
+
361
+
362
+ def test_colormap_high_low_color():
363
+ """Test handling of clim extremes."""
364
+ hilo = get_colormap('HiLo')
365
+ white = (1, 1, 1, 1)
366
+ gray = (0.5, 0.5, 0.5, 1)
367
+ black = (0, 0, 0, 1)
368
+ red = (1, 0, 0, 1)
369
+ blue = (0, 0, 1, 1)
370
+
371
+ assert_array_equal(hilo[0].rgba, [blue])
372
+ assert_array_almost_equal(hilo[0.000001].rgba, [black])
373
+ assert_array_equal(hilo[0.5].rgba, [gray])
374
+ assert_array_almost_equal(hilo[0.999999].rgba, [white])
375
+ assert_array_equal(hilo[1].rgba, [red])
376
+
377
+
352
378
  run_tests_if_main()
vispy/ext/cocoapy.py CHANGED
@@ -18,26 +18,6 @@ else:
18
18
  string_types = basestring, # noqa
19
19
 
20
20
 
21
- # handle dlopen cache changes in macOS 11 (Big Sur)
22
- # ref https://stackoverflow.com/questions/63475461/unable-to-import-opengl-gl-in-python-on-macos
23
- try:
24
- import OpenGL.GL # noqa
25
- except ImportError:
26
- # print('Drat, patching for Big Sur')
27
- orig_util_find_library = util.find_library
28
-
29
- def new_util_find_library(name):
30
- res = orig_util_find_library(name)
31
- if res:
32
- return res
33
- lut = {
34
- 'objc': 'libobjc.dylib',
35
- 'quartz': 'Quartz.framework/Quartz'
36
- }
37
- return lut.get(name, name+'.framework/'+name)
38
- util.find_library = new_util_find_library
39
-
40
-
41
21
  # Based on Pyglet code
42
22
 
43
23
  ##############################################################################
@@ -1285,7 +1265,7 @@ NSApplicationActivationPolicyProhibited = 2
1285
1265
 
1286
1266
  # QUARTZ / COREGRAPHICS
1287
1267
 
1288
- quartz = cdll.LoadLibrary(util.find_library('quartz'))
1268
+ quartz = cdll.LoadLibrary(util.find_library('Quartz'))
1289
1269
 
1290
1270
  CGDirectDisplayID = c_uint32 # CGDirectDisplay.h
1291
1271
  CGError = c_int32 # CGError.h
@@ -386,7 +386,9 @@ class MeshData(object):
386
386
  if indexed is None:
387
387
  return self._vertex_normals
388
388
  elif indexed == 'faces':
389
- return self._vertex_normals[self.get_faces()]
389
+ if self._vertex_normals_indexed_by_faces is None:
390
+ self._vertex_normals_indexed_by_faces = self._vertex_normals[self.get_faces()]
391
+ return self._vertex_normals_indexed_by_faces
390
392
 
391
393
  def get_vertex_colors(self, indexed=None):
392
394
  """Get vertex colors
@@ -2,6 +2,7 @@ import sys
2
2
  from unittest import SkipTest
3
3
 
4
4
  import numpy as np
5
+ import pytest
5
6
  from numpy.testing import assert_array_almost_equal
6
7
 
7
8
  from vispy.testing import run_tests_if_main
@@ -503,4 +504,91 @@ def test_edge_event():
503
504
  t.triangulate()
504
505
 
505
506
 
507
+ def test_triangulate_triangle():
508
+ pts = np.array([
509
+ [4, 4],
510
+ [1, 4],
511
+ [1, 2],
512
+ ])
513
+ t = _triangulation_from_points(pts)
514
+
515
+ t.triangulate()
516
+
517
+ assert len(t.tris) == 1
518
+ _assert_triangle_pts_in_input(t, pts)
519
+
520
+
521
+ def test_triangulate_square():
522
+ pts = np.array([
523
+ [4, 4],
524
+ [1, 4],
525
+ [1, 2],
526
+ [4, 2],
527
+ ])
528
+ t = _triangulation_from_points(pts)
529
+
530
+ t.triangulate()
531
+
532
+ assert len(t.tris) == 2
533
+ _assert_triangle_pts_in_input(t, pts)
534
+
535
+
536
+ def test_triangulate_triangle_with_collinear_pts():
537
+ pts = np.array([
538
+ [4, 4],
539
+ [3, 4],
540
+ [1, 4],
541
+ [1, 2],
542
+ ])
543
+ t = _triangulation_from_points(pts)
544
+
545
+ t.triangulate()
546
+
547
+ assert len(t.tris) in (1, 2)
548
+ _assert_triangle_pts_in_input(t, pts)
549
+
550
+
551
+ def test_triangulate_collinear_path():
552
+ pts = np.array([
553
+ [4, 4],
554
+ [3, 4],
555
+ [1, 4],
556
+ ])
557
+ t = _triangulation_from_points(pts)
558
+
559
+ t.triangulate()
560
+
561
+ assert len(t.tris) == 0
562
+ _assert_triangle_pts_in_input(t, pts)
563
+
564
+
565
+ @pytest.mark.xfail(reason="See https://github.com/vispy/vispy/issues/2247")
566
+ def test_triangulate_collinear_path_with_repeat():
567
+ pts = np.array([
568
+ [4, 4],
569
+ [3, 4],
570
+ [1, 4],
571
+ [4, 4],
572
+ [1, 2],
573
+ ])
574
+ t = _triangulation_from_points(pts)
575
+
576
+ t.triangulate()
577
+
578
+ assert len(t.tris) == 0
579
+ _assert_triangle_pts_in_input(t, pts)
580
+
581
+
582
+ def _assert_triangle_pts_in_input(t, input_pts):
583
+ pt_indices_in_tris = set(v for tri in t.tris for v in tri)
584
+ for i in pt_indices_in_tris:
585
+ assert np.any(np.all(t.pts[i] == input_pts, axis=1))
586
+
587
+
588
+ def _triangulation_from_points(points):
589
+ inds = np.arange(points.shape[0])[:, np.newaxis]
590
+ edges = np.hstack([inds, np.roll(inds, -1)])
591
+ return T(points, edges)
592
+
593
+
506
594
  run_tests_if_main()
@@ -99,7 +99,7 @@ class Triangulation(object):
99
99
  self._tops = self.edges.max(axis=1)
100
100
  self._bottoms = self.edges.min(axis=1)
101
101
 
102
- # inintialize sweep front
102
+ # initialize sweep front
103
103
  # values in this list are indexes into self.pts
104
104
  self._front = [0, 2, 1]
105
105
 
@@ -185,7 +185,7 @@ class Triangulation(object):
185
185
  for j in self._bottoms[self._tops == i]:
186
186
  # Make sure edge (j, i) is present in mesh
187
187
  # because edge event may have created a new front list
188
- self._edge_event(i, j)
188
+ self._edge_event(i, int(j))
189
189
  front = self._front
190
190
 
191
191
  self._finalize()
@@ -203,7 +203,7 @@ class Triangulation(object):
203
203
  while k < idx-1:
204
204
  # if edges lie in counterclockwise direction, then signed area
205
205
  # is positive
206
- if self._iscounterclockwise(front[k], front[k+1], front[k+2]):
206
+ if self._orientation((front[k], front[k+1]), front[k+2]) < 0:
207
207
  self._add_tri(front[k], front[k+1], front[k+2])
208
208
  front.pop(k+1)
209
209
  idx -= 1
@@ -398,7 +398,7 @@ class Triangulation(object):
398
398
 
399
399
  upper_polygon.append(front[front_index+front_dir])
400
400
 
401
- # (iii) triangluate empty areas
401
+ # (iii) triangulate empty areas
402
402
 
403
403
  for polygon in [lower_polygon, upper_polygon]:
404
404
  dist = self._distances_from_line((i, j), polygon)
@@ -673,13 +673,6 @@ class Triangulation(object):
673
673
  d = (a + c - b) / ((4 * a * c)**0.5)
674
674
  return d
675
675
 
676
- def _iscounterclockwise(self, a, b, c):
677
- # Check if the points lie in counter-clockwise order or not
678
- A = self.pts[a]
679
- B = self.pts[b]
680
- C = self.pts[c]
681
- return _cross_2d(B - A, C - B) > 0
682
-
683
676
  def _edges_intersect(self, edge1, edge2):
684
677
  """Return 1 if edges intersect completely (endpoints excluded)"""
685
678
  h12 = self._intersect_edge_arrays(self.pts[np.array(edge1)],
@@ -748,7 +741,7 @@ class Triangulation(object):
748
741
  # sanity check
749
742
  assert a != b and b != c and c != a
750
743
 
751
- # ignore flat tris
744
+ # ignore tris with duplicate points
752
745
  pa = self.pts[a]
753
746
  pb = self.pts[b]
754
747
  pc = self.pts[c]
@@ -761,8 +754,13 @@ class Triangulation(object):
761
754
  raise Exception("Cannot add %s; already have %s" %
762
755
  ((a, b, c), t))
763
756
 
757
+ # ignore lines
758
+ orientation = self._orientation((a, b), c)
759
+ if orientation == 0:
760
+ return
761
+
764
762
  # TODO: should add to edges_lookup after legalization??
765
- if self._iscounterclockwise(a, b, c):
763
+ if orientation < 0:
766
764
  assert (a, b) not in self._edges_lookup
767
765
  assert (b, c) not in self._edges_lookup
768
766
  assert (c, a) not in self._edges_lookup