-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathVisPyVisuals.py
596 lines (487 loc) · 18.1 KB
/
VisPyVisuals.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
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
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
from vispy.visuals import CompoundVisual, LineVisual, MeshVisual, TextVisual, MarkersVisual
from vispy.scene.visuals import VisualNode, generate_docstring, visuals
from vispy.gloo import set_state
from vispy.color import Color
from shapely.geometry import Polygon, LineString, LinearRing
import threading
import numpy as np
from VisPyTesselators import GLUTess
class FlatCAMLineVisual(LineVisual):
def __init__(self, pos=None, color=(0.5, 0.5, 0.5, 1), width=1, connect='strip',
method='gl', antialias=False):
LineVisual.__init__(self, pos=None, color=(0.5, 0.5, 0.5, 1), width=1, connect='strip',
method='gl', antialias=True)
def clear_data(self):
self._bounds = None
self._pos = None
self._changed['pos'] = True
self.update()
def _update_shape_buffers(data, triangulation='glu'):
"""
Translates Shapely geometry to internal buffers for speedup redraws
:param data: dict
Input shape data
:param triangulation: str
Triangulation engine
"""
mesh_vertices = [] # Vertices for mesh
mesh_tris = [] # Faces for mesh
mesh_colors = [] # Face colors
line_pts = [] # Vertices for line
line_colors = [] # Line color
geo, color, face_color, tolerance = data['geometry'], data['color'], data['face_color'], data['tolerance']
if geo is not None and not geo.is_empty:
simple = geo.simplify(tolerance) if tolerance else geo # Simplified shape
pts = [] # Shape line points
tri_pts = [] # Mesh vertices
tri_tris = [] # Mesh faces
if type(geo) == LineString:
# Prepare lines
pts = _linestring_to_segments(list(simple.coords))
elif type(geo) == LinearRing:
# Prepare lines
pts = _linearring_to_segments(list(simple.coords))
elif type(geo) == Polygon:
# Prepare polygon faces
if face_color is not None:
if triangulation == 'glu':
gt = GLUTess()
tri_tris, tri_pts = gt.triangulate(simple)
else:
print("Triangulation type '%s' isn't implemented. Drawing only edges." % triangulation)
# Prepare polygon edges
if color is not None:
pts = _linearring_to_segments(list(simple.exterior.coords))
for ints in simple.interiors:
pts += _linearring_to_segments(list(ints.coords))
# Appending data for mesh
if len(tri_pts) > 0 and len(tri_tris) > 0:
mesh_tris += tri_tris
mesh_vertices += tri_pts
mesh_colors += [Color(face_color).rgba] * (len(tri_tris) // 3)
# Appending data for line
if len(pts) > 0:
line_pts += pts
line_colors += [Color(color).rgba] * len(pts)
# Store buffers
data['line_pts'] = line_pts
data['line_colors'] = line_colors
data['mesh_vertices'] = mesh_vertices
data['mesh_tris'] = mesh_tris
data['mesh_colors'] = mesh_colors
# Clear shapely geometry
del data['geometry']
return data
def _linearring_to_segments(arr):
# Close linear ring
"""
Translates linear ring to line segments
:param arr: numpy.array
Array of linear ring vertices
:return: numpy.array
Line segments
"""
if arr[0] != arr[-1]:
arr.append(arr[0])
return _linestring_to_segments(arr)
def _linestring_to_segments(arr):
"""
Translates line strip to segments
:param arr: numpy.array
Array of line strip vertices
:return: numpy.array
Line segments
"""
return [arr[i // 2] for i in range(0, len(arr) * 2)][1:-1]
class ShapeGroup(object):
def __init__(self, collection):
"""
Represents group of shapes in collection
:param collection: ShapeCollection
Collection to work with
"""
self._collection = collection
self._indexes = []
self._visible = True
self._color = None
def add(self, **kwargs):
"""
Adds shape to collection and store index in group
:param kwargs: keyword arguments
Arguments for ShapeCollection.add function
"""
self._indexes.append(self._collection.add(**kwargs))
def clear(self, update=False):
"""
Removes group shapes from collection, clear indexes
:param update: bool
Set True to redraw collection
"""
for i in self._indexes:
self._collection.remove(i, False)
del self._indexes[:]
if update:
self._collection.redraw([]) # Skip waiting results
def redraw(self):
"""
Redraws shape collection
"""
self._collection.redraw(self._indexes)
@property
def visible(self):
"""
Visibility of group
:return: bool
"""
return self._visible
@visible.setter
def visible(self, value):
"""
Visibility of group
:param value: bool
"""
self._visible = value
for i in self._indexes:
self._collection.data[i]['visible'] = value
self._collection.redraw([])
class ShapeCollectionVisual(CompoundVisual):
def __init__(self, line_width=1, triangulation='gpc', layers=3, pool=None, **kwargs):
"""
Represents collection of shapes to draw on VisPy scene
:param line_width: float
Width of lines/edges
:param triangulation: str
Triangulation method used for polygons translation
'vispy' - VisPy lib triangulation
'gpc' - Polygon2 lib
:param layers: int
Layers count
Each layer adds 2 visuals on VisPy scene. Be careful: more layers cause less fps
:param kwargs:
"""
self.data = {}
self.last_key = -1
# Thread locks
self.key_lock = threading.Lock()
self.results_lock = threading.Lock()
self.update_lock = threading.Lock()
# Process pool
self.pool = pool
self.results = {}
self._meshes = [MeshVisual() for _ in range(0, layers)]
# self._lines = [LineVisual(antialias=True) for _ in range(0, layers)]
self._lines = [FlatCAMLineVisual(antialias=True) for _ in range(0, layers)]
self._line_width = line_width
self._triangulation = triangulation
visuals_ = [self._lines[i // 2] if i % 2 else self._meshes[i // 2] for i in range(0, layers * 2)]
CompoundVisual.__init__(self, visuals_, **kwargs)
for m in self._meshes:
pass
m.set_gl_state(polygon_offset_fill=True, polygon_offset=(1, 1), cull_face=False)
for l in self._lines:
pass
l.set_gl_state(blend=True)
self.freeze()
def add(self, shape=None, color=None, face_color=None, alpha=None, visible=True,
update=False, layer=1, tolerance=0.01):
"""
Adds shape to collection
:return:
:param shape: shapely.geometry
Shapely geometry object
:param color: str, tuple
Line/edge color
:param face_color: str, tuple
Polygon face color
:param visible: bool
Shape visibility
:param update: bool
Set True to redraw collection
:param layer: int
Layer number. 0 - lowest.
:param tolerance: float
Geometry simplifying tolerance
:return: int
Index of shape
"""
# Get new key
self.key_lock.acquire(True)
self.last_key += 1
key = self.last_key
self.key_lock.release()
# Prepare data for translation
self.data[key] = {'geometry': shape, 'color': color, 'alpha': alpha, 'face_color': face_color,
'visible': visible, 'layer': layer, 'tolerance': tolerance}
# Add data to process pool if pool exists
try:
self.results[key] = self.pool.map_async(_update_shape_buffers, [self.data[key]])
except:
self.data[key] = _update_shape_buffers(self.data[key])
if update:
self.redraw() # redraw() waits for pool process end
return key
def remove(self, key, update=False):
"""
Removes shape from collection
:param key: int
Shape index to remove
:param update:
Set True to redraw collection
"""
# Remove process result
self.results_lock.acquire(True)
if key in list(self.results.copy().keys()):
del self.results[key]
self.results_lock.release()
# Remove data
del self.data[key]
if update:
self.__update()
def clear(self, update=False):
"""
Removes all shapes from collection
:param update: bool
Set True to redraw collection
"""
self.data.clear()
if update:
self.__update()
def __update(self):
"""
Merges internal buffers, sets data to visuals, redraws collection on scene
"""
mesh_vertices = [[] for _ in range(0, len(self._meshes))] # Vertices for mesh
mesh_tris = [[] for _ in range(0, len(self._meshes))] # Faces for mesh
mesh_colors = [[] for _ in range(0, len(self._meshes))] # Face colors
line_pts = [[] for _ in range(0, len(self._lines))] # Vertices for line
line_colors = [[] for _ in range(0, len(self._lines))] # Line color
# Lock sub-visuals updates
self.update_lock.acquire(True)
# Merge shapes buffers
for data in list(self.data.values()):
if data['visible'] and 'line_pts' in data:
try:
line_pts[data['layer']] += data['line_pts']
line_colors[data['layer']] += data['line_colors']
mesh_tris[data['layer']] += [x + len(mesh_vertices[data['layer']])
for x in data['mesh_tris']]
mesh_vertices[data['layer']] += data['mesh_vertices']
mesh_colors[data['layer']] += data['mesh_colors']
except Exception as e:
print("Data error", e)
# Updating meshes
for i, mesh in enumerate(self._meshes):
if len(mesh_vertices[i]) > 0:
set_state(polygon_offset_fill=False)
mesh.set_data(np.asarray(mesh_vertices[i]), np.asarray(mesh_tris[i], dtype=np.uint32)
.reshape((-1, 3)), face_colors=np.asarray(mesh_colors[i]))
else:
mesh.set_data()
mesh._bounds_changed()
# Updating lines
for i, line in enumerate(self._lines):
if len(line_pts[i]) > 0:
line.set_data(np.asarray(line_pts[i]), np.asarray(line_colors[i]), self._line_width, 'segments')
else:
line.clear_data()
line._bounds_changed()
self._bounds_changed()
self.update_lock.release()
def redraw(self, indexes=None):
"""
Redraws collection
:param indexes: list
Shape indexes to get from process pool
"""
# Only one thread can update data
self.results_lock.acquire(True)
for i in list(self.data.copy().keys()) if not indexes else indexes:
if i in list(self.results.copy().keys()):
try:
self.results[i].wait() # Wait for process results
if i in self.data:
self.data[i] = self.results[i].get()[0] # Store translated data
del self.results[i]
except Exception as e:
print(e, indexes)
self.results_lock.release()
self.__update()
def lock_updates(self):
self.update_lock.acquire(True)
def unlock_updates(self):
self.update_lock.release()
class TextGroup(object):
def __init__(self, collection):
self._collection = collection
self._index = None
self._visible = None
def set(self, **kwargs):
"""
Adds text to collection and store index
:param kwargs: keyword arguments
Arguments for TextCollection.add function
"""
self._index = self._collection.add(**kwargs)
def clear(self, update=False):
"""
Removes text from collection, clear index
:param update: bool
Set True to redraw collection
"""
if self._index is not None:
self._collection.remove(self._index, False)
self._index = None
if update:
self._collection.redraw()
def redraw(self):
"""
Redraws text collection
"""
self._collection.redraw()
@property
def visible(self):
"""
Visibility of group
:return: bool
"""
return self._visible
@visible.setter
def visible(self, value):
"""
Visibility of group
:param value: bool
"""
self._visible = value
self._collection.data[self._index]['visible'] = value
self._collection.redraw()
class TextCollectionVisual(TextVisual):
def __init__(self, **kwargs):
"""
Represents collection of shapes to draw on VisPy scene
:param kwargs: keyword arguments
Arguments to pass for TextVisual
"""
self.data = {}
self.last_key = -1
self.lock = threading.Lock()
super(TextCollectionVisual, self).__init__(**kwargs)
self.freeze()
def add(self, text, pos, visible=True, update=True):
"""
Adds array of text to collection
:param text: list
Array of strings ['str1', 'str2', ... ]
:param pos: list
Array of string positions [(0, 0), (10, 10), ... ]
:param update: bool
Set True to redraw collection
:return: int
Index of array
"""
# Get new key
self.lock.acquire(True)
self.last_key += 1
key = self.last_key
self.lock.release()
# Prepare data for translation
self.data[key] = {'text': text, 'pos': pos, 'visible': visible}
if update:
self.redraw()
return key
def remove(self, key, update=False):
"""
Removes shape from collection
:param key: int
Shape index to remove
:param update:
Set True to redraw collection
"""
del self.data[key]
if update:
self.__update()
def clear(self, update=False):
"""
Removes all shapes from colleciton
:param update: bool
Set True to redraw collection
"""
self.data.clear()
if update:
self.__update()
def __update(self):
"""
Merges internal buffers, sets data to visuals, redraws collection on scene
"""
labels = []
pos = []
# Merge buffers
for data in list(self.data.values()):
if data['visible']:
try:
labels += data['text']
pos += data['pos']
except Exception as e:
print("Data error", e)
# Updating text
if len(labels) > 0:
self.text = labels
self.pos = pos
else:
self.text = None
self.pos = (0, 0)
self._bounds_changed()
def redraw(self):
"""
Redraws collection
"""
self.__update()
# Add 'enabled' property to visual nodes
def create_fast_node(subclass):
# Create a new subclass of Node.
# Decide on new class name
clsname = subclass.__name__
if not (clsname.endswith('Visual') and
issubclass(subclass, visuals.BaseVisual)):
raise RuntimeError('Class "%s" must end with Visual, and must '
'subclass BaseVisual' % clsname)
clsname = clsname[:-6]
# Generate new docstring based on visual docstring
try:
doc = generate_docstring(subclass, clsname)
except Exception:
# If parsing fails, just return the original Visual docstring
doc = subclass.__doc__
# New __init__ method
def __init__(self, *args, **kwargs):
parent = kwargs.pop('parent', None)
name = kwargs.pop('name', None)
self.name = name # to allow __str__ before Node.__init__
self._visual_superclass = subclass
# parent: property,
# _parent: attribute of Node class
# __parent: attribute of fast_node class
self.__parent = parent
self._enabled = False
subclass.__init__(self, *args, **kwargs)
self.unfreeze()
VisualNode.__init__(self, parent=parent, name=name)
self.freeze()
# Create new class
cls = type(clsname, (VisualNode, subclass),
{'__init__': __init__, '__doc__': doc})
# 'Enabled' property clears/restores 'parent' property of Node class
# Scene will be painted quicker than when using 'visible' property
def get_enabled(self):
return self._enabled
def set_enabled(self, enabled):
if enabled:
self.parent = self.__parent # Restore parent
else:
if self.parent: # Store parent
self.__parent = self.parent
self.parent = None
cls.enabled = property(get_enabled, set_enabled)
return cls
ShapeCollection = create_fast_node(ShapeCollectionVisual)
TextCollection = create_fast_node(TextCollectionVisual)
Cursor = create_fast_node(MarkersVisual)