Merge branch 'VisPyExperemental' into develop

# Conflicts:
#	camlib.py
This commit is contained in:
HDR
2016-07-15 14:53:56 +06:00
10 changed files with 667 additions and 766 deletions

View File

@@ -12,6 +12,7 @@ from PyQt4 import QtCore
import time # Just used for debugging. Double check before removing.
from xml.dom.minidom import parseString as parse_xml_string
from contextlib import contextmanager
from vispy.geometry import Rect
########################################
## Imports part of FlatCAM ##
@@ -100,7 +101,7 @@ class App(QtCore.QObject):
# Emitted by new_object() and passes the new object as argument.
# on_object_created() adds the object to the collection,
# and emits new_object_available.
object_created = QtCore.pyqtSignal(object)
object_created = QtCore.pyqtSignal(object, bool)
# Emitted when a new object has been added to the collection
# and is ready to be used.
@@ -188,9 +189,13 @@ class App(QtCore.QObject):
#### Plot Area ####
# self.plotcanvas = PlotCanvas(self.ui.splitter)
self.plotcanvas = PlotCanvas(self.ui.right_layout, self)
self.plotcanvas.mpl_connect('button_press_event', self.on_click_over_plot)
self.plotcanvas.mpl_connect('motion_notify_event', self.on_mouse_move_over_plot)
self.plotcanvas.mpl_connect('key_press_event', self.on_key_over_plot)
# self.plotcanvas.mpl_connect('button_press_event', self.on_click_over_plot)
# self.plotcanvas.mpl_connect('motion_notify_event', self.on_mouse_move_over_plot)
# self.plotcanvas.mpl_connect('key_press_event', self.on_key_over_plot)
self.plotcanvas.vis_connect('mouse_move', self.on_mouse_move_over_plot)
self.plotcanvas.vis_connect('mouse_release', self.on_click_over_plot)
self.plotcanvas.vis_connect('key_press', self.on_key_over_plot)
self.ui.splitter.setStretchFactor(1, 2)
@@ -453,10 +458,10 @@ class App(QtCore.QObject):
self.thr2 = QtCore.QThread()
self.worker2.moveToThread(self.thr2)
self.connect(self.thr2, QtCore.SIGNAL("started()"), self.worker2.run)
self.connect(self.thr2, QtCore.SIGNAL("started()"),
lambda: self.worker_task.emit({'fcn': self.version_check,
'params': [],
'worker_name': "worker2"}))
# self.connect(self.thr2, QtCore.SIGNAL("started()"),
# lambda: self.worker_task.emit({'fcn': self.version_check,
# 'params': [],
# 'worker_name': "worker2"}))
self.thr2.start()
### Signal handling ###
@@ -629,10 +634,12 @@ class App(QtCore.QObject):
except ZeroDivisionError:
self.progress.emit(0)
return
for obj in self.collection.get_list():
if obj != self.collection.get_active() or not except_current:
obj.options['plot'] = False
obj.plot()
# obj.plot()
obj.visible = False
percentage += delta
self.progress.emit(int(percentage*100))
@@ -1027,7 +1034,7 @@ class App(QtCore.QObject):
# Move the object to the main thread and let the app know that it is available.
obj.moveToThread(QtGui.QApplication.instance().thread())
self.object_created.emit(obj)
self.object_created.emit(obj, plot)
return obj
@@ -1440,8 +1447,8 @@ class App(QtCore.QObject):
return
# Remove plot
self.plotcanvas.figure.delaxes(self.collection.get_active().axes)
self.plotcanvas.auto_adjust_axes()
# self.plotcanvas.figure.delaxes(self.collection.get_active().axes)
# self.plotcanvas.auto_adjust_axes()
# Clear form
self.setup_component_editor()
@@ -1458,7 +1465,8 @@ class App(QtCore.QObject):
:return: None
"""
self.plotcanvas.auto_adjust_axes()
# self.plotcanvas.auto_adjust_axes()
self.plotcanvas.vispy_canvas.update()
self.on_zoom_fit(None)
def on_toolbar_replot(self):
@@ -1482,7 +1490,7 @@ class App(QtCore.QObject):
def on_row_activated(self, index):
self.ui.notebook.setCurrentWidget(self.ui.selected_tab)
def on_object_created(self, obj):
def on_object_created(self, obj, plot):
"""
Event callback for object creation.
@@ -1497,8 +1505,10 @@ class App(QtCore.QObject):
self.inform.emit("Object (%s) created: %s" % (obj.kind, obj.options['name']))
self.new_object_available.emit(obj)
obj.plot()
self.on_zoom_fit(None)
if plot:
obj.plot()
self.on_zoom_fit(None)
t1 = time.time() # DEBUG
self.log.debug("%f seconds adding object and plotting." % (t1 - t0))
@@ -1511,15 +1521,16 @@ class App(QtCore.QObject):
:param event: Ignored.
:return: None
"""
# xmin, ymin, xmax, ymax = self.collection.get_bounds()
# width = xmax - xmin
# height = ymax - ymin
# xmin -= 0.05 * width
# xmax += 0.05 * width
# ymin -= 0.05 * height
# ymax += 0.05 * height
# self.plotcanvas.adjust_axes(xmin, ymin, xmax, ymax)
xmin, ymin, xmax, ymax = self.collection.get_bounds()
width = xmax - xmin
height = ymax - ymin
xmin -= 0.05 * width
xmax += 0.05 * width
ymin -= 0.05 * height
ymax += 0.05 * height
self.plotcanvas.adjust_axes(xmin, ymin, xmax, ymax)
self.plotcanvas.fit_view()
def on_key_over_plot(self, event):
"""
@@ -1574,13 +1585,17 @@ class App(QtCore.QObject):
"""
# So it can receive key presses
self.plotcanvas.canvas.setFocus()
self.plotcanvas.vispy_canvas.native.setFocus()
pos = self.plotcanvas.vispy_canvas.translate_coords(event.pos)
try:
App.log.debug('button=%d, x=%d, y=%d, xdata=%f, ydata=%f' % (
event.button, event.x, event.y, event.xdata, event.ydata))
event.button, event.pos[0], event.pos[1], pos[0], pos[1]))
# App.log.debug('button=%d, x=%d, y=%d, xdata=%f, ydata=%f' % (
# event.button, event.x, event.y, event.xdata, event.ydata))
self.clipboard.setText(self.defaults["point_clipboard_format"] % (event.xdata, event.ydata))
self.clipboard.setText(self.defaults["point_clipboard_format"] % (event.pos[0], event.pos[1]))
except Exception, e:
App.log.debug("Outside plot?")
@@ -1596,15 +1611,26 @@ class App(QtCore.QObject):
:return: None
"""
pos = self.plotcanvas.vispy_canvas.translate_coords(event.pos)
try: # May fail in case mouse not within axes
self.ui.position_label.setText("X: %.4f Y: %.4f" % (
event.xdata, event.ydata))
self.mouse = [event.xdata, event.ydata]
pos[0], pos[1]))
self.mouse = [pos[0], pos[1]]
except:
self.ui.position_label.setText("")
self.mouse = None
# try: # May fail in case mouse not within axes
# self.ui.position_label.setText("X: %.4f Y: %.4f" % (
# event.xdata, event.ydata))
# self.mouse = [event.xdata, event.ydata]
#
# except:
# self.ui.position_label.setText("")
# self.mouse = None
def on_file_new(self):
"""
Callback for menu item File->New. Returns the application to its
@@ -2265,6 +2291,8 @@ class App(QtCore.QObject):
return
for obj in self.collection.get_list():
obj.plot()
self.plotcanvas.fit_view() # Fit in proper thread
percentage += delta
self.progress.emit(int(percentage*100))
@@ -4137,7 +4165,8 @@ class App(QtCore.QObject):
return
for obj in self.collection.get_list():
obj.options['plot'] = True
obj.plot()
obj.visible = True
# obj.plot()
percentage += delta
self.progress.emit(int(percentage*100))

View File

@@ -20,6 +20,7 @@ from numpy.linalg import solve
from rtree import index as rtindex
from vispy.scene.visuals import Markers
class BufferSelectionTool(FlatCAMTool):
"""
@@ -500,7 +501,7 @@ class FCSelect(DrawTool):
except StopIteration:
return ""
if self.draw_app.key != 'control':
if self.draw_app.key != 'Control':
self.draw_app.selected = []
self.draw_app.set_selected(closest_shape)
@@ -591,7 +592,6 @@ class FlatCAMDraw(QtCore.QObject):
self.app = app
self.canvas = app.plotcanvas
self.axes = self.canvas.new_axes("draw")
### Drawing Toolbar ###
self.drawing_toolbar = QtGui.QToolBar()
@@ -695,10 +695,15 @@ class FlatCAMDraw(QtCore.QObject):
### Data
self.active_tool = None
self.storage = FlatCAMDraw.make_storage()
self.utility = []
# VisPy visuals
self.fcgeometry = None
self.shapes = self.app.plotcanvas.new_shape_collection()
self.tool_shape = self.app.plotcanvas.new_shape_collection()
self.cursor = self.app.plotcanvas.new_cursor()
## List of selected shapes.
self.selected = []
@@ -706,6 +711,8 @@ class FlatCAMDraw(QtCore.QObject):
self.move_timer.setSingleShot(True)
self.key = None # Currently pressed key
self.x = None # Current mouse cursor pos
self.y = None
def make_callback(thetool):
def f():
@@ -749,20 +756,35 @@ class FlatCAMDraw(QtCore.QObject):
self.snap_max_dist_entry.editingFinished.connect(lambda: entry2option("snap_max", self.snap_max_dist_entry))
def activate(self):
pass
print "activate"
parent = self.canvas.vispy_canvas.view.scene
self.shapes.parent = parent
self.tool_shape.parent = parent
self.cursor.parent = parent
def connect_canvas_event_handlers(self):
## Canvas events
self.cid_canvas_click = self.canvas.mpl_connect('button_press_event', self.on_canvas_click)
self.cid_canvas_move = self.canvas.mpl_connect('motion_notify_event', self.on_canvas_move)
self.cid_canvas_key = self.canvas.mpl_connect('key_press_event', self.on_canvas_key)
self.cid_canvas_key_release = self.canvas.mpl_connect('key_release_event', self.on_canvas_key_release)
# self.cid_canvas_click = self.canvas.mpl_connect('button_press_event', self.on_canvas_click)
# self.cid_canvas_move = self.canvas.mpl_connect('motion_notify_event', self.on_canvas_move)
# self.cid_canvas_key = self.canvas.mpl_connect('key_press_event', self.on_canvas_key)
# self.cid_canvas_key_release = self.canvas.mpl_connect('key_release_event', self.on_canvas_key_release)
self.canvas.vis_connect('mouse_release', self.on_canvas_click)
self.canvas.vis_connect('mouse_move', self.on_canvas_move)
self.canvas.vis_connect('key_press', self.on_canvas_key)
self.canvas.vis_connect('key_release', self.on_canvas_key_release)
def disconnect_canvas_event_handlers(self):
self.canvas.mpl_disconnect(self.cid_canvas_click)
self.canvas.mpl_disconnect(self.cid_canvas_move)
self.canvas.mpl_disconnect(self.cid_canvas_key)
self.canvas.mpl_disconnect(self.cid_canvas_key_release)
# self.canvas.mpl_disconnect(self.cid_canvas_click)
# self.canvas.mpl_disconnect(self.cid_canvas_move)
# self.canvas.mpl_disconnect(self.cid_canvas_key)
# self.canvas.mpl_disconnect(self.cid_canvas_key_release)
self.canvas.vis_disconnect('mouse_release', self.on_canvas_click)
self.canvas.vis_disconnect('mouse_move', self.on_canvas_move)
self.canvas.vis_disconnect('key_press', self.on_canvas_key)
self.canvas.vis_disconnect('key_release', self.on_canvas_key_release)
def add_shape(self, shape):
"""
@@ -800,6 +822,16 @@ class FlatCAMDraw(QtCore.QObject):
self.drawing_toolbar.setDisabled(True)
self.snap_toolbar.setDisabled(True) # TODO: Combine and move into tool
# Hide vispy visuals
if self.shapes.parent is not None:
self.shapes.parent = None
self.tool_shape.parent = None
self.cursor.parent = None
# Show original geometry
if self.fcgeometry:
self.fcgeometry.visible = True
def delete_utility_geometry(self):
#for_deletion = [shape for shape in self.shape_buffer if shape.utility]
#for_deletion = [shape for shape in self.storage.get_objects() if shape.utility]
@@ -807,6 +839,9 @@ class FlatCAMDraw(QtCore.QObject):
for shape in for_deletion:
self.delete_shape(shape)
self.tool_shape.clear(update=True)
self.tool_shape.redraw()
def cutpath(self):
selected = self.get_selected()
tools = selected[1:]
@@ -847,6 +882,11 @@ class FlatCAMDraw(QtCore.QObject):
"Expected a Geometry, got %s" % type(fcgeometry)
self.deactivate()
self.activate()
# Hide original geometry
self.fcgeometry = fcgeometry
fcgeometry.visible = False
self.connect_canvas_event_handlers()
self.select_tool("select")
@@ -896,10 +936,14 @@ class FlatCAMDraw(QtCore.QObject):
:param event: Event object dispatched by Matplotlib
:return: None
"""
pos = self.canvas.vispy_canvas.translate_coords(event.pos)
# Selection with left mouse button
if self.active_tool is not None and event.button is 1:
# Dispatch event to active_tool
msg = self.active_tool.click(self.snap(event.xdata, event.ydata))
# msg = self.active_tool.click(self.snap(event.xdata, event.ydata))
msg = self.active_tool.click(self.snap(pos[0], pos[1]))
self.app.info(msg)
# If it is a shape generating tool
@@ -915,44 +959,24 @@ class FlatCAMDraw(QtCore.QObject):
def on_canvas_move(self, event):
"""
event.x and .y have canvas coordinates
event.xdaya and .ydata have plot coordinates
Called on 'mouse_move' event
:param event: Event object dispatched by Matplotlib
:return:
"""
self.on_canvas_move_effective(event)
return None
event.pos have canvas screen coordinates
# self.move_timer.stop()
#
# if self.active_tool is None:
# return
#
# # Make a function to avoid late evaluation
# def make_callback():
# def f():
# self.on_canvas_move_effective(event)
# return f
# callback = make_callback()
#
# self.move_timer.timeout.connect(callback)
# self.move_timer.start(500) # Stops if aready running
def on_canvas_move_effective(self, event):
"""
Is called after timeout on timer set in on_canvas_move.
For details on animating on MPL see:
http://wiki.scipy.org/Cookbook/Matplotlib/Animations
event.x and .y have canvas coordinates
event.xdaya and .ydata have plot coordinates
:param event: Event object dispatched by Matplotlib
:param event: Event object dispatched by VisPy SceneCavas
:return: None
"""
pos = self.canvas.vispy_canvas.translate_coords(event.pos)
event.xdata, event.ydata = pos[0], pos[1]
self.x = event.xdata
self.y = event.ydata
# Prevent updates on pan
if len(event.buttons) > 0:
return
try:
x = float(event.xdata)
y = float(event.ydata)
@@ -966,34 +990,22 @@ class FlatCAMDraw(QtCore.QObject):
x, y = self.snap(x, y)
### Utility geometry (animated)
self.canvas.canvas.restore_region(self.canvas.background)
geo = self.active_tool.utility_geometry(data=(x, y))
if isinstance(geo, DrawToolShape) and geo.geo is not None:
# Remove any previous utility shape
self.delete_utility_geometry()
self.tool_shape.clear(update=True)
# Add the new utility shape
self.add_shape(geo)
try:
for el in list(geo.geo):
self.tool_shape.add(el, color='#FF000080', update=False)
except TypeError:
self.tool_shape.add(geo.geo, color='#FF000080', update=False)
self.tool_shape.redraw()
# Efficient plotting for fast animation
#self.canvas.canvas.restore_region(self.canvas.background)
elements = self.plot_shape(geometry=geo.geo,
linespec="b--",
linewidth=1,
animated=True)
for el in elements:
self.axes.draw_artist(el)
#self.canvas.canvas.blit(self.axes.bbox)
# Pointer (snapped)
elements = self.axes.plot(x, y, 'bo', animated=True)
for el in elements:
self.axes.draw_artist(el)
self.canvas.canvas.blit(self.axes.bbox)
# Update cursor
self.cursor.set_data(np.asarray([(x, y)]), symbol='s', edge_color='red', size=5)
def on_canvas_key(self, event):
"""
@@ -1002,13 +1014,13 @@ class FlatCAMDraw(QtCore.QObject):
:param event:
:return:
"""
self.key = event.key
self.key = event.key.name
### Finish the current action. Use with tools that do not
### complete automatically, like a polygon or path.
if event.key == ' ':
if event.key.name == 'Space':
if isinstance(self.active_tool, FCShapeTool):
self.active_tool.click(self.snap(event.xdata, event.ydata))
self.active_tool.click(self.snap(self.x, self.y))
self.active_tool.make()
if self.active_tool.complete:
self.on_shape_complete()
@@ -1016,7 +1028,7 @@ class FlatCAMDraw(QtCore.QObject):
return
### Abort the current action
if event.key == 'escape':
if event.key.name == 'Escape':
# TODO: ...?
#self.on_tool_select("select")
self.app.info("Cancelled.")
@@ -1030,32 +1042,32 @@ class FlatCAMDraw(QtCore.QObject):
return
### Delete selected object
if event.key == '-':
if event.key.name == '-':
self.delete_selected()
self.replot()
### Move
if event.key == 'm':
if event.key.name == 'M':
self.move_btn.setChecked(True)
self.on_tool_select('move')
self.active_tool.set_origin(self.snap(event.xdata, event.ydata))
self.active_tool.set_origin(self.snap(self.x, self.y))
self.app.info("Click on target point.")
### Copy
if event.key == 'c':
if event.key.name == 'C':
self.copy_btn.setChecked(True)
self.on_tool_select('copy')
self.active_tool.set_origin(self.snap(event.xdata, event.ydata))
self.active_tool.set_origin(self.snap(self.x, self.y))
self.app.info("Click on target point.")
### Snap
if event.key == 'g':
if event.key.name == 'G':
self.grid_snap_btn.trigger()
if event.key == 'k':
if event.key.name == 'K':
self.corner_snap_btn.trigger()
### Buffer
if event.key == 'b':
if event.key.name == 'B':
self.on_buffer_tool()
### Propagate to tool
@@ -1088,7 +1100,7 @@ class FlatCAMDraw(QtCore.QObject):
self.selected = []
def plot_shape(self, geometry=None, linespec='b-', linewidth=1, animated=False):
def plot_shape(self, geometry=None, color='black',linewidth=1):
"""
Plots a geometric object or list of objects without rendering. Plotted objects
are returned as a list. This allows for efficient/animated rendering.
@@ -1096,7 +1108,6 @@ class FlatCAMDraw(QtCore.QObject):
:param geometry: Geometry to be plotted (Any Shapely.geom kind or list of such)
:param linespec: Matplotlib linespec string.
:param linewidth: Width of lines in # of pixels.
:param animated: If geometry is to be animated. (See MPL plot())
:return: List of plotted elements.
"""
plot_elements = []
@@ -1106,70 +1117,54 @@ class FlatCAMDraw(QtCore.QObject):
try:
for geo in geometry:
plot_elements += self.plot_shape(geometry=geo,
linespec=linespec,
linewidth=linewidth,
animated=animated)
plot_elements += self.plot_shape(geometry=geo, color=color, linewidth=linewidth)
## Non-iterable
except TypeError:
## DrawToolShape
if isinstance(geometry, DrawToolShape):
plot_elements += self.plot_shape(geometry=geometry.geo,
linespec=linespec,
linewidth=linewidth,
animated=animated)
plot_elements += self.plot_shape(geometry=geometry.geo, color=color, linewidth=linewidth)
## Polygon: Dscend into exterior and each interior.
## Polygon: Descend into exterior and each interior.
if type(geometry) == Polygon:
plot_elements += self.plot_shape(geometry=geometry.exterior,
linespec=linespec,
linewidth=linewidth,
animated=animated)
plot_elements += self.plot_shape(geometry=geometry.interiors,
linespec=linespec,
linewidth=linewidth,
animated=animated)
plot_elements += self.plot_shape(geometry=geometry.exterior, color=color, linewidth=linewidth)
plot_elements += self.plot_shape(geometry=geometry.interiors, color=color, linewidth=linewidth)
if type(geometry) == LineString or type(geometry) == LinearRing:
x, y = geometry.coords.xy
element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
plot_elements.append(element)
plot_elements.append(self.shapes.add(geometry, color=color))
if type(geometry) == Point:
x, y = geometry.coords.xy
element, = self.axes.plot(x, y, 'bo', linewidth=linewidth, animated=animated)
plot_elements.append(element)
pass
return plot_elements
def plot_all(self):
"""
Plots all shapes in the editor.
Clears the axes, plots, and call self.canvas.auto_adjust_axes.
:return: None
:rtype: None
"""
self.app.log.debug("plot_all()")
self.axes.cla()
self.shapes.clear(update=True)
for shape in self.storage.get_objects():
if shape.geo is None: # TODO: This shouldn't have happened
continue
if shape in self.selected:
self.plot_shape(geometry=shape.geo, linespec='k-', linewidth=2)
self.plot_shape(geometry=shape.geo, color='blue', linewidth=2)
continue
self.plot_shape(geometry=shape.geo)
self.plot_shape(geometry=shape.geo, color='red')
for shape in self.utility:
self.plot_shape(geometry=shape.geo, linespec='k--', linewidth=1)
self.plot_shape(geometry=shape.geo, linewidth=1)
continue
self.canvas.auto_adjust_axes()
self.shapes.redraw()
def on_shape_complete(self):
self.app.log.debug("on_shape_complete()")
@@ -1179,6 +1174,7 @@ class FlatCAMDraw(QtCore.QObject):
# Remove any utility shapes
self.delete_utility_geometry()
self.tool_shape.clear(update=True)
# Replot and reset tool.
self.replot()
@@ -1196,7 +1192,6 @@ class FlatCAMDraw(QtCore.QObject):
self.selected.remove(shape)
def replot(self):
self.axes = self.canvas.new_axes("draw")
self.plot_all()
@staticmethod

View File

@@ -7,6 +7,7 @@ import inspect # TODO: For debugging only.
from camlib import *
from FlatCAMCommon import LoudDict
from FlatCAMDraw import FlatCAMDraw
from VisPyVisuals import ShapeCollection
########################################
@@ -39,9 +40,11 @@ class FlatCAMObj(QtCore.QObject):
self.form_fields = {}
self.axes = None # Matplotlib axes
self.kind = None # Override with proper name
# self.shapes = ShapeCollection(parent=self.app.plotcanvas.vispy_canvas.view.scene)
self.shapes = self.app.plotcanvas.new_shape_group()
self.muted_ui = False
# assert isinstance(self.ui, ObjectUI)
@@ -49,6 +52,9 @@ class FlatCAMObj(QtCore.QObject):
# self.ui.offset_button.clicked.connect(self.on_offset_button_click)
# self.ui.scale_button.clicked.connect(self.on_scale_button_click)
def __del__(self):
pass
def from_dict(self, d):
"""
This supersedes ``from_dict`` in derived classes. Derived classes
@@ -103,38 +109,6 @@ class FlatCAMObj(QtCore.QObject):
self.scale(factor)
self.plot()
def setup_axes(self, figure):
"""
1) Creates axes if they don't exist. 2) Clears axes. 3) Attaches
them to figure if not part of the figure. 4) Sets transparent
background. 5) Sets 1:1 scale aspect ratio.
:param figure: A Matplotlib.Figure on which to add/configure axes.
:type figure: matplotlib.figure.Figure
:return: None
:rtype: None
"""
if self.axes is None:
FlatCAMApp.App.log.debug("setup_axes(): New axes")
self.axes = figure.add_axes([0.05, 0.05, 0.9, 0.9],
label=self.options["name"])
elif self.axes not in figure.axes:
FlatCAMApp.App.log.debug("setup_axes(): Clearing and attaching axes")
self.axes.cla()
figure.add_axes(self.axes)
else:
FlatCAMApp.App.log.debug("setup_axes(): Clearing Axes")
self.axes.cla()
# Remove all decoration. The app's axes will have
# the ticks and grid.
self.axes.set_frame_on(False) # No frame
self.axes.set_xticks([]) # No tick
self.axes.set_yticks([]) # No ticks
self.axes.patch.set_visible(False) # No background
self.axes.set_aspect(1)
def to_form(self):
"""
Copies options to the UI form.
@@ -236,7 +210,6 @@ class FlatCAMObj(QtCore.QObject):
def plot(self):
"""
Plot this object (Extend this method to implement the actual plotting).
Axes get created, appended to canvas and cleared before plotting.
Call this in descendants before doing the plotting.
:return: Whether to continue plotting or not depending on the "plot" option.
@@ -244,17 +217,7 @@ class FlatCAMObj(QtCore.QObject):
"""
FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMObj.plot()")
# Axes must exist and be attached to canvas.
if self.axes is None or self.axes not in self.app.plotcanvas.figure.axes:
self.axes = self.app.plotcanvas.new_axes(self.options['name'])
if not self.options["plot"]:
self.axes.cla()
self.app.plotcanvas.auto_adjust_axes()
return False
# Clear axes or we will plot on top of them.
self.axes.cla() # TODO: Thread safe?
self.shapes.clear()
return True
def serialize(self):
@@ -277,6 +240,18 @@ class FlatCAMObj(QtCore.QObject):
"""
return
@property
def visible(self):
return self.shapes.visible
@visible.setter
def visible(self, value):
self.shapes.visible = value
try:
self.annotation.parent = self.app.plotcanvas.vispy_canvas.view.scene \
if (value and not self.annotation.parent) else None
except:
pass
class FlatCAMGerber(FlatCAMObj, Gerber):
"""
@@ -547,7 +522,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
if self.muted_ui:
return
self.read_form_item('plot')
self.plot()
self.visible = self.options['plot']
# self.plot()
def on_solid_cb_click(self, *args):
if self.muted_ui:
@@ -597,33 +573,20 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
except TypeError:
geometry = [geometry]
if self.options["multicolored"]:
linespec = '-'
else:
linespec = 'k-'
def random_color():
color = np.random.rand(4)
color[3] = 1
return color
if self.options["solid"]:
for poly in geometry:
# TODO: Too many things hardcoded.
try:
patch = PolygonPatch(poly,
facecolor="#BBF268",
edgecolor="#006E20",
alpha=0.75,
zorder=2)
self.axes.add_patch(patch)
except AssertionError:
FlatCAMApp.App.log.warning("A geometry component was not a polygon:")
FlatCAMApp.App.log.warning(str(poly))
self.shapes.add(poly, color='#006E20BF', face_color=random_color() if self.options['multicolored'] else
'#BBF268BF', visible=self.options['plot'])
else:
for poly in geometry:
x, y = poly.exterior.xy
self.axes.plot(x, y, linespec)
for ints in poly.interiors:
x, y = ints.coords.xy
self.axes.plot(x, y, linespec)
self.app.plotcanvas.auto_adjust_axes()
self.shapes.add(poly, color=random_color() if self.options['multicolored'] else 'black',
visible=self.options['plot'])
self.shapes.redraw()
def serialize(self):
return {
@@ -927,7 +890,8 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
if self.muted_ui:
return
self.read_form_item('plot')
self.plot()
self.visible = self.options['plot']
# self.plot()
def on_solid_cb_click(self, *args):
if self.muted_ui:
@@ -957,22 +921,14 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
# Plot excellon (All polygons?)
if self.options["solid"]:
for geo in self.solid_geometry:
patch = PolygonPatch(geo,
facecolor="#C40000",
edgecolor="#750000",
alpha=0.75,
zorder=3)
self.axes.add_patch(patch)
self.shapes.add(geo, color='#750000BF', face_color='#C40000BF', visible=self.options['plot'])
else:
for geo in self.solid_geometry:
x, y = geo.exterior.coords.xy
self.axes.plot(x, y, 'r-')
self.shapes.add(geo.exterior, color='red', visible=self.options['plot'])
for ints in geo.interiors:
x, y = ints.coords.xy
self.axes.plot(x, y, 'g-')
self.app.plotcanvas.auto_adjust_axes()
self.shapes.add(ints, color='green', visible=self.options['plot'])
self.shapes.redraw()
class FlatCAMCNCjob(FlatCAMObj, CNCjob):
"""
@@ -1009,6 +965,8 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
# from predecessors.
self.ser_attrs += ['options', 'kind']
# self.annotation = self.app.plotcanvas.new_annotation()
def set_ui(self, ui):
FlatCAMObj.set_ui(self, ui)
@@ -1121,7 +1079,8 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
if self.muted_ui:
return
self.read_form_item('plot')
self.plot()
self.visible = self.options['plot']
# self.plot()
def plot(self):
@@ -1130,9 +1089,9 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
if not FlatCAMObj.plot(self):
return
self.plot2(self.axes, tooldia=self.options["tooldia"])
self.plot2(tooldia=self.options["tooldia"], obj=self, visible=self.options['plot'])
self.app.plotcanvas.auto_adjust_axes()
self.shapes.redraw()
def convert_units(self, units):
factor = CNCjob.convert_units(self, units)
@@ -1402,7 +1361,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
if self.muted_ui:
return
self.read_form_item('plot')
self.plot()
self.visible = self.options['plot']
# self.plot()
def scale(self, factor):
"""
@@ -1462,26 +1422,11 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
self.plot_element(sub_el)
except TypeError: # Element is not iterable...
if type(element) == Polygon:
x, y = element.exterior.coords.xy
self.axes.plot(x, y, 'r-')
for ints in element.interiors:
x, y = ints.coords.xy
self.axes.plot(x, y, 'r-')
return
if type(element) == LineString or type(element) == LinearRing:
x, y = element.coords.xy
self.axes.plot(x, y, 'r-')
return
FlatCAMApp.App.log.warning("Did not plot:" + str(type(element)))
self.shapes.add(element, color='red', visible=self.options['plot'], layer=0)
def plot(self):
"""
Plots the object into its axes. If None, of if the axes
are not part of the app's figure, it fetches new ones.
Adds the object into collection.
:return: None
"""
@@ -1491,42 +1436,5 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
if not FlatCAMObj.plot(self):
return
# Make sure solid_geometry is iterable.
# TODO: This method should not modify the object !!!
# try:
# _ = iter(self.solid_geometry)
# except TypeError:
# if self.solid_geometry is None:
# self.solid_geometry = []
# else:
# self.solid_geometry = [self.solid_geometry]
#
# for geo in self.solid_geometry:
#
# if type(geo) == Polygon:
# x, y = geo.exterior.coords.xy
# self.axes.plot(x, y, 'r-')
# for ints in geo.interiors:
# x, y = ints.coords.xy
# self.axes.plot(x, y, 'r-')
# continue
#
# if type(geo) == LineString or type(geo) == LinearRing:
# x, y = geo.coords.xy
# self.axes.plot(x, y, 'r-')
# continue
#
# if type(geo) == MultiPolygon:
# for poly in geo:
# x, y = poly.exterior.coords.xy
# self.axes.plot(x, y, 'r-')
# for ints in poly.interiors:
# x, y = ints.coords.xy
# self.axes.plot(x, y, 'r-')
# continue
#
# FlatCAMApp.App.log.warning("Did not plot:", str(type(geo)))
self.plot_element(self.solid_geometry)
self.app.plotcanvas.auto_adjust_axes()
self.shapes.redraw()

View File

@@ -32,7 +32,8 @@ class Measurement(FlatCAMTool):
def install(self):
FlatCAMTool.install(self)
self.app.ui.right_layout.addWidget(self)
self.app.plotcanvas.mpl_connect('key_press_event', self.on_key_press)
# TODO: Translate to vis
# self.app.plotcanvas.mpl_connect('key_press_event', self.on_key_press)
def run(self):
self.toggle()

View File

@@ -214,6 +214,7 @@ class ObjectCollection(QtCore.QAbstractListModel):
self.beginRemoveRows(QtCore.QModelIndex(), row, row)
self.object_list[row].shapes.clear(True)
self.object_list.pop(row)
self.endRemoveRows()
@@ -295,6 +296,9 @@ class ObjectCollection(QtCore.QAbstractListModel):
self.beginResetModel()
for obj in self.object_list:
obj.shapes.clear(obj == self.object_list[-1])
self.object_list = []
self.checked_indexes = []

View File

@@ -6,108 +6,23 @@
# MIT Licence #
############################################################
from PyQt4 import QtGui, QtCore
from PyQt4 import QtCore
# Prevent conflict with Qt5 and above.
from matplotlib import use as mpl_use
mpl_use("Qt4Agg")
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_agg import FigureCanvasAgg
import FlatCAMApp
import logging
from VisPyCanvas import VisPyCanvas
from VisPyVisuals import ShapeGroup, ShapeCollection
from vispy.scene.visuals import Markers, Text
import numpy as np
from vispy.geometry import Rect
log = logging.getLogger('base')
class CanvasCache(QtCore.QObject):
"""
Case story #1:
1) No objects in the project.
2) Object is created (new_object() emits object_created(obj)).
on_object_created() adds (i) object to collection and emits
(ii) new_object_available() then calls (iii) object.plot()
3) object.plot() creates axes if necessary on
app.collection.figure. Then plots on it.
4) Plots on a cache-size canvas (in background).
5) Plot completes. Bitmap is generated.
6) Visible canvas is painted.
"""
# Signals:
# A bitmap is ready to be displayed.
new_screen = QtCore.pyqtSignal()
def __init__(self, plotcanvas, app, dpi=50):
super(CanvasCache, self).__init__()
self.app = app
self.plotcanvas = plotcanvas
self.dpi = dpi
self.figure = Figure(dpi=dpi)
self.axes = self.figure.add_axes([0.0, 0.0, 1.0, 1.0], alpha=1.0)
self.axes.set_frame_on(False)
self.axes.set_xticks([])
self.axes.set_yticks([])
self.canvas = FigureCanvasAgg(self.figure)
self.cache = None
def run(self):
log.debug("CanvasCache Thread Started!")
self.plotcanvas.update_screen_request.connect(self.on_update_req)
self.app.new_object_available.connect(self.on_new_object_available)
def on_update_req(self, extents):
"""
Event handler for an updated display request.
:param extents: [xmin, xmax, ymin, ymax, zoom(optional)]
"""
log.debug("Canvas update requested: %s" % str(extents))
# Note: This information below might be out of date. Establish
# a protocol regarding when to change the canvas in the main
# thread and when to check these values here in the background,
# or pass this data in the signal (safer).
log.debug("Size: %s [px]" % str(self.plotcanvas.get_axes_pixelsize()))
log.debug("Density: %s [units/px]" % str(self.plotcanvas.get_density()))
# Move the requested screen portion to the main thread
# and inform about the update:
self.new_screen.emit()
# Continue to update the cache.
def on_new_object_available(self):
log.debug("A new object is available. Should plot it!")
class PlotCanvas(QtCore.QObject):
"""
Class handling the plotting area in the application.
"""
# Signals:
# Request for new bitmap to display. The parameter
# is a list with [xmin, xmax, ymin, ymax, zoom(optional)]
update_screen_request = QtCore.pyqtSignal(list)
def __init__(self, container, app):
"""
The constructor configures the Matplotlib figure that
@@ -129,208 +44,20 @@ class PlotCanvas(QtCore.QObject):
# Parent container
self.container = container
# Plots go onto a single matplotlib.figure
self.figure = Figure(dpi=50) # TODO: dpi needed?
self.figure.patch.set_visible(False)
# These axes show the ticks and grid. No plotting done here.
# New axes must have a label, otherwise mpl returns an existing one.
self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
self.axes.set_aspect(1)
self.axes.grid(True)
# The canvas is the top level container (FigureCanvasQTAgg)
self.canvas = FigureCanvas(self.figure)
# self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus)
# self.canvas.setFocus()
#self.canvas.set_hexpand(1)
#self.canvas.set_vexpand(1)
#self.canvas.set_can_focus(True) # For key press
# Attach to parent
#self.container.attach(self.canvas, 0, 0, 600, 400) # TODO: Height and width are num. columns??
self.container.addWidget(self.canvas) # Qt
self.vispy_canvas = VisPyCanvas()
self.vispy_canvas.create_native()
self.vispy_canvas.native.setParent(self.app.ui)
self.container.addWidget(self.vispy_canvas.native)
# Copy a bitmap of the canvas for quick animation.
# Update every time the canvas is re-drawn.
self.background = self.canvas.copy_from_bbox(self.axes.bbox)
self.shape_collection = self.new_shape_collection()
self.shape_collection.parent = self.vispy_canvas.view.scene
### Bitmap Cache
self.cache = CanvasCache(self, self.app)
self.cache_thread = QtCore.QThread()
self.cache.moveToThread(self.cache_thread)
super(PlotCanvas, self).connect(self.cache_thread, QtCore.SIGNAL("started()"), self.cache.run)
# self.connect()
self.cache_thread.start()
self.cache.new_screen.connect(self.on_new_screen)
def vis_connect(self, event_name, callback):
return getattr(self.vispy_canvas.events, event_name).connect(callback)
# Events
self.canvas.mpl_connect('button_press_event', self.on_mouse_press)
self.canvas.mpl_connect('button_release_event', self.on_mouse_release)
self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
#self.canvas.connect('configure-event', self.auto_adjust_axes)
self.canvas.mpl_connect('resize_event', self.auto_adjust_axes)
#self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
#self.canvas.connect("scroll-event", self.on_scroll)
self.canvas.mpl_connect('scroll_event', self.on_scroll)
self.canvas.mpl_connect('key_press_event', self.on_key_down)
self.canvas.mpl_connect('key_release_event', self.on_key_up)
self.canvas.mpl_connect('draw_event', self.on_draw)
self.mouse = [0, 0]
self.key = None
self.pan_axes = []
self.panning = False
def on_new_screen(self):
log.debug("Cache updated the screen!")
def on_key_down(self, event):
"""
:param event:
:return:
"""
FlatCAMApp.App.log.debug('on_key_down(): ' + str(event.key))
self.key = event.key
def on_key_up(self, event):
"""
:param event:
:return:
"""
self.key = None
def mpl_connect(self, event_name, callback):
"""
Attach an event handler to the canvas through the Matplotlib interface.
:param event_name: Name of the event
:type event_name: str
:param callback: Function to call
:type callback: func
:return: Connection id
:rtype: int
"""
return self.canvas.mpl_connect(event_name, callback)
def mpl_disconnect(self, cid):
"""
Disconnect callback with the give id.
:param cid: Callback id.
:return: None
"""
self.canvas.mpl_disconnect(cid)
def connect(self, event_name, callback):
"""
Attach an event handler to the canvas through the native Qt interface.
:param event_name: Name of the event
:type event_name: str
:param callback: Function to call
:type callback: function
:return: Nothing
"""
self.canvas.connect(event_name, callback)
def clear(self):
"""
Clears axes and figure.
:return: None
"""
# Clear
self.axes.cla()
try:
self.figure.clf()
except KeyError:
FlatCAMApp.App.log.warning("KeyError in MPL figure.clf()")
# Re-build
self.figure.add_axes(self.axes)
self.axes.set_aspect(1)
self.axes.grid(True)
# Re-draw
self.canvas.draw_idle()
def adjust_axes(self, xmin, ymin, xmax, ymax):
"""
Adjusts all axes while maintaining the use of the whole canvas
and an aspect ratio to 1:1 between x and y axes. The parameters are an original
request that will be modified to fit these restrictions.
:param xmin: Requested minimum value for the X axis.
:type xmin: float
:param ymin: Requested minimum value for the Y axis.
:type ymin: float
:param xmax: Requested maximum value for the X axis.
:type xmax: float
:param ymax: Requested maximum value for the Y axis.
:type ymax: float
:return: None
"""
# FlatCAMApp.App.log.debug("PC.adjust_axes()")
width = xmax - xmin
height = ymax - ymin
try:
r = width / height
except ZeroDivisionError:
FlatCAMApp.App.log.error("Height is %f" % height)
return
canvas_w, canvas_h = self.canvas.get_width_height()
canvas_r = float(canvas_w) / canvas_h
x_ratio = float(self.x_margin) / canvas_w
y_ratio = float(self.y_margin) / canvas_h
if r > canvas_r:
ycenter = (ymin + ymax) / 2.0
newheight = height * r / canvas_r
ymin = ycenter - newheight / 2.0
ymax = ycenter + newheight / 2.0
else:
xcenter = (xmax + xmin) / 2.0
newwidth = width * canvas_r / r
xmin = xcenter - newwidth / 2.0
xmax = xcenter + newwidth / 2.0
# Adjust axes
for ax in self.figure.get_axes():
if ax._label != 'base':
ax.set_frame_on(False) # No frame
ax.set_xticks([]) # No tick
ax.set_yticks([]) # No ticks
ax.patch.set_visible(False) # No background
ax.set_aspect(1)
ax.set_xlim((xmin, xmax))
ax.set_ylim((ymin, ymax))
ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio])
# Sync re-draw to proper paint on form resize
self.canvas.draw()
##### Temporary place-holder for cached update #####
self.update_screen_request.emit([0, 0, 0, 0, 0])
def auto_adjust_axes(self, *args):
"""
Calls ``adjust_axes()`` using the extents of the base axes.
:rtype : None
:return: None
"""
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
self.adjust_axes(xmin, ymin, xmax, ymax)
def vis_disconnect(self, event_name, callback):
getattr(self.vispy_canvas.events, event_name).disconnect(callback)
def zoom(self, factor, center=None):
"""
@@ -344,181 +71,30 @@ class PlotCanvas(QtCore.QObject):
:return: None
"""
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
width = xmax - xmin
height = ymax - ymin
self.vispy_canvas.view.camera.zoom(factor, center)
if center is None or center == [None, None]:
center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
def new_shape_group(self):
return ShapeGroup(self.shape_collection) # TODO: Make local shape collection
# For keeping the point at the pointer location
relx = (xmax - center[0]) / width
rely = (ymax - center[1]) / height
def new_shape_collection(self):
return ShapeCollection()
new_width = width / factor
new_height = height / factor
def new_cursor(self):
return Markers(pos=np.empty((0, 2)))
xmin = center[0] - new_width * (1 - relx)
xmax = center[0] + new_width * relx
ymin = center[1] - new_height * (1 - rely)
ymax = center[1] + new_height * rely
def new_annotation(self):
return Text(parent=self.vispy_canvas.view.scene)
# Adjust axes
for ax in self.figure.get_axes():
ax.set_xlim((xmin, xmax))
ax.set_ylim((ymin, ymax))
def fit_view(self, rect=None):
if not rect:
rect = Rect(0, 0, 10, 10)
try:
rect.left, rect.right = self.shape_collection.bounds(axis=0)
rect.bottom, rect.top = self.shape_collection.bounds(axis=1)
except TypeError:
pass
# Async re-draw
self.canvas.draw_idle()
self.vispy_canvas.view.camera.rect = rect
##### Temporary place-holder for cached update #####
self.update_screen_request.emit([0, 0, 0, 0, 0])
def pan(self, x, y):
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
width = xmax - xmin
height = ymax - ymin
# Adjust axes
for ax in self.figure.get_axes():
ax.set_xlim((xmin + x * width, xmax + x * width))
ax.set_ylim((ymin + y * height, ymax + y * height))
# Re-draw
self.canvas.draw_idle()
##### Temporary place-holder for cached update #####
self.update_screen_request.emit([0, 0, 0, 0, 0])
def new_axes(self, name):
"""
Creates and returns an Axes object attached to this object's Figure.
:param name: Unique label for the axes.
:return: Axes attached to the figure.
:rtype: Axes
"""
return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name)
def on_scroll(self, event):
"""
Scroll event handler.
:param event: Event object containing the event information.
:return: None
"""
# So it can receive key presses
# self.canvas.grab_focus()
self.canvas.setFocus()
# Event info
# z, direction = event.get_scroll_direction()
if self.key is None:
if event.button == 'up':
self.zoom(1.5, self.mouse)
else:
self.zoom(1 / 1.5, self.mouse)
return
if self.key == 'shift':
if event.button == 'up':
self.pan(0.3, 0)
else:
self.pan(-0.3, 0)
return
if self.key == 'control':
if event.button == 'up':
self.pan(0, 0.3)
else:
self.pan(0, -0.3)
return
def on_mouse_press(self, event):
# Check for middle mouse button press
if event.button == 2:
# Prepare axes for pan (using 'matplotlib' pan function)
self.pan_axes = []
for a in self.figure.get_axes():
if (event.x is not None and event.y is not None and a.in_axes(event) and
a.get_navigate() and a.can_pan()):
a.start_pan(event.x, event.y, 1)
self.pan_axes.append(a)
# Set pan view flag
if len(self.pan_axes) > 0: self.panning = True;
def on_mouse_release(self, event):
# Check for middle mouse button release to complete pan procedure
if event.button == 2:
for a in self.pan_axes:
a.end_pan()
# Clear pan flag
self.panning = False
def on_mouse_move(self, event):
"""
Mouse movement event hadler. Stores the coordinates. Updates view on pan.
:param event: Contains information about the event.
:return: None
"""
self.mouse = [event.xdata, event.ydata]
# Update pan view on mouse move
if self.panning is True:
for a in self.pan_axes:
a.drag_pan(1, event.key, event.x, event.y)
# Async re-draw (redraws only on thread idle state, uses timer on backend)
self.canvas.draw_idle()
##### Temporary place-holder for cached update #####
self.update_screen_request.emit([0, 0, 0, 0, 0])
def on_draw(self, renderer):
# Store background on canvas redraw
self.background = self.canvas.copy_from_bbox(self.axes.bbox)
def get_axes_pixelsize(self):
"""
Axes size in pixels.
:return: Pixel width and height
:rtype: tuple
"""
bbox = self.axes.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
width, height = bbox.width, bbox.height
width *= self.figure.dpi
height *= self.figure.dpi
return width, height
def get_density(self):
"""
Returns unit length per pixel on horizontal
and vertical axes.
:return: X and Y density
:rtype: tuple
"""
xpx, ypx = self.get_axes_pixelsize()
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
width = xmax - xmin
height = ymax - ymin
return width / xpx, height / ypx
def clear(self):
pass

133
VisPyCanvas.py Normal file
View File

@@ -0,0 +1,133 @@
import numpy as np
from PyQt4.QtGui import QPalette
import vispy.scene as scene
from vispy.scene.widgets import Grid
from vispy.scene.cameras.base_camera import BaseCamera
# Patch VisPy Grid to prevent updating layout on PaintGL
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
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
grid = self.central_widget.add_grid(margin=10)
grid.spacing = 0
top_padding = grid.add_widget(row=0, col=0, col_span=2)
top_padding.height_max = 24
yaxis = scene.AxisWidget(orientation='left', axis_color='black', text_color='black', font_size=12)
yaxis.width_max = 50
grid.add_widget(yaxis, row=1, col=0)
xaxis = scene.AxisWidget(orientation='bottom', axis_color='black', text_color='black', font_size=12)
xaxis.height_max = 40
grid.add_widget(xaxis, row=2, col=1)
right_padding = grid.add_widget(row=0, col=2, row_span=2)
right_padding.width_max = 24
view = grid.add_view(row=1, col=1, border_color='black', bgcolor='white')
view.camera = Camera(aspect=1)
grid1 = scene.GridLines(parent=view.scene, color='gray')
grid1.set_gl_state(depth_test=False)
xaxis.link_view(view)
yaxis.link_view(view)
# shapes = scene.Line(parent=view.scene)
# view.add(shapes)
self.grid = grid1
self.view = view
self.freeze()
self.measure_fps()
def translate_coords(self, pos):
tr = self.grid.get_transform('canvas', 'visual')
return tr.map(pos)
class Camera(scene.PanZoomCamera):
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
# Scrolling
BaseCamera.viewbox_mouse_event(self, event)
if event.type == 'mouse_wheel':
center = self._scene_transform.imap(event.pos)
self.zoom((1 + self.zoom_factor) ** (-event.delta[1] * 30), center)
event.handled = True
elif event.type == 'mouse_move':
if event.press_event is None:
return
modifiers = event.mouse_event.modifiers
p1 = event.mouse_event.press_event.pos
p2 = event.mouse_event.pos
if event.button in [2, 3] and not modifiers:
# Translate
p1 = np.array(event.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(event.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.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

262
VisPyVisuals.py Normal file
View File

@@ -0,0 +1,262 @@
from vispy.visuals import CompoundVisual, LineVisual, MeshVisual
from vispy.scene.visuals import create_visual_node
from vispy.gloo import set_state
from vispy.geometry.triangulation import Triangulation
from vispy.color import Color
from shapely.geometry import Polygon, LineString, LinearRing
import numpy as np
try:
from shapely.ops import triangulate
import Polygon as gpc
except:
pass
# Add clear_data method to LineVisual
def clear_data(self):
self._bounds = None
self._pos = None
self._changed['pos'] = True
self.update()
LineVisual.clear_data = clear_data
class ShapeGroup(object):
def __init__(self, collection):
self._collection = collection
self._indexes = []
self._visible = True
def add(self, shape, color=None, face_color=None, visible=True, update=False, layer=1, order=0):
self._indexes.append(self._collection.add(shape, color, face_color, visible, update, layer, order))
def clear(self, update=False):
for i in self._indexes:
self._collection.remove(i, False)
del self._indexes[:]
if update:
self._collection.redraw()
def redraw(self):
self._collection.redraw()
@property
def visible(self):
return self._visible
@visible.setter
def visible(self, value):
self._visible = value
for i in self._indexes:
self._collection.data[i]['visible'] = value
self._collection.redraw()
class ShapeCollectionVisual(CompoundVisual):
total_segments = 0
total_tris = 0
def __init__(self, line_width=1, triangulation='gpc', layers=3, **kwargs):
self.data = {}
self.last_key = -1
self._meshes = [MeshVisual() for _ in range(0, layers)]
self._lines = [LineVisual(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:
m.set_gl_state(polygon_offset_fill=True, polygon_offset=(1, 1), cull_face=False)
for l in self._lines:
l.set_gl_state(blend=True)
self.freeze()
def add(self, shape, color=None, face_color=None, visible=True, update=False, layer=1, order=0):
self.last_key += 1
self.data[self.last_key] = {'geometry': shape, 'color': color, 'face_color': face_color,
'visible': visible, 'layer': layer, 'order': order}
self.update_shape_buffers(self.last_key)
if update:
self._update()
return self.last_key
def update_shape_buffers(self, key):
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 = self.data[key]['geometry'], self.data[key]['color'], self.data[key]['face_color']
if geo is not None and not geo.is_empty:
simple = geo.simplify(0.01) # Simplified shape
pts = [] # Shape line points
tri_pts = [] # Mesh vertices
tri_tris = [] # Mesh faces
if type(geo) == LineString:
# Prepare lines
pts = self._linestring_to_segments(np.asarray(simple)).tolist()
elif type(geo) == LinearRing:
# Prepare lines
pts = self._linearring_to_segments(np.asarray(simple)).tolist()
elif type(geo) == Polygon:
# Prepare polygon faces
if face_color is not None:
if self._triangulation == 'vispy':
# VisPy triangulation
# Concatenated arrays of external & internal line rings
vertices = self._open_ring(np.asarray(simple.exterior))
edges = self._generate_edges(len(vertices))
for ints in simple.interiors:
v = self._open_ring(np.asarray(ints))
edges = np.append(edges, self._generate_edges(len(v)) + len(vertices), 0)
vertices = np.append(vertices, v, 0)
tri = Triangulation(vertices, edges)
tri.triangulate()
tri_pts, tri_tris = tri.pts.tolist(), tri.tris.tolist()
elif self._triangulation == 'gpc':
# GPC triangulation
p = gpc.Polygon(np.asarray(simple.exterior))
for ints in simple.interiors:
q = gpc.Polygon(np.asarray(ints))
p -= q
for strip in p.triStrip():
# Generate tris indexes for triangle strips
a = [[x + y for x in range(0, 3)] for y in range(0, len(strip) - 2)]
# Append vertices & tris
tri_tris += [[x + len(tri_pts) for x in y] for y in a]
tri_pts += strip
# Prepare polygon edges
if color is not None:
pts = self._linearring_to_segments(np.asarray(simple.exterior)).tolist()
for ints in simple.interiors:
pts += self._linearring_to_segments(np.asarray(ints)).tolist()
# 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)
# Appending data for line
if len(pts) > 0:
line_pts += pts
line_colors += [Color(color).rgba] * len(pts)
# Store buffers
self.data[key]['line_pts'] = line_pts
self.data[key]['line_colors'] = line_colors
self.data[key]['mesh_vertices'] = mesh_vertices
self.data[key]['mesh_tris'] = mesh_tris
self.data[key]['mesh_colors'] = mesh_colors
def remove(self, key, update=False):
self.data.pop(key)
if update:
self._update()
def clear(self, update=False):
self.data.clear()
if update:
self._update()
def _update(self):
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
# Merge shapes buffers
for data in self.data.values():
if data['visible']:
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 y] for y 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),
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()
def redraw(self):
self._update()
@staticmethod
def _open_ring(vertices):
return vertices[:-1] if not np.any(vertices[0] != vertices[-1]) else vertices
@staticmethod
def _generate_edges(count):
edges = np.empty((count, 2), dtype=np.uint32)
edges[:, 0] = np.arange(count)
edges[:, 1] = edges[:, 0] + 1
edges[-1, 1] = 0
return edges
@staticmethod
def _linearring_to_segments(arr):
# Close linear ring
if np.any(arr[0] != arr[-1]):
arr = np.concatenate([arr, arr[:1]], axis=0)
return ShapeCollection._linestring_to_segments(arr)
@staticmethod
def _linestring_to_segments(arr):
return np.asarray(np.repeat(arr, 2, axis=0)[1:-1])
ShapeCollection = create_visual_node(ShapeCollectionVisual)

View File

@@ -13,7 +13,6 @@ from cStringIO import StringIO
from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos, dot, float32, \
transpose
from numpy.linalg import solve, norm
from matplotlib.figure import Figure
import re
import sys
import traceback
@@ -21,8 +20,6 @@ from decimal import Decimal
import collections
import numpy as np
import matplotlib
#import matplotlib.pyplot as plt
#from scipy.spatial import Delaunay, KDTree
from rtree import index as rtindex
@@ -42,14 +39,13 @@ from descartes.patch import PolygonPatch
import simplejson as json
# TODO: Commented for FlatCAM packaging with cx_freeze
#from matplotlib.pyplot import plot, subplot
import xml.etree.ElementTree as ET
from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path
# from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path
import itertools
import xml.etree.ElementTree as ET
from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path
# from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path
from svgparse import *
@@ -3243,13 +3239,12 @@ class CNCjob(Geometry):
#
# return fig
def plot2(self, axes, tooldia=None, dpi=75, margin=0.1,
color={"T": ["#F0E24D", "#B5AB3A"], "C": ["#5E6CFF", "#4650BD"]},
alpha={"T": 0.3, "C": 1.0}, tool_tolerance=0.0005):
def plot2(self, tooldia=None, dpi=75, margin=0.1,
color={"T": ["#F0E24D4C", "#B5AB3A4C"], "C": ["#5E6CFFFF", "#4650BDFF"]},
alpha={"T": 0.3, "C": 1.0}, tool_tolerance=0.0005, obj=None, visible=False):
"""
Plots the G-code job onto the given axes.
:param axes: Matplotlib axes on which to plot.
:param tooldia: Tool diameter.
:param dpi: Not used!
:param margin: Not used!
@@ -3265,24 +3260,22 @@ class CNCjob(Geometry):
if tooldia == 0:
for geo in self.gcode_parsed:
linespec = '--'
linecolor = color[geo['kind'][0]][1]
if geo['kind'][0] == 'C':
linespec = 'k-'
x, y = geo['geom'].coords.xy
axes.plot(x, y, linespec, color=linecolor)
obj.shapes.add(geo['geom'], color=color[geo['kind'][0]][1], visible=visible)
else:
text = []
pos = []
for geo in self.gcode_parsed:
path_num += 1
axes.annotate(str(path_num), xy=geo['geom'].coords[0],
xycoords='data')
text.append(str(path_num))
pos.append(geo['geom'].coords[0])
poly = geo['geom'].buffer(tooldia / 2.0).simplify(tool_tolerance)
# if "C" in geo['kind']:
patch = PolygonPatch(poly, facecolor=color[geo['kind'][0]][0],
edgecolor=color[geo['kind'][0]][1],
alpha=alpha[geo['kind'][0]], zorder=2)
axes.add_patch(patch)
obj.shapes.add(poly, color=color[geo['kind'][0]][1], face_color=color[geo['kind'][0]][0],
visible=visible, layer=1 if geo['kind'][0] == 'C' else 2)
# obj.annotation.text = text
# obj.annotation.pos = pos
def create_geometry(self):
# TODO: This takes forever. Too much data?

View File

@@ -22,7 +22,7 @@
import xml.etree.ElementTree as ET
import re
import itertools
from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path
# from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path
from shapely.geometry import LinearRing, LineString, Point
from shapely.affinity import translate, rotate, scale, skew, affine_transform
import numpy as np