diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 62757ef..a1f2199 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -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)) diff --git a/FlatCAMDraw.py b/FlatCAMDraw.py index fbfdc05..f963e15 100644 --- a/FlatCAMDraw.py +++ b/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 diff --git a/FlatCAMObj.py b/FlatCAMObj.py index 8860bcb..65ce913 100644 --- a/FlatCAMObj.py +++ b/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() diff --git a/MeasurementTool.py b/MeasurementTool.py index f8396d0..12327f3 100644 --- a/MeasurementTool.py +++ b/MeasurementTool.py @@ -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() diff --git a/ObjectCollection.py b/ObjectCollection.py index e423e5d..cb9dd3d 100644 --- a/ObjectCollection.py +++ b/ObjectCollection.py @@ -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 = [] diff --git a/PlotCanvas.py b/PlotCanvas.py index 94469d2..5d6bd83 100644 --- a/PlotCanvas.py +++ b/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 \ No newline at end of file diff --git a/VisPyCanvas.py b/VisPyCanvas.py new file mode 100644 index 0000000..255951f --- /dev/null +++ b/VisPyCanvas.py @@ -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 \ No newline at end of file diff --git a/VisPyVisuals.py b/VisPyVisuals.py new file mode 100644 index 0000000..2219080 --- /dev/null +++ b/VisPyVisuals.py @@ -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) diff --git a/camlib.py b/camlib.py index 169a726..69e3430 100644 --- a/camlib.py +++ b/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? diff --git a/svgparse.py b/svgparse.py index 544f0ba..fd88c8e 100644 --- a/svgparse.py +++ b/svgparse.py @@ -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