mirror of
https://github.com/Denvi/FlatCAM.git
synced 2026-03-06 16:27:19 +01:00
Merge branch 'VisPyExperemental' into develop
# Conflicts: # camlib.py
This commit is contained in:
@@ -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))
|
||||
|
||||
|
||||
217
FlatCAMDraw.py
217
FlatCAMDraw.py
@@ -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
|
||||
|
||||
188
FlatCAMObj.py
188
FlatCAMObj.py
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
|
||||
496
PlotCanvas.py
496
PlotCanvas.py
@@ -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
133
VisPyCanvas.py
Normal 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
262
VisPyVisuals.py
Normal 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)
|
||||
39
camlib.py
39
camlib.py
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user