- added a visual cue in Menu -> Edit about the entries to enter the Editor and to Save & Exit Editor. When one is enabled the other is disabled.

- grouped all the UI files in flatcamGUI folder
This commit is contained in:
Marius Stanciu
2019-03-11 12:23:26 +02:00
committed by Marius
parent 29722de6ac
commit 9d33e08ecf
25 changed files with 47 additions and 79 deletions

5727
flatcamGUI/FlatCAMGUI.py Normal file

File diff suppressed because it is too large Load Diff

1420
flatcamGUI/GUIElements.py Normal file

File diff suppressed because it is too large Load Diff

1676
flatcamGUI/ObjectUI.py Normal file

File diff suppressed because it is too large Load Diff

236
flatcamGUI/PlotCanvas.py Normal file
View File

@@ -0,0 +1,236 @@
############################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# http://caram.cl/software/flatcam #
# Author: Juan Pablo Caram (c) #
# Date: 2/5/2014 #
# MIT Licence #
############################################################
from PyQt5 import QtCore
import logging
from flatcamGUI.VisPyCanvas import VisPyCanvas, time
from flatcamGUI.VisPyVisuals import ShapeGroup, ShapeCollection, TextCollection, TextGroup, Cursor
from vispy.scene.visuals import InfiniteLine, Line
import numpy as np
from vispy.geometry import Rect
log = logging.getLogger('base')
class PlotCanvas(QtCore.QObject):
"""
Class handling the plotting area in the application.
"""
def __init__(self, container, app):
"""
The constructor configures the VisPy figure that
will contain all plots, creates the base axes and connects
events to the plotting area.
:param container: The parent container in which to draw plots.
:rtype: PlotCanvas
"""
super(PlotCanvas, self).__init__()
self.app = app
# Parent container
self.container = container
# workspace lines; I didn't use the rectangle because I didn't want to add another VisPy Node,
# which might decrease performance
self.b_line, self.r_line, self.t_line, self.l_line = None, None, None, None
# Attach to parent
self.vispy_canvas = VisPyCanvas()
self.vispy_canvas.create_native()
self.vispy_canvas.native.setParent(self.app.ui)
self.container.addWidget(self.vispy_canvas.native)
### AXIS ###
self.v_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 1.0), vertical=True,
parent=self.vispy_canvas.view.scene)
self.h_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 1.0), vertical=False,
parent=self.vispy_canvas.view.scene)
# draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
# all CNC have a limited workspace
self.draw_workspace()
# if self.app.defaults['global_workspace'] is True:
# if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
# self.wkspace_t = Line(pos=)
self.shape_collections = []
self.shape_collection = self.new_shape_collection()
self.app.pool_recreated.connect(self.on_pool_recreated)
self.text_collection = self.new_text_collection()
# TODO: Should be setting to show/hide CNC job annotations (global or per object)
self.text_collection.enabled = False
# draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
# all CNC have a limited workspace
def draw_workspace(self):
a = np.empty((0, 0))
a4p_in = np.array([(0, 0), (8.3, 0), (8.3, 11.7), (0, 11.7)])
a4l_in = np.array([(0, 0), (11.7, 0), (11.7, 8.3), (0, 8.3)])
a3p_in = np.array([(0, 0), (11.7, 0), (11.7, 16.5), (0, 16.5)])
a3l_in = np.array([(0, 0), (16.5, 0), (16.5, 11.7), (0, 11.7)])
a4p_mm = np.array([(0, 0), (210, 0), (210, 297), (0, 297)])
a4l_mm = np.array([(0, 0), (297, 0), (297,210), (0, 210)])
a3p_mm = np.array([(0, 0), (297, 0), (297, 420), (0, 420)])
a3l_mm = np.array([(0, 0), (420, 0), (420, 297), (0, 297)])
if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
if self.app.defaults['global_workspaceT'] == 'A4P':
a = a4p_mm
elif self.app.defaults['global_workspaceT'] == 'A4L':
a = a4l_mm
elif self.app.defaults['global_workspaceT'] == 'A3P':
a = a3p_mm
elif self.app.defaults['global_workspaceT'] == 'A3L':
a = a3l_mm
else:
if self.app.defaults['global_workspaceT'] == 'A4P':
a = a4p_in
elif self.app.defaults['global_workspaceT'] == 'A4L':
a = a4l_in
elif self.app.defaults['global_workspaceT'] == 'A3P':
a = a3p_in
elif self.app.defaults['global_workspaceT'] == 'A3L':
a = a3l_in
self.delete_workspace()
self.b_line = Line(pos=a[0:2], color=(0.70, 0.3, 0.3, 1.0),
antialias= True, method='agg', parent=self.vispy_canvas.view.scene)
self.r_line = Line(pos=a[1:3], color=(0.70, 0.3, 0.3, 1.0),
antialias= True, method='agg', parent=self.vispy_canvas.view.scene)
self.t_line = Line(pos=a[2:4], color=(0.70, 0.3, 0.3, 1.0),
antialias= True, method='agg', parent=self.vispy_canvas.view.scene)
self.l_line = Line(pos=np.array((a[0], a[3])), color=(0.70, 0.3, 0.3, 1.0),
antialias= True, method='agg', parent=self.vispy_canvas.view.scene)
if self.app.defaults['global_workspace'] is False:
self.delete_workspace()
# delete the workspace lines from the plot by removing the parent
def delete_workspace(self):
try:
self.b_line.parent = None
self.r_line.parent = None
self.t_line.parent = None
self.l_line.parent = None
except:
pass
# redraw the workspace lines on the plot by readding them to the parent view.scene
def restore_workspace(self):
try:
self.b_line.parent = self.vispy_canvas.view.scene
self.r_line.parent = self.vispy_canvas.view.scene
self.t_line.parent = self.vispy_canvas.view.scene
self.l_line.parent = self.vispy_canvas.view.scene
except:
pass
def vis_connect(self, event_name, callback):
return getattr(self.vispy_canvas.events, event_name).connect(callback)
def vis_disconnect(self, event_name, callback):
getattr(self.vispy_canvas.events, event_name).disconnect(callback)
def zoom(self, factor, center=None):
"""
Zooms the plot by factor around a given
center point. Takes care of re-drawing.
:param factor: Number by which to scale the plot.
:type factor: float
:param center: Coordinates [x, y] of the point around which to scale the plot.
:type center: list
:return: None
"""
self.vispy_canvas.view.camera.zoom(factor, center)
def new_shape_group(self, shape_collection=None):
if shape_collection:
return ShapeGroup(shape_collection)
return ShapeGroup(self.shape_collection)
def new_shape_collection(self, **kwargs):
# sc = ShapeCollection(parent=self.vispy_canvas.view.scene, pool=self.app.pool, **kwargs)
# self.shape_collections.append(sc)
# return sc
return ShapeCollection(parent=self.vispy_canvas.view.scene, pool=self.app.pool, **kwargs)
def new_cursor(self):
c = Cursor(pos=np.empty((0, 2)), parent=self.vispy_canvas.view.scene)
c.antialias = 0
return c
def new_text_group(self):
return TextGroup(self.text_collection)
def new_text_collection(self, **kwargs):
return TextCollection(parent=self.vispy_canvas.view.scene, **kwargs)
def fit_view(self, rect=None):
# Lock updates in other threads
self.shape_collection.lock_updates()
if not rect:
rect = Rect(-1, -1, 20, 20)
try:
rect.left, rect.right = self.shape_collection.bounds(axis=0)
rect.bottom, rect.top = self.shape_collection.bounds(axis=1)
except TypeError:
pass
# adjust the view camera to be slightly bigger than the bounds so the shape colleaction can be seen clearly
# otherwise the shape collection boundary will have no border
rect.left *= 0.96
rect.bottom *= 0.96
rect.right *= 1.01
rect.top *= 1.01
self.vispy_canvas.view.camera.rect = rect
self.shape_collection.unlock_updates()
def fit_center(self, loc, rect=None):
# Lock updates in other threads
self.shape_collection.lock_updates()
if not rect:
try:
rect = Rect(loc[0]-20, loc[1]-20, 40, 40)
except TypeError:
pass
self.vispy_canvas.view.camera.rect = rect
self.shape_collection.unlock_updates()
def clear(self):
pass
def redraw(self):
self.shape_collection.redraw([])
self.text_collection.redraw()
def on_pool_recreated(self, pool):
self.shape_collection.pool = pool

175
flatcamGUI/VisPyCanvas.py Normal file
View File

@@ -0,0 +1,175 @@
############################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# http://flatcam.org #
# File Author: Dennis Hayrullin #
# Date: 2/5/2016 #
# MIT Licence #
############################################################
import numpy as np
from PyQt5.QtGui import QPalette
import vispy.scene as scene
from vispy.scene.cameras.base_camera import BaseCamera
from vispy.color import Color
import time
white = Color("#ffffff" )
black = Color("#000000")
class VisPyCanvas(scene.SceneCanvas):
def __init__(self, config=None):
scene.SceneCanvas.__init__(self, keys=None, config=config)
self.unfreeze()
back_color = str(QPalette().color(QPalette.Window).name())
self.central_widget.bgcolor = back_color
self.central_widget.border_color = back_color
self.grid_widget = self.central_widget.add_grid(margin=10)
self.grid_widget.spacing = 0
top_padding = self.grid_widget.add_widget(row=0, col=0, col_span=2)
top_padding.height_max = 0
self.yaxis = scene.AxisWidget(orientation='left', axis_color='black', text_color='black', font_size=8)
self.yaxis.width_max = 55
self.grid_widget.add_widget(self.yaxis, row=1, col=0)
self.xaxis = scene.AxisWidget(orientation='bottom', axis_color='black', text_color='black', font_size=8)
self.xaxis.height_max = 25
self.grid_widget.add_widget(self.xaxis, row=2, col=1)
right_padding = self.grid_widget.add_widget(row=0, col=2, row_span=2)
# right_padding.width_max = 24
right_padding.width_max = 0
view = self.grid_widget.add_view(row=1, col=1, border_color='black', bgcolor='white')
view.camera = Camera(aspect=1, rect=(-25,-25,150,150))
# Following function was removed from 'prepare_draw()' of 'Grid' class by patch,
# it is necessary to call manually
self.grid_widget._update_child_widget_dim()
self.xaxis.link_view(view)
self.yaxis.link_view(view)
grid1 = scene.GridLines(parent=view.scene, color='dimgray')
grid1.set_gl_state(depth_test=False)
self.view = view
self.grid = grid1
self.freeze()
# self.measure_fps()
def translate_coords(self, pos):
tr = self.grid.get_transform('canvas', 'visual')
return tr.map(pos)
def translate_coords_2(self, pos):
tr = self.grid.get_transform('visual', 'document')
return tr.map(pos)
class Camera(scene.PanZoomCamera):
def __init__(self, **kwargs):
super(Camera, 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(Camera, 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
# 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':
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
modifiers = event.mouse_event.modifiers
# self.pan_button_setting is actually self.FlatCAM.APP.defaults['global_pan_button']
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)

134
flatcamGUI/VisPyPatches.py Normal file
View File

@@ -0,0 +1,134 @@
############################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# http://flatcam.org #
# File Author: Dennis Hayrullin #
# Date: 2/5/2016 #
# MIT Licence #
############################################################
from vispy.visuals import markers, LineVisual, InfiniteLineVisual
from vispy.visuals.axis import Ticker, _get_ticks_talbot
from vispy.scene.widgets import Grid
import numpy as np
def apply_patches():
# Patch MarkersVisual to have crossed lines marker
cross_lines = """
float cross(vec2 pointcoord, float size)
{
//vbar
float r1 = abs(pointcoord.x - 0.5)*size;
float r2 = abs(pointcoord.y - 0.5)*size - $v_size/2;
float vbar = max(r1,r2);
//hbar
float r3 = abs(pointcoord.y - 0.5)*size;
float r4 = abs(pointcoord.x - 0.5)*size - $v_size/2;
float hbar = max(r3,r4);
return min(vbar, hbar);
}
"""
markers._marker_dict['++'] = cross_lines
markers.marker_types = tuple(sorted(list(markers._marker_dict.copy().keys())))
# # Add clear_data method to LineVisual to have possibility of clearing data
# def clear_data(self):
# self._bounds = None
# self._pos = None
# self._changed['pos'] = True
# self.update()
#
# LineVisual.clear_data = clear_data
# Patch VisPy Grid to prevent updating layout on PaintGL, which cause low fps
def _prepare_draw(self, view):
pass
def _update_clipper(self):
super(Grid, self)._update_clipper()
try:
self._update_child_widget_dim()
except Exception as e:
print(e)
Grid._prepare_draw = _prepare_draw
Grid._update_clipper = _update_clipper
# Patch InfiniteLine visual to 1px width
def _prepare_draw(self, view=None):
"""This method is called immediately before each draw.
The *view* argument indicates which view is about to be drawn.
"""
GL = None
from vispy.app._default_app import default_app
if default_app is not None and \
default_app.backend_name != 'ipynb_webgl':
try:
import OpenGL.GL as GL
except Exception: # can be other than ImportError sometimes
pass
if GL:
GL.glDisable(GL.GL_LINE_SMOOTH)
GL.glLineWidth(1.0)
if self._changed['pos']:
self.pos_buf.set_data(self._pos)
self._changed['pos'] = False
if self._changed['color']:
self._program.vert['color'] = self._color
self._changed['color'] = False
InfiniteLineVisual._prepare_draw = _prepare_draw
# Patch AxisVisual to have less axis labels
def _get_tick_frac_labels(self):
"""Get the major ticks, minor ticks, and major labels"""
minor_num = 4 # number of minor ticks per major division
if (self.axis.scale_type == 'linear'):
domain = self.axis.domain
if domain[1] < domain[0]:
flip = True
domain = domain[::-1]
else:
flip = False
offset = domain[0]
scale = domain[1] - domain[0]
transforms = self.axis.transforms
length = self.axis.pos[1] - self.axis.pos[0] # in logical coords
n_inches = np.sqrt(np.sum(length ** 2)) / transforms.dpi
# major = np.linspace(domain[0], domain[1], num=11)
# major = MaxNLocator(10).tick_values(*domain)
major = _get_ticks_talbot(domain[0], domain[1], n_inches, 1)
labels = ['%g' % x for x in major]
majstep = major[1] - major[0]
minor = []
minstep = majstep / (minor_num + 1)
minstart = 0 if self.axis._stop_at_major[0] else -1
minstop = -1 if self.axis._stop_at_major[1] else 0
for i in range(minstart, len(major) + minstop):
maj = major[0] + i * majstep
minor.extend(np.linspace(maj + minstep,
maj + majstep - minstep,
minor_num))
major_frac = (major - offset) / scale
minor_frac = (np.array(minor) - offset) / scale
major_frac = major_frac[::-1] if flip else major_frac
use_mask = (major_frac > -0.0001) & (major_frac < 1.0001)
major_frac = major_frac[use_mask]
labels = [l for li, l in enumerate(labels) if use_mask[li]]
minor_frac = minor_frac[(minor_frac > -0.0001) &
(minor_frac < 1.0001)]
elif self.axis.scale_type == 'logarithmic':
return NotImplementedError
elif self.axis.scale_type == 'power':
return NotImplementedError
return major_frac, minor_frac, labels
Ticker._get_tick_frac_labels = _get_tick_frac_labels

View File

@@ -0,0 +1,98 @@
############################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# http://flatcam.org #
# File Author: Dennis Hayrullin #
# Date: 2/5/2016 #
# MIT Licence #
############################################################
from OpenGL import GLU
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])
def _on_error(self, 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

604
flatcamGUI/VisPyVisuals.py Normal file
View File

@@ -0,0 +1,604 @@
############################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# http://flatcam.org #
# File Author: Dennis Hayrullin #
# Date: 2/5/2016 #
# MIT Licence #
############################################################
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 flatcamGUI.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)

0
flatcamGUI/__init__.py Normal file
View File