Просмотр исходного кода

Basic geometry editor for goemetry objects started.

Juan Pablo Caram 11 лет назад
Родитель
Сommit
73aa0c38a0
9 измененных файлов с 646 добавлено и 14 удалено
  1. 58 9
      FlatCAMApp.py
  2. 553 0
      FlatCAMDraw.py
  3. 8 0
      FlatCAMGUI.py
  4. 6 0
      FlatCAMObj.py
  5. 5 1
      FlatCAMWorker.py
  6. 7 2
      ObjectCollection.py
  7. 7 0
      PlotCanvas.py
  8. 1 1
      defaults.json
  9. 1 1
      recent.json

+ 58 - 9
FlatCAMApp.py

@@ -24,6 +24,7 @@ from FlatCAMCommon import LoudDict
 from FlatCAMTool import *
 
 from FlatCAMShell import FCShell
+from FlatCAMDraw import FlatCAMDraw
 
 
 ########################################
@@ -48,7 +49,7 @@ class App(QtCore.QObject):
     version_date = "2014/10"
 
     ## URL for update checks and statistics
-    version_url = "http://flatcam.org/FlatCAM/apptalk/version"
+    version_url = "http://flatcam.org/version"
 
     ## App URL
     app_url = "http://flatcam.org"
@@ -74,6 +75,9 @@ class App(QtCore.QObject):
         """
 
         App.log.info("FlatCAM Starting...")
+        self.path = os.path.dirname(sys.argv[0])
+        #App.log.debug("Running in " + os.path.realpath(__file__))
+        App.log.debug("Running in " + self.path)
 
         QtCore.QObject.__init__(self)
 
@@ -307,6 +311,9 @@ class App(QtCore.QObject):
         self.ui.menufilesaveprojectas.triggered.connect(self.on_file_saveprojectas)
         self.ui.menufilesaveprojectcopy.triggered.connect(lambda: self.on_file_saveprojectas(make_copy=True))
         self.ui.menufilesavedefaults.triggered.connect(self.on_file_savedefaults)
+        self.ui.menueditnew.triggered.connect(lambda: self.new_object('geometry', 'New Geometry', lambda x, y: None))
+        self.ui.menueditedit.triggered.connect(self.edit_geometry)
+        self.ui.menueditok.triggered.connect(self.editor2geometry)
         self.ui.menueditdelete.triggered.connect(self.on_delete)
         self.ui.menuoptions_transfer_a2o.triggered.connect(self.on_options_app2object)
         self.ui.menuoptions_transfer_a2p.triggered.connect(self.on_options_app2project)
@@ -327,6 +334,9 @@ class App(QtCore.QObject):
         self.ui.zoom_out_btn.triggered.connect(lambda: self.plotcanvas.zoom(1/1.5))
         self.ui.clear_plot_btn.triggered.connect(self.plotcanvas.clear)
         self.ui.replot_btn.triggered.connect(self.on_toolbar_replot)
+        self.ui.newgeo_btn.triggered.connect(lambda: self.new_object('geometry', 'New Geometry', lambda x, y: None))
+        self.ui.editgeo_btn.triggered.connect(self.edit_geometry)
+        self.ui.updategeo_btn.triggered.connect(self.editor2geometry)
         self.ui.delete_btn.triggered.connect(self.on_delete)
         self.ui.shell_btn.triggered.connect(lambda: self.shell.show())
         # Object list
@@ -351,6 +361,8 @@ class App(QtCore.QObject):
         self.measeurement_tool = Measurement(self)
         self.measeurement_tool.install()
 
+        self.draw = FlatCAMDraw(self, disabled=True)
+
         #############
         ### Shell ###
         #############
@@ -411,6 +423,34 @@ class App(QtCore.QObject):
         # Send to worker
         self.worker_task.emit({'fcn': worker_task, 'params': [self]})
 
+    def edit_geometry(self):
+        """
+        Send the current geometry object (if any) into the editor.
+
+        :return: None
+        """
+        if not isinstance(self.collection.get_active(), FlatCAMGeometry):
+            self.info("Select a Geometry Object to edit.")
+            return
+
+        self.draw.edit_fcgeometry(self.collection.get_active())
+
+    def editor2geometry(self):
+        """
+        Transfers the geometry in the editor to the current geometry object.
+
+        :return:
+        """
+        geo = self.collection.get_active()
+        if not isinstance(geo, FlatCAMGeometry):
+            self.info("Select a Geometry Object to update.")
+            return
+
+        self.draw.update_fcgeometry(geo)
+        self.draw.clear()
+        self.draw.drawing_toolbar.setDisabled(True)
+        geo.plot()
+
     def report_usage(self, resource):
         """
         Increments usage counter for the given resource
@@ -491,7 +531,7 @@ class App(QtCore.QObject):
         :return: None
         """
         try:
-            f = open("defaults.json")
+            f = open(self.path + "/defaults.json")
             options = f.read()
             f.close()
         except IOError:
@@ -630,7 +670,9 @@ class App(QtCore.QObject):
             try:
                 self.options_form_fields[option].set_value(self.options[option])
             except KeyError:
-                self.log.error("options_write_form(): No field for: %s" % option)
+                # Changed from error to debug. This allows to have data stored
+                # which is not user-editable.
+                self.log.debug("options_write_form(): No field for: %s" % option)
 
     def on_about(self):
         """
@@ -702,11 +744,13 @@ class App(QtCore.QObject):
 
         # Read options from file
         try:
-            f = open("defaults.json")
+            f = open(self.path + "/defaults.json")
             options = f.read()
             f.close()
         except:
+            e = sys.exc_info()[0]
             App.log.error("Could not load defaults file.")
+            App.log.error(str(e))
             self.inform.emit("ERROR: Could not load defaults file.")
             return
 
@@ -1262,8 +1306,11 @@ class App(QtCore.QObject):
         if str(filename) == "":
             self.inform.emit("Open cancelled.")
         else:
-            self.worker_task.emit({'fcn': self.open_project,
-                                   'params': [filename]})
+            # self.worker_task.emit({'fcn': self.open_project,
+            #                        'params': [filename]})
+            # The above was failing because open_project() is not
+            # thread safe. The new_project()
+            self.open_project(filename)
 
     def on_file_saveproject(self):
         """
@@ -1501,6 +1548,7 @@ class App(QtCore.QObject):
         """
         App.log.debug("Opening project: " + filename)
 
+        ## Open and parse
         try:
             f = open(filename, 'r')
         except IOError:
@@ -1518,15 +1566,16 @@ class App(QtCore.QObject):
 
         self.file_opened.emit("project", filename)
 
-        # Clear the current project
+        ## Clear the current project
+        ## NOT THREAD SAFE ##
         self.on_file_new()
 
-        # Project options
+        ##Project options
         self.options.update(d['options'])
         self.project_filename = filename
         self.ui.units_label.setText("[" + self.options["units"] + "]")
 
-        # Re create objects
+        ## Re create objects
         App.log.debug("Re-creating objects...")
         for obj in d['objs']:
             def obj_init(obj_inst, app_inst):

+ 553 - 0
FlatCAMDraw.py

@@ -0,0 +1,553 @@
+from PyQt4 import QtGui, QtCore, Qt
+import FlatCAMApp
+
+from shapely.geometry import Polygon, LineString, Point, LinearRing
+from shapely.geometry import MultiPoint, MultiPolygon
+from shapely.geometry import box as shply_box
+from shapely.ops import cascaded_union, unary_union
+import shapely.affinity as affinity
+from shapely.wkt import loads as sloads
+from shapely.wkt import dumps as sdumps
+from shapely.geometry.base import BaseGeometry
+
+from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos
+
+
+class DrawTool(object):
+    def __init__(self, draw_app):
+        self.draw_app = draw_app
+        self.complete = False
+        self.start_msg = "Click on 1st point..."
+        self.points = []
+        self.geometry = None
+
+    def click(self, point):
+        return ""
+
+    def utility_geometry(self, data=None):
+        return None
+
+
+class FCShapeTool(DrawTool):
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+
+    def make(self):
+        pass
+
+
+class FCCircle(FCShapeTool):
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.start_msg = "Click on CENTER ..."
+
+    def click(self, point):
+        self.points.append(point)
+
+        if len(self.points) == 1:
+            return "Click on perimeter to complete ..."
+
+        if len(self.points) == 2:
+            self.make()
+            return "Done."
+
+        return ""
+
+    def utility_geometry(self, data=None):
+        if len(self.points) == 1:
+            p1 = self.points[0]
+            p2 = data
+            radius = sqrt((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2)
+            return Point(p1).buffer(radius)
+
+        return None
+
+    def make(self):
+        p1 = self.points[0]
+        p2 = self.points[1]
+        radius = sqrt((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2)
+        self.geometry = Point(p1).buffer(radius)
+        self.complete = True
+
+
+class FCRectangle(FCShapeTool):
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.start_msg = "Click on 1st corner ..."
+
+    def click(self, point):
+        self.points.append(point)
+
+        if len(self.points) == 1:
+            return "Click on opposite corner to complete ..."
+
+        if len(self.points) == 2:
+            self.make()
+            return "Done."
+
+        return ""
+
+    def utility_geometry(self, data=None):
+        if len(self.points) == 1:
+            p1 = self.points[0]
+            p2 = data
+            return LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])
+
+        return None
+
+    def make(self):
+        p1 = self.points[0]
+        p2 = self.points[1]
+        #self.geometry = LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])
+        self.geometry = Polygon([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])
+        self.complete = True
+
+
+class FCPolygon(FCShapeTool):
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.start_msg = "Click on 1st point ..."
+
+    def click(self, point):
+        self.points.append(point)
+
+        if len(self.points) > 0:
+            return "Click on next point or hit SPACE to complete ..."
+
+        return ""
+
+    def utility_geometry(self, data=None):
+        if len(self.points) == 1:
+            temp_points = [x for x in self.points]
+            temp_points.append(data)
+            return LineString(temp_points)
+
+        if len(self.points) > 1:
+            temp_points = [x for x in self.points]
+            temp_points.append(data)
+            return LinearRing(temp_points)
+
+        return None
+
+    def make(self):
+        # self.geometry = LinearRing(self.points)
+        self.geometry = Polygon(self.points)
+        self.complete = True
+
+
+class FCPath(FCPolygon):
+    def make(self):
+        self.geometry = LineString(self.points)
+        self.complete = True
+
+    def utility_geometry(self, data=None):
+        if len(self.points) > 1:
+            temp_points = [x for x in self.points]
+            temp_points.append(data)
+            return LineString(temp_points)
+
+        return None
+
+
+class FCSelect(DrawTool):
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.shape_buffer = self.draw_app.shape_buffer
+        self.start_msg = "Click on geometry to select"
+
+    def click(self, point):
+        min_distance = Inf
+        closest_shape = None
+
+        for shape in self.shape_buffer:
+            if self.draw_app.key != 'control':
+                shape["selected"] = False
+
+            distance = Point(point).distance(shape["geometry"])
+            if distance < min_distance:
+                closest_shape = shape
+                min_distance = distance
+
+        if closest_shape is not None:
+            closest_shape["selected"] = True
+            return "Shape selected."
+
+        return "Nothing selected."
+
+
+class FlatCAMDraw:
+    def __init__(self, app, disabled=False):
+        assert isinstance(app, FlatCAMApp.App)
+        self.app = app
+        self.canvas = app.plotcanvas
+        self.axes = self.canvas.new_axes("draw")
+
+        ### Drawing Toolbar ###
+        self.drawing_toolbar = QtGui.QToolBar()
+        self.drawing_toolbar.setDisabled(disabled)
+        self.app.ui.addToolBar(self.drawing_toolbar)
+        self.select_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), 'Select')
+        self.add_circle_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/circle32.png'), 'Add Circle')
+        self.add_rectangle_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/rectangle32.png'), 'Add Rectangle')
+        self.add_polygon_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/polygon32.png'), 'Add Polygon')
+        self.add_path_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/path32.png'), 'Add Path')
+        self.union_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/union32.png'), 'Polygon Union')
+
+        ### Event handlers ###
+        ## Canvas events
+        self.canvas.mpl_connect('button_press_event', self.on_canvas_click)
+        self.canvas.mpl_connect('motion_notify_event', self.on_canvas_move)
+        self.canvas.mpl_connect('key_press_event', self.on_canvas_key)
+        self.canvas.mpl_connect('key_release_event', self.on_canvas_key_release)
+
+        self.union_btn.triggered.connect(self.union)
+
+        ## Toolbar events and properties
+        self.tools = {
+            "select": {"button": self.select_btn,
+                       "constructor": FCSelect},
+            "circle": {"button": self.add_circle_btn,
+                       "constructor": FCCircle},
+            "rectangle": {"button": self.add_rectangle_btn,
+                          "constructor": FCRectangle},
+            "polygon": {"button": self.add_polygon_btn,
+                        "constructor": FCPolygon},
+            "path": {"button": self.add_path_btn,
+                     "constructor": FCPath}
+        }
+
+        # Data
+        self.active_tool = None
+        self.shape_buffer = []
+
+        self.move_timer = QtCore.QTimer()
+        self.move_timer.setSingleShot(True)
+
+        self.key = None  # Currently pressed key
+
+        def make_callback(tool):
+            def f():
+                self.on_tool_select(tool)
+            return f
+
+        for tool in self.tools:
+            self.tools[tool]["button"].triggered.connect(make_callback(tool))  # Events
+            self.tools[tool]["button"].setCheckable(True)  # Checkable
+
+    def clear(self):
+        self.active_tool = None
+        self.shape_buffer = []
+        self.replot()
+
+    def on_tool_select(self, tool):
+        """
+
+        :rtype : None
+        """
+        self.app.log.debug("on_tool_select('%s')" % tool)
+
+        # This is to make the group behave as radio group
+        if tool in self.tools:
+            if self.tools[tool]["button"].isChecked():
+                self.app.log.debug("%s is checked.")
+                for t in self.tools:
+                    if t != tool:
+                        self.tools[t]["button"].setChecked(False)
+
+                self.active_tool = self.tools[tool]["constructor"](self)
+                self.app.info(self.active_tool.start_msg)
+            else:
+                self.app.log.debug("%s is NOT checked.")
+                for t in self.tools:
+                    self.tools[t]["button"].setChecked(False)
+                self.active_tool = None
+
+    def on_canvas_click(self, event):
+        """
+        event.x .y have canvas coordinates
+        event.xdaya .ydata have plot coordinates
+
+        :param event:
+        :return:
+        """
+        if self.active_tool is not None:
+            # Dispatch event to active_tool
+            msg = self.active_tool.click((event.xdata, event.ydata))
+            self.app.info(msg)
+
+            # If it is a shape generating tool
+            if isinstance(self.active_tool, FCShapeTool) and self.active_tool.complete:
+                self.on_shape_complete()
+                return
+
+            if isinstance(self.active_tool, FCSelect):
+                self.app.log.debug("Replotting after click.")
+                self.replot()
+
+    def on_canvas_move(self, event):
+        """
+        event.x .y have canvas coordinates
+        event.xdaya .ydata have plot coordinates
+
+        :param event:
+        :return:
+        """
+        self.on_canvas_move_effective(event)
+        return
+
+        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 .y have canvas coordinates
+        event.xdaya .ydata have plot coordinates
+
+        :param event:
+        :return:
+        """
+
+        try:
+            x = float(event.xdata)
+            y = float(event.ydata)
+        except TypeError:
+            return
+
+        if self.active_tool is None:
+            return
+
+        geo = self.active_tool.utility_geometry(data=(x, y))
+
+        if geo is not None:
+
+            # Remove any previous utility shape
+            for shape in self.shape_buffer:
+                if shape['utility']:
+                    self.shape_buffer.remove(shape)
+
+            # Add the new utility shape
+            self.shape_buffer.append({
+                'geometry': geo,
+                'selected': False,
+                'utility': True
+            })
+
+            # Efficient plotting for fast animation
+            elements = self.plot_shape(geometry=geo, linespec="b--", animated=True)
+            self.canvas.canvas.restore_region(self.canvas.background)
+            for el in elements:
+                self.axes.draw_artist(el)
+            self.canvas.canvas.blit(self.axes.bbox)
+
+            #self.replot()
+
+    def on_canvas_key(self, event):
+        """
+        event.key has the key.
+
+        :param event:
+        :return:
+        """
+        self.key = event.key
+
+        ### Finish the current action. Use with tools that do not
+        ### complete automatically, like a polygon or path.
+        if event.key == ' ':
+            if isinstance(self.active_tool, FCShapeTool):
+                self.active_tool.click((event.xdata, event.ydata))
+                self.active_tool.make()
+                if self.active_tool.complete:
+                    self.on_shape_complete()
+            return
+
+        ### Abort the current action
+        if event.key == 'escape':
+            # TODO: ...?
+            self.on_tool_select("select")
+            self.app.info("Cancelled.")
+            for_deletion = [shape for shape in self.shape_buffer if shape['utility']]
+            for shape in for_deletion:
+                self.shape_buffer.remove(shape)
+
+            self.replot()
+            return
+
+        ### Delete selected object
+        if event.key == '-':
+            self.delete_selected()
+            self.replot()
+
+    def on_canvas_key_release(self, event):
+        self.key = None
+
+    def delete_selected(self):
+        for_deletion = [shape for shape in self.shape_buffer if shape["selected"]]
+
+        for shape in for_deletion:
+            self.shape_buffer.remove(shape)
+            self.app.info("Shape deleted.")
+
+    def plot_shape(self, geometry=None, linespec='b-', linewidth=1, animated=False):
+        self.app.log.debug("plot_shape()")
+        plot_elements = []
+
+        if geometry is None:
+            geometry = self.active_tool.geometry
+        try:
+            _ = iter(geometry)
+            iterable_geometry = geometry
+        except TypeError:
+            iterable_geometry = [geometry]
+
+        for geo in iterable_geometry:
+
+            if type(geo) == Polygon:
+                x, y = geo.exterior.coords.xy
+                element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
+                plot_elements.append(element)
+                for ints in geo.interiors:
+                    x, y = ints.coords.xy
+                    element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
+                    plot_elements.append(element)
+                continue
+
+            if type(geo) == LineString or type(geo) == LinearRing:
+                x, y = geo.coords.xy
+                element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
+                plot_elements.append(element)
+                continue
+
+            if type(geo) == MultiPolygon:
+                for poly in geo:
+                    x, y = poly.exterior.coords.xy
+                    element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
+                    plot_elements.append(element)
+                    for ints in poly.interiors:
+                        x, y = ints.coords.xy
+                        element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
+                        plot_elements.append(element)
+                continue
+
+        return plot_elements
+        # self.canvas.auto_adjust_axes()
+
+    def plot_all(self):
+        self.app.log.debug("plot_all()")
+        self.axes.cla()
+        for shape in self.shape_buffer:
+            if shape['utility']:
+                self.plot_shape(geometry=shape['geometry'], linespec='k--', linewidth=1)
+                continue
+
+            if shape['selected']:
+                self.plot_shape(geometry=shape['geometry'], linespec='k-', linewidth=2)
+                continue
+
+            self.plot_shape(geometry=shape['geometry'])
+
+        self.canvas.auto_adjust_axes()
+
+    def on_shape_complete(self):
+        self.app.log.debug("on_shape_complete()")
+
+        # For some reason plotting just the last created figure does not
+        # work. The figure is not shown. Calling replot does the trick
+        # which generates a new axes object.
+        #self.plot_shape()
+        #self.canvas.auto_adjust_axes()
+
+        self.shape_buffer.append({'geometry': self.active_tool.geometry,
+                                  'selected': False,
+                                  'utility': False})
+
+        # Remove any utility shapes
+        for shape in self.shape_buffer:
+            if shape['utility']:
+                self.shape_buffer.remove(shape)
+
+        self.replot()
+        self.active_tool = type(self.active_tool)(self)
+
+    def replot(self):
+        #self.canvas.clear()
+        self.axes = self.canvas.new_axes("draw")
+        self.plot_all()
+
+    def edit_fcgeometry(self, fcgeometry):
+        try:
+            _ = iter(fcgeometry.solid_geometry)
+            geometry = fcgeometry.solid_geometry
+        except TypeError:
+            geometry = [fcgeometry.solid_geometry]
+
+        # Delete contents of editor.
+        self.shape_buffer = []
+
+        # Link shapes into editor.
+        for shape in geometry:
+            self.shape_buffer.append({'geometry': shape,
+                                      'selected': False,
+                                      'utility': False})
+
+        self.replot()
+        self.drawing_toolbar.setDisabled(False)
+
+    def update_fcgeometry(self, fcgeometry):
+        """
+        Transfers the drawing tool shape buffer to the selected geometry
+        object. The geometry already in the object are removed.
+
+        :param fcgeometry: FlatCAMGeometry
+        :return: None
+        """
+        fcgeometry.solid_geometry = []
+        for shape in self.shape_buffer:
+            fcgeometry.solid_geometry.append(shape['geometry'])
+
+    def union(self):
+        """
+        Makes union of selected polygons. Original polygons
+        are deleted.
+
+        :return: None.
+        """
+        targets = [shape for shape in self.shape_buffer if shape['selected']]
+
+        results = cascaded_union([t['geometry'] for t in targets])
+
+        for shape in targets:
+            self.shape_buffer.remove(shape)
+
+        try:
+            for geo in results:
+
+                self.shape_buffer.append({
+                    'geometry': geo,
+                    'selected': True,
+                    'utility': False
+                })
+        except TypeError:
+            self.shape_buffer.append({
+                'geometry': results,
+                'selected': True,
+                'utility': False
+            })
+
+        self.replot()

+ 8 - 0
FlatCAMGUI.py

@@ -67,6 +67,10 @@ class FlatCAMGUI(QtGui.QMainWindow):
 
         ### Edit ###
         self.menuedit = self.menu.addMenu('&Edit')
+        self.menueditnew = self.menuedit.addAction(QtGui.QIcon('share/new_geo16.png'), 'New Geometry')
+        self.menueditedit = self.menuedit.addAction(QtGui.QIcon('share/edit16.png'), 'Edit Geometry')
+        self.menueditok = self.menuedit.addAction(QtGui.QIcon('share/edit_ok16.png'), 'Update Geometry')
+        #self.menueditcancel = self.menuedit.addAction(QtGui.QIcon('share/cancel_edit16.png'), "Cancel Edit")
         self.menueditdelete = self.menuedit.addAction(QtGui.QIcon('share/trash16.png'), 'Delete')
 
         ### Options ###
@@ -107,6 +111,10 @@ class FlatCAMGUI(QtGui.QMainWindow):
         self.zoom_in_btn = self.toolbar.addAction(QtGui.QIcon('share/zoom_in32.png'), "&Zoom In")
         self.clear_plot_btn = self.toolbar.addAction(QtGui.QIcon('share/clear_plot32.png'), "&Clear Plot")
         self.replot_btn = self.toolbar.addAction(QtGui.QIcon('share/replot32.png'), "&Replot")
+        self.newgeo_btn = self.toolbar.addAction(QtGui.QIcon('share/new_geo32.png'), "New Blank Geometry")
+        self.editgeo_btn = self.toolbar.addAction(QtGui.QIcon('share/edit32.png'), "Edit Geometry")
+        self.updategeo_btn = self.toolbar.addAction(QtGui.QIcon('share/edit_ok32.png'), "Update Geometry")
+        #self.canceledit_btn = self.toolbar.addAction(QtGui.QIcon('share/cancel_edit32.png'), "Cancel Edit")
         self.delete_btn = self.toolbar.addAction(QtGui.QIcon('share/delete32.png'), "&Delete")
         self.shell_btn = self.toolbar.addAction(QtGui.QIcon('share/shell32.png'), "&Command Line")
 

+ 6 - 0
FlatCAMObj.py

@@ -4,6 +4,7 @@ import FlatCAMApp
 import inspect  # TODO: For debugging only.
 from camlib import *
 from FlatCAMCommon import LoudDict
+from FlatCAMDraw import FlatCAMDraw
 
 
 ########################################
@@ -880,6 +881,11 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         # from predecessors.
         self.ser_attrs += ['options', 'kind']
 
+    def build_ui(self):
+        FlatCAMObj.build_ui(self)
+
+
+
     def set_ui(self, ui):
         FlatCAMObj.set_ui(self, ui)
 

+ 5 - 1
FlatCAMWorker.py

@@ -1,5 +1,4 @@
 from PyQt4 import QtCore
-#import Queue
 import FlatCAMApp
 
 
@@ -16,10 +15,15 @@ class Worker(QtCore.QObject):
 
     def run(self):
         FlatCAMApp.App.log.debug("Worker Started!")
+
+        # Tasks are queued in the event listener.
         self.app.worker_task.connect(self.do_worker_task)
 
     def do_worker_task(self, task):
         FlatCAMApp.App.log.debug("Running task: %s" % str(task))
+
+        # 'worker_name' property of task allows to target
+        # specific worker.
         if 'worker_name' in task and task['worker_name'] == self.name:
             task['fcn'](*task['params'])
             return

+ 7 - 2
ObjectCollection.py

@@ -135,6 +135,11 @@ class ObjectCollection(QtCore.QAbstractListModel):
         self.endRemoveRows()
 
     def get_active(self):
+        """
+        Returns the active object or None
+
+        :return: FlatCAMObj or None
+        """
         selections = self.view.selectedIndexes()
         if len(selections) == 0:
             return None
@@ -143,8 +148,8 @@ class ObjectCollection(QtCore.QAbstractListModel):
 
     def set_active(self, name):
         """
-        Selects object by name from the project list. This trigger the
-        list_selection_changed event and call on_list_selection changed.
+        Selects object by name from the project list. This triggers the
+        list_selection_changed event and call on_list_selection_changed.
 
         :param name: Name of the FlatCAM Object
         :return: None

+ 7 - 0
PlotCanvas.py

@@ -53,6 +53,10 @@ class PlotCanvas:
         #self.container.attach(self.canvas, 0, 0, 600, 400)  # TODO: Height and width are num. columns??
         self.container.addWidget(self.canvas)  # Qt
 
+        # 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)
+
         # Events
         self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
         #self.canvas.connect('configure-event', self.auto_adjust_axes)
@@ -194,6 +198,7 @@ class PlotCanvas:
 
         # Re-draw
         self.canvas.draw()
+        self.background = self.canvas.copy_from_bbox(self.axes.bbox)
 
     def auto_adjust_axes(self, *args):
         """
@@ -246,6 +251,7 @@ class PlotCanvas:
 
         # Re-draw
         self.canvas.draw()
+        self.background = self.canvas.copy_from_bbox(self.axes.bbox)
 
     def pan(self, x, y):
         xmin, xmax = self.axes.get_xlim()
@@ -260,6 +266,7 @@ class PlotCanvas:
 
         # Re-draw
         self.canvas.draw()
+        self.background = self.canvas.copy_from_bbox(self.axes.bbox)
 
     def new_axes(self, name):
         """

+ 1 - 1
defaults.json

@@ -1 +1 @@
-{}
+{"gerber_cutoutgapsize": 0.15, "gerber_noncopperrounded": false, "geometry_paintoverlap": 0.15, "cncjob_append": "", "excellon_feedrate": 3.0, "serial": "q808lhee8dc0k21d0o7b", "stats": {"on_file_openproject": 3, "on_options_app2project": 33, "save_defaults": 8918, "on_delete": 3, "on_about": 1, "geometry_on_paint_button": 4, "on_fileopengerber": 1, "on_toolbar_replot": 7, "gerber_on_generatebb_button": 1, "gerber_on_iso_button": 6, "geometry_on_generatecnc_button": 2, "on_file_new": 33, "on_file_saveproject": 1, "exec_command": 4}, "gerber_plot": true, "excellon_drillz": -0.1, "geometry_feedrate": 3.0, "units": "IN", "excellon_travelz": 0.1, "gerber_multicolored": false, "gerber_solid": true, "gerber_isopasses": 1, "excellon_plot": true, "gerber_isotooldia": 0.016, "cncjob_tooldia": 0.016, "geometry_travelz": 0.1, "gerber_cutoutmargin": 0.1, "excellon_solid": false, "geometry_paintmargin": 0.0, "geometry_cutz": -0.002, "gerber_noncoppermargin": 0.0, "gerber_cutouttooldia": 0.07, "gerber_gaps": "4", "gerber_bboxmargin": 0.0, "cncjob_plot": true, "geometry_plot": true, "gerber_isooverlap": 0.15, "gerber_bboxrounded": false, "geometry_cnctooldia": 0.016, "geometry_painttooldia": 0.07}

+ 1 - 1
recent.json

@@ -1 +1 @@
-[]
+[{"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/Top2.gbr"}, {"kind": "project", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/easy_eda_test/easy_eda.fc"}]