- added an example showing performance degradation in VisPy 0.9.1
This commit is contained in:
724
Utils/vispy_example_qt6_draw.py
Normal file
724
Utils/vispy_example_qt6_draw.py
Normal file
@@ -0,0 +1,724 @@
|
||||
from PyQt6.QtGui import QPalette, QScreen
|
||||
from PyQt6 import QtCore, QtWidgets
|
||||
|
||||
import threading
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
from OpenGL import GLU
|
||||
|
||||
import vispy.scene as scene
|
||||
from vispy.scene.cameras.base_camera import BaseCamera
|
||||
from vispy.color import Color
|
||||
from vispy.visuals import CompoundVisual, MeshVisual, LineVisual
|
||||
from vispy.scene.visuals import VisualNode, generate_docstring, visuals
|
||||
from vispy.gloo import set_state
|
||||
|
||||
from shapely.geometry import Polygon, LineString, LinearRing
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
class VisPyCanvas(scene.SceneCanvas):
|
||||
|
||||
def __init__(self, config=None):
|
||||
super().__init__(config=config, keys=None)
|
||||
|
||||
self.unfreeze()
|
||||
|
||||
# Colors used by the Scene
|
||||
theme_color = Color('#FFFFFF')
|
||||
tick_color = Color('#000000')
|
||||
back_color = str(QPalette().color(QPalette.ColorRole.Window).name())
|
||||
|
||||
# Central Widget Colors
|
||||
self.central_widget.bgcolor = back_color
|
||||
self.central_widget.border_color = back_color
|
||||
|
||||
# Add a Grid Widget
|
||||
self.grid_widget = self.central_widget.add_grid(margin=10)
|
||||
self.grid_widget.spacing = 0
|
||||
|
||||
# TOP Padding
|
||||
top_padding = self.grid_widget.add_widget(row=0, col=0, col_span=2)
|
||||
top_padding.height_max = 0
|
||||
|
||||
# RIGHT Padding
|
||||
right_padding = self.grid_widget.add_widget(row=0, col=2, row_span=2)
|
||||
right_padding.width_max = 0
|
||||
|
||||
# X Axis
|
||||
self.xaxis = scene.AxisWidget(
|
||||
orientation='bottom', axis_color=tick_color, text_color=tick_color,
|
||||
font_size=8, axis_width=1,
|
||||
anchors=['center', 'bottom']
|
||||
)
|
||||
self.xaxis.height_max = 40
|
||||
self.grid_widget.add_widget(self.xaxis, row=2, col=1)
|
||||
|
||||
# Y Axis
|
||||
self.yaxis = scene.AxisWidget(
|
||||
orientation='left', axis_color=tick_color, text_color=tick_color,
|
||||
font_size=8, axis_width=1
|
||||
)
|
||||
self.yaxis.width_max = 70
|
||||
self.grid_widget.add_widget(self.yaxis, row=1, col=0)
|
||||
|
||||
# View & Camera
|
||||
self.view = self.grid_widget.add_view(row=1, col=1, border_color=tick_color,
|
||||
bgcolor=theme_color)
|
||||
self.view.camera = MyCamera(aspect=1, rect=(-25, -25, 150, 150))
|
||||
|
||||
self.xaxis.link_view(self.view)
|
||||
self.yaxis.link_view(self.view)
|
||||
|
||||
# add GridLines
|
||||
self.grid = scene.GridLines(parent=self.view.scene, color='dimgray')
|
||||
self.grid.set_gl_state(depth_test=False)
|
||||
|
||||
self.freeze()
|
||||
|
||||
|
||||
class MyCamera(scene.PanZoomCamera):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(MyCamera, self).__init__(**kwargs)
|
||||
|
||||
self.minimum_scene_size = 0.01
|
||||
self.maximum_scene_size = 10000
|
||||
|
||||
self.last_event = None
|
||||
self.last_time = 0
|
||||
|
||||
# Default mouse button for panning is RMB
|
||||
self.pan_button_setting = "2"
|
||||
|
||||
def zoom(self, factor, center=None):
|
||||
center = center if (center is not None) else self.center
|
||||
super(MyCamera, self).zoom(factor, center)
|
||||
|
||||
def viewbox_mouse_event(self, event):
|
||||
"""
|
||||
The SubScene received a mouse event; update transform
|
||||
accordingly.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
event : instance of Event
|
||||
The event.
|
||||
"""
|
||||
if event.handled or not self.interactive:
|
||||
return
|
||||
|
||||
# key modifiers
|
||||
modifiers = event.mouse_event.modifiers
|
||||
|
||||
# Limit mouse move events
|
||||
last_event = event.last_event
|
||||
t = time.time()
|
||||
if t - self.last_time > 0.015:
|
||||
self.last_time = t
|
||||
if self.last_event:
|
||||
last_event = self.last_event
|
||||
self.last_event = None
|
||||
else:
|
||||
if not self.last_event:
|
||||
self.last_event = last_event
|
||||
event.handled = True
|
||||
return
|
||||
|
||||
# ################### Scrolling ##########################
|
||||
BaseCamera.viewbox_mouse_event(self, event)
|
||||
|
||||
if event.type == 'mouse_wheel':
|
||||
if not modifiers:
|
||||
center = self._scene_transform.imap(event.pos)
|
||||
scale = (1 + self.zoom_factor) ** (-event.delta[1] * 30)
|
||||
self.limited_zoom(scale, center)
|
||||
event.handled = True
|
||||
|
||||
elif event.type == 'mouse_move':
|
||||
if event.press_event is None:
|
||||
return
|
||||
|
||||
# ################ Panning ############################
|
||||
if event.button == int(self.pan_button_setting) and not modifiers:
|
||||
# Translate
|
||||
p1 = np.array(last_event.pos)[:2]
|
||||
p2 = np.array(event.pos)[:2]
|
||||
p1s = self._transform.imap(p1)
|
||||
p2s = self._transform.imap(p2)
|
||||
self.pan(p1s-p2s)
|
||||
event.handled = True
|
||||
elif event.button in [2, 3] and 'Shift' in modifiers:
|
||||
# Zoom
|
||||
p1c = np.array(last_event.pos)[:2]
|
||||
p2c = np.array(event.pos)[:2]
|
||||
scale = ((1 + self.zoom_factor) **
|
||||
((p1c-p2c) * np.array([1, -1])))
|
||||
center = self._transform.imap(event.press_event.pos[:2])
|
||||
self.limited_zoom(scale, center)
|
||||
event.handled = True
|
||||
else:
|
||||
event.handled = False
|
||||
elif event.type == 'mouse_press':
|
||||
# accept the event if it is button 1 or 2.
|
||||
# This is required in order to receive future events
|
||||
event.handled = event.button in [1, 2, 3]
|
||||
else:
|
||||
event.handled = False
|
||||
|
||||
def limited_zoom(self, scale, center):
|
||||
|
||||
try:
|
||||
zoom_in = scale[1] < 1
|
||||
except IndexError:
|
||||
zoom_in = scale < 1
|
||||
|
||||
if (not zoom_in and self.rect.width < self.maximum_scene_size) \
|
||||
or (zoom_in and self.rect.width > self.minimum_scene_size):
|
||||
self.zoom(scale, center)
|
||||
|
||||
|
||||
class MyGui(QtWidgets.QMainWindow):
|
||||
|
||||
def __init__(self, app):
|
||||
super().__init__()
|
||||
|
||||
self.setWindowTitle("VisPy Test")
|
||||
|
||||
self.my_app = app
|
||||
|
||||
# add Menubar
|
||||
self.menu = self.menuBar()
|
||||
self.menufile = self.menu.addMenu("File")
|
||||
self.menuedit = self.menu.addMenu("Edit")
|
||||
self.menufhelp = self.menu.addMenu("Help")
|
||||
|
||||
# add a Toolbar
|
||||
self.file_toolbar = QtWidgets.QToolBar("File Toolbar")
|
||||
self.addToolBar(self.file_toolbar)
|
||||
self.button = self.file_toolbar.addAction("Open")
|
||||
|
||||
# add Central Widget
|
||||
self.c_widget = QtWidgets.QWidget()
|
||||
self.central_layout = QtWidgets.QVBoxLayout()
|
||||
self.c_widget.setLayout(self.central_layout)
|
||||
self.setCentralWidget(self.c_widget)
|
||||
|
||||
# add InfoBar
|
||||
self.infobar = self.statusBar()
|
||||
self.fps_label = QtWidgets.QLabel("FPS: 0.0")
|
||||
self.infobar.addWidget(self.fps_label)
|
||||
|
||||
|
||||
class MyApp(QtCore.QObject):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.ui = MyGui(app=self)
|
||||
|
||||
# VisPyCanvas instance
|
||||
self.vispy_canvas = VisPyCanvas()
|
||||
|
||||
self.vispy_canvas.unfreeze()
|
||||
self.vispy_canvas.create_native()
|
||||
self.vispy_canvas.native.setParent(self.ui)
|
||||
self.ui.central_layout.addWidget(self.vispy_canvas.native)
|
||||
self.vispy_canvas.freeze()
|
||||
|
||||
self.ui.show()
|
||||
|
||||
# add a shape on canvas
|
||||
self.shape_collection = ShapeCollection(parent=self.vispy_canvas.view.scene, layers=1)
|
||||
element = Polygon([(1, 1), (110, 1), (110, 110), (1, 110), (1, 1)])
|
||||
self.shape_collection.add(shape=element, color='red', face_color='#0000FAAF', update=True)
|
||||
# show FPS
|
||||
self.vispy_canvas.measure_fps(callback=self.show_fps)
|
||||
|
||||
def show_fps(self, fps_val):
|
||||
self.ui.fps_label.setText("FPS: %1.1f" % float(fps_val))
|
||||
|
||||
|
||||
class ShapeCollectionVisual(CompoundVisual):
|
||||
|
||||
def __init__(self, linewidth=1, triangulation='vispy', layers=3, pool=None, **kwargs):
|
||||
"""
|
||||
Represents collection of shapes to draw on VisPy scene
|
||||
:param linewidth: 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.results = {}
|
||||
|
||||
self._meshes = [MeshVisual() for _ in range(0, layers)]
|
||||
self._lines = [LineVisual(antialias=True) for _ in range(0, layers)]
|
||||
|
||||
self._line_width = linewidth
|
||||
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:
|
||||
m.set_gl_state(polygon_offset_fill=True, polygon_offset=(1, 1), cull_face=False)
|
||||
|
||||
for lne in self._lines:
|
||||
lne.set_gl_state(blend=True)
|
||||
|
||||
self.freeze()
|
||||
|
||||
def add(self, shape=None, color=None, face_color=None, alpha=None, visible=True,
|
||||
update=False, layer=0, tolerance=None, linewidth=None):
|
||||
"""
|
||||
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 alpha: str
|
||||
Polygon transparency
|
||||
: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
|
||||
:param linewidth: int
|
||||
Width of the line
|
||||
: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}
|
||||
|
||||
if linewidth:
|
||||
self._line_width = linewidth
|
||||
|
||||
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.last_key = -1
|
||||
self.data.clear()
|
||||
if update:
|
||||
self.__update()
|
||||
|
||||
def update_visibility(self, state: bool, indexes=None) -> None:
|
||||
# Lock sub-visuals updates
|
||||
self.update_lock.acquire(True)
|
||||
if indexes is None:
|
||||
for k, data in list(self.data.items()):
|
||||
self.data[k]['visible'] = state
|
||||
else:
|
||||
for k, data in list(self.data.items()):
|
||||
if k in indexes:
|
||||
self.data[k]['visible'] = state
|
||||
|
||||
self.update_lock.release()
|
||||
|
||||
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("VisPyVisuals.ShapeCollectionVisual._update() --> Data error. %s" % str(e))
|
||||
|
||||
# Updating meshes
|
||||
for i, mesh in enumerate(self._meshes):
|
||||
if len(mesh_vertices[i]) > 0:
|
||||
set_state(polygon_offset_fill=False)
|
||||
faces_array = np.asarray(mesh_tris[i], dtype=np.uint32)
|
||||
mesh.set_data(
|
||||
vertices=np.asarray(mesh_vertices[i]),
|
||||
faces=faces_array.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.visible = True
|
||||
line.set_data(
|
||||
pos=np.asarray(line_pts[i]),
|
||||
color=np.asarray(line_colors[i]),
|
||||
width=self._line_width,
|
||||
connect='segments')
|
||||
else:
|
||||
# line.clear_data()
|
||||
line.visible = False
|
||||
|
||||
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.keys()) if not indexes else indexes:
|
||||
if i in list(self.results.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("VisPyVisuals.ShapeCollectionVisual.redraw() --> Data error = %s. Indexes = %s" %
|
||||
(str(e), str(indexes)))
|
||||
|
||||
self.results_lock.release()
|
||||
|
||||
self.__update()
|
||||
|
||||
def lock_updates(self):
|
||||
self.update_lock.acquire(True)
|
||||
|
||||
def unlock_updates(self):
|
||||
self.update_lock.release()
|
||||
|
||||
|
||||
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:
|
||||
simplified_geo = 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(simplified_geo.coords))
|
||||
|
||||
elif type(geo) == LinearRing:
|
||||
# Prepare lines
|
||||
pts = _linearring_to_segments(list(simplified_geo.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(simplified_geo)
|
||||
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(simplified_geo.exterior.coords))
|
||||
for ints in simplified_geo.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
|
||||
face_color_rgba = Color(face_color).rgba
|
||||
# mesh_colors += [face_color_rgba] * (len(tri_tris) // 3)
|
||||
mesh_colors += [face_color_rgba for __ in range(len(tri_tris) // 3)]
|
||||
|
||||
# Appending data for line
|
||||
if len(pts) > 0:
|
||||
line_pts += pts
|
||||
colo_rgba = Color(color).rgba
|
||||
# line_colors += [colo_rgba] * len(pts)
|
||||
line_colors += [colo_rgba for __ in range(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]
|
||||
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
class GLUTess:
|
||||
def __init__(self):
|
||||
"""
|
||||
OpenGL GLU triangulation class
|
||||
"""
|
||||
self.tris = []
|
||||
self.pts = []
|
||||
self.vertex_index = 0
|
||||
|
||||
def _on_begin_primitive(self, type):
|
||||
pass
|
||||
|
||||
def _on_new_vertex(self, vertex):
|
||||
self.tris.append(vertex)
|
||||
|
||||
# Force GLU to return separate triangles (GLU_TRIANGLES)
|
||||
def _on_edge_flag(self, flag):
|
||||
pass
|
||||
|
||||
def _on_combine(self, coords, data, weight):
|
||||
return coords[0], coords[1], coords[2]
|
||||
|
||||
@staticmethod
|
||||
def _on_error(errno):
|
||||
print("GLUTess error:", errno)
|
||||
|
||||
def _on_end_primitive(self):
|
||||
pass
|
||||
|
||||
def triangulate(self, polygon):
|
||||
"""
|
||||
Triangulates polygon
|
||||
:param polygon: shapely.geometry.polygon
|
||||
Polygon to tessellate
|
||||
:return: list, list
|
||||
Array of triangle vertex indices [t0i0, t0i1, t0i2, t1i0, t1i1, ... ]
|
||||
Array of polygon points [(x0, y0), (x1, y1), ... ]
|
||||
"""
|
||||
# Create tessellation object
|
||||
tess = GLU.gluNewTess()
|
||||
|
||||
# Setup callbacks
|
||||
GLU.gluTessCallback(tess, GLU.GLU_TESS_BEGIN, self._on_begin_primitive)
|
||||
GLU.gluTessCallback(tess, GLU.GLU_TESS_VERTEX, self._on_new_vertex)
|
||||
GLU.gluTessCallback(tess, GLU.GLU_TESS_EDGE_FLAG, self._on_edge_flag)
|
||||
GLU.gluTessCallback(tess, GLU.GLU_TESS_COMBINE, self._on_combine)
|
||||
GLU.gluTessCallback(tess, GLU.GLU_TESS_ERROR, self._on_error)
|
||||
GLU.gluTessCallback(tess, GLU.GLU_TESS_END, self._on_end_primitive)
|
||||
|
||||
# Reset data
|
||||
del self.tris[:]
|
||||
del self.pts[:]
|
||||
self.vertex_index = 0
|
||||
|
||||
# Define polygon
|
||||
GLU.gluTessBeginPolygon(tess, None)
|
||||
|
||||
def define_contour(contour):
|
||||
vertices = list(contour.coords) # Get vertices coordinates
|
||||
if vertices[0] == vertices[-1]: # Open ring
|
||||
vertices = vertices[:-1]
|
||||
|
||||
self.pts += vertices
|
||||
|
||||
GLU.gluTessBeginContour(tess) # Start contour
|
||||
|
||||
# Set vertices
|
||||
for vertex in vertices:
|
||||
point = (vertex[0], vertex[1], 0)
|
||||
GLU.gluTessVertex(tess, point, self.vertex_index)
|
||||
self.vertex_index += 1
|
||||
|
||||
GLU.gluTessEndContour(tess) # End contour
|
||||
|
||||
# Polygon exterior
|
||||
define_contour(polygon.exterior)
|
||||
|
||||
# Interiors
|
||||
for interior in polygon.interiors:
|
||||
define_contour(interior)
|
||||
|
||||
# Start tessellation
|
||||
GLU.gluTessEndPolygon(tess)
|
||||
|
||||
# Free resources
|
||||
GLU.gluDeleteTess(tess)
|
||||
|
||||
return self.tris, self.pts
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
|
||||
m_app = MyApp()
|
||||
sys.exit(app.exec())
|
||||
Reference in New Issue
Block a user