Marius Stanciu пре 7 година
родитељ
комит
e48d2d2f49
100 измењених фајлова са 38404 додато и 0 уклоњено
  1. 37 0
      FlatCAM.py
  2. 5945 0
      FlatCAMApp.py
  3. 48 0
      FlatCAMCommon.py
  4. 5055 0
      FlatCAMEditor.py
  5. 2364 0
      FlatCAMGUI.py
  6. 4004 0
      FlatCAMObj.py
  7. 28 0
      FlatCAMPool.py
  8. 78 0
      FlatCAMPostProc.py
  9. 156 0
      FlatCAMProcess.py
  10. 85 0
      FlatCAMTool.py
  11. 67 0
      FlatCAMWorker.py
  12. 44 0
      FlatCAMWorkerStack.py
  13. 710 0
      GUIElements.py
  14. 9 0
      LICENSE
  15. 815 0
      ObjectCollection.py
  16. 1166 0
      ObjectUI.py
  17. 453 0
      ParseDXF.py
  18. 809 0
      ParseDXF_Spline.py
  19. 332 0
      ParseFont.py
  20. 652 0
      ParseSVG.py
  21. 228 0
      PlotCanvas.py
  22. 1407 0
      README.md
  23. 167 0
      VisPyCanvas.py
  24. 126 0
      VisPyPatches.py
  25. 90 0
      VisPyTesselators.py
  26. 596 0
      VisPyVisuals.py
  27. 6435 0
      camlib.py
  28. 169 0
      flatcamTools/ToolCalculators.py
  29. 390 0
      flatcamTools/ToolCutout.py
  30. 467 0
      flatcamTools/ToolDblSided.py
  31. 206 0
      flatcamTools/ToolFilm.py
  32. 172 0
      flatcamTools/ToolImage.py
  33. 352 0
      flatcamTools/ToolMeasurement.py
  34. 238 0
      flatcamTools/ToolMove.py
  35. 882 0
      flatcamTools/ToolNonCopperClear.py
  36. 1106 0
      flatcamTools/ToolPaint.py
  37. 369 0
      flatcamTools/ToolPanelize.py
  38. 123 0
      flatcamTools/ToolProperties.py
  39. 361 0
      flatcamTools/ToolShell.py
  40. 756 0
      flatcamTools/ToolTransform.py
  41. 16 0
      flatcamTools/__init__.py
  42. 91 0
      make_win.py
  43. 119 0
      postprocessors/Roland_MDX_20.py
  44. 0 0
      postprocessors/__init__.py
  45. 137 0
      postprocessors/default.py
  46. 134 0
      postprocessors/grbl_11.py
  47. 74 0
      postprocessors/grbl_laser.py
  48. 152 0
      postprocessors/manual_toolchange.py
  49. 135 0
      postprocessors/marlin.py
  50. 18 0
      requirements.txt
  51. 31 0
      setup_ubuntu.sh
  52. BIN
      share/active.gif
  53. BIN
      share/activity2.gif
  54. BIN
      share/addarray16.png
  55. BIN
      share/addarray20.png
  56. BIN
      share/addarray32.png
  57. BIN
      share/arc16.png
  58. BIN
      share/arc24.png
  59. BIN
      share/arc32.png
  60. BIN
      share/axis32.png
  61. BIN
      share/blocked16.png
  62. BIN
      share/bold32.png
  63. BIN
      share/buffer16-2.png
  64. BIN
      share/buffer16.png
  65. BIN
      share/buffer20.png
  66. BIN
      share/buffer24.png
  67. BIN
      share/bug16.png
  68. BIN
      share/calculator24.png
  69. BIN
      share/cancel_edit16.png
  70. BIN
      share/cancel_edit32.png
  71. BIN
      share/circle32.png
  72. BIN
      share/clear_plot16.png
  73. BIN
      share/clear_plot32.png
  74. BIN
      share/cnc16.png
  75. BIN
      share/cnc32.png
  76. BIN
      share/code.png
  77. BIN
      share/convert24.png
  78. BIN
      share/copy.png
  79. BIN
      share/copy16.png
  80. BIN
      share/copy32.png
  81. BIN
      share/copy_geo.png
  82. BIN
      share/corner32.png
  83. BIN
      share/cut16.png
  84. BIN
      share/cut32.png
  85. BIN
      share/cutpath16.png
  86. BIN
      share/cutpath24.png
  87. BIN
      share/cutpath32.png
  88. BIN
      share/defaults.png
  89. BIN
      share/delete32.png
  90. BIN
      share/deleteshape16.png
  91. BIN
      share/deleteshape24.png
  92. BIN
      share/deleteshape32.png
  93. BIN
      share/doubleside16.png
  94. BIN
      share/doubleside32.png
  95. BIN
      share/draw32.png
  96. BIN
      share/drill16.png
  97. BIN
      share/drill32.png
  98. BIN
      share/dxf16.png
  99. BIN
      share/edit16.png
  100. BIN
      share/edit32.png

+ 37 - 0
FlatCAM.py

@@ -0,0 +1,37 @@
+import sys
+from PyQt5 import sip
+
+from PyQt5 import QtGui, QtCore, QtWidgets
+from FlatCAMApp import App
+from multiprocessing import freeze_support
+import VisPyPatches
+
+if sys.platform == "win32":
+    # cx_freeze 'module win32' workaround
+    import OpenGL.platform.win32
+
+def debug_trace():
+    """
+    Set a tracepoint in the Python debugger that works with Qt
+    :return: None
+    """
+    from PyQt5.QtCore import pyqtRemoveInputHook
+    #from pdb import set_trace
+    pyqtRemoveInputHook()
+    #set_trace()
+
+# All X11 calling should be thread safe otherwise we have strange issues
+# QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads)
+# NOTE: Never talk to the GUI from threads! This is why I commented the above.
+
+
+if __name__ == '__main__':
+    freeze_support()
+
+    debug_trace()
+    VisPyPatches.apply_patches()
+
+    app = QtWidgets.QApplication(sys.argv)
+    fc = App()
+    sys.exit(app.exec_())
+

+ 5945 - 0
FlatCAMApp.py

@@ -0,0 +1,5945 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+import sys
+import traceback
+import urllib.request, urllib.parse, urllib.error
+import getopt
+import os
+import random
+import logging
+import simplejson as json
+
+import re
+import os
+import tkinter as tk
+from PyQt5 import QtCore, QtGui, QtWidgets, QtPrintSupport
+import time  # Just used for debugging. Double check before removing.
+import urllib.request, urllib.parse, urllib.error
+import webbrowser
+from contextlib import contextmanager
+from xml.dom.minidom import parseString as parse_xml_string
+from copy import copy,deepcopy
+import numpy as np
+from datetime import datetime
+import gc
+
+########################################
+##      Imports part of FlatCAM       ##
+########################################
+from ObjectCollection import *
+from FlatCAMObj import *
+from PlotCanvas import *
+from FlatCAMGUI import *
+from FlatCAMCommon import LoudDict
+from FlatCAMPostProc import load_postprocessors
+from FlatCAMEditor import FlatCAMGeoEditor, FlatCAMExcEditor
+from FlatCAMProcess import *
+from FlatCAMWorkerStack import WorkerStack
+from VisPyVisuals import Color
+from vispy.gloo.util import _screenshot
+from vispy.io import write_png
+
+from flatcamTools import *
+
+from multiprocessing import Pool
+import tclCommands
+
+from ParseFont import *
+
+
+########################################
+##                App                 ##
+########################################
+class App(QtCore.QObject):
+    """
+    The main application class. The constructor starts the GUI.
+    """
+
+    # Get Cmd Line Options
+    cmd_line_shellfile = ''
+    cmd_line_help = "FlatCam.py --shellfile=<cmd_line_shellfile>"
+    try:
+        # Multiprocessing pool will spawn additional processes with 'multiprocessing-fork' flag
+        cmd_line_options, args = getopt.getopt(sys.argv[1:], "h:", ["shellfile=", "multiprocessing-fork="])
+    except getopt.GetoptError:
+        print(cmd_line_help)
+        sys.exit(2)
+    for opt, arg in cmd_line_options:
+        if opt == '-h':
+            print(cmd_line_help)
+            sys.exit()
+        elif opt == '--shellfile':
+            cmd_line_shellfile = arg
+
+    # Logging ##
+    log = logging.getLogger('base')
+    log.setLevel(logging.DEBUG)
+    # log.setLevel(logging.WARNING)
+    formatter = logging.Formatter('[%(levelname)s][%(threadName)s] %(message)s')
+    handler = logging.StreamHandler()
+    handler.setFormatter(formatter)
+    log.addHandler(handler)
+
+    # Version
+    version = 'Beta1'
+    version_date = "2019/01/03"
+
+    # URL for update checks and statistics
+    version_url = "http://flatcam.org/version"
+
+    # App URL
+    app_url = "http://flatcam.org"
+
+    # Manual URL
+    manual_url = "http://flatcam.org/manual/index.html"
+    video_url = "https://www.youtube.com/playlist?list=PLVvP2SYRpx-AQgNlfoxw93tXUXon7G94_"
+
+
+    ##################
+    ##    Signals   ##
+    ##################
+
+    # Inform the user
+    # Handled by:
+    #  * App.info() --> Print on the status bar
+    inform = QtCore.pyqtSignal(str)
+
+    # General purpose background task
+    worker_task = QtCore.pyqtSignal(dict)
+
+    # File opened
+    # Handled by:
+    #  * register_folder()
+    #  * register_recent()
+    # Note: Setting the parameters to unicode does not seem
+    #       to have an effect. Then are received as Qstring
+    #       anyway.
+
+    # File type and filename
+    file_opened = QtCore.pyqtSignal(str, str)
+    # File type and filename
+    file_saved = QtCore.pyqtSignal(str, str)
+
+    # Percentage of progress
+    progress = QtCore.pyqtSignal(int)
+
+    plots_updated = QtCore.pyqtSignal()
+
+    # Emitted by new_object() and passes the new object as argument, plot flag.
+    # on_object_created() adds the object to the collection, plots on appropriate flag
+    # and emits new_object_available.
+    object_created = QtCore.pyqtSignal(object, bool, bool)
+
+    # Emitted when a object has been changed (like scaled, mirrored)
+    object_changed = QtCore.pyqtSignal(object)
+
+    # Emitted after object has been plotted.
+    # Calls 'on_zoom_fit' method to fit object in scene view in main thread to prevent drawing glitches.
+    object_plotted = QtCore.pyqtSignal(object)
+
+    # Emitted when a new object has been added to the collection
+    # and is ready to be used.
+    new_object_available = QtCore.pyqtSignal(object)
+    message = QtCore.pyqtSignal(str, str, str)
+
+    # Emmited when shell command is finished(one command only)
+    shell_command_finished = QtCore.pyqtSignal(object)
+
+    # Emitted when multiprocess pool has been recreated
+    pool_recreated = QtCore.pyqtSignal(object)
+
+    # Emitted when an unhandled exception happens
+    # in the worker task.
+    thread_exception = QtCore.pyqtSignal(object)
+
+    def __init__(self, user_defaults=True, post_gui=None):
+        """
+        Starts the application.
+
+        :return: app
+        :rtype: App
+        """
+
+        App.log.info("FlatCAM Starting...")
+
+        ###################
+        ### OS-specific ###
+        ###################
+
+        # Folder for user settings.
+        if sys.platform == 'win32':
+            from win32com.shell import shell, shellcon
+            if platform.architecture()[0] == '32bit':
+                App.log.debug("Win32!")
+            else:
+                App.log.debug("Win64!")
+            self.data_path = shell.SHGetFolderPath(0, shellcon.CSIDL_APPDATA, None, 0) + \
+                '/FlatCAM'
+            self.os = 'windows'
+        else:  # Linux/Unix/MacOS
+            self.data_path = os.path.expanduser('~') + \
+                '/.FlatCAM'
+            self.os = 'unix'
+
+        ###############################
+        ### Setup folders and files ###
+        ###############################
+
+        if not os.path.exists(self.data_path):
+            os.makedirs(self.data_path)
+            App.log.debug('Created data folder: ' + self.data_path)
+            os.makedirs(os.path.join(self.data_path, 'postprocessors'))
+            App.log.debug('Created data postprocessors folder: ' + os.path.join(self.data_path, 'postprocessors'))
+
+        self.postprocessorpaths = os.path.join(self.data_path,'postprocessors')
+        if not os.path.exists(self.postprocessorpaths):
+            os.makedirs(self.postprocessorpaths)
+            App.log.debug('Created postprocessors folder: ' + self.postprocessorpaths)
+
+        # create defaults.json file if there is none
+        try:
+            f = open(self.data_path + '/defaults.json')
+            f.close()
+        except IOError:
+            App.log.debug('Creating empty defaults.json')
+            f = open(self.data_path + '/defaults.json', 'w')
+            json.dump({}, f)
+            f.close()
+
+        # create factory_defaults.json file if there is none
+        try:
+            f = open(self.data_path + '/factory_defaults.json')
+            f.close()
+        except IOError:
+            App.log.debug('Creating empty factory_defaults.json')
+            f = open(self.data_path + '/factory_defaults.json', 'w')
+            json.dump({}, f)
+            f.close()
+
+        try:
+            f = open(self.data_path + '/recent.json')
+            f.close()
+        except IOError:
+            App.log.debug('Creating empty recent.json')
+            f = open(self.data_path + '/recent.json', 'w')
+            json.dump([], f)
+            f.close()
+
+        # Application directory. CHDIR to it. Otherwise, trying to load
+        # GUI icons will fail as their path is relative.
+        # This will fail under cx_freeze ...
+        self.app_home = os.path.dirname(os.path.realpath(__file__))
+        App.log.debug("Application path is " + self.app_home)
+        App.log.debug("Started in " + os.getcwd())
+
+        # cx_freeze workaround
+        if os.path.isfile(self.app_home):
+            self.app_home = os.path.dirname(self.app_home)
+
+        os.chdir(self.app_home)
+
+        # Create multiprocessing pool
+        self.pool = Pool()
+
+
+        ####################
+        ## Initialize GUI ##
+        ####################
+
+        # FlatCAM colors used in plotting
+        self.FC_light_green = '#BBF268BF'
+        self.FC_dark_green = '#006E20BF'
+        self.FC_light_blue = '#a5a5ffbf'
+        self.FC_dark_blue = '#0000ffbf'
+
+        QtCore.QObject.__init__(self)
+
+        self.ui = FlatCAMGUI(self.version, self)
+        # self.connect(self.ui,
+        #              QtCore.SIGNAL("geomUpdate(int, int, int, int, int)"),
+        #              self.save_geometry) PyQt4
+        self.ui.geom_update[int, int, int, int, int].connect(self.save_geometry)
+        self.ui.final_save.connect(self.final_save)
+
+        ##############
+        #### Data ####
+        ##############
+        self.recent = []
+        self.clipboard = QtWidgets.QApplication.clipboard()
+        self.proc_container = FCVisibleProcessContainer(self.ui.activity_view)
+
+        self.project_filename = None
+        self.toggle_units_ignore = False
+
+        # self.defaults_form = PreferencesUI()
+        self.general_defaults_form = GeneralPreferencesUI()
+        self.gerber_defaults_form = GerberPreferencesUI()
+        self.excellon_defaults_form = ExcellonPreferencesUI()
+        self.geometry_defaults_form = GeometryPreferencesUI()
+        self.cncjob_defaults_form = CNCJobPreferencesUI()
+
+        # when adding entries here read the comments in the  method found bellow named:
+        # def new_object(self, kind, name, initialize, active=True, fit=True, plot=True)
+        self.defaults_form_fields = {
+            "units": self.general_defaults_form.general_group.units_radio,
+            "global_shell_at_startup": self.general_defaults_form.general_group.shell_startup_cb,
+            "global_gridx": self.general_defaults_form.general_group.gridx_entry,
+            "global_gridy": self.general_defaults_form.general_group.gridy_entry,
+            "global_plot_fill": self.general_defaults_form.general_group.pf_color_entry,
+            "global_plot_line": self.general_defaults_form.general_group.pl_color_entry,
+            "global_sel_fill": self.general_defaults_form.general_group.sf_color_entry,
+            "global_sel_line": self.general_defaults_form.general_group.sl_color_entry,
+            "global_alt_sel_fill": self.general_defaults_form.general_group.alt_sf_color_entry,
+            "global_alt_sel_line": self.general_defaults_form.general_group.alt_sl_color_entry,
+            "global_draw_color": self.general_defaults_form.general_group.draw_color_entry,
+            "global_sel_draw_color": self.general_defaults_form.general_group.sel_draw_color_entry,
+            "global_pan_button": self.general_defaults_form.general_group.pan_button_radio,
+            "global_mselect_key": self.general_defaults_form.general_group.mselect_radio,
+            # "global_pan_with_space_key": self.general_defaults_form.general_group.pan_with_space_cb,
+            "global_workspace": self.general_defaults_form.general_group.workspace_cb,
+            "global_workspaceT": self.general_defaults_form.general_group.wk_cb,
+
+            "gerber_plot": self.gerber_defaults_form.gerber_group.plot_cb,
+            "gerber_solid": self.gerber_defaults_form.gerber_group.solid_cb,
+            "gerber_multicolored": self.gerber_defaults_form.gerber_group.multicolored_cb,
+            "gerber_isotooldia": self.gerber_defaults_form.gerber_group.iso_tool_dia_entry,
+            "gerber_isopasses": self.gerber_defaults_form.gerber_group.iso_width_entry,
+            "gerber_isooverlap": self.gerber_defaults_form.gerber_group.iso_overlap_entry,
+            "gerber_ncctools": self.gerber_defaults_form.gerber_group.ncc_tool_dia_entry,
+            "gerber_nccoverlap": self.gerber_defaults_form.gerber_group.ncc_overlap_entry,
+            "gerber_nccmargin": self.gerber_defaults_form.gerber_group.ncc_margin_entry,
+            "gerber_nccmethod": self.gerber_defaults_form.gerber_group.ncc_method_radio,
+            "gerber_nccconnect": self.gerber_defaults_form.gerber_group.ncc_connect_cb,
+            "gerber_ncccontour": self.gerber_defaults_form.gerber_group.ncc_contour_cb,
+            "gerber_nccrest": self.gerber_defaults_form.gerber_group.ncc_rest_cb,
+
+            "gerber_combine_passes": self.gerber_defaults_form.gerber_group.combine_passes_cb,
+            "gerber_milling_type": self.gerber_defaults_form.gerber_group.milling_type_radio,
+            "gerber_cutouttooldia": self.gerber_defaults_form.gerber_group.cutout_tooldia_entry,
+            "gerber_cutoutmargin": self.gerber_defaults_form.gerber_group.cutout_margin_entry,
+            "gerber_cutoutgapsize": self.gerber_defaults_form.gerber_group.cutout_gap_entry,
+            "gerber_gaps": self.gerber_defaults_form.gerber_group.gaps_radio,
+            "gerber_noncoppermargin": self.gerber_defaults_form.gerber_group.noncopper_margin_entry,
+            "gerber_noncopperrounded": self.gerber_defaults_form.gerber_group.noncopper_rounded_cb,
+            "gerber_bboxmargin": self.gerber_defaults_form.gerber_group.bbmargin_entry,
+            "gerber_bboxrounded": self.gerber_defaults_form.gerber_group.bbrounded_cb,
+            "gerber_circle_steps": self.gerber_defaults_form.gerber_group.circle_steps_entry,
+            "excellon_plot": self.excellon_defaults_form.excellon_group.plot_cb,
+            "excellon_solid": self.excellon_defaults_form.excellon_group.solid_cb,
+            "excellon_drillz": self.excellon_defaults_form.excellon_group.cutz_entry,
+            "excellon_travelz": self.excellon_defaults_form.excellon_group.travelz_entry,
+            "excellon_feedrate": self.excellon_defaults_form.excellon_group.feedrate_entry,
+            "excellon_feedrate_rapid": self.excellon_defaults_form.excellon_group.feedrate_rapid_entry,
+            "excellon_spindlespeed": self.excellon_defaults_form.excellon_group.spindlespeed_entry,
+            "excellon_dwell": self.excellon_defaults_form.excellon_group.dwell_cb,
+            "excellon_dwelltime": self.excellon_defaults_form.excellon_group.dwelltime_entry,
+            "excellon_toolchange": self.excellon_defaults_form.excellon_group.toolchange_cb,
+            "excellon_toolchangez": self.excellon_defaults_form.excellon_group.toolchangez_entry,
+            "excellon_toolchangexy": self.excellon_defaults_form.excellon_group.toolchangexy_entry,
+            "excellon_ppname_e": self.excellon_defaults_form.excellon_group.pp_excellon_name_cb,
+            "excellon_startz": self.excellon_defaults_form.excellon_group.estartz_entry,
+            "excellon_endz": self.excellon_defaults_form.excellon_group.eendz_entry,
+            "excellon_tooldia": self.excellon_defaults_form.excellon_group.tooldia_entry,
+            "excellon_slot_tooldia": self.excellon_defaults_form.excellon_group.slot_tooldia_entry,
+            "excellon_format_upper_in": self.excellon_defaults_form.excellon_group.excellon_format_upper_in_entry,
+            "excellon_format_lower_in": self.excellon_defaults_form.excellon_group.excellon_format_lower_in_entry,
+            "excellon_format_upper_mm": self.excellon_defaults_form.excellon_group.excellon_format_upper_mm_entry,
+            "excellon_format_lower_mm": self.excellon_defaults_form.excellon_group.excellon_format_lower_mm_entry,
+            "excellon_zeros": self.excellon_defaults_form.excellon_group.excellon_zeros_radio,
+            "excellon_units": self.excellon_defaults_form.excellon_group.excellon_units_radio,
+            "excellon_optimization_type": self.excellon_defaults_form.excellon_group.excellon_optimization_radio,
+            "excellon_gcode_type": self.excellon_defaults_form.excellon_group.excellon_gcode_type_radio,
+            "geometry_plot": self.geometry_defaults_form.geometry_group.plot_cb,
+            "geometry_cutz": self.geometry_defaults_form.geometry_group.cutz_entry,
+            "geometry_travelz": self.geometry_defaults_form.geometry_group.travelz_entry,
+            "geometry_feedrate": self.geometry_defaults_form.geometry_group.cncfeedrate_entry,
+            "geometry_feedrate_z": self.geometry_defaults_form.geometry_group.cncplunge_entry,
+            "geometry_feedrate_rapid": self.geometry_defaults_form.geometry_group.cncfeedrate_rapid_entry,
+            "geometry_cnctooldia": self.geometry_defaults_form.geometry_group.cnctooldia_entry,
+            "geometry_painttooldia": self.geometry_defaults_form.geometry_group.painttooldia_entry,
+            "geometry_spindlespeed": self.geometry_defaults_form.geometry_group.cncspindlespeed_entry,
+            "geometry_dwell": self.geometry_defaults_form.geometry_group.dwell_cb,
+            "geometry_dwelltime": self.geometry_defaults_form.geometry_group.dwelltime_entry,
+            "geometry_paintoverlap": self.geometry_defaults_form.geometry_group.paintoverlap_entry,
+            "geometry_paintmargin": self.geometry_defaults_form.geometry_group.paintmargin_entry,
+            "geometry_paintmethod": self.geometry_defaults_form.geometry_group.paintmethod_combo,
+            "geometry_selectmethod": self.geometry_defaults_form.geometry_group.selectmethod_combo,
+            "geometry_pathconnect": self.geometry_defaults_form.geometry_group.pathconnect_cb,
+            "geometry_paintcontour": self.geometry_defaults_form.geometry_group.contour_cb,
+            "geometry_ppname_g": self.geometry_defaults_form.geometry_group.pp_geometry_name_cb,
+            "geometry_toolchange": self.geometry_defaults_form.geometry_group.toolchange_cb,
+            "geometry_toolchangez": self.geometry_defaults_form.geometry_group.toolchangez_entry,
+            "geometry_toolchangexy": self.geometry_defaults_form.geometry_group.toolchangexy_entry,
+            "geometry_startz": self.geometry_defaults_form.geometry_group.gstartz_entry,
+            "geometry_endz": self.geometry_defaults_form.geometry_group.gendz_entry,
+            "geometry_multidepth": self.geometry_defaults_form.geometry_group.multidepth_cb,
+            "geometry_depthperpass": self.geometry_defaults_form.geometry_group.depthperpass_entry,
+            "geometry_extracut": self.geometry_defaults_form.geometry_group.extracut_cb,
+            "geometry_circle_steps": self.geometry_defaults_form.geometry_group.circle_steps_entry,
+            "cncjob_plot": self.cncjob_defaults_form.cncjob_group.plot_cb,
+            "cncjob_tooldia": self.cncjob_defaults_form.cncjob_group.tooldia_entry,
+            "cncjob_coords_decimals": self.cncjob_defaults_form.cncjob_group.coords_dec_entry,
+            "cncjob_fr_decimals": self.cncjob_defaults_form.cncjob_group.fr_dec_entry,
+            "cncjob_prepend": self.cncjob_defaults_form.cncjob_group.prepend_text,
+            "cncjob_append": self.cncjob_defaults_form.cncjob_group.append_text,
+            "cncjob_steps_per_circle": self.cncjob_defaults_form.cncjob_group.steps_per_circle_entry
+        }
+        # loads postprocessors
+        self.postprocessors = load_postprocessors(self)
+
+        for name in list(self.postprocessors.keys()):
+            self.geometry_defaults_form.geometry_group.pp_geometry_name_cb.addItem(name)
+            self.excellon_defaults_form.excellon_group.pp_excellon_name_cb.addItem(name)
+
+        self.defaults = LoudDict()
+        self.defaults.set_change_callback(self.on_defaults_dict_change)  # When the dictionary changes.
+        self.defaults.update({
+            "global_serial": 0,
+            "global_stats": {},
+            "units": "IN",
+            "global_gridx": 1.0,
+            "global_gridy": 1.0,
+            "global_plot_fill": '#BBF268BF',
+            "global_plot_line": '#006E20BF',
+            "global_sel_fill": '#a5a5ffbf',
+            "global_sel_line": '#0000ffbf',
+            "global_alt_sel_fill": '#BBF268BF',
+            "global_alt_sel_line": '#006E20BF',
+            "global_draw_color": '#FF0000',
+            "global_sel_draw_color": '#0000FF',
+            "global_pan_button": '2',
+            "global_mselect_key": 'Control',
+            # "global_pan_with_space_key": False,
+            "global_workspace": False,
+            "global_workspaceT": "A4P",
+            "global_toolbar_view": 31,
+
+            "gerber_plot": True,
+            "gerber_solid": True,
+            "gerber_multicolored": False,
+            "gerber_isotooldia": 0.016,
+            "gerber_isopasses": 1,
+            "gerber_isooverlap": 0.15,
+            "gerber_ncctools": "1.0, 0.5",
+            "gerber_nccoverlap": 0.4,
+            "gerber_nccmargin": 1,
+            "gerber_nccmethod": "seed",
+            "gerber_nccconnect": True,
+            "gerber_ncccontour": True,
+            "gerber_nccrest": False,
+
+            "gerber_combine_passes": False,
+            "gerber_milling_type": "cl",
+            "gerber_cutouttooldia": 0.07,
+            "gerber_cutoutmargin": 0.1,
+            "gerber_cutoutgapsize": 0.15,
+            "gerber_gaps": "4",
+            "gerber_noncoppermargin": 0.0,
+            "gerber_noncopperrounded": False,
+            "gerber_bboxmargin": 0.0,
+            "gerber_bboxrounded": False,
+            "gerber_circle_steps": 64,
+
+            "excellon_plot": True,
+            "excellon_solid": False,
+            "excellon_drillz": -0.1,
+            "excellon_travelz": 0.1,
+            "excellon_feedrate": 3.0,
+            "excellon_feedrate_rapid": 3.0,
+            "excellon_spindlespeed": None,
+            "excellon_dwell": False,
+            "excellon_dwelltime": 1,
+            "excellon_toolchange": False,
+            "excellon_toolchangez": 1.0,
+            "excellon_toolchangexy": "0.0, 0.0",
+            "excellon_tooldia": 0.016,
+            "excellon_slot_tooldia": 0.016,
+            "excellon_startz": None,
+            "excellon_endz": 2.0,
+            "excellon_ppname_e": 'default',
+            "excellon_format_upper_in": 2,
+            "excellon_format_lower_in": 4,
+            "excellon_format_upper_mm": 3,
+            "excellon_format_lower_mm": 3,
+            "excellon_zeros": "L",
+            "excellon_units": "INCH",
+            "excellon_optimization_type": 'B',
+            "excellon_search_time": 3,
+            "excellon_gcode_type": "drills",
+
+            "geometry_plot": True,
+            "geometry_cutz": -0.002,
+            "geometry_travelz": 0.1,
+            "geometry_toolchange": False,
+            "geometry_toolchangez": 1.0,
+            "geometry_toolchangexy": "0.0, 0.0",
+            "geometry_startz": None,
+            "geometry_endz": 2.0,
+            "geometry_feedrate": 3.0,
+            "geometry_feedrate_z": 3.0,
+            "geometry_feedrate_rapid": 3.0,
+            "geometry_cnctooldia": 0.016,
+            "geometry_spindlespeed": None,
+            "geometry_dwell": False,
+            "geometry_dwelltime": 1,
+            "geometry_painttooldia": 0.07,
+            "geometry_paintoverlap": 0.15,
+            "geometry_paintmargin": 0.0,
+            "geometry_paintmethod": "seed",
+            "geometry_selectmethod": "single",
+            "geometry_pathconnect": True,
+            "geometry_paintcontour": True,
+            "geometry_ppname_g": 'default',
+            "geometry_depthperpass": 0.002,
+            "geometry_multidepth": False,
+            "geometry_extracut": False,
+            "geometry_circle_steps": 64,
+
+            "cncjob_plot": True,
+            "cncjob_tooldia": 0.0393701,
+            "cncjob_coords_decimals": 4,
+            "cncjob_fr_decimals": 2,
+            "cncjob_prepend": "",
+            "cncjob_append": "",
+            "cncjob_steps_per_circle": 64,
+            "global_background_timeout": 300000,  # Default value is 5 minutes
+            "global_verbose_error_level": 0,  # Shell verbosity 0 = default
+                                       # (python trace only for unknown errors),
+                                       # 1 = show trace(show trace allways),
+                                       # 2 = (For the future).
+
+            # Persistence
+            "global_last_folder": None,
+            "global_last_save_folder": None,
+
+            # Default window geometry
+            "global_def_win_x": 100,
+            "global_def_win_y": 100,
+            "global_def_win_w": 1024,
+            "global_def_win_h": 650,
+
+            # Constants...
+            "global_defaults_save_period_ms": 20000,   # Time between default saves.
+            "global_shell_shape": [500, 300],          # Shape of the shell in pixels.
+            "global_shell_at_startup": False,          # Show the shell at startup.
+            "global_recent_limit": 10,                 # Max. items in recent list.
+            "fit_key": '1',
+            "zoom_out_key": '2',
+            "zoom_in_key": '3',
+            "grid_toggle_key": 'G',
+            "zoom_ratio": 1.5,
+            "global_point_clipboard_format": "(%.4f, %.4f)",
+            "global_zdownrate": None,
+            "gerber_use_buffer_for_union": True
+        })
+
+        ###############################
+        ### Load defaults from file ###
+        if user_defaults:
+            self.load_defaults()
+
+        chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
+        if self.defaults['global_serial'] == 0 or len(str(self.defaults['global_serial'])) < 10:
+            self.defaults['global_serial'] = ''.join([random.choice(chars) for i in range(20)])
+            self.save_defaults(silent=True)
+
+        self.propagate_defaults(silent=True)
+        self.restore_main_win_geom()
+
+        def auto_save_defaults():
+            try:
+                self.save_defaults(silent=True)
+                self.propagate_defaults(silent=True)
+            finally:
+                QtCore.QTimer.singleShot(self.defaults["global_defaults_save_period_ms"], auto_save_defaults)
+
+        # the following lines activates automatic defaults save
+        # if user_defaults:
+        #     QtCore.QTimer.singleShot(self.defaults["global_defaults_save_period_ms"], auto_save_defaults)
+
+        # self.options_form = PreferencesUI()
+        self.general_options_form = GeneralPreferencesUI()
+        self.gerber_options_form = GerberPreferencesUI()
+        self.excellon_options_form = ExcellonPreferencesUI()
+        self.geometry_options_form = GeometryPreferencesUI()
+        self.cncjob_options_form = CNCJobPreferencesUI()
+
+        self.options_form_fields = {
+            "units": self.general_options_form.general_group.units_radio,
+            "global_gridx": self.general_options_form.general_group.gridx_entry,
+            "global_gridy": self.general_options_form.general_group.gridy_entry,
+            "gerber_plot": self.gerber_options_form.gerber_group.plot_cb,
+            "gerber_solid": self.gerber_options_form.gerber_group.solid_cb,
+            "gerber_multicolored": self.gerber_options_form.gerber_group.multicolored_cb,
+            "gerber_isotooldia": self.gerber_options_form.gerber_group.iso_tool_dia_entry,
+            "gerber_isopasses": self.gerber_options_form.gerber_group.iso_width_entry,
+            "gerber_isooverlap": self.gerber_options_form.gerber_group.iso_overlap_entry,
+            "gerber_ncctools": self.gerber_options_form.gerber_group.ncc_tool_dia_entry,
+            "gerber_nccoverlap": self.gerber_options_form.gerber_group.ncc_overlap_entry,
+            "gerber_nccmargin": self.gerber_options_form.gerber_group.ncc_margin_entry,
+            "gerber_combine_passes": self.gerber_options_form.gerber_group.combine_passes_cb,
+            "gerber_cutouttooldia": self.gerber_options_form.gerber_group.cutout_tooldia_entry,
+            "gerber_cutoutmargin": self.gerber_options_form.gerber_group.cutout_margin_entry,
+            "gerber_cutoutgapsize": self.gerber_options_form.gerber_group.cutout_gap_entry,
+            "gerber_gaps": self.gerber_options_form.gerber_group.gaps_radio,
+            "gerber_noncoppermargin": self.gerber_options_form.gerber_group.noncopper_margin_entry,
+            "gerber_noncopperrounded": self.gerber_options_form.gerber_group.noncopper_rounded_cb,
+            "gerber_bboxmargin": self.gerber_options_form.gerber_group.bbmargin_entry,
+            "gerber_bboxrounded": self.gerber_options_form.gerber_group.bbrounded_cb,
+            "excellon_plot": self.excellon_options_form.excellon_group.plot_cb,
+            "excellon_solid": self.excellon_options_form.excellon_group.solid_cb,
+            "excellon_drillz": self.excellon_options_form.excellon_group.cutz_entry,
+            "excellon_travelz": self.excellon_options_form.excellon_group.travelz_entry,
+            "excellon_feedrate": self.excellon_options_form.excellon_group.feedrate_entry,
+            "excellon_feedrate_rapid": self.excellon_options_form.excellon_group.feedrate_rapid_entry,
+            "excellon_spindlespeed": self.excellon_options_form.excellon_group.spindlespeed_entry,
+            "excellon_dwell": self.excellon_options_form.excellon_group.dwell_cb,
+            "excellon_dwelltime": self.excellon_options_form.excellon_group.dwelltime_entry,
+            "excellon_toolchange": self.excellon_options_form.excellon_group.toolchange_cb,
+            "excellon_toolchangez": self.excellon_options_form.excellon_group.toolchangez_entry,
+            "excellon_toolchangexy": self.excellon_options_form.excellon_group.toolchangexy_entry,
+            "excellon_tooldia": self.excellon_options_form.excellon_group.tooldia_entry,
+            "excellon_ppname_e": self.excellon_options_form.excellon_group.pp_excellon_name_cb,
+            "excellon_startz": self.excellon_options_form.excellon_group.estartz_entry,
+            "excellon_endz": self.excellon_options_form.excellon_group.eendz_entry,
+            "excellon_format_upper_in": self.excellon_options_form.excellon_group.excellon_format_upper_in_entry,
+            "excellon_format_lower_in": self.excellon_options_form.excellon_group.excellon_format_lower_in_entry,
+            "excellon_format_upper_mm": self.excellon_options_form.excellon_group.excellon_format_upper_mm_entry,
+            "excellon_format_lower_mm": self.excellon_options_form.excellon_group.excellon_format_lower_mm_entry,
+            "excellon_zeros": self.excellon_options_form.excellon_group.excellon_zeros_radio,
+            "excellon_units": self.excellon_options_form.excellon_group.excellon_units_radio,
+            "excellon_optimization_type": self.excellon_options_form.excellon_group.excellon_optimization_radio,
+            "geometry_plot": self.geometry_options_form.geometry_group.plot_cb,
+            "geometry_cutz": self.geometry_options_form.geometry_group.cutz_entry,
+            "geometry_travelz": self.geometry_options_form.geometry_group.travelz_entry,
+            "geometry_feedrate": self.geometry_options_form.geometry_group.cncfeedrate_entry,
+            "geometry_feedrate_z": self.geometry_options_form.geometry_group.cncplunge_entry,
+            "geometry_feedrate_rapid": self.geometry_options_form.geometry_group.cncfeedrate_rapid_entry,
+            "geometry_spindlespeed": self.geometry_options_form.geometry_group.cncspindlespeed_entry,
+            "geometry_dwell": self.geometry_options_form.geometry_group.dwell_cb,
+            "geometry_dwelltime": self.geometry_options_form.geometry_group.dwelltime_entry,
+            "geometry_cnctooldia": self.geometry_options_form.geometry_group.cnctooldia_entry,
+            "geometry_painttooldia": self.geometry_options_form.geometry_group.painttooldia_entry,
+            "geometry_paintoverlap": self.geometry_options_form.geometry_group.paintoverlap_entry,
+            "geometry_paintmargin": self.geometry_options_form.geometry_group.paintmargin_entry,
+            "geometry_selectmethod": self.geometry_options_form.geometry_group.selectmethod_combo,
+            "geometry_ppname_g": self.geometry_options_form.geometry_group.pp_geometry_name_cb,
+            "geometry_toolchange": self.geometry_options_form.geometry_group.toolchange_cb,
+            "geometry_toolchangez": self.geometry_options_form.geometry_group.toolchangez_entry,
+            "geometry_toolchangexy": self.geometry_options_form.geometry_group.toolchangexy_entry,
+            "geometry_startz": self.geometry_options_form.geometry_group.gstartz_entry,
+            "geometry_endz": self.geometry_options_form.geometry_group.gendz_entry,
+            "geometry_depthperpass": self.geometry_options_form.geometry_group.depthperpass_entry,
+            "geometry_multidepth": self.geometry_options_form.geometry_group.multidepth_cb,
+            "geometry_extracut": self.geometry_options_form.geometry_group.extracut_cb,
+            "cncjob_plot": self.cncjob_options_form.cncjob_group.plot_cb,
+            "cncjob_tooldia": self.cncjob_options_form.cncjob_group.tooldia_entry,
+            "cncjob_prepend": self.cncjob_options_form.cncjob_group.prepend_text,
+            "cncjob_append": self.cncjob_options_form.cncjob_group.append_text
+        }
+
+        for name in list(self.postprocessors.keys()):
+            self.geometry_options_form.geometry_group.pp_geometry_name_cb.addItem(name)
+            self.excellon_options_form.excellon_group.pp_excellon_name_cb.addItem(name)
+
+        self.options = LoudDict()
+        self.options.set_change_callback(self.on_options_dict_change)
+        self.options.update({
+            "units": "IN",
+            "global_gridx": 1.0,
+            "global_gridy": 1.0,
+            "gerber_plot": True,
+            "gerber_solid": True,
+            "gerber_multicolored": False,
+            "gerber_isotooldia": 0.016,
+            "gerber_isopasses": 1,
+            "gerber_isooverlap": 0.15,
+            "gerber_ncctools": "1.0, 0.5",
+            "gerber_nccoverlap": 0.4,
+            "gerber_nccmargin": 1,
+            "gerber_combine_passes": True,
+            "gerber_cutouttooldia": 0.07,
+            "gerber_cutoutmargin": 0.1,
+            "gerber_cutoutgapsize": 0.15,
+            "gerber_gaps": "4",
+            "gerber_noncoppermargin": 0.0,
+            "gerber_noncopperrounded": False,
+            "gerber_bboxmargin": 0.0,
+            "gerber_bboxrounded": False,
+            "excellon_plot": True,
+            "excellon_solid": False,
+            "excellon_drillz": -0.1,
+            "excellon_travelz": 0.1,
+            "excellon_feedrate": 3.0,
+            "excellon_feedrate_rapid": 3.0,
+            "excellon_spindlespeed": None,
+            "excellon_dwell": True,
+            "excellon_dwelltime": 1000,
+            "excellon_toolchange": False,
+            "excellon_toolchangez": 1.0,
+            "excellon_toolchangexy": "0.0, 0.0",
+            "excellon_tooldia": 0.016,
+            "excellon_ppname_e": 'default',
+            "excellon_format_upper_in": 2,
+            "excellon_format_lower_in": 4,
+            "excellon_format_upper_mm": 3,
+            "excellon_format_lower_mm": 3,
+            "excellon_units": 'INCH',
+            "excellon_optimization_type": 'B',
+            "excellon_search_time": 3,
+            "excellon_startz": None,
+            "excellon_endz": 2.0,
+            "excellon_zeros": "L",
+            "geometry_plot": True,
+            "geometry_cutz": -0.002,
+            "geometry_travelz": 0.1,
+            "geometry_feedrate": 3.0,
+            "geometry_feedrate_z": 3.0,
+            "geometry_feedrate_rapid": 3.0,
+            "geometry_spindlespeed": None,
+            "geometry_dwell": True,
+            "geometry_dwelltime": 1000,
+            "geometry_cnctooldia": 0.016,
+            "geometry_painttooldia": 0.07,
+            "geometry_paintoverlap": 0.15,
+            "geometry_paintmargin": 0.0,
+            "geometry_selectmethod": "single",
+            "geometry_toolchange": False,
+            "geometry_toolchangez": 2.0,
+            "geometry_toolchangexy": "0.0, 0.0",
+            "geometry_startz": None,
+            "geometry_endz": 2.0,
+            "geometry_ppname_g": "default",
+            "geometry_depthperpass": 0.002,
+            "geometry_multidepth": False,
+            "geometry_extracut": False,
+            "cncjob_plot": True,
+            "cncjob_tooldia": 0.016,
+            "cncjob_prepend": "",
+            "cncjob_append": "",
+            "global_background_timeout": 300000,  # Default value is 5 minutes
+            "global_verbose_error_level": 0,  # Shell verbosity:
+                                       # 0 = default(python trace only for unknown errors),
+                                       # 1 = show trace(show trace allways), 2 = (For the future).
+        })
+
+        self.options.update(self.defaults)  # Copy app defaults to project options
+
+        self.gen_form = None
+        self.ger_form = None
+        self.exc_form = None
+        self.geo_form = None
+        self.cnc_form = None
+        self.on_options_combo_change(0)  # Will show the initial form
+
+        ### Define OBJECT COLLECTION ###
+        self.collection = ObjectCollection(self)
+        self.ui.project_tab_layout.addWidget(self.collection.view)
+        ###
+
+        self.log.debug("Finished creating Object Collection.")
+
+        ### Initialize the color box's color in Preferences -> Global -> Color
+        # Init Plot Colors
+        self.general_defaults_form.general_group.pf_color_entry.set_value(self.defaults['global_plot_fill'])
+        self.general_defaults_form.general_group.pf_color_button.setStyleSheet(
+            "background-color:%s" % str(self.defaults['global_plot_fill'])[:7])
+        self.general_defaults_form.general_group.pf_color_alpha_spinner.set_value(
+            int(self.defaults['global_plot_fill'][7:9], 16))
+        self.general_defaults_form.general_group.pf_color_alpha_slider.setValue(
+            int(self.defaults['global_plot_fill'][7:9], 16))
+
+        self.general_defaults_form.general_group.pl_color_entry.set_value(self.defaults['global_plot_line'])
+        self.general_defaults_form.general_group.pl_color_button.setStyleSheet(
+            "background-color:%s" % str(self.defaults['global_plot_line'])[:7])
+
+        # Init Left-Right Selection colors
+        self.general_defaults_form.general_group.sf_color_entry.set_value(self.defaults['global_sel_fill'])
+        self.general_defaults_form.general_group.sf_color_button.setStyleSheet(
+            "background-color:%s" % str(self.defaults['global_sel_fill'])[:7])
+        self.general_defaults_form.general_group.sf_color_alpha_spinner.set_value(
+            int(self.defaults['global_sel_fill'][7:9], 16))
+        self.general_defaults_form.general_group.sf_color_alpha_slider.setValue(
+            int(self.defaults['global_sel_fill'][7:9], 16))
+
+        self.general_defaults_form.general_group.sl_color_entry.set_value(self.defaults['global_sel_line'])
+        self.general_defaults_form.general_group.sl_color_button.setStyleSheet(
+            "background-color:%s" % str(self.defaults['global_sel_line'])[:7])
+
+        # Init Right-Left Selection colors
+        self.general_defaults_form.general_group.alt_sf_color_entry.set_value(self.defaults['global_alt_sel_fill'])
+        self.general_defaults_form.general_group.alt_sf_color_button.setStyleSheet(
+            "background-color:%s" % str(self.defaults['global_alt_sel_fill'])[:7])
+        self.general_defaults_form.general_group.alt_sf_color_alpha_spinner.set_value(
+            int(self.defaults['global_sel_fill'][7:9], 16))
+        self.general_defaults_form.general_group.alt_sf_color_alpha_slider.setValue(
+            int(self.defaults['global_sel_fill'][7:9], 16))
+
+        self.general_defaults_form.general_group.alt_sl_color_entry.set_value(self.defaults['global_alt_sel_line'])
+        self.general_defaults_form.general_group.alt_sl_color_button.setStyleSheet(
+            "background-color:%s" % str(self.defaults['global_alt_sel_line'])[:7])
+
+        # Init Draw color and Selection Draw Color
+        self.general_defaults_form.general_group.draw_color_entry.set_value(self.defaults['global_draw_color'])
+        self.general_defaults_form.general_group.draw_color_button.setStyleSheet(
+            "background-color:%s" % str(self.defaults['global_draw_color'])[:7])
+
+        self.general_defaults_form.general_group.sel_draw_color_entry.set_value(self.defaults['global_sel_draw_color'])
+        self.general_defaults_form.general_group.sel_draw_color_button.setStyleSheet(
+            "background-color:%s" % str(self.defaults['global_sel_draw_color'])[:7])
+        #### End of Data ####
+
+        #### Plot Area ####
+        start_plot_time = time.time()   # debug
+        self.plotcanvas = PlotCanvas(self.ui.right_layout, self)
+
+        self.plotcanvas.vis_connect('mouse_move', self.on_mouse_move_over_plot)
+        self.plotcanvas.vis_connect('mouse_press', self.on_mouse_click_over_plot)
+        self.plotcanvas.vis_connect('mouse_release', self.on_mouse_click_release_over_plot)
+        self.plotcanvas.vis_connect('mouse_double_click', self.on_double_click_over_plot)
+        # Keys over plot enabled
+        self.plotcanvas.vis_connect('key_press', self.on_key_over_plot)
+        self.plotcanvas.vis_connect('key_release', self.on_key_release_over_plot)
+
+        self.ui.splitter.setStretchFactor(1, 2)
+
+        # So it can receive key presses
+        self.plotcanvas.vispy_canvas.native.setFocus()
+
+        self.app_cursor = self.plotcanvas.new_cursor()
+        self.app_cursor.enabled = False
+
+        # to use for tools like Measurement tool who depends on the event sources who are changed inside the Editors
+        # depending on from where those tools are called different actions can be done
+        self.call_source = 'app'
+
+        end_plot_time = time.time()
+        self.log.debug("Finished Canvas initialization in %s seconds." % (str(end_plot_time - start_plot_time)))
+
+        ### EDITOR section
+        self.geo_editor = FlatCAMGeoEditor(self, disabled=True)
+        self.exc_editor = FlatCAMExcEditor(self)
+
+        # start with GRID activated
+        self.ui.grid_snap_btn.trigger()
+        self.ui.corner_snap_btn.setEnabled(False)
+        self.ui.snap_max_dist_entry.setEnabled(False)
+        self.ui.g_editor_cmenu.setEnabled(False)
+        self.ui.e_editor_cmenu.setEnabled(False)
+
+        #### Adjust tabs width ####
+        # self.collection.view.setMinimumWidth(self.ui.options_scroll_area.widget().sizeHint().width() +
+        #     self.ui.options_scroll_area.verticalScrollBar().sizeHint().width())
+        self.collection.view.setMinimumWidth(290)
+
+        self.log.debug("Finished adding Geometry and Excellon Editor's.")
+
+        #### Worker ####
+        self.workers = WorkerStack()
+        self.worker_task.connect(self.workers.add_task)
+
+
+        ### Signal handling ###
+        ## Custom signals
+        self.inform.connect(self.info)
+        self.message.connect(self.message_dialog)
+        self.progress.connect(self.set_progress_bar)
+        self.object_created.connect(self.on_object_created)
+        self.object_changed.connect(self.on_object_changed)
+        self.object_plotted.connect(self.on_object_plotted)
+        self.plots_updated.connect(self.on_plots_updated)
+        self.file_opened.connect(self.register_recent)
+        self.file_opened.connect(lambda kind, filename: self.register_folder(filename))
+        self.file_saved.connect(lambda kind, filename: self.register_save_folder(filename))
+
+
+        ## Standard signals
+        # Menu
+        self.ui.menufilenew.triggered.connect(self.on_file_new)
+        self.ui.menufileopengerber.triggered.connect(self.on_fileopengerber)
+        self.ui.menufileopengerber_follow.triggered.connect(self.on_fileopengerber_follow)
+        self.ui.menufileopenexcellon.triggered.connect(self.on_fileopenexcellon)
+        self.ui.menufileopengcode.triggered.connect(self.on_fileopengcode)
+        self.ui.menufileopenproject.triggered.connect(self.on_file_openproject)
+        self.ui.menufilerunscript.triggered.connect(self.on_filerunscript)
+
+        self.ui.menufileimportsvg.triggered.connect(lambda: self.on_file_importsvg("geometry"))
+        self.ui.menufileimportsvg_as_gerber.triggered.connect(lambda: self.on_file_importsvg("gerber"))
+
+        self.ui.menufileimportdxf.triggered.connect(lambda: self.on_file_importdxf("geometry"))
+        self.ui.menufileimportdxf_as_gerber.triggered.connect(lambda: self.on_file_importdxf("gerber"))
+
+        self.ui.menufileexportsvg.triggered.connect(self.on_file_exportsvg)
+        self.ui.menufileexportpng.triggered.connect(self.on_file_exportpng)
+        self.ui.menufileexportexcellon.triggered.connect(lambda: self.on_file_exportexcellon(altium_format=None))
+        self.ui.menufileexportexcellon_altium.triggered.connect(lambda: self.on_file_exportexcellon(altium_format=True))
+
+        self.ui.menufileexportdxf.triggered.connect(self.on_file_exportdxf)
+
+        self.ui.menufilesaveproject.triggered.connect(self.on_file_saveproject)
+        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.menufile_exit.triggered.connect(self.on_app_exit)
+
+        self.ui.menueditnew.triggered.connect(lambda: self.new_object('geometry', 'new_g', lambda x, y: None))
+        self.ui.menueditnewexc.triggered.connect(self.new_excellon_object)
+        self.ui.menueditedit.triggered.connect(self.object2editor)
+        self.ui.menueditok.triggered.connect(self.editor2object)
+
+        self.ui.menuedit_convertjoin.triggered.connect(self.on_edit_join)
+        self.ui.menuedit_convertjoinexc.triggered.connect(self.on_edit_join_exc)
+        self.ui.menuedit_convert_sg2mg.triggered.connect(self.on_convert_singlegeo_to_multigeo)
+        self.ui.menuedit_convert_mg2sg.triggered.connect(self.on_convert_multigeo_to_singlegeo)
+
+        self.ui.menueditdelete.triggered.connect(self.on_delete)
+
+        self.ui.menueditcopyobject.triggered.connect(self.on_copy_object)
+        self.ui.menueditcopyobjectasgeom.triggered.connect(self.on_copy_object_as_geometry)
+        self.ui.menueditorigin.triggered.connect(self.on_set_origin)
+        self.ui.menueditjump.triggered.connect(self.on_jump_to)
+
+        self.ui.menueditselectall.triggered.connect(self.on_selectall)
+        self.ui.menueditpreferences.triggered.connect(self.on_preferences)
+
+        # self.ui.menuoptions_transfer_a2o.triggered.connect(self.on_options_app2object)
+        # self.ui.menuoptions_transfer_a2p.triggered.connect(self.on_options_app2project)
+        # self.ui.menuoptions_transfer_o2a.triggered.connect(self.on_options_object2app)
+        # self.ui.menuoptions_transfer_p2a.triggered.connect(self.on_options_project2app)
+        # self.ui.menuoptions_transfer_o2p.triggered.connect(self.on_options_object2project)
+        # self.ui.menuoptions_transfer_p2o.triggered.connect(self.on_options_project2object)
+
+        self.ui.menuoptions_transform_rotate.triggered.connect(self.on_rotate)
+
+        self.ui.menuoptions_transform_skewx.triggered.connect(self.on_skewx)
+        self.ui.menuoptions_transform_skewy.triggered.connect(self.on_skewy)
+
+        self.ui.menuoptions_transform_flipx.triggered.connect(self.on_flipx)
+        self.ui.menuoptions_transform_flipy.triggered.connect(self.on_flipy)
+
+
+        self.ui.menuviewdisableall.triggered.connect(lambda: self.disable_plots(self.collection.get_list()))
+        self.ui.menuviewdisableother.triggered.connect(lambda: self.disable_plots(self.collection.get_non_selected()))
+        self.ui.menuviewenable.triggered.connect(lambda: self.enable_plots(self.collection.get_list()))
+        self.ui.menuview_zoom_fit.triggered.connect(self.on_zoom_fit)
+        self.ui.menuview_zoom_in.triggered.connect(lambda: self.plotcanvas.zoom(1 / 1.5))
+        self.ui.menuview_zoom_out.triggered.connect(lambda: self.plotcanvas.zoom(1.5))
+        self.ui.menuview_toggle_axis.triggered.connect(self.on_toggle_axis)
+        self.ui.menuview_toggle_workspace.triggered.connect(self.on_workspace_menu)
+
+        self.ui.menutoolshell.triggered.connect(self.on_toggle_shell)
+
+        self.ui.menuhelp_about.triggered.connect(self.on_about)
+        self.ui.menuhelp_home.triggered.connect(lambda: webbrowser.open(self.app_url))
+        self.ui.menuhelp_manual.triggered.connect(lambda: webbrowser.open(self.manual_url))
+        self.ui.menuhelp_videohelp.triggered.connect(lambda: webbrowser.open(self.video_url))
+        self.ui.menuhelp_shortcut_list.triggered.connect(self.on_shortcut_list)
+
+        self.ui.menuprojectenable.triggered.connect(lambda: self.enable_plots(self.collection.get_selected()))
+        self.ui.menuprojectdisable.triggered.connect(lambda: self.disable_plots(self.collection.get_selected()))
+        self.ui.menuprojectgeneratecnc.triggered.connect(lambda: self.generate_cnc_job(self.collection.get_selected()))
+        self.ui.menuprojectdelete.triggered.connect(self.on_delete)
+
+        # Toolbar
+        #self.ui.file_new_btn.triggered.connect(self.on_file_new)
+        self.ui.file_open_btn.triggered.connect(self.on_file_openproject)
+        self.ui.file_save_btn.triggered.connect(self.on_file_saveproject)
+        self.ui.file_open_gerber_btn.triggered.connect(self.on_fileopengerber)
+        self.ui.file_open_excellon_btn.triggered.connect(self.on_fileopenexcellon)
+
+        self.ui.clear_plot_btn.triggered.connect(self.clear_plots)
+        self.ui.replot_btn.triggered.connect(self.plot_all)
+        self.ui.zoom_fit_btn.triggered.connect(self.on_zoom_fit)
+        self.ui.zoom_in_btn.triggered.connect(lambda: self.plotcanvas.zoom(1 / 1.5))
+        self.ui.zoom_out_btn.triggered.connect(lambda: self.plotcanvas.zoom(1.5))
+
+        self.ui.newgeo_btn.triggered.connect(lambda: self.new_object('geometry', 'new_g', lambda x, y: None))
+        self.ui.newexc_btn.triggered.connect(self.new_excellon_object)
+        self.ui.editgeo_btn.triggered.connect(self.object2editor)
+        self.ui.update_obj_btn.triggered.connect(self.editor2object)
+        self.ui.delete_btn.triggered.connect(self.on_delete)
+        self.ui.shell_btn.triggered.connect(self.on_toggle_shell)
+
+        # Context Menu
+        self.ui.gridmenu_1.triggered.connect(lambda: self.ui.grid_gap_x_entry.setText("0.05"))
+        self.ui.gridmenu_2.triggered.connect(lambda: self.ui.grid_gap_x_entry.setText("0.1"))
+        self.ui.gridmenu_3.triggered.connect(lambda: self.ui.grid_gap_x_entry.setText("0.2"))
+        self.ui.gridmenu_4.triggered.connect(lambda: self.ui.grid_gap_x_entry.setText("0.5"))
+        self.ui.gridmenu_5.triggered.connect(lambda: self.ui.grid_gap_x_entry.setText("1.0"))
+        self.ui.draw_line.triggered.connect(self.geo_editor.draw_tool_path)
+        self.ui.draw_rect.triggered.connect(self.geo_editor.draw_tool_rectangle)
+        self.ui.draw_cut.triggered.connect(self.geo_editor.cutpath)
+        self.ui.drill.triggered.connect(self.exc_editor.exc_add_drill)
+        self.ui.drill_array.triggered.connect(self.exc_editor.exc_add_drill_array)
+        self.ui.drill_copy.triggered.connect(self.exc_editor.exc_copy_drills)
+
+
+        self.ui.zoomfit.triggered.connect(self.on_zoom_fit)
+        self.ui.clearplot.triggered.connect(self.clear_plots)
+        self.ui.replot.triggered.connect(self.plot_all)
+        self.ui.popmenu_properties.triggered.connect(self.obj_properties)
+
+        # Preferences Plot Area TAB
+        self.ui.options_combo.activated.connect(self.on_options_combo_change)
+        self.ui.pref_save_button.clicked.connect(self.on_save_button)
+        self.ui.pref_factory_button.clicked.connect(self.load_factory_defaults)
+        self.ui.pref_load_button.clicked.connect(self.load_user_defaults)
+        self.general_options_form.general_group.units_radio.group_toggle_fn = self.on_toggle_units
+        # Setting plot colors signals
+        self.general_defaults_form.general_group.pf_color_entry.editingFinished.connect(self.on_pf_color_entry)
+        self.general_defaults_form.general_group.pf_color_button.clicked.connect(self.on_pf_color_button)
+        self.general_defaults_form.general_group.pf_color_alpha_spinner.valueChanged.connect(self.on_pf_color_spinner)
+        self.general_defaults_form.general_group.pf_color_alpha_slider.valueChanged.connect(self.on_pf_color_slider)
+        self.general_defaults_form.general_group.pl_color_entry.editingFinished.connect(self.on_pl_color_entry)
+        self.general_defaults_form.general_group.pl_color_button.clicked.connect(self.on_pl_color_button)
+        # Setting selection (left - right) colors signals
+        self.general_defaults_form.general_group.sf_color_entry.editingFinished.connect(self.on_sf_color_entry)
+        self.general_defaults_form.general_group.sf_color_button.clicked.connect(self.on_sf_color_button)
+        self.general_defaults_form.general_group.sf_color_alpha_spinner.valueChanged.connect(self.on_sf_color_spinner)
+        self.general_defaults_form.general_group.sf_color_alpha_slider.valueChanged.connect(self.on_sf_color_slider)
+        self.general_defaults_form.general_group.sl_color_entry.editingFinished.connect(self.on_sl_color_entry)
+        self.general_defaults_form.general_group.sl_color_button.clicked.connect(self.on_sl_color_button)
+        # Setting selection (right - left) colors signals
+        self.general_defaults_form.general_group.alt_sf_color_entry.editingFinished.connect(self.on_alt_sf_color_entry)
+        self.general_defaults_form.general_group.alt_sf_color_button.clicked.connect(self.on_alt_sf_color_button)
+        self.general_defaults_form.general_group.alt_sf_color_alpha_spinner.valueChanged.connect(
+            self.on_alt_sf_color_spinner)
+        self.general_defaults_form.general_group.alt_sf_color_alpha_slider.valueChanged.connect(
+            self.on_alt_sf_color_slider)
+        self.general_defaults_form.general_group.alt_sl_color_entry.editingFinished.connect(self.on_alt_sl_color_entry)
+        self.general_defaults_form.general_group.alt_sl_color_button.clicked.connect(self.on_alt_sl_color_button)
+        # Setting Editor Draw colors signals
+        self.general_defaults_form.general_group.draw_color_entry.editingFinished.connect(self.on_draw_color_entry)
+        self.general_defaults_form.general_group.draw_color_button.clicked.connect(self.on_draw_color_button)
+
+        self.general_defaults_form.general_group.sel_draw_color_entry.editingFinished.connect(self.on_sel_draw_color_entry)
+        self.general_defaults_form.general_group.sel_draw_color_button.clicked.connect(self.on_sel_draw_color_button)
+
+        self.general_defaults_form.general_group.wk_cb.currentIndexChanged.connect(self.on_workspace_modified)
+        self.general_defaults_form.general_group.workspace_cb.stateChanged.connect(self.on_workspace)
+
+
+        # Modify G-CODE Plot Area TAB
+        self.ui.code_editor.textChanged.connect(self.handleTextChanged)
+        self.ui.buttonOpen.clicked.connect(self.handleOpen)
+        self.ui.buttonPrint.clicked.connect(self.handlePrint)
+        self.ui.buttonPreview.clicked.connect(self.handlePreview)
+        self.ui.buttonSave.clicked.connect(self.handleSaveGCode)
+        self.ui.buttonFind.clicked.connect(self.handleFindGCode)
+        self.ui.buttonReplace.clicked.connect(self.handleReplaceGCode)
+
+        # Object list
+        self.collection.view.activated.connect(self.on_row_activated)
+
+        # Monitor the checkbox from the Application Defaults Tab and show the TCL shell or not depending on it's value
+        self.general_defaults_form.general_group.shell_startup_cb.clicked.connect(self.on_toggle_shell)
+
+        # Load the defaults values into the Excellon Format and Excellon Zeros fields
+        self.excellon_defaults_form.excellon_group.excellon_defaults_button.clicked.connect(
+            self.on_excellon_defaults_button)
+
+        # Load the defaults values into the Excellon Format and Excellon Zeros fields
+        self.excellon_options_form.excellon_group.excellon_defaults_button.clicked.connect(
+            self.on_excellon_options_button)
+
+        # this is a flag to signal to other tools that the ui tooltab is locked and not accessible
+        self.tool_tab_locked = False
+
+        ####################
+        ### Other setups ###
+        ####################
+        # Sets up FlatCAMObj, FCProcess and FCProcessContainer.
+        self.setup_obj_classes()
+
+        self.setup_recent_items()
+        self.setup_component_editor()
+
+        #############
+        ### Shell ###
+        #############
+
+        ###
+        # Auto-complete KEYWORDS
+        self.tcl_commands_list = ['add_circle', 'add_poly', 'add_polygon', 'add_polyline', 'add_rectangle',
+                                  'aligndrill', 'clear',
+                                  'aligndrillgrid', 'cncjob', 'cutout', 'delete', 'drillcncjob', 'export_gcode',
+                                  'export_svg', 'ext', 'exteriors', 'follow', 'geo_union', 'geocutout', 'get_names',
+                                  'get_sys', 'getsys', 'help', 'import_svg', 'interiors', 'isolate', 'join_excellon',
+                                  'join_excellons', 'join_geometries', 'join_geometry', 'list_sys', 'listsys', 'mill',
+                                  'millholes', 'mirror', 'new', 'new_geometry', 'offset', 'open_excellon', 'open_gcode',
+                                  'open_gerber', 'open_project', 'options', 'paint', 'pan', 'panel', 'panelize', 'plot',
+                                  'save', 'save_project', 'save_sys', 'scale', 'set_active', 'set_sys', 'setsys',
+                                  'skew', 'subtract_poly', 'subtract_rectangle', 'version', 'write_gcode'
+                              ]
+
+        self.ordinary_keywords = ['name', 'center_x', 'center_y', 'radius', 'x0', 'y0', 'x1', 'y1', 'box', 'axis',
+                                  'holes','grid', 'minoffset', 'gridoffset','axisoffset', 'dia', 'dist', 'gridoffsetx',
+                                  'gridoffsety', 'columns', 'rows', 'z_cut', 'z_move', 'feedrate', 'feedrate_rapid',
+                                  'tooldia', 'multidepth', 'extracut', 'depthperpass', 'ppname_g', 'outname', 'margin',
+                                  'gaps', 'gapsize', 'tools', 'drillz', 'travelz', 'spindlespeed', 'toolchange',
+                                  'toolchangez', 'endz', 'ppname_e', 'opt_type', 'preamble', 'postamble', 'filename',
+                                  'scale_factor', 'type', 'passes', 'overlap', 'combine', 'use_threads', 'x', 'y',
+                                  'follow', 'all', 'spacing_columns', 'spacing_rows', 'factor', 'value', 'angle_x',
+                                  'angle_y', 'gridx', 'gridy', 'True', 'False'
+                             ]
+
+        self.myKeywords = self.tcl_commands_list + self.ordinary_keywords
+
+        self.shell = FCShell(self)
+        self.shell._edit.set_model_data(self.myKeywords)
+        self.shell.setWindowIcon(self.ui.app_icon)
+        self.shell.setWindowTitle("FlatCAM Shell")
+        self.shell.resize(*self.defaults["global_shell_shape"])
+        self.shell.append_output("FlatCAM %s\n(c) 2014-2019 Juan Pablo Caram\n\n" % self.version)
+        self.shell.append_output("Type help to get started.\n\n")
+
+        self.init_tcl()
+
+        self.ui.shell_dock = QtWidgets.QDockWidget("FlatCAM TCL Shell")
+        self.ui.shell_dock.setWidget(self.shell)
+        self.ui.shell_dock.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas)
+        self.ui.shell_dock.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable |
+                                       QtWidgets.QDockWidget.DockWidgetFloatable |
+                                       QtWidgets.QDockWidget.DockWidgetClosable)
+        self.ui.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.ui.shell_dock)
+
+        # show TCL shell at start-up based on the Menu -? Edit -> Preferences setting.
+        if self.defaults["global_shell_at_startup"]:
+            self.ui.shell_dock.show()
+        else:
+            self.ui.shell_dock.hide()
+
+        #########################
+        ### Tools and Plugins ###
+        #########################
+
+        # always install tools only after the shell is initialized because the self.inform.emit() depends on shell
+        self.install_tools()
+
+        ### System Font Parsing ###
+        self.f_parse = ParseFont()
+        self.parse_system_fonts()
+
+        # test if the program was started with a script as parameter
+        if self.cmd_line_shellfile:
+            try:
+                with open(self.cmd_line_shellfile, "r") as myfile:
+                    cmd_line_shellfile_text = myfile.read()
+                    self.shell._sysShell.exec_command(cmd_line_shellfile_text)
+            except Exception as ext:
+                print("ERROR: ", ext)
+                sys.exit(2)
+
+        # coordinates for relative position display
+        self.rel_point1 = (0, 0)
+        self.rel_point2 = (0, 0)
+
+        # variable to store coordinates
+        self.pos = (0, 0)
+        self.pos_jump = (0, 0)
+
+        # variable to store if there was motion before right mouse button click (panning)
+        self.panning_action = False
+        # variable to store if a command is active (then the var is not None) and which one it is
+        self.command_active = None
+        # variable to store the status of moving selection action
+        # None value means that it's not an selection action
+        # True value = a selection from left to right
+        # False value = a selection from right to left
+        self.selection_type = None
+
+        # List to store the objects that are currently loaded in FlatCAM
+        # This list is updated on each object creation or object delete
+        self.all_objects_list = []
+
+        # List to store the objects that are selected
+        self.sel_objects_list = []
+
+        # holds the key modifier if pressed (CTRL, SHIFT or ALT)
+        self.key_modifiers = None
+
+        # Variable to hold the status of the axis
+        self.toggle_axis = True
+
+        self.cursor = None
+
+        # Variable to store the GCODE that was edited
+        self.gcode_edited = ""
+
+        self.grb_list = ['gbr', 'ger', 'gtl', 'gbl', 'gts', 'gbs', 'gtp', 'gbp', 'gto', 'gbo', 'gm1', 'gm2', 'gm3', 'gko',
+                    'cmp', 'sol', 'stc', 'sts', 'plc', 'pls', 'crc', 'crs', 'tsm', 'bsm', 'ly2', 'ly15', 'dim', 'mil',
+                    'grb', 'top', 'bot', 'smt', 'smb', 'sst', 'ssb', 'spt', 'spb', 'pho', 'gdo', 'art', 'gbd']
+        self.exc_list = ['drl', 'txt', 'xln', 'drd', 'tap', 'exc']
+        self.gcode_list = ['nc', 'ncc', 'tap', 'gcode', 'cnc', 'ecs', 'fnc', 'dnc', 'ncg', 'gc', 'fan', 'fgc', 'din',
+                      'xpi', 'hnc', 'h', 'i', 'ncp', 'min', 'gcd', 'rol', 'mpr', 'ply', 'out', 'eia', 'plt', 'sbp',
+                      'mpf']
+        self.svg_list = ['svg']
+        self.dxf_list = ['dxf']
+        self.prj_list = ['flatprj']
+
+        # global variable used by NCC Tool to signal that some polygons could not be cleared, if True
+        # flag for polygons not cleared
+        self.poly_not_cleared = False
+
+        ### Save defaults to factory_defaults.json file ###
+        ### It's done only once after install #############
+        factory_file = open(self.data_path + '/factory_defaults.json')
+        fac_def_from_file = factory_file.read()
+        factory_defaults = json.loads(fac_def_from_file)
+
+        # if the file contain an empty dictionary then save the factory defaults into the file
+        if not factory_defaults:
+            self.save_factory_defaults(silent=False)
+        factory_file.close()
+
+        # Post-GUI initialization: Experimental attempt
+        # to perform unit tests on the GUI.
+        # if post_gui is not None:
+        #     post_gui(self)
+
+        App.log.debug("END of constructor. Releasing control.")
+
+        # accept a project file as command line parameter
+        # the path/file_name must be enclosed in quotes if it contain spaces
+        for argument in App.args:
+            if '.FlatPrj' in argument:
+                try:
+                    project_name = str(argument)
+
+                    if project_name == "":
+                        self.inform.emit("Open cancelled.")
+                    else:
+                        # self.open_project(project_name)
+                        run_from_arg = True
+                        self.worker_task.emit({'fcn': self.open_project,
+                                               'params': [project_name, run_from_arg]})
+                except Exception as e:
+                    log.debug("Could not open FlatCAM project file as App parameter due: %s" % str(e))
+
+
+    def defaults_read_form(self):
+        for option in self.defaults_form_fields:
+            try:
+                self.defaults[option] = self.defaults_form_fields[option].get_value()
+            except:
+                pass
+
+    def defaults_write_form(self):
+        for option in self.defaults:
+            self.defaults_write_form_field(option)
+            # try:
+            #     self.defaults_form_fields[option].set_value(self.defaults[option])
+            # except KeyError:
+            #     #self.log.debug("defaults_write_form(): No field for: %s" % option)
+            #     # TODO: Rethink this?
+            #     pass
+
+    def defaults_write_form_field(self, field):
+        try:
+            self.defaults_form_fields[field].set_value(self.defaults[field])
+        except KeyError:
+            #self.log.debug("defaults_write_form(): No field for: %s" % option)
+            # TODO: Rethink this?
+            pass
+
+    def clear_pool(self):
+        self.pool.close()
+
+        self.pool = Pool()
+        self.pool_recreated.emit(self.pool)
+
+        gc.collect()
+
+    # the order that the tools are installed is important as they can depend on each other install position
+    def install_tools(self):
+        self.dblsidedtool = DblSidedTool(self)
+        self.dblsidedtool.install(icon=QtGui.QIcon('share/doubleside16.png'), separator=True)
+
+        self.measurement_tool = Measurement(self)
+        self.measurement_tool.install(icon=QtGui.QIcon('share/measure16.png'), separator=True)
+
+        self.panelize_tool = Panelize(self)
+        self.panelize_tool.install(icon=QtGui.QIcon('share/panel16.png'))
+
+        self.film_tool = Film(self)
+        self.film_tool.install(icon=QtGui.QIcon('share/film16.png'), separator=True)
+
+        self.move_tool = ToolMove(self)
+        self.move_tool.install(icon=QtGui.QIcon('share/move16.png'), pos=self.ui.menuedit,
+                               before=self.ui.menueditorigin)
+
+        self.cutout_tool = ToolCutout(self)
+        self.cutout_tool.install(icon=QtGui.QIcon('share/cut16.png'), pos=self.ui.menutool,
+                                 before=self.measurement_tool.menuAction)
+
+        self.ncclear_tool = NonCopperClear(self)
+        self.ncclear_tool.install(icon=QtGui.QIcon('share/flatcam_icon16.png'), pos=self.ui.menutool,
+                                 before=self.measurement_tool.menuAction, separator=True)
+
+        self.paint_tool = ToolPaint(self)
+        self.paint_tool.install(icon=QtGui.QIcon('share/paint16.png'), pos=self.ui.menutool,
+                                  before=self.measurement_tool.menuAction, separator=True)
+
+        self.calculator_tool = ToolCalculator(self)
+        self.calculator_tool.install(icon=QtGui.QIcon('share/calculator24.png'))
+
+        self.transform_tool = ToolTransform(self)
+        self.transform_tool.install(icon=QtGui.QIcon('share/transform.png'), pos=self.ui.menuoptions, separator=True)
+
+        self.properties_tool = Properties(self)
+        self.properties_tool.install(icon=QtGui.QIcon('share/properties32.png'), pos=self.ui.menuoptions)
+
+        self.image_tool = ToolImage(self)
+        self.image_tool.install(icon=QtGui.QIcon('share/image32.png'), pos=self.ui.menufileimport,
+                                separator=True)
+
+        self.log.debug("Tools are installed.")
+
+    def init_tools(self):
+
+        # delete the data currently in the Tools Tab and the Tab itself
+        widget = QtWidgets.QTabWidget.widget(self.ui.notebook, 2)
+        if widget is not None:
+            widget.deleteLater()
+        self.ui.notebook.removeTab(2)
+
+        # rebuild the Tools Tab
+        self.ui.tool_tab = QtWidgets.QWidget()
+        self.ui.tool_tab_layout = QtWidgets.QVBoxLayout(self.ui.tool_tab)
+        self.ui.tool_tab_layout.setContentsMargins(2, 2, 2, 2)
+        self.ui.notebook.addTab(self.ui.tool_tab, "Tool")
+        self.ui.tool_scroll_area = VerticalScrollArea()
+        self.ui.tool_tab_layout.addWidget(self.ui.tool_scroll_area)
+
+        # reinstall all the Tools as some may have been removed when the data was removed from the Tools Tab
+        self.install_tools()
+        self.log.debug("Tools are initialized.")
+
+    def parse_system_fonts(self):
+        self.worker_task.emit({'fcn': self.f_parse.get_fonts_by_types,
+                               'params': []})
+
+    def object2editor(self):
+        """
+        Send the current Geometry or Excellon object (if any) into the editor.
+
+        :return: None
+        """
+
+        if isinstance(self.collection.get_active(), FlatCAMGeometry):
+            edited_object = self.collection.get_active()
+            # for now, if the Geometry is MultiGeo do not allow the editing
+            if edited_object.multigeo is True:
+                self.inform.emit("[warning_notcl]Editing a MultiGeo Geometry is not possible for the moment.")
+                return
+            self.ui.update_obj_btn.setEnabled(True)
+            self.geo_editor.edit_fcgeometry(edited_object)
+            self.ui.g_editor_cmenu.setEnabled(True)
+            # set call source to the Editor we go into
+            self.call_source = 'geo_editor'
+
+            # prevent the user to change anything in the Selected Tab while the Geo Editor is active
+            sel_tab_widget_list = self.ui.selected_tab.findChildren(QtWidgets.QWidget)
+            for w in sel_tab_widget_list:
+                w.setEnabled(False)
+        elif isinstance(self.collection.get_active(), FlatCAMExcellon):
+            self.ui.update_obj_btn.setEnabled(True)
+            self.exc_editor.edit_exc_obj(self.collection.get_active())
+            self.ui.e_editor_cmenu.setEnabled(True)
+            # set call source to the Editor we go into
+            self.call_source = 'exc_editor'
+        else:
+            self.inform.emit("[warning_notcl]Select a Geometry or Excellon Object to edit.")
+            return
+
+        # make sure that we can't select another object while in Editor Mode:
+        self.collection.view.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
+
+        # delete any selection shape that might be active as they are not relevant in Editor
+        self.delete_selection_shape()
+
+
+        self.ui.plot_tab_area.setTabText(0, "EDITOR Area")
+        self.inform.emit("[warning_notcl]Editor is activated ...")
+
+    def editor2object(self):
+        """
+        Transfers the Geometry or Excellon from the editor to the current object.
+
+        :return: None
+        """
+        edited_obj = self.collection.get_active()
+        obj_type = ""
+
+        if isinstance(edited_obj, FlatCAMGeometry):
+            obj_type = "Geometry"
+            self.geo_editor.update_fcgeometry(edited_obj)
+            self.geo_editor.update_options(edited_obj)
+
+            self.geo_editor.deactivate()
+
+            # edited_obj.on_tool_delete(all=True)
+            # edited_obj.on_tool_add(dia=edited_obj.options['cnctooldia'])
+
+            self.ui.corner_snap_btn.setEnabled(False)
+            self.ui.update_obj_btn.setEnabled(False)
+            self.ui.g_editor_cmenu.setEnabled(False)
+            self.ui.e_editor_cmenu.setEnabled(False)
+
+            # update the geo object options so it is including the bounding box values
+            try:
+                xmin, ymin, xmax, ymax = edited_obj.bounds()
+                edited_obj.options['xmin'] = xmin
+                edited_obj.options['ymin'] = ymin
+                edited_obj.options['xmax'] = xmax
+                edited_obj.options['ymax'] = ymax
+            except AttributeError:
+                self.inform.emit("[warning] Object empty after edit.")
+
+        elif isinstance(edited_obj, FlatCAMExcellon):
+            obj_type = "Excellon"
+
+            self.exc_editor.update_exc_obj(edited_obj)
+
+            self.exc_editor.deactivate()
+            self.ui.corner_snap_btn.setEnabled(False)
+            self.ui.update_obj_btn.setEnabled(False)
+            self.ui.g_editor_cmenu.setEnabled(False)
+            self.ui.e_editor_cmenu.setEnabled(False)
+
+        else:
+            self.inform.emit("[warning_notcl]Select a Geometry or Excellon Object to update.")
+            return
+
+        # restore the call_source to app
+        self.call_source = 'app'
+
+        edited_obj.plot()
+        self.ui.plot_tab_area.setTabText(0, "Plot Area")
+        self.inform.emit("[success] %s is updated, returning to App..." % obj_type)
+
+        # reset the Object UI to original settings
+        # edited_obj.set_ui(edited_obj.ui_type())
+        # edited_obj.build_ui()
+        # make sure that we reenable the selection on Project Tab after returning from Editor Mode:
+        self.collection.view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+
+
+    def get_last_folder(self):
+        return self.defaults["global_last_folder"]
+
+    def get_last_save_folder(self):
+        return self.defaults["global_last_save_folder"]
+
+    def report_usage(self, resource):
+        """
+        Increments usage counter for the given resource
+        in self.defaults['global_stats'].
+
+        :param resource: Name of the resource.
+        :return: None
+        """
+
+        if resource in self.defaults['global_stats']:
+            self.defaults['global_stats'][resource] += 1
+        else:
+            self.defaults['global_stats'][resource] = 1
+
+    def init_tcl(self):
+        if hasattr(self,'tcl'):
+            # self.tcl = None
+            # TODO  we need  to clean  non default variables and procedures here
+            # new object cannot be used here as it  will not remember values created for next passes,
+            # because tcl  was execudted in old instance of TCL
+            pass
+        else:
+            self.tcl = tk.Tcl()
+            self.setup_shell()
+        self.log.debug("TCL Shell has been initialized.")
+
+    # TODO: This shouldn't be here.
+    class TclErrorException(Exception):
+        """
+        this exception is deffined here, to be able catch it if we sucessfully handle all errors from shell command
+        """
+        pass
+
+    def shell_message(self, msg, show=False, error=False):
+        """
+        Shows a message on the FlatCAM Shell
+
+        :param msg: Message to display.
+        :param show: Opens the shell.
+        :param error: Shows the message as an error.
+        :return: None
+        """
+        if show:
+            self.ui.shell_dock.show()
+        try:
+            if error:
+                self.shell.append_error(msg + "\n")
+            else:
+                self.shell.append_output(msg + "\n")
+        except AttributeError:
+            log.debug("shell_message() is called before Shell Class is instantiated. The message is: %s", str(msg))
+
+    def raise_tcl_unknown_error(self, unknownException):
+        """
+        Raise exception if is different type than TclErrorException
+        this is here mainly to show unknown errors inside TCL shell console.
+
+        :param unknownException:
+        :return:
+        """
+
+        if not isinstance(unknownException, self.TclErrorException):
+            self.raise_tcl_error("Unknown error: %s" % str(unknownException))
+        else:
+            raise unknownException
+
+    def display_tcl_error(self, error, error_info=None):
+        """
+        escape bracket [ with \  otherwise there is error
+        "ERROR: missing close-bracket" instead of real error
+        :param error: it may be text  or exception
+        :return: None
+        """
+
+        if isinstance(error, Exception):
+
+            exc_type, exc_value, exc_traceback = error_info
+            if not isinstance(error, self.TclErrorException):
+                show_trace = 1
+            else:
+                show_trace = int(self.defaults['global_verbose_error_level'])
+
+            if show_trace > 0:
+                trc = traceback.format_list(traceback.extract_tb(exc_traceback))
+                trc_formated = []
+                for a in reversed(trc):
+                    trc_formated.append(a.replace("    ", " > ").replace("\n", ""))
+                text = "%s\nPython traceback: %s\n%s" % (exc_value,
+                                 exc_type,
+                                 "\n".join(trc_formated))
+
+            else:
+                text = "%s" % error
+        else:
+            text = error
+
+        text = text.replace('[', '\\[').replace('"', '\\"')
+
+        self.tcl.eval('return -code error "%s"' % text)
+
+    def raise_tcl_error(self, text):
+        """
+        this method  pass exception from python into TCL as error, so we get stacktrace and reason
+        :param text: text of error
+        :return: raise exception
+        """
+
+        self.display_tcl_error(text)
+        raise self.TclErrorException(text)
+
+    def exec_command(self, text):
+        """
+        Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
+        Also handles execution in separated threads
+
+        :param text:
+        :return: output if there was any
+        """
+
+        self.report_usage('exec_command')
+
+        result = self.exec_command_test(text, False)
+
+        #MS: added this method call so the geometry is updated once the TCL
+        #command is executed
+        self.plot_all()
+
+        return result
+
+    def exec_command_test(self, text, reraise=True):
+        """
+        Same as exec_command(...) with additional control over  exceptions.
+        Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
+
+        :param text: Input command
+        :param reraise: Re-raise TclError exceptions in Python (mostly for unitttests).
+        :return: Output from the command
+        """
+
+        text = str(text)
+
+        try:
+            self.shell.open_proccessing()  # Disables input box.
+            result = self.tcl.eval(str(text))
+            if result != 'None':
+                self.shell.append_output(result + '\n')
+
+        except tk.TclError as e:
+            # This will display more precise answer if something in TCL shell fails
+            result = self.tcl.eval("set errorInfo")
+            self.log.error("Exec command Exception: %s" % (result + '\n'))
+            self.shell.append_error('ERROR: ' + result + '\n')
+            # Show error in console and just return or in test raise exception
+            if reraise:
+                raise e
+
+        finally:
+            self.shell.close_proccessing()
+            pass
+        return result
+
+        # """
+        # Code below is unsused. Saved for later.
+        # """
+
+        # parts = re.findall(r'([\w\\:\.]+|".*?")+', text)
+        # parts = [p.replace('\n', '').replace('"', '') for p in parts]
+        # self.log.debug(parts)
+        # try:
+        #     if parts[0] not in commands:
+        #         self.shell.append_error("Unknown command\n")
+        #         return
+        #
+        #     #import inspect
+        #     #inspect.getargspec(someMethod)
+        #     if (type(commands[parts[0]]["params"]) is not list and len(parts)-1 != commands[parts[0]]["params"]) or \
+        #             (type(commands[parts[0]]["params"]) is list and len(parts)-1 not in commands[parts[0]]["params"]):
+        #         self.shell.append_error(
+        #             "Command %s takes %d arguments. %d given.\n" %
+        #             (parts[0], commands[parts[0]]["params"], len(parts)-1)
+        #         )
+        #         return
+        #
+        #     cmdfcn = commands[parts[0]]["fcn"]
+        #     cmdconv = commands[parts[0]]["converters"]
+        #     if len(parts) - 1 > 0:
+        #         retval = cmdfcn(*[cmdconv[i](parts[i + 1]) for i in range(len(parts)-1)])
+        #     else:
+        #         retval = cmdfcn()
+        #     retfcn = commands[parts[0]]["retfcn"]
+        #     if retval and retfcn(retval):
+        #         self.shell.append_output(retfcn(retval) + "\n")
+        #
+        # except Exception as e:
+        #     #self.shell.append_error(''.join(traceback.format_exc()))
+        #     #self.shell.append_error("?\n")
+        #     self.shell.append_error(str(e) + "\n")
+
+    def info(self, msg):
+        """
+        Informs the user. Normally on the status bar, optionally
+        also on the shell.
+
+        :param msg: Text to write.
+        :return: None
+        """
+
+        # Type of message in brackets at the begining of the message.
+        match = re.search("\[([^\]]+)\](.*)", msg)
+        if match:
+            level = match.group(1)
+            msg_ = match.group(2)
+            self.ui.fcinfo.set_status(str(msg_), level=level)
+
+            if level == "error" or level == "warning":
+                self.shell_message(msg, error=True, show=True)
+            elif level == "error_notcl" or level == "warning_notcl":
+                self.shell_message(msg, error=True, show=False)
+            else:
+                self.shell_message(msg, error=False, show=False)
+        else:
+            self.ui.fcinfo.set_status(str(msg), level="info")
+
+            # make sure that if the message is to clear the infobar with a space
+            # is not printed over and over on the shell
+            if msg != '':
+                self.shell_message(msg)
+
+    def load_defaults(self):
+        """
+        Loads the aplication's default settings from defaults.json into
+        ``self.defaults``.
+
+        :return: None
+        """
+        try:
+            f = open(self.data_path + "/defaults.json")
+            options = f.read()
+            f.close()
+        except IOError:
+            self.log.error("Could not load defaults file.")
+            self.inform.emit("[error] Could not load defaults file.")
+            # in case the defaults file can't be loaded, show all toolbars
+            self.defaults["global_toolbar_view"] = 31
+            return
+
+        try:
+            defaults = json.loads(options)
+        except:
+            # in case the defaults file can't be loaded, show all toolbars
+            self.defaults["global_toolbar_view"] = 31
+            e = sys.exc_info()[0]
+            App.log.error(str(e))
+            self.inform.emit("[error] Failed to parse defaults file.")
+            return
+        self.defaults.update(defaults)
+
+        # restore the toolbar view
+        tb = self.defaults["global_toolbar_view"]
+        if tb & 1:
+            self.ui.toolbarfile.setVisible(True)
+        else:
+            self.ui.toolbarfile.setVisible(False)
+
+        if tb & 2:
+            self.ui.toolbargeo.setVisible(True)
+        else:
+            self.ui.toolbargeo.setVisible(False)
+
+        if tb & 4:
+            self.ui.toolbarview.setVisible(True)
+        else:
+            self.ui.toolbarview.setVisible(False)
+
+        if tb & 8:
+            self.ui.toolbartools.setVisible(True)
+        else:
+            self.ui.toolbartools.setVisible(False)
+
+        if tb & 16:
+            self.ui.snap_toolbar.setVisible(True)
+        else:
+            self.ui.snap_toolbar.setVisible(False)
+
+    def load_factory_defaults(self):
+        """
+        Loads the aplication's factory default settings from factory_defaults.json into
+        ``self.defaults``.
+
+        :return: None
+        """
+        try:
+            f = open(self.data_path + "/factory_defaults.json")
+            options = f.read()
+            f.close()
+        except IOError:
+            self.log.error("Could not load factory defaults file.")
+            self.inform.emit("[error] Could not load factory defaults file.")
+            return
+
+        try:
+            factory_defaults = json.loads(options)
+        except:
+            e = sys.exc_info()[0]
+            App.log.error(str(e))
+            self.inform.emit("[error] Failed to parse factory defaults file.")
+            return
+        self.defaults.update(factory_defaults)
+        self.inform.emit("[success] Imported Factory Defaults ...")
+
+    def load_user_defaults(self):
+        self.load_defaults()
+        self.inform.emit("[success] Loaded User Defaults ...")
+
+    def save_geometry(self, x, y, width, height, notebook_width):
+        self.defaults["global_def_win_x"] = x
+        self.defaults["global_def_win_y"] = y
+        self.defaults["global_def_win_w"] = width
+        self.defaults["global_def_win_h"] = height
+        self.defaults["def_notebook_width"] = notebook_width
+        self.save_defaults()
+
+    def message_dialog(self, title, message, kind="info"):
+        icon = {"info": QtWidgets.QMessageBox.Information,
+                "warning": QtWidgets.QMessageBox.Warning,
+                "error": QtWidgets.QMessageBox.Critical}[str(kind)]
+        dlg = QtWidgets.QMessageBox(icon, title, message, parent=self.ui)
+        dlg.setText(message)
+        dlg.exec_()
+
+    def register_recent(self, kind, filename):
+
+        self.log.debug("register_recent()")
+        self.log.debug("   %s" % kind)
+        self.log.debug("   %s" % filename)
+
+        record = {'kind': str(kind), 'filename': str(filename)}
+        if record in self.recent:
+            return
+
+        self.recent.insert(0, record)
+
+        if len(self.recent) > self.defaults['global_recent_limit']:  # Limit reached
+            self.recent.pop()
+
+        try:
+            f = open(self.data_path + '/recent.json', 'w')
+        except IOError:
+            App.log.error("Failed to open recent items file for writing.")
+            self.inform.emit('[error_notcl]Failed to open recent files file for writing.')
+            return
+
+        #try:
+        json.dump(self.recent, f)
+        # except:
+        #     App.log.error("Failed to write to recent items file.")
+        #     self.inform.emit('ERROR: Failed to write to recent items file.')
+        #     f.close()
+
+        f.close()
+
+        # Re-buid the recent items menu
+        self.setup_recent_items()
+
+    def new_object(self, kind, name, initialize, active=True, fit=True, plot=True, autoselected=True):
+        """
+        Creates a new specalized FlatCAMObj and attaches it to the application,
+        this is, updates the GUI accordingly, any other records and plots it.
+        This method is thread-safe.
+
+        Notes:
+            * If the name is in use, the self.collection will modify it
+              when appending it to the collection. There is no need to handle
+              name conflicts here.
+
+        :param kind: The kind of object to create. One of 'gerber',
+         'excellon', 'cncjob' and 'geometry'.
+        :type kind: str
+        :param name: Name for the object.
+        :type name: str
+        :param initialize: Function to run after creation of the object
+         but before it is attached to the application. The function is
+         called with 2 parameters: the new object and the App instance.
+        :type initialize: function
+        :return: None
+        :rtype: None
+        """
+
+        App.log.debug("new_object()")
+        self.plot = plot
+        self.autoselected = autoselected
+        t0 = time.time()  # Debug
+
+        ## Create object
+        classdict = {
+            "gerber": FlatCAMGerber,
+            "excellon": FlatCAMExcellon,
+            "cncjob": FlatCAMCNCjob,
+            "geometry": FlatCAMGeometry
+        }
+
+        App.log.debug("Calling object constructor...")
+        obj = classdict[kind](name)
+        obj.units = self.options["units"]  # TODO: The constructor should look at defaults.
+
+        # Set options from "Project options" form
+        self.options_read_form()
+
+        # IMPORTANT
+        # The key names in defaults and options dictionary's are not random:
+        # they have to have in name first the type of the object (geometry, excellon, cncjob and gerber) or how it's
+        # called here, the 'kind' followed by an underline. The function called above (self.options_read_form()) copy
+        # the options from project options form into the self.options. After that, below, depending on the type of
+        # object that is created, it will strip the name of the object and the underline (if the original key was
+        # let's say "excellon_toolchange", it will strip the excellon_) and to the obj.options the key will become
+        # "toolchange"
+        for option in self.options:
+            if option.find(kind + "_") == 0:
+                oname = option[len(kind) + 1:]
+                obj.options[oname] = self.options[option]
+
+        # if kind == 'geometry':
+        #     obj.tools = {}
+        # elif kind == 'cncjob':
+        #     obj.cnc_tools = {}
+
+        # Initialize as per user request
+        # User must take care to implement initialize
+        # in a thread-safe way as is is likely that we
+        # have been invoked in a separate thread.
+        t1 = time.time()
+        self.log.debug("%f seconds before initialize()." % (t1 - t0))
+        try:
+            return_value = initialize(obj, self)
+        except Exception as e:
+            if str(e) == "Empty Geometry":
+                self.inform.emit("[error_notcl] Object (%s) failed because: %s" % (kind, str(e)))
+            else:
+                self.inform.emit("[error] Object (%s) failed because: %s" % (kind, str(e)))
+            return "fail"
+
+        t2 = time.time()
+        self.log.debug("%f seconds executing initialize()." % (t2 - t1))
+
+        if return_value == 'fail':
+            return "fail"
+
+        # Check units and convert if necessary
+        # This condition CAN be true because initialize() can change obj.units
+        if self.options["units"].upper() != obj.units.upper():
+            self.inform.emit("Converting units to " + self.options["units"] + ".")
+            obj.convert_units(self.options["units"])
+            t3 = time.time()
+            self.log.debug("%f seconds converting units." % (t3 - t2))
+
+        # Create the bounding box for the object and then add the results to the obj.options
+        try:
+            xmin, ymin, xmax, ymax = obj.bounds()
+            obj.options['xmin'] = xmin
+            obj.options['ymin'] = ymin
+            obj.options['xmax'] = xmax
+            obj.options['ymax'] = ymax
+        except:
+            log.warning("The object has no bounds properties.")
+            pass
+
+        FlatCAMApp.App.log.debug("Moving new object back to main thread.")
+
+        # Move the object to the main thread and let the app know that it is available.
+        obj.moveToThread(QtWidgets.QApplication.instance().thread())
+        self.object_created.emit(obj, self.plot, self.autoselected)
+
+        return obj
+
+    def new_excellon_object(self):
+        self.new_object('excellon', 'new_e', lambda x, y: None)
+
+    def on_object_created(self, obj, plot, autoselect):
+        """
+        Event callback for object creation.
+
+        :param obj: The newly created FlatCAM object.
+        :return: None
+        """
+        t0 = time.time()  # DEBUG
+        self.log.debug("on_object_created()")
+
+        # The Collection might change the name if there is a collision
+        self.collection.append(obj)
+
+        # after adding the object to the collection always update the list of objects that are in the collection
+        self.all_objects_list = self.collection.get_list()
+
+        self.inform.emit("[success]Object (%s) created: %s" % (obj.kind, obj.options['name']))
+        self.new_object_available.emit(obj)
+
+        # update the SHELL auto-completer model with the name of the new object
+        self.myKeywords.append(obj.options['name'])
+        self.shell._edit.set_model_data(self.myKeywords)
+
+        if autoselect:
+            # select the just opened object but deselect the previous ones
+            self.collection.set_all_inactive()
+            self.collection.set_active(obj.options["name"])
+
+        def worker_task(obj):
+            with self.proc_container.new("Plotting"):
+                obj.plot()
+                t1 = time.time()  # DEBUG
+                self.log.debug("%f seconds adding object and plotting." % (t1 - t0))
+                self.object_plotted.emit(obj)
+
+        # Send to worker
+        # self.worker.add_task(worker_task, [self])
+        if plot:
+            self.worker_task.emit({'fcn': worker_task, 'params': [obj]})
+
+    def on_object_changed(self, obj):
+        # update the bounding box data from obj.options
+        xmin, ymin, xmax, ymax = obj.bounds()
+        obj.options['xmin'] = xmin
+        obj.options['ymin'] = ymin
+        obj.options['xmax'] = xmax
+        obj.options['ymax'] = ymax
+
+        log.debug("Object changed, updating the bounding box data on self.options")
+        # delete the old selection shape
+        self.delete_selection_shape()
+
+    def on_object_plotted(self, obj):
+        self.on_zoom_fit(None)
+
+    def options_read_form(self):
+        for option in self.options_form_fields:
+            self.options[option] = self.options_form_fields[option].get_value()
+
+    def options_write_form(self):
+        for option in self.options:
+            self.options_write_form_field(option)
+
+    def options_write_form_field(self, field):
+        try:
+            self.options_form_fields[field].set_value(self.options[field])
+        except KeyError:
+            # Changed from error to debug. This allows to have data stored
+            # which is not user-editable.
+            # self.log.debug("options_write_form_field(): No field for: %s" % field)
+            pass
+
+    def on_about(self):
+        """
+        Displays the "about" dialog.
+
+        :return: None
+        """
+        self.report_usage("on_about")
+
+        version = self.version
+        version_date = self.version_date
+
+        class AboutDialog(QtWidgets.QDialog):
+            def __init__(self, parent=None):
+                QtWidgets.QDialog.__init__(self, parent)
+
+                # Icon and title
+                self.setWindowIcon(parent.app_icon)
+                self.setWindowTitle("FlatCAM")
+
+                layout1 = QtWidgets.QVBoxLayout()
+                self.setLayout(layout1)
+
+                layout2 = QtWidgets.QHBoxLayout()
+                layout1.addLayout(layout2)
+
+                logo = QtWidgets.QLabel()
+                logo.setPixmap(QtGui.QPixmap('share/flatcam_icon256.png'))
+                layout2.addWidget(logo, stretch=0)
+
+                title = QtWidgets.QLabel(
+                    "<font size=8><B>FlatCAM</B></font><BR>"
+                    "Version %s (%s) - %s <BR>"
+                    "<BR>"
+                    "2D Computer-Aided Printed Circuit Board<BR>"
+                    "Manufacturing.<BR>"
+                    "<BR>"
+                    "(c) 2014-2019 <B>Juan Pablo Caram</B><BR>"
+                    "<BR>"
+                    "<B>Contributors:</B><BR>"
+                    "Denis Hayrullin<BR>"
+                    "Kamil Sopko<BR>"
+                    "Matthieu Berthomé<BR>"
+                    "and many others found "
+                    "<a href = \"https://bitbucket.org/jpcgt/flatcam/pull-requests/?state=MERGED\">here.</a><BR>"
+                    "<BR>"
+                    "Development is done "
+                    "<a href = \"https://bitbucket.org/jpcgt/flatcam/src/beta/\">here.</a><BR>"
+                    "DOWNLOAD area "
+                    "<a href = \"https://bitbucket.org/jpcgt/flatcam/downloads/\">here.</a><BR>"
+                    "" % (version, version_date, platform.architecture()[0])
+                )
+                title.setOpenExternalLinks(True)
+
+                layout2.addWidget(title, stretch=1)
+
+                layout3 = QtWidgets.QHBoxLayout()
+                layout1.addLayout(layout3)
+                layout3.addStretch()
+                okbtn = QtWidgets.QPushButton("Close")
+                layout3.addWidget(okbtn)
+
+                okbtn.clicked.connect(self.accept)
+
+        AboutDialog(self.ui).exec_()
+
+    def on_file_savedefaults(self):
+        """
+        Callback for menu item File->Save Defaults. Saves application default options
+        ``self.defaults`` to defaults.json.
+
+        :return: None
+        """
+
+        self.save_defaults()
+
+    def on_app_exit(self):
+        self.save_defaults()
+        log.debug("Application defaults saved ... Exit event.")
+        QtWidgets.qApp.quit()
+
+    def save_defaults(self, silent=False):
+        """
+        Saves application default options
+        ``self.defaults`` to defaults.json.
+
+        :return: None
+        """
+        self.report_usage("save_defaults")
+
+        # Read options from file
+        try:
+            f = open(self.data_path + "/defaults.json")
+            defaults_file_content = 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_notcl] Could not load defaults file.")
+            return
+
+        try:
+            defaults = json.loads(defaults_file_content)
+        except:
+            e = sys.exc_info()[0]
+            App.log.error("Failed to parse defaults file.")
+            App.log.error(str(e))
+            self.inform.emit("[error_notcl] Failed to parse defaults file.")
+            return
+
+        # Update options
+        self.defaults_read_form()
+        defaults.update(self.defaults)
+        self.propagate_defaults(silent=True)
+
+        # Save update options
+        try:
+            f = open(self.data_path + "/defaults.json", "w")
+            json.dump(defaults, f)
+            f.close()
+        except:
+            self.inform.emit("[error_notcl] Failed to write defaults to file.")
+            return
+
+        # Save the toolbar view
+        tb_status = 0
+        if self.ui.toolbarfile.isVisible():
+            tb_status += 1
+
+        if self.ui.toolbargeo.isVisible():
+            tb_status += 2
+
+        if self.ui.toolbarview.isVisible():
+            tb_status += 4
+
+        if self.ui.toolbartools.isVisible():
+            tb_status += 8
+
+        if self.ui.snap_toolbar.isVisible():
+            tb_status += 16
+
+        self.defaults["global_toolbar_view"] = tb_status
+
+        if not silent:
+            self.inform.emit("[success]Defaults saved.")
+
+    def save_factory_defaults(self, silent=False):
+        """
+                Saves application factory default options
+                ``self.defaults`` to factory_defaults.json.
+                It's a one time job done just after the first install.
+
+                :return: None
+                """
+        self.report_usage("save_factory_defaults")
+
+        # Read options from file
+        try:
+            f_f_def = open(self.data_path + "/factory_defaults.json")
+            factory_defaults_file_content = f_f_def.read()
+            f_f_def.close()
+        except:
+            e = sys.exc_info()[0]
+            App.log.error("Could not load factory defaults file.")
+            App.log.error(str(e))
+            self.inform.emit("[error_notcl] Could not load factory defaults file.")
+            return
+
+        try:
+            factory_defaults = json.loads(factory_defaults_file_content)
+        except:
+            e = sys.exc_info()[0]
+            App.log.error("Failed to parse factory defaults file.")
+            App.log.error(str(e))
+            self.inform.emit("[error_notcl] Failed to parse factory defaults file.")
+            return
+
+        # Update options
+        self.defaults_read_form()
+        factory_defaults.update(self.defaults)
+        self.propagate_defaults(silent=True)
+
+        # Save update options
+        try:
+            f_f_def_s = open(self.data_path + "/factory_defaults.json", "w")
+            json.dump(factory_defaults, f_f_def_s)
+            f_f_def_s.close()
+        except:
+            self.inform.emit("[error_notcl] Failed to write factory defaults to file.")
+            return
+
+        if silent is False:
+            self.inform.emit("Factory defaults saved.")
+
+    def final_save(self):
+        self.save_defaults()
+        log.debug("Application defaults saved ... Exit event.")
+
+    def on_toggle_shell(self):
+        """
+        toggle shell if is  visible close it if  closed open it
+        :return:
+        """
+
+        if self.ui.shell_dock.isVisible():
+            self.ui.shell_dock.hide()
+        else:
+            self.ui.shell_dock.show()
+
+    def on_edit_join(self, name=None):
+        """
+        Callback for Edit->Join. Joins the selected geometry objects into
+        a new one.
+
+        :return: None
+        """
+
+        obj_name_single = str(name) if name else "Combo_SingleGeo"
+        obj_name_multi = str(name) if name else "Combo_MultiGeo"
+
+        tooldias = []
+        geo_type_list = []
+
+        objs = self.collection.get_selected()
+        for obj in objs:
+            geo_type_list.append(obj.multigeo)
+
+        # if len(set(geo_type_list)) == 1 means that all list elements are the same
+        if len(set(geo_type_list)) != 1:
+            self.inform.emit("[error] Failed join. The Geometry objects are of different types.\n"
+                             "At least one is MultiGeo type and the other is SingleGeo type. A possibility is to "
+                             "convert from one to another and retry joining \n"
+                             "but in the case of converting from MultiGeo to SingleGeo, informations may be lost and "
+                             "the result may not be what was expected. \n"
+                             "Check the generated GCODE.")
+            return
+
+        # if at least one True object is in the list then due of the previous check, all list elements are True objects
+        if True in geo_type_list:
+            def initialize(obj, app):
+                FlatCAMGeometry.merge(objs, obj, multigeo=True)
+
+                # rename all the ['name] key in obj.tools[tooluid]['data'] to the obj_name_multi
+                for v in obj.tools.values():
+                    v['data']['name'] = obj_name_multi
+            self.new_object("geometry", obj_name_multi, initialize)
+        else:
+            def initialize(obj, app):
+                FlatCAMGeometry.merge(objs, obj, multigeo=False)
+
+                # rename all the ['name] key in obj.tools[tooluid]['data'] to the obj_name_multi
+                for v in obj.tools.values():
+                    v['data']['name'] = obj_name_single
+            self.new_object("geometry", obj_name_single, initialize)
+
+    def on_edit_join_exc(self):
+        """
+        Callback for Edit->Join Excellon. Joins the selected excellon objects into
+        a new one.
+
+        :return: None
+        """
+        objs = self.collection.get_selected()
+
+        for obj in objs:
+            if not isinstance(obj, FlatCAMExcellon):
+                self.inform.emit("[error_notcl]Failed. Excellon joining works only on Excellon objects.")
+                return
+
+        def initialize(obj, app):
+            FlatCAMExcellon.merge(objs, obj)
+
+        self.new_object("excellon", 'Combo_Excellon', initialize)
+
+    def on_convert_singlegeo_to_multigeo(self):
+        obj = self.collection.get_active()
+
+        if obj is None:
+            self.inform.emit("[error_notcl]Failed. Select a Geometry Object and try again.")
+            return
+
+        if not isinstance(obj, FlatCAMGeometry):
+            self.inform.emit("[error_notcl]Expected a FlatCAMGeometry, got %s" % type(obj))
+            return
+
+        obj.multigeo = True
+        for tooluid, dict_value in obj.tools.items():
+            dict_value['solid_geometry'] = deepcopy(obj.solid_geometry)
+        if not isinstance(obj.solid_geometry, list):
+            obj.solid_geometry = [obj.solid_geometry]
+        obj.solid_geometry[:] = []
+        obj.plot()
+
+        self.inform.emit("[success] A Geometry object was converted to MultiGeo type.")
+
+    def on_convert_multigeo_to_singlegeo(self):
+        obj = self.collection.get_active()
+
+        if obj is None:
+            self.inform.emit("[error_notcl]Failed. Select a Geometry Object and try again.")
+            return
+
+        if not isinstance(obj, FlatCAMGeometry):
+            self.inform.emit("[error_notcl]Expected a FlatCAMGeometry, got %s" % type(obj))
+            return
+
+        obj.multigeo = False
+        total_solid_geometry = []
+        for tooluid, dict_value in obj.tools.items():
+            total_solid_geometry += deepcopy(dict_value['solid_geometry'])
+            # clear the original geometry
+            dict_value['solid_geometry'][:] = []
+        obj.solid_geometry = deepcopy(total_solid_geometry)
+        obj.plot()
+
+        self.inform.emit("[success] A Geometry object was converted to SingleGeo type.")
+
+    def on_options_dict_change(self, field):
+        self.options_write_form_field(field)
+
+        if field == "units":
+            self.set_screen_units(self.options['units'])
+
+    def on_defaults_dict_change(self, field):
+        self.defaults_write_form_field(field)
+
+    def set_screen_units(self, units):
+        self.ui.units_label.setText("[" + self.options["units"].lower() + "]")
+
+    def on_toggle_units(self):
+        """
+        Callback for the Units radio-button change in the Options tab.
+        Changes the application's default units or the current project's units.
+        If changing the project's units, the change propagates to all of
+        the objects in the project.
+
+        :return: None
+        """
+
+        self.report_usage("on_toggle_units")
+
+        if self.toggle_units_ignore:
+            return
+
+        # If option is the same, then ignore
+        if self.general_options_form.general_group.units_radio.get_value().upper() == self.options["units"].upper():
+            self.log.debug("on_toggle_units(): Same as options, so ignoring.")
+            return
+
+        # Options to scale
+        dimensions = ['gerber_isotooldia', 'gerber_cutoutmargin', 'gerber_cutoutgapsize',
+                      'gerber_noncoppermargin', 'gerber_bboxmargin','gerber_isooverlap','gerber_nccoverlap',
+                      'gerber_nccmargin','gerber_cutouttooldia','gerber_cutoutgapsize','gerber_cutoutmargin',
+                      'gerber_noncoppermargin','gerber_bboxmargin',
+                      'excellon_drillz', "excellon_toolchangexy",
+                      'excellon_travelz', 'excellon_feedrate', 'excellon_feedrate_rapid', 'excellon_toolchangez',
+                      'excellon_tooldia', 'excellon_endz', 'cncjob_tooldia',
+                      'geometry_cutz', 'geometry_travelz', 'geometry_feedrate', 'geometry_feedrate_rapid',
+                      'geometry_cnctooldia', 'geometry_painttooldia', 'geometry_paintoverlap', 'geometry_toolchangexy',
+                      'geometry_toolchangez',
+                      'geometry_paintmargin', 'geometry_endz', 'geometry_depthperpass', 'global_gridx', 'global_gridy']
+
+        def scale_options(sfactor):
+            for dim in dimensions:
+                if dim == 'excellon_toolchangexy':
+                    coords_xy = [float(eval(a)) for a in self.defaults["excellon_toolchangexy"].split(",")]
+                    coords_xy[0] *= sfactor
+                    coords_xy[1] *= sfactor
+                    self.options['excellon_toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1])
+                elif dim == 'geometry_toolchangexy':
+                    coords_xy = [float(eval(a)) for a in self.defaults["geometry_toolchangexy"].split(",")]
+                    coords_xy[0] *= sfactor
+                    coords_xy[1] *= sfactor
+                    self.options['geometry_toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1])
+                else:
+                    self.options[dim] *= sfactor
+
+        # The scaling factor depending on choice of units.
+        factor = 1/25.4
+        if self.general_options_form.general_group.units_radio.get_value().upper() == 'MM':
+            factor = 25.4
+
+
+        # Changing project units. Warn user.
+        msgbox = QtWidgets.QMessageBox()
+        msgbox.setText("<B>Change project units ...</B>")
+        msgbox.setInformativeText("Changing the units of the project causes all geometrical "
+                                  "properties of all objects to be scaled accordingly. Continue?")
+        msgbox.setStandardButtons(QtWidgets.QMessageBox.Cancel | QtWidgets.QMessageBox.Ok)
+        msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+
+        response = msgbox.exec_()
+
+        if response == QtWidgets.QMessageBox.Ok:
+            self.options_read_form()
+            scale_options(factor)
+            self.options_write_form()
+
+            # change this only if the workspace is active
+            if self.defaults['global_workspace'] is True:
+                self.plotcanvas.draw_workspace()
+
+            # adjust the grid values on the main toolbar
+            self.ui.grid_gap_x_entry.set_value(float(self.ui.grid_gap_x_entry.get_value()) * factor)
+            self.ui.grid_gap_y_entry.set_value(float(self.ui.grid_gap_y_entry.get_value()) * factor)
+
+            for obj in self.collection.get_list():
+                units = self.general_options_form.general_group.units_radio.get_value().upper()
+                obj.convert_units(units)
+
+                # make that the properties stored in the object are also updated
+                self.object_changed.emit(obj)
+                obj.build_ui()
+
+            current = self.collection.get_active()
+            if current is not None:
+                # the transfer of converted values to the UI form for Geometry is done local in the FlatCAMObj.py
+                if not isinstance(current, FlatCAMGeometry):
+                    current.to_form()
+
+            self.plot_all()
+        else:
+            # Undo toggling
+            self.toggle_units_ignore = True
+            if self.general_options_form.general_group.units_radio.get_value().upper() == 'MM':
+                self.general_options_form.general_group.units_radio.set_value('IN')
+            else:
+                self.general_options_form.general_group.units_radio.set_value('MM')
+            self.toggle_units_ignore = False
+
+        self.options_read_form()
+        self.inform.emit("Converted units to %s" % self.options["units"])
+        #self.ui.units_label.setText("[" + self.options["units"] + "]")
+        self.set_screen_units(self.options["units"])
+
+    def on_toggle_axis(self):
+        if self.toggle_axis is False:
+            self.plotcanvas.v_line.set_data(color=(0.70, 0.3, 0.3, 1.0))
+            self.plotcanvas.h_line.set_data(color=(0.70, 0.3, 0.3, 1.0))
+            self.plotcanvas.redraw()
+            self.toggle_axis = True
+        else:
+            self.plotcanvas.v_line.set_data(color=(0.0, 0.0, 0.0, 0.0))
+
+            self.plotcanvas.h_line.set_data(color=(0.0, 0.0, 0.0, 0.0))
+            self.plotcanvas.redraw()
+            self.toggle_axis = False
+
+    def on_options_combo_change(self, sel):
+        """
+        Called when the combo box to choose between application defaults and
+        project option changes value. The corresponding variables are
+        copied to the UI.
+
+        :param sel: The option index that was chosen.
+        :return: None
+        """
+
+        # combo_sel = self.ui.notebook.combo_options.get_active()
+        App.log.debug("Options --> %s" % sel)
+
+        # form = [self.defaults_form, self.options_form][sel]
+        # self.ui.notebook.options_contents.pack_start(form, False, False, 1)
+
+        if sel == 0:
+            self.gen_form = self.general_defaults_form
+            self.ger_form = self.gerber_defaults_form
+            self.exc_form = self.excellon_defaults_form
+            self.geo_form = self.geometry_defaults_form
+            self.cnc_form = self.cncjob_defaults_form
+        elif sel == 1:
+            self.gen_form = self.general_options_form
+            self.ger_form = self.gerber_options_form
+            self.exc_form = self.excellon_options_form
+            self.geo_form = self.geometry_options_form
+            self.cnc_form = self.cncjob_options_form
+        else:
+            return
+
+        try:
+            self.ui.general_scroll_area.takeWidget()
+        except:
+            self.log.debug("Nothing to remove")
+        self.ui.general_scroll_area.setWidget(self.gen_form)
+        self.gen_form.show()
+
+        try:
+            self.ui.gerber_scroll_area.takeWidget()
+        except:
+            self.log.debug("Nothing to remove")
+        self.ui.gerber_scroll_area.setWidget(self.ger_form)
+        self.ger_form.show()
+
+        try:
+            self.ui.excellon_scroll_area.takeWidget()
+        except:
+            self.log.debug("Nothing to remove")
+        self.ui.excellon_scroll_area.setWidget(self.exc_form)
+        self.exc_form.show()
+
+        try:
+            self.ui.geometry_scroll_area.takeWidget()
+        except:
+            self.log.debug("Nothing to remove")
+        self.ui.geometry_scroll_area.setWidget(self.geo_form)
+        self.geo_form.show()
+
+        try:
+            self.ui.cncjob_scroll_area.takeWidget()
+        except:
+            self.log.debug("Nothing to remove")
+        self.ui.cncjob_scroll_area.setWidget(self.cnc_form)
+        self.cnc_form.show()
+
+        self.log.debug("Finished GUI form initialization.")
+
+        # self.options2form()
+
+    def on_excellon_defaults_button(self):
+        self.defaults_form_fields["excellon_format_lower_in"].set_value('4')
+        self.defaults_form_fields["excellon_format_upper_in"].set_value('2')
+        self.defaults_form_fields["excellon_format_lower_mm"].set_value('3')
+        self.defaults_form_fields["excellon_format_upper_mm"].set_value('3')
+        self.defaults_form_fields["excellon_zeros"].set_value('L')
+        self.defaults_form_fields["excellon_units"].set_value('INCH')
+        log.debug("Excellon app defaults loaded ...")
+
+    def on_excellon_options_button(self):
+
+        self.options_form_fields["excellon_format_lower_in"].set_value('4')
+        self.options_form_fields["excellon_format_upper_in"].set_value('2')
+        self.options_form_fields["excellon_format_lower_mm"].set_value('3')
+        self.options_form_fields["excellon_format_upper_mm"].set_value('3')
+        self.options_form_fields["excellon_zeros"].set_value('L')
+        self.options_form_fields["excellon_units"].set_value('INCH')
+        log.debug("Excellon options defaults loaded ...")
+
+    # Setting plot colors handlers
+    def on_pf_color_entry(self):
+        self.defaults['global_plot_fill'] = self.general_defaults_form.general_group.pf_color_entry.get_value()[:7] + \
+                                            self.defaults['global_plot_fill'][7:9]
+        self.general_defaults_form.general_group.pf_color_button.setStyleSheet(
+            "background-color:%s" % str(self.defaults['global_plot_fill'])[:7])
+
+    def on_pf_color_button(self):
+        current_color = QtGui.QColor(self.defaults['global_plot_fill'][:7])
+
+        c_dialog = QtWidgets.QColorDialog()
+        plot_fill_color = c_dialog.getColor(initial=current_color)
+
+        if plot_fill_color.isValid() is False:
+            return
+
+        self.general_defaults_form.general_group.pf_color_button.setStyleSheet(
+            "background-color:%s" % str(plot_fill_color.name()))
+
+        new_val = str(plot_fill_color.name()) + str(self.defaults['global_plot_fill'][7:9])
+        self.general_defaults_form.general_group.pf_color_entry.set_value(new_val)
+        self.defaults['global_plot_fill'] = new_val
+
+    def on_pf_color_spinner(self):
+        spinner_value = self.general_defaults_form.general_group.pf_color_alpha_spinner.value()
+        self.general_defaults_form.general_group.pf_color_alpha_slider.setValue(spinner_value)
+        self.defaults['global_plot_fill'] = self.defaults['global_plot_fill'][:7] + \
+                                            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+        self.defaults['global_plot_line'] = self.defaults['global_plot_line'][:7] + \
+                                            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+
+    def on_pf_color_slider(self):
+        slider_value = self.general_defaults_form.general_group.pf_color_alpha_slider.value()
+        self.general_defaults_form.general_group.pf_color_alpha_spinner.setValue(slider_value)
+
+    def on_pl_color_entry(self):
+        self.defaults['global_plot_line'] = self.general_defaults_form.general_group.pl_color_entry.get_value()[:7] + \
+                                            self.defaults['global_plot_line'][7:9]
+        self.general_defaults_form.general_group.pl_color_button.setStyleSheet(
+            "background-color:%s" % str(self.defaults['global_plot_line'])[:7])
+
+    def on_pl_color_button(self):
+        current_color = QtGui.QColor(self.defaults['global_plot_line'][:7])
+        # print(current_color)
+
+        c_dialog = QtWidgets.QColorDialog()
+        plot_line_color = c_dialog.getColor(initial=current_color)
+
+        if plot_line_color.isValid() is False:
+            return
+
+        self.general_defaults_form.general_group.pl_color_button.setStyleSheet(
+            "background-color:%s" % str(plot_line_color.name()))
+
+        new_val_line = str(plot_line_color.name()) + str(self.defaults['global_plot_line'][7:9])
+        self.general_defaults_form.general_group.pl_color_entry.set_value(new_val_line)
+        self.defaults['global_plot_line'] = new_val_line
+
+    # Setting selection colors (left - right) handlers
+    def on_sf_color_entry(self):
+        self.defaults['global_sel_fill'] = self.general_defaults_form.general_group.sf_color_entry.get_value()[:7] + \
+                                            self.defaults['global_sel_fill'][7:9]
+        self.general_defaults_form.general_group.sf_color_button.setStyleSheet(
+            "background-color:%s" % str(self.defaults['global_sel_fill'])[:7])
+
+    def on_sf_color_button(self):
+        current_color = QtGui.QColor(self.defaults['global_sel_fill'][:7])
+
+        c_dialog = QtWidgets.QColorDialog()
+        plot_fill_color = c_dialog.getColor(initial=current_color)
+
+        if plot_fill_color.isValid() is False:
+            return
+
+        self.general_defaults_form.general_group.sf_color_button.setStyleSheet(
+            "background-color:%s" % str(plot_fill_color.name()))
+
+        new_val = str(plot_fill_color.name()) + str(self.defaults['global_sel_fill'][7:9])
+        self.general_defaults_form.general_group.sf_color_entry.set_value(new_val)
+        self.defaults['global_sel_fill'] = new_val
+
+    def on_sf_color_spinner(self):
+        spinner_value = self.general_defaults_form.general_group.sf_color_alpha_spinner.value()
+        self.general_defaults_form.general_group.sf_color_alpha_slider.setValue(spinner_value)
+        self.defaults['global_sel_fill'] = self.defaults['global_sel_fill'][:7] + \
+                                            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+        self.defaults['global_sel_line'] = self.defaults['global_sel_line'][:7] + \
+                                            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+
+    def on_sf_color_slider(self):
+        slider_value = self.general_defaults_form.general_group.sf_color_alpha_slider.value()
+        self.general_defaults_form.general_group.sf_color_alpha_spinner.setValue(slider_value)
+
+    def on_sl_color_entry(self):
+        self.defaults['global_sel_line'] = self.general_defaults_form.general_group.sl_color_entry.get_value()[:7] + \
+                                            self.defaults['global_sel_line'][7:9]
+        self.general_defaults_form.general_group.sl_color_button.setStyleSheet(
+            "background-color:%s" % str(self.defaults['global_sel_line'])[:7])
+
+    def on_sl_color_button(self):
+        current_color = QtGui.QColor(self.defaults['global_sel_line'][:7])
+
+        c_dialog = QtWidgets.QColorDialog()
+        plot_line_color = c_dialog.getColor(initial=current_color)
+
+        if plot_line_color.isValid() is False:
+            return
+
+        self.general_defaults_form.general_group.sl_color_button.setStyleSheet(
+            "background-color:%s" % str(plot_line_color.name()))
+
+        new_val_line = str(plot_line_color.name()) + str(self.defaults['global_sel_line'][7:9])
+        self.general_defaults_form.general_group.sl_color_entry.set_value(new_val_line)
+        self.defaults['global_sel_line'] = new_val_line
+
+    # Setting selection colors (right - left) handlers
+    def on_alt_sf_color_entry(self):
+        self.defaults['global_alt_sel_fill'] = self.general_defaults_form.general_group \
+                                   .alt_sf_color_entry.get_value()[:7] + self.defaults['global_alt_sel_fill'][7:9]
+        self.general_defaults_form.general_group.alt_sf_color_button.setStyleSheet(
+            "background-color:%s" % str(self.defaults['global_alt_sel_fill'])[:7])
+
+    def on_alt_sf_color_button(self):
+        current_color = QtGui.QColor(self.defaults['global_alt_sel_fill'][:7])
+
+        c_dialog = QtWidgets.QColorDialog()
+        plot_fill_color = c_dialog.getColor(initial=current_color)
+
+        if plot_fill_color.isValid() is False:
+            return
+
+        self.general_defaults_form.general_group.alt_sf_color_button.setStyleSheet(
+            "background-color:%s" % str(plot_fill_color.name()))
+
+        new_val = str(plot_fill_color.name()) + str(self.defaults['global_alt_sel_fill'][7:9])
+        self.general_defaults_form.general_group.alt_sf_color_entry.set_value(new_val)
+        self.defaults['global_alt_sel_fill'] = new_val
+
+    def on_alt_sf_color_spinner(self):
+        spinner_value = self.general_defaults_form.general_group.alt_sf_color_alpha_spinner.value()
+        self.general_defaults_form.general_group.alt_sf_color_alpha_slider.setValue(spinner_value)
+        self.defaults['global_alt_sel_fill'] = self.defaults['global_alt_sel_fill'][:7] + \
+                                            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+        self.defaults['global_alt_sel_line'] = self.defaults['global_alt_sel_line'][:7] + \
+                                            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+
+    def on_alt_sf_color_slider(self):
+        slider_value = self.general_defaults_form.general_group.alt_sf_color_alpha_slider.value()
+        self.general_defaults_form.general_group.alt_sf_color_alpha_spinner.setValue(slider_value)
+
+    def on_alt_sl_color_entry(self):
+        self.defaults['global_alt_sel_line'] = self.general_defaults_form.general_group \
+                                   .alt_sl_color_entry.get_value()[:7] + self.defaults['global_alt_sel_line'][7:9]
+        self.general_defaults_form.general_group.alt_sl_color_button.setStyleSheet(
+            "background-color:%s" % str(self.defaults['global_alt_sel_line'])[:7])
+
+    def on_alt_sl_color_button(self):
+        current_color = QtGui.QColor(self.defaults['global_alt_sel_line'][:7])
+
+        c_dialog = QtWidgets.QColorDialog()
+        plot_line_color = c_dialog.getColor(initial=current_color)
+
+        if plot_line_color.isValid() is False:
+            return
+
+        self.general_defaults_form.general_group.alt_sl_color_button.setStyleSheet(
+            "background-color:%s" % str(plot_line_color.name()))
+
+        new_val_line = str(plot_line_color.name()) + str(self.defaults['global_alt_sel_line'][7:9])
+        self.general_defaults_form.general_group.alt_sl_color_entry.set_value(new_val_line)
+        self.defaults['global_alt_sel_line'] = new_val_line
+
+    # Setting Editor colors
+    def on_draw_color_entry(self):
+        self.defaults['global_draw_color'] = self.general_defaults_form.general_group \
+                                                   .draw_color_entry.get_value()
+        self.general_defaults_form.general_group.draw_color_button.setStyleSheet(
+            "background-color:%s" % str(self.defaults['global_draw_color']))
+
+    def on_draw_color_button(self):
+        current_color = QtGui.QColor(self.defaults['global_draw_color'])
+
+        c_dialog = QtWidgets.QColorDialog()
+        draw_color = c_dialog.getColor(initial=current_color)
+
+        if draw_color.isValid() is False:
+            return
+
+        self.general_defaults_form.general_group.draw_color_button.setStyleSheet(
+            "background-color:%s" % str(draw_color.name()))
+
+        new_val = str(draw_color.name())
+        self.general_defaults_form.general_group.draw_color_entry.set_value(new_val)
+        self.defaults['global_draw_color'] = new_val
+
+    def on_sel_draw_color_entry(self):
+        self.defaults['global_sel_draw_color'] = self.general_defaults_form.general_group \
+                                                   .sel_draw_color_entry.get_value()
+        self.general_defaults_form.general_group.sel_draw_color_button.setStyleSheet(
+            "background-color:%s" % str(self.defaults['global_sel_draw_color']))
+
+    def on_sel_draw_color_button(self):
+        current_color = QtGui.QColor(self.defaults['global_sel_draw_color'])
+
+        c_dialog = QtWidgets.QColorDialog()
+        sel_draw_color = c_dialog.getColor(initial=current_color)
+
+        if sel_draw_color.isValid() is False:
+            return
+
+        self.general_defaults_form.general_group.sel_draw_color_button.setStyleSheet(
+            "background-color:%s" % str(sel_draw_color.name()))
+
+        new_val_sel = str(sel_draw_color.name())
+        self.general_defaults_form.general_group.sel_draw_color_entry.set_value(new_val_sel)
+        self.defaults['global_sel_draw_color'] = new_val_sel
+
+    def on_workspace_modified(self):
+        self.save_defaults(silent=True)
+        self.plotcanvas.draw_workspace()
+
+    def on_workspace(self):
+        if self.general_defaults_form.general_group.workspace_cb.isChecked():
+            self.plotcanvas.restore_workspace()
+        else:
+            self.plotcanvas.delete_workspace()
+
+        self.save_defaults(silent=True)
+
+    def on_workspace_menu(self):
+        if self.general_defaults_form.general_group.workspace_cb.isChecked():
+            self.general_defaults_form.general_group.workspace_cb.setChecked(False)
+        else:
+            self.general_defaults_form.general_group.workspace_cb.setChecked(True)
+        self.on_workspace()
+
+    def on_save_button(self):
+        self.save_defaults(silent=False)
+        # load the defaults so they are updated into the app
+        self.load_defaults()
+        # Re-fresh project options
+        self.on_options_app2project()
+
+    def handleOpen(self):
+        filter_group = " G-Code Files (*.nc);; G-Code Files (*.txt);; G-Code Files (*.tap);; G-Code Files (*.cnc);; " \
+                   "All Files (*.*)"
+        path, _ = QtWidgets.QFileDialog.getOpenFileName(
+            caption='Open file', directory=self.get_last_folder(), filter=filter_group)
+        if path:
+            file = QtCore.QFile(path)
+            if file.open(QtCore.QIODevice.ReadOnly):
+                stream = QtCore.QTextStream(file)
+                self.gcode_edited = stream.readAll()
+                self.ui.code_editor.setPlainText(self.gcode_edited)
+                file.close()
+
+    def handlePrint(self):
+        dialog = QtPrintSupport.QPrintDialog()
+        if dialog.exec_() == QtWidgets.QDialog.Accepted:
+            self.ui.code_editor.document().print_(dialog.printer())
+
+    def handlePreview(self):
+        dialog = QtPrintSupport.QPrintPreviewDialog()
+        dialog.paintRequested.connect(self.ui.code_editor.print_)
+        dialog.exec_()
+
+    def handleTextChanged(self):
+        # enable = not self.ui.code_editor.document().isEmpty()
+        # self.ui.buttonPrint.setEnabled(enable)
+        # self.ui.buttonPreview.setEnabled(enable)
+        pass
+
+    def handleSaveGCode(self):
+        _filter_ = " G-Code Files (*.nc);; G-Code Files (*.txt);; G-Code Files (*.tap);; G-Code Files (*.cnc);; " \
+                   "All Files (*.*)"
+        try:
+            filename = str(QtWidgets.QFileDialog.getSaveFileName(
+                caption="Export G-Code ...", directory=self.defaults["global_last_folder"], filter=_filter_)[0])
+        except TypeError:
+            filename = str(QtWidgets.QFileDialog.getSaveFileName(caption="Export G-Code ...", filter=_filter_)[0])
+
+        try:
+            my_gcode = self.ui.code_editor.toPlainText()
+            with open(filename, 'w') as f:
+                for line in my_gcode:
+                    f.write(line)
+
+        except FileNotFoundError:
+            self.inform.emit("[WARNING] No such file or directory")
+            return
+
+        # Just for adding it to the recent files list.
+        self.file_opened.emit("cncjob", filename)
+
+        self.file_saved.emit("cncjob", filename)
+        self.inform.emit("Saved to: " + filename)
+
+    def handleFindGCode(self):
+        flags = QtGui.QTextDocument.FindCaseSensitively
+        text_to_be_found = self.ui.entryFind.get_value()
+
+        r = self.ui.code_editor.find(str(text_to_be_found), flags)
+        if r is False:
+            self.ui.code_editor.moveCursor(QtGui.QTextCursor.Start)
+
+
+    def handleReplaceGCode(self):
+        old = self.ui.entryFind.get_value()
+        new = self.ui.entryReplace.get_value()
+
+        if self.ui.sel_all_cb.isChecked():
+            while True:
+                cursor = self.ui.code_editor.textCursor()
+                cursor.beginEditBlock()
+                flags = QtGui.QTextDocument.FindCaseSensitively
+                # self.ui.editor is the QPlainTextEdit
+                r = self.ui.code_editor.find(str(old), flags)
+                if r:
+                    qc = self.ui.code_editor.textCursor()
+                    if qc.hasSelection():
+                        qc.insertText(new)
+                else:
+                    self.ui.code_editor.moveCursor(QtGui.QTextCursor.Start)
+                    break
+            # Mark end of undo block
+            cursor.endEditBlock()
+        else:
+            cursor = self.ui.code_editor.textCursor()
+            cursor.beginEditBlock()
+            qc = self.ui.code_editor.textCursor()
+            if qc.hasSelection():
+                qc.insertText(new)
+            # Mark end of undo block
+            cursor.endEditBlock()
+
+
+    def on_new_geometry(self):
+        def initialize(obj, self):
+            obj.multitool = False
+
+        self.new_object('geometry', 'new_g', initialize)
+        self.plot_all()
+
+    def on_delete(self):
+        """
+        Delete the currently selected FlatCAMObjs.
+
+        :return: None
+        """
+
+        # Make sure that the deletion will happen only after the Editor is no longer active otherwise we might delete
+        # a geometry object before we update it.
+        if self.geo_editor.editor_active is False and self.exc_editor.editor_active is False:
+            if self.collection.get_active():
+                self.log.debug("on_delete()")
+                self.report_usage("on_delete")
+
+                while (self.collection.get_active()):
+                    self.delete_first_selected()
+
+                self.inform.emit("Object(s) deleted ...")
+                # make sure that the selection shape is deleted, too
+                self.delete_selection_shape()
+            else:
+                self.inform.emit("Failed. No object(s) selected...")
+        else:
+            self.inform.emit("Save the work in Editor and try again ...")
+
+    def on_set_origin(self):
+        """
+        Set the origin to the left mouse click position
+
+        :return: None
+        """
+
+        #display the message for the user
+        #and ask him to click on the desired position
+        self.inform.emit('Click to set the origin ...')
+
+        self.plotcanvas.vis_connect('mouse_press', self.on_set_zero_click)
+
+    def on_jump_to(self):
+        """
+        Jump to a location by setting the mouse cursor location
+        :return:
+
+        """
+
+        dia_box = Dialog_box(title="Jump to Coordinates", label="Enter the coordinates in format X,Y:")
+
+        if dia_box.ok is True:
+            try:
+                location = eval(dia_box.location)
+                if not isinstance(location, tuple):
+                    self.inform.emit("Wrong coordinates. Enter coordinates in format: X,Y")
+                    return
+            except:
+                return
+        else:
+            return
+
+        self.plotcanvas.fit_center(loc=location)
+
+        cursor = QtGui.QCursor()
+
+        canvas_origin = self.plotcanvas.vispy_canvas.native.mapToGlobal(QtCore.QPoint(0, 0))
+        jump_loc = self.plotcanvas.vispy_canvas.translate_coords_2((location[0], location[1]))
+
+        cursor.setPos(canvas_origin.x() + jump_loc[0], (canvas_origin.y() + jump_loc[1]))
+        self.inform.emit("Done.")
+
+    def on_copy_object(self):
+
+        def initialize(obj_init, app):
+            obj_init.solid_geometry = obj.solid_geometry
+            if obj.tools:
+                obj_init.tools = obj.tools
+
+        def initialize_excellon(obj_init, app):
+            obj_init.tools = obj.tools
+
+            # drills are offset, so they need to be deep copied
+            obj_init.drills = deepcopy(obj.drills)
+            obj_init.create_geometry()
+
+        for obj in self.collection.get_selected():
+
+            obj_name = obj.options["name"]
+
+            try:
+                if isinstance(obj, FlatCAMExcellon):
+                    self.new_object("excellon", str(obj_name) + "_copy", initialize_excellon)
+                elif isinstance(obj,FlatCAMGerber):
+                    self.new_object("gerber", str(obj_name) + "_copy", initialize)
+                elif isinstance(obj,FlatCAMGeometry):
+                    self.new_object("geometry", str(obj_name) + "_copy", initialize)
+            except Exception as e:
+                return "Operation failed: %s" % str(e)
+
+    def on_copy_object2(self, custom_name):
+
+        def initialize_geometry(obj_init, app):
+            obj_init.solid_geometry = obj.solid_geometry
+            if obj.tools:
+                obj_init.tools = obj.tools
+
+        def initialize_gerber(obj_init, app):
+            obj_init.solid_geometry = obj.solid_geometry
+            obj_init.apertures = deepcopy(obj.apertures)
+            obj_init.aperture_macros = deepcopy(obj.aperture_macros)
+
+        def initialize_excellon(obj_init, app):
+            obj_init.tools = obj.tools
+            # drills are offset, so they need to be deep copied
+            obj_init.drills = deepcopy(obj.drills)
+            obj_init.create_geometry()
+
+        for obj in self.collection.get_selected():
+            obj_name = obj.options["name"]
+            try:
+                if isinstance(obj, FlatCAMExcellon):
+                    self.new_object("excellon", str(obj_name) + custom_name, initialize_excellon)
+                elif isinstance(obj,FlatCAMGerber):
+                    self.new_object("gerber", str(obj_name) + custom_name, initialize_gerber)
+                elif isinstance(obj,FlatCAMGeometry):
+                    self.new_object("geometry", str(obj_name) + custom_name, initialize_geometry)
+            except Exception as e:
+                return "Operation failed: %s" % str(e)
+
+    def on_rename_object(self, text):
+        named_obj = self.collection.get_active()
+        for obj in named_obj:
+            if obj is list:
+                self.on_rename_object(text)
+            else:
+                try:
+                    obj.options['name'] = text
+                except:
+                    log.warning("Could not rename the object in the list")
+
+    def on_copy_object_as_geometry(self):
+
+        def initialize(obj_init, app):
+            obj_init.solid_geometry = obj.solid_geometry
+            if obj.tools:
+                obj_init.tools = obj.tools
+
+        def initialize_excellon(obj, app):
+            objs = self.collection.get_selected()
+            FlatCAMGeometry.merge(objs, obj)
+
+        for obj in self.collection.get_selected():
+
+            obj_name = obj.options["name"]
+
+            try:
+                if isinstance(obj, FlatCAMExcellon):
+                    self.new_object("geometry", str(obj_name) + "_gcopy", initialize_excellon)
+                else:
+                    self.new_object("geometry", str(obj_name) + "_gcopy", initialize)
+
+            except Exception as e:
+                return "Operation failed: %s" % str(e)
+
+    def on_set_zero_click(self, event):
+        #this function will be available only for mouse left click
+        pos =[]
+        pos_canvas = self.plotcanvas.vispy_canvas.translate_coords(event.pos)
+        if event.button == 1:
+            if self.grid_status() == True:
+                pos = self.geo_editor.snap(pos_canvas[0], pos_canvas[1])
+            else:
+                pos = pos_canvas
+
+            x = 0 - pos[0]
+            y = 0 - pos[1]
+            for obj in self.collection.get_list():
+                obj.offset((x,y))
+                self.object_changed.emit(obj)
+                # obj.plot()
+            self.plot_all()
+            self.inform.emit('[success] Origin set ...')
+            self.plotcanvas.vis_disconnect('mouse_press', self.on_set_zero_click)
+
+    def on_selectall(self):
+        # delete the possible selection box around a possible selected object
+        self.delete_selection_shape()
+        for name in self.collection.get_names():
+            self.collection.set_active(name)
+            curr_sel_obj = self.collection.get_by_name(name)
+            # create the selection box around the selected object
+            self.draw_selection_shape(curr_sel_obj)
+
+    def on_preferences(self):
+
+        # add the tab if it was closed
+        self.ui.plot_tab_area.addTab(self.ui.preferences_tab, "Preferences")
+
+        # delete the absolute and relative position and messages in the infobar
+        self.ui.position_label.setText("")
+        self.ui.rel_position_label.setText("")
+
+        # Switch plot_area to preferences page
+        self.ui.plot_tab_area.setCurrentWidget(self.ui.preferences_tab)
+        self.ui.show()
+
+    def on_flipy(self):
+        obj_list = self.collection.get_selected()
+        xminlist = []
+        yminlist = []
+        xmaxlist = []
+        ymaxlist = []
+
+        if not obj_list:
+            self.inform.emit("[warning_notcl] No object selected.")
+            msg = "Please Select an object to flip!"
+            warningbox = QtWidgets.QMessageBox()
+            warningbox.setText(msg)
+            warningbox.setWindowTitle("Warning ...")
+            warningbox.setWindowIcon(QtGui.QIcon('share/warning.png'))
+            warningbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+            warningbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+            warningbox.exec_()
+        else:
+            try:
+                # first get a bounding box to fit all
+                for obj in obj_list:
+                    xmin, ymin, xmax, ymax = obj.bounds()
+                    xminlist.append(xmin)
+                    yminlist.append(ymin)
+                    xmaxlist.append(xmax)
+                    ymaxlist.append(ymax)
+
+                # get the minimum x,y and maximum x,y for all objects selected
+                xminimal = min(xminlist)
+                yminimal = min(yminlist)
+                xmaximal = max(xmaxlist)
+                ymaximal = max(ymaxlist)
+
+                px = 0.5 * (xminimal + xmaximal)
+                py = 0.5 * (yminimal + ymaximal)
+
+                # execute mirroring
+                for obj in obj_list:
+                    obj.mirror('X', [px, py])
+                    obj.plot()
+                    self.object_changed.emit(obj)
+            except Exception as e:
+                self.inform.emit("[error_notcl] Due of %s, Flip action was not executed." % str(e))
+                return
+
+    def on_flipx(self):
+        obj_list = self.collection.get_selected()
+        xminlist = []
+        yminlist = []
+        xmaxlist = []
+        ymaxlist = []
+
+        if not obj_list:
+            self.inform.emit("[warning_notcl] No object selected.")
+            msg = "Please Select an object to flip!"
+            warningbox = QtWidgets.QMessageBox()
+            warningbox.setText(msg)
+            warningbox.setWindowTitle("Warning ...")
+            warningbox.setWindowIcon(QtGui.QIcon('share/warning.png'))
+            warningbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+            warningbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+            warningbox.exec_()
+        else:
+            try:
+                # first get a bounding box to fit all
+                for obj in obj_list:
+                    xmin, ymin, xmax, ymax = obj.bounds()
+                    xminlist.append(xmin)
+                    yminlist.append(ymin)
+                    xmaxlist.append(xmax)
+                    ymaxlist.append(ymax)
+
+                # get the minimum x,y and maximum x,y for all objects selected
+                xminimal = min(xminlist)
+                yminimal = min(yminlist)
+                xmaximal = max(xmaxlist)
+                ymaximal = max(ymaxlist)
+
+                px = 0.5 * (xminimal + xmaximal)
+                py = 0.5 * (yminimal + ymaximal)
+
+                # execute mirroring
+                for obj in obj_list:
+                    obj.mirror('Y', [px, py])
+                    obj.plot()
+                    self.object_changed.emit(obj)
+            except Exception as e:
+                self.inform.emit("[error_notcl] Due of %s, Flip action was not executed." % str(e))
+                return
+
+    def on_rotate(self, silent=False, preset=None):
+        obj_list = self.collection.get_selected()
+        xminlist = []
+        yminlist = []
+        xmaxlist = []
+        ymaxlist = []
+
+        if not obj_list:
+            self.inform.emit("[warning_notcl] No object selected.")
+            msg = "Please Select an object to rotate!"
+            warningbox = QtWidgets.QMessageBox()
+            warningbox.setText(msg)
+            warningbox.setWindowTitle("Warning ...")
+            warningbox.setWindowIcon(QtGui.QIcon('share/warning.png'))
+            warningbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+            warningbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+            warningbox.exec_()
+        else:
+            if silent is False:
+                rotatebox = FCInputDialog(title="Transform", text="Enter the Angle value:",
+                                          min=-360, max=360, decimals=3)
+                num, ok = rotatebox.get_value()
+            else:
+                num = preset
+                ok = True
+
+            if ok:
+                try:
+                    # first get a bounding box to fit all
+                    for obj in obj_list:
+                        xmin, ymin, xmax, ymax = obj.bounds()
+                        xminlist.append(xmin)
+                        yminlist.append(ymin)
+                        xmaxlist.append(xmax)
+                        ymaxlist.append(ymax)
+
+                    # get the minimum x,y and maximum x,y for all objects selected
+                    xminimal = min(xminlist)
+                    yminimal = min(yminlist)
+                    xmaximal = max(xmaxlist)
+                    ymaximal = max(ymaxlist)
+                    px = 0.5 * (xminimal + xmaximal)
+                    py = 0.5 * (yminimal + ymaximal)
+
+                    for sel_obj in obj_list:
+                        sel_obj.rotate(-num, point=(px, py))
+                        sel_obj.plot()
+                        self.object_changed.emit(sel_obj)
+                except Exception as e:
+                    self.inform.emit("[error_notcl] Due of %s, rotation movement was not executed." % str(e))
+                    return
+
+    def on_skewx(self):
+        obj_list = self.collection.get_selected()
+        xminlist = []
+        yminlist = []
+
+        if not obj_list:
+            self.inform.emit("[warning_notcl] No object selected.")
+            msg = "Please Select an object to skew/shear!"
+            warningbox = QtWidgets.QMessageBox()
+            warningbox.setText(msg)
+            warningbox.setWindowTitle("Warning ...")
+            warningbox.setWindowIcon(QtGui.QIcon('share/warning.png'))
+            warningbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+            warningbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+            warningbox.exec_()
+        else:
+            skewxbox = FCInputDialog(title="Transform", text="Enter the Angle value:",
+                                          min=-360, max=360, decimals=3)
+            num, ok = skewxbox.get_value()
+            if ok:
+                # first get a bounding box to fit all
+                for obj in obj_list:
+                    xmin, ymin, xmax, ymax = obj.bounds()
+                    xminlist.append(xmin)
+                    yminlist.append(ymin)
+
+                # get the minimum x,y and maximum x,y for all objects selected
+                xminimal = min(xminlist)
+                yminimal = min(yminlist)
+
+                for obj in obj_list:
+                    obj.skew(num, 0, point=(xminimal, yminimal))
+                    obj.plot()
+                    self.object_changed.emit(obj)
+
+    def on_skewy(self):
+        obj_list = self.collection.get_selected()
+        xminlist = []
+        yminlist = []
+
+        if not obj_list:
+            self.inform.emit("[warning_notcl] No object selected.")
+            msg = "Please Select an object to skew/shear!"
+            warningbox = QtWidgets.QMessageBox()
+            warningbox.setText(msg)
+            warningbox.setWindowTitle("Warning ...")
+            warningbox.setWindowIcon(QtGui.QIcon('share/warning.png'))
+            warningbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+            warningbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+            warningbox.exec_()
+        else:
+            skewybox = FCInputDialog(title="Transform", text="Enter the Angle value:",
+                                          min=-360, max=360, decimals=3)
+            num, ok = skewybox.get_value()
+            if ok:
+                # first get a bounding box to fit all
+                for obj in obj_list:
+                    xmin, ymin, xmax, ymax = obj.bounds()
+                    xminlist.append(xmin)
+                    yminlist.append(ymin)
+
+                # get the minimum x,y and maximum x,y for all objects selected
+                xminimal = min(xminlist)
+                yminimal = min(yminlist)
+
+                for obj in obj_list:
+                    obj.skew(0, num, point=(xminimal, yminimal))
+                    obj.plot()
+                    self.object_changed.emit(obj)
+
+    def delete_first_selected(self):
+        # Keep this for later
+        try:
+            name = self.collection.get_active().options["name"]
+        except AttributeError:
+            self.log.debug("Nothing selected for deletion")
+            return
+
+        # Remove plot
+        # self.plotcanvas.figure.delaxes(self.collection.get_active().axes)
+        # self.plotcanvas.auto_adjust_axes()
+
+        # Clear form
+        self.setup_component_editor()
+
+        # Remove from dictionary
+        self.collection.delete_active()
+
+        self.inform.emit("Object deleted: %s" % name)
+
+    def on_plots_updated(self):
+        """
+        Callback used to report when the plots have changed.
+        Adjust axes and zooms to fit.
+
+        :return: None
+        """
+        # self.plotcanvas.auto_adjust_axes()
+        self.plotcanvas.vispy_canvas.update()           # TODO: Need update canvas?
+        self.on_zoom_fit(None)
+
+    # TODO: Rework toolbar 'clear', 'replot' functions
+    def on_toolbar_replot(self):
+        """
+        Callback for toolbar button. Re-plots all objects.
+
+        :return: None
+        """
+
+        self.report_usage("on_toolbar_replot")
+        self.log.debug("on_toolbar_replot()")
+
+        try:
+            self.collection.get_active().read_form()
+        except AttributeError:
+            self.log.debug("on_toolbar_replot(): AttributeError")
+            pass
+
+        self.plot_all()
+
+    def on_row_activated(self, index):
+        if index.isValid():
+            if index.internalPointer().parent_item != self.collection.root_item:
+                self.ui.notebook.setCurrentWidget(self.ui.selected_tab)
+
+    def on_zoom_fit(self, event):
+        """
+        Callback for zoom-out request. This can be either from the corresponding
+        toolbar button or the '1' key when the canvas is focused. Calls ``self.adjust_axes()``
+        with axes limits from the geometry bounds of all objects.
+
+        :param event: Ignored.
+        :return: None
+        """
+
+        self.plotcanvas.fit_view()
+
+    def grid_status(self):
+        if self.ui.grid_snap_btn.isChecked():
+            return 1
+        else:
+            return 0
+
+    def on_key_over_plot(self, event):
+        """
+        Callback for the key pressed event when the canvas is focused. Keyboard
+        shortcuts are handled here. So far, these are the shortcuts:
+
+        ==========  ============================================
+        Key         Action
+        ==========  ============================================
+        '1'         Zoom-fit. Fits the axes limits to the data.
+        '2'         Zoom-out.
+        '3'         Zoom-in.
+        'ctrl+m'         Toggle on-off the measuring tool.
+        ==========  ============================================
+
+        :param event: Ignored.
+        :return: None
+        """
+
+        self.key_modifiers = QtWidgets.QApplication.keyboardModifiers()
+
+        if self.key_modifiers == QtCore.Qt.ControlModifier:
+            if event.key == 'A':
+                self.on_selectall()
+
+            if event.key == 'C':
+                self.on_copy_object()
+
+            if event.key == 'E':
+                self.on_fileopenexcellon()
+            if event.key == 'G':
+                self.on_fileopengerber()
+
+            if event.key == 'M':
+                self.measurement_tool.run()
+
+            if event.key == 'O':
+                self.on_file_openproject()
+
+            if event.key == 'S':
+                self.on_file_saveproject()
+
+            return
+        elif self.key_modifiers == QtCore.Qt.AltModifier:
+            # place holder for further shortcut key
+            return
+        elif self.key_modifiers == QtCore.Qt.ShiftModifier:
+            # place holder for further shortcut key
+
+            # Toggle axis
+            if event.key == 'G':
+                self.on_toggle_axis()
+
+            # Rotate Object by 90 degree CCW
+            if event.key == 'R':
+                self.on_rotate(silent=True, preset=-90)
+
+            # Toggle Workspace
+            if event.key == 'W':
+                self.on_workspace_menu()
+
+            return
+        else:
+            if event.key == self.defaults['fit_key']:  # 1
+                self.on_zoom_fit(None)
+                return
+
+            if event.key == self.defaults['zoom_out_key']:  # 2
+                self.plotcanvas.zoom(1 / self.defaults['zoom_ratio'], self.mouse)
+                return
+
+            if event.key == self.defaults['zoom_in_key']:  # 3
+                self.plotcanvas.zoom(self.defaults['zoom_ratio'], self.mouse)
+                return
+
+            if event.key == 'Delete':
+                self.on_delete()
+                return
+
+            if event.key == 'Space':
+                if self.collection.get_active() is not None:
+                    self.collection.get_active().ui.plot_cb.toggle()
+                    self.delete_selection_shape()
+
+            if event.key == 'C':
+                self.on_copy_name()
+
+            if event.key == 'E':
+                self.object2editor()
+
+            if event.key == self.defaults['grid_toggle_key']:  # G
+                self.ui.grid_snap_btn.trigger()
+
+            if event.key == 'J':
+                self.on_jump_to()
+
+            if event.key == 'M':
+                self.move_tool.toggle()
+                return
+
+            if event.key == 'N':
+                self.on_new_geometry()
+
+            if event.key == 'Q':
+                if self.options["units"] == 'MM':
+                    self.general_options_form.general_group.units_radio.set_value("IN")
+                else:
+                    self.general_options_form.general_group.units_radio.set_value("MM")
+                self.on_toggle_units()
+
+            if event.key == 'R':
+                self.on_rotate(silent=True, preset=90)
+
+            if event.key == 'S':
+                self.on_toggle_shell()
+
+            if event.key == 'T':
+                self.transform_tool.run()
+
+            if event.key == 'V':
+                self.on_zoom_fit(None)
+
+            if event.key == 'X':
+                self.on_flipx()
+
+            if event.key == 'Y':
+                self.on_flipy()
+
+            if event.key == '`':
+                self.on_shortcut_list()
+
+    def on_key_release_over_plot(self, event):
+        modifiers = QtWidgets.QApplication.keyboardModifiers()
+
+        if modifiers == QtCore.Qt.ControlModifier:
+            return
+        elif modifiers == QtCore.Qt.AltModifier:
+            # place holder for further shortcut key
+            return
+        elif modifiers == QtCore.Qt.ShiftModifier:
+            # place holder for further shortcut key
+            return
+        else:
+            return
+
+    def on_shortcut_list(self):
+
+        msg = '''<b>Shortcut list</b><br>
+<br>
+<b>~:</b>       Show Shortcut List<br>
+<br>
+<b>1:</b>       Zoom Fit<br>
+<b>2:</b>       Zoom Out<br>
+<b>3:</b>       Zoom In<br>
+<b>A:</b>       Draw an Arc (when in Edit Mode)<br>
+<b>C:</b>       Copy Obj_Name<br>
+<b>C:</b>       Copy Geo Item (when in Edit Mode)<br>
+<b>E:</b>       Edit Object (if selected)<br>
+<b>G:</b>       Grid On/Off<br>
+<b>J:</b>       Jump to Coordinates<br>
+<b>M:</b>       Move Obj<br>
+<b>M:</b>       Move Geo Item (when in Edit Mode)<br>
+<b>N:</b>       New Geometry<br>
+<b>N:</b>       Draw a Polygon (when in Edit Mode)<br>
+<b>O:</b>       Draw a Circle (when in Edit Mode)<br>
+<b>Q:</b>       Change Units<br>
+<b>P:</b>       Draw a Path (when in Edit Mode)<br>
+<b>R:</b>       Rotate by 90 degree CW<br>
+<b>R:</b>       Draw Rectangle (when in Edit Mode)<br>
+<b>S:</b>       Shell Toggle<br>
+<b>T:</b>       Transformation<br>
+<b>V:</b>       View Fit<br>
+<b>X:</b>       Flip on X_axis<br>
+<b>Y:</b>       Flip on Y_axis<br>
+<br>
+<b>Space:</b>    En(Dis)able Obj Plot<br>
+<b>CTRL+A:</b>   Select All<br>
+<b>CTRL+C:</b>   Copy Obj<br>
+<b>CTRL+E:</b>   Open Excellon File<br>
+<b>CTRL+G:</b>   Open Gerber File<br>
+<b>CTRL+M:</b>   Measurement Tool<br>
+<b>CTRL+O:</b>   Open Project<br>
+<b>CTRL+S:</b>   Save Project As<br>
+<b>CTRL+S:</b>   Save Object and Exit Editor (when in Edit Mode)<br>
+<br>
+<b>SHIFT+G:</b>  Toggle the axis<br>
+<b>SHIFT+R:</b>  Rotate by 90 degree CCW<br>
+<b>SHIFT+W:</b>  Toggle the workspace<br>
+<b>Del:</b>      Delete Obj'''
+
+        helpbox = QtWidgets.QMessageBox()
+        helpbox.setText(msg)
+        helpbox.setWindowTitle("Help")
+        helpbox.setWindowIcon(QtGui.QIcon('share/help.png'))
+        helpbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+        helpbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+        helpbox.exec_()
+
+    def on_copy_name(self):
+        obj = self.collection.get_active()
+        name = obj.options["name"]
+        self.clipboard.setText(name)
+        self.inform.emit("Name copied on clipboard ...")
+
+    def on_mouse_click_over_plot(self, event):
+        """
+        Callback for the mouse click event over the plot. This event is generated
+        by the Matplotlib backend and has been registered in ``self.__init__()``.
+        For details, see: http://matplotlib.org/users/event_handling.html
+
+        Default actions are:
+
+        * Copy coordinates to clipboard. Ex.: (65.5473, -13.2679)
+
+        :param event: Contains information about the event, like which button
+            was clicked, the pixel coordinates and the axes coordinates.
+        :return: None
+        """
+        self.pos = []
+
+        # So it can receive key presses
+        self.plotcanvas.vispy_canvas.native.setFocus()
+        # Set the mouse button for panning
+        self.plotcanvas.vispy_canvas.view.camera.pan_button_setting = self.defaults['global_pan_button']
+
+        self.pos_canvas = self.plotcanvas.vispy_canvas.translate_coords(event.pos)
+
+        if self.grid_status() == True:
+            self.pos = self.geo_editor.snap(self.pos_canvas[0], self.pos_canvas[1])
+            self.app_cursor.enabled = True
+        else:
+            self.pos = (self.pos_canvas[0], self.pos_canvas[1])
+            self.app_cursor.enabled = False
+
+        try:
+            App.log.debug('button=%d, x=%d, y=%d, xdata=%f, ydata=%f' % (
+            event.button, event.pos[0], event.pos[1], self.pos[0], self.pos[1]))
+            modifiers = QtWidgets.QApplication.keyboardModifiers()
+
+            if event.button == 1:
+                # Reset here the relative coordinates so there is a new reference on the click position
+                if self.rel_point1 is None:
+                    self.rel_point1 = self.pos
+                else:
+                    self.rel_point2 = copy(self.rel_point1)
+                    self.rel_point1 = self.pos
+
+                # If the SHIFT key is pressed when LMB is clicked then the coordinates are copied to clipboard
+                if modifiers == QtCore.Qt.ShiftModifier:
+                    self.clipboard.setText(self.defaults["global_point_clipboard_format"] % (self.pos[0], self.pos[1]))
+                    return
+
+                # If the CTRL key is pressed when the LMB is clicked then if the object is selected it will deselect,
+                # and if it's not selected then it will be selected
+                if modifiers == QtCore.Qt.ControlModifier:
+                    # If there is no active command (self.command_active is None) then we check if we clicked on
+                    # a object by checking the bounding limits against mouse click position
+                    if self.command_active is None:
+                        self.select_objects(key='CTRL')
+                else:
+                    # If there is no active command (self.command_active is None) then we check if we clicked on a object by
+                    # checking the bounding limits against mouse click position
+                    if self.command_active is None:
+                        self.select_objects()
+
+            self.on_mouse_move_over_plot(event, origin_click=True)
+        except Exception as e:
+            App.log.debug("App.on_mouse_click_over_plot() --> Outside plot? --> %s" % str(e))
+
+    def on_double_click_over_plot(self, event):
+        # make double click work only for the LMB
+        if event.button == 1:
+            if not self.collection.get_selected():
+                pass
+            else:
+                self.ui.notebook.setCurrentWidget(self.ui.selected_tab)
+                #delete the selection shape(S) as it may be in the way
+                self.delete_selection_shape()
+
+    def on_mouse_move_over_plot(self, event, origin_click=None):
+        """
+        Callback for the mouse motion event over the plot. This event is generated
+        by the Matplotlib backend and has been registered in ``self.__init__()``.
+        For details, see: http://matplotlib.org/users/event_handling.html
+
+        :param event: Contains information about the event.
+        :param origin_click
+        :return: None
+        """
+
+        # So it can receive key presses
+        self.plotcanvas.vispy_canvas.native.setFocus()
+        self.pos_jump = event.pos
+
+        if origin_click is True:
+            pass
+        else:
+            # if the RMB is clicked and mouse is moving over plot then 'panning_action' is True
+            if event.button == 2:
+                self.panning_action = True
+                return
+            else:
+                self.panning_action = False
+
+        if self.rel_point1 is not None:
+            try:  # May fail in case mouse not within axes
+                pos_canvas = self.plotcanvas.vispy_canvas.translate_coords(event.pos)
+                if self.grid_status():
+                    pos = self.geo_editor.snap(pos_canvas[0], pos_canvas[1])
+                    self.app_cursor.enabled = True
+                    # Update cursor
+                    self.app_cursor.set_data(np.asarray([(pos[0], pos[1])]), symbol='++', edge_color='black', size=20)
+                else:
+                    pos = (pos_canvas[0], pos_canvas[1])
+                    self.app_cursor.enabled = False
+
+                self.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
+                                               "<b>Y</b>: %.4f" % (pos[0], pos[1]))
+
+                dx = pos[0] - self.rel_point1[0]
+                dy = pos[1] - self.rel_point1[1]
+                self.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                                   "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (dx, dy))
+                self.mouse = [pos[0], pos[1]]
+
+                # if the mouse is moved and the LMB is clicked then the action is a selection
+                if event.is_dragging == 1 and event.button == 1:
+                    self.delete_selection_shape()
+                    if dx < 0:
+                        self.draw_moving_selection_shape(self.pos, pos, color=self.defaults['global_alt_sel_line'],
+                                                     face_color=self.defaults['global_alt_sel_fill'])
+                        self.selection_type = False
+                    else:
+                        self.draw_moving_selection_shape(self.pos, pos)
+                        self.selection_type = True
+
+                # delete the status message on mouse move
+                # self.inform.emit("")
+
+            except:
+                self.ui.position_label.setText("")
+                self.ui.rel_position_label.setText("")
+                self.mouse = None
+
+    def on_mouse_click_release_over_plot(self, event):
+        """
+        Callback for the mouse click release over plot. This event is generated by the Matplotlib backend
+        and has been registered in ''self.__init__()''.
+        :param event: contains information about the event.
+        :return:
+        """
+
+        pos_canvas = self.plotcanvas.vispy_canvas.translate_coords(event.pos)
+        if self.grid_status():
+            pos = self.geo_editor.snap(pos_canvas[0], pos_canvas[1])
+        else:
+            pos = (pos_canvas[0], pos_canvas[1])
+
+        # if the released mouse button was RMB then test if it was a panning motion or not, if not it was a context
+        # canvas menu
+        try:
+            if event.button == 2:  # right click
+                if self.panning_action is True:
+                    self.panning_action = False
+                else:
+                    self.cursor = QtGui.QCursor()
+                    self.ui.popMenu.popup(self.cursor.pos())
+        except Exception as e:
+            log.warning("Error: %s" % str(e))
+            return
+
+        # if the released mouse button was LMB then test if we had a right-to-left selection or a left-to-right
+        # selection and then select a type of selection ("enclosing" or "touching")
+        try:
+            if event.button == 1:  # left click
+                if self.selection_type is not None:
+                    self.selection_area_handler(self.pos, pos, self.selection_type)
+                    self.selection_type = None
+        except Exception as e:
+            log.warning("Error: %s" % str(e))
+            return
+
+    def selection_area_handler(self, start_pos, end_pos, sel_type):
+        """
+
+        :param start_pos: mouse position when the selection LMB click was done
+        :param end_pos: mouse position when the left mouse button is released
+        :param sel_type: if True it's a left to right selection (enclosure), if False it's a 'touch' selection
+        :return:
+        """
+        poly_selection = Polygon([start_pos, (end_pos[0], start_pos[1]), end_pos, (start_pos[0], end_pos[1])])
+
+        self.delete_selection_shape()
+        for obj in self.collection.get_list():
+            try:
+                # select the object(s) only if it is enabled (plotted)
+                if obj.options['plot']:
+                    poly_obj = Polygon([(obj.options['xmin'], obj.options['ymin']), (obj.options['xmax'], obj.options['ymin']),
+                                        (obj.options['xmax'], obj.options['ymax']), (obj.options['xmin'], obj.options['ymax'])])
+                    if sel_type is True:
+                        if poly_obj.within(poly_selection):
+                            # create the selection box around the selected object
+                            self.draw_selection_shape(obj)
+                            self.collection.set_active(obj.options['name'])
+                    else:
+                        if poly_selection.intersects(poly_obj):
+                            # create the selection box around the selected object
+                            self.draw_selection_shape(obj)
+                            self.collection.set_active(obj.options['name'])
+            except:
+                # the Exception here will happen if we try to select on screen and we have an newly (and empty)
+                # just created Geometry or Excellon object that do not have the xmin, xmax, ymin, ymax options.
+                # In this case poly_obj creation (see above) will fail
+                pass
+
+    def select_objects(self, key=None):
+        # list where we store the overlapped objects under our mouse left click position
+        objects_under_the_click_list = []
+
+        # Populate the list with the overlapped objects on the click position
+        curr_x, curr_y = self.pos
+        for obj in self.all_objects_list:
+            if (curr_x >= obj.options['xmin']) and (curr_x <= obj.options['xmax']) and \
+                    (curr_y >= obj.options['ymin']) and (curr_y <= obj.options['ymax']):
+                if obj.options['name'] not in objects_under_the_click_list:
+                    if obj.options['plot']:
+                        # add objects to the objects_under_the_click list only if the object is plotted
+                        # (active and not disabled)
+                        objects_under_the_click_list.append(obj.options['name'])
+        try:
+            # If there is no element in the overlapped objects list then make everyone inactive
+            # because we selected "nothing"
+            if not objects_under_the_click_list:
+                self.collection.set_all_inactive()
+                # delete the possible selection box around a possible selected object
+                self.delete_selection_shape()
+                # and as a convenience move the focus to the Project tab because Selected tab is now empty
+                self.ui.notebook.setCurrentWidget(self.ui.project_tab)
+
+            else:
+                # case when there is only an object under the click and we toggle it
+                if len(objects_under_the_click_list) == 1:
+                    if self.collection.get_active() is None :
+                        self.collection.set_active(objects_under_the_click_list[0])
+                        # create the selection box around the selected object
+                        curr_sel_obj = self.collection.get_active()
+                        self.draw_selection_shape(curr_sel_obj)
+                    elif self.collection.get_active().options['name'] not in objects_under_the_click_list:
+                        self.collection.set_all_inactive()
+                        self.delete_selection_shape()
+                        self.collection.set_active(objects_under_the_click_list[0])
+                        # create the selection box around the selected object
+                        curr_sel_obj = self.collection.get_active()
+                        self.draw_selection_shape(curr_sel_obj)
+                    else:
+                        self.collection.set_all_inactive()
+                        self.delete_selection_shape()
+                else:
+                    # If there is no selected object
+                    # make active the first element of the overlapped objects list
+                    if self.collection.get_active() is None:
+                        self.collection.set_active(objects_under_the_click_list[0])
+
+                    name_sel_obj = self.collection.get_active().options['name']
+                    # In case that there is a selected object but it is not in the overlapped object list
+                    # make that object inactive and activate the first element in the overlapped object list
+                    if name_sel_obj not in objects_under_the_click_list:
+                        self.collection.set_inactive(name_sel_obj)
+                        name_sel_obj = objects_under_the_click_list[0]
+                        self.collection.set_active(name_sel_obj)
+                    else:
+                        name_sel_obj_idx = objects_under_the_click_list.index(name_sel_obj)
+                        self.collection.set_all_inactive()
+                        self.collection.set_active(objects_under_the_click_list[(name_sel_obj_idx + 1) % len(objects_under_the_click_list)])
+
+                    curr_sel_obj = self.collection.get_active()
+                    # delete the possible selection box around a possible selected object
+                    self.delete_selection_shape()
+                    # create the selection box around the selected object
+                    self.draw_selection_shape(curr_sel_obj)
+
+                    # for obj in self.collection.get_list():
+                    #     obj.plot()
+                    # curr_sel_obj.plot(color=self.FC_dark_blue, face_color=self.FC_light_blue)
+
+                    # TODO: on selected objects change the object colors and do not draw the selection box
+                    # self.plotcanvas.vispy_canvas.update() # this updates the canvas
+        except Exception as e:
+            log.error("[error] Something went bad. %s" % str(e))
+            return
+
+    def delete_selection_shape(self):
+        self.move_tool.sel_shapes.clear()
+        self.move_tool.sel_shapes.redraw()
+
+    def draw_selection_shape(self, sel_obj):
+        """
+
+        :param sel_obj: the object for which the selection shape must be drawn
+        :return:
+        """
+
+        pt1 = (float(sel_obj.options['xmin']), float(sel_obj.options['ymin']))
+        pt2 = (float(sel_obj.options['xmax']), float(sel_obj.options['ymin']))
+        pt3 = (float(sel_obj.options['xmax']), float(sel_obj.options['ymax']))
+        pt4 = (float(sel_obj.options['xmin']), float(sel_obj.options['ymax']))
+        sel_rect = Polygon([pt1, pt2, pt3, pt4])
+
+        #blue_t = Color('blue')
+        blue_t = Color(self.defaults['global_sel_fill'])
+        blue_t.alpha = 0.3
+        self.sel_objects_list.append(self.move_tool.sel_shapes.add(sel_rect, color=self.defaults['global_sel_line'],
+                                                               face_color=blue_t, update=True, layer=0, tolerance=None))
+
+    def draw_moving_selection_shape(self, old_coords, coords, **kwargs):
+        """
+
+        :param old_coords: old coordinates
+        :param coords: new coordinates
+        :return:
+        """
+
+        if 'color' in kwargs:
+            color = kwargs['color']
+        else:
+            color = self.defaults['global_sel_line']
+
+        if 'face_color' in kwargs:
+            face_color = kwargs['face_color']
+        else:
+            face_color = self.defaults['global_sel_fill']
+        x0, y0 = old_coords
+        x1, y1 = coords
+        pt1 = (x0, y0)
+        pt2 = (x1, y0)
+        pt3 = (x1, y1)
+        pt4 = (x0, y1)
+        sel_rect = Polygon([pt1, pt2, pt3, pt4])
+
+        color_t = Color(face_color)
+        color_t.alpha = 0.3
+        self.move_tool.sel_shapes.add(sel_rect, color=color, face_color=color_t, update=True,
+                                      layer=0, tolerance=None)
+
+    def on_file_new(self):
+        """
+        Callback for menu item File->New. Returns the application to its
+        startup state. This method is thread-safe.
+
+        :return: None
+        """
+
+        self.report_usage("on_file_new")
+
+        # Remove everything from memory
+        App.log.debug("on_file_new()")
+
+        # Clear pool
+        self.clear_pool()
+
+        # tcl needs to be reinitialized, otherwise  old shell variables etc  remains
+        self.init_tcl()
+
+        self.delete_selection_shape()
+
+        self.collection.delete_all()
+
+        self.setup_component_editor()
+
+        # Clear project filename
+        self.project_filename = None
+
+        # Load the application defaults
+        self.load_defaults()
+
+        # Re-fresh project options
+        self.on_options_app2project()
+
+        # Init Tools
+        self.init_tools()
+
+        # take the focus of the Notebook on Project Tab.
+        self.ui.notebook.setCurrentWidget(self.ui.project_tab)
+
+    def obj_properties(self):
+        self.properties_tool.run()
+
+    def on_fileopengerber(self):
+        """
+        File menu callback for opening a Gerber.
+
+        :return: None
+        """
+
+        self.report_usage("on_fileopengerber")
+        App.log.debug("on_fileopengerber()")
+
+        _filter_ = "Gerber Files (*.gbr *.ger *.gtl *.gbl *.gts *.gbs *.gtp *.gbp *.gto *.gbo *.gm1 *.gml *.gm3 *.gko " \
+                   "*.cmp *.sol *.stc *.sts *.plc *.pls *.crc *.crs *.tsm *.bsm *.ly2 *.ly15 *.dim *.mil *.grb" \
+                   "*.top *.bot *.smt *.smb *.sst *.ssb *.spt *.spb *.pho *.gdo *.art *.gbd);;" \
+                   "Protel Files (*.gtl *.gbl *.gts *.gbs *.gto *.gbo *.gtp *.gbp *.gml *.gm1 *.gm3 *.gko);;" \
+                   "Eagle Files (*.cmp *.sol *.stc *.sts *.plc *.pls *.crc *.crs *.tsm *.bsm *.ly2 *.ly15 *.dim *.mil);;" \
+                   "OrCAD Files (*.top *.bot *.smt *.smb *.sst *.ssb *.spt *.spb);;" \
+                   "Allegro Files (*.art);;" \
+                   "Mentor Files (*.pho *.gdo);;" \
+                   "All Files (*.*)"
+
+        try:
+            filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Open Gerber",
+                                                         directory=self.get_last_folder(), filter=_filter_)
+        except TypeError:
+            filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Open Gerber", filter=_filter_)
+
+        # The Qt methods above will return a QString which can cause problems later.
+        # So far json.dump() will fail to serialize it.
+        # TODO: Improve the serialization methods and remove this fix.
+        filename = str(filename)
+
+        if filename == "":
+            self.inform.emit("[warning_notcl]Open Gerber cancelled.")
+        else:
+            self.worker_task.emit({'fcn': self.open_gerber,
+                                   'params': [filename]})
+
+    def on_fileopengerber_follow(self):
+        """
+        File menu callback for opening a Gerber.
+
+        :return: None
+        """
+
+        self.report_usage("on_fileopengerber_follow")
+        App.log.debug("on_fileopengerber_follow()")
+        _filter_ = "Gerber Files (*.gbr *.ger *.gtl *.gbl *.gts *.gbs *.gtp *.gbp *.gto *.gbo *.gm1 *.gml *.gm3 *.gko " \
+                   "*.cmp *.sol *.stc *.sts *.plc *.pls *.crc *.crs *.tsm *.bsm *.ly2 *.ly15 *.dim *.mil *.grb" \
+                   "*.top *.bot *.smt *.smb *.sst *.ssb *.spt *.spb *.pho *.gdo *.art *.gbd);;" \
+                   "Protel Files (*.gtl *.gbl *.gts *.gbs *.gto *.gbo *.gtp *.gbp *.gml *.gm1 *.gm3 *.gko);;" \
+                   "Eagle Files (*.cmp *.sol *.stc *.sts *.plc *.pls *.crc *.crs *.tsm *.bsm *.ly2 *.ly15 *.dim *.mil);;" \
+                   "OrCAD Files (*.top *.bot *.smt *.smb *.sst *.ssb *.spt *.spb);;" \
+                   "Allegro Files (*.art);;" \
+                   "Mentor Files (*.pho *.gdo);;" \
+                   "All Files (*.*)"
+        try:
+            filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Open Gerber with Follow",
+                                                         directory=self.get_last_folder(), filter=_filter_)
+        except TypeError:
+            filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Open Gerber with Follow", filter=_filter_)
+
+        # The Qt methods above will return a QString which can cause problems later.
+        # So far json.dump() will fail to serialize it.
+        # TODO: Improve the serialization methods and remove this fix.
+        filename = str(filename)
+        follow = True
+
+        if filename == "":
+            self.inform.emit("[warning_notcl]Open Gerber-Follow cancelled.")
+        else:
+            self.worker_task.emit({'fcn': self.open_gerber,
+                                   'params': [filename, follow]})
+
+    def on_fileopenexcellon(self):
+        """
+        File menu callback for opening an Excellon file.
+
+        :return: None
+        """
+
+        self.report_usage("on_fileopenexcellon")
+        App.log.debug("on_fileopenexcellon()")
+
+        _filter_ = "Excellon Files (*.drl *.txt *.xln *.drd *.tap *.exc);;" \
+                   "All Files (*.*)"
+
+        try:
+            filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Open Excellon",
+                                                         directory=self.get_last_folder(), filter=_filter_)
+        except TypeError:
+            filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Open Excellon", filter=_filter_)
+
+        # The Qt methods above will return a QString which can cause problems later.
+        # So far json.dump() will fail to serialize it.
+        # TODO: Improve the serialization methods and remove this fix.
+        filename = str(filename)
+
+        if filename == "":
+            self.inform.emit("[warning_notcl]Open Excellon cancelled.")
+        else:
+            self.worker_task.emit({'fcn': self.open_excellon,
+                                   'params': [filename]})
+
+    def on_fileopengcode(self):
+        """
+        File menu call back for opening gcode.
+
+        :return: None
+        """
+
+        self.report_usage("on_fileopengcode")
+        App.log.debug("on_fileopengcode()")
+
+        # https://bobcadsupport.com/helpdesk/index.php?/Knowledgebase/Article/View/13/5/known-g-code-file-extensions
+        _filter_ = "G-Code Files (*.txt *.nc *.ncc *.tap *.gcode *.cnc *.ecs *.fnc *.dnc *.ncg *.gc *.fan *.fgc" \
+                   " *.din *.xpi *.hnc *.h *.i *.ncp *.min *.gcd *.rol *.mpr *.ply *.out *.eia *.plt *.sbp *.mpf);;" \
+                   "All Files (*.*)"
+        try:
+            filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Open G-Code",
+                                                         directory=self.get_last_folder(), filter=_filter_)
+        except TypeError:
+            filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Open G-Code", filter=_filter_)
+
+        # The Qt methods above will return a QString which can cause problems later.
+        # So far json.dump() will fail to serialize it.
+        # TODO: Improve the serialization methods and remove this fix.
+        filename = str(filename)
+
+        if filename == "":
+            self.inform.emit("[warning_notcl]Open G-Code cancelled.")
+        else:
+            self.worker_task.emit({'fcn': self.open_gcode,
+                                   'params': [filename]})
+
+    def on_file_openproject(self):
+        """
+        File menu callback for opening a project.
+
+        :return: None
+        """
+
+        self.report_usage("on_file_openproject")
+        App.log.debug("on_file_openproject()")
+        _filter_ = "FlatCAM Project (*.FlatPrj);;All Files (*.*)"
+        try:
+            filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Open Project",
+                                                         directory=self.get_last_folder(), filter=_filter_)
+        except TypeError:
+            filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Open Project", filter = _filter_)
+
+        # The Qt methods above will return a QString which can cause problems later.
+        # So far json.dump() will fail to serialize it.
+        # TODO: Improve the serialization methods and remove this fix.
+        filename = str(filename)
+
+        if filename == "":
+            self.inform.emit("[warning_notcl]Open Project cancelled.")
+        else:
+            # 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_exportsvg(self):
+        """
+        Callback for menu item File->Export SVG.
+
+        :return: None
+        """
+        self.report_usage("on_file_exportsvg")
+        App.log.debug("on_file_exportsvg()")
+
+        obj = self.collection.get_active()
+        if obj is None:
+            self.inform.emit("WARNING: No object selected.")
+            msg = "Please Select a Geometry object to export"
+            msgbox = QtWidgets.QMessageBox()
+            msgbox.setInformativeText(msg)
+            msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+            msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+            msgbox.exec_()
+            return
+
+        # Check for more compatible types and add as required
+        if (not isinstance(obj, FlatCAMGeometry) and not isinstance(obj, FlatCAMGerber) and not isinstance(obj, FlatCAMCNCjob)
+            and not isinstance(obj, FlatCAMExcellon)):
+            msg = "[error_notcl] Only Geometry, Gerber and CNCJob objects can be used."
+            msgbox = QtWidgets.QMessageBox()
+            msgbox.setInformativeText(msg)
+            msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+            msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+            msgbox.exec_()
+            return
+
+        name = self.collection.get_active().options["name"]
+
+        filter = "SVG File (*.svg);;All Files (*.*)"
+        try:
+            filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG",
+                                                         directory=self.get_last_save_folder(), filter=filter)
+        except TypeError:
+            filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG", filter=filter)
+
+        filename = str(filename)
+
+        if filename == "":
+            self.inform.emit("[warning_notcl]Export SVG cancelled.")
+            return
+        else:
+            self.export_svg(name, filename)
+            self.file_saved.emit("SVG", filename)
+
+    def on_file_exportpng(self):
+
+        self.report_usage("on_file_exportpng")
+        App.log.debug("on_file_exportpng()")
+
+        image = _screenshot()
+        data = np.asarray(image)
+        if not data.ndim == 3 and data.shape[-1] in (3, 4):
+            self.inform.emit('[[warning_notcl]] Data must be a 3D array with last dimension 3 or 4')
+            return
+
+        filter = "PNG File (*.png);;All Files (*.*)"
+        try:
+            filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export PNG Image",
+                                                         directory=self.get_last_save_folder(), filter=filter)
+        except TypeError:
+            filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export PNG Image", filter=filter)
+
+        filename = str(filename)
+
+        if filename == "":
+            self.inform.emit("Export PNG cancelled.")
+            return
+        else:
+            write_png(filename, data)
+            self.file_saved.emit("png", filename)
+
+    def on_file_exportexcellon(self, altium_format=None):
+        """
+        Callback for menu item File->Export SVG.
+
+        :return: None
+        """
+        self.report_usage("on_file_exportexcellon")
+        App.log.debug("on_file_exportexcellon()")
+
+        obj = self.collection.get_active()
+        if obj is None:
+            self.inform.emit("[warning_notcl] No object selected.")
+            msg = "Please Select an Excellon object to export"
+            msgbox = QtWidgets.QMessageBox()
+            msgbox.setInformativeText(msg)
+            msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+            msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+            msgbox.exec_()
+            return
+
+        # Check for more compatible types and add as required
+        if not isinstance(obj, FlatCAMExcellon):
+            msg = "[warning_notcl] Only Excellon objects can be used."
+            msgbox = QtWidgets.QMessageBox()
+            msgbox.setInformativeText(msg)
+            msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+            msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+            msgbox.exec_()
+            return
+
+        name = self.collection.get_active().options["name"]
+
+        filter = "Excellon File (*.drl);;Excellon File (*.txt);;All Files (*.*)"
+        try:
+            filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export Excellon",
+                                                         directory=self.get_last_save_folder(), filter=filter)
+        except TypeError:
+            filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export Excellon", filter=filter)
+
+        filename = str(filename)
+
+        if filename == "":
+            self.inform.emit("[warning_notcl]Export Excellon cancelled.")
+            return
+        else:
+            if altium_format == None:
+                self.export_excellon(name, filename)
+                self.file_saved.emit("Excellon", filename)
+            else:
+                self.export_excellon(name, filename, altium_format=True)
+                self.file_saved.emit("Excellon", filename)
+
+    def on_file_exportdxf(self):
+        """
+                Callback for menu item File->Export DXF.
+
+                :return: None
+                """
+        self.report_usage("on_file_exportdxf")
+        App.log.debug("on_file_exportdxf()")
+
+        obj = self.collection.get_active()
+        if obj is None:
+            self.inform.emit("W[warning_notcl] No object selected.")
+            msg = "Please Select a Geometry object to export"
+            msgbox = QtWidgets.QMessageBox()
+            msgbox.setInformativeText(msg)
+            msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+            msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+            msgbox.exec_()
+            return
+
+        # Check for more compatible types and add as required
+        if not isinstance(obj, FlatCAMGeometry):
+            msg = "[error_notcl] Only Geometry objects can be used."
+            msgbox = QtWidgets.QMessageBox()
+            msgbox.setInformativeText(msg)
+            msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+            msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+            msgbox.exec_()
+            return
+
+        name = self.collection.get_active().options["name"]
+
+        filter = "DXF File (*.DXF);;All Files (*.*)"
+        try:
+            filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export DXF",
+                                                         directory=self.get_last_save_folder(), filter=filter)
+        except TypeError:
+            filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export DXF", filter=filter)
+
+        filename = str(filename)
+
+        if filename == "":
+            self.inform.emit("[warning_notcl]Export DXF cancelled.")
+            return
+        else:
+            self.export_dxf(name, filename)
+            self.file_saved.emit("DXF", filename)
+
+    def on_file_importsvg(self, type_of_obj):
+        """
+        Callback for menu item File->Import SVG.
+        :param type_of_obj: to import the SVG as Geometry or as Gerber
+        :type type_of_obj: str
+        :return: None
+        """
+        self.report_usage("on_file_importsvg")
+        App.log.debug("on_file_importsvg()")
+
+        filter = "SVG File (*.svg);;All Files (*.*)"
+        try:
+            filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Import SVG",
+                                                         directory=self.get_last_folder(), filter=filter)
+        except TypeError:
+            filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Import SVG", filter=filter)
+
+        filename = str(filename)
+        if type_of_obj is not "geometry" and type_of_obj is not "gerber":
+            type_of_obj = "geometry"
+
+        if filename == "":
+            self.inform.emit("[warning_notcl]Open cancelled.")
+        else:
+            self.worker_task.emit({'fcn': self.import_svg,
+                                   'params': [filename, type_of_obj]})
+            #  self.import_svg(filename, "geometry")
+
+    def on_file_importdxf(self, type_of_obj):
+        """
+        Callback for menu item File->Import DXF.
+        :param type_of_obj: to import the DXF as Geometry or as Gerber
+        :type type_of_obj: str
+        :return: None
+        """
+        self.report_usage("on_file_importdxf")
+        App.log.debug("on_file_importdxf()")
+
+        filter = "DXF File (*.DXF);;All Files (*.*)"
+        try:
+            filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Import DXF",
+                                                         directory=self.get_last_folder(), filter=filter)
+        except TypeError:
+            filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Import DXF", filter=filter)
+
+        filename = str(filename)
+        if type_of_obj is not "geometry" and type_of_obj is not "gerber":
+            type_of_obj = "geometry"
+
+        if filename == "":
+            self.inform.emit("[warning_notcl]Open cancelled.")
+        else:
+            self.worker_task.emit({'fcn': self.import_dxf,
+                                   'params': [filename, type_of_obj]})
+
+    def on_filerunscript(self):
+        """
+                File menu callback for loading and running a TCL script.
+
+                :return: None
+                """
+
+        self.report_usage("on_filerunscript")
+        App.log.debug("on_file_runscript()")
+        _filter_ = "TCL script (*.TCL);;TCL script (*.TXT);;All Files (*.*)"
+        try:
+            filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Open TCL script",
+                                                         directory=self.get_last_folder(), filter=_filter_)
+        except TypeError:
+            filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Open TCL script", filter=_filter_)
+
+        # The Qt methods above will return a QString which can cause problems later.
+        # So far json.dump() will fail to serialize it.
+        # TODO: Improve the serialization methods and remove this fix.
+        filename = str(filename)
+
+        if filename == "":
+            self.inform.emit("[warning_notcl]Open TCL script cancelled.")
+        else:
+            try:
+                with open(filename, "r") as tcl_script:
+                    cmd_line_shellfile_content = tcl_script.read()
+                    self.shell._sysShell.exec_command(cmd_line_shellfile_content)
+            except Exception as ext:
+                print("ERROR: ", ext)
+                sys.exit(2)
+
+    def on_file_saveproject(self):
+        """
+        Callback for menu item File->Save Project. Saves the project to
+        ``self.project_filename`` or calls ``self.on_file_saveprojectas()``
+        if set to None. The project is saved by calling ``self.save_project()``.
+
+        :return: None
+        """
+
+        self.report_usage("on_file_saveproject")
+
+        if self.project_filename is None:
+            self.on_file_saveprojectas()
+        else:
+            self.worker_task.emit({'fcn': self.save_project,
+                                   'params': [self.project_filename]})
+            # self.save_project(self.project_filename)
+
+            self.file_opened.emit("project", self.project_filename)
+
+            self.file_saved.emit("project", self.project_filename)
+
+    def on_file_saveprojectas(self, make_copy=False):
+        """
+        Callback for menu item File->Save Project As... Opens a file
+        chooser and saves the project to the given file via
+        ``self.save_project()``.
+
+        :return: None
+        """
+
+        self.report_usage("on_file_saveprojectas")
+
+        filter = "FlatCAM Project (*.FlatPrj);; All Files (*.*)"
+        try:
+            filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Save Project As ...",
+                                                         directory=self.get_last_save_folder(), filter=filter)
+        except TypeError:
+            filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Save Project As ...", filter=filter)
+
+        filename = str(filename)
+
+        if filename == '':
+            self.inform.emit("[warning_notcl]Save Project cancelled.")
+            return
+
+        try:
+            f = open(filename, 'r')
+            f.close()
+            exists = True
+        except IOError:
+            exists = False
+
+        msg = "File exists. Overwrite?"
+        if exists:
+            msgbox = QtWidgets.QMessageBox()
+            msgbox.setInformativeText(msg)
+            msgbox.setStandardButtons(QtWidgets.QMessageBox.Cancel |QtWidgets.QMessageBox.Ok)
+            msgbox.setDefaultButton(QtWidgets.QMessageBox.Cancel)
+            result = msgbox.exec_()
+            if result ==QtWidgets.QMessageBox.Cancel:
+                return
+
+        self.worker_task.emit({'fcn': self.save_project,
+                               'params': [filename]})
+        # self.save_project(filename)
+        self.file_opened.emit("project", filename)
+
+        self.file_saved.emit("project", filename)
+        if not make_copy:
+            self.project_filename = filename
+
+    def export_svg(self, obj_name, filename, scale_factor=0.00):
+        """
+        Exports a Geometry Object to an SVG file.
+
+        :param filename: Path to the SVG file to save to.
+        :return:
+        """
+        if filename is None:
+            filename = self.defaults["global_last_save_folder"]
+
+        self.log.debug("export_svg()")
+
+        try:
+            obj = self.collection.get_by_name(str(obj_name))
+        except:
+            # TODO: The return behavior has not been established... should raise exception?
+            return "Could not retrieve object: %s" % obj_name
+
+        with self.proc_container.new("Exporting SVG") as proc:
+            exported_svg = obj.export_svg(scale_factor=scale_factor)
+
+            # Determine bounding area for svg export
+            bounds = obj.bounds()
+            size = obj.size()
+
+            # Convert everything to strings for use in the xml doc
+            svgwidth = str(size[0])
+            svgheight = str(size[1])
+            minx = str(bounds[0])
+            miny = str(bounds[1] - size[1])
+            uom = obj.units.lower()
+
+            # Add a SVG Header and footer to the svg output from shapely
+            # The transform flips the Y Axis so that everything renders
+            # properly within svg apps such as inkscape
+            svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
+                         'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
+            svg_header += 'width="' + svgwidth + uom + '" '
+            svg_header += 'height="' + svgheight + uom + '" '
+            svg_header += 'viewBox="' + minx + ' ' + miny + ' ' + svgwidth + ' ' + svgheight + '">'
+            svg_header += '<g transform="scale(1,-1)">'
+            svg_footer = '</g> </svg>'
+            svg_elem = svg_header + exported_svg + svg_footer
+
+            # Parse the xml through a xml parser just to add line feeds
+            # and to make it look more pretty for the output
+            svgcode = parse_xml_string(svg_elem)
+            with open(filename, 'w') as fp:
+                fp.write(svgcode.toprettyxml())
+
+            self.file_saved.emit("SVG", filename)
+            self.inform.emit("[success] SVG file exported to " + filename)
+
+    def export_svg_negative(self, obj_name, box_name, filename, boundary, scale_factor=0.00, use_thread=True):
+        """
+        Exports a Geometry Object to an SVG file in negative.
+
+        :param filename: Path to the SVG file to save to.
+        :param: use_thread: If True use threads
+        :type: Bool
+        :return:
+        """
+
+        if filename is None:
+            filename = self.defaults["global_last_save_folder"]
+
+        self.log.debug("export_svg() negative")
+
+        try:
+            obj = self.collection.get_by_name(str(obj_name))
+        except:
+            # TODO: The return behavior has not been established... should raise exception?
+            return "Could not retrieve object: %s" % obj_name
+
+        try:
+            box = self.collection.get_by_name(str(box_name))
+        except:
+            # TODO: The return behavior has not been established... should raise exception?
+            return "Could not retrieve object: %s" % box_name
+
+        if box is None:
+            self.inform.emit("[warning_notcl]No object Box. Using instead %s" % obj)
+            box = obj
+
+        def make_negative_film():
+            exported_svg = obj.export_svg(scale_factor=scale_factor)
+
+            self.progress.emit(40)
+
+            # Determine bounding area for svg export
+            bounds = box.bounds()
+            size = box.size()
+
+            uom = obj.units.lower()
+
+            # Convert everything to strings for use in the xml doc
+            svgwidth = str(size[0] + (2 * boundary))
+            svgheight = str(size[1] + (2 * boundary))
+            minx = str(bounds[0] - boundary)
+            miny = str(bounds[1] + boundary + size[1])
+            miny_rect = str(bounds[1] - boundary)
+
+            # Add a SVG Header and footer to the svg output from shapely
+            # The transform flips the Y Axis so that everything renders
+            # properly within svg apps such as inkscape
+            svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
+                         'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
+            svg_header += 'width="' + svgwidth + uom + '" '
+            svg_header += 'height="' + svgheight + uom + '" '
+            svg_header += 'viewBox="' + minx + ' -' + miny + ' ' + svgwidth + ' ' + svgheight + '" '
+            svg_header += '>'
+            svg_header += '<g transform="scale(1,-1)">'
+            svg_footer = '</g> </svg>'
+
+            self.progress.emit(60)
+
+            # Change the attributes of the exported SVG
+            # We don't need stroke-width - wrong, we do when we have lines with certain width
+            # We set opacity to maximum
+            # We set the color to WHITE
+            root = ET.fromstring(exported_svg)
+            for child in root:
+                child.set('fill', '#FFFFFF')
+                child.set('opacity', '1.0')
+                child.set('stroke', '#FFFFFF')
+
+            # first_svg_elem = 'rect x="' + minx + '" ' + 'y="' + miny_rect + '" '
+            # first_svg_elem += 'width="' + svgwidth + '" ' + 'height="' + svgheight + '" '
+            # first_svg_elem += 'fill="#000000" opacity="1.0" stroke-width="0.0"'
+
+            first_svg_elem_tag = 'rect'
+            first_svg_elem_attribs = {
+                'x': minx,
+                'y': miny_rect,
+                'width': svgwidth,
+                'height': svgheight,
+                'id': 'neg_rect',
+                'style': 'fill:#000000;opacity:1.0;stroke-width:0.0'
+            }
+
+            root.insert(0, ET.Element(first_svg_elem_tag, first_svg_elem_attribs))
+            exported_svg = ET.tostring(root)
+
+            svg_elem = svg_header + str(exported_svg) + svg_footer
+            self.progress.emit(80)
+
+            # Parse the xml through a xml parser just to add line feeds
+            # and to make it look more pretty for the output
+            doc = parse_xml_string(svg_elem)
+            with open(filename, 'w') as fp:
+                fp.write(doc.toprettyxml())
+
+            self.progress.emit(100)
+
+            self.file_saved.emit("SVG", filename)
+            self.inform.emit("[success] SVG file exported to " + filename)
+
+        if use_thread is True:
+            proc = self.proc_container.new("Generating Film ... Please wait.")
+
+            def job_thread_film(app_obj):
+                try:
+                    make_negative_film()
+                except Exception as e:
+                    proc.done()
+                    return
+                proc.done()
+
+            self.worker_task.emit({'fcn': job_thread_film, 'params': [self]})
+        else:
+            make_negative_film()
+
+    def export_svg_black(self, obj_name, box_name, filename, scale_factor=0.00, use_thread=True):
+        """
+        Exports a Geometry Object to an SVG file in negative.
+
+        :param filename: Path to the SVG file to save to.
+        :param: use_thread: If True use threads
+        :type: Bool
+        :return:
+        """
+
+        if filename is None:
+            filename = self.defaults["global_last_save_folder"]
+
+        self.log.debug("export_svg() black")
+
+        try:
+            obj = self.collection.get_by_name(str(obj_name))
+        except:
+            # TODO: The return behavior has not been established... should raise exception?
+            return "Could not retrieve object: %s" % obj_name
+
+        try:
+            box = self.collection.get_by_name(str(box_name))
+        except:
+            # TODO: The return behavior has not been established... should raise exception?
+            return "Could not retrieve object: %s" % box_name
+
+        if box is None:
+            self.inform.emit("[warning_notcl]No object Box. Using instead %s" % obj)
+            box = obj
+
+        def make_black_film():
+            exported_svg = obj.export_svg(scale_factor=scale_factor)
+
+            self.progress.emit(40)
+
+            # Change the attributes of the exported SVG
+            # We don't need stroke-width
+            # We set opacity to maximum
+            # We set the colour to WHITE
+            root = ET.fromstring(exported_svg)
+            for child in root:
+                child.set('fill', '#000000')
+                child.set('opacity', '1.0')
+                child.set('stroke', '#000000')
+
+            exported_svg = ET.tostring(root)
+
+            # Determine bounding area for svg export
+            bounds = box.bounds()
+            size = box.size()
+
+            # This contain the measure units
+            uom = obj.units.lower()
+
+            # Define a boundary around SVG of about 1.0mm (~39mils)
+            if uom in "mm":
+                boundary = 1.0
+            else:
+                boundary = 0.0393701
+
+            self.progress.emit(80)
+
+            # Convert everything to strings for use in the xml doc
+            svgwidth = str(size[0] + (2 * boundary))
+            svgheight = str(size[1] + (2 * boundary))
+            minx = str(bounds[0] - boundary)
+            miny = str(bounds[1] + boundary + size[1])
+
+            self.log.debug(minx)
+            self.log.debug(miny)
+
+            # Add a SVG Header and footer to the svg output from shapely
+            # The transform flips the Y Axis so that everything renders
+            # properly within svg apps such as inkscape
+            svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
+                         'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
+            svg_header += 'width="' + svgwidth + uom + '" '
+            svg_header += 'height="' + svgheight + uom + '" '
+            svg_header += 'viewBox="' + minx + ' -' + miny + ' ' + svgwidth + ' ' + svgheight + '" '
+            svg_header += '>'
+            svg_header += '<g transform="scale(1,-1)">'
+            svg_footer = '</g> </svg>'
+
+            svg_elem = str(svg_header) + str(exported_svg) + str(svg_footer)
+
+            self.progress.emit(90)
+
+            # Parse the xml through a xml parser just to add line feeds
+            # and to make it look more pretty for the output
+            doc = parse_xml_string(svg_elem)
+            with open(filename, 'w') as fp:
+                fp.write(doc.toprettyxml())
+            self.progress.emit(100)
+
+            self.file_saved.emit("SVG", filename)
+            self.inform.emit("[success] SVG file exported to " + filename)
+
+        if use_thread is True:
+            proc = self.proc_container.new("Generating Film ... Please wait.")
+
+            def job_thread_film(app_obj):
+                try:
+                    make_black_film()
+                except Exception as e:
+                    proc.done()
+                    return
+                proc.done()
+
+            self.worker_task.emit({'fcn': job_thread_film, 'params': [self]})
+        else:
+            make_black_film()
+
+    def export_excellon(self, obj_name, filename, altium_format=None, use_thread=True):
+        """
+        Exports a Geometry Object to an Excellon file.
+
+        :param filename: Path to the Excellon file to save to.
+        :return:
+        """
+        if filename is None:
+            filename = self.defaults["global_last_save_folder"]
+
+        self.log.debug("export_excellon()")
+
+        format_exc = ';FILE_FORMAT=2:4\n'
+        units = ''
+
+        try:
+            obj = self.collection.get_by_name(str(obj_name))
+        except:
+            # TODO: The return behavior has not been established... should raise exception?
+            return "Could not retrieve object: %s" % obj_name
+
+        # updated units
+        units = self.general_options_form.general_group.units_radio.get_value().upper()
+        if units == 'IN' or units == 'INCH':
+            units = 'INCH'
+
+        elif units == 'MM' or units == 'METRIC':
+            units ='METRIC'
+
+        def make_excellon():
+            try:
+                time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
+
+                header = 'M48\n'
+                header += ';EXCELLON GENERATED BY FLATCAM - www.flatcam.org 2018\n'
+                header += ';Filename: %s' % str(obj_name) + '\n'
+                header += ';Created on : %s' % time_str + '\n'
+
+                if altium_format == None:
+                    has_slots, excellon_code = obj.export_excellon()
+                    header += units + '\n'
+
+                    for tool in obj.tools:
+                        if units == 'METRIC':
+                            header += 'T' + str(tool) + 'F00S00' + 'C' + '%.2f' % float(obj.tools[tool]['C']) + '\n'
+                        else:
+                            header += 'T' + str(tool) + 'F00S00' + 'C' + '%.4f' % float(obj.tools[tool]['C']) + '\n'
+                else:
+                    has_slots, excellon_code = obj.export_excellon_altium()
+                    header += 'INCH,LZ\n'
+                    header += format_exc
+
+                    for tool in obj.tools:
+                        if units == 'METRIC':
+                            header += 'T' + str(tool) + 'F00S00' + 'C' + \
+                                      '%.4f' % (float(obj.tools[tool]['C']) / 25.4) + '\n'
+                        else:
+                            header += 'T' + str(tool) + 'F00S00' + 'C' + '%.4f' % float(obj.tools[tool]['C']) + '\n'
+
+                header += '%\n'
+                footer = 'M30\n'
+
+                exported_excellon = header
+                exported_excellon += excellon_code
+                exported_excellon += footer
+
+                with open(filename, 'w') as fp:
+                    fp.write(exported_excellon)
+
+                self.file_saved.emit("Excellon", filename)
+                self.inform.emit("[success] Excellon file exported to " + filename)
+            except:
+                return 'fail'
+
+        if use_thread is True:
+
+            with self.proc_container.new("Exporting Excellon") as proc:
+
+                def job_thread_exc(app_obj):
+                    ret = make_excellon()
+                    if ret == 'fail':
+                        self.inform.emit('[error_notcl] Could not export Excellon file.')
+                        return
+
+                self.worker_task.emit({'fcn': job_thread_exc, 'params': [self]})
+        else:
+            ret = make_excellon()
+            if ret == 'fail':
+                self.inform.emit('[error_notcl] Could not export Excellon file.')
+                return
+
+    def export_dxf(self, obj_name, filename, use_thread=True):
+        """
+        Exports a Geometry Object to an DXF file.
+
+        :param filename: Path to the DXF file to save to.
+        :return:
+        """
+        if filename is None:
+            filename = self.defaults["global_last_save_folder"]
+
+        self.log.debug("export_dxf()")
+
+        format_exc = ''
+        units = ''
+
+        try:
+            obj = self.collection.get_by_name(str(obj_name))
+        except:
+            # TODO: The return behavior has not been established... should raise exception?
+            return "Could not retrieve object: %s" % obj_name
+
+        # updated units
+        units = self.general_options_form.general_group.units_radio.get_value().upper()
+        if units == 'IN' or units == 'INCH':
+            units = 'INCH'
+        elif units == 'MM' or units == 'METIRC':
+            units ='METRIC'
+
+
+        def make_dxf():
+            try:
+                dxf_code = obj.export_dxf()
+                dxf_code.saveas(filename)
+
+                self.file_saved.emit("DXF", filename)
+                self.inform.emit("[success] DXF file exported to " + filename)
+            except:
+                return 'fail'
+
+        if use_thread is True:
+
+            with self.proc_container.new("Exporting DXF") as proc:
+
+                def job_thread_exc(app_obj):
+                    ret = make_dxf()
+                    if ret == 'fail':
+                        self.inform.emit('[[warning_notcl]] Could not export DXF file.')
+                        return
+
+                self.worker_task.emit({'fcn': job_thread_exc, 'params': [self]})
+        else:
+            ret = make_dxf()
+            if ret == 'fail':
+                self.inform.emit('[[warning_notcl]] Could not export DXF file.')
+                return
+
+    def import_svg(self, filename, geo_type='geometry', outname=None):
+        """
+        Adds a new Geometry Object to the projects and populates
+        it with shapes extracted from the SVG file.
+
+        :param filename: Path to the SVG file.
+        :param outname:
+        :return:
+        """
+        obj_type = ""
+        if geo_type is None or geo_type == "geometry":
+            obj_type = "geometry"
+        elif geo_type == "gerber":
+            obj_type = geo_type
+        else:
+            self.inform.emit("[error_notcl] Not supported type was choosed as parameter. "
+                             "Only Geometry and Gerber are supported")
+            return
+
+        units = self.general_options_form.general_group.units_radio.get_value().upper()
+
+        def obj_init(geo_obj, app_obj):
+            geo_obj.import_svg(filename, obj_type, units=units)
+            geo_obj.multigeo = False
+
+        with self.proc_container.new("Importing SVG") as proc:
+
+            # Object name
+            name = outname or filename.split('/')[-1].split('\\')[-1]
+
+            self.new_object(obj_type, name, obj_init, autoselected=False)
+            self.progress.emit(20)
+            # Register recent file
+            self.file_opened.emit("svg", filename)
+
+            # GUI feedback
+            self.inform.emit("[success] Opened: " + filename)
+            self.progress.emit(100)
+
+    def import_dxf(self, filename, geo_type='geometry', outname=None):
+        """
+        Adds a new Geometry Object to the projects and populates
+        it with shapes extracted from the DXF file.
+
+        :param filename: Path to the DXF file.
+        :param outname:
+        :type putname: str
+        :return:
+        """
+
+        obj_type = ""
+        if geo_type is None or geo_type == "geometry":
+            obj_type = "geometry"
+        elif geo_type == "gerber":
+            obj_type = geo_type
+        else:
+            self.inform.emit("[error_notcl] Not supported type was choosed as parameter. "
+                             "Only Geometry and Gerber are supported")
+            return
+
+        units = self.general_options_form.general_group.units_radio.get_value().upper()
+
+        def obj_init(geo_obj, app_obj):
+            geo_obj.import_dxf(filename, obj_type, units=units)
+            geo_obj.multigeo = False
+
+        with self.proc_container.new("Importing DXF") as proc:
+
+            # Object name
+            name = outname or filename.split('/')[-1].split('\\')[-1]
+
+            self.new_object(obj_type, name, obj_init, autoselected=False)
+            self.progress.emit(20)
+            # Register recent file
+            self.file_opened.emit("dxf", filename)
+
+            # GUI feedback
+            self.inform.emit("[success] Opened: " + filename)
+            self.progress.emit(100)
+
+    def import_image(self, filename, type='gerber', dpi=96, mode='black', mask=[250, 250, 250, 250], outname=None):
+        """
+        Adds a new Geometry Object to the projects and populates
+        it with shapes extracted from the SVG file.
+
+        :param filename: Path to the SVG file.
+        :param outname:
+        :return:
+        """
+        obj_type = ""
+        if type is None or type == "geometry":
+            obj_type = "geometry"
+        elif type == "gerber":
+            obj_type = type
+        else:
+            self.inform.emit("[error_notcl] Not supported type was picked as parameter. "
+                             "Only Geometry and Gerber are supported")
+            return
+
+        def obj_init(geo_obj, app_obj):
+            geo_obj.import_image(filename, units=units, dpi=dpi, mode=mode, mask=mask)
+            geo_obj.multigeo = False
+
+        with self.proc_container.new("Importing Image") as proc:
+
+            # Object name
+            name = outname or filename.split('/')[-1].split('\\')[-1]
+            units = self.general_options_form.general_group.units_radio.get_value()
+
+            self.new_object(obj_type, name, obj_init)
+            self.progress.emit(20)
+            # Register recent file
+            self.file_opened.emit("image", filename)
+
+            # GUI feedback
+            self.inform.emit("[success] Opened: " + filename)
+            self.progress.emit(100)
+
+    def open_gerber(self, filename, follow=False, outname=None):
+        """
+        Opens a Gerber file, parses it and creates a new object for
+        it in the program. Thread-safe.
+
+        :param outname: Name of the resulting object. None causes the
+            name to be that of the file.
+        :param filename: Gerber file filename
+        :type filename: str
+        :param follow: If true, the parser will not create polygons, just lines
+            following the gerber path.
+        :type follow: bool
+        :return: None
+        """
+
+        # How the object should be initialized
+        def obj_init(gerber_obj, app_obj):
+
+            assert isinstance(gerber_obj, FlatCAMGerber), \
+                "Expected to initialize a FlatCAMGerber but got %s" % type(gerber_obj)
+
+            # Opening the file happens here
+            self.progress.emit(30)
+            try:
+                gerber_obj.parse_file(filename, follow=follow)
+            except IOError:
+                app_obj.inform.emit("[error_notcl] Failed to open file: " + filename)
+                app_obj.progress.emit(0)
+                self.inform.emit('[error_notcl] Failed to open file: ' + filename)
+                return "fail"
+            except ParseError as err:
+                app_obj.inform.emit("[error_notcl] Failed to parse file: " + filename + ". " + str(err))
+                app_obj.progress.emit(0)
+                self.log.error(str(err))
+                return "fail"
+
+            except:
+                msg = "[error] An internal error has ocurred. See shell.\n"
+                msg += traceback.format_exc()
+                app_obj.inform.emit(msg)
+                return "fail"
+
+            if gerber_obj.is_empty():
+                # app_obj.inform.emit("[error] No geometry found in file: " + filename)
+                # self.collection.set_active(gerber_obj.options["name"])
+                # self.collection.delete_active()
+                self.inform.emit("[error_notcl] Object is not Gerber file or empty. Aborting object creation.")
+                return "fail"
+
+            # Further parsing
+            self.progress.emit(70)  # TODO: Note the mixture of self and app_obj used here
+
+        if follow is False:
+            App.log.debug("open_gerber()")
+        else:
+            App.log.debug("open_gerber() with 'follow' attribute")
+
+        with self.proc_container.new("Opening Gerber") as proc:
+
+            self.progress.emit(10)
+
+            # Object name
+            name = outname or filename.split('/')[-1].split('\\')[-1]
+
+            ### Object creation ###
+            ret = self.new_object("gerber", name, obj_init, autoselected=False)
+            if ret == 'fail':
+                self.inform.emit('[error_notcl] Open Gerber failed. Probable not a Gerber file.')
+                return
+
+            # Register recent file
+            self.file_opened.emit("gerber", filename)
+
+            self.progress.emit(100)
+
+            # GUI feedback
+            self.inform.emit("[success] Opened: " + filename)
+
+
+    def open_excellon(self, filename, outname=None):
+        """
+        Opens an Excellon file, parses it and creates a new object for
+        it in the program. Thread-safe.
+
+        :param outname: Name of the resulting object. None causes the
+            name to be that of the file.
+        :param filename: Excellon file filename
+        :type filename: str
+        :return: None
+        """
+
+        App.log.debug("open_excellon()")
+
+        #self.progress.emit(10)
+
+        # How the object should be initialized
+        def obj_init(excellon_obj, app_obj):
+            # self.progress.emit(20)
+
+            try:
+                ret = excellon_obj.parse_file(filename)
+                if ret == "fail":
+                    self.inform.emit("[error_notcl] This is not Excellon file.")
+                    return "fail"
+            except IOError:
+                app_obj.inform.emit("[error_notcl] Cannot open file: " + filename)
+                self.progress.emit(0)  # TODO: self and app_bjj mixed
+                return "fail"
+
+            except:
+                msg = "[error_notcl] An internal error has occurred. See shell.\n"
+                msg += traceback.format_exc()
+                app_obj.inform.emit(msg)
+                return "fail"
+
+            ret = excellon_obj.create_geometry()
+            if ret == 'fail':
+                return "fail"
+
+            if excellon_obj.is_empty():
+                app_obj.inform.emit("[error_notcl] No geometry found in file: " + filename)
+                return "fail"
+
+        with self.proc_container.new("Opening Excellon."):
+
+            # Object name
+            name = outname or filename.split('/')[-1].split('\\')[-1]
+
+            ret = self.new_object("excellon", name, obj_init, autoselected=False)
+            if ret == 'fail':
+                self.inform.emit('[error_notcl] Open Excellon file failed. Probable not an Excellon file.')
+                return
+
+                # Register recent file
+            self.file_opened.emit("excellon", filename)
+
+            # GUI feedback
+            self.inform.emit("[success] Opened: " + filename)
+            # self.progress.emit(100)
+
+    def open_gcode(self, filename, outname=None):
+        """
+        Opens a G-gcode file, parses it and creates a new object for
+        it in the program. Thread-safe.
+
+        :param outname: Name of the resulting object. None causes the
+            name to be that of the file.
+        :param filename: G-code file filename
+        :type filename: str
+        :return: None
+        """
+        App.log.debug("open_gcode()")
+
+        # How the object should be initialized
+        def obj_init(job_obj, app_obj_):
+            """
+
+            :type app_obj_: App
+            """
+            assert isinstance(app_obj_, App), \
+                "Initializer expected App, got %s" % type(app_obj_)
+
+            self.progress.emit(10)
+
+            try:
+                f = open(filename)
+                gcode = f.read()
+                f.close()
+            except IOError:
+                app_obj_.inform.emit("[error_notcl] Failed to open " + filename)
+                self.progress.emit(0)
+                return "fail"
+
+            job_obj.gcode = gcode
+
+            self.progress.emit(20)
+
+            ret = job_obj.gcode_parse()
+            if ret == "fail":
+                self.inform.emit("[error_notcl] This is not GCODE")
+                return "fail"
+
+            self.progress.emit(60)
+            job_obj.create_geometry()
+
+        with self.proc_container.new("Opening G-Code."):
+
+            # Object name
+            name = outname or filename.split('/')[-1].split('\\')[-1]
+
+            # New object creation and file processing
+            ret = self.new_object("cncjob", name, obj_init, autoselected=False)
+            if ret == 'fail':
+                self.inform.emit("[error_notcl] Failed to create CNCJob Object. Probable not a GCode file.\n "
+                                 "Attempting to create a FlatCAM CNCJob Object from "
+                                 "G-Code file failed during processing")
+                return "fail"
+
+            # Register recent file
+            self.file_opened.emit("cncjob", filename)
+
+            # GUI feedback
+            self.inform.emit("[success] Opened: " + filename)
+            self.progress.emit(100)
+
+    def open_project(self, filename, run_from_arg=None):
+        """
+        Loads a project from the specified file.
+
+        1) Loads and parses file
+        2) Registers the file as recently opened.
+        3) Calls on_file_new()
+        4) Updates options
+        5) Calls new_object() with the object's from_dict() as init method.
+        6) Calls plot_all()
+
+        :param filename:  Name of the file from which to load.
+        :type filename: str
+        :return: None
+        """
+        App.log.debug("Opening project: " + filename)
+
+        # Open and parse
+        try:
+            f = open(filename, 'r')
+        except IOError:
+            App.log.error("Failed to open project file: %s" % filename)
+            self.inform.emit("[error_notcl] Failed to open project file: %s" % filename)
+            return
+
+        try:
+            d = json.load(f, object_hook=dict2obj)
+        except:
+            App.log.error("Failed to parse project file: %s" % filename)
+            self.inform.emit("[error_notcl] Failed to parse project file: %s" % filename)
+            f.close()
+            return
+
+        self.file_opened.emit("project", filename)
+
+        # Clear the current project
+        ## NOT THREAD SAFE ##
+        if run_from_arg is True:
+            pass
+        else:
+            self.on_file_new()
+
+        #Project options
+        self.options.update(d['options'])
+        self.project_filename = filename
+        # self.ui.units_label.setText("[" + self.options["units"] + "]")
+        self.set_screen_units(self.options["units"])
+
+        # Re create objects
+        App.log.debug("Re-creating objects...")
+        for obj in d['objs']:
+            def obj_init(obj_inst, app_inst):
+                obj_inst.from_dict(obj)
+            App.log.debug(obj['kind'] + ":  " + obj['options']['name'])
+            self.new_object(obj['kind'], obj['options']['name'], obj_init, active=False, fit=False, plot=True)
+
+        # self.plot_all()
+        self.inform.emit("[success] Project loaded from: " + filename)
+        App.log.debug("Project loaded")
+
+    def propagate_defaults(self, silent=False):
+        """
+        This method is used to set default values in classes. It's
+        an alternative to project options but allows the use
+        of values invisible to the user.
+
+        :return: None
+        """
+
+        if silent is False:
+            self.log.debug("propagate_defaults()")
+
+        # Which objects to update the given parameters.
+        routes = {
+            "global_zdownrate": CNCjob,
+            "excellon_zeros": Excellon,
+            "excellon_format_upper_in": Excellon,
+            "excellon_format_lower_in": Excellon,
+            "excellon_format_upper_mm": Excellon,
+            "excellon_format_lower_mm": Excellon,
+            "excellon_units": Excellon,
+            "gerber_use_buffer_for_union": Gerber,
+            "geometry_multidepth": Geometry
+        }
+
+        for param in routes:
+            if param in routes[param].defaults:
+                try:
+                    routes[param].defaults[param] = self.defaults[param]
+                    if silent is False:
+                        self.log.debug("  " + param + " OK")
+                except KeyError:
+                    if silent is False:
+                        self.log.debug("  ERROR: " + param + " not in defaults.")
+            else:
+                # Try extracting the name:
+                # classname_param here is param in the object
+                if param.find(routes[param].__name__.lower() + "_") == 0:
+                    p = param[len(routes[param].__name__) + 1:]
+                    if p in routes[param].defaults:
+                        routes[param].defaults[p] = self.defaults[param]
+                        if silent is False:
+                            self.log.debug("  " + param + " OK!")
+
+    def restore_main_win_geom(self):
+        try:
+            self.ui.setGeometry(self.defaults["global_def_win_x"],
+                                self.defaults["global_def_win_y"],
+                                self.defaults["global_def_win_w"],
+                                self.defaults["global_def_win_h"])
+            self.ui.splitter.setSizes([self.defaults["def_notebook_width"], 0])
+        except KeyError:
+            pass
+
+    def plot_all(self):
+        """
+        Re-generates all plots from all objects.
+
+        :return: None
+        """
+        self.log.debug("Plot_all()")
+
+        for obj in self.collection.get_list():
+            def worker_task(obj):
+                with self.proc_container.new("Plotting"):
+                    obj.plot()
+                    self.object_plotted.emit(obj)
+
+            # Send to worker
+            self.worker_task.emit({'fcn': worker_task, 'params': [obj]})
+
+
+        # self.progress.emit(10)
+        #
+        # def worker_task(app_obj):
+        #     print "worker task"
+        #     percentage = 0.1
+        #     try:
+        #         delta = 0.9 / len(self.collection.get_list())
+        #     except ZeroDivisionError:
+        #         self.progress.emit(0)
+        #         return
+        #     for obj in self.collection.get_list():
+        #         with self.proc_container.new("Plotting"):
+        #             obj.plot()
+        #             app_obj.object_plotted.emit(obj)
+        #
+        #         percentage += delta
+        #         self.progress.emit(int(percentage*100))
+        #
+        #     self.progress.emit(0)
+        #     self.plots_updated.emit()
+        #
+        # # Send to worker
+        # #self.worker.add_task(worker_task, [self])
+        # self.worker_task.emit({'fcn': worker_task, 'params': [self]})
+
+    def register_folder(self, filename):
+        self.defaults["global_last_folder"] = os.path.split(str(filename))[0]
+
+    def register_save_folder(self, filename):
+        self.defaults['global_last_save_folder'] = os.path.split(str(filename))[0]
+
+    def set_progress_bar(self, percentage, text=""):
+        self.ui.progress_bar.setValue(int(percentage))
+
+    def setup_shell(self):
+        """
+        Creates shell functions. Runs once at startup.
+
+        :return: None
+        """
+
+        self.log.debug("setup_shell()")
+
+        def shelp(p=None):
+            if not p:
+                return "Available commands:\n" + \
+                       '\n'.join(['  ' + cmd for cmd in sorted(commands)]) + \
+                       "\n\nType help <command_name> for usage.\n Example: help open_gerber"
+
+            if p not in commands:
+                return "Unknown command: %s" % p
+
+            return commands[p]["help"]
+
+        # --- Migrated to new architecture ---
+        # def options(name):
+        #     ops = self.collection.get_by_name(str(name)).options
+        #     return '\n'.join(["%s: %s" % (o, ops[o]) for o in ops])
+
+        def h(*args):
+            """
+            Pre-processes arguments to detect '-keyword value' pairs into dictionary
+            and standalone parameters into list.
+            """
+
+            kwa = {}
+            a = []
+            n = len(args)
+            name = None
+            for i in range(n):
+                match = re.search(r'^-([a-zA-Z].*)', args[i])
+                if match:
+                    assert name is None
+                    name = match.group(1)
+                    continue
+
+                if name is None:
+                    a.append(args[i])
+                else:
+                    kwa[name] = args[i]
+                    name = None
+
+            return a, kwa
+
+        @contextmanager
+        def wait_signal(signal, timeout=10000):
+            """
+            Block loop until signal emitted, timeout (ms) elapses
+            or unhandled exception happens in a thread.
+
+            :param signal: Signal to wait for.
+            """
+            loop = QtCore.QEventLoop()
+
+            # Normal termination
+            signal.connect(loop.quit)
+
+            # Termination by exception in thread
+            self.thread_exception.connect(loop.quit)
+
+            status = {'timed_out': False}
+
+            def report_quit():
+                status['timed_out'] = True
+                loop.quit()
+
+            yield
+
+            # Temporarily change how exceptions are managed.
+            oeh = sys.excepthook
+            ex = []
+
+            def except_hook(type_, value, traceback_):
+                ex.append(value)
+                oeh(type_, value, traceback_)
+            sys.excepthook = except_hook
+
+            # Terminate on timeout
+            if timeout is not None:
+                QtCore.QTimer.singleShot(timeout, report_quit)
+
+            #### Block ####
+            loop.exec_()
+
+            # Restore exception management
+            sys.excepthook = oeh
+            if ex:
+                self.raiseTclError(str(ex[0]))
+
+            if status['timed_out']:
+                raise Exception('Timed out!')
+
+        def make_docs():
+            output = ''
+            import collections
+            od = collections.OrderedDict(sorted(commands.items()))
+            for cmd_, val in od.items():
+                output += cmd_ + ' \n' + ''.join(['~'] * len(cmd_)) + '\n'
+
+                t = val['help']
+                usage_i = t.find('>')
+                if usage_i < 0:
+                    expl = t
+                    output += expl + '\n\n'
+                    continue
+
+                expl = t[:usage_i - 1]
+                output += expl + '\n\n'
+
+                end_usage_i = t[usage_i:].find('\n')
+
+                if end_usage_i < 0:
+                    end_usage_i = len(t[usage_i:])
+                    output += '    ' + t[usage_i:] + '\n       No parameters.\n'
+                else:
+                    extras = t[usage_i+end_usage_i+1:]
+                    parts = [s.strip() for s in extras.split('\n')]
+
+                    output += '    ' + t[usage_i:usage_i+end_usage_i] + '\n'
+                    for p in parts:
+                        output += '       ' + p + '\n\n'
+
+            return output
+
+        '''
+            Howto implement TCL shell commands:
+
+            All parameters passed to command should be possible to set as None and test it afterwards.
+            This is because we need to see error caused in tcl,
+            if None value as default parameter is not allowed TCL will return empty error.
+            Use:
+                def mycommand(name=None,...):
+
+            Test it like this:
+            if name is None:
+
+                self.raise_tcl_error('Argument name is missing.')
+
+            When error ocurre, always use raise_tcl_error, never return "sometext" on error,
+            otherwise we will miss it and processing will silently continue.
+            Method raise_tcl_error  pass error into TCL interpreter, then raise python exception,
+            which is catched in exec_command and displayed in TCL shell console with red background.
+            Error in console is displayed  with TCL  trace.
+
+            This behavior works only within main thread,
+            errors with promissed tasks can be catched and detected only with log.
+            TODO: this problem have to be addressed somehow, maybe rewrite promissing to be blocking somehow for TCL shell.
+
+            Kamil's comment: I will rewrite existing TCL commands from time to time to follow this rules.
+
+        '''
+
+        commands = {
+            'help': {
+                'fcn': shelp,
+                'help': "Shows list of commands."
+            },
+        }
+
+        # Import/overwrite tcl commands as objects of TclCommand descendants
+        # This modifies the variable 'commands'.
+        tclCommands.register_all_commands(self, commands)
+
+        # Add commands to the tcl interpreter
+        for cmd in commands:
+            self.tcl.createcommand(cmd, commands[cmd]['fcn'])
+
+        # Make the tcl puts function return instead of print to stdout
+        self.tcl.eval('''
+            rename puts original_puts
+            proc puts {args} {
+                if {[llength $args] == 1} {
+                    return "[lindex $args 0]"
+                } else {
+                    eval original_puts $args
+                }
+            }
+            ''')
+
+    def setup_recent_items(self):
+
+        # TODO: Move this to constructor
+        icons = {
+            "gerber": "share/flatcam_icon16.png",
+            "excellon": "share/drill16.png",
+            "cncjob": "share/cnc16.png",
+            "project": "share/project16.png",
+            "svg": "share/geometry16.png",
+            "dxf": "share/dxf16.png",
+            "image": "share/image16.png"
+
+        }
+
+        openers = {
+            'gerber': lambda fname: self.worker_task.emit({'fcn': self.open_gerber, 'params': [fname]}),
+            'excellon': lambda fname: self.worker_task.emit({'fcn': self.open_excellon, 'params': [fname]}),
+            'cncjob': lambda fname: self.worker_task.emit({'fcn': self.open_gcode, 'params': [fname]}),
+            'project': self.open_project,
+            'svg': self.import_svg,
+            'dxf': self.import_dxf,
+            'image': self.import_image
+        }
+
+        # Open file
+        try:
+            f = open(self.data_path + '/recent.json')
+        except IOError:
+            App.log.error("Failed to load recent item list.")
+            self.inform.emit("[error_notcl] Failed to load recent item list.")
+            return
+
+        try:
+            self.recent = json.load(f)
+        except json.scanner.JSONDecodeError:
+            App.log.error("Failed to parse recent item list.")
+            self.inform.emit("[error_notcl] Failed to parse recent item list.")
+            f.close()
+            return
+        f.close()
+
+        # Closure needed to create callbacks in a loop.
+        # Otherwise late binding occurs.
+        def make_callback(func, fname):
+            def opener():
+                func(fname)
+            return opener
+
+        def reset_recent():
+            # Reset menu
+            self.ui.recent.clear()
+            self.recent = []
+            try:
+                f = open(self.data_path + '/recent.json', 'w')
+            except IOError:
+                App.log.error("Failed to open recent items file for writing.")
+                return
+
+            json.dump(self.recent, f)
+
+        # Reset menu
+        self.ui.recent.clear()
+
+        # Create menu items
+        for recent in self.recent:
+            filename = recent['filename'].split('/')[-1].split('\\')[-1]
+
+            try:
+                action = QtWidgets.QAction(QtGui.QIcon(icons[recent["kind"]]), filename, self)
+
+                # Attach callback
+                o = make_callback(openers[recent["kind"]], recent['filename'])
+                action.triggered.connect(o)
+
+                self.ui.recent.addAction(action)
+
+            except KeyError:
+                App.log.error("Unsupported file type: %s" % recent["kind"])
+
+        # Last action in Recent Files menu is one that Clear the content
+        clear_action = QtWidgets.QAction(QtGui.QIcon('share/trash32.png'), "Clear Recent files", self)
+        clear_action.triggered.connect(reset_recent)
+        self.ui.recent.addSeparator()
+        self.ui.recent.addAction(clear_action)
+
+        # self.builder.get_object('open_recent').set_submenu(recent_menu)
+        # self.ui.menufilerecent.set_submenu(recent_menu)
+        # recent_menu.show_all()
+        # self.ui.recent.show()
+
+        self.log.debug("Recent items list has been populated.")
+
+    def setup_component_editor(self):
+        label = QtWidgets.QLabel("Choose an item from Project")
+        label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter)
+        self.ui.selected_scroll_area.setWidget(label)
+
+    def setup_obj_classes(self):
+        """
+        Sets up application specifics on the FlatCAMObj class.
+
+        :return: None
+        """
+        FlatCAMObj.app = self
+        ObjectCollection.app = self
+
+        FCProcess.app = self
+        FCProcessContainer.app = self
+
+    def version_check(self):
+        """
+        Checks for the latest version of the program. Alerts the
+        user if theirs is outdated. This method is meant to be run
+        in a separate thread.
+
+        :return: None
+        """
+
+        self.log.debug("version_check()")
+        full_url = App.version_url + \
+            "?s=" + str(self.defaults['serial']) + \
+            "&v=" + str(self.version) + \
+            "&os=" + str(self.os) + \
+            "&" + urllib.parse.urlencode(self.defaults["global_stats"])
+        App.log.debug("Checking for updates @ %s" % full_url)
+
+        ### Get the data
+        try:
+            f = urllib.request.urlopen(full_url)
+        except:
+            # App.log.warning("Failed checking for latest version. Could not connect.")
+            self.log.warning("Failed checking for latest version. Could not connect.")
+            self.inform.emit("[warning] Failed checking for latest version. Could not connect.")
+            return
+
+        try:
+            data = json.load(f)
+        except Exception as e:
+            App.log.error("Could not parse information about latest version.")
+            self.inform.emit("[error_notcl] Could not parse information about latest version.")
+            App.log.debug("json.load(): %s" % str(e))
+            f.close()
+            return
+
+        f.close()
+
+        ### Latest version?
+        if self.version >= data["version"]:
+            App.log.debug("FlatCAM is up to date!")
+            self.inform.emit("[success] FlatCAM is up to date!")
+            return
+
+        App.log.debug("Newer version available.")
+        self.message.emit(
+            "Newer Version Available",
+            str("There is a newer version of FlatCAM " +
+                           "available for download:<br><br>" +
+                           "<B>" + data["name"] + "</b><br>" +
+                           data["message"].replace("\n", "<br>")),
+            "info"
+        )
+
+    def enable_plots(self, objects):
+        def worker_task(app_obj):
+            percentage = 0.1
+            try:
+                delta = 0.9 / len(objects)
+            except ZeroDivisionError:
+                self.progress.emit(0)
+                return
+            for obj in objects:
+                obj.options['plot'] = True
+                percentage += delta
+                self.progress.emit(int(percentage*100))
+
+            self.progress.emit(0)
+            self.plots_updated.emit()
+            self.collection.update_view()
+
+        # Send to worker
+        # self.worker.add_task(worker_task, [self])
+        self.worker_task.emit({'fcn': worker_task, 'params': [self]})
+
+    def disable_plots(self, objects):
+        # TODO: This method is very similar to replot_all. Try to merge.
+        """
+        Disables plots
+        :param objects: list
+            Objects to be disabled
+        :return:
+        """
+        self.progress.emit(10)
+
+        def worker_task(app_obj):
+            percentage = 0.1
+            try:
+                delta = 0.9 / len(objects)
+            except ZeroDivisionError:
+                self.progress.emit(0)
+                return
+
+            for obj in objects:
+                obj.options['plot'] = False
+                percentage += delta
+                self.progress.emit(int(percentage*100))
+
+            self.progress.emit(0)
+            self.plots_updated.emit()
+            self.collection.update_view()
+
+        # Send to worker
+        self.worker_task.emit({'fcn': worker_task, 'params': [self]})
+
+    def clear_plots(self):
+
+        objects = self.collection.get_list()
+
+        for obj in objects:
+            obj.clear(obj == objects[-1])
+
+        # Clear pool to free memory
+        self.clear_pool()
+
+    def generate_cnc_job(self, objects):
+        for obj in objects:
+            obj.generatecncjob()
+
+    def save_project(self, filename):
+        """
+        Saves the current project to the specified file.
+
+        :param filename: Name of the file in which to save.
+        :type filename: str
+        :return: None
+        """
+        self.log.debug("save_project()")
+
+        with self.proc_container.new("Saving FlatCAM Project") as proc:
+            ## Capture the latest changes
+            # Current object
+            try:
+                self.collection.get_active().read_form()
+            except:
+                self.log.debug("[warning] There was no active object")
+                pass
+            # Project options
+            self.options_read_form()
+
+            # Serialize the whole project
+            d = {"objs": [obj.to_dict() for obj in self.collection.get_list()],
+                 "options": self.options,
+                 "version": self.version}
+
+            # Open file
+            try:
+                f = open(filename, 'w')
+            except IOError:
+                App.log.error("[error] Failed to open file for saving: %s", filename)
+                return
+
+            # Write
+            json.dump(d, f, default=to_dict, indent=2, sort_keys=True)
+            f.close()
+
+        # verification of the saved project
+            # Open and parse
+            try:
+                saved_f = open(filename, 'r')
+            except IOError:
+                self.inform.emit("[error_notcl] Failed to verify project file: %s. Retry to save it." % filename)
+                return
+
+            try:
+                saved_d = json.load(saved_f, object_hook=dict2obj)
+            except:
+                self.inform.emit("[error_notcl] Failed to parse saved project file: %s. Retry to save it." % filename)
+                f.close()
+                return
+            saved_f.close()
+
+            if 'version' in saved_d:
+                self.inform.emit("[success] Project saved to: %s" % filename)
+            else:
+                self.inform.emit("[error_notcl] Failed to save project file: %s. Retry to save it." % filename)
+
+    def on_options_app2project(self):
+        """
+        Callback for Options->Transfer Options->App=>Project. Copies options
+        from application defaults to project defaults.
+
+        :return: None
+        """
+
+        self.report_usage("on_options_app2project")
+
+        self.defaults_read_form()
+        self.options.update(self.defaults)
+        self.options_write_form()
+
+    def on_options_project2app(self):
+        """
+        Callback for Options->Transfer Options->Project=>App. Copies options
+        from project defaults to application defaults.
+
+        :return: None
+        """
+
+        self.report_usage("on_options_project2app")
+
+        self.options_read_form()
+        self.defaults.update(self.options)
+        self.defaults_write_form()
+
+    def on_options_project2object(self):
+        """
+        Callback for Options->Transfer Options->Project=>Object. Copies options
+        from project defaults to the currently selected object.
+
+        :return: None
+        """
+
+        self.report_usage("on_options_project2object")
+
+        self.options_read_form()
+        obj = self.collection.get_active()
+        if obj is None:
+            self.inform.emit("WARNING: No object selected.")
+            return
+        for option in self.options:
+            if option.find(obj.kind + "_") == 0:
+                oname = option[len(obj.kind) + 1:]
+                obj.options[oname] = self.options[option]
+        obj.to_form()  # Update UI
+
+    def on_options_object2project(self):
+        """
+        Callback for Options->Transfer Options->Object=>Project. Copies options
+        from the currently selected object to project defaults.
+
+        :return: None
+        """
+
+        self.report_usage("on_options_object2project")
+
+        obj = self.collection.get_active()
+        if obj is None:
+            self.inform.emit("WARNING: No object selected.")
+            return
+        obj.read_form()
+        for option in obj.options:
+            if option in ['name']:  # TODO: Handle this better...
+                continue
+            self.options[obj.kind + "_" + option] = obj.options[option]
+        self.options_write_form()
+
+    def on_options_object2app(self):
+        """
+        Callback for Options->Transfer Options->Object=>App. Copies options
+        from the currently selected object to application defaults.
+
+        :return: None
+        """
+
+        self.report_usage("on_options_object2app")
+
+        obj = self.collection.get_active()
+        if obj is None:
+            self.inform.emit("WARNING: No object selected.")
+            return
+        obj.read_form()
+        for option in obj.options:
+            if option in ['name']:  # TODO: Handle this better...
+                continue
+            self.defaults[obj.kind + "_" + option] = obj.options[option]
+        self.defaults_write_form()
+
+    def on_options_app2object(self):
+        """
+        Callback for Options->Transfer Options->App=>Object. Copies options
+        from application defaults to the currently selected object.
+
+        :return: None
+        """
+
+        self.report_usage("on_options_app2object")
+
+        self.defaults_read_form()
+        obj = self.collection.get_active()
+        if obj is None:
+            self.inform.emit("WARNING: No object selected.")
+            return
+        for option in self.defaults:
+            if option.find(obj.kind + "_") == 0:
+                oname = option[len(obj.kind) + 1:]
+                obj.options[oname] = self.defaults[option]
+        obj.to_form()  # Update UI
+
+# end of file

+ 48 - 0
FlatCAMCommon.py

@@ -0,0 +1,48 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+class LoudDict(dict):
+    """
+    A Dictionary with a callback for
+    item changes.
+    """
+
+    def __init__(self, *args, **kwargs):
+        dict.__init__(self, *args, **kwargs)
+        self.callback = lambda x: None
+
+    def __setitem__(self, key, value):
+        """
+        Overridden __setitem__ method. Will emit 'changed(QString)'
+        if the item was changed, with key as parameter.
+        """
+        if key in self and self.__getitem__(key) == value:
+            return
+
+        dict.__setitem__(self, key, value)
+        self.callback(key)
+
+    def update(self, *args, **kwargs):
+        if len(args) > 1:
+            raise TypeError("update expected at most 1 arguments, got %d" % len(args))
+        other = dict(*args, **kwargs)
+        for key in other:
+            self[key] = other[key]
+
+    def set_change_callback(self, callback):
+        """
+        Assigns a function as callback on item change. The callback
+        will receive the key of the object that was changed.
+
+        :param callback: Function to call on item change.
+        :type callback: func
+        :return: None
+        """
+
+        self.callback = callback
+

+ 5055 - 0
FlatCAMEditor.py

@@ -0,0 +1,5055 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+from PyQt5 import QtGui, QtCore, QtWidgets
+from PyQt5.QtCore import Qt
+import FlatCAMApp
+from camlib import *
+from FlatCAMTool import FlatCAMTool
+from ObjectUI import LengthEntry, RadioSet
+
+from shapely.geometry import Polygon, LineString, Point, LinearRing, MultiLineString
+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, sign, dot
+from numpy.linalg import solve
+
+from rtree import index as rtindex
+from GUIElements import OptionalInputSection, FCCheckBox, FCEntry, FCEntry2, FCComboBox, FCTextAreaRich, \
+    VerticalScrollArea, FCTable
+from vispy.scene.visuals import Markers
+from copy import copy
+import freetype as ft
+
+
+class BufferSelectionTool(FlatCAMTool):
+    """
+    Simple input for buffer distance.
+    """
+
+    toolName = "Buffer Selection"
+
+    def __init__(self, app, draw_app):
+        FlatCAMTool.__init__(self, app)
+
+        self.draw_app = draw_app
+
+        # Title
+        title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
+        self.layout.addWidget(title_label)
+
+        # this way I can hide/show the frame
+        self.buffer_tool_frame = QtWidgets.QFrame()
+        self.buffer_tool_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.buffer_tool_frame)
+        self.buffer_tools_box = QtWidgets.QVBoxLayout()
+        self.buffer_tools_box.setContentsMargins(0, 0, 0, 0)
+        self.buffer_tool_frame.setLayout(self.buffer_tools_box)
+
+        # Form Layout
+        form_layout = QtWidgets.QFormLayout()
+        self.buffer_tools_box.addLayout(form_layout)
+
+        # Buffer distance
+        self.buffer_distance_entry = LengthEntry()
+        form_layout.addRow("Buffer distance:", self.buffer_distance_entry)
+        self.buffer_corner_lbl = QtWidgets.QLabel("Buffer corner:")
+        self.buffer_corner_lbl.setToolTip(
+            "There are 3 types of corners:\n"
+            " - 'Round': the corner is rounded for exterior buffer.\n"
+            " - 'Square:' the corner is met in a sharp angle for exterior buffer.\n"
+            " - 'Beveled:' the corner is a line that directly connects the features meeting in the corner"
+        )
+        self.buffer_corner_cb = FCComboBox()
+        self.buffer_corner_cb.addItem("Round")
+        self.buffer_corner_cb.addItem("Square")
+        self.buffer_corner_cb.addItem("Beveled")
+        form_layout.addRow(self.buffer_corner_lbl, self.buffer_corner_cb)
+
+        # Buttons
+        hlay = QtWidgets.QHBoxLayout()
+        self.buffer_tools_box.addLayout(hlay)
+
+        self.buffer_int_button = QtWidgets.QPushButton("Buffer Interior")
+        hlay.addWidget(self.buffer_int_button)
+        self.buffer_ext_button = QtWidgets.QPushButton("Buffer Exterior")
+        hlay.addWidget(self.buffer_ext_button)
+
+        hlay1 = QtWidgets.QHBoxLayout()
+        self.buffer_tools_box.addLayout(hlay1)
+
+        self.buffer_button = QtWidgets.QPushButton("Full Buffer")
+        hlay1.addWidget(self.buffer_button)
+
+        self.layout.addStretch()
+
+        # Signals
+        self.buffer_button.clicked.connect(self.on_buffer)
+        self.buffer_int_button.clicked.connect(self.on_buffer_int)
+        self.buffer_ext_button.clicked.connect(self.on_buffer_ext)
+
+        # Init GUI
+        self.buffer_distance_entry.set_value(0.01)
+
+    def on_buffer(self):
+        buffer_distance = self.buffer_distance_entry.get_value()
+        # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment
+        # I populated the combobox such that the index coincide with the join styles value (whcih is really an INT)
+        join_style = self.buffer_corner_cb.currentIndex() + 1
+        self.draw_app.buffer(buffer_distance, join_style)
+
+    def on_buffer_int(self):
+        buffer_distance = self.buffer_distance_entry.get_value()
+        # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment
+        # I populated the combobox such that the index coincide with the join styles value (whcih is really an INT)
+        join_style = self.buffer_corner_cb.currentIndex() + 1
+        self.draw_app.buffer_int(buffer_distance, join_style)
+
+    def on_buffer_ext(self):
+        buffer_distance = self.buffer_distance_entry.get_value()
+        # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment
+        # I populated the combobox such that the index coincide with the join styles value (whcih is really an INT)
+        join_style = self.buffer_corner_cb.currentIndex() + 1
+        self.draw_app.buffer_ext(buffer_distance, join_style)
+
+    def hide_tool(self):
+        self.buffer_tool_frame.hide()
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
+
+class TextInputTool(FlatCAMTool):
+    """
+    Simple input for buffer distance.
+    """
+
+    toolName = "Text Input Tool"
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+
+        self.app = app
+        self.text_path = []
+
+        # this way I can hide/show the frame
+        self.text_tool_frame = QtWidgets.QFrame()
+        self.text_tool_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.text_tool_frame)
+        self.text_tools_box = QtWidgets.QVBoxLayout()
+        self.text_tools_box.setContentsMargins(0, 0, 0, 0)
+        self.text_tool_frame.setLayout(self.text_tools_box)
+
+        # Title
+        title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
+        self.text_tools_box.addWidget(title_label)
+
+        # Form Layout
+        self.form_layout = QtWidgets.QFormLayout()
+        self.text_tools_box.addLayout(self.form_layout)
+
+        # Font type
+        if sys.platform == "win32":
+            f_current = QtGui.QFont("Arial")
+        elif sys.platform == "linux":
+            f_current = QtGui.QFont("FreeMono")
+        else:
+            f_current = QtGui.QFont("Helvetica Neue")
+
+        self.font_name = f_current.family()
+
+        self.font_type_cb = QtWidgets.QFontComboBox(self)
+        self.font_type_cb.setCurrentFont(f_current)
+        self.form_layout.addRow("Font:", self.font_type_cb)
+
+        # Flag variables to show if font is bold, italic, both or none (regular)
+        self.font_bold = False
+        self.font_italic = False
+
+        # # Create dictionaries with the filenames of the fonts
+        # # Key: Fontname
+        # # Value: Font File Name.ttf
+        #
+        # # regular fonts
+        # self.ff_names_regular ={}
+        # # bold fonts
+        # self.ff_names_bold = {}
+        # # italic fonts
+        # self.ff_names_italic = {}
+        # # bold and italic fonts
+        # self.ff_names_bi = {}
+        #
+        # if sys.platform == 'win32':
+        #     from winreg import ConnectRegistry, OpenKey, EnumValue, HKEY_LOCAL_MACHINE
+        #     registry = ConnectRegistry(None, HKEY_LOCAL_MACHINE)
+        #     font_key = OpenKey(registry, "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts")
+        #     try:
+        #         i = 0
+        #         while 1:
+        #             name_font, value, type = EnumValue(font_key, i)
+        #             k = name_font.replace(" (TrueType)", '')
+        #             if 'Bold' in k and 'Italic' in k:
+        #                 k = k.replace(" Bold Italic", '')
+        #                 self.ff_names_bi.update({k: value})
+        #             elif 'Bold' in k:
+        #                 k = k.replace(" Bold", '')
+        #                 self.ff_names_bold.update({k: value})
+        #             elif 'Italic' in k:
+        #                 k = k.replace(" Italic", '')
+        #                 self.ff_names_italic.update({k: value})
+        #             else:
+        #                 self.ff_names_regular.update({k: value})
+        #             i += 1
+        #     except WindowsError:
+        #         pass
+
+        # Font size
+        self.font_size_cb = FCComboBox()
+        self.font_size_cb.setEditable(True)
+        self.font_size_cb.setMinimumContentsLength(3)
+        self.font_size_cb.setMaximumWidth(70)
+
+        font_sizes = ['6', '7', '8', '9', '10', '11', '12', '13', '14',
+                     '15', '16', '18', '20', '22', '24', '26', '28',
+                     '32', '36', '40', '44', '48', '54', '60', '66',
+                     '72', '80', '88', '96']
+
+        for i in font_sizes:
+            self.font_size_cb.addItem(i)
+        self.font_size_cb.setCurrentIndex(4)
+
+        hlay = QtWidgets.QHBoxLayout()
+        hlay.addWidget(self.font_size_cb)
+        hlay.addStretch()
+
+        self.font_bold_tb = QtWidgets.QToolButton()
+        self.font_bold_tb.setCheckable(True)
+        self.font_bold_tb.setIcon(QtGui.QIcon('share/bold32.png'))
+        hlay.addWidget(self.font_bold_tb)
+
+        self.font_italic_tb = QtWidgets.QToolButton()
+        self.font_italic_tb.setCheckable(True)
+        self.font_italic_tb.setIcon(QtGui.QIcon('share/italic32.png'))
+        hlay.addWidget(self.font_italic_tb)
+
+        self.form_layout.addRow("Size:", hlay)
+
+        # Text input
+        self.text_input_entry = FCTextAreaRich()
+        self.text_input_entry.setTabStopWidth(12)
+        self.text_input_entry.setMinimumHeight(200)
+        # self.text_input_entry.setMaximumHeight(150)
+        self.text_input_entry.setCurrentFont(f_current)
+        self.text_input_entry.setFontPointSize(10)
+        self.form_layout.addRow("Text:", self.text_input_entry)
+
+        # Buttons
+        hlay1 = QtWidgets.QHBoxLayout()
+        self.form_layout.addRow("", hlay1)
+        hlay1.addStretch()
+        self.apply_button = QtWidgets.QPushButton("Apply")
+        hlay1.addWidget(self.apply_button)
+
+        # self.layout.addStretch()
+
+        # Signals
+        self.apply_button.clicked.connect(self.on_apply_button)
+        self.font_type_cb.currentFontChanged.connect(self.font_family)
+        self.font_size_cb.activated.connect(self.font_size)
+        self.font_bold_tb.clicked.connect(self.on_bold_button)
+        self.font_italic_tb.clicked.connect(self.on_italic_button)
+
+    def on_apply_button(self):
+        font_to_geo_type = ""
+
+        if self.font_bold is True:
+            font_to_geo_type = 'bold'
+        elif self.font_italic is True:
+            font_to_geo_type = 'italic'
+        elif self.font_bold is True and self.font_italic is True:
+            font_to_geo_type = 'bi'
+        elif self.font_bold is False and self.font_italic is False:
+            font_to_geo_type = 'regular'
+        string_to_geo = self.text_input_entry.get_value()
+        font_to_geo_size = self.font_size_cb.get_value()
+
+        self.text_path = self.app.f_parse.font_to_geometry(
+                    char_string=string_to_geo,
+                    font_name=self.font_name,
+                    font_size=font_to_geo_size,
+                    font_type=font_to_geo_type,
+                    units=self.app.general_options_form.general_group.units_radio.get_value().upper())
+
+    def font_family(self, font):
+        self.text_input_entry.selectAll()
+        font.setPointSize(float(self.font_size_cb.get_value()))
+        self.text_input_entry.setCurrentFont(font)
+        self.font_name = self.font_type_cb.currentFont().family()
+
+    def font_size(self):
+        self.text_input_entry.selectAll()
+        self.text_input_entry.setFontPointSize(float(self.font_size_cb.get_value()))
+
+    def on_bold_button(self):
+        if self.font_bold_tb.isChecked():
+            self.text_input_entry.selectAll()
+            self.text_input_entry.setFontWeight(QtGui.QFont.Bold)
+            self.font_bold = True
+        else:
+            self.text_input_entry.selectAll()
+            self.text_input_entry.setFontWeight(QtGui.QFont.Normal)
+            self.font_bold = False
+
+    def on_italic_button(self):
+        if self.font_italic_tb.isChecked():
+            self.text_input_entry.selectAll()
+            self.text_input_entry.setFontItalic(True)
+            self.font_italic = True
+        else:
+            self.text_input_entry.selectAll()
+            self.text_input_entry.setFontItalic(False)
+            self.font_italic = False
+
+    def hide_tool(self):
+        self.text_tool_frame.hide()
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
+
+
+class PaintOptionsTool(FlatCAMTool):
+    """
+    Inputs to specify how to paint the selected polygons.
+    """
+
+    toolName = "Paint Options"
+
+    def __init__(self, app, fcdraw):
+        FlatCAMTool.__init__(self, app)
+
+        self.app = app
+        self.fcdraw = fcdraw
+
+        ## Title
+        title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
+        self.layout.addWidget(title_label)
+
+        grid = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid)
+
+        # Tool dia
+        ptdlabel = QtWidgets.QLabel('Tool dia:')
+        ptdlabel.setToolTip(
+            "Diameter of the tool to\n"
+            "be used in the operation."
+        )
+        grid.addWidget(ptdlabel, 0, 0)
+
+        self.painttooldia_entry = LengthEntry()
+        grid.addWidget(self.painttooldia_entry, 0, 1)
+
+        # Overlap
+        ovlabel = QtWidgets.QLabel('Overlap:')
+        ovlabel.setToolTip(
+            "How much (fraction) of the tool width to overlap each tool pass.\n"
+            "Example:\n"
+            "A value here of 0.25 means 25% from the tool diameter found above.\n\n"
+            "Adjust the value starting with lower values\n"
+            "and increasing it if areas that should be painted are still \n"
+            "not painted.\n"
+            "Lower values = faster processing, faster execution on PCB.\n"
+            "Higher values = slow processing and slow execution on CNC\n"
+            "due of too many paths."
+        )
+        grid.addWidget(ovlabel, 1, 0)
+        self.paintoverlap_entry = LengthEntry()
+        grid.addWidget(self.paintoverlap_entry, 1, 1)
+
+        # Margin
+        marginlabel = QtWidgets.QLabel('Margin:')
+        marginlabel.setToolTip(
+            "Distance by which to avoid\n"
+            "the edges of the polygon to\n"
+            "be painted."
+        )
+        grid.addWidget(marginlabel, 2, 0)
+        self.paintmargin_entry = LengthEntry()
+        grid.addWidget(self.paintmargin_entry, 2, 1)
+
+        # Method
+        methodlabel = QtWidgets.QLabel('Method:')
+        methodlabel.setToolTip(
+            "Algorithm to paint the polygon:<BR>"
+            "<B>Standard</B>: Fixed step inwards.<BR>"
+            "<B>Seed-based</B>: Outwards from seed."
+        )
+        grid.addWidget(methodlabel, 3, 0)
+        self.paintmethod_combo = RadioSet([
+            {"label": "Standard", "value": "standard"},
+            {"label": "Seed-based", "value": "seed"},
+            {"label": "Straight lines", "value": "lines"}
+        ], orientation='vertical', stretch=False)
+        grid.addWidget(self.paintmethod_combo, 3, 1)
+
+        # Connect lines
+        pathconnectlabel = QtWidgets.QLabel("Connect:")
+        pathconnectlabel.setToolTip(
+            "Draw lines between resulting\n"
+            "segments to minimize tool lifts."
+        )
+        grid.addWidget(pathconnectlabel, 4, 0)
+        self.pathconnect_cb = FCCheckBox()
+        grid.addWidget(self.pathconnect_cb, 4, 1)
+
+        contourlabel = QtWidgets.QLabel("Contour:")
+        contourlabel.setToolTip(
+            "Cut around the perimeter of the polygon\n"
+            "to trim rough edges."
+        )
+        grid.addWidget(contourlabel, 5, 0)
+        self.paintcontour_cb = FCCheckBox()
+        grid.addWidget(self.paintcontour_cb, 5, 1)
+
+
+        ## Buttons
+        hlay = QtWidgets.QHBoxLayout()
+        self.layout.addLayout(hlay)
+        hlay.addStretch()
+        self.paint_button = QtWidgets.QPushButton("Paint")
+        hlay.addWidget(self.paint_button)
+
+        self.layout.addStretch()
+
+        ## Signals
+        self.paint_button.clicked.connect(self.on_paint)
+
+        ## Init GUI
+        self.painttooldia_entry.set_value(0)
+        self.paintoverlap_entry.set_value(0)
+        self.paintmargin_entry.set_value(0)
+        self.paintmethod_combo.set_value("seed")
+
+
+    def on_paint(self):
+
+        tooldia = self.painttooldia_entry.get_value()
+        overlap = self.paintoverlap_entry.get_value()
+        margin = self.paintmargin_entry.get_value()
+        method = self.paintmethod_combo.get_value()
+        contour = self.paintcontour_cb.get_value()
+        connect = self.pathconnect_cb.get_value()
+
+        self.fcdraw.paint(tooldia, overlap, margin, connect=connect, contour=contour, method=method)
+        self.fcdraw.select_tool("select")
+        self.app.ui.notebook.setTabText(2, "Tools")
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
+
+
+class DrawToolShape(object):
+    """
+    Encapsulates "shapes" under a common class.
+    """
+
+    tolerance = None
+
+    @staticmethod
+    def get_pts(o):
+        """
+        Returns a list of all points in the object, where
+        the object can be a Polygon, Not a polygon, or a list
+        of such. Search is done recursively.
+
+        :param: geometric object
+        :return: List of points
+        :rtype: list
+        """
+        pts = []
+
+        ## Iterable: descend into each item.
+        try:
+            for subo in o:
+                pts += DrawToolShape.get_pts(subo)
+
+        ## Non-iterable
+        except TypeError:
+            if o is not None:
+                ## DrawToolShape: descend into .geo.
+                if isinstance(o, DrawToolShape):
+                    pts += DrawToolShape.get_pts(o.geo)
+
+                ## Descend into .exerior and .interiors
+                elif type(o) == Polygon:
+                    pts += DrawToolShape.get_pts(o.exterior)
+                    for i in o.interiors:
+                        pts += DrawToolShape.get_pts(i)
+                elif type(o) == MultiLineString:
+                    for line in o:
+                        pts += DrawToolShape.get_pts(line)
+                ## Has .coords: list them.
+                else:
+                    if DrawToolShape.tolerance is not None:
+                        pts += list(o.simplify(DrawToolShape.tolerance).coords)
+                    else:
+                        pts += list(o.coords)
+            else:
+                return
+        return pts
+
+    def __init__(self, geo=[]):
+
+        # Shapely type or list of such
+        self.geo = geo
+        self.utility = False
+
+    def get_all_points(self):
+        return DrawToolShape.get_pts(self)
+
+
+class DrawToolUtilityShape(DrawToolShape):
+    """
+    Utility shapes are temporary geometry in the editor
+    to assist in the creation of shapes. For example it
+    will show the outline of a rectangle from the first
+    point to the current mouse pointer before the second
+    point is clicked and the final geometry is created.
+    """
+
+    def __init__(self, geo=[]):
+        super(DrawToolUtilityShape, self).__init__(geo=geo)
+        self.utility = True
+
+
+class DrawTool(object):
+    """
+    Abstract Class representing a tool in the drawing
+    program. Can generate geometry, including temporary
+    utility geometry that is updated on user clicks
+    and mouse motion.
+    """
+
+    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  # DrawToolShape or None
+
+    def click(self, point):
+        """
+        :param point: [x, y] Coordinate pair.
+        """
+        return ""
+
+    def click_release(self, point):
+        """
+        :param point: [x, y] Coordinate pair.
+        """
+        return ""
+
+    def on_key(self, key):
+        return None
+
+    def utility_geometry(self, data=None):
+        return None
+
+
+class FCShapeTool(DrawTool):
+    """
+    Abstract class for tools that create a shape.
+    """
+
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+
+    def make(self):
+        pass
+
+
+class FCCircle(FCShapeTool):
+    """
+    Resulting type: Polygon
+    """
+
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.start_msg = "Click on CENTER ..."
+        self.steps_per_circ = self.draw_app.app.defaults["geometry_circle_steps"]
+
+    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 DrawToolUtilityShape(Point(p1).buffer(radius, int(self.steps_per_circ / 4)))
+
+        return None
+
+    def make(self):
+        p1 = self.points[0]
+        p2 = self.points[1]
+        radius = distance(p1, p2)
+        self.geometry = DrawToolShape(Point(p1).buffer(radius, int(self.steps_per_circ / 4)))
+        self.complete = True
+
+
+class FCArc(FCShapeTool):
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.start_msg = "Click on CENTER ..."
+
+        # Direction of rotation between point 1 and 2.
+        # 'cw' or 'ccw'. Switch direction by hitting the
+        # 'o' key.
+        self.direction = "cw"
+
+        # Mode
+        # C12 = Center, p1, p2
+        # 12C = p1, p2, Center
+        # 132 = p1, p3, p2
+        self.mode = "c12"  # Center, p1, p2
+
+        self.steps_per_circ = self.draw_app.app.defaults["geometry_circle_steps"]
+
+    def click(self, point):
+        self.points.append(point)
+
+        if len(self.points) == 1:
+            return "Click on 1st point ..."
+
+        if len(self.points) == 2:
+            return "Click on 2nd point to complete ..."
+
+        if len(self.points) == 3:
+            self.make()
+            return "Done."
+
+        return ""
+
+    def on_key(self, key):
+        if key == 'o':
+            self.direction = 'cw' if self.direction == 'ccw' else 'ccw'
+            return 'Direction: ' + self.direction.upper()
+
+        if key == 'p':
+            if self.mode == 'c12':
+                self.mode = '12c'
+            elif self.mode == '12c':
+                self.mode = '132'
+            else:
+                self.mode = 'c12'
+            return 'Mode: ' + self.mode
+
+    def utility_geometry(self, data=None):
+        if len(self.points) == 1:  # Show the radius
+            center = self.points[0]
+            p1 = data
+
+            return DrawToolUtilityShape(LineString([center, p1]))
+
+        if len(self.points) == 2:  # Show the arc
+
+            if self.mode == 'c12':
+                center = self.points[0]
+                p1 = self.points[1]
+                p2 = data
+
+                radius = sqrt((center[0] - p1[0]) ** 2 + (center[1] - p1[1]) ** 2)
+                startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
+                stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
+
+                return DrawToolUtilityShape([LineString(arc(center, radius, startangle, stopangle,
+                                                            self.direction, self.steps_per_circ)),
+                                             Point(center)])
+
+            elif self.mode == '132':
+                p1 = array(self.points[0])
+                p3 = array(self.points[1])
+                p2 = array(data)
+
+                center, radius, t = three_point_circle(p1, p2, p3)
+                direction = 'cw' if sign(t) > 0 else 'ccw'
+
+                startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
+                stopangle = arctan2(p3[1] - center[1], p3[0] - center[0])
+
+                return DrawToolUtilityShape([LineString(arc(center, radius, startangle, stopangle,
+                                                            direction, self.steps_per_circ)),
+                                             Point(center), Point(p1), Point(p3)])
+
+            else:  # '12c'
+                p1 = array(self.points[0])
+                p2 = array(self.points[1])
+
+                # Midpoint
+                a = (p1 + p2) / 2.0
+
+                # Parallel vector
+                c = p2 - p1
+
+                # Perpendicular vector
+                b = dot(c, array([[0, -1], [1, 0]], dtype=float32))
+                b /= norm(b)
+
+                # Distance
+                t = distance(data, a)
+
+                # Which side? Cross product with c.
+                # cross(M-A, B-A), where line is AB and M is test point.
+                side = (data[0] - p1[0]) * c[1] - (data[1] - p1[1]) * c[0]
+                t *= sign(side)
+
+                # Center = a + bt
+                center = a + b * t
+
+                radius = norm(center - p1)
+                startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
+                stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
+
+                return DrawToolUtilityShape([LineString(arc(center, radius, startangle, stopangle,
+                                                            self.direction, self.steps_per_circ)),
+                                             Point(center)])
+
+        return None
+
+    def make(self):
+
+        if self.mode == 'c12':
+            center = self.points[0]
+            p1 = self.points[1]
+            p2 = self.points[2]
+
+            radius = distance(center, p1)
+            startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
+            stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
+            self.geometry = DrawToolShape(LineString(arc(center, radius, startangle, stopangle,
+                                                         self.direction, self.steps_per_circ)))
+
+        elif self.mode == '132':
+            p1 = array(self.points[0])
+            p3 = array(self.points[1])
+            p2 = array(self.points[2])
+
+            center, radius, t = three_point_circle(p1, p2, p3)
+            direction = 'cw' if sign(t) > 0 else 'ccw'
+
+            startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
+            stopangle = arctan2(p3[1] - center[1], p3[0] - center[0])
+
+            self.geometry = DrawToolShape(LineString(arc(center, radius, startangle, stopangle,
+                                                         direction, self.steps_per_circ)))
+
+        else:  # self.mode == '12c'
+            p1 = array(self.points[0])
+            p2 = array(self.points[1])
+            pc = array(self.points[2])
+
+            # Midpoint
+            a = (p1 + p2) / 2.0
+
+            # Parallel vector
+            c = p2 - p1
+
+            # Perpendicular vector
+            b = dot(c, array([[0, -1], [1, 0]], dtype=float32))
+            b /= norm(b)
+
+            # Distance
+            t = distance(pc, a)
+
+            # Which side? Cross product with c.
+            # cross(M-A, B-A), where line is AB and M is test point.
+            side = (pc[0] - p1[0]) * c[1] - (pc[1] - p1[1]) * c[0]
+            t *= sign(side)
+
+            # Center = a + bt
+            center = a + b * t
+
+            radius = norm(center - p1)
+            startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
+            stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
+
+            self.geometry = DrawToolShape(LineString(arc(center, radius, startangle, stopangle,
+                                                         self.direction, self.steps_per_circ)))
+        self.complete = True
+
+
+class FCRectangle(FCShapeTool):
+    """
+    Resulting type: Polygon
+    """
+
+    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 DrawToolUtilityShape(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 = DrawToolShape(Polygon([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])]))
+        self.complete = True
+
+
+class FCPolygon(FCShapeTool):
+    """
+    Resulting type: Polygon
+    """
+
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.start_msg = "Click on 1st point ..."
+
+    def click(self, point):
+        self.draw_app.in_action = True
+        self.points.append(point)
+
+        if len(self.points) > 0:
+            return "Click on next point or hit ENTER 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 DrawToolUtilityShape(LineString(temp_points))
+
+        if len(self.points) > 1:
+            temp_points = [x for x in self.points]
+            temp_points.append(data)
+            return DrawToolUtilityShape(LinearRing(temp_points))
+
+        return None
+
+    def make(self):
+        # self.geometry = LinearRing(self.points)
+        self.geometry = DrawToolShape(Polygon(self.points))
+        self.draw_app.in_action = False
+        self.complete = True
+
+    def on_key(self, key):
+        if key == 'backspace':
+            if len(self.points) > 0:
+                self.points = self.points[0:-1]
+
+
+class FCPath(FCPolygon):
+    """
+    Resulting type: LineString
+    """
+
+    def make(self):
+        self.geometry = DrawToolShape(LineString(self.points))
+        self.draw_app.in_action = False
+        self.complete = True
+
+    def utility_geometry(self, data=None):
+        if len(self.points) > 0:
+            temp_points = [x for x in self.points]
+            temp_points.append(data)
+            return DrawToolUtilityShape(LineString(temp_points))
+
+        return None
+
+    def on_key(self, key):
+        if key == 'backspace':
+            if len(self.points) > 0:
+                self.points = self.points[0:-1]
+
+
+class FCSelect(DrawTool):
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.storage = self.draw_app.storage
+        # self.shape_buffer = self.draw_app.shape_buffer
+        # self.selected = self.draw_app.selected
+
+    def click_release(self, point):
+
+        self.select_shapes(point)
+        return ""
+
+    def select_shapes(self, pos):
+        # list where we store the overlapped shapes under our mouse left click position
+        over_shape_list = []
+
+        # pos[0] and pos[1] are the mouse click coordinates (x, y)
+        for obj_shape in self.storage.get_objects():
+            # first method of click selection -> inconvenient
+            # minx, miny, maxx, maxy = obj_shape.geo.bounds
+            # if (minx <= pos[0] <= maxx) and (miny <= pos[1] <= maxy):
+            #     over_shape_list.append(obj_shape)
+
+            # second method of click selection -> slow
+            # outside = obj_shape.geo.buffer(0.1)
+            # inside = obj_shape.geo.buffer(-0.1)
+            # shape_band = outside.difference(inside)
+            # if Point(pos).within(shape_band):
+            #     over_shape_list.append(obj_shape)
+
+            # 3rd method of click selection -> inconvenient
+            try:
+                _, closest_shape = self.storage.nearest(pos)
+            except StopIteration:
+                return ""
+
+            over_shape_list.append(closest_shape)
+
+        try:
+            # if there is no shape under our click then deselect all shapes
+            # it will not work for 3rd method of click selection
+            if not over_shape_list:
+                self.draw_app.selected = []
+                FlatCAMGeoEditor.draw_shape_idx = -1
+            else:
+                # if there are shapes under our click then advance through the list of them, one at the time in a
+                # circular way
+                FlatCAMGeoEditor.draw_shape_idx = (FlatCAMGeoEditor.draw_shape_idx + 1) % len(over_shape_list)
+                obj_to_add = over_shape_list[int(FlatCAMGeoEditor.draw_shape_idx)]
+
+                key_modifier = QtWidgets.QApplication.keyboardModifiers()
+                if self.draw_app.app.defaults["global_mselect_key"] == 'Control':
+                    # if CONTROL key is pressed then we add to the selected list the current shape but if it's already
+                    # in the selected list, we removed it. Therefore first click selects, second deselects.
+                    if key_modifier == Qt.ControlModifier:
+                        if obj_to_add in self.draw_app.selected:
+                            self.draw_app.selected.remove(obj_to_add)
+                        else:
+                            self.draw_app.selected.append(obj_to_add)
+                    else:
+                        self.draw_app.selected = []
+                        self.draw_app.selected.append(obj_to_add)
+                else:
+                    if key_modifier == Qt.ShiftModifier:
+                        if obj_to_add in self.draw_app.selected:
+                            self.draw_app.selected.remove(obj_to_add)
+                        else:
+                            self.draw_app.selected.append(obj_to_add)
+                    else:
+                        self.draw_app.selected = []
+                        self.draw_app.selected.append(obj_to_add)
+
+        except Exception as e:
+            log.error("[ERROR] Something went bad. %s" % str(e))
+            raise
+
+
+class FCDrillSelect(DrawTool):
+    def __init__(self, exc_editor_app):
+        DrawTool.__init__(self, exc_editor_app)
+
+        self.exc_editor_app = exc_editor_app
+        self.storage = self.exc_editor_app.storage_dict
+        # self.selected = self.exc_editor_app.selected
+
+        # here we store all shapes that were selected so we can search for the nearest to our click location
+        self.sel_storage = FlatCAMExcEditor.make_storage()
+
+        self.exc_editor_app.resize_frame.hide()
+        self.exc_editor_app.array_frame.hide()
+
+    def click(self, point):
+        key_modifier = QtWidgets.QApplication.keyboardModifiers()
+        if self.exc_editor_app.app.defaults["global_mselect_key"] == 'Control':
+            if key_modifier == Qt.ControlModifier:
+                pass
+            else:
+                self.exc_editor_app.selected = []
+        else:
+            if key_modifier == Qt.ShiftModifier:
+                pass
+            else:
+                self.exc_editor_app.selected = []
+
+    def click_release(self, point):
+        self.select_shapes(point)
+        return ""
+
+    def select_shapes(self, pos):
+        self.exc_editor_app.tools_table_exc.clearSelection()
+
+        try:
+            # for storage in self.exc_editor_app.storage_dict:
+            #     _, partial_closest_shape = self.exc_editor_app.storage_dict[storage].nearest(pos)
+            #     if partial_closest_shape is not None:
+            #         self.sel_storage.insert(partial_closest_shape)
+            #
+            # _, closest_shape = self.sel_storage.nearest(pos)
+
+            for storage in self.exc_editor_app.storage_dict:
+                for shape in self.exc_editor_app.storage_dict[storage].get_objects():
+                    self.sel_storage.insert(shape)
+
+            _, closest_shape = self.sel_storage.nearest(pos)
+
+
+            # constrain selection to happen only within a certain bounding box
+            x_coord, y_coord = closest_shape.geo[0].xy
+            delta = (x_coord[1] - x_coord[0])
+            # closest_shape_coords = (((x_coord[0] + delta / 2)), y_coord[0])
+            xmin = x_coord[0] - (0.7 * delta)
+            xmax = x_coord[0] + (1.7 * delta)
+            ymin = y_coord[0] - (0.7 * delta)
+            ymax = y_coord[0] + (1.7 * delta)
+        except StopIteration:
+            return ""
+
+        if pos[0] < xmin or pos[0] > xmax or pos[1] < ymin or pos[1] > ymax:
+            self.exc_editor_app.selected = []
+        else:
+            key_modifier = QtWidgets.QApplication.keyboardModifiers()
+            if self.exc_editor_app.app.defaults["global_mselect_key"] == 'Control':
+                # if CONTROL key is pressed then we add to the selected list the current shape but if it's already
+                # in the selected list, we removed it. Therefore first click selects, second deselects.
+                if key_modifier == Qt.ControlModifier:
+                    if closest_shape in self.exc_editor_app.selected:
+                        self.exc_editor_app.selected.remove(closest_shape)
+                    else:
+                        self.exc_editor_app.selected.append(closest_shape)
+                else:
+                    self.exc_editor_app.selected = []
+                    self.exc_editor_app.selected.append(closest_shape)
+            else:
+                if key_modifier == Qt.ShiftModifier:
+                    if closest_shape in self.exc_editor_app.selected:
+                        self.exc_editor_app.selected.remove(closest_shape)
+                    else:
+                        self.exc_editor_app.selected.append(closest_shape)
+                else:
+                    self.exc_editor_app.selected = []
+                    self.exc_editor_app.selected.append(closest_shape)
+
+            # select the diameter of the selected shape in the tool table
+            for storage in self.exc_editor_app.storage_dict:
+                for shape_s in self.exc_editor_app.selected:
+                    if shape_s in self.exc_editor_app.storage_dict[storage].get_objects():
+                        for key in self.exc_editor_app.tool2tooldia:
+                            if self.exc_editor_app.tool2tooldia[key] == storage:
+                                item = self.exc_editor_app.tools_table_exc.item((key - 1), 1)
+                                self.exc_editor_app.tools_table_exc.setCurrentItem(item)
+                                # item.setSelected(True)
+                                # self.exc_editor_app.tools_table_exc.selectItem(key - 1)
+                                # midx = self.exc_editor_app.tools_table_exc.model().index((key - 1), 0)
+                                # self.exc_editor_app.tools_table_exc.setCurrentIndex(midx)
+                                self.draw_app.last_tool_selected = key
+        # delete whatever is in selection storage, there is no longer need for those shapes
+        self.sel_storage = FlatCAMExcEditor.make_storage()
+
+        return ""
+
+        # pos[0] and pos[1] are the mouse click coordinates (x, y)
+        # for storage in self.exc_editor_app.storage_dict:
+        #     for obj_shape in self.exc_editor_app.storage_dict[storage].get_objects():
+        #         minx, miny, maxx, maxy = obj_shape.geo.bounds
+        #         if (minx <= pos[0] <= maxx) and (miny <= pos[1] <= maxy):
+        #             over_shape_list.append(obj_shape)
+        #
+        # try:
+        #     # if there is no shape under our click then deselect all shapes
+        #     if not over_shape_list:
+        #         self.exc_editor_app.selected = []
+        #         FlatCAMExcEditor.draw_shape_idx = -1
+        #         self.exc_editor_app.tools_table_exc.clearSelection()
+        #     else:
+        #         # if there are shapes under our click then advance through the list of them, one at the time in a
+        #         # circular way
+        #         FlatCAMExcEditor.draw_shape_idx = (FlatCAMExcEditor.draw_shape_idx + 1) % len(over_shape_list)
+        #         obj_to_add = over_shape_list[int(FlatCAMExcEditor.draw_shape_idx)]
+        #
+        #         if self.exc_editor_app.app.defaults["global_mselect_key"] == 'Shift':
+        #             if self.exc_editor_app.modifiers == Qt.ShiftModifier:
+        #                 if obj_to_add in self.exc_editor_app.selected:
+        #                     self.exc_editor_app.selected.remove(obj_to_add)
+        #                 else:
+        #                     self.exc_editor_app.selected.append(obj_to_add)
+        #             else:
+        #                 self.exc_editor_app.selected = []
+        #                 self.exc_editor_app.selected.append(obj_to_add)
+        #         else:
+        #             # if CONTROL key is pressed then we add to the selected list the current shape but if it's already
+        #             # in the selected list, we removed it. Therefore first click selects, second deselects.
+        #             if self.exc_editor_app.modifiers == Qt.ControlModifier:
+        #                 if obj_to_add in self.exc_editor_app.selected:
+        #                     self.exc_editor_app.selected.remove(obj_to_add)
+        #                 else:
+        #                     self.exc_editor_app.selected.append(obj_to_add)
+        #             else:
+        #                 self.exc_editor_app.selected = []
+        #                 self.exc_editor_app.selected.append(obj_to_add)
+        #
+        #     for storage in self.exc_editor_app.storage_dict:
+        #         for shape in self.exc_editor_app.selected:
+        #             if shape in self.exc_editor_app.storage_dict[storage].get_objects():
+        #                 for key in self.exc_editor_app.tool2tooldia:
+        #                     if self.exc_editor_app.tool2tooldia[key] == storage:
+        #                         item = self.exc_editor_app.tools_table_exc.item((key - 1), 1)
+        #                         item.setSelected(True)
+        #                         # self.exc_editor_app.tools_table_exc.selectItem(key - 1)
+        #
+        # except Exception as e:
+        #     log.error("[ERROR] Something went bad. %s" % str(e))
+        #     raise
+
+
+class FCMove(FCShapeTool):
+    def __init__(self, draw_app):
+        FCShapeTool.__init__(self, draw_app)
+        # self.shape_buffer = self.draw_app.shape_buffer
+        self.origin = None
+        self.destination = None
+        self.start_msg = "Click on reference point."
+
+    def set_origin(self, origin):
+        self.origin = origin
+
+    def click(self, point):
+        if len(self.draw_app.get_selected()) == 0:
+            return "Nothing to move."
+
+        if self.origin is None:
+            self.set_origin(point)
+            return "Click on final location."
+        else:
+            self.destination = point
+            self.make()
+            return "Done."
+
+    def make(self):
+        # Create new geometry
+        dx = self.destination[0] - self.origin[0]
+        dy = self.destination[1] - self.origin[1]
+        self.geometry = [DrawToolShape(affinity.translate(geom.geo, xoff=dx, yoff=dy))
+                         for geom in self.draw_app.get_selected()]
+
+        # Delete old
+        self.draw_app.delete_selected()
+
+        # # Select the new
+        # for g in self.geometry:
+        #     # Note that g is not in the app's buffer yet!
+        #     self.draw_app.set_selected(g)
+
+        self.complete = True
+
+    def utility_geometry(self, data=None):
+        """
+        Temporary geometry on screen while using this tool.
+
+        :param data:
+        :return:
+        """
+        geo_list = []
+
+        if self.origin is None:
+            return None
+
+        if len(self.draw_app.get_selected()) == 0:
+            return None
+
+        dx = data[0] - self.origin[0]
+        dy = data[1] - self.origin[1]
+        for geom in self.draw_app.get_selected():
+            geo_list.append(affinity.translate(geom.geo, xoff=dx, yoff=dy))
+
+        return DrawToolUtilityShape(geo_list)
+        # return DrawToolUtilityShape([affinity.translate(geom.geo, xoff=dx, yoff=dy)
+        #                              for geom in self.draw_app.get_selected()])
+
+
+class FCCopy(FCMove):
+
+    def make(self):
+        # Create new geometry
+        dx = self.destination[0] - self.origin[0]
+        dy = self.destination[1] - self.origin[1]
+        self.geometry = [DrawToolShape(affinity.translate(geom.geo, xoff=dx, yoff=dy))
+                         for geom in self.draw_app.get_selected()]
+        self.complete = True
+
+
+class FCText(FCShapeTool):
+    def __init__(self, draw_app):
+        FCShapeTool.__init__(self, draw_app)
+        # self.shape_buffer = self.draw_app.shape_buffer
+        self.draw_app = draw_app
+        self.app = draw_app.app
+
+        self.start_msg = "Click on the Destination point..."
+        self.origin = (0, 0)
+        self.text_gui = TextInputTool(self.app)
+        self.text_gui.run()
+
+    def click(self, point):
+        # Create new geometry
+        dx = point[0]
+        dy = point[1]
+        try:
+            self.geometry = DrawToolShape(affinity.translate(self.text_gui.text_path, xoff=dx, yoff=dy))
+        except Exception as e:
+            log.debug("Font geometry is empty or incorrect: %s" % str(e))
+            self.draw_app.app.inform.emit("[error]Font not supported. Only Regular, Bold, Italic and BoldItalic are "
+                                          "supported. Error: %s" % str(e))
+            self.text_gui.text_path = []
+            self.text_gui.hide_tool()
+            self.draw_app.select_tool('select')
+            return
+
+        self.text_gui.text_path = []
+        self.text_gui.hide_tool()
+        self.complete = True
+
+    def utility_geometry(self, data=None):
+        """
+        Temporary geometry on screen while using this tool.
+
+        :param data: mouse position coords
+        :return:
+        """
+
+        dx = data[0] - self.origin[0]
+        dy = data[1] - self.origin[1]
+
+        try:
+            return DrawToolUtilityShape(affinity.translate(self.text_gui.text_path, xoff=dx, yoff=dy))
+        except:
+            return
+
+class FCBuffer(FCShapeTool):
+    def __init__(self, draw_app):
+        FCShapeTool.__init__(self, draw_app)
+        # self.shape_buffer = self.draw_app.shape_buffer
+        self.draw_app = draw_app
+        self.app = draw_app.app
+
+        self.start_msg = "Create buffer geometry ..."
+        self.origin = (0, 0)
+        self.buff_tool = BufferSelectionTool(self.app, self.draw_app)
+        self.buff_tool.run()
+        self.app.ui.notebook.setTabText(2, "Buffer Tool")
+        self.activate()
+
+    def on_buffer(self):
+        buffer_distance = self.buff_tool.buffer_distance_entry.get_value()
+        # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment
+        # I populated the combobox such that the index coincide with the join styles value (whcih is really an INT)
+        join_style = self.buff_tool.buffer_corner_cb.currentIndex() + 1
+        self.draw_app.buffer(buffer_distance, join_style)
+        self.app.ui.notebook.setTabText(2, "Tools")
+        self.disactivate()
+
+    def on_buffer_int(self):
+        buffer_distance = self.buff_tool.buffer_distance_entry.get_value()
+        # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment
+        # I populated the combobox such that the index coincide with the join styles value (whcih is really an INT)
+        join_style = self.buff_tool.buffer_corner_cb.currentIndex() + 1
+        self.draw_app.buffer_int(buffer_distance, join_style)
+        self.app.ui.notebook.setTabText(2, "Tools")
+        self.disactivate()
+
+    def on_buffer_ext(self):
+        buffer_distance = self.buff_tool.buffer_distance_entry.get_value()
+        # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment
+        # I populated the combobox such that the index coincide with the join styles value (whcih is really an INT)
+        join_style = self.buff_tool.buffer_corner_cb.currentIndex() + 1
+        self.draw_app.buffer_ext(buffer_distance, join_style)
+        self.app.ui.notebook.setTabText(2, "Tools")
+        self.disactivate()
+
+    def activate(self):
+        self.buff_tool.buffer_button.clicked.disconnect()
+        self.buff_tool.buffer_int_button.clicked.disconnect()
+        self.buff_tool.buffer_ext_button.clicked.disconnect()
+
+        self.buff_tool.buffer_button.clicked.connect(self.on_buffer)
+        self.buff_tool.buffer_int_button.clicked.connect(self.on_buffer_int)
+        self.buff_tool.buffer_ext_button.clicked.connect(self.on_buffer_ext)
+
+    def disactivate(self):
+        self.buff_tool.buffer_button.clicked.disconnect()
+        self.buff_tool.buffer_int_button.clicked.disconnect()
+        self.buff_tool.buffer_ext_button.clicked.disconnect()
+
+        self.buff_tool.buffer_button.clicked.connect(self.buff_tool.on_buffer)
+        self.buff_tool.buffer_int_button.clicked.connect(self.buff_tool.on_buffer_int)
+        self.buff_tool.buffer_ext_button.clicked.connect(self.buff_tool.on_buffer_ext)
+        self.complete = True
+        self.draw_app.select_tool("select")
+        self.buff_tool.hide_tool()
+
+
+class FCPaint(FCShapeTool):
+    def __init__(self, draw_app):
+        FCShapeTool.__init__(self, draw_app)
+        # self.shape_buffer = self.draw_app.shape_buffer
+        self.draw_app = draw_app
+        self.app = draw_app.app
+
+        self.start_msg = "Create Paint geometry ..."
+        self.origin = (0, 0)
+        self.paint_tool = PaintOptionsTool(self.app, self.draw_app)
+        self.paint_tool.run()
+        self.app.ui.notebook.setTabText(2, "Paint Tool")
+
+
+class FCRotate(FCShapeTool):
+    def __init__(self, draw_app):
+        FCShapeTool.__init__(self, draw_app)
+
+        geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y))
+
+        if isinstance(geo, DrawToolShape) and geo.geo is not None:
+            self.draw_app.draw_utility_geometry(geo=geo)
+
+        self.draw_app.app.inform.emit("Click anywhere to finish the Rotation")
+
+    def set_origin(self, origin):
+        self.origin = origin
+
+
+    def make(self):
+        # Create new geometry
+        # dx = self.origin[0]
+        # dy = self.origin[1]
+        self.geometry = [DrawToolShape(affinity.rotate(geom.geo, angle = -90, origin='center'))
+                         for geom in self.draw_app.get_selected()]
+        # Delete old
+        self.draw_app.delete_selected()
+        self.complete = True
+
+        # MS: automatically select the Select Tool after finishing the action but is not working yet :(
+        #self.draw_app.select_tool("select")
+
+    def on_key(self, key):
+        if key == 'Enter':
+            if self.complete == True:
+                self.make()
+
+    def click(self, point):
+        self.make()
+        return "Done."
+
+    def utility_geometry(self, data=None):
+        """
+        Temporary geometry on screen while using this tool.
+
+        :param data:
+        :return:
+        """
+        return DrawToolUtilityShape([affinity.rotate(geom.geo, angle = -90, origin='center')
+                                     for geom in self.draw_app.get_selected()])
+
+
+class FCDrillAdd(FCShapeTool):
+    """
+    Resulting type: MultiLineString
+    """
+
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+
+        self.selected_dia = None
+        try:
+            self.draw_app.app.inform.emit(self.start_msg)
+            # self.selected_dia = self.draw_app.tool2tooldia[self.draw_app.tools_table_exc.currentRow() + 1]
+            self.selected_dia = self.draw_app.tool2tooldia[self.draw_app.last_tool_selected]
+            # as a visual marker, select again in tooltable the actual tool that we are using
+            # remember that it was deselected when clicking on canvas
+            item = self.draw_app.tools_table_exc.item((self.draw_app.last_tool_selected - 1), 1)
+            self.draw_app.tools_table_exc.setCurrentItem(item)
+
+        except KeyError:
+            self.draw_app.app.inform.emit("[warning_notcl] To add a drill first select a tool")
+            self.draw_app.select_tool("select")
+            return
+
+        geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y))
+
+        if isinstance(geo, DrawToolShape) and geo.geo is not None:
+            self.draw_app.draw_utility_geometry(geo=geo)
+
+        self.draw_app.app.inform.emit("Click on target location ...")
+
+        # Switch notebook to Selected page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.selected_tab)
+
+    def click(self, point):
+        self.make()
+        return "Done."
+
+    def utility_geometry(self, data=None):
+        self.points = data
+        return DrawToolUtilityShape(self.util_shape(data))
+
+    def util_shape(self, point):
+
+        start_hor_line = ((point[0] - (self.selected_dia / 2)), point[1])
+        stop_hor_line = ((point[0] + (self.selected_dia / 2)), point[1])
+        start_vert_line = (point[0], (point[1] - (self.selected_dia / 2)))
+        stop_vert_line = (point[0], (point[1] + (self.selected_dia / 2)))
+
+        return MultiLineString([(start_hor_line, stop_hor_line), (start_vert_line, stop_vert_line)])
+
+    def make(self):
+
+        # add the point to drills if the diameter is a key in the dict, if not, create it add the drill location
+        # to the value, as a list of itself
+        if self.selected_dia in self.draw_app.points_edit:
+            self.draw_app.points_edit[self.selected_dia].append(self.points)
+        else:
+            self.draw_app.points_edit[self.selected_dia] = [self.points]
+
+        self.draw_app.current_storage = self.draw_app.storage_dict[self.selected_dia]
+        self.geometry = DrawToolShape(self.util_shape(self.points))
+        self.complete = True
+        self.draw_app.app.inform.emit("[success]Done. Drill added.")
+
+
+class FCDrillArray(FCShapeTool):
+    """
+    Resulting type: MultiLineString
+    """
+
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+
+        self.draw_app.array_frame.show()
+
+        self.selected_dia = None
+        self.drill_axis = 'X'
+        self.drill_array = 'linear'
+        self.drill_array_size = None
+        self.drill_pitch = None
+
+        self.drill_angle = None
+        self.drill_direction = None
+        self.drill_radius = None
+
+        self.origin = None
+        self.destination = None
+        self.flag_for_circ_array = None
+
+        self.last_dx = 0
+        self.last_dy = 0
+
+        self.pt = []
+
+        try:
+            self.draw_app.app.inform.emit(self.start_msg)
+            self.selected_dia = self.draw_app.tool2tooldia[self.draw_app.last_tool_selected]
+            # as a visual marker, select again in tooltable the actual tool that we are using
+            # remember that it was deselected when clicking on canvas
+            item = self.draw_app.tools_table_exc.item((self.draw_app.last_tool_selected - 1), 1)
+            self.draw_app.tools_table_exc.setCurrentItem(item)
+        except KeyError:
+            self.draw_app.app.inform.emit("[warning_notcl] To add an Drill Array first select a tool in Tool Table")
+            return
+
+        geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y), static=True)
+
+        if isinstance(geo, DrawToolShape) and geo.geo is not None:
+            self.draw_app.draw_utility_geometry(geo=geo)
+
+        self.draw_app.app.inform.emit("Click on target location ...")
+
+        # Switch notebook to Selected page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.selected_tab)
+
+    def click(self, point):
+
+        if self.drill_array == 'Linear':
+            self.make()
+            return
+        else:
+            if self.flag_for_circ_array is None:
+                self.draw_app.in_action = True
+                self.pt.append(point)
+
+                self.flag_for_circ_array = True
+                self.set_origin(point)
+                self.draw_app.app.inform.emit("Click on the circular array Start position")
+            else:
+                self.destination = point
+                self.make()
+                self.flag_for_circ_array = None
+                return
+
+    def set_origin(self, origin):
+        self.origin = origin
+
+    def utility_geometry(self, data=None, static=None):
+        self.drill_axis = self.draw_app.drill_axis_radio.get_value()
+        self.drill_direction = self.draw_app.drill_direction_radio.get_value()
+        self.drill_array = self.draw_app.array_type_combo.get_value()
+        try:
+            self.drill_array_size = int(self.draw_app.drill_array_size_entry.get_value())
+            try:
+                self.drill_pitch = float(self.draw_app.drill_pitch_entry.get_value())
+                self.drill_angle = float(self.draw_app.drill_angle_entry.get_value())
+            except TypeError:
+                self.draw_app.app.inform.emit(
+                    "[error_notcl] The value is not Float. Check for comma instead of dot separator.")
+                return
+        except Exception as e:
+            self.draw_app.app.inform.emit("[error_notcl] The value is mistyped. Check the value.")
+            return
+
+        if self.drill_array == 'Linear':
+            # if self.origin is None:
+            #     self.origin = (0, 0)
+            #
+            # dx = data[0] - self.origin[0]
+            # dy = data[1] - self.origin[1]
+            dx = data[0]
+            dy = data[1]
+
+            geo_list = []
+            geo = None
+            self.points = data
+
+            for item in range(self.drill_array_size):
+                if self.drill_axis == 'X':
+                    geo = self.util_shape(((data[0] + (self.drill_pitch * item)), data[1]))
+                if self.drill_axis == 'Y':
+                    geo = self.util_shape((data[0], (data[1] + (self.drill_pitch * item))))
+                if static is None or static is False:
+                    geo_list.append(affinity.translate(geo, xoff=(dx - self.last_dx), yoff=(dy - self.last_dy)))
+                else:
+                    geo_list.append(geo)
+            # self.origin = data
+
+            self.last_dx = dx
+            self.last_dy = dy
+            return DrawToolUtilityShape(geo_list)
+        else:
+            if len(self.pt) > 0:
+                temp_points = [x for x in self.pt]
+                temp_points.append(data)
+                return DrawToolUtilityShape(LineString(temp_points))
+
+
+    def util_shape(self, point):
+        start_hor_line = ((point[0] - (self.selected_dia / 2)), point[1])
+        stop_hor_line = ((point[0] + (self.selected_dia / 2)), point[1])
+        start_vert_line = (point[0], (point[1] - (self.selected_dia / 2)))
+        stop_vert_line = (point[0], (point[1] + (self.selected_dia / 2)))
+
+        return MultiLineString([(start_hor_line, stop_hor_line), (start_vert_line, stop_vert_line)])
+
+    def make(self):
+        self.geometry = []
+        geo = None
+
+        # add the point to drills if the diameter is a key in the dict, if not, create it add the drill location
+        # to the value, as a list of itself
+        if self.selected_dia not in self.draw_app.points_edit:
+            self.draw_app.points_edit[self.selected_dia] = []
+        for i in range(self.drill_array_size):
+            self.draw_app.points_edit[self.selected_dia].append(self.points)
+
+        self.draw_app.current_storage = self.draw_app.storage_dict[self.selected_dia]
+
+        if self.drill_array == 'Linear':
+            for item in range(self.drill_array_size):
+                if self.drill_axis == 'X':
+                    geo = self.util_shape(((self.points[0] + (self.drill_pitch * item)), self.points[1]))
+                if self.drill_axis == 'Y':
+                    geo = self.util_shape((self.points[0], (self.points[1] + (self.drill_pitch * item))))
+
+                self.geometry.append(DrawToolShape(geo))
+        else:
+            if (self.drill_angle * self.drill_array_size) > 360:
+                self.draw_app.app.inform.emit("[warning_notcl]Too many drills for the selected spacing angle.")
+                return
+
+            radius = distance(self.destination, self.origin)
+            initial_angle = math.asin((self.destination[1] - self.origin[1]) / radius)
+            for i in range(self.drill_array_size):
+                angle_radians = math.radians(self.drill_angle * i)
+                if self.drill_direction == 'CW':
+                    x = self.origin[0] + radius * math.cos(-angle_radians + initial_angle)
+                    y = self.origin[1] + radius * math.sin(-angle_radians + initial_angle)
+                else:
+                    x = self.origin[0] + radius * math.cos(angle_radians + initial_angle)
+                    y = self.origin[1] + radius * math.sin(angle_radians + initial_angle)
+
+                geo = self.util_shape((x, y))
+                self.geometry.append(DrawToolShape(geo))
+        self.complete = True
+        self.draw_app.app.inform.emit("[success]Done. Drill Array added.")
+        self.draw_app.in_action = True
+        self.draw_app.array_frame.hide()
+        return
+
+class FCDrillResize(FCShapeTool):
+
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.draw_app.app.inform.emit("Click on the Drill(s) to resize ...")
+        self.resize_dia = None
+        self.draw_app.resize_frame.show()
+        self.points = None
+        self.selected_dia_list = []
+        self.current_storage = None
+        self.geometry = []
+        self.destination_storage = None
+
+        self.draw_app.resize_btn.clicked.connect(self.make)
+
+        # Switch notebook to Selected page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.selected_tab)
+
+    def make(self):
+        self.draw_app.is_modified = True
+
+        try:
+            new_dia = self.draw_app.resdrill_entry.get_value()
+        except:
+            self.draw_app.app.inform.emit("[error_notcl]Resize drill(s) failed. Please enter a diameter for resize.")
+            return
+
+        if new_dia not in self.draw_app.olddia_newdia:
+            self.destination_storage = FlatCAMGeoEditor.make_storage()
+            self.draw_app.storage_dict[new_dia] = self.destination_storage
+
+            # self.olddia_newdia dict keeps the evidence on current tools diameters as keys and gets updated on values
+            # each time a tool diameter is edited or added
+            self.draw_app.olddia_newdia[new_dia] = new_dia
+        else:
+            self.destination_storage = self.draw_app.storage_dict[new_dia]
+
+        for index in self.draw_app.tools_table_exc.selectedIndexes():
+            row = index.row()
+            # on column 1 in tool tables we hold the diameters, and we retrieve them as strings
+            # therefore below we convert to float
+            dia_on_row = self.draw_app.tools_table_exc.item(row, 1).text()
+            self.selected_dia_list.append(float(dia_on_row))
+
+        # since we add a new tool, we update also the intial state of the tool_table through it's dictionary
+        # we add a new entry in the tool2tooldia dict
+        self.draw_app.tool2tooldia[len(self.draw_app.olddia_newdia)] = new_dia
+
+        sel_shapes_to_be_deleted = []
+
+        for sel_dia in self.selected_dia_list:
+            self.current_storage = self.draw_app.storage_dict[sel_dia]
+            for select_shape in self.draw_app.get_selected():
+                if select_shape in self.current_storage.get_objects():
+                    factor = new_dia / sel_dia
+                    self.geometry.append(
+                        DrawToolShape(affinity.scale(select_shape.geo, xfact=factor, yfact=factor, origin='center'))
+                    )
+                    self.current_storage.remove(select_shape)
+                    # a hack to make the tool_table display less drills per diameter when shape(drill) is deleted
+                    # self.points_edit it's only useful first time when we load the data into the storage
+                    # but is still used as reference when building tool_table in self.build_ui()
+                    # the number of drills displayed in column 2 is just a len(self.points_edit) therefore
+                    # deleting self.points_edit elements (doesn't matter who but just the number)
+                    # solved the display issue.
+                    del self.draw_app.points_edit[sel_dia][0]
+
+                    sel_shapes_to_be_deleted.append(select_shape)
+
+                    self.draw_app.on_exc_shape_complete(self.destination_storage)
+                    # a hack to make the tool_table display more drills per diameter when shape(drill) is added
+                    # self.points_edit it's only useful first time when we load the data into the storage
+                    # but is still used as reference when building tool_table in self.build_ui()
+                    # the number of drills displayed in column 2 is just a len(self.points_edit) therefore
+                    # deleting self.points_edit elements (doesn't matter who but just the number)
+                    # solved the display issue.
+                    if new_dia not in self.draw_app.points_edit:
+                        self.draw_app.points_edit[new_dia] = [(0, 0)]
+                    else:
+                        self.draw_app.points_edit[new_dia].append((0,0))
+                    self.geometry = []
+
+                    # if following the resize of the drills there will be no more drills for the selected tool then
+                    # delete that tool
+                    if not self.draw_app.points_edit[sel_dia]:
+                        self.draw_app.on_tool_delete(sel_dia)
+
+            for shp in sel_shapes_to_be_deleted:
+                self.draw_app.selected.remove(shp)
+            sel_shapes_to_be_deleted = []
+
+        self.draw_app.build_ui()
+        self.draw_app.replot()
+
+        self.draw_app.resize_frame.hide()
+        self.complete = True
+        self.draw_app.app.inform.emit("[success]Done. Drill Resize completed.")
+
+        # MS: always return to the Select Tool
+        self.draw_app.select_tool("select")
+
+
+class FCDrillMove(FCShapeTool):
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        # self.shape_buffer = self.draw_app.shape_buffer
+        self.origin = None
+        self.destination = None
+        self.selected_dia_list = []
+
+        if self.draw_app.launched_from_shortcuts is True:
+            self.draw_app.launched_from_shortcuts = False
+            self.draw_app.app.inform.emit("Click on target location ...")
+        else:
+            self.draw_app.app.inform.emit("Click on reference location ...")
+        self.current_storage = None
+        self.geometry = []
+
+        for index in self.draw_app.tools_table_exc.selectedIndexes():
+            row = index.row()
+            # on column 1 in tool tables we hold the diameters, and we retrieve them as strings
+            # therefore below we convert to float
+            dia_on_row = self.draw_app.tools_table_exc.item(row, 1).text()
+            self.selected_dia_list.append(float(dia_on_row))
+
+        # Switch notebook to Selected page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.selected_tab)
+
+    def set_origin(self, origin):
+        self.origin = origin
+
+    def click(self, point):
+        if len(self.draw_app.get_selected()) == 0:
+            return "Nothing to move."
+
+        if self.origin is None:
+            self.set_origin(point)
+            self.draw_app.app.inform.emit("Click on target location ...")
+            return
+        else:
+            self.destination = point
+            self.make()
+
+            # MS: always return to the Select Tool
+            self.draw_app.select_tool("select")
+            return
+
+    def make(self):
+        # Create new geometry
+        dx = self.destination[0] - self.origin[0]
+        dy = self.destination[1] - self.origin[1]
+        sel_shapes_to_be_deleted = []
+
+        for sel_dia in self.selected_dia_list:
+            self.current_storage = self.draw_app.storage_dict[sel_dia]
+            for select_shape in self.draw_app.get_selected():
+                if select_shape in self.current_storage.get_objects():
+
+                    self.geometry.append(DrawToolShape(affinity.translate(select_shape.geo, xoff=dx, yoff=dy)))
+                    self.current_storage.remove(select_shape)
+                    sel_shapes_to_be_deleted.append(select_shape)
+                    self.draw_app.on_exc_shape_complete(self.current_storage)
+                    self.geometry = []
+
+            for shp in sel_shapes_to_be_deleted:
+                self.draw_app.selected.remove(shp)
+            sel_shapes_to_be_deleted = []
+
+        self.draw_app.build_ui()
+        self.draw_app.app.inform.emit("[success]Done. Drill(s) Move completed.")
+
+    def utility_geometry(self, data=None):
+        """
+        Temporary geometry on screen while using this tool.
+
+        :param data:
+        :return:
+        """
+        geo_list = []
+
+        if self.origin is None:
+            return None
+
+        if len(self.draw_app.get_selected()) == 0:
+            return None
+
+        dx = data[0] - self.origin[0]
+        dy = data[1] - self.origin[1]
+        for geom in self.draw_app.get_selected():
+            geo_list.append(affinity.translate(geom.geo, xoff=dx, yoff=dy))
+        return DrawToolUtilityShape(geo_list)
+
+
+class FCDrillCopy(FCDrillMove):
+
+    def make(self):
+        # Create new geometry
+        dx = self.destination[0] - self.origin[0]
+        dy = self.destination[1] - self.origin[1]
+        sel_shapes_to_be_deleted = []
+
+        for sel_dia in self.selected_dia_list:
+            self.current_storage = self.draw_app.storage_dict[sel_dia]
+            for select_shape in self.draw_app.get_selected():
+                if select_shape in self.current_storage.get_objects():
+                    self.geometry.append(DrawToolShape(affinity.translate(select_shape.geo, xoff=dx, yoff=dy)))
+
+                    # add some fake drills into the self.draw_app.points_edit to update the drill count in tool table
+                    self.draw_app.points_edit[sel_dia].append((0, 0))
+
+                    sel_shapes_to_be_deleted.append(select_shape)
+                    self.draw_app.on_exc_shape_complete(self.current_storage)
+                    self.geometry = []
+
+            for shp in sel_shapes_to_be_deleted:
+                self.draw_app.selected.remove(shp)
+            sel_shapes_to_be_deleted = []
+
+        self.draw_app.build_ui()
+        self.draw_app.app.inform.emit("[success]Done. Drill(s) copied.")
+
+
+########################
+### Main Application ###
+########################
+class FlatCAMGeoEditor(QtCore.QObject):
+
+    draw_shape_idx = -1
+
+    def __init__(self, app, disabled=False):
+        assert isinstance(app, FlatCAMApp.App), \
+            "Expected the app to be a FlatCAMApp.App, got %s" % type(app)
+
+        super(FlatCAMGeoEditor, self).__init__()
+
+        self.app = app
+        self.canvas = app.plotcanvas
+
+        self.app.ui.geo_edit_toolbar.setDisabled(disabled)
+        self.app.ui.snap_max_dist_entry.setDisabled(disabled)
+
+        self.app.ui.geo_add_circle_menuitem.triggered.connect(lambda: self.select_tool('circle'))
+        self.app.ui.geo_add_arc_menuitem.triggered.connect(lambda: self.select_tool('arc'))
+        self.app.ui.geo_add_rectangle_menuitem.triggered.connect(lambda: self.select_tool('rectangle'))
+        self.app.ui.geo_add_polygon_menuitem.triggered.connect(lambda: self.select_tool('polygon'))
+        self.app.ui.geo_add_path_menuitem.triggered.connect(lambda: self.select_tool('path'))
+        self.app.ui.geo_add_text_menuitem.triggered.connect(lambda: self.select_tool('text'))
+        self.app.ui.geo_paint_menuitem.triggered.connect(self.on_paint_tool)
+        self.app.ui.geo_buffer_menuitem.triggered.connect(self.on_buffer_tool)
+        self.app.ui.geo_delete_menuitem.triggered.connect(self.on_delete_btn)
+        self.app.ui.geo_union_menuitem.triggered.connect(self.union)
+        self.app.ui.geo_intersection_menuitem.triggered.connect(self.intersection)
+        self.app.ui.geo_subtract_menuitem.triggered.connect(self.subtract)
+        self.app.ui.geo_cutpath_menuitem.triggered.connect(self.cutpath)
+        self.app.ui.geo_copy_menuitem.triggered.connect(lambda: self.select_tool('copy'))
+
+        self.app.ui.geo_union_btn.triggered.connect(self.union)
+        self.app.ui.geo_intersection_btn.triggered.connect(self.intersection)
+        self.app.ui.geo_subtract_btn.triggered.connect(self.subtract)
+        self.app.ui.geo_cutpath_btn.triggered.connect(self.cutpath)
+        self.app.ui.geo_delete_btn.triggered.connect(self.on_delete_btn)
+
+        ## Toolbar events and properties
+        self.tools = {
+            "select": {"button": self.app.ui.geo_select_btn,
+                       "constructor": FCSelect},
+            "arc": {"button": self.app.ui.geo_add_arc_btn,
+                    "constructor": FCArc},
+            "circle": {"button": self.app.ui.geo_add_circle_btn,
+                       "constructor": FCCircle},
+            "path": {"button": self.app.ui.geo_add_path_btn,
+                     "constructor": FCPath},
+            "rectangle": {"button": self.app.ui.geo_add_rectangle_btn,
+                          "constructor": FCRectangle},
+            "polygon": {"button": self.app.ui.geo_add_polygon_btn,
+                        "constructor": FCPolygon},
+            "text": {"button": self.app.ui.geo_add_text_btn,
+                     "constructor": FCText},
+            "buffer": {"button": self.app.ui.geo_add_buffer_btn,
+                     "constructor": FCBuffer},
+            "paint": {"button": self.app.ui.geo_add_paint_btn,
+                       "constructor": FCPaint},
+            "move": {"button": self.app.ui.geo_move_btn,
+                     "constructor": FCMove},
+            "rotate": {"button": self.app.ui.geo_rotate_btn,
+                     "constructor": FCRotate},
+            "copy": {"button": self.app.ui.geo_copy_btn,
+                     "constructor": FCCopy}
+        }
+
+        ### Data
+        self.active_tool = None
+
+        self.storage = FlatCAMGeoEditor.make_storage()
+        self.utility = []
+
+        # VisPy visuals
+        self.fcgeometry = None
+        self.shapes = self.app.plotcanvas.new_shape_collection(layers=1)
+        self.tool_shape = self.app.plotcanvas.new_shape_collection(layers=1)
+        self.app.pool_recreated.connect(self.pool_recreated)
+
+        # Remove from scene
+        self.shapes.enabled = False
+        self.tool_shape.enabled = False
+
+        ## List of selected shapes.
+        self.selected = []
+
+        self.flat_geo = []
+
+        self.move_timer = QtCore.QTimer()
+        self.move_timer.setSingleShot(True)
+
+        self.key = None  # Currently pressed key
+        self.geo_key_modifiers = None
+        self.x = None  # Current mouse cursor pos
+        self.y = None
+        # Current snapped mouse pos
+        self.snap_x = None
+        self.snap_y = None
+        self.pos = None
+
+        # signal that there is an action active like polygon or path
+        self.in_action = False
+
+        def make_callback(thetool):
+            def f():
+                self.on_tool_select(thetool)
+            return f
+
+        for tool in self.tools:
+            self.tools[tool]["button"].triggered.connect(make_callback(tool))  # Events
+            self.tools[tool]["button"].setCheckable(True)  # Checkable
+
+        self.app.ui.grid_snap_btn.triggered.connect(self.on_grid_toggled)
+        self.app.ui.corner_snap_btn.triggered.connect(lambda: self.toolbar_tool_toggle("corner_snap"))
+
+        self.options = {
+            "global_gridx": 0.1,
+            "global_gridy": 0.1,
+            "snap_max": 0.05,
+            "grid_snap": True,
+            "corner_snap": False,
+            "grid_gap_link": True
+        }
+        self.app.options_read_form()
+
+        for option in self.options:
+            if option in self.app.options:
+                self.options[option] = self.app.options[option]
+
+        self.app.ui.grid_gap_x_entry.setText(str(self.options["global_gridx"]))
+        self.app.ui.grid_gap_y_entry.setText(str(self.options["global_gridy"]))
+        self.app.ui.snap_max_dist_entry.setText(str(self.options["snap_max"]))
+        self.app.ui.grid_gap_link_cb.setChecked(True)
+
+        self.rtree_index = rtindex.Index()
+
+        def entry2option(option, entry):
+            try:
+                self.options[option] = float(entry.text())
+            except Exception as e:
+                log.debug(str(e))
+
+        self.app.ui.grid_gap_x_entry.setValidator(QtGui.QDoubleValidator())
+        self.app.ui.grid_gap_x_entry.textChanged.connect(
+            lambda: entry2option("global_gridx", self.app.ui.grid_gap_x_entry))
+
+        self.app.ui.grid_gap_y_entry.setValidator(QtGui.QDoubleValidator())
+        self.app.ui.grid_gap_y_entry.textChanged.connect(
+            lambda: entry2option("global_gridy", self.app.ui.grid_gap_y_entry))
+
+        self.app.ui.snap_max_dist_entry.setValidator(QtGui.QDoubleValidator())
+        self.app.ui.snap_max_dist_entry.textChanged.connect(
+            lambda: entry2option("snap_max", self.app.ui.snap_max_dist_entry))
+
+        # store the status of the editor so the Delete at object level will not work until the edit is finished
+        self.editor_active = False
+
+        # if using Paint store here the tool diameter used
+        self.paint_tooldia = None
+
+    def pool_recreated(self, pool):
+        self.shapes.pool = pool
+        self.tool_shape.pool = pool
+
+    def activate(self):
+        self.connect_canvas_event_handlers()
+        self.shapes.enabled = True
+        self.tool_shape.enabled = True
+        self.app.app_cursor.enabled = True
+        self.app.ui.snap_max_dist_entry.setDisabled(False)
+        self.app.ui.corner_snap_btn.setEnabled(True)
+
+        self.app.ui.geo_editor_menu.setDisabled(False)
+        # Tell the App that the editor is active
+        self.editor_active = True
+
+    def deactivate(self):
+        self.disconnect_canvas_event_handlers()
+        self.clear()
+        self.app.ui.geo_edit_toolbar.setDisabled(True)
+        self.app.ui.geo_edit_toolbar.setVisible(False)
+        self.app.ui.snap_max_dist_entry.setDisabled(True)
+        self.app.ui.corner_snap_btn.setEnabled(False)
+        # never deactivate the snap toolbar - MS
+        # self.app.ui.snap_toolbar.setDisabled(True)  # TODO: Combine and move into tool
+
+        # Disable visuals
+        self.shapes.enabled = False
+        self.tool_shape.enabled = False
+        self.app.app_cursor.enabled = False
+
+        self.app.ui.geo_editor_menu.setDisabled(True)
+        # Tell the app that the editor is no longer active
+        self.editor_active = False
+
+        # Show original geometry
+        if self.fcgeometry:
+            self.fcgeometry.visible = True
+
+    def connect_canvas_event_handlers(self):
+        ## Canvas events
+
+        # make sure that the shortcuts key and mouse events will no longer be linked to the methods from FlatCAMApp
+        # but those from FlatCAMGeoEditor
+        self.app.plotcanvas.vis_disconnect('key_press', self.app.on_key_over_plot)
+        self.app.plotcanvas.vis_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
+        self.app.plotcanvas.vis_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
+        self.app.plotcanvas.vis_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
+        self.app.plotcanvas.vis_disconnect('mouse_double_click', self.app.on_double_click_over_plot)
+        self.app.collection.view.keyPressed.disconnect()
+        self.app.collection.view.clicked.disconnect()
+
+        self.canvas.vis_connect('mouse_press', self.on_canvas_click)
+        self.canvas.vis_connect('mouse_move', self.on_canvas_move)
+        self.canvas.vis_connect('mouse_release', self.on_canvas_click_release)
+        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.vis_disconnect('mouse_press', self.on_canvas_click)
+        self.canvas.vis_disconnect('mouse_move', self.on_canvas_move)
+        self.canvas.vis_disconnect('mouse_release', self.on_canvas_click_release)
+        self.canvas.vis_disconnect('key_press', self.on_canvas_key)
+        self.canvas.vis_disconnect('key_release', self.on_canvas_key_release)
+
+        # we restore the key and mouse control to FlatCAMApp method
+        self.app.plotcanvas.vis_connect('key_press', self.app.on_key_over_plot)
+        self.app.plotcanvas.vis_connect('mouse_press', self.app.on_mouse_click_over_plot)
+        self.app.plotcanvas.vis_connect('mouse_move', self.app.on_mouse_move_over_plot)
+        self.app.plotcanvas.vis_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
+        self.app.plotcanvas.vis_connect('mouse_double_click', self.app.on_double_click_over_plot)
+        self.app.collection.view.keyPressed.connect(self.app.collection.on_key)
+        self.app.collection.view.clicked.connect(self.app.collection.on_mouse_down)
+
+    def add_shape(self, shape):
+        """
+        Adds a shape to the shape storage.
+
+        :param shape: Shape to be added.
+        :type shape: DrawToolShape
+        :return: None
+        """
+
+        # List of DrawToolShape?
+        if isinstance(shape, list):
+            for subshape in shape:
+                self.add_shape(subshape)
+            return
+
+        assert isinstance(shape, DrawToolShape), \
+            "Expected a DrawToolShape, got %s" % type(shape)
+
+        assert shape.geo is not None, \
+            "Shape object has empty geometry (None)"
+
+        assert (isinstance(shape.geo, list) and len(shape.geo) > 0) or \
+               not isinstance(shape.geo, list), \
+            "Shape objects has empty geometry ([])"
+
+        if isinstance(shape, DrawToolUtilityShape):
+            self.utility.append(shape)
+        else:
+            self.storage.insert(shape)  # TODO: Check performance
+
+    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]
+        for_deletion = [shape for shape in self.utility]
+        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:]
+        toolgeo = cascaded_union([shp.geo for shp in tools])
+
+        target = selected[0]
+        if type(target.geo) == Polygon:
+            for ring in poly2rings(target.geo):
+                self.add_shape(DrawToolShape(ring.difference(toolgeo)))
+            self.delete_shape(target)
+        elif type(target.geo) == LineString or type(target.geo) == LinearRing:
+            self.add_shape(DrawToolShape(target.geo.difference(toolgeo)))
+            self.delete_shape(target)
+        elif type(target.geo) == MultiLineString:
+            try:
+                for linestring in target.geo:
+                    self.add_shape(DrawToolShape(linestring.difference(toolgeo)))
+            except:
+                self.app.log.warning("Current LinearString does not intersect the target")
+            self.delete_shape(target)
+        else:
+            self.app.log.warning("Not implemented. Object type: %s" % str(type(target.geo)))
+
+        self.replot()
+
+    def toolbar_tool_toggle(self, key):
+        self.options[key] = self.sender().isChecked()
+        if self.options[key] == True:
+            return 1
+        else:
+            return 0
+
+    def clear(self):
+        self.active_tool = None
+        # self.shape_buffer = []
+        self.selected = []
+        self.shapes.clear(update=True)
+        self.tool_shape.clear(update=True)
+
+        self.storage = FlatCAMGeoEditor.make_storage()
+        self.replot()
+
+    def edit_fcgeometry(self, fcgeometry):
+        """
+        Imports the geometry from the given FlatCAM Geometry object
+        into the editor.
+
+        :param fcgeometry: FlatCAMGeometry
+        :return: None
+        """
+        assert isinstance(fcgeometry, Geometry), \
+            "Expected a Geometry, got %s" % type(fcgeometry)
+
+        self.deactivate()
+        self.activate()
+
+        # Hide original geometry
+        self.fcgeometry = fcgeometry
+        fcgeometry.visible = False
+
+        # Set selection tolerance
+        DrawToolShape.tolerance = fcgeometry.drawing_tolerance * 10
+
+        self.select_tool("select")
+
+        # Link shapes into editor.
+        for shape in fcgeometry.flatten():
+            if shape is not None:  # TODO: Make flatten never create a None
+                if type(shape) == Polygon:
+                    self.add_shape(DrawToolShape(shape.exterior))
+                    for inter in shape.interiors:
+                        self.add_shape(DrawToolShape(inter))
+                else:
+                    self.add_shape(DrawToolShape(shape))
+
+        self.replot()
+        self.app.ui.geo_edit_toolbar.setDisabled(False)
+        self.app.ui.geo_edit_toolbar.setVisible(True)
+        self.app.ui.snap_toolbar.setDisabled(False)
+
+        # start with GRID toolbar activated
+        if self.app.ui.grid_snap_btn.isChecked() == False:
+            self.app.ui.grid_snap_btn.trigger()
+
+    def on_buffer_tool(self):
+        buff_tool = BufferSelectionTool(self.app, self)
+        buff_tool.run()
+
+    def on_paint_tool(self):
+        paint_tool = PaintOptionsTool(self.app, self)
+        paint_tool.run()
+
+    def on_tool_select(self, tool):
+        """
+        Behavior of the toolbar. Tool initialization.
+
+        :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." % tool)
+                for t in self.tools:
+                    if t != tool:
+                        self.tools[t]["button"].setChecked(False)
+
+                self.active_tool = self.tools[tool]["constructor"](self)
+                if not isinstance(self.active_tool, FCSelect):
+                    self.app.inform.emit(self.active_tool.start_msg)
+            else:
+                self.app.log.debug("%s is NOT checked." % tool)
+                for t in self.tools:
+                    self.tools[t]["button"].setChecked(False)
+                self.active_tool = None
+
+    def draw_tool_path(self):
+        self.select_tool('path')
+        return
+
+    def draw_tool_rectangle(self):
+        self.select_tool('rectangle')
+        return
+
+    def on_grid_toggled(self):
+        self.toolbar_tool_toggle("grid_snap")
+
+        # make sure that the cursor shape is enabled/disabled, too
+        if self.options['grid_snap'] is True:
+            self.app.app_cursor.enabled = True
+        else:
+            self.app.app_cursor.enabled = False
+
+    def on_canvas_click(self, event):
+        """
+        event.x and .y have canvas coordinates
+        event.xdaya and .ydata have plot coordinates
+
+        :param event: Event object dispatched by Matplotlib
+        :return: None
+        """
+
+        if event.button is 1:
+            self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                                   "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
+            self.pos = self.canvas.vispy_canvas.translate_coords(event.pos)
+
+            ### Snap coordinates
+            x, y = self.snap(self.pos[0], self.pos[1])
+
+            self.pos = (x, y)
+
+            # 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(self.pos[0], self.pos[1]))
+
+                # If it is a shape generating tool
+                if isinstance(self.active_tool, FCShapeTool) and self.active_tool.complete:
+                    self.on_shape_complete()
+
+                    # MS: always return to the Select Tool
+                    self.select_tool("select")
+                    return
+
+                if isinstance(self.active_tool, FCSelect):
+                    # self.app.log.debug("Replotting after click.")
+                    self.replot()
+
+            else:
+                self.app.log.debug("No active tool to respond to click!")
+
+    def on_canvas_move(self, event):
+        """
+        Called on 'mouse_move' event
+
+        event.pos have canvas screen coordinates
+
+        :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
+
+        # if the RMB is clicked and mouse is moving over plot then 'panning_action' is True
+        if event.button == 2:
+            self.app.panning_action = True
+            return
+        else:
+            self.app.panning_action = False
+
+        try:
+            x = float(event.xdata)
+            y = float(event.ydata)
+        except TypeError:
+            return
+
+        if self.active_tool is None:
+            return
+
+        ### Snap coordinates
+        x, y = self.snap(x, y)
+
+        self.snap_x = x
+        self.snap_y = y
+
+        # update the position label in the infobar since the APP mouse event handlers are disconnected
+        self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
+                                       "<b>Y</b>: %.4f" % (x, y))
+
+        if self.pos is None:
+            self.pos = (0, 0)
+        dx = x - self.pos[0]
+        dy = y - self.pos[1]
+
+        # update the reference position label in the infobar since the APP mouse event handlers are disconnected
+        self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                           "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (dx, dy))
+
+        ### Utility geometry (animated)
+        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.tool_shape.clear(update=True)
+            self.draw_utility_geometry(geo=geo)
+
+        ### Selection area on canvas section ###
+        dx = pos[0] - self.pos[0]
+        if event.is_dragging == 1 and event.button == 1:
+            self.app.delete_selection_shape()
+            if dx < 0:
+                self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x,y),
+                     color=self.app.defaults["global_alt_sel_line"],
+                     face_color=self.app.defaults['global_alt_sel_fill'])
+                self.app.selection_type = False
+            else:
+                self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x,y))
+                self.app.selection_type = True
+        else:
+            self.app.selection_type = None
+
+        # Update cursor
+        self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color='black', size=20)
+
+    def on_canvas_click_release(self, event):
+        pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
+
+        if self.app.grid_status():
+            pos = self.snap(pos_canvas[0], pos_canvas[1])
+        else:
+            pos = (pos_canvas[0], pos_canvas[1])
+
+        # if the released mouse button was RMB then test if it was a panning motion or not, if not it was a context
+        # canvas menu
+        try:
+            if event.button == 2:  # right click
+                if self.app.panning_action is True:
+                    self.app.panning_action = False
+                else:
+                    if self.in_action is False:
+                        self.app.cursor = QtGui.QCursor()
+                        self.app.ui.popMenu.popup(self.app.cursor.pos())
+                    else:
+                        # if right click on canvas and the active tool need to be finished (like Path or Polygon)
+                        # right mouse click will finish the action
+                        if isinstance(self.active_tool, FCShapeTool):
+                            self.active_tool.click(self.snap(self.x, self.y))
+                            self.active_tool.make()
+                            if self.active_tool.complete:
+                                self.on_shape_complete()
+                                self.app.inform.emit("[success]Done.")
+                            # automatically make the selection tool active after completing current action
+                            self.select_tool('select')
+        except Exception as e:
+            log.warning("Error: %s" % str(e))
+            return
+
+        # if the released mouse button was LMB then test if we had a right-to-left selection or a left-to-right
+        # selection and then select a type of selection ("enclosing" or "touching")
+        try:
+            if event.button == 1:  # left click
+                if self.app.selection_type is not None:
+                    self.draw_selection_area_handler(self.pos, pos, self.app.selection_type)
+                    self.app.selection_type = None
+                elif isinstance(self.active_tool, FCSelect):
+                    # Dispatch event to active_tool
+                    # msg = self.active_tool.click(self.snap(event.xdata, event.ydata))
+                    msg = self.active_tool.click_release((self.pos[0], self.pos[1]))
+                    self.app.inform.emit(msg)
+                    self.replot()
+        except Exception as e:
+            log.warning("Error: %s" % str(e))
+            return
+
+    def draw_selection_area_handler(self, start_pos, end_pos, sel_type):
+        """
+
+        :param start_pos: mouse position when the selection LMB click was done
+        :param end_pos: mouse position when the left mouse button is released
+        :param sel_type: if True it's a left to right selection (enclosure), if False it's a 'touch' selection
+        :type Bool
+        :return:
+        """
+        poly_selection = Polygon([start_pos, (end_pos[0], start_pos[1]), end_pos, (start_pos[0], end_pos[1])])
+
+        self.app.delete_selection_shape()
+        for obj in self.storage.get_objects():
+            if (sel_type is True and poly_selection.contains(obj.geo)) or \
+                    (sel_type is False and poly_selection.intersects(obj.geo)):
+                    if self.key == self.app.defaults["global_mselect_key"]:
+                        if obj in self.selected:
+                            self.selected.remove(obj)
+                        else:
+                            # add the object to the selected shapes
+                            self.selected.append(obj)
+                    else:
+                        self.selected.append(obj)
+        self.replot()
+
+    def draw_utility_geometry(self, geo):
+            # Add the new utility shape
+            try:
+                # this case is for the Font Parse
+                for el in list(geo.geo):
+                    if type(el) == MultiPolygon:
+                        for poly in el:
+                            self.tool_shape.add(
+                                shape=poly,
+                                color=(self.app.defaults["global_draw_color"] + '80'),
+                                update=False,
+                                layer=0,
+                                tolerance=None
+                            )
+                    elif type(el) == MultiLineString:
+                        for linestring in el:
+                            self.tool_shape.add(
+                                shape=linestring,
+                                color=(self.app.defaults["global_draw_color"] + '80'),
+                                update=False,
+                                layer=0,
+                                tolerance=None
+                            )
+                    else:
+                        self.tool_shape.add(
+                            shape=el,
+                            color=(self.app.defaults["global_draw_color"] + '80'),
+                            update=False,
+                            layer=0,
+                            tolerance=None
+                        )
+            except TypeError:
+                self.tool_shape.add(
+                    shape=geo.geo, color=(self.app.defaults["global_draw_color"] + '80'),
+                    update=False, layer=0, tolerance=None)
+
+            self.tool_shape.redraw()
+
+    def on_canvas_key(self, event):
+        """
+        event.key has the key.
+
+        :param event:
+        :return:
+        """
+        self.key = event.key.name
+        self.geo_key_modifiers = QtWidgets.QApplication.keyboardModifiers()
+
+        if self.geo_key_modifiers == Qt.ControlModifier:
+            # save (update) the current geometry and return to the App
+            if self.key == 'S':
+                self.app.editor2object()
+                return
+
+            # toggle the measurement tool
+            if self.key == 'M':
+                self.app.measurement_tool.run()
+                return
+
+        # Finish the current action. Use with tools that do not
+        # complete automatically, like a polygon or path.
+        if event.key.name == 'Enter':
+            if isinstance(self.active_tool, FCShapeTool):
+                self.active_tool.click(self.snap(self.x, self.y))
+                self.active_tool.make()
+                if self.active_tool.complete:
+                    self.on_shape_complete()
+                    self.app.inform.emit("[success]Done.")
+                # automatically make the selection tool active after completing current action
+                self.select_tool('select')
+            return
+
+        # Abort the current action
+        if event.key.name == 'Escape':
+            # TODO: ...?
+            # self.on_tool_select("select")
+            self.app.inform.emit("[warning_notcl]Cancelled.")
+
+            self.delete_utility_geometry()
+
+            self.replot()
+            # self.select_btn.setChecked(True)
+            # self.on_tool_select('select')
+            self.select_tool('select')
+            return
+
+        # Delete selected object
+        if event.key.name == 'Delete':
+            self.delete_selected()
+            self.replot()
+
+        # Move
+        if event.key.name == 'Space':
+            self.app.ui.geo_rotate_btn.setChecked(True)
+            self.on_tool_select('rotate')
+            self.active_tool.set_origin(self.snap(self.x, self.y))
+
+        # Arc Tool
+        if event.key.name == 'A':
+            self.select_tool('arc')
+
+        # Buffer
+        if event.key.name == 'B':
+            self.select_tool('buffer')
+
+        # Copy
+        if event.key.name == 'C':
+            self.app.ui.geo_copy_btn.setChecked(True)
+            self.on_tool_select('copy')
+            self.active_tool.set_origin(self.snap(self.x, self.y))
+            self.app.inform.emit("Click on target point.")
+
+        # Grid Snap
+        if event.key.name == 'G':
+            self.app.ui.grid_snap_btn.trigger()
+
+            # make sure that the cursor shape is enabled/disabled, too
+            if self.options['grid_snap'] is True:
+                self.app.app_cursor.enabled = True
+            else:
+                self.app.app_cursor.enabled = False
+
+        # Paint
+        if event.key.name == 'I':
+            self.select_tool('paint')
+
+        # Corner Snap
+        if event.key.name == 'K':
+            self.app.ui.corner_snap_btn.trigger()
+
+        # Move
+        if event.key.name == 'M':
+            self.app.ui.geo_move_btn.setChecked(True)
+            self.on_tool_select('move')
+            self.active_tool.set_origin(self.snap(self.x, self.y))
+            self.app.inform.emit("Click on target point.")
+
+        # Polygon Tool
+        if event.key.name == 'N':
+            self.select_tool('polygon')
+
+        # Circle Tool
+        if event.key.name == 'O':
+            self.select_tool('circle')
+
+        # Path Tool
+        if event.key.name == 'P':
+            self.select_tool('path')
+
+        # Rectangle Tool
+        if event.key.name == 'R':
+            self.select_tool('rectangle')
+
+        # Select Tool
+        if event.key.name == 'S':
+            self.select_tool('select')
+
+        # Add Text Tool
+        if event.key.name == 'T':
+            self.select_tool('text')
+
+        # Cut Action Tool
+        if event.key.name == 'X':
+            if self.get_selected() is not None:
+                self.cutpath()
+            else:
+                msg = 'Please first select a geometry item to be cutted\n' \
+                      'then select the geometry item that will be cutted\n' \
+                      'out of the first item. In the end press ~X~ key or\n' \
+                      'the toolbar button.' \
+
+                messagebox =QtWidgets.QMessageBox()
+                messagebox.setText(msg)
+                messagebox.setWindowTitle("Warning")
+                messagebox.setWindowIcon(QtGui.QIcon('share/warning.png'))
+                messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+                messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+                messagebox.exec_()
+
+        # Propagate to tool
+        response = None
+        if self.active_tool is not None:
+            response = self.active_tool.on_key(event.key)
+        if response is not None:
+            self.app.inform.emit(response)
+
+        # Show Shortcut list
+        if event.key.name == '`':
+            self.on_shortcut_list()
+
+    def on_shortcut_list(self):
+        msg = '''<b>Shortcut list in Geometry Editor</b><br>
+<br>
+<b>A:</b>       Add an 'Arc'<br>
+<b>B:</b>       Add a Buffer Geo<br>
+<b>C:</b>       Copy Geo Item<br>
+<b>G:</b>       Grid Snap On/Off<br>
+<b>G:</b>       Paint Tool<br>
+<b>K:</b>       Corner Snap On/Off<br>
+<b>M:</b>       Move Geo Item<br>
+<br>
+<b>N:</b>       Add an 'Polygon'<br>
+<b>O:</b>       Add a 'Circle'<br>
+<b>P:</b>       Add a 'Path'<br>
+<b>R:</b>       Add an 'Rectangle'<br>
+<b>S:</b>       Select Tool Active<br>
+<b>T:</b>       Add Text Geometry<br>
+<br>
+<b>X:</b>       Cut Path<br>
+<br>
+<b>~:</b>       Show Shortcut List<br>
+<br>
+<b>Space:</b>   Rotate selected Geometry<br>
+<b>Enter:</b>   Finish Current Action<br>
+<b>Escape:</b>  Abort Current Action<br>
+<b>Delete:</b>  Delete Obj'''
+
+        helpbox =QtWidgets.QMessageBox()
+        helpbox.setText(msg)
+        helpbox.setWindowTitle("Help")
+        helpbox.setWindowIcon(QtGui.QIcon('share/help.png'))
+        helpbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+        helpbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+        helpbox.exec_()
+
+    def on_canvas_key_release(self, event):
+        self.key = None
+
+    def on_delete_btn(self):
+        self.delete_selected()
+        self.replot()
+
+    def delete_selected(self):
+        tempref = [s for s in self.selected]
+        for shape in tempref:
+            self.delete_shape(shape)
+
+        self.selected = []
+
+    def delete_shape(self, shape):
+
+        if shape in self.utility:
+            self.utility.remove(shape)
+            return
+
+        self.storage.remove(shape)
+
+        if shape in self.selected:
+            self.selected.remove(shape)  # TODO: Check performance
+
+    def get_selected(self):
+        """
+        Returns list of shapes that are selected in the editor.
+
+        :return: List of shapes.
+        """
+        # return [shape for shape in self.shape_buffer if shape["selected"]]
+        return self.selected
+
+    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.
+
+        :param geometry: Geometry to be plotted (Any Shapely.geom kind or list of such)
+        :param color: Shape color
+        :param linewidth: Width of lines in # of pixels.
+        :return: List of plotted elements.
+        """
+        plot_elements = []
+
+        if geometry is None:
+            geometry = self.active_tool.geometry
+
+        try:
+            for geo in geometry:
+                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, color=color, linewidth=linewidth)
+
+            ## Polygon: Descend into exterior and each interior.
+            if type(geometry) == Polygon:
+                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:
+                plot_elements.append(self.shapes.add(shape=geometry, color=color, layer=0,
+                                                     tolerance=self.fcgeometry.drawing_tolerance))
+
+            if type(geometry) == Point:
+                pass
+
+        return plot_elements
+
+    def plot_all(self):
+        """
+        Plots all shapes in the editor.
+
+        :return: None
+        :rtype: None
+        """
+        # self.app.log.debug("plot_all()")
+        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, color=self.app.defaults['global_sel_draw_color'], linewidth=2)
+                continue
+
+            self.plot_shape(geometry=shape.geo, color=self.app.defaults['global_draw_color'])
+
+        for shape in self.utility:
+            self.plot_shape(geometry=shape.geo, linewidth=1)
+            continue
+
+        self.shapes.redraw()
+
+    def replot(self):
+        self.plot_all()
+
+    def on_shape_complete(self):
+        self.app.log.debug("on_shape_complete()")
+
+        # Add shape
+        self.add_shape(self.active_tool.geometry)
+
+        # Remove any utility shapes
+        self.delete_utility_geometry()
+        self.tool_shape.clear(update=True)
+
+        # Replot and reset tool.
+        self.replot()
+        # self.active_tool = type(self.active_tool)(self)
+
+    @staticmethod
+    def make_storage():
+
+        ## Shape storage.
+        storage = FlatCAMRTreeStorage()
+        storage.get_points = DrawToolShape.get_pts
+
+        return storage
+
+    def select_tool(self, toolname):
+        """
+        Selects a drawing tool. Impacts the object and GUI.
+
+        :param toolname: Name of the tool.
+        :return: None
+        """
+        self.tools[toolname]["button"].setChecked(True)
+        self.on_tool_select(toolname)
+
+    def set_selected(self, shape):
+
+        # Remove and add to the end.
+        if shape in self.selected:
+            self.selected.remove(shape)
+
+        self.selected.append(shape)
+
+    def set_unselected(self, shape):
+        if shape in self.selected:
+            self.selected.remove(shape)
+
+    def snap(self, x, y):
+        """
+        Adjusts coordinates to snap settings.
+
+        :param x: Input coordinate X
+        :param y: Input coordinate Y
+        :return: Snapped (x, y)
+        """
+
+        snap_x, snap_y = (x, y)
+        snap_distance = Inf
+
+        ### Object (corner?) snap
+        ### No need for the objects, just the coordinates
+        ### in the index.
+        if self.options["corner_snap"]:
+            try:
+                nearest_pt, shape = self.storage.nearest((x, y))
+
+                nearest_pt_distance = distance((x, y), nearest_pt)
+                if nearest_pt_distance <= self.options["snap_max"]:
+                    snap_distance = nearest_pt_distance
+                    snap_x, snap_y = nearest_pt
+            except (StopIteration, AssertionError):
+                pass
+
+        ### Grid snap
+        if self.options["grid_snap"]:
+            if self.options["global_gridx"] != 0:
+                snap_x_ = round(x / self.options["global_gridx"]) * self.options['global_gridx']
+            else:
+                snap_x_ = x
+
+            # If the Grid_gap_linked on Grid Toolbar is checked then the snap distance on GridY entry will be ignored
+            # and it will use the snap distance from GridX entry
+            if self.app.ui.grid_gap_link_cb.isChecked():
+                if self.options["global_gridx"] != 0:
+                    snap_y_ = round(y / self.options["global_gridx"]) * self.options['global_gridx']
+                else:
+                    snap_y_ = y
+            else:
+                if self.options["global_gridy"] != 0:
+                    snap_y_ = round(y / self.options["global_gridy"]) * self.options['global_gridy']
+                else:
+                    snap_y_ = y
+            nearest_grid_distance = distance((x, y), (snap_x_, snap_y_))
+            if nearest_grid_distance < snap_distance:
+                snap_x, snap_y = (snap_x_, snap_y_)
+
+        return snap_x, snap_y
+
+    def update_fcgeometry(self, fcgeometry):
+        """
+        Transfers the geometry 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:
+        for shape in self.storage.get_objects():
+            fcgeometry.solid_geometry.append(shape.geo)
+
+        # re-enable all the widgets in the Selected Tab that were disabled after entering in Edit Geometry Mode
+        sel_tab_widget_list = self.app.ui.selected_tab.findChildren(QtWidgets.QWidget)
+        for w in sel_tab_widget_list:
+            w.setEnabled(True)
+
+    def update_options(self, obj):
+        if self.paint_tooldia:
+            obj.options['cnctooldia'] = self.paint_tooldia
+            self.paint_tooldia = None
+            return True
+        else:
+            return False
+
+    def union(self):
+        """
+        Makes union of selected polygons. Original polygons
+        are deleted.
+
+        :return: None.
+        """
+
+        results = cascaded_union([t.geo for t in self.get_selected()])
+
+        # Delete originals.
+        for_deletion = [s for s in self.get_selected()]
+        for shape in for_deletion:
+            self.delete_shape(shape)
+
+        # Selected geometry is now gone!
+        self.selected = []
+
+        self.add_shape(DrawToolShape(results))
+
+        self.replot()
+
+    def intersection(self):
+        """
+        Makes intersectino of selected polygons. Original polygons are deleted.
+
+        :return: None
+        """
+
+        shapes = self.get_selected()
+
+        results = shapes[0].geo
+
+        for shape in shapes[1:]:
+            results = results.intersection(shape.geo)
+
+        # Delete originals.
+        for_deletion = [s for s in self.get_selected()]
+        for shape in for_deletion:
+            self.delete_shape(shape)
+
+        # Selected geometry is now gone!
+        self.selected = []
+
+        self.add_shape(DrawToolShape(results))
+
+        self.replot()
+
+    def subtract(self):
+        selected = self.get_selected()
+        try:
+            tools = selected[1:]
+            toolgeo = cascaded_union([shp.geo for shp in tools])
+            result = selected[0].geo.difference(toolgeo)
+
+            self.delete_shape(selected[0])
+            self.add_shape(DrawToolShape(result))
+
+            self.replot()
+        except Exception as e:
+            log.debug(str(e))
+
+    def buffer(self, buf_distance, join_style):
+        selected = self.get_selected()
+
+        if buf_distance < 0:
+            self.app.inform.emit(
+                "[error_notcl]Negative buffer value is not accepted. Use Buffer interior to generate an 'inside' shape")
+
+            # deselect everything
+            self.selected = []
+            self.replot()
+            return
+
+        if len(selected) == 0:
+            self.app.inform.emit("[warning_notcl] Nothing selected for buffering.")
+            return
+
+        if not isinstance(buf_distance, float):
+            self.app.inform.emit("[warning_notcl] Invalid distance for buffering.")
+
+            # deselect everything
+            self.selected = []
+            self.replot()
+            return
+
+        pre_buffer = cascaded_union([t.geo for t in selected])
+        results = pre_buffer.buffer(buf_distance - 1e-10, resolution=32, join_style=join_style)
+        if results.is_empty:
+            self.app.inform.emit("[error_notcl]Failed, the result is empty. Choose a different buffer value.")
+            # deselect everything
+            self.selected = []
+            self.replot()
+            return
+        self.add_shape(DrawToolShape(results))
+
+        self.replot()
+        self.app.inform.emit("[success]Full buffer geometry created.")
+
+    def buffer_int(self, buf_distance, join_style):
+        selected = self.get_selected()
+
+        if buf_distance < 0:
+            self.app.inform.emit(
+                "[error_notcl]Negative buffer value is not accepted. Use Buffer interior to generate an 'inside' shape")
+            # deselect everything
+            self.selected = []
+            self.replot()
+            return
+
+        if len(selected) == 0:
+            self.app.inform.emit("[warning_notcl] Nothing selected for buffering.")
+            return
+
+        if not isinstance(buf_distance, float):
+            self.app.inform.emit("[warning_notcl] Invalid distance for buffering.")
+            # deselect everything
+            self.selected = []
+            self.replot()
+            return
+
+        pre_buffer = cascaded_union([t.geo for t in selected])
+        results = pre_buffer.buffer(-buf_distance + 1e-10, resolution=32, join_style=join_style)
+        if results.is_empty:
+            self.app.inform.emit("[error_notcl]Failed, the result is empty. Choose a smaller buffer value.")
+            # deselect everything
+            self.selected = []
+            self.replot()
+            return
+        if type(results) == MultiPolygon:
+            for poly in results:
+                self.add_shape(DrawToolShape(poly.exterior))
+        else:
+            self.add_shape(DrawToolShape(results.exterior))
+
+        self.replot()
+        self.app.inform.emit("[success]Exterior buffer geometry created.")
+        # selected = self.get_selected()
+        #
+        # if len(selected) == 0:
+        #     self.app.inform.emit("[WARNING] Nothing selected for buffering.")
+        #     return
+        #
+        # if not isinstance(buf_distance, float):
+        #     self.app.inform.emit("[warning] Invalid distance for buffering.")
+        #     return
+        #
+        # pre_buffer = cascaded_union([t.geo for t in selected])
+        # results = pre_buffer.buffer(buf_distance)
+        # if results.is_empty:
+        #     self.app.inform.emit("Failed. Choose a smaller buffer value.")
+        #     return
+        #
+        # int_geo = []
+        # if type(results) == MultiPolygon:
+        #     for poly in results:
+        #         for g in poly.interiors:
+        #             int_geo.append(g)
+        #         res = cascaded_union(int_geo)
+        #         self.add_shape(DrawToolShape(res))
+        # else:
+        #     print(results.interiors)
+        #     for g in results.interiors:
+        #         int_geo.append(g)
+        #     res = cascaded_union(int_geo)
+        #     self.add_shape(DrawToolShape(res))
+        #
+        # self.replot()
+        # self.app.inform.emit("Interior buffer geometry created.")
+
+    def buffer_ext(self, buf_distance, join_style):
+        selected = self.get_selected()
+
+        if buf_distance < 0:
+            self.app.inform.emit("[error_notcl]Negative buffer value is not accepted. "
+                                 "Use Buffer interior to generate an 'inside' shape")
+            # deselect everything
+            self.selected = []
+            self.replot()
+            return
+
+        if len(selected) == 0:
+            self.app.inform.emit("[warning_notcl] Nothing selected for buffering.")
+            return
+
+        if not isinstance(buf_distance, float):
+            self.app.inform.emit("[warning_notcl] Invalid distance for buffering.")
+            # deselect everything
+            self.selected = []
+            self.replot()
+            return
+
+        pre_buffer = cascaded_union([t.geo for t in selected])
+        results = pre_buffer.buffer(buf_distance - 1e-10, resolution=32, join_style=join_style)
+        if results.is_empty:
+            self.app.inform.emit("[error_notcl]Failed, the result is empty. Choose a different buffer value.")
+            # deselect everything
+            self.selected = []
+            self.replot()
+            return
+        if type(results) == MultiPolygon:
+            for poly in results:
+                self.add_shape(DrawToolShape(poly.exterior))
+        else:
+            self.add_shape(DrawToolShape(results.exterior))
+
+        self.replot()
+        self.app.inform.emit("[success]Exterior buffer geometry created.")
+
+    # def paint(self, tooldia, overlap, margin, method):
+    #     selected = self.get_selected()
+    #
+    #     if len(selected) == 0:
+    #         self.app.inform.emit("[warning] Nothing selected for painting.")
+    #         return
+    #
+    #     for param in [tooldia, overlap, margin]:
+    #         if not isinstance(param, float):
+    #             param_name = [k for k, v in locals().items() if v is param][0]
+    #             self.app.inform.emit("[warning] Invalid value for {}".format(param))
+    #
+    #     # Todo: Check for valid method.
+    #
+    #     # Todo: This is the 3rd implementation on painting polys... try to consolidate
+    #
+    #     results = []
+    #
+    #     def recurse(geo):
+    #         try:
+    #             for subg in geo:
+    #                 for subsubg in recurse(subg):
+    #                     yield subsubg
+    #         except TypeError:
+    #             if isinstance(geo, LinearRing):
+    #                 yield geo
+    #
+    #         raise StopIteration
+    #
+    #     for geo in selected:
+    #         print(type(geo.geo))
+    #
+    #         local_results = []
+    #         for poly in recurse(geo.geo):
+    #             if method == "seed":
+    #                 # Type(cp) == FlatCAMRTreeStorage | None
+    #                 cp = Geometry.clear_polygon2(poly.buffer(-margin),
+    #                                              tooldia, overlap=overlap)
+    #
+    #             else:
+    #                 # Type(cp) == FlatCAMRTreeStorage | None
+    #                 cp = Geometry.clear_polygon(poly.buffer(-margin),
+    #                                             tooldia, overlap=overlap)
+    #
+    #             if cp is not None:
+    #                 local_results += list(cp.get_objects())
+    #
+    #             results.append(cascaded_union(local_results))
+    #
+    #     # This is a dirty patch:
+    #     for r in results:
+    #         self.add_shape(DrawToolShape(r))
+    #
+    #     self.replot()
+
+    def paint(self, tooldia, overlap, margin, connect, contour, method):
+
+        self.paint_tooldia = tooldia
+
+        selected = self.get_selected()
+
+        if len(selected) == 0:
+            self.app.inform.emit("[warning_notcl]Nothing selected for painting.")
+            return
+
+        for param in [tooldia, overlap, margin]:
+            if not isinstance(param, float):
+                param_name = [k for k, v in locals().items() if v is param][0]
+                self.app.inform.emit("[warning] Invalid value for {}".format(param))
+
+        results = []
+
+        if tooldia >= overlap:
+            self.app.inform.emit(
+                "[error_notcl] Could not do Paint. Overlap value has to be less than Tool Dia value.")
+            return
+
+        def recurse(geometry, reset=True):
+            """
+            Creates a list of non-iterable linear geometry objects.
+            Results are placed in self.flat_geometry
+
+            :param geometry: Shapely type or list or list of list of such.
+            :param reset: Clears the contents of self.flat_geometry.
+            """
+
+            if geometry is None:
+                return
+
+            if reset:
+                self.flat_geo = []
+
+            ## If iterable, expand recursively.
+            try:
+                for geo in geometry:
+                    if geo is not None:
+                        recurse(geometry=geo, reset=False)
+
+            ## Not iterable, do the actual indexing and add.
+            except TypeError:
+                self.flat_geo.append(geometry)
+
+            return self.flat_geo
+
+        for geo in selected:
+
+            local_results = []
+            for geo_obj in recurse(geo.geo):
+                try:
+                    if type(geo_obj) == Polygon:
+                        poly_buf = geo_obj.buffer(-margin)
+                    else:
+                        poly_buf = Polygon(geo_obj).buffer(-margin)
+
+                    if method == "seed":
+                        cp = Geometry.clear_polygon2(poly_buf,
+                                                 tooldia, self.app.defaults["geometry_circle_steps"],
+                                                 overlap=overlap, contour=contour, connect=connect)
+                    elif method == "lines":
+                        cp = Geometry.clear_polygon3(poly_buf,
+                                                 tooldia, self.app.defaults["geometry_circle_steps"],
+                                                 overlap=overlap, contour=contour, connect=connect)
+
+                    else:
+                        cp = Geometry.clear_polygon(poly_buf,
+                                                tooldia, self.app.defaults["geometry_circle_steps"],
+                                                overlap=overlap, contour=contour, connect=connect)
+
+                    if cp is not None:
+                        local_results += list(cp.get_objects())
+                except Exception as e:
+                    log.debug("Could not Paint the polygons. %s" % str(e))
+                    self.app.inform.emit(
+                        "[error] Could not do Paint. Try a different combination of parameters. "
+                        "Or a different method of Paint\n%s" % str(e))
+                    return
+
+                # add the result to the results list
+                results.append(cascaded_union(local_results))
+
+        # This is a dirty patch:
+        for r in results:
+            self.add_shape(DrawToolShape(r))
+        self.app.inform.emit(
+            "[success] Paint done.")
+        self.replot()
+
+
+class FlatCAMExcEditor(QtCore.QObject):
+
+    draw_shape_idx = -1
+
+    def __init__(self, app):
+        assert isinstance(app, FlatCAMApp.App), \
+            "Expected the app to be a FlatCAMApp.App, got %s" % type(app)
+
+        super(FlatCAMExcEditor, self).__init__()
+
+        self.app = app
+        self.canvas = self.app.plotcanvas
+
+        self.exc_edit_widget = QtWidgets.QWidget()
+        layout = QtWidgets.QVBoxLayout()
+        self.exc_edit_widget.setLayout(layout)
+
+        ## Page Title box (spacing between children)
+        self.title_box = QtWidgets.QHBoxLayout()
+        layout.addLayout(self.title_box)
+
+        ## Page Title icon
+        pixmap = QtGui.QPixmap('share/flatcam_icon32.png')
+        self.icon = QtWidgets.QLabel()
+        self.icon.setPixmap(pixmap)
+        self.title_box.addWidget(self.icon, stretch=0)
+
+        ## Title label
+        self.title_label = QtWidgets.QLabel("<font size=5><b>" + 'Excellon Editor' + "</b></font>")
+        self.title_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.title_box.addWidget(self.title_label, stretch=1)
+
+        ## Object name
+        self.name_box = QtWidgets.QHBoxLayout()
+        layout.addLayout(self.name_box)
+        name_label = QtWidgets.QLabel("Name:")
+        self.name_box.addWidget(name_label)
+        self.name_entry = FCEntry()
+        self.name_box.addWidget(self.name_entry)
+
+        ## Box box for custom widgets
+        # This gets populated in offspring implementations.
+        self.custom_box = QtWidgets.QVBoxLayout()
+        layout.addLayout(self.custom_box)
+
+        # add a frame and inside add a vertical box layout. Inside this vbox layout I add all the Drills widgets
+        # this way I can hide/show the frame
+        self.drills_frame = QtWidgets.QFrame()
+        self.drills_frame.setContentsMargins(0, 0, 0, 0)
+        self.custom_box.addWidget(self.drills_frame)
+        self.tools_box = QtWidgets.QVBoxLayout()
+        self.tools_box.setContentsMargins(0, 0, 0, 0)
+        self.drills_frame.setLayout(self.tools_box)
+
+        #### Tools Drills ####
+        self.tools_table_label = QtWidgets.QLabel('<b>Tools Table</b>')
+        self.tools_table_label.setToolTip(
+            "Tools in this Excellon object\n"
+            "when are used for drilling."
+        )
+        self.tools_box.addWidget(self.tools_table_label)
+
+        self.tools_table_exc = FCTable()
+        self.tools_box.addWidget(self.tools_table_exc)
+
+        self.tools_table_exc.setColumnCount(4)
+        self.tools_table_exc.setHorizontalHeaderLabels(['#', 'Diameter', 'D', 'S'])
+        self.tools_table_exc.setSortingEnabled(False)
+        self.tools_table_exc.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+
+        self.empty_label = QtWidgets.QLabel('')
+        self.tools_box.addWidget(self.empty_label)
+
+        #### Add a new Tool ####
+        self.addtool_label = QtWidgets.QLabel('<b>Add/Delete Tool</b>')
+        self.addtool_label.setToolTip(
+            "Add/Delete a tool to the tool list\n"
+            "for this Excellon object."
+        )
+        self.tools_box.addWidget(self.addtool_label)
+
+        grid1 = QtWidgets.QGridLayout()
+        self.tools_box.addLayout(grid1)
+
+        addtool_entry_lbl = QtWidgets.QLabel('Tool Dia:')
+        addtool_entry_lbl.setToolTip(
+            "Diameter for the new tool"
+        )
+        grid1.addWidget(addtool_entry_lbl, 0, 0)
+
+        hlay = QtWidgets.QHBoxLayout()
+        self.addtool_entry = LengthEntry()
+        hlay.addWidget(self.addtool_entry)
+
+        self.addtool_btn = QtWidgets.QPushButton('Add Tool')
+        self.addtool_btn.setToolTip(
+            "Add a new tool to the tool list\n"
+            "with the diameter specified above."
+        )
+        self.addtool_btn.setFixedWidth(80)
+        hlay.addWidget(self.addtool_btn)
+        grid1.addLayout(hlay, 0, 1)
+
+        grid2 = QtWidgets.QGridLayout()
+        self.tools_box.addLayout(grid2)
+
+        self.deltool_btn = QtWidgets.QPushButton('Delete Tool')
+        self.deltool_btn.setToolTip(
+            "Delete a tool in the tool list\n"
+            "by selecting a row in the tool table."
+        )
+        grid2.addWidget(self.deltool_btn, 0, 1)
+
+        # add a frame and inside add a vertical box layout. Inside this vbox layout I add all the Drills widgets
+        # this way I can hide/show the frame
+        self.resize_frame = QtWidgets.QFrame()
+        self.resize_frame.setContentsMargins(0, 0, 0, 0)
+        self.tools_box.addWidget(self.resize_frame)
+        self.resize_box = QtWidgets.QVBoxLayout()
+        self.resize_box.setContentsMargins(0, 0, 0, 0)
+        self.resize_frame.setLayout(self.resize_box)
+
+        #### Resize a  drill ####
+        self.emptyresize_label = QtWidgets.QLabel('')
+        self.resize_box.addWidget(self.emptyresize_label)
+
+        self.drillresize_label = QtWidgets.QLabel('<b>Resize Drill(s)</b>')
+        self.drillresize_label.setToolTip(
+            "Resize a drill or a selection of drills."
+        )
+        self.resize_box.addWidget(self.drillresize_label)
+
+        grid3 = QtWidgets.QGridLayout()
+        self.resize_box.addLayout(grid3)
+
+        res_entry_lbl = QtWidgets.QLabel('Resize Dia:')
+        res_entry_lbl.setToolTip(
+            "Diameter to resize to."
+        )
+        grid3.addWidget(addtool_entry_lbl, 0, 0)
+
+        hlay2 = QtWidgets.QHBoxLayout()
+        self.resdrill_entry = LengthEntry()
+        hlay2.addWidget(self.resdrill_entry)
+
+        self.resize_btn = QtWidgets.QPushButton('Resize')
+        self.resize_btn.setToolTip(
+            "Resize drill(s)"
+        )
+        self.resize_btn.setFixedWidth(80)
+        hlay2.addWidget(self.resize_btn)
+        grid3.addLayout(hlay2, 0, 1)
+
+        self.resize_frame.hide()
+
+        # add a frame and inside add a vertical box layout. Inside this vbox layout I add
+        # all the add drill array  widgets
+        # this way I can hide/show the frame
+        self.array_frame = QtWidgets.QFrame()
+        self.array_frame.setContentsMargins(0, 0, 0, 0)
+        self.tools_box.addWidget(self.array_frame)
+        self.array_box = QtWidgets.QVBoxLayout()
+        self.array_box.setContentsMargins(0, 0, 0, 0)
+        self.array_frame.setLayout(self.array_box)
+
+        #### Add DRILL Array ####
+        self.emptyarray_label = QtWidgets.QLabel('')
+        self.array_box.addWidget(self.emptyarray_label)
+
+        self.drillarray_label = QtWidgets.QLabel('<b>Add Drill Array</b>')
+        self.drillarray_label.setToolTip(
+            "Add an array of drills (linear or circular array)"
+        )
+        self.array_box.addWidget(self.drillarray_label)
+
+        self.array_type_combo = FCComboBox()
+        self.array_type_combo.setToolTip(
+            "Select the type of drills array to create.\n"
+            "It can be Linear X(Y) or Circular"
+        )
+        self.array_type_combo.addItem("Linear")
+        self.array_type_combo.addItem("Circular")
+
+        self.array_box.addWidget(self.array_type_combo)
+
+        self.array_form = QtWidgets.QFormLayout()
+        self.array_box.addLayout(self.array_form)
+
+        self.drill_array_size_label = QtWidgets.QLabel('Nr of drills:')
+        self.drill_array_size_label.setToolTip(
+            "Specify how many drills to be in the array."
+        )
+        self.drill_array_size_label.setFixedWidth(100)
+
+        self.drill_array_size_entry = LengthEntry()
+        self.array_form.addRow(self.drill_array_size_label, self.drill_array_size_entry)
+
+        self.array_linear_frame = QtWidgets.QFrame()
+        self.array_linear_frame.setContentsMargins(0, 0, 0, 0)
+        self.array_box.addWidget(self.array_linear_frame)
+        self.linear_box = QtWidgets.QVBoxLayout()
+        self.linear_box.setContentsMargins(0, 0, 0, 0)
+        self.array_linear_frame.setLayout(self.linear_box)
+
+        self.linear_form = QtWidgets.QFormLayout()
+        self.linear_box.addLayout(self.linear_form)
+
+        self.drill_pitch_label = QtWidgets.QLabel('Pitch:')
+        self.drill_pitch_label.setToolTip(
+            "Pitch = Distance between elements of the array."
+        )
+        self.drill_pitch_label.setFixedWidth(100)
+
+        self.drill_pitch_entry = LengthEntry()
+        self.linear_form.addRow(self.drill_pitch_label, self.drill_pitch_entry)
+
+        self.drill_axis_label = QtWidgets.QLabel('Axis:')
+        self.drill_axis_label.setToolTip(
+            "Axis on which the linear array is oriented: 'X' or 'Y'."
+        )
+        self.drill_axis_label.setFixedWidth(100)
+
+        self.drill_axis_radio = RadioSet([{'label': 'X', 'value': 'X'},
+                                          {'label': 'Y', 'value': 'Y'}])
+        self.drill_axis_radio.set_value('X')
+        self.linear_form.addRow(self.drill_axis_label, self.drill_axis_radio)
+
+        self.array_circular_frame = QtWidgets.QFrame()
+        self.array_circular_frame.setContentsMargins(0, 0, 0, 0)
+        self.array_box.addWidget(self.array_circular_frame)
+        self.circular_box = QtWidgets.QVBoxLayout()
+        self.circular_box.setContentsMargins(0, 0, 0, 0)
+        self.array_circular_frame.setLayout(self.circular_box)
+
+        self.drill_angle_label = QtWidgets.QLabel('Angle:')
+        self.drill_angle_label.setToolTip(
+            "Angle at which each element in circular array is placed."
+        )
+        self.drill_angle_label.setFixedWidth(100)
+
+        self.circular_form = QtWidgets.QFormLayout()
+        self.circular_box.addLayout(self.circular_form)
+
+        self.drill_angle_entry = LengthEntry()
+        self.circular_form.addRow(self.drill_angle_label, self.drill_angle_entry)
+
+        self.drill_direction_label = QtWidgets.QLabel('Direction:')
+        self.drill_direction_label.setToolTip(
+            "Direction for circular array."
+            "Can be CW = clockwise or CCW = counter clockwise."
+        )
+        self.drill_direction_label.setFixedWidth(100)
+
+        self.drill_direction_radio = RadioSet([{'label': 'CW', 'value': 'CW'},
+                                          {'label': 'CCW.', 'value': 'CCW'}])
+        self.drill_direction_radio.set_value('CW')
+        self.circular_form.addRow(self.drill_direction_label, self.drill_direction_radio)
+
+        self.array_circular_frame.hide()
+        self.array_frame.hide()
+        self.tools_box.addStretch()
+
+        ## Toolbar events and properties
+        self.tools_exc = {
+            "select": {"button": self.app.ui.select_drill_btn,
+                       "constructor": FCDrillSelect},
+            "add": {"button": self.app.ui.add_drill_btn,
+                    "constructor": FCDrillAdd},
+            "add_array": {"button": self.app.ui.add_drill_array_btn,
+                          "constructor": FCDrillArray},
+            "resize": {"button": self.app.ui.resize_drill_btn,
+                       "constructor": FCDrillResize},
+            "copy": {"button": self.app.ui.copy_drill_btn,
+                     "constructor": FCDrillCopy},
+            "move": {"button": self.app.ui.move_drill_btn,
+                     "constructor": FCDrillMove},
+        }
+
+        ### Data
+        self.active_tool = None
+
+        self.storage_dict = {}
+        self.current_storage = []
+
+        # build the data from the Excellon point into a dictionary
+        #  {tool_dia: [geometry_in_points]}
+        self.points_edit = {}
+        self.sorted_diameters =[]
+
+        self.new_drills = []
+        self.new_tools = {}
+        self.new_slots = {}
+
+        # dictionary to store the tool_row and diameters in Tool_table
+        # it will be updated everytime self.build_ui() is called
+        self.olddia_newdia = {}
+
+        self.tool2tooldia = {}
+
+        # this will store the value for the last selected tool, for use after clicking on canvas when the selection
+        # is cleared but as a side effect also the selected tool is cleared
+        self.last_tool_selected = None
+        self.utility = []
+
+        # this will flag if the Editor "tools" are launched from key shortcuts (True) or from menu toolbar (False)
+        self.launched_from_shortcuts = False
+
+        self.app.ui.delete_drill_btn.triggered.connect(self.on_delete_btn)
+        self.name_entry.returnPressed.connect(self.on_name_activate)
+        self.addtool_btn.clicked.connect(self.on_tool_add)
+        # self.addtool_entry.editingFinished.connect(self.on_tool_add)
+        self.deltool_btn.clicked.connect(self.on_tool_delete)
+        self.tools_table_exc.selectionModel().currentChanged.connect(self.on_row_selected)
+        self.array_type_combo.currentIndexChanged.connect(self.on_array_type_combo)
+
+        self.drill_array_size_entry.set_value(5)
+        self.drill_pitch_entry.set_value(2.54)
+        self.drill_angle_entry.set_value(12)
+        self.drill_direction_radio.set_value('CW')
+        self.drill_axis_radio.set_value('X')
+        self.exc_obj = None
+
+        # VisPy Visuals
+        self.shapes = self.app.plotcanvas.new_shape_collection(layers=1)
+        self.tool_shape = self.app.plotcanvas.new_shape_collection(layers=1)
+        self.app.pool_recreated.connect(self.pool_recreated)
+
+        # Remove from scene
+        self.shapes.enabled = False
+        self.tool_shape.enabled = False
+
+        ## List of selected shapes.
+        self.selected = []
+
+        self.move_timer = QtCore.QTimer()
+        self.move_timer.setSingleShot(True)
+
+        ## Current application units in Upper Case
+        self.units = self.app.general_options_form.general_group.units_radio.get_value().upper()
+
+        self.key = None  # Currently pressed key
+        self.modifiers = None
+        self.x = None  # Current mouse cursor pos
+        self.y = None
+        # Current snapped mouse pos
+        self.snap_x = None
+        self.snap_y = None
+        self.pos = None
+
+        def make_callback(thetool):
+            def f():
+                self.on_tool_select(thetool)
+            return f
+
+        for tool in self.tools_exc:
+            self.tools_exc[tool]["button"].triggered.connect(make_callback(tool))  # Events
+            self.tools_exc[tool]["button"].setCheckable(True)  # Checkable
+
+        self.options = {
+            "global_gridx": 0.1,
+            "global_gridy": 0.1,
+            "snap_max": 0.05,
+            "grid_snap": True,
+            "corner_snap": False,
+            "grid_gap_link": True
+        }
+        self.app.options_read_form()
+
+        for option in self.options:
+            if option in self.app.options:
+                self.options[option] = self.app.options[option]
+
+        self.rtree_exc_index = rtindex.Index()
+        # flag to show if the object was modified
+        self.is_modified = False
+
+        self.edited_obj_name = ""
+
+        # variable to store the total amount of drills per job
+        self.tot_drill_cnt = 0
+        self.tool_row = 0
+
+        # variable to store the total amount of slots per job
+        self.tot_slot_cnt = 0
+        self.tool_row_slots = 0
+
+        self.tool_row = 0
+
+        # store the status of the editor so the Delete at object level will not work until the edit is finished
+        self.editor_active = False
+
+        def entry2option(option, entry):
+            self.options[option] = float(entry.text())
+
+        # store the status of the editor so the Delete at object level will not work until the edit is finished
+        self.editor_active = False
+
+    def pool_recreated(self, pool):
+        self.shapes.pool = pool
+        self.tool_shape.pool = pool
+
+    @staticmethod
+    def make_storage():
+
+        ## Shape storage.
+        storage = FlatCAMRTreeStorage()
+        storage.get_points = DrawToolShape.get_pts
+
+        return storage
+
+    def set_ui(self):
+        # updated units
+        self.units = self.app.general_options_form.general_group.units_radio.get_value().upper()
+
+        self.olddia_newdia.clear()
+        self.tool2tooldia.clear()
+
+        # build the self.points_edit dict {dimaters: [point_list]}
+        for drill in self.exc_obj.drills:
+            if drill['tool'] in self.exc_obj.tools:
+                if self.units == 'IN':
+                    tool_dia = float('%.3f' % self.exc_obj.tools[drill['tool']]['C'])
+                else:
+                    tool_dia = float('%.2f' % self.exc_obj.tools[drill['tool']]['C'])
+
+                try:
+                    self.points_edit[tool_dia].append(drill['point'])
+                except KeyError:
+                    self.points_edit[tool_dia] = [drill['point']]
+        # update the olddia_newdia dict to make sure we have an updated state of the tool_table
+        for key in self.points_edit:
+            self.olddia_newdia[key] = key
+
+        sort_temp = []
+        for diam in self.olddia_newdia:
+            sort_temp.append(float(diam))
+        self.sorted_diameters = sorted(sort_temp)
+
+        # populate self.intial_table_rows dict with the tool number as keys and tool diameters as values
+        for i in range(len(self.sorted_diameters)):
+            tt_dia = self.sorted_diameters[i]
+            self.tool2tooldia[i + 1] = tt_dia
+
+    def build_ui(self):
+
+        try:
+            # if connected, disconnect the signal from the slot on item_changed as it creates issues
+            self.tools_table_exc.itemChanged.disconnect()
+        except:
+            pass
+
+        # updated units
+        self.units = self.app.general_options_form.general_group.units_radio.get_value().upper()
+
+        # make a new name for the new Excellon object (the one with edited content)
+        self.edited_obj_name = self.exc_obj.options['name']
+        self.name_entry.set_value(self.edited_obj_name)
+
+        if self.units == "IN":
+            self.addtool_entry.set_value(0.039)
+        else:
+            self.addtool_entry.set_value(1)
+
+        sort_temp = []
+
+        for diam in self.olddia_newdia:
+            sort_temp.append(float(diam))
+        self.sorted_diameters = sorted(sort_temp)
+
+        # here, self.sorted_diameters will hold in a oblique way, the number of tools
+        n = len(self.sorted_diameters)
+        # we have (n+2) rows because there are 'n' tools, each a row, plus the last 2 rows for totals.
+        self.tools_table_exc.setRowCount(n + 2)
+
+        self.tot_drill_cnt = 0
+        self.tot_slot_cnt = 0
+
+        self.tool_row = 0
+        # this variable will serve as the real tool_number
+        tool_id = 0
+
+        for tool_no in self.sorted_diameters:
+            tool_id += 1
+            drill_cnt = 0  # variable to store the nr of drills per tool
+            slot_cnt = 0  # variable to store the nr of slots per tool
+
+            # Find no of drills for the current tool
+            for tool_dia in self.points_edit:
+                if float(tool_dia) == tool_no:
+                    drill_cnt = len(self.points_edit[tool_dia])
+
+            self.tot_drill_cnt += drill_cnt
+
+            try:
+                # Find no of slots for the current tool
+                for slot in self.slots:
+                    if slot['tool'] == tool_no:
+                        slot_cnt += 1
+
+                self.tot_slot_cnt += slot_cnt
+            except AttributeError:
+                # log.debug("No slots in the Excellon file")
+                # slot editing not implemented
+                pass
+
+            id = QtWidgets.QTableWidgetItem('%d' % int(tool_id))
+            id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.tools_table_exc.setItem(self.tool_row, 0, id)  # Tool name/id
+
+            # Make sure that the drill diameter when in MM is with no more than 2 decimals
+            # There are no drill bits in MM with more than 3 decimals diameter
+            # For INCH the decimals should be no more than 3. There are no drills under 10mils
+            if self.units == 'MM':
+                dia = QtWidgets.QTableWidgetItem('%.2f' % self.olddia_newdia[tool_no])
+            else:
+                dia = QtWidgets.QTableWidgetItem('%.3f' % self.olddia_newdia[tool_no])
+
+            dia.setFlags(QtCore.Qt.ItemIsEnabled)
+
+            drill_count = QtWidgets.QTableWidgetItem('%d' % drill_cnt)
+            drill_count.setFlags(QtCore.Qt.ItemIsEnabled)
+
+            # if the slot number is zero is better to not clutter the GUI with zero's so we print a space
+            if slot_cnt > 0:
+                slot_count = QtWidgets.QTableWidgetItem('%d' % slot_cnt)
+            else:
+                slot_count = QtWidgets.QTableWidgetItem('')
+            slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
+
+            self.tools_table_exc.setItem(self.tool_row, 1, dia)  # Diameter
+            self.tools_table_exc.setItem(self.tool_row, 2, drill_count)  # Number of drills per tool
+            self.tools_table_exc.setItem(self.tool_row, 3, slot_count)  # Number of drills per tool
+            self.tool_row += 1
+
+        # make the diameter column editable
+        for row in range(self.tool_row):
+            self.tools_table_exc.item(row, 1).setFlags(
+                QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.tools_table_exc.item(row, 2).setForeground(QtGui.QColor(0, 0, 0))
+            self.tools_table_exc.item(row, 3).setForeground(QtGui.QColor(0, 0, 0))
+
+        # add a last row with the Total number of drills
+        # HACK: made the text on this cell '9999' such it will always be the one before last when sorting
+        # it will have to have the foreground color (font color) white
+        empty = QtWidgets.QTableWidgetItem('9998')
+        empty.setForeground(QtGui.QColor(255, 255, 255))
+
+        empty.setFlags(empty.flags() ^ QtCore.Qt.ItemIsEnabled)
+        empty_b = QtWidgets.QTableWidgetItem('')
+        empty_b.setFlags(empty_b.flags() ^ QtCore.Qt.ItemIsEnabled)
+
+        label_tot_drill_count = QtWidgets.QTableWidgetItem('Total Drills')
+        tot_drill_count = QtWidgets.QTableWidgetItem('%d' % self.tot_drill_cnt)
+
+        label_tot_drill_count.setFlags(label_tot_drill_count.flags() ^ QtCore.Qt.ItemIsEnabled)
+        tot_drill_count.setFlags(tot_drill_count.flags() ^ QtCore.Qt.ItemIsEnabled)
+
+        self.tools_table_exc.setItem(self.tool_row, 0, empty)
+        self.tools_table_exc.setItem(self.tool_row, 1, label_tot_drill_count)
+        self.tools_table_exc.setItem(self.tool_row, 2, tot_drill_count)  # Total number of drills
+        self.tools_table_exc.setItem(self.tool_row, 3, empty_b)
+
+        font = QtGui.QFont()
+        font.setBold(True)
+        font.setWeight(75)
+
+        for k in [1, 2]:
+            self.tools_table_exc.item(self.tool_row, k).setForeground(QtGui.QColor(127, 0, 255))
+            self.tools_table_exc.item(self.tool_row, k).setFont(font)
+
+        self.tool_row += 1
+
+        # add a last row with the Total number of slots
+        # HACK: made the text on this cell '9999' such it will always be the last when sorting
+        # it will have to have the foreground color (font color) white
+        empty_2 = QtWidgets.QTableWidgetItem('9999')
+        empty_2.setForeground(QtGui.QColor(255, 255, 255))
+
+        empty_2.setFlags(empty_2.flags() ^ QtCore.Qt.ItemIsEnabled)
+
+        empty_3 = QtWidgets.QTableWidgetItem('')
+        empty_3.setFlags(empty_3.flags() ^ QtCore.Qt.ItemIsEnabled)
+
+        label_tot_slot_count = QtWidgets.QTableWidgetItem('Total Slots')
+        tot_slot_count = QtWidgets.QTableWidgetItem('%d' % self.tot_slot_cnt)
+        label_tot_slot_count.setFlags(label_tot_slot_count.flags() ^ QtCore.Qt.ItemIsEnabled)
+        tot_slot_count.setFlags(tot_slot_count.flags() ^ QtCore.Qt.ItemIsEnabled)
+
+        self.tools_table_exc.setItem(self.tool_row, 0, empty_2)
+        self.tools_table_exc.setItem(self.tool_row, 1, label_tot_slot_count)
+        self.tools_table_exc.setItem(self.tool_row, 2, empty_3)
+        self.tools_table_exc.setItem(self.tool_row, 3, tot_slot_count)  # Total number of slots
+
+        for kl in [1, 2, 3]:
+            self.tools_table_exc.item(self.tool_row, kl).setFont(font)
+            self.tools_table_exc.item(self.tool_row, kl).setForeground(QtGui.QColor(0, 70, 255))
+
+
+        # all the tools are selected by default
+        self.tools_table_exc.selectColumn(0)
+        #
+        self.tools_table_exc.resizeColumnsToContents()
+        self.tools_table_exc.resizeRowsToContents()
+
+        vertical_header = self.tools_table_exc.verticalHeader()
+        # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
+        vertical_header.hide()
+        self.tools_table_exc.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header = self.tools_table_exc.horizontalHeader()
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+        horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
+        # horizontal_header.setStretchLastSection(True)
+
+        self.tools_table_exc.setSortingEnabled(True)
+        # sort by tool diameter
+        self.tools_table_exc.sortItems(1)
+
+        # After sorting, to display also the number of drills in the right row we need to update self.initial_rows dict
+        # with the new order. Of course the last 2 rows in the tool table are just for display therefore we don't
+        # use them
+        self.tool2tooldia.clear()
+        for row in range(self.tools_table_exc.rowCount() - 2):
+            tool = int(self.tools_table_exc.item(row, 0).text())
+            diameter = float(self.tools_table_exc.item(row, 1).text())
+            self.tool2tooldia[tool] = diameter
+
+        self.tools_table_exc.setMinimumHeight(self.tools_table_exc.getHeight())
+        self.tools_table_exc.setMaximumHeight(self.tools_table_exc.getHeight())
+
+        # make sure no rows are selected so the user have to click the correct row, meaning selecting the correct tool
+        self.tools_table_exc.clearSelection()
+
+        # Remove anything else in the GUI Selected Tab
+        self.app.ui.selected_scroll_area.takeWidget()
+        # Put ourself in the GUI Selected Tab
+        self.app.ui.selected_scroll_area.setWidget(self.exc_edit_widget)
+        # Switch notebook to Selected page
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
+
+        # we reactivate the signals after the after the tool adding as we don't need to see the tool been populated
+        self.tools_table_exc.itemChanged.connect(self.on_tool_edit)
+
+    def on_tool_add(self):
+        self.is_modified = True
+        tool_dia = float(self.addtool_entry.get_value())
+
+        if tool_dia not in self.olddia_newdia:
+            storage_elem = FlatCAMGeoEditor.make_storage()
+            self.storage_dict[tool_dia] = storage_elem
+
+            # self.olddia_newdia dict keeps the evidence on current tools diameters as keys and gets updated on values
+            # each time a tool diameter is edited or added
+            self.olddia_newdia[tool_dia] = tool_dia
+        else:
+            self.app.inform.emit("[warning_notcl]Tool already in the original or actual tool list.\n"
+                                 "Save and reedit Excellon if you need to add this tool. ")
+            return
+
+        # since we add a new tool, we update also the initial state of the tool_table through it's dictionary
+        # we add a new entry in the tool2tooldia dict
+        self.tool2tooldia[len(self.olddia_newdia)] = tool_dia
+
+        self.app.inform.emit("[success]Added new tool with dia: %s %s" % (str(tool_dia), str(self.units)))
+
+        self.build_ui()
+
+        # make a quick sort through the tool2tooldia dict so we find which row to select
+        row_to_be_selected = None
+        for key in sorted(self.tool2tooldia):
+            if self.tool2tooldia[key] == tool_dia:
+                row_to_be_selected = int(key) - 1
+                break
+
+        self.tools_table_exc.selectRow(row_to_be_selected)
+
+    def on_tool_delete(self, dia=None):
+        self.is_modified = True
+        deleted_tool_dia_list = []
+
+        try:
+            if dia is None or dia is False:
+                # deleted_tool_dia = float(self.tools_table_exc.item(self.tools_table_exc.currentRow(), 1).text())
+                for index in self.tools_table_exc.selectionModel().selectedRows():
+                    row = index.row()
+                    deleted_tool_dia_list.append(float(self.tools_table_exc.item(row, 1).text()))
+            else:
+                if isinstance(dia, list):
+                    for dd in dia:
+                        deleted_tool_dia_list.append(float('%.4f' % dd))
+                else:
+                    deleted_tool_dia_list.append(float('%.4f' % dia))
+        except:
+            self.app.inform.emit("[warning_notcl]Select a tool in Tool Table")
+            return
+
+        for deleted_tool_dia in deleted_tool_dia_list:
+
+            # delete the storage used for that tool
+            storage_elem = FlatCAMGeoEditor.make_storage()
+            self.storage_dict[deleted_tool_dia] = storage_elem
+            self.storage_dict.pop(deleted_tool_dia, None)
+
+            # I've added this flag_del variable because dictionary don't like
+            # having keys deleted while iterating through them
+            flag_del = []
+            # self.points_edit.pop(deleted_tool_dia, None)
+            for deleted_tool in self.tool2tooldia:
+                if self.tool2tooldia[deleted_tool] == deleted_tool_dia:
+                    flag_del.append(deleted_tool)
+
+            if flag_del:
+                for tool_to_be_deleted in flag_del:
+                    self.tool2tooldia.pop(tool_to_be_deleted, None)
+                    # delete also the drills from points_edit dict just in case we add the tool again, we don't want to show the
+                    # number of drills from before was deleter
+                    self.points_edit[deleted_tool_dia] = []
+                flag_del = []
+
+            self.olddia_newdia.pop(deleted_tool_dia, None)
+
+            self.app.inform.emit("[success]Deleted tool with dia: %s %s" % (str(deleted_tool_dia), str(self.units)))
+
+        self.replot()
+        # self.app.inform.emit("Could not delete selected tool")
+
+        self.build_ui()
+
+    def on_tool_edit(self):
+        # if connected, disconnect the signal from the slot on item_changed as it creates issues
+        self.tools_table_exc.itemChanged.disconnect()
+        # self.tools_table_exc.selectionModel().currentChanged.disconnect()
+
+        self.is_modified = True
+        geometry = []
+        current_table_dia_edited = None
+
+        if self.tools_table_exc.currentItem() is not None:
+            current_table_dia_edited = float(self.tools_table_exc.currentItem().text())
+
+        row_of_item_changed = self.tools_table_exc.currentRow()
+
+        # rows start with 0, tools start with 1 so we adjust the value by 1
+        key_in_tool2tooldia = row_of_item_changed + 1
+
+        dia_changed = self.tool2tooldia[key_in_tool2tooldia]
+
+        # tool diameter is not used so we create a new tool with the desired diameter
+        if current_table_dia_edited not in self.olddia_newdia.values():
+            # update the dict that holds as keys our initial diameters and as values the edited diameters
+            self.olddia_newdia[dia_changed] = current_table_dia_edited
+            # update the dict that holds tool_no as key and tool_dia as value
+            self.tool2tooldia[key_in_tool2tooldia] = current_table_dia_edited
+            self.replot()
+        else:
+            # tool diameter is already in use so we move the drills from the prior tool to the new tool
+            factor = current_table_dia_edited / dia_changed
+            for shape in self.storage_dict[dia_changed].get_objects():
+                geometry.append(DrawToolShape(
+                    MultiLineString([affinity.scale(subgeo, xfact=factor, yfact=factor) for subgeo in shape.geo])))
+
+                self.points_edit[current_table_dia_edited].append((0, 0))
+            self.add_exc_shape(geometry, self.storage_dict[current_table_dia_edited])
+
+            self.on_tool_delete(dia=dia_changed)
+
+        # we reactivate the signals after the after the tool editing
+        self.tools_table_exc.itemChanged.connect(self.on_tool_edit)
+        # self.tools_table_exc.selectionModel().currentChanged.connect(self.on_row_selected)
+
+    def on_name_activate(self):
+        self.edited_obj_name = self.name_entry.get_value()
+
+    def activate(self):
+        self.connect_canvas_event_handlers()
+
+        # self.app.collection.view.keyPressed.connect(self.on_canvas_key)
+
+        self.shapes.enabled = True
+        self.tool_shape.enabled = True
+        # self.app.app_cursor.enabled = True
+        self.app.ui.snap_max_dist_entry.setDisabled(False)
+        self.app.ui.corner_snap_btn.setEnabled(True)
+        # Tell the App that the editor is active
+        self.editor_active = True
+
+    def deactivate(self):
+        self.disconnect_canvas_event_handlers()
+        self.clear()
+        self.app.ui.exc_edit_toolbar.setDisabled(True)
+        self.app.ui.exc_edit_toolbar.setVisible(False)
+        self.app.ui.snap_max_dist_entry.setDisabled(True)
+        self.app.ui.corner_snap_btn.setEnabled(False)
+
+        # Disable visuals
+        self.shapes.enabled = False
+        self.tool_shape.enabled = False
+        # self.app.app_cursor.enabled = False
+
+        # Tell the app that the editor is no longer active
+        self.editor_active = False
+
+        # Show original geometry
+        if self.exc_obj:
+            self.exc_obj.visible = True
+
+    def connect_canvas_event_handlers(self):
+        ## Canvas events
+
+        # make sure that the shortcuts key and mouse events will no longer be linked to the methods from FlatCAMApp
+        # but those from FlatCAMGeoEditor
+        self.app.plotcanvas.vis_disconnect('key_press', self.app.on_key_over_plot)
+        self.app.plotcanvas.vis_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
+        self.app.plotcanvas.vis_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
+        self.app.plotcanvas.vis_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
+        self.app.plotcanvas.vis_disconnect('mouse_double_click', self.app.on_double_click_over_plot)
+        self.app.collection.view.keyPressed.disconnect()
+        self.app.collection.view.clicked.disconnect()
+
+        self.canvas.vis_connect('mouse_press', self.on_canvas_click)
+        self.canvas.vis_connect('mouse_move', self.on_canvas_move)
+        self.canvas.vis_connect('mouse_release', self.on_canvas_click_release)
+        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.vis_disconnect('mouse_press', self.on_canvas_click)
+        self.canvas.vis_disconnect('mouse_move', self.on_canvas_move)
+        self.canvas.vis_disconnect('mouse_release', self.on_canvas_click_release)
+        self.canvas.vis_disconnect('key_press', self.on_canvas_key)
+        self.canvas.vis_disconnect('key_release', self.on_canvas_key_release)
+
+        # we restore the key and mouse control to FlatCAMApp method
+        self.app.plotcanvas.vis_connect('key_press', self.app.on_key_over_plot)
+        self.app.plotcanvas.vis_connect('mouse_press', self.app.on_mouse_click_over_plot)
+        self.app.plotcanvas.vis_connect('mouse_move', self.app.on_mouse_move_over_plot)
+        self.app.plotcanvas.vis_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
+        self.app.plotcanvas.vis_connect('mouse_double_click', self.app.on_double_click_over_plot)
+        self.app.collection.view.keyPressed.connect(self.app.collection.on_key)
+        self.app.collection.view.clicked.connect(self.app.collection.on_mouse_down)
+
+    def clear(self):
+        self.active_tool = None
+        # self.shape_buffer = []
+        self.selected = []
+
+        self.points_edit = {}
+        self.new_tools = {}
+        self.new_drills = []
+
+        self.storage_dict = {}
+
+        self.shapes.clear(update=True)
+        self.tool_shape.clear(update=True)
+
+        # self.storage = FlatCAMExcEditor.make_storage()
+        self.replot()
+
+    def edit_exc_obj(self, exc_obj):
+        """
+        Imports the geometry from the given FlatCAM Excellon object
+        into the editor.
+
+        :param fcgeometry: FlatCAMExcellon
+        :return: None
+        """
+
+        assert isinstance(exc_obj, Excellon), \
+            "Expected an Excellon Object, got %s" % type(exc_obj)
+
+        self.deactivate()
+        self.activate()
+
+        # Hide original geometry
+        self.exc_obj = exc_obj
+        exc_obj.visible = False
+
+        # Set selection tolerance
+        # DrawToolShape.tolerance = fc_excellon.drawing_tolerance * 10
+
+        self.select_tool("select")
+
+        self.set_ui()
+
+        # now that we hava data, create the GUI interface and add it to the Tool Tab
+        self.build_ui()
+
+        # we activate this after the initial build as we don't need to see the tool been populated
+        self.tools_table_exc.itemChanged.connect(self.on_tool_edit)
+
+        # build the geometry for each tool-diameter, each drill will be represented by a '+' symbol
+        # and then add it to the storage elements (each storage elements is a member of a list
+        for tool_dia in self.points_edit:
+            storage_elem = FlatCAMGeoEditor.make_storage()
+            for point in self.points_edit[tool_dia]:
+                # make a '+' sign, the line length is the tool diameter
+                start_hor_line = ((point.x - (tool_dia / 2)), point.y)
+                stop_hor_line = ((point.x + (tool_dia / 2)), point.y)
+                start_vert_line = (point.x, (point.y - (tool_dia / 2)))
+                stop_vert_line = (point.x, (point.y + (tool_dia / 2)))
+                shape = MultiLineString([(start_hor_line, stop_hor_line),(start_vert_line, stop_vert_line)])
+                if shape is not None:
+                    self.add_exc_shape(DrawToolShape(shape), storage_elem)
+            self.storage_dict[tool_dia] = storage_elem
+
+        self.replot()
+        self.app.ui.exc_edit_toolbar.setDisabled(False)
+        self.app.ui.exc_edit_toolbar.setVisible(True)
+        self.app.ui.snap_toolbar.setDisabled(False)
+
+        # start with GRID toolbar activated
+        if self.app.ui.grid_snap_btn.isChecked() is False:
+            self.app.ui.grid_snap_btn.trigger()
+
+    def update_exc_obj(self, exc_obj):
+        """
+        Create a new Excellon object that contain the edited content of the source Excellon object
+
+        :param exc_obj: FlatCAMExcellon
+        :return: None
+        """
+
+        # this dictionary will contain tooldia's as keys and a list of coordinates tuple as values
+        # the values of this dict are coordinates of the holes (drills)
+        edited_points = {}
+        for storage_tooldia in self.storage_dict:
+            for x in self.storage_dict[storage_tooldia].get_objects():
+
+                # all x.geo in self.storage_dict[storage] are MultiLinestring objects
+                # each MultiLineString is made out of Linestrings
+                # select first Linestring object in the current MultiLineString
+                first_linestring = x.geo[0]
+                # get it's coordinates
+                first_linestring_coords = first_linestring.coords
+                x_coord = first_linestring_coords[0][0] + (float(storage_tooldia) / 2)
+                y_coord = first_linestring_coords[0][1]
+
+                # create a tuple with the coordinates (x, y) and add it to the list that is the value of the
+                # edited_points dictionary
+                point = (x_coord, y_coord)
+                if not storage_tooldia in edited_points:
+                    edited_points[storage_tooldia] = [point]
+                else:
+                    edited_points[storage_tooldia].append(point)
+
+        # recreate the drills and tools to be added to the new Excellon edited object
+        # first, we look in the tool table if one of the tool diameters was changed then
+        # append that a tuple formed by (old_dia, edited_dia) to a list
+        changed_key = []
+        for initial_dia in self.olddia_newdia:
+            edited_dia = self.olddia_newdia[initial_dia]
+            if edited_dia != initial_dia:
+                for old_dia in edited_points:
+                    if old_dia == initial_dia:
+                        changed_key.append((old_dia, edited_dia))
+            # if the initial_dia is not in edited_points it means it is a new tool with no drill points
+            # (and we have to add it)
+            # because in case we have drill points it will have to be already added in edited_points
+            # if initial_dia not in edited_points.keys():
+            #     edited_points[initial_dia] = []
+
+        for el in changed_key:
+            edited_points[el[1]] = edited_points.pop(el[0])
+
+        # Let's sort the edited_points dictionary by keys (diameters) and store the result in a zipped list
+        # ordered_edited_points is a ordered list of tuples;
+        # element[0] of the tuple is the diameter and
+        # element[1] of the tuple is a list of coordinates (a tuple themselves)
+        ordered_edited_points = sorted(zip(edited_points.keys(), edited_points.values()))
+
+        current_tool = 0
+        for tool_dia in ordered_edited_points:
+            current_tool += 1
+
+            # create the self.tools for the new Excellon object (the one with edited content)
+            name = str(current_tool)
+            spec = {"C": float(tool_dia[0])}
+            self.new_tools[name] = spec
+
+            # create the self.drills for the new Excellon object (the one with edited content)
+            for point in tool_dia[1]:
+                self.new_drills.append(
+                    {
+                        'point': Point(point),
+                        'tool': str(current_tool)
+                    }
+                )
+
+        if self.is_modified is True:
+            if "_edit" in self.edited_obj_name:
+                try:
+                    id = int(self.edited_obj_name[-1]) + 1
+                    self.edited_obj_name = self.edited_obj_name[:-1] + str(id)
+                except ValueError:
+                    self.edited_obj_name += "_1"
+            else:
+                self.edited_obj_name += "_edit"
+
+        self.app.worker_task.emit({'fcn': self.new_edited_excellon,
+                                   'params': [self.edited_obj_name]})
+
+        if self.exc_obj.slots:
+            self.new_slots = self.exc_obj.slots
+
+        # reset the tool table
+        self.tools_table_exc.clear()
+        self.tools_table_exc.setHorizontalHeaderLabels(['#', 'Diameter', 'D', 'S'])
+        self.last_tool_selected = None
+
+        # delete the edited Excellon object which will be replaced by a new one having the edited content of the first
+        self.app.collection.set_active(self.exc_obj.options['name'])
+        self.app.collection.delete_active()
+
+        # restore GUI to the Selected TAB
+        # Remove anything else in the GUI
+        self.app.ui.tool_scroll_area.takeWidget()
+        # Switch notebook to Selected page
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
+
+    def new_edited_excellon(self, outname):
+        """
+        Creates a new Excellon object for the edited Excellon. Thread-safe.
+
+        :param outname: Name of the resulting object. None causes the
+            name to be that of the file.
+        :type outname: str
+        :return: None
+        """
+
+        self.app.log.debug("Update the Excellon object with edited content. Source is %s" %
+                           self.exc_obj.options['name'])
+
+        # How the object should be initialized
+        def obj_init(excellon_obj, app_obj):
+            # self.progress.emit(20)
+            excellon_obj.drills = self.new_drills
+            excellon_obj.tools = self.new_tools
+            excellon_obj.slots = self.new_slots
+
+            try:
+                excellon_obj.create_geometry()
+            except KeyError:
+                self.app.inform.emit(
+                    "[error_notcl] There are no Tools definitions in the file. Aborting Excellon creation.")
+            except:
+                msg = "[error] An internal error has ocurred. See shell.\n"
+                msg += traceback.format_exc()
+                app_obj.inform.emit(msg)
+                raise
+                # raise
+
+        with self.app.proc_container.new("Creating Excellon."):
+
+            try:
+                self.app.new_object("excellon", outname, obj_init)
+            except Exception as e:
+                log.error("Error on object creation: %s" % str(e))
+                self.app.progress.emit(100)
+                return
+
+            self.app.inform.emit("[success]Excellon editing finished.")
+            # self.progress.emit(100)
+
+    def on_tool_select(self, tool):
+        """
+        Behavior of the toolbar. Tool initialization.
+
+        :rtype : None
+        """
+        current_tool = tool
+
+        self.app.log.debug("on_tool_select('%s')" % tool)
+
+        if self.last_tool_selected is None and current_tool is not 'select':
+            # self.draw_app.select_tool('select')
+            self.complete = True
+            current_tool = 'select'
+            self.app.inform.emit("[warning_notcl]Cancelled. There is no Tool/Drill selected")
+
+        # This is to make the group behave as radio group
+        if current_tool in self.tools_exc:
+            if self.tools_exc[current_tool]["button"].isChecked():
+                self.app.log.debug("%s is checked." % current_tool)
+                for t in self.tools_exc:
+                    if t != current_tool:
+                        self.tools_exc[t]["button"].setChecked(False)
+
+                # this is where the Editor toolbar classes (button's) are instantiated
+                self.active_tool = self.tools_exc[current_tool]["constructor"](self)
+                # self.app.inform.emit(self.active_tool.start_msg)
+            else:
+                self.app.log.debug("%s is NOT checked." % current_tool)
+                for t in self.tools_exc:
+                    self.tools_exc[t]["button"].setChecked(False)
+                self.active_tool = None
+
+    def on_row_selected(self):
+        self.selected = []
+
+        try:
+            selected_dia = self.tool2tooldia[self.tools_table_exc.currentRow() + 1]
+            self.last_tool_selected = self.tools_table_exc.currentRow() + 1
+            for obj in self.storage_dict[selected_dia].get_objects():
+                self.selected.append(obj)
+        except Exception as e:
+            self.app.log.debug(str(e))
+
+        self.replot()
+
+    def toolbar_tool_toggle(self, key):
+        self.options[key] = self.sender().isChecked()
+        if self.options[key] == True:
+            return 1
+        else:
+            return 0
+
+    def on_canvas_click(self, event):
+        """
+        event.x and .y have canvas coordinates
+        event.xdaya and .ydata have plot coordinates
+
+        :param event: Event object dispatched by Matplotlib
+        :return: None
+        """
+
+        if event.button is 1:
+            self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                                   "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
+            self.pos = self.canvas.vispy_canvas.translate_coords(event.pos)
+
+            ### Snap coordinates
+            x, y = self.app.geo_editor.snap(self.pos[0], self.pos[1])
+
+            self.pos = (x, y)
+            # print(self.active_tool)
+
+            # 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.app.geo_editor.snap(event.xdata, event.ydata))
+                msg = self.active_tool.click(self.app.geo_editor.snap(self.pos[0], self.pos[1]))
+
+                # If it is a shape generating tool
+                if isinstance(self.active_tool, FCShapeTool) and self.active_tool.complete:
+                    if self.current_storage is not None:
+                        self.on_exc_shape_complete(self.current_storage)
+                        self.build_ui()
+                    # MS: always return to the Select Tool
+                    self.select_tool("select")
+                    return
+
+                if isinstance(self.active_tool, FCDrillSelect):
+                    # self.app.log.debug("Replotting after click.")
+                    self.replot()
+            else:
+                self.app.log.debug("No active tool to respond to click!")
+
+    def on_exc_shape_complete(self, storage):
+        self.app.log.debug("on_shape_complete()")
+
+        # Add shape
+        if type(storage) is list:
+            for item_storage in storage:
+                self.add_exc_shape(self.active_tool.geometry, item_storage)
+        else:
+            self.add_exc_shape(self.active_tool.geometry, storage)
+
+        # Remove any utility shapes
+        self.delete_utility_geometry()
+        self.tool_shape.clear(update=True)
+
+        # Replot and reset tool.
+        self.replot()
+        # self.active_tool = type(self.active_tool)(self)
+
+    def add_exc_shape(self, shape, storage):
+        """
+        Adds a shape to the shape storage.
+
+        :param shape: Shape to be added.
+        :type shape: DrawToolShape
+        :return: None
+        """
+        # List of DrawToolShape?
+        if isinstance(shape, list):
+            for subshape in shape:
+                self.add_exc_shape(subshape, storage)
+            return
+
+        assert isinstance(shape, DrawToolShape), \
+            "Expected a DrawToolShape, got %s" % str(type(shape))
+
+        assert shape.geo is not None, \
+            "Shape object has empty geometry (None)"
+
+        assert (isinstance(shape.geo, list) and len(shape.geo) > 0) or \
+               not isinstance(shape.geo, list), \
+            "Shape objects has empty geometry ([])"
+
+        if isinstance(shape, DrawToolUtilityShape):
+            self.utility.append(shape)
+        else:
+            storage.insert(shape)  # TODO: Check performance
+
+    def add_shape(self, shape):
+        """
+        Adds a shape to the shape storage.
+
+        :param shape: Shape to be added.
+        :type shape: DrawToolShape
+        :return: None
+        """
+
+        # List of DrawToolShape?
+        if isinstance(shape, list):
+            for subshape in shape:
+                self.add_shape(subshape)
+            return
+
+        assert isinstance(shape, DrawToolShape), \
+            "Expected a DrawToolShape, got %s" % type(shape)
+
+        assert shape.geo is not None, \
+            "Shape object has empty geometry (None)"
+
+        assert (isinstance(shape.geo, list) and len(shape.geo) > 0) or \
+               not isinstance(shape.geo, list), \
+            "Shape objects has empty geometry ([])"
+
+        if isinstance(shape, DrawToolUtilityShape):
+            self.utility.append(shape)
+        else:
+            self.storage.insert(shape)  # TODO: Check performance
+
+    def on_canvas_click_release(self, event):
+        pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
+
+        self.modifiers = QtWidgets.QApplication.keyboardModifiers()
+
+        if self.app.grid_status():
+            pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
+        else:
+            pos = (pos_canvas[0], pos_canvas[1])
+
+        # if the released mouse button was RMB then test if it was a panning motion or not, if not it was a context
+        # canvas menu
+        try:
+            if event.button == 2:  # right click
+                if self.app.panning_action is True:
+                    self.app.panning_action = False
+                else:
+                    self.app.cursor = QtGui.QCursor()
+                    self.app.ui.popMenu.popup(self.app.cursor.pos())
+        except Exception as e:
+            log.warning("Error: %s" % str(e))
+            raise
+
+        # if the released mouse button was LMB then test if we had a right-to-left selection or a left-to-right
+        # selection and then select a type of selection ("enclosing" or "touching")
+        try:
+            if event.button == 1:  # left click
+                if self.app.selection_type is not None:
+                    self.draw_selection_area_handler(self.pos, pos, self.app.selection_type)
+                    self.app.selection_type = None
+                elif isinstance(self.active_tool, FCDrillSelect):
+                    # Dispatch event to active_tool
+                    # msg = self.active_tool.click(self.app.geo_editor.snap(event.xdata, event.ydata))
+                    # msg = self.active_tool.click_release((self.pos[0], self.pos[1]))
+                    # self.app.inform.emit(msg)
+                    self.active_tool.click_release((self.pos[0], self.pos[1]))
+                    self.replot()
+        except Exception as e:
+            log.warning("Error: %s" % str(e))
+            raise
+
+    def draw_selection_area_handler(self, start_pos, end_pos, sel_type):
+        """
+        :param start_pos: mouse position when the selection LMB click was done
+        :param end_pos: mouse position when the left mouse button is released
+        :param sel_type: if True it's a left to right selection (enclosure), if False it's a 'touch' selection
+        :type Bool
+        :return:
+        """
+        poly_selection = Polygon([start_pos, (end_pos[0], start_pos[1]), end_pos, (start_pos[0], end_pos[1])])
+
+        self.app.delete_selection_shape()
+        for storage in self.storage_dict:
+            for obj in self.storage_dict[storage].get_objects():
+                if (sel_type is True and poly_selection.contains(obj.geo)) or \
+                        (sel_type is False and poly_selection.intersects(obj.geo)):
+                    if self.key == self.app.defaults["global_mselect_key"]:
+                        if obj in self.selected:
+                            self.selected.remove(obj)
+                        else:
+                            # add the object to the selected shapes
+                            self.selected.append(obj)
+                    else:
+                        self.selected.append(obj)
+
+        # select the diameter of the selected shape in the tool table
+        for storage in self.storage_dict:
+            for shape_s in self.selected:
+                if shape_s in self.storage_dict[storage].get_objects():
+                    for key in self.tool2tooldia:
+                        if self.tool2tooldia[key] == storage:
+                            item = self.tools_table_exc.item((key - 1), 1)
+                            self.tools_table_exc.setCurrentItem(item)
+                            self.last_tool_selected = key
+                            # item.setSelected(True)
+                            # self.exc_editor_app.tools_table_exc.selectItem(key - 1)
+
+        self.replot()
+
+    def on_canvas_move(self, event):
+        """
+        Called on 'mouse_move' event
+
+        event.pos have canvas screen coordinates
+
+        :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
+
+        # if the RMB is clicked and mouse is moving over plot then 'panning_action' is True
+        if event.button == 2:
+            self.app.panning_action = True
+            return
+        else:
+            self.app.panning_action = False
+
+        try:
+            x = float(event.xdata)
+            y = float(event.ydata)
+        except TypeError:
+            return
+
+        if self.active_tool is None:
+            return
+
+        ### Snap coordinates
+        x, y = self.app.geo_editor.app.geo_editor.snap(x, y)
+
+        self.snap_x = x
+        self.snap_y = y
+
+        # update the position label in the infobar since the APP mouse event handlers are disconnected
+        self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
+                                       "<b>Y</b>: %.4f" % (x, y))
+
+        if self.pos is None:
+            self.pos = (0, 0)
+        dx = x - self.pos[0]
+        dy = y - self.pos[1]
+
+        # update the reference position label in the infobar since the APP mouse event handlers are disconnected
+        self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                           "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (dx, dy))
+
+        ### Utility geometry (animated)
+        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.tool_shape.clear(update=True)
+            self.draw_utility_geometry(geo=geo)
+
+        ### Selection area on canvas section ###
+        dx = pos[0] - self.pos[0]
+        if event.is_dragging == 1 and event.button == 1:
+            self.app.delete_selection_shape()
+            if dx < 0:
+                self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x,y),
+                     color=self.app.defaults["global_alt_sel_line"],
+                     face_color=self.app.defaults['global_alt_sel_fill'])
+                self.app.selection_type = False
+            else:
+                self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x,y))
+                self.app.selection_type = True
+        else:
+            self.app.selection_type = None
+
+        # Update cursor
+        self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color='black', size=20)
+
+
+    def on_canvas_key(self, event):
+        """
+        event.key has the key.
+
+        :param event:
+        :return:
+        """
+        self.key = event.key.name
+        self.modifiers = QtWidgets.QApplication.keyboardModifiers()
+
+        if self.modifiers == Qt.ControlModifier:
+            # save (update) the current geometry and return to the App
+            if self.key == 'S':
+                self.app.editor2object()
+                return
+
+            # toggle the measurement tool
+            if self.key == 'M':
+                self.app.measurement_tool.run()
+                return
+
+        # Abort the current action
+        if event.key.name == 'Escape':
+            # TODO: ...?
+            # self.on_tool_select("select")
+            self.app.inform.emit("[warning_notcl]Cancelled.")
+
+            self.delete_utility_geometry()
+
+            self.replot()
+            # self.select_btn.setChecked(True)
+            # self.on_tool_select('select')
+            self.select_tool('select')
+            return
+
+        # Delete selected object
+        if event.key.name == 'Delete':
+            self.launched_from_shortcuts = True
+            if self.selected:
+                self.delete_selected()
+                self.replot()
+            else:
+                self.app.inform.emit("[warning_notcl]Cancelled. Nothing selected to delete.")
+            return
+
+        # Add Array of Drill Hole Tool
+        if event.key.name == 'A':
+            self.launched_from_shortcuts = True
+            self.app.inform.emit("Click on target point.")
+            self.app.ui.add_drill_array_btn.setChecked(True)
+            self.select_tool('add_array')
+            return
+
+        # Copy
+        if event.key.name == 'C':
+            self.launched_from_shortcuts = True
+            if self.selected:
+                self.app.inform.emit("Click on target point.")
+                self.app.ui.copy_drill_btn.setChecked(True)
+                self.on_tool_select('copy')
+                self.active_tool.set_origin((self.snap_x, self.snap_y))
+            else:
+                self.app.inform.emit("[warning_notcl]Cancelled. Nothing selected to copy.")
+            return
+
+        # Add Drill Hole Tool
+        if event.key.name == 'D':
+            self.launched_from_shortcuts = True
+            self.app.inform.emit("Click on target point.")
+            self.app.ui.add_drill_btn.setChecked(True)
+            self.select_tool('add')
+            return
+
+        # Grid Snap
+        if event.key.name == 'G':
+            self.launched_from_shortcuts = True
+            # make sure that the cursor shape is enabled/disabled, too
+            if self.options['grid_snap'] is True:
+                self.app.app_cursor.enabled = False
+            else:
+                self.app.app_cursor.enabled = True
+            self.app.ui.grid_snap_btn.trigger()
+            return
+
+        # Corner Snap
+        if event.key.name == 'K':
+            self.launched_from_shortcuts = True
+            self.app.ui.corner_snap_btn.trigger()
+            return
+
+        # Move
+        if event.key.name == 'M':
+            self.launched_from_shortcuts = True
+            if self.selected:
+                self.app.inform.emit("Click on target point.")
+                self.app.ui.move_drill_btn.setChecked(True)
+                self.on_tool_select('move')
+                self.active_tool.set_origin((self.snap_x, self.snap_y))
+            else:
+                self.app.inform.emit("[warning_notcl]Cancelled. Nothing selected to move.")
+            return
+
+        # Resize Tool
+        if event.key.name == 'R':
+            self.launched_from_shortcuts = True
+            self.select_tool('resize')
+            return
+
+        # Select Tool
+        if event.key.name == 'S':
+            self.launched_from_shortcuts = True
+            self.select_tool('select')
+            return
+
+        # Propagate to tool
+        response = None
+        if self.active_tool is not None:
+            response = self.active_tool.on_key(event.key)
+        if response is not None:
+            self.app.inform.emit(response)
+
+        # Show Shortcut list
+        if event.key.name == '`':
+            self.on_shortcut_list()
+            return
+
+    def on_shortcut_list(self):
+        msg = '''<b>Shortcut list in Geometry Editor</b><br>
+<br>
+<b>A:</b>       Add an 'Drill Array'<br>
+<b>C:</b>       Copy Drill Hole<br>
+<b>D:</b>       Add an Drill Hole<br>
+<b>G:</b>       Grid Snap On/Off<br>
+<b>K:</b>       Corner Snap On/Off<br>
+<b>M:</b>       Move Drill Hole<br>
+<br>
+<b>R:</b>       Resize a 'Drill Hole'<br>
+<b>S:</b>       Select Tool Active<br>
+<br>
+<b>~:</b>       Show Shortcut List<br>
+<br>
+<b>Enter:</b>   Finish Current Action<br>
+<b>Escape:</b>  Abort Current Action<br>
+<b>Delete:</b>  Delete Drill Hole'''
+
+        helpbox =QtWidgets.QMessageBox()
+        helpbox.setText(msg)
+        helpbox.setWindowTitle("Help")
+        helpbox.setWindowIcon(QtGui.QIcon('share/help.png'))
+        helpbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+        helpbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+        helpbox.exec_()
+
+    def on_canvas_key_release(self, event):
+        self.key = None
+
+    def draw_utility_geometry(self, geo):
+            # Add the new utility shape
+            try:
+                # this case is for the Font Parse
+                for el in list(geo.geo):
+                    if type(el) == MultiPolygon:
+                        for poly in el:
+                            self.tool_shape.add(
+                                shape=poly,
+                                color=(self.app.defaults["global_draw_color"] + '80'),
+                                update=False,
+                                layer=0,
+                                tolerance=None
+                            )
+                    elif type(el) == MultiLineString:
+                        for linestring in el:
+                            self.tool_shape.add(
+                                shape=linestring,
+                                color=(self.app.defaults["global_draw_color"] + '80'),
+                                update=False,
+                                layer=0,
+                                tolerance=None
+                            )
+                    else:
+                        self.tool_shape.add(
+                            shape=el,
+                            color=(self.app.defaults["global_draw_color"] + '80'),
+                            update=False,
+                            layer=0,
+                            tolerance=None
+                        )
+            except TypeError:
+                self.tool_shape.add(
+                    shape=geo.geo, color=(self.app.defaults["global_draw_color"] + '80'),
+                    update=False, layer=0, tolerance=None)
+
+            self.tool_shape.redraw()
+
+
+    def replot(self):
+        self.plot_all()
+
+    def plot_all(self):
+        """
+        Plots all shapes in the editor.
+
+        :return: None
+        :rtype: None
+        """
+        # self.app.log.debug("plot_all()")
+        self.shapes.clear(update=True)
+
+        for storage in self.storage_dict:
+            for shape_plus in self.storage_dict[storage].get_objects():
+                if shape_plus.geo is None:
+                    continue
+
+                if shape_plus in self.selected:
+                    self.plot_shape(geometry=shape_plus.geo, color=self.app.defaults['global_sel_draw_color'], linewidth=2)
+                    continue
+                self.plot_shape(geometry=shape_plus.geo, color=self.app.defaults['global_draw_color'])
+
+        # 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, color=self.app.defaults['global_sel_draw_color'], linewidth=2)
+        #         continue
+        #
+        #     self.plot_shape(geometry=shape.geo, color=self.app.defaults['global_draw_color'])
+
+
+
+        for shape in self.utility:
+            self.plot_shape(geometry=shape.geo, linewidth=1)
+            continue
+
+        self.shapes.redraw()
+
+    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.
+
+        :param geometry: Geometry to be plotted (Any Shapely.geom kind or list of such)
+        :param color: Shape color
+        :param linewidth: Width of lines in # of pixels.
+        :return: List of plotted elements.
+        """
+        plot_elements = []
+
+        if geometry is None:
+            geometry = self.active_tool.geometry
+
+        try:
+            for geo in geometry:
+                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, color=color, linewidth=linewidth)
+
+            ## Polygon: Descend into exterior and each interior.
+            if type(geometry) == Polygon:
+                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:
+                plot_elements.append(self.shapes.add(shape=geometry, color=color, layer=0))
+
+            if type(geometry) == Point:
+                pass
+
+        return plot_elements
+
+    def on_shape_complete(self):
+        self.app.log.debug("on_shape_complete()")
+
+        # Add shape
+        self.add_shape(self.active_tool.geometry)
+
+        # Remove any utility shapes
+        self.delete_utility_geometry()
+        self.tool_shape.clear(update=True)
+
+        # Replot and reset tool.
+        self.replot()
+        # self.active_tool = type(self.active_tool)(self)
+
+    def get_selected(self):
+        """
+        Returns list of shapes that are selected in the editor.
+
+        :return: List of shapes.
+        """
+        # return [shape for shape in self.shape_buffer if shape["selected"]]
+        return self.selected
+
+    def delete_selected(self):
+        temp_ref = [s for s in self.selected]
+        for shape_sel in temp_ref:
+            self.delete_shape(shape_sel)
+
+        self.selected = []
+        self.build_ui()
+        self.app.inform.emit("[success]Done. Drill(s) deleted.")
+
+    def delete_shape(self, shape):
+        self.is_modified = True
+
+        if shape in self.utility:
+            self.utility.remove(shape)
+            return
+
+        for storage in self.storage_dict:
+            # try:
+            #     self.storage_dict[storage].remove(shape)
+            # except:
+            #     pass
+            if shape in self.storage_dict[storage].get_objects():
+                self.storage_dict[storage].remove(shape)
+                # a hack to make the tool_table display less drills per diameter
+                # self.points_edit it's only useful first time when we load the data into the storage
+                # but is still used as referecen when building tool_table in self.build_ui()
+                # the number of drills displayed in column 2 is just a len(self.points_edit) therefore
+                # deleting self.points_edit elements (doesn't matter who but just the number) solved the display issue.
+                del self.points_edit[storage][0]
+
+        if shape in self.selected:
+            self.selected.remove(shape)  # TODO: Check performance
+
+    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]
+        for_deletion = [shape for shape in self.utility]
+        for shape in for_deletion:
+            self.delete_shape(shape)
+
+        self.tool_shape.clear(update=True)
+        self.tool_shape.redraw()
+
+    def on_delete_btn(self):
+        self.delete_selected()
+        self.replot()
+
+    def select_tool(self, toolname):
+        """
+        Selects a drawing tool. Impacts the object and GUI.
+
+        :param toolname: Name of the tool.
+        :return: None
+        """
+        self.tools_exc[toolname]["button"].setChecked(True)
+        self.on_tool_select(toolname)
+
+    def set_selected(self, shape):
+
+        # Remove and add to the end.
+        if shape in self.selected:
+            self.selected.remove(shape)
+
+        self.selected.append(shape)
+
+    def set_unselected(self, shape):
+        if shape in self.selected:
+            self.selected.remove(shape)
+
+    def on_array_type_combo(self):
+        if self.array_type_combo.currentIndex() == 0:
+            self.array_circular_frame.hide()
+            self.array_linear_frame.show()
+        else:
+            self.delete_utility_geometry()
+            self.array_circular_frame.show()
+            self.array_linear_frame.hide()
+            self.app.inform.emit("Click on the circular array Center position")
+
+    def exc_add_drill(self):
+        self.select_tool('add')
+        return
+
+    def exc_add_drill_array(self):
+        self.select_tool('add_array')
+        return
+
+    def exc_copy_drills(self):
+        self.select_tool('copy')
+        return
+
+def distance(pt1, pt2):
+    return sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2)
+
+
+def mag(vec):
+    return sqrt(vec[0] ** 2 + vec[1] ** 2)
+
+
+def poly2rings(poly):
+    return [poly.exterior] + [interior for interior in poly.interiors]

+ 2364 - 0
FlatCAMGUI.py

@@ -0,0 +1,2364 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+from PyQt5 import QtGui, QtCore, QtWidgets
+from PyQt5.QtCore import Qt
+from GUIElements import *
+import platform
+
+class FlatCAMGUI(QtWidgets.QMainWindow):
+    # Emitted when persistent window geometry needs to be retained
+    geom_update = QtCore.pyqtSignal(int, int, int, int, int, name='geomUpdate')
+    final_save = QtCore.pyqtSignal(name='saveBeforeExit')
+
+    def __init__(self, version, app):
+        super(FlatCAMGUI, self).__init__()
+
+        self.app = app
+        # Divine icon pack by Ipapun @ finicons.com
+
+        #####################################
+        ### BUILDING THE GUI IS DONE HERE ###
+        #####################################
+
+        ############
+        ### Menu ###
+        ############
+        self.menu = self.menuBar()
+
+        ### File ###
+        self.menufile = self.menu.addMenu('&File')
+
+        # New
+        self.menufilenew = QtWidgets.QAction(QtGui.QIcon('share/file16.png'), '&New Project', self)
+        self.menufile.addAction(self.menufilenew)
+
+        self.menufile_open = self.menufile.addMenu(QtGui.QIcon('share/folder32_bis.png'), 'Open')
+        # Open gerber ...
+        self.menufileopengerber = QtWidgets.QAction(QtGui.QIcon('share/flatcam_icon24.png'), 'Open &Gerber ...', self)
+        self.menufile_open.addAction(self.menufileopengerber)
+
+        # Open gerber with follow...
+        self.menufileopengerber_follow = QtWidgets.QAction(QtGui.QIcon('share/flatcam_icon24.png'),
+                                                       'Open &Gerber (w/ Follow)', self)
+        self.menufile_open.addAction(self.menufileopengerber_follow)
+        self.menufile_open.addSeparator()
+
+        # Open Excellon ...
+        self.menufileopenexcellon = QtWidgets.QAction(QtGui.QIcon('share/open_excellon32.png'), 'Open &Excellon ...',
+                                                  self)
+        self.menufile_open.addAction(self.menufileopenexcellon)
+
+        # Open G-Code ...
+        self.menufileopengcode = QtWidgets.QAction(QtGui.QIcon('share/code.png'), 'Open G-&Code ...', self)
+        self.menufile_open.addAction(self.menufileopengcode)
+
+        # Open Project ...
+        self.menufileopenproject = QtWidgets.QAction(QtGui.QIcon('share/folder16.png'), 'Open &Project ...', self)
+        self.menufile_open.addAction(self.menufileopenproject)
+
+        # Recent
+        self.recent = self.menufile.addMenu(QtGui.QIcon('share/recent_files.png'), "Recent files")
+
+        # Separator
+        self.menufile.addSeparator()
+
+        # Run Scripts
+        self.menufilerunscript = QtWidgets.QAction(QtGui.QIcon('share/script16.png'), 'Run Script', self)
+        self.menufile.addAction(self.menufilerunscript)
+
+        # Separator
+        self.menufile.addSeparator()
+
+        # Import ...
+        self.menufileimport = self.menufile.addMenu(QtGui.QIcon('share/import.png'), 'Import')
+        self.menufileimportsvg = QtWidgets.QAction(QtGui.QIcon('share/svg16.png'),
+                                               '&SVG as Geometry Object', self)
+        self.menufileimport.addAction(self.menufileimportsvg)
+        self.menufileimportsvg_as_gerber = QtWidgets.QAction(QtGui.QIcon('share/svg16.png'),
+                                                         '&SVG as Gerber Object', self)
+        self.menufileimport.addAction(self.menufileimportsvg_as_gerber)
+        self.menufileimport.addSeparator()
+
+        self.menufileimportdxf = QtWidgets.QAction(QtGui.QIcon('share/dxf16.png'),
+                                               '&DXF as Geometry Object', self)
+        self.menufileimport.addAction(self.menufileimportdxf)
+        self.menufileimportdxf_as_gerber = QtWidgets.QAction(QtGui.QIcon('share/dxf16.png'),
+                                                         '&DXF as Gerber Object', self)
+        self.menufileimport.addAction(self.menufileimportdxf_as_gerber)
+        self.menufileimport.addSeparator()
+
+        # Export ...
+        self.menufileexport = self.menufile.addMenu(QtGui.QIcon('share/export.png'), 'Export')
+        self.menufileexportsvg = QtWidgets.QAction(QtGui.QIcon('share/export.png'), 'Export &SVG ...', self)
+        self.menufileexport.addAction(self.menufileexportsvg)
+
+        self.menufileexportdxf = QtWidgets.QAction(QtGui.QIcon('share/export.png'), 'Export DXF ...', self)
+        self.menufileexport.addAction(self.menufileexportdxf)
+
+        self.menufileexport.addSeparator()
+
+        self.menufileexportpng = QtWidgets.QAction(QtGui.QIcon('share/export_png32.png'), 'Export &PNG ...', self)
+        self.menufileexport.addAction(self.menufileexportpng)
+
+        self.menufileexport.addSeparator()
+
+        self.menufileexportexcellon = QtWidgets.QAction(QtGui.QIcon('share/drill32.png'), 'Export &Excellon ...', self)
+        self.menufileexport.addAction(self.menufileexportexcellon)
+
+        self.menufileexportexcellon_altium = QtWidgets.QAction(QtGui.QIcon('share/drill32.png'),
+                                                           'Export Excellon 2:4 LZ INCH ...', self)
+        self.menufileexport.addAction(self.menufileexportexcellon_altium)
+
+        # Separator
+        self.menufile.addSeparator()
+
+        # Save Defaults
+        self.menufilesavedefaults = QtWidgets.QAction(QtGui.QIcon('share/defaults.png'), 'Save &Defaults', self)
+        self.menufile.addAction(self.menufilesavedefaults)
+
+        # Separator
+        self.menufile.addSeparator()
+
+        self.menufile_save = self.menufile.addMenu(QtGui.QIcon('share/save_as.png'), 'Save')
+        # Save Project
+        self.menufilesaveproject = QtWidgets.QAction(QtGui.QIcon('share/floppy16.png'), '&Save Project', self)
+        self.menufile_save.addAction(self.menufilesaveproject)
+
+        # Save Project As ...
+        self.menufilesaveprojectas = QtWidgets.QAction(QtGui.QIcon('share/save_as.png'), 'Save Project &As ...', self)
+        self.menufile_save.addAction(self.menufilesaveprojectas)
+
+        # Save Project Copy ...
+        self.menufilesaveprojectcopy = QtWidgets.QAction(QtGui.QIcon('share/floppy16.png'), 'Save Project C&opy ...',
+                                                     self)
+        self.menufile_save.addAction(self.menufilesaveprojectcopy)
+
+        # Separator
+        self.menufile.addSeparator()
+
+        # Quit
+        self.menufile_exit = QtWidgets.QAction(QtGui.QIcon('share/power16.png'), 'E&xit', self)
+        # exitAction.setShortcut('Ctrl+Q')
+        # exitAction.setStatusTip('Exit application')
+        self.menufile.addAction(self.menufile_exit)
+
+        ### Edit ###
+        self.menuedit = self.menu.addMenu('&Edit')
+        self.menueditnew = self.menuedit.addAction(QtGui.QIcon('share/new_geo16.png'), '&New Geometry')
+        self.menueditnewexc = self.menuedit.addAction(QtGui.QIcon('share/new_geo16.png'), 'New Excellon')
+        # Separator
+        self.menuedit.addSeparator()
+        self.menueditedit = self.menuedit.addAction(QtGui.QIcon('share/edit16.png'), 'Edit Object')
+        self.menueditok = self.menuedit.addAction(QtGui.QIcon('share/edit_ok16.png'), '&Update Object')
+        # Separator
+        self.menuedit.addSeparator()
+        self.menuedit_convert = self.menuedit.addMenu(QtGui.QIcon('share/convert24.png'), 'Conversion')
+        self.menuedit_convertjoin = self.menuedit_convert.addAction(QtGui.QIcon('share/join16.png'), '&Join Geo/Gerber')
+        self.menuedit_convertjoinexc = self.menuedit_convert.addAction(QtGui.QIcon('share/join16.png'), 'Join Excellon')
+        # Separator
+        self.menuedit_convert.addSeparator()
+        self.menuedit_convert_sg2mg = self.menuedit_convert.addAction(
+            QtGui.QIcon('share/convert24.png'), 'Convert Single to MultiGeo')
+        self.menuedit_convert_mg2sg = self.menuedit_convert.addAction(
+            QtGui.QIcon('share/convert24.png'), 'Convert Multi to SingleGeo')
+
+        # Separator
+        self.menuedit.addSeparator()
+        self.menueditdelete = self.menuedit.addAction(QtGui.QIcon('share/trash16.png'), '&Delete')
+
+        # Separator
+        self.menuedit.addSeparator()
+        self.menueditcopyobject = self.menuedit.addAction(QtGui.QIcon('share/copy.png'), '&Copy Object')
+        self.menueditcopyobjectasgeom = self.menuedit.addAction(QtGui.QIcon('share/copy_geo.png'),
+                                                                'Copy as &Geom')
+
+        # Separator
+        self.menuedit.addSeparator()
+        self.menueditorigin = self.menuedit.addAction(QtGui.QIcon('share/origin.png'), 'Se&t Origin')
+        self.menueditjump = self.menuedit.addAction(QtGui.QIcon('share/jump_to16.png'), 'Jump to Location')
+
+        # Separator
+        self.menuedit.addSeparator()
+        self.menueditselectall = self.menuedit.addAction(QtGui.QIcon('share/select_all.png'),
+                                                         '&Select All')
+
+        # Separator
+        self.menuedit.addSeparator()
+        self.menueditpreferences = self.menuedit.addAction(QtGui.QIcon('share/pref.png'), '&Preferences')
+
+        ### Options ###
+        self.menuoptions = self.menu.addMenu('&Options')
+        # self.menuoptions_transfer = self.menuoptions.addMenu(QtGui.QIcon('share/transfer.png'), 'Transfer options')
+        # self.menuoptions_transfer_a2p = self.menuoptions_transfer.addAction("Application to Project")
+        # self.menuoptions_transfer_p2a = self.menuoptions_transfer.addAction("Project to Application")
+        # self.menuoptions_transfer_p2o = self.menuoptions_transfer.addAction("Project to Object")
+        # self.menuoptions_transfer_o2p = self.menuoptions_transfer.addAction("Object to Project")
+        # self.menuoptions_transfer_a2o = self.menuoptions_transfer.addAction("Application to Object")
+        # self.menuoptions_transfer_o2a = self.menuoptions_transfer.addAction("Object to Application")
+
+        # Separator
+        # self.menuoptions.addSeparator()
+
+        # self.menuoptions_transform = self.menuoptions.addMenu(QtGui.QIcon('share/transform.png'),
+        #                                                       '&Transform Object')
+        self.menuoptions_transform_rotate = self.menuoptions.addAction(QtGui.QIcon('share/rotate.png'),
+                                                                                 "&Rotate Selection")
+        # Separator
+        self.menuoptions.addSeparator()
+
+        self.menuoptions_transform_skewx = self.menuoptions.addAction(QtGui.QIcon('share/skewX.png'),
+                                                                                "&Skew on X axis")
+        self.menuoptions_transform_skewy = self.menuoptions.addAction(QtGui.QIcon('share/skewY.png'),
+                                                                                "S&kew on Y axis")
+
+        # Separator
+        self.menuoptions.addSeparator()
+        self.menuoptions_transform_flipx = self.menuoptions.addAction(QtGui.QIcon('share/flipx.png'),
+                                                                                "Flip on &X axis")
+        self.menuoptions_transform_flipy = self.menuoptions.addAction(QtGui.QIcon('share/flipy.png'),
+                                                                                "Flip on &Y axis")
+        # Separator
+        self.menuoptions.addSeparator()
+
+        ### View ###
+        self.menuview = self.menu.addMenu('&View')
+        self.menuviewenable = self.menuview.addAction(QtGui.QIcon('share/replot16.png'), 'Enable all plots')
+        self.menuviewdisableall = self.menuview.addAction(QtGui.QIcon('share/clear_plot16.png'),
+                                                          'Disable all plots')
+        self.menuviewdisableother = self.menuview.addAction(QtGui.QIcon('share/clear_plot16.png'),
+                                                            'Disable non-selected')
+        # Separator
+        self.menuview.addSeparator()
+        self.menuview_zoom_fit = self.menuview.addAction(QtGui.QIcon('share/zoom_fit32.png'), "&Zoom Fit")
+        self.menuview_zoom_in = self.menuview.addAction(QtGui.QIcon('share/zoom_in32.png'), "&Zoom In")
+        self.menuview_zoom_out = self.menuview.addAction(QtGui.QIcon('share/zoom_out32.png'), "&Zoom Out")
+
+        self.menuview.addSeparator()
+        self.menuview_toggle_axis = self.menuview.addAction(QtGui.QIcon('share/axis32.png'), "&Toggle Axis")
+        self.menuview_toggle_workspace = self.menuview.addAction(QtGui.QIcon('share/workspace24.png'),
+                                                                 "Toggle Workspace")
+
+
+        ### FlatCAM Editor menu ###
+        # self.editor_menu = QtWidgets.QMenu("Editor")
+        # self.menu.addMenu(self.editor_menu)
+        self.geo_editor_menu = QtWidgets.QMenu("Geo Editor")
+        self.menu.addMenu(self.geo_editor_menu)
+
+        # self.select_menuitem = self.menu.addAction(QtGui.QIcon('share/pointer16.png'), "Select 'Esc'")
+        self.geo_add_circle_menuitem = self.geo_editor_menu.addAction(QtGui.QIcon('share/circle32.png'), 'Add Circle')
+        self.geo_add_arc_menuitem = self.geo_editor_menu.addAction(QtGui.QIcon('share/arc16.png'), 'Add Arc')
+        self.geo_editor_menu.addSeparator()
+        self.geo_add_rectangle_menuitem = self.geo_editor_menu.addAction(QtGui.QIcon('share/rectangle32.png'), 'Add Rectangle')
+        self.geo_add_polygon_menuitem = self.geo_editor_menu.addAction(QtGui.QIcon('share/polygon32.png'), 'Add Polygon')
+        self.geo_add_path_menuitem = self.geo_editor_menu.addAction(QtGui.QIcon('share/path32.png'), 'Add Path')
+        self.geo_editor_menu.addSeparator()
+        self.geo_add_text_menuitem = self.geo_editor_menu.addAction(QtGui.QIcon('share/text32.png'), 'Add Text')
+        self.geo_editor_menu.addSeparator()
+        self.geo_union_menuitem = self.geo_editor_menu.addAction(QtGui.QIcon('share/union16.png'), 'Polygon Union')
+        self.geo_intersection_menuitem = self.geo_editor_menu.addAction(QtGui.QIcon('share/intersection16.png'),
+                                                         'Polygon Intersection')
+        self.geo_subtract_menuitem = self.geo_editor_menu.addAction(QtGui.QIcon('share/subtract16.png'), 'Polygon Subtraction')
+        self.geo_editor_menu.addSeparator()
+        self.geo_cutpath_menuitem = self.geo_editor_menu.addAction(QtGui.QIcon('share/cutpath16.png'), 'Cut Path')
+        # self.move_menuitem = self.menu.addAction(QtGui.QIcon('share/move16.png'), "Move Objects 'm'")
+        self.geo_copy_menuitem = self.geo_editor_menu.addAction(QtGui.QIcon('share/copy16.png'), "Copy Geom")
+        self.geo_delete_menuitem = self.geo_editor_menu.addAction(QtGui.QIcon('share/deleteshape16.png'), "Delete Shape")
+        self.geo_editor_menu.addSeparator()
+        self.geo_buffer_menuitem = self.geo_editor_menu.addAction(QtGui.QIcon('share/buffer16.png'), "Buffer Selection")
+        self.geo_paint_menuitem = self.geo_editor_menu.addAction(QtGui.QIcon('share/paint16.png'), "Paint Selection")
+        self.geo_editor_menu.addSeparator()
+
+        # self.exc_editor_menu = QtWidgets.QMenu("Excellon Editor")
+        # self.menu.addMenu(self.exc_editor_menu)
+
+        self.geo_editor_menu.setDisabled(True)
+        # self.exc_editor_menu.setDisabled(True)
+
+        ### Tool ###
+        # self.menutool = self.menu.addMenu('&Tool')
+        self.menutool = QtWidgets.QMenu('&Tool')
+        self.menutoolaction = self.menu.addMenu(self.menutool)
+        self.menutoolshell = self.menutool.addAction(QtGui.QIcon('share/shell16.png'), '&Command Line')
+
+        ### Help ###
+        self.menuhelp = self.menu.addMenu('&Help')
+        self.menuhelp_about = self.menuhelp.addAction(QtGui.QIcon('share/tv16.png'), 'About FlatCAM')
+        self.menuhelp_home = self.menuhelp.addAction(QtGui.QIcon('share/home16.png'), 'Home')
+        self.menuhelp_manual = self.menuhelp.addAction(QtGui.QIcon('share/globe16.png'), 'Manual')
+        self.menuhelp.addSeparator()
+        self.menuhelp_shortcut_list = self.menuhelp.addAction(QtGui.QIcon('share/shortcuts24.png'), 'Shortcuts List')
+        self.menuhelp_videohelp = self.menuhelp.addAction(QtGui.QIcon('share/videohelp24.png'), 'See on YouTube')
+
+        ####################
+        ### Context menu ###
+        ####################
+
+        self.menuproject = QtWidgets.QMenu()
+        self.menuprojectenable = self.menuproject.addAction('Enable')
+        self.menuprojectdisable = self.menuproject.addAction('Disable')
+        self.menuproject.addSeparator()
+        self.menuprojectgeneratecnc = self.menuproject.addAction('Generate CNC')
+        self.menuproject.addSeparator()
+        self.menuprojectdelete = self.menuproject.addAction('Delete')
+
+        ###############
+        ### Toolbar ###
+        ###############
+        self.toolbarfile = QtWidgets.QToolBar('File Toolbar')
+        self.addToolBar(self.toolbarfile)
+        self.file_open_gerber_btn = self.toolbarfile.addAction(QtGui.QIcon('share/flatcam_icon32.png'),
+                                                               "Open GERBER")
+        self.file_open_excellon_btn = self.toolbarfile.addAction(QtGui.QIcon('share/drill32.png'), "Open EXCELLON")
+        self.toolbarfile.addSeparator()
+        self.file_open_btn = self.toolbarfile.addAction(QtGui.QIcon('share/folder32.png'), "Open project")
+        self.file_save_btn = self.toolbarfile.addAction(QtGui.QIcon('share/floppy32.png'), "Save project")
+
+        self.toolbargeo = QtWidgets.QToolBar('Edit Toolbar')
+        self.addToolBar(self.toolbargeo)
+
+        self.newgeo_btn = self.toolbargeo.addAction(QtGui.QIcon('share/new_geo32_bis.png'), "New Blank Geometry")
+        self.newexc_btn = self.toolbargeo.addAction(QtGui.QIcon('share/new_exc32.png'), "New Blank Excellon")
+        self.toolbargeo.addSeparator()
+        self.editgeo_btn = self.toolbargeo.addAction(QtGui.QIcon('share/edit32.png'), "Editor")
+        self.update_obj_btn = self.toolbargeo.addAction(QtGui.QIcon('share/edit_ok32_bis.png'), "Save Object")
+        self.update_obj_btn.setEnabled(False)
+        self.toolbargeo.addSeparator()
+        self.delete_btn = self.toolbargeo.addAction(QtGui.QIcon('share/cancel_edit32.png'), "&Delete")
+
+        self.toolbarview = QtWidgets.QToolBar('View Toolbar')
+        self.addToolBar(self.toolbarview)
+        self.replot_btn = self.toolbarview.addAction(QtGui.QIcon('share/replot32.png'), "&Replot")
+        self.clear_plot_btn = self.toolbarview.addAction(QtGui.QIcon('share/clear_plot32.png'), "&Clear plot")
+        self.zoom_in_btn = self.toolbarview.addAction(QtGui.QIcon('share/zoom_in32.png'), "Zoom In")
+        self.zoom_out_btn = self.toolbarview.addAction(QtGui.QIcon('share/zoom_out32.png'), "Zoom Out")
+        self.zoom_fit_btn = self.toolbarview.addAction(QtGui.QIcon('share/zoom_fit32.png'), "Zoom Fit")
+
+        # self.toolbarview.setVisible(False)
+
+        self.toolbartools = QtWidgets.QToolBar('Tools Toolbar')
+        self.addToolBar(self.toolbartools)
+        self.shell_btn = self.toolbartools.addAction(QtGui.QIcon('share/shell32.png'), "&Command Line")
+
+        ### Drill Editor Toolbar ###
+        self.exc_edit_toolbar = QtWidgets.QToolBar('Excellon Editor Toolbar')
+        self.addToolBar(self.exc_edit_toolbar)
+
+        self.select_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), "Select 'Esc'")
+        self.add_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/plus16.png'), 'Add Drill Hole')
+        self.add_drill_array_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon('share/addarray16.png'), 'Add Drill Hole Array')
+        self.resize_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/resize16.png'), 'Resize Drill')
+        self.exc_edit_toolbar.addSeparator()
+
+        self.copy_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/copy32.png'), 'Copy Drill')
+        self.delete_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/deleteshape32.png'), "Delete Drill")
+
+        self.exc_edit_toolbar.addSeparator()
+        self.move_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/move32.png'), "Move Drill")
+
+        self.exc_edit_toolbar.setDisabled(True)
+        self.exc_edit_toolbar.setVisible(False)
+
+        ### Geometry Editor Toolbar ###
+        self.geo_edit_toolbar = QtWidgets.QToolBar('Geometry Editor Toolbar')
+        self.geo_edit_toolbar.setVisible(False)
+        self.addToolBar(self.geo_edit_toolbar)
+
+        self.geo_select_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), "Select 'Esc'")
+        self.geo_add_circle_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/circle32.png'), 'Add Circle')
+        self.geo_add_arc_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/arc32.png'), 'Add Arc')
+        self.geo_add_rectangle_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/rectangle32.png'), 'Add Rectangle')
+
+        self.geo_edit_toolbar.addSeparator()
+        self.geo_add_path_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/path32.png'), 'Add Path')
+        self.geo_add_polygon_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/polygon32.png'), 'Add Polygon')
+        self.geo_edit_toolbar.addSeparator()
+        self.geo_add_text_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/text32.png'), 'Add Text')
+        self.geo_add_buffer_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/buffer16-2.png'), 'Add Buffer')
+        self.geo_add_paint_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/paint20_1.png'), 'Paint Shape')
+
+        self.geo_edit_toolbar.addSeparator()
+        self.geo_union_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/union32.png'), 'Polygon Union')
+        self.geo_intersection_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/intersection32.png'),
+                                                               'Polygon Intersection')
+        self.geo_subtract_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/subtract32.png'), 'Polygon Subtraction')
+
+        self.geo_edit_toolbar.addSeparator()
+        self.geo_cutpath_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/cutpath32.png'), 'Cut Path')
+        self.geo_copy_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/copy32.png'), "Copy Objects 'c'")
+        self.geo_rotate_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/rotate.png'), "Rotate Objects 'Space'")
+        self.geo_delete_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/deleteshape32.png'), "Delete Shape '-'")
+
+        self.geo_edit_toolbar.addSeparator()
+        self.geo_move_btn = self.geo_edit_toolbar.addAction(QtGui.QIcon('share/move32.png'), "Move Objects 'm'")
+
+        ### Snap Toolbar ###
+        self.snap_toolbar = QtWidgets.QToolBar('Grid Toolbar')
+        # Snap GRID toolbar is always active to facilitate usage of measurements done on GRID
+        self.addToolBar(self.snap_toolbar)
+
+        self.grid_snap_btn = self.snap_toolbar.addAction(QtGui.QIcon('share/grid32.png'), 'Snap to grid')
+        self.grid_gap_x_entry = FCEntry2()
+        self.grid_gap_x_entry.setMaximumWidth(70)
+        self.grid_gap_x_entry.setToolTip("Grid X distance")
+        self.snap_toolbar.addWidget(self.grid_gap_x_entry)
+
+        self.grid_gap_y_entry = FCEntry2()
+        self.grid_gap_y_entry.setMaximumWidth(70)
+        self.grid_gap_y_entry.setToolTip("Grid Y distance")
+        self.snap_toolbar.addWidget(self.grid_gap_y_entry)
+
+        self.grid_space_label = QtWidgets.QLabel("  ")
+        self.snap_toolbar.addWidget(self.grid_space_label)
+        self.grid_gap_link_cb = FCCheckBox()
+        self.grid_gap_link_cb.setToolTip("When active, value on Grid_X\n"
+                                         "is copied to the Grid_Y value.")
+        self.snap_toolbar.addWidget(self.grid_gap_link_cb)
+
+        self.ois_grid = OptionalInputSection(self.grid_gap_link_cb, [self.grid_gap_y_entry], logic=False)
+
+        self.corner_snap_btn = self.snap_toolbar.addAction(QtGui.QIcon('share/corner32.png'), 'Snap to corner')
+
+        self.snap_max_dist_entry = QtWidgets.QLineEdit()
+        self.snap_max_dist_entry.setMaximumWidth(70)
+        self.snap_max_dist_entry.setToolTip("Max. magnet distance")
+        self.snap_toolbar.addWidget(self.snap_max_dist_entry)
+
+        self.grid_snap_btn.setCheckable(True)
+        self.corner_snap_btn.setCheckable(True)
+
+        ################
+        ### Splitter ###
+        ################
+        self.splitter = QtWidgets.QSplitter()
+        self.setCentralWidget(self.splitter)
+
+        ################
+        ### Notebook ###
+        ################
+        self.notebook = QtWidgets.QTabWidget()
+        self.splitter.addWidget(self.notebook)
+
+        ### Project ###
+        self.project_tab = QtWidgets.QWidget()
+        # project_tab.setMinimumWidth(250)  # Hack
+        self.project_tab_layout = QtWidgets.QVBoxLayout(self.project_tab)
+        self.project_tab_layout.setContentsMargins(2, 2, 2, 2)
+        self.notebook.addTab(self.project_tab, "Project")
+
+        ### Selected ###
+        self.selected_tab = QtWidgets.QWidget()
+        self.selected_tab_layout = QtWidgets.QVBoxLayout(self.selected_tab)
+        self.selected_tab_layout.setContentsMargins(2, 2, 2, 2)
+        self.selected_scroll_area = VerticalScrollArea()
+        self.selected_tab_layout.addWidget(self.selected_scroll_area)
+        self.notebook.addTab(self.selected_tab, "Selected")
+
+        ### Tool ###
+        self.tool_tab = QtWidgets.QWidget()
+        self.tool_tab_layout = QtWidgets.QVBoxLayout(self.tool_tab)
+        self.tool_tab_layout.setContentsMargins(2, 2, 2, 2)
+        self.notebook.addTab(self.tool_tab, "Tool")
+        self.tool_scroll_area = VerticalScrollArea()
+        self.tool_tab_layout.addWidget(self.tool_scroll_area)
+
+        self.right_widget = QtWidgets.QWidget()
+        self.right_widget.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
+        self.splitter.addWidget(self.right_widget)
+
+        self.right_lay = QtWidgets.QVBoxLayout()
+        self.right_lay.setContentsMargins(0, 0, 0, 0)
+        self.right_widget.setLayout(self.right_lay)
+        self.plot_tab_area = FCTab()
+        self.right_lay.addWidget(self.plot_tab_area)
+        self.plot_tab_area.setTabsClosable(True)
+
+        plot_tab = QtWidgets.QWidget()
+        self.plot_tab_area.addTab(plot_tab, "Plot Area")
+
+        self.right_layout = QtWidgets.QVBoxLayout()
+        self.right_layout.setContentsMargins(2, 2, 2, 2)
+        plot_tab.setLayout(self.right_layout)
+
+        # remove the close button from the Plot Area tab (first tab index = 0) as this one will always be ON
+        self.plot_tab_area.protectTab(0)
+
+        ########################################
+        ### HERE WE BUILD THE PREF. TAB AREA ###
+        ########################################
+        self.preferences_tab = QtWidgets.QWidget()
+        self.pref_tab_layout = QtWidgets.QVBoxLayout(self.preferences_tab)
+        self.pref_tab_layout.setContentsMargins(2, 2, 2, 2)
+
+        self.pref_tab_area = FCTab()
+        self.pref_tab_area.setTabsClosable(False)
+        self.pref_tab_area_tabBar = self.pref_tab_area.tabBar()
+        self.pref_tab_area_tabBar.setStyleSheet("QTabBar::tab{width:80px;}")
+        self.pref_tab_area_tabBar.setExpanding(True)
+        self.pref_tab_layout.addWidget(self.pref_tab_area)
+
+        self.general_tab = QtWidgets.QWidget()
+        self.pref_tab_area.addTab(self.general_tab, "General")
+        self.general_tab_lay = QtWidgets.QVBoxLayout()
+        self.general_tab_lay.setContentsMargins(2, 2, 2, 2)
+        self.general_tab.setLayout(self.general_tab_lay)
+
+        self.hlay1 = QtWidgets.QHBoxLayout()
+        self.general_tab_lay.addLayout(self.hlay1)
+
+        self.options_combo = QtWidgets.QComboBox()
+        self.options_combo.addItem("APP.  DEFAULTS")
+        self.options_combo.addItem("PROJ. OPTIONS ")
+        self.hlay1.addWidget(self.options_combo)
+
+        self.hlay1.addStretch()
+
+        self.general_scroll_area = VerticalScrollArea()
+        self.general_tab_lay.addWidget(self.general_scroll_area)
+
+        self.gerber_tab = QtWidgets.QWidget()
+        self.pref_tab_area.addTab(self.gerber_tab, "GERBER")
+        self.gerber_tab_lay = QtWidgets.QVBoxLayout()
+        self.gerber_tab_lay.setContentsMargins(2, 2, 2, 2)
+        self.gerber_tab.setLayout(self.gerber_tab_lay)
+
+        self.gerber_scroll_area = VerticalScrollArea()
+        self.gerber_tab_lay.addWidget(self.gerber_scroll_area)
+
+        self.excellon_tab = QtWidgets.QWidget()
+        self.pref_tab_area.addTab(self.excellon_tab, "EXCELLON")
+        self.excellon_tab_lay = QtWidgets.QVBoxLayout()
+        self.excellon_tab_lay.setContentsMargins(2, 2, 2, 2)
+        self.excellon_tab.setLayout(self.excellon_tab_lay)
+
+        self.excellon_scroll_area = VerticalScrollArea()
+        self.excellon_tab_lay.addWidget(self.excellon_scroll_area)
+
+        self.geometry_tab = QtWidgets.QWidget()
+        self.pref_tab_area.addTab(self.geometry_tab, "GEOMETRY")
+        self.geometry_tab_lay = QtWidgets.QVBoxLayout()
+        self.geometry_tab_lay.setContentsMargins(2, 2, 2, 2)
+        self.geometry_tab.setLayout(self.geometry_tab_lay)
+
+        self.geometry_scroll_area = VerticalScrollArea()
+        self.geometry_tab_lay.addWidget(self.geometry_scroll_area)
+
+        self.cncjob_tab = QtWidgets.QWidget()
+        self.pref_tab_area.addTab(self.cncjob_tab, "CNC-JOB")
+        self.cncjob_tab_lay = QtWidgets.QVBoxLayout()
+        self.cncjob_tab_lay.setContentsMargins(2, 2, 2, 2)
+        self.cncjob_tab.setLayout(self.cncjob_tab_lay)
+
+        self.cncjob_scroll_area = VerticalScrollArea()
+        self.cncjob_tab_lay.addWidget(self.cncjob_scroll_area)
+
+        self.pref_tab_bottom_layout = QtWidgets.QHBoxLayout()
+        self.pref_tab_bottom_layout.setAlignment(QtCore.Qt.AlignVCenter)
+        self.pref_tab_layout.addLayout(self.pref_tab_bottom_layout)
+
+        self.pref_tab_bottom_layout_1 = QtWidgets.QHBoxLayout()
+        self.pref_tab_bottom_layout_1.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.pref_tab_bottom_layout.addLayout(self.pref_tab_bottom_layout_1)
+
+        self.pref_factory_button = QtWidgets.QPushButton()
+        self.pref_factory_button.setText("Import Factory Def.")
+        self.pref_factory_button.setFixedWidth(110)
+        self.pref_tab_bottom_layout_1.addWidget(self.pref_factory_button)
+
+        self.pref_load_button = QtWidgets.QPushButton()
+        self.pref_load_button.setText("Load User Defaults")
+        self.pref_load_button.setFixedWidth(110)
+        self.pref_tab_bottom_layout_1.addWidget(self.pref_load_button)
+
+        self.pref_tab_bottom_layout_2 = QtWidgets.QHBoxLayout()
+        self.pref_tab_bottom_layout_2.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.pref_tab_bottom_layout.addLayout(self.pref_tab_bottom_layout_2)
+
+        self.pref_save_button = QtWidgets.QPushButton()
+        self.pref_save_button.setText("Save")
+        self.pref_save_button.setFixedWidth(100)
+        self.pref_tab_bottom_layout_2.addWidget(self.pref_save_button)
+
+        ########################################
+        ### HERE WE BUILD THE CONTEXT MENU FOR RMB CLICK ON CANVAS ###
+        ########################################
+        self.popMenu = QtWidgets.QMenu()
+
+        self.cmenu_gridmenu = self.popMenu.addMenu(QtGui.QIcon('share/grid32_menu.png'), "Grids")
+        self.gridmenu_1 = self.cmenu_gridmenu.addAction(QtGui.QIcon('share/grid32_menu.png'), "0.05")
+        self.gridmenu_2 = self.cmenu_gridmenu.addAction(QtGui.QIcon('share/grid32_menu.png'), "0.10")
+        self.gridmenu_3 = self.cmenu_gridmenu.addAction(QtGui.QIcon('share/grid32_menu.png'), "0.20")
+        self.gridmenu_4 = self.cmenu_gridmenu.addAction(QtGui.QIcon('share/grid32_menu.png'), "0.50")
+        self.gridmenu_5 = self.cmenu_gridmenu.addAction(QtGui.QIcon('share/grid32_menu.png'), "1.00")
+
+        self.g_editor_cmenu = self.popMenu.addMenu(QtGui.QIcon('share/draw32.png'), "Geo Editor")
+        self.draw_line = self.g_editor_cmenu.addAction(QtGui.QIcon('share/path32.png'), "Line")
+        self.draw_rect = self.g_editor_cmenu.addAction(QtGui.QIcon('share/rectangle32.png'), "Rectangle")
+        self.draw_cut = self.g_editor_cmenu.addAction(QtGui.QIcon('share/cutpath32.png'), "Cut")
+
+        self.e_editor_cmenu = self.popMenu.addMenu(QtGui.QIcon('share/drill32.png'), "Exc Editor")
+        self.drill = self.e_editor_cmenu.addAction(QtGui.QIcon('share/drill32.png'), "Add Drill")
+        self.drill_array = self.e_editor_cmenu.addAction(QtGui.QIcon('share/addarray32.png'), "Add Drill Array")
+        self.drill_copy = self.e_editor_cmenu.addAction(QtGui.QIcon('share/copy32.png'), "Copy Drill(s)")
+
+        self.cmenu_viewmenu = self.popMenu.addMenu(QtGui.QIcon('share/view64.png'), "View")
+        self.zoomfit = self.cmenu_viewmenu.addAction(QtGui.QIcon('share/zoom_fit32.png'), "Zoom Fit")
+        self.clearplot = self.cmenu_viewmenu.addAction(QtGui.QIcon('share/clear_plot32.png'), "Clear Plot")
+        self.replot = self.cmenu_viewmenu.addAction(QtGui.QIcon('share/replot32.png'), "Replot")
+
+        self.popmenu_properties = self.popMenu.addAction(QtGui.QIcon('share/properties32.png'), "Properties")
+
+
+        ####################################
+        ### Here we build the CNCJob Tab ###
+        ####################################
+        self.cncjob_tab = QtWidgets.QWidget()
+        self.cncjob_tab_layout = QtWidgets.QGridLayout(self.cncjob_tab)
+        self.cncjob_tab_layout.setContentsMargins(2, 2, 2, 2)
+        self.cncjob_tab.setLayout(self.cncjob_tab_layout)
+
+        self.code_editor = QtWidgets.QTextEdit()
+        stylesheet = """
+                        QTextEdit { selection-background-color:yellow;
+                                    selection-color:black;
+                        }
+                     """
+
+        self.code_editor.setStyleSheet(stylesheet)
+
+        self.buttonPreview = QtWidgets.QPushButton('Print Preview')
+        self.buttonPrint = QtWidgets.QPushButton('Print CNC Code')
+        self.buttonFind = QtWidgets.QPushButton('Find in CNC Code')
+        self.buttonFind.setFixedWidth(100)
+        self.buttonPreview.setFixedWidth(100)
+        self.entryFind = FCEntry()
+        self.entryFind.setMaximumWidth(200)
+        self.buttonReplace = QtWidgets.QPushButton('Replace With')
+        self.buttonReplace.setFixedWidth(100)
+        self.entryReplace = FCEntry()
+        self.entryReplace.setMaximumWidth(200)
+        self.sel_all_cb = QtWidgets.QCheckBox('All')
+        self.sel_all_cb.setToolTip(
+            "When checked it will replace all instances in the 'Find' box\n"
+            "with the text in the 'Replace' box.."
+        )
+        self.buttonOpen = QtWidgets.QPushButton('Open CNC Code')
+        self.buttonSave = QtWidgets.QPushButton('Save CNC Code')
+
+        self.cncjob_tab_layout.addWidget(self.code_editor, 0, 0, 1, 5)
+
+        cnc_tab_lay_1 = QtWidgets.QHBoxLayout()
+        cnc_tab_lay_1.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        cnc_tab_lay_1.addWidget(self.buttonFind)
+        cnc_tab_lay_1.addWidget(self.entryFind)
+        cnc_tab_lay_1.addWidget(self.buttonReplace)
+        cnc_tab_lay_1.addWidget(self.entryReplace)
+        cnc_tab_lay_1.addWidget(self.sel_all_cb)
+        self.cncjob_tab_layout.addLayout(cnc_tab_lay_1, 1, 0, 1, 1, QtCore.Qt.AlignLeft)
+
+        cnc_tab_lay_3 = QtWidgets.QHBoxLayout()
+        cnc_tab_lay_3.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        cnc_tab_lay_3.addWidget(self.buttonPreview)
+        cnc_tab_lay_3.addWidget(self.buttonPrint)
+        self.cncjob_tab_layout.addLayout(cnc_tab_lay_3, 2, 0, 1, 1, QtCore.Qt.AlignLeft)
+
+        cnc_tab_lay_4 = QtWidgets.QHBoxLayout()
+        cnc_tab_lay_4.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        cnc_tab_lay_4.addWidget(self.buttonOpen)
+        cnc_tab_lay_4.addWidget(self.buttonSave)
+        self.cncjob_tab_layout.addLayout(cnc_tab_lay_4, 2, 4, 1, 1)
+
+        ##################################
+        ### Build InfoBar is done here ###
+        ##################################
+        self.infobar = self.statusBar()
+        self.fcinfo = FlatCAMInfoBar()
+        self.infobar.addWidget(self.fcinfo, stretch=1)
+
+        self.rel_position_label = QtWidgets.QLabel(
+            "<b>Dx</b>: 0.0000&nbsp;&nbsp;   <b>Dy</b>: 0.0000&nbsp;&nbsp;&nbsp;&nbsp;")
+        self.rel_position_label.setMinimumWidth(110)
+        self.rel_position_label.setToolTip("Relative neasurement.\nReference is last click position")
+        self.infobar.addWidget(self.rel_position_label)
+
+        self.position_label = QtWidgets.QLabel(
+            "&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: 0.0000&nbsp;&nbsp;   <b>Y</b>: 0.0000")
+        self.position_label.setMinimumWidth(110)
+        self.position_label.setToolTip("Absolute neasurement.\nReference is (X=0, Y= 0) position")
+        self.infobar.addWidget(self.position_label)
+
+        self.units_label = QtWidgets.QLabel("[in]")
+        self.units_label.setMargin(2)
+        self.infobar.addWidget(self.units_label)
+
+        # disabled
+        self.progress_bar = QtWidgets.QProgressBar()
+        self.progress_bar.setMinimum(0)
+        self.progress_bar.setMaximum(100)
+        # infobar.addWidget(self.progress_bar)
+
+        self.activity_view = FlatCAMActivityView()
+        self.infobar.addWidget(self.activity_view)
+
+        self.app_icon = QtGui.QIcon()
+        self.app_icon.addFile('share/flatcam_icon16.png', QtCore.QSize(16, 16))
+        self.app_icon.addFile('share/flatcam_icon24.png', QtCore.QSize(24, 24))
+        self.app_icon.addFile('share/flatcam_icon32.png', QtCore.QSize(32, 32))
+        self.app_icon.addFile('share/flatcam_icon48.png', QtCore.QSize(48, 48))
+        self.app_icon.addFile('share/flatcam_icon128.png', QtCore.QSize(128, 128))
+        self.app_icon.addFile('share/flatcam_icon256.png', QtCore.QSize(256, 256))
+        self.setWindowIcon(self.app_icon)
+
+        self.setGeometry(100, 100, 1024, 650)
+        self.setWindowTitle('FlatCAM %s - %s' % (version, platform.architecture()[0]))
+        self.show()
+
+        self.filename = ""
+        self.setAcceptDrops(True)
+
+    def dragEnterEvent(self, event):
+        if event.mimeData().hasUrls:
+            event.accept()
+        else:
+            event.ignore()
+
+    def dragMoveEvent(self, event):
+        if event.mimeData().hasUrls:
+            event.accept()
+        else:
+            event.ignore()
+
+    def dropEvent(self, event):
+        if event.mimeData().hasUrls:
+            event.setDropAction(QtCore.Qt.CopyAction)
+            event.accept()
+            for url in event.mimeData().urls():
+                self.filename = str(url.toLocalFile())
+
+                if self.filename == "":
+                    self.app.inform.emit("Open cancelled.")
+                else:
+                    if self.filename.lower().rpartition('.')[-1] in self.app.grb_list:
+                        self.app.worker_task.emit({'fcn': self.app.open_gerber,
+                                                   'params': [self.filename]})
+                    else:
+                        event.ignore()
+
+                    if self.filename.lower().rpartition('.')[-1] in self.app.exc_list:
+                        self.app.worker_task.emit({'fcn': self.app.open_excellon,
+                                                   'params': [self.filename]})
+                    else:
+                        event.ignore()
+
+                    if self.filename.lower().rpartition('.')[-1] in self.app.gcode_list:
+                        self.app.worker_task.emit({'fcn': self.app.open_gcode,
+                                                   'params': [self.filename]})
+                    else:
+                        event.ignore()
+
+                    if self.filename.lower().rpartition('.')[-1] in self.app.svg_list:
+                        object_type = 'geometry'
+                        self.app.worker_task.emit({'fcn': self.app.import_svg,
+                                                   'params': [self.filename, object_type, None]})
+
+                    if self.filename.lower().rpartition('.')[-1] in self.app.dxf_list:
+                        object_type = 'geometry'
+                        self.app.worker_task.emit({'fcn': self.app.import_dxf,
+                                                   'params': [self.filename, object_type, None]})
+
+                    if self.filename.lower().rpartition('.')[-1] in self.app.prj_list:
+                        # self.app.open_project() is not Thread Safe
+                        self.app.open_project(self.filename)
+                    else:
+                        event.ignore()
+        else:
+            event.ignore()
+
+    def closeEvent(self, event):
+        grect = self.geometry()
+
+        # self.splitter.sizes()[0] is actually the size of the "notebook"
+        self.geom_update.emit(grect.x(), grect.y(), grect.width(), grect.height(), self.splitter.sizes()[0])
+        self.final_save.emit()
+
+        QtWidgets.qApp.quit()
+
+
+class GeneralPreferencesUI(QtWidgets.QWidget):
+    def __init__(self, parent=None):
+        QtWidgets.QWidget.__init__(self, parent=parent)
+        self.layout = QtWidgets.QVBoxLayout()
+        self.setLayout(self.layout)
+
+        self.general_group = GeneralPrefGroupUI()
+        self.general_group.setFixedWidth(260)
+        self.layout.addWidget(self.general_group)
+
+
+class GerberPreferencesUI(QtWidgets.QWidget):
+
+    def __init__(self, parent=None):
+        QtWidgets.QWidget.__init__(self, parent=parent)
+        self.layout = QtWidgets.QVBoxLayout()
+        self.setLayout(self.layout)
+
+        self.gerber_group = GerberPrefGroupUI()
+        self.gerber_group.setFixedWidth(260)
+        self.layout.addWidget(self.gerber_group)
+
+
+class ExcellonPreferencesUI(QtWidgets.QWidget):
+
+    def __init__(self, parent=None):
+        QtWidgets.QWidget.__init__(self, parent=parent)
+        self.layout = QtWidgets.QVBoxLayout()
+        self.setLayout(self.layout)
+
+        self.excellon_group = ExcellonPrefGroupUI()
+        self.excellon_group.setFixedWidth(260)
+        self.layout.addWidget(self.excellon_group)
+
+
+class GeometryPreferencesUI(QtWidgets.QWidget):
+
+    def __init__(self, parent=None):
+        QtWidgets.QWidget.__init__(self, parent=parent)
+        self.layout = QtWidgets.QVBoxLayout()
+        self.setLayout(self.layout)
+
+        self.geometry_group = GeometryPrefGroupUI()
+        self.geometry_group.setFixedWidth(260)
+        self.layout.addWidget(self.geometry_group)
+
+
+class CNCJobPreferencesUI(QtWidgets.QWidget):
+
+    def __init__(self, parent=None):
+        QtWidgets.QWidget.__init__(self, parent=parent)
+        self.layout = QtWidgets.QVBoxLayout()
+        self.setLayout(self.layout)
+
+        self.cncjob_group = CNCJobPrefGroupUI()
+        self.cncjob_group.setFixedWidth(260)
+        self.layout.addWidget(self.cncjob_group)
+
+
+class OptionsGroupUI(QtWidgets.QGroupBox):
+    def __init__(self, title, parent=None):
+        # QtGui.QGroupBox.__init__(self, title, parent=parent)
+        super(OptionsGroupUI, self).__init__()
+        self.setStyleSheet("""
+        QGroupBox
+        {
+            font-size: 16px;
+            font-weight: bold;
+        }
+        """)
+
+        self.layout = QtWidgets.QVBoxLayout()
+        self.setLayout(self.layout)
+
+
+class GeneralPrefGroupUI(OptionsGroupUI):
+    def __init__(self, parent=None):
+        super(GeneralPrefGroupUI, self).__init__(self)
+
+        self.setTitle(str("Global Preferences"))
+
+        # Create a form layout for the Application general settings
+        self.form_box = QtWidgets.QFormLayout()
+
+        # Units for FlatCAM
+        self.unitslabel = QtWidgets.QLabel('<b>Units:</b>')
+        self.unitslabel.setToolTip("Those are units in which FlatCAM works.")
+        self.units_radio = RadioSet([{'label': 'IN', 'value': 'IN'},
+                                     {'label': 'MM', 'value': 'MM'}])
+
+        # Shell StartUp CB
+        self.shell_startup_label = QtWidgets.QLabel('Shell at StartUp:')
+        self.shell_startup_label.setToolTip(
+            "Check this box if you want the shell to\n"
+            "start automatically at startup."
+        )
+        self.shell_startup_cb = FCCheckBox(label='')
+        self.shell_startup_cb.setToolTip(
+            "Check this box if you want the shell to\n"
+            "start automatically at startup."
+        )
+
+        # Grid X Entry
+        self.gridx_label = QtWidgets.QLabel('Grid X value:')
+        self.gridx_label.setToolTip(
+            "This is the Grid value on X axis\n"
+        )
+        self.gridx_entry = LengthEntry()
+
+        # Grid Y Entry
+        self.gridy_label = QtWidgets.QLabel('Grid Y value:')
+        self.gridy_label.setToolTip(
+            "This is the Grid value on Y axis\n"
+        )
+        self.gridy_entry = LengthEntry()
+
+        # Select mouse pan button
+        self.panbuttonlabel = QtWidgets.QLabel('<b>Pan Button:</b>')
+        self.panbuttonlabel.setToolTip("Select the mouse button to use for panning.")
+        self.pan_button_radio = RadioSet([{'label': 'Middle But.', 'value': '3'},
+                                     {'label': 'Right But.', 'value': '2'}])
+
+        # Multiple Selection Modifier Key
+        self.mselectlabel = QtWidgets.QLabel('<b>Multiple Sel:</b>')
+        self.mselectlabel.setToolTip("Select the key used for multiple selection.")
+        self.mselect_radio = RadioSet([{'label': 'CTRL', 'value': 'Control'},
+                                     {'label': 'SHIFT', 'value': 'Shift'}])
+
+        # # Mouse panning with "Space" key, CB
+        # self.pan_with_space_label = QtWidgets.QLabel('Pan w/ Space:')
+        # self.pan_with_space_label.setToolTip(
+        #     "Check this box if you want to pan when mouse is moved,\n"
+        #     "and key 'Space' is pressed."
+        # )
+        # self.pan_with_space_cb = FCCheckBox(label='')
+        # self.pan_with_space_cb.setToolTip(
+        #     "Check this box if you want to pan when mouse is moved,\n"
+        #     "and key 'Space' is pressed."
+        # )
+
+        # Workspace
+        self.workspace_lbl = QtWidgets.QLabel('Workspace:')
+        self.workspace_lbl.setToolTip(
+            "Draw a delimiting rectangle on canvas.\n"
+            "The purpose is to illustrate the limits for our work."
+        )
+        self.workspace_type_lbl = QtWidgets.QLabel('Wk. format:')
+        self.workspace_type_lbl.setToolTip(
+            "Select the type of rectangle to be used on canvas,\n"
+            "as valid workspace."
+        )
+        self.workspace_cb = FCCheckBox()
+        self.wk_cb = FCComboBox()
+        self.wk_cb.addItem('A4P')
+        self.wk_cb.addItem('A4L')
+        self.wk_cb.addItem('A3P')
+        self.wk_cb.addItem('A3L')
+
+        self.wks = OptionalInputSection(self.workspace_cb, [self.workspace_type_lbl, self.wk_cb])
+
+        # Plot Fill Color
+        self.pf_color_label = QtWidgets.QLabel('Plot Fill:')
+        self.pf_color_label.setToolTip(
+            "Set the fill color for plotted objects.\n"
+            "First 6 digits are the color and the last 2\n"
+            "digits are for alpha (transparency) level."
+        )
+        self.pf_color_entry = FCEntry()
+        self.pf_color_button = QtWidgets.QPushButton()
+        self.pf_color_button.setFixedSize(15, 15)
+
+        self.form_box_child_1 = QtWidgets.QHBoxLayout()
+        self.form_box_child_1.addWidget(self.pf_color_entry)
+        self.form_box_child_1.addWidget(self.pf_color_button)
+        self.form_box_child_1.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+
+        # Plot Fill Transparency Level
+        self.pf_alpha_label = QtWidgets.QLabel('Alpha Level:')
+        self.pf_alpha_label.setToolTip(
+            "Set the fill transparency for plotted objects."
+        )
+        self.pf_color_alpha_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
+        self.pf_color_alpha_slider.setMinimum(0)
+        self.pf_color_alpha_slider.setMaximum(255)
+        self.pf_color_alpha_slider.setSingleStep(1)
+
+        self.pf_color_alpha_spinner = FCSpinner()
+        self.pf_color_alpha_spinner.setFixedWidth(70)
+        self.pf_color_alpha_spinner.setMinimum(0)
+        self.pf_color_alpha_spinner.setMaximum(255)
+
+        self.form_box_child_2 = QtWidgets.QHBoxLayout()
+        self.form_box_child_2.addWidget(self.pf_color_alpha_slider)
+        self.form_box_child_2.addWidget(self.pf_color_alpha_spinner)
+
+        # Plot Line Color
+        self.pl_color_label = QtWidgets.QLabel('Plot Line:')
+        self.pl_color_label.setToolTip(
+            "Set the line color for plotted objects."
+        )
+        self.pl_color_entry = FCEntry()
+        self.pl_color_button = QtWidgets.QPushButton()
+        self.pl_color_button.setFixedSize(15, 15)
+
+        self.form_box_child_3 = QtWidgets.QHBoxLayout()
+        self.form_box_child_3.addWidget(self.pl_color_entry)
+        self.form_box_child_3.addWidget(self.pl_color_button)
+        self.form_box_child_3.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+
+        # Plot Selection (left - right) Fill Color
+        self.sf_color_label = QtWidgets.QLabel('Sel. Fill:')
+        self.sf_color_label.setToolTip(
+            "Set the fill color for the selection box\n"
+            "in case that the selection is done from left to right.\n"
+            "First 6 digits are the color and the last 2\n"
+            "digits are for alpha (transparency) level."
+        )
+        self.sf_color_entry = FCEntry()
+        self.sf_color_button = QtWidgets.QPushButton()
+        self.sf_color_button.setFixedSize(15, 15)
+
+        self.form_box_child_4 = QtWidgets.QHBoxLayout()
+        self.form_box_child_4.addWidget(self.sf_color_entry)
+        self.form_box_child_4.addWidget(self.sf_color_button)
+        self.form_box_child_4.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+
+        # Plot Selection (left - right) Fill Transparency Level
+        self.sf_alpha_label = QtWidgets.QLabel('Alpha Level:')
+        self.sf_alpha_label.setToolTip(
+            "Set the fill transparency for the 'left to right' selection box."
+        )
+        self.sf_color_alpha_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
+        self.sf_color_alpha_slider.setMinimum(0)
+        self.sf_color_alpha_slider.setMaximum(255)
+        self.sf_color_alpha_slider.setSingleStep(1)
+
+        self.sf_color_alpha_spinner = FCSpinner()
+        self.sf_color_alpha_spinner.setFixedWidth(70)
+        self.sf_color_alpha_spinner.setMinimum(0)
+        self.sf_color_alpha_spinner.setMaximum(255)
+
+        self.form_box_child_5 = QtWidgets.QHBoxLayout()
+        self.form_box_child_5.addWidget(self.sf_color_alpha_slider)
+        self.form_box_child_5.addWidget(self.sf_color_alpha_spinner)
+
+        # Plot Selection (left - right) Line Color
+        self.sl_color_label = QtWidgets.QLabel('Sel. Line:')
+        self.sl_color_label.setToolTip(
+            "Set the line color for the 'left to right' selection box."
+        )
+        self.sl_color_entry = FCEntry()
+        self.sl_color_button = QtWidgets.QPushButton()
+        self.sl_color_button.setFixedSize(15, 15)
+
+        self.form_box_child_6 = QtWidgets.QHBoxLayout()
+        self.form_box_child_6.addWidget(self.sl_color_entry)
+        self.form_box_child_6.addWidget(self.sl_color_button)
+        self.form_box_child_6.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+
+        # Plot Selection (right - left) Fill Color
+        self.alt_sf_color_label = QtWidgets.QLabel('Sel2. Fill:')
+        self.alt_sf_color_label.setToolTip(
+            "Set the fill color for the selection box\n"
+            "in case that the selection is done from right to left.\n"
+            "First 6 digits are the color and the last 2\n"
+            "digits are for alpha (transparency) level."
+        )
+        self.alt_sf_color_entry = FCEntry()
+        self.alt_sf_color_button = QtWidgets.QPushButton()
+        self.alt_sf_color_button.setFixedSize(15, 15)
+
+        self.form_box_child_7 = QtWidgets.QHBoxLayout()
+        self.form_box_child_7.addWidget(self.alt_sf_color_entry)
+        self.form_box_child_7.addWidget(self.alt_sf_color_button)
+        self.form_box_child_7.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+
+        # Plot Selection (right - left) Fill Transparency Level
+        self.alt_sf_alpha_label = QtWidgets.QLabel('Alpha Level:')
+        self.alt_sf_alpha_label.setToolTip(
+            "Set the fill transparency for selection 'right to left' box."
+        )
+        self.alt_sf_color_alpha_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
+        self.alt_sf_color_alpha_slider.setMinimum(0)
+        self.alt_sf_color_alpha_slider.setMaximum(255)
+        self.alt_sf_color_alpha_slider.setSingleStep(1)
+
+        self.alt_sf_color_alpha_spinner = FCSpinner()
+        self.alt_sf_color_alpha_spinner.setFixedWidth(70)
+        self.alt_sf_color_alpha_spinner.setMinimum(0)
+        self.alt_sf_color_alpha_spinner.setMaximum(255)
+
+        self.form_box_child_8 = QtWidgets.QHBoxLayout()
+        self.form_box_child_8.addWidget(self.alt_sf_color_alpha_slider)
+        self.form_box_child_8.addWidget(self.alt_sf_color_alpha_spinner)
+
+        # Plot Selection (right - left) Line Color
+        self.alt_sl_color_label = QtWidgets.QLabel('Sel2. Line:')
+        self.alt_sl_color_label.setToolTip(
+            "Set the line color for the 'right to left' selection box."
+        )
+        self.alt_sl_color_entry = FCEntry()
+        self.alt_sl_color_button = QtWidgets.QPushButton()
+        self.alt_sl_color_button.setFixedSize(15, 15)
+
+        self.form_box_child_9 = QtWidgets.QHBoxLayout()
+        self.form_box_child_9.addWidget(self.alt_sl_color_entry)
+        self.form_box_child_9.addWidget(self.alt_sl_color_button)
+        self.form_box_child_9.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+
+        # Editor Draw Color
+        self.draw_color_label = QtWidgets.QLabel('Editor Draw:')
+        self.alt_sf_color_label.setToolTip(
+            "Set the color for the shape."
+        )
+        self.draw_color_entry = FCEntry()
+        self.draw_color_button = QtWidgets.QPushButton()
+        self.draw_color_button.setFixedSize(15, 15)
+
+        self.form_box_child_10 = QtWidgets.QHBoxLayout()
+        self.form_box_child_10.addWidget(self.draw_color_entry)
+        self.form_box_child_10.addWidget(self.draw_color_button)
+        self.form_box_child_10.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+
+        # Editor Draw Selection Color
+        self.sel_draw_color_label = QtWidgets.QLabel('Editor Draw Sel.:')
+        self.sel_draw_color_label.setToolTip(
+            "Set the color of the shape when selected."
+        )
+        self.sel_draw_color_entry = FCEntry()
+        self.sel_draw_color_button = QtWidgets.QPushButton()
+        self.sel_draw_color_button.setFixedSize(15, 15)
+
+        self.form_box_child_11 = QtWidgets.QHBoxLayout()
+        self.form_box_child_11.addWidget(self.sel_draw_color_entry)
+        self.form_box_child_11.addWidget(self.sel_draw_color_button)
+        self.form_box_child_11.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+
+        # Just to add empty rows
+        self.spacelabel = QtWidgets.QLabel('')
+
+        # Add (label - input field) pair to the QFormLayout
+        self.form_box.addRow(self.unitslabel, self.units_radio)
+        self.form_box.addRow(self.spacelabel, self.spacelabel)
+        self.form_box.addRow(self.shell_startup_label, self.shell_startup_cb)
+        self.form_box.addRow(self.gridx_label, self.gridx_entry)
+        self.form_box.addRow(self.gridy_label, self.gridy_entry)
+        self.form_box.addRow(self.panbuttonlabel, self.pan_button_radio)
+        self.form_box.addRow(self.mselectlabel, self.mselect_radio)
+        # self.form_box.addRow(self.pan_with_space_label, self.pan_with_space_cb)
+        self.form_box.addRow(self.workspace_lbl, self.workspace_cb)
+        self.form_box.addRow(self.workspace_type_lbl, self.wk_cb)
+        self.form_box.addRow(self.spacelabel, self.spacelabel)
+        self.form_box.addRow(self.pf_color_label, self.form_box_child_1)
+        self.form_box.addRow(self.pf_alpha_label, self.form_box_child_2)
+        self.form_box.addRow(self.pl_color_label, self.form_box_child_3)
+        self.form_box.addRow(self.sf_color_label, self.form_box_child_4)
+        self.form_box.addRow(self.sf_alpha_label, self.form_box_child_5)
+        self.form_box.addRow(self.sl_color_label, self.form_box_child_6)
+        self.form_box.addRow(self.alt_sf_color_label, self.form_box_child_7)
+        self.form_box.addRow(self.alt_sf_alpha_label, self.form_box_child_8)
+        self.form_box.addRow(self.alt_sl_color_label, self.form_box_child_9)
+        self.form_box.addRow(self.draw_color_label, self.form_box_child_10)
+        self.form_box.addRow(self.sel_draw_color_label, self.form_box_child_11)
+
+        # Add the QFormLayout that holds the Application general defaults
+        # to the main layout of this TAB
+        self.layout.addLayout(self.form_box)
+
+
+class GerberPrefGroupUI(OptionsGroupUI):
+    def __init__(self, parent=None):
+        # OptionsGroupUI.__init__(self, "Gerber Options", parent=parent)
+        super(GerberPrefGroupUI, self).__init__(self)
+
+        self.setTitle(str("Gerber Options"))
+
+        ## Plot options
+        self.plot_options_label = QtWidgets.QLabel("<b>Plot Options:</b>")
+        self.layout.addWidget(self.plot_options_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+        # Plot CB
+        self.plot_cb = FCCheckBox(label='Plot')
+        self.plot_options_label.setToolTip(
+            "Plot (show) this object."
+        )
+        grid0.addWidget(self.plot_cb, 0, 0)
+
+        # Solid CB
+        self.solid_cb = FCCheckBox(label='Solid')
+        self.solid_cb.setToolTip(
+            "Solid color polygons."
+        )
+        grid0.addWidget(self.solid_cb, 0, 1)
+
+        # Multicolored CB
+        self.multicolored_cb = FCCheckBox(label='Multicolored')
+        self.multicolored_cb.setToolTip(
+            "Draw polygons in different colors."
+        )
+        grid0.addWidget(self.multicolored_cb, 0, 2)
+
+        # Number of circle steps for circular aperture linear approximation
+        self.circle_steps_label = QtWidgets.QLabel("Circle Steps:")
+        self.circle_steps_label.setToolTip(
+            "The number of circle steps for Gerber \n"
+            "circular aperture linear approximation."
+        )
+        grid0.addWidget(self.circle_steps_label, 1, 0)
+        self.circle_steps_entry = IntEntry()
+        grid0.addWidget(self.circle_steps_entry, 1, 1)
+
+        ## Isolation Routing
+        self.isolation_routing_label = QtWidgets.QLabel("<b>Isolation Routing:</b>")
+        self.isolation_routing_label.setToolTip(
+            "Create a Geometry object with\n"
+            "toolpaths to cut outside polygons."
+        )
+        self.layout.addWidget(self.isolation_routing_label)
+
+        # Cutting Tool Diameter
+        grid1 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid1)
+        tdlabel = QtWidgets.QLabel('Tool dia:')
+        tdlabel.setToolTip(
+            "Diameter of the cutting tool."
+        )
+        grid1.addWidget(tdlabel, 0, 0)
+        self.iso_tool_dia_entry = LengthEntry()
+        grid1.addWidget(self.iso_tool_dia_entry, 0, 1)
+
+        # Nr of passes
+        passlabel = QtWidgets.QLabel('Width (# passes):')
+        passlabel.setToolTip(
+            "Width of the isolation gap in\n"
+            "number (integer) of tool widths."
+        )
+        grid1.addWidget(passlabel, 1, 0)
+        self.iso_width_entry = IntEntry()
+        grid1.addWidget(self.iso_width_entry, 1, 1)
+
+        # Pass overlap
+        overlabel = QtWidgets.QLabel('Pass overlap:')
+        overlabel.setToolTip(
+            "How much (fraction) of the tool width to overlap each tool pass.\n"
+            "Example:\n"
+            "A value here of 0.25 means an overlap of 25% from the tool diameter found above."
+        )
+        grid1.addWidget(overlabel, 2, 0)
+        self.iso_overlap_entry = FloatEntry()
+        grid1.addWidget(self.iso_overlap_entry, 2, 1)
+
+        milling_type_label = QtWidgets.QLabel('Milling Type:')
+        milling_type_label.setToolTip(
+            "Milling type:\n"
+            "- climb / best for precision milling and to reduce tool usage\n"
+            "- conventional / useful when there is no backlash compensation"
+        )
+        grid1.addWidget(milling_type_label, 3, 0)
+        self.milling_type_radio = RadioSet([{'label': 'Climb', 'value': 'cl'},
+                                            {'label': 'Conv.', 'value': 'cv'}])
+        grid1.addWidget(self.milling_type_radio, 3, 1)
+
+        # Combine passes
+        self.combine_passes_cb = FCCheckBox(label='Combine Passes')
+        self.combine_passes_cb.setToolTip(
+            "Combine all passes into one object"
+        )
+        grid1.addWidget(self.combine_passes_cb, 4, 0)
+
+        ## Clear non-copper regions
+        self.clearcopper_label = QtWidgets.QLabel("<b>Clear non-copper:</b>")
+        self.clearcopper_label.setToolTip(
+            "Create a Geometry object with\n"
+            "toolpaths to cut all non-copper regions."
+        )
+        self.layout.addWidget(self.clearcopper_label)
+
+        grid5 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid5)
+        ncctdlabel = QtWidgets.QLabel('Tools dia:')
+        ncctdlabel.setToolTip(
+            "Diameters of the cutting tools, separated by ','"
+        )
+        grid5.addWidget(ncctdlabel, 0, 0)
+        self.ncc_tool_dia_entry = FCEntry()
+        grid5.addWidget(self.ncc_tool_dia_entry, 0, 1)
+
+        nccoverlabel = QtWidgets.QLabel('Overlap:')
+        nccoverlabel.setToolTip(
+            "How much (fraction) of the tool width to overlap each tool pass.\n"
+            "Example:\n"
+            "A value here of 0.25 means 25% from the tool diameter found above.\n\n"
+            "Adjust the value starting with lower values\n"
+            "and increasing it if areas that should be cleared are still \n"
+            "not cleared.\n"
+            "Lower values = faster processing, faster execution on PCB.\n"
+            "Higher values = slow processing and slow execution on CNC\n"
+            "due of too many paths."
+        )
+        grid5.addWidget(nccoverlabel, 1, 0)
+        self.ncc_overlap_entry = FloatEntry()
+        grid5.addWidget(self.ncc_overlap_entry, 1, 1)
+
+        nccmarginlabel = QtWidgets.QLabel('Margin:')
+        nccmarginlabel.setToolTip(
+            "Bounding box margin."
+        )
+        grid5.addWidget(nccmarginlabel, 2, 0)
+        self.ncc_margin_entry = FloatEntry()
+        grid5.addWidget(self.ncc_margin_entry, 2, 1)
+
+        # Method
+        methodlabel = QtWidgets.QLabel('Method:')
+        methodlabel.setToolTip(
+            "Algorithm for non-copper clearing:<BR>"
+            "<B>Standard</B>: Fixed step inwards.<BR>"
+            "<B>Seed-based</B>: Outwards from seed.<BR>"
+            "<B>Line-based</B>: Parallel lines."
+        )
+        grid5.addWidget(methodlabel, 3, 0)
+        self.ncc_method_radio = RadioSet([
+            {"label": "Standard", "value": "standard"},
+            {"label": "Seed-based", "value": "seed"},
+            {"label": "Straight lines", "value": "lines"}
+        ], orientation='vertical', stretch=False)
+        grid5.addWidget(self.ncc_method_radio, 3, 1)
+
+        # Connect lines
+        pathconnectlabel = QtWidgets.QLabel("Connect:")
+        pathconnectlabel.setToolTip(
+            "Draw lines between resulting\n"
+            "segments to minimize tool lifts."
+        )
+        grid5.addWidget(pathconnectlabel, 4, 0)
+        self.ncc_connect_cb = FCCheckBox()
+        grid5.addWidget(self.ncc_connect_cb, 4, 1)
+
+        contourlabel = QtWidgets.QLabel("Contour:")
+        contourlabel.setToolTip(
+            "Cut around the perimeter of the polygon\n"
+            "to trim rough edges."
+        )
+        grid5.addWidget(contourlabel, 5, 0)
+        self.ncc_contour_cb = FCCheckBox()
+        grid5.addWidget(self.ncc_contour_cb, 5, 1)
+
+        restlabel = QtWidgets.QLabel("Rest M.:")
+        restlabel.setToolTip(
+            "If checked, use 'rest machining'.\n"
+            "Basically it will clear copper outside PCB features,\n"
+            "using the biggest tool and continue with the next tools,\n"
+            "from bigger to smaller, to clear areas of copper that\n"
+            "could not be cleared by previous tool.\n"
+            "If not checked, use the standard algorithm."
+        )
+        grid5.addWidget(restlabel, 6, 0)
+        self.ncc_rest_cb = FCCheckBox()
+        grid5.addWidget(self.ncc_rest_cb, 6, 1)
+
+        ## Board cuttout
+        self.board_cutout_label = QtWidgets.QLabel("<b>Board cutout:</b>")
+        self.board_cutout_label.setToolTip(
+            "Create toolpaths to cut around\n"
+            "the PCB and separate it from\n"
+            "the original board."
+        )
+        self.layout.addWidget(self.board_cutout_label)
+
+        grid2 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid2)
+        tdclabel = QtWidgets.QLabel('Tool dia:')
+        tdclabel.setToolTip(
+            "Diameter of the cutting tool."
+        )
+        grid2.addWidget(tdclabel, 0, 0)
+        self.cutout_tooldia_entry = LengthEntry()
+        grid2.addWidget(self.cutout_tooldia_entry, 0, 1)
+
+        marginlabel = QtWidgets.QLabel('Margin:')
+        marginlabel.setToolTip(
+            "Distance from objects at which\n"
+            "to draw the cutout."
+        )
+        grid2.addWidget(marginlabel, 1, 0)
+        self.cutout_margin_entry = LengthEntry()
+        grid2.addWidget(self.cutout_margin_entry, 1, 1)
+
+        gaplabel = QtWidgets.QLabel('Gap size:')
+        gaplabel.setToolTip(
+            "Size of the gaps in the toolpath\n"
+            "that will remain to hold the\n"
+            "board in place."
+        )
+        grid2.addWidget(gaplabel, 2, 0)
+        self.cutout_gap_entry = LengthEntry()
+        grid2.addWidget(self.cutout_gap_entry, 2, 1)
+
+        gapslabel = QtWidgets.QLabel('Gaps:')
+        gapslabel.setToolTip(
+            "Where to place the gaps, Top/Bottom\n"
+            "Left/Rigt, or on all 4 sides."
+        )
+        grid2.addWidget(gapslabel, 3, 0)
+        self.gaps_radio = RadioSet([{'label': '2 (T/B)', 'value': 'tb'},
+                                    {'label': '2 (L/R)', 'value': 'lr'},
+                                    {'label': '4', 'value': '4'}])
+        grid2.addWidget(self.gaps_radio, 3, 1)
+
+        ## Non-copper regions
+        self.noncopper_label = QtWidgets.QLabel("<b>Non-copper regions:</b>")
+        self.noncopper_label.setToolTip(
+            "Create polygons covering the\n"
+            "areas without copper on the PCB.\n"
+            "Equivalent to the inverse of this\n"
+            "object. Can be used to remove all\n"
+            "copper from a specified region."
+        )
+        self.layout.addWidget(self.noncopper_label)
+
+        grid3 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid3)
+
+        # Margin
+        bmlabel = QtWidgets.QLabel('Boundary Margin:')
+        bmlabel.setToolTip(
+            "Specify the edge of the PCB\n"
+            "by drawing a box around all\n"
+            "objects with this minimum\n"
+            "distance."
+        )
+        grid3.addWidget(bmlabel, 0, 0)
+        self.noncopper_margin_entry = LengthEntry()
+        grid3.addWidget(self.noncopper_margin_entry, 0, 1)
+
+        # Rounded corners
+        self.noncopper_rounded_cb = FCCheckBox(label="Rounded corners")
+        self.noncopper_rounded_cb.setToolTip(
+            "Creates a Geometry objects with polygons\n"
+            "covering the copper-free areas of the PCB."
+        )
+        grid3.addWidget(self.noncopper_rounded_cb, 1, 0, 1, 2)
+
+        ## Bounding box
+        self.boundingbox_label = QtWidgets.QLabel('<b>Bounding Box:</b>')
+        self.layout.addWidget(self.boundingbox_label)
+
+        grid4 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid4)
+
+        bbmargin = QtWidgets.QLabel('Boundary Margin:')
+        bbmargin.setToolTip(
+            "Distance of the edges of the box\n"
+            "to the nearest polygon."
+        )
+        grid4.addWidget(bbmargin, 0, 0)
+        self.bbmargin_entry = LengthEntry()
+        grid4.addWidget(self.bbmargin_entry, 0, 1)
+
+        self.bbrounded_cb = FCCheckBox(label="Rounded corners")
+        self.bbrounded_cb.setToolTip(
+            "If the bounding box is \n"
+            "to have rounded corners\n"
+            "their radius is equal to\n"
+            "the margin."
+        )
+        grid4.addWidget(self.bbrounded_cb, 1, 0, 1, 2)
+        self.layout.addStretch()
+
+
+class ExcellonPrefGroupUI(OptionsGroupUI):
+
+    def __init__(self, parent=None):
+        # OptionsGroupUI.__init__(self, "Excellon Options", parent=parent)
+        super(ExcellonPrefGroupUI, self).__init__(self)
+
+        self.setTitle(str("Excellon Options"))
+
+        # Plot options
+        self.plot_options_label = QtWidgets.QLabel("<b>Plot Options:</b>")
+        self.layout.addWidget(self.plot_options_label)
+
+        grid1 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid1)
+        self.plot_cb = FCCheckBox(label='Plot')
+        self.plot_cb.setToolTip(
+            "Plot (show) this object."
+        )
+        grid1.addWidget(self.plot_cb, 0, 0)
+        self.solid_cb = FCCheckBox(label='Solid')
+        self.solid_cb.setToolTip(
+            "Solid circles."
+        )
+        grid1.addWidget(self.solid_cb, 0, 1)
+
+        # Excellon format
+        self.excellon_format_label = QtWidgets.QLabel("<b>Excellon Format:</b>")
+        self.excellon_format_label.setToolTip(
+            "The NC drill files, usually named Excellon files\n"
+            "are files that can be found in different formats.\n"
+            "Here we set the format used when the provided\n"
+            "coordinates are not using period.\n"
+            "\n"
+            "Possible presets:\n"
+            "\n"
+            "PROTEUS 3:3 MM LZ\n"
+            "DipTrace 5:2 MM TZ\n"
+            "\n"
+            "EAGLE 3:3 MM TZ\n"
+            "EAGLE 4:3 MM TZ\n"
+            "EAGLE 2:5 INCH TZ\n"
+            "EAGLE 3:5 INCH TZ\n"
+            "\n"
+            "ALTIUM 2:4 INCH LZ\n"
+            "Sprint Layout 2:4 INCH LZ"
+            "\n"
+            "KiCAD 3:5 INCH TZ"
+        )
+        self.layout.addWidget(self.excellon_format_label)
+
+        hlay1 = QtWidgets.QHBoxLayout()
+        self.layout.addLayout(hlay1)
+        self.excellon_format_in_label = QtWidgets.QLabel("INCH")
+        self.excellon_format_in_label.setAlignment(QtCore.Qt.AlignLeft)
+        self.excellon_format_in_label.setToolTip(
+            "Default values for INCH are 2:4")
+        hlay1.addWidget(self.excellon_format_in_label, QtCore.Qt.AlignLeft)
+
+        self.excellon_format_upper_in_entry = IntEntry()
+        self.excellon_format_upper_in_entry.setMaxLength(1)
+        self.excellon_format_upper_in_entry.setAlignment(QtCore.Qt.AlignRight)
+        self.excellon_format_upper_in_entry.setFixedWidth(30)
+        self.excellon_format_upper_in_entry.setToolTip(
+            "This numbers signify the number of digits in\n"
+            "the whole part of Excellon coordinates."
+        )
+        hlay1.addWidget(self.excellon_format_upper_in_entry, QtCore.Qt.AlignLeft)
+
+        excellon_separator_in_label= QtWidgets.QLabel(':')
+        excellon_separator_in_label.setFixedWidth(5)
+        hlay1.addWidget(excellon_separator_in_label, QtCore.Qt.AlignLeft)
+
+        self.excellon_format_lower_in_entry = IntEntry()
+        self.excellon_format_lower_in_entry.setMaxLength(1)
+        self.excellon_format_lower_in_entry.setAlignment(QtCore.Qt.AlignRight)
+        self.excellon_format_lower_in_entry.setFixedWidth(30)
+        self.excellon_format_lower_in_entry.setToolTip(
+            "This numbers signify the number of digits in\n"
+            "the decimal part of Excellon coordinates."
+        )
+        hlay1.addWidget(self.excellon_format_lower_in_entry, QtCore.Qt.AlignLeft)
+        hlay1.addStretch()
+
+        hlay2 = QtWidgets.QHBoxLayout()
+        self.layout.addLayout(hlay2)
+        self.excellon_format_mm_label = QtWidgets.QLabel("METRIC")
+        self.excellon_format_mm_label.setAlignment(QtCore.Qt.AlignLeft)
+        self.excellon_format_mm_label.setToolTip(
+            "Default values for METRIC are 3:3")
+        hlay2.addWidget(self.excellon_format_mm_label, QtCore.Qt.AlignLeft)
+
+        self.excellon_format_upper_mm_entry = IntEntry()
+        self.excellon_format_upper_mm_entry.setMaxLength(1)
+        self.excellon_format_upper_mm_entry.setAlignment(QtCore.Qt.AlignRight)
+        self.excellon_format_upper_mm_entry.setFixedWidth(30)
+        self.excellon_format_upper_mm_entry.setToolTip(
+            "This numbers signify the number of digits in\n"
+            "the whole part of Excellon coordinates."
+        )
+        hlay2.addWidget(self.excellon_format_upper_mm_entry, QtCore.Qt.AlignLeft)
+
+        excellon_separator_mm_label= QtWidgets.QLabel(':')
+        excellon_separator_mm_label.setFixedWidth(5)
+        hlay2.addWidget(excellon_separator_mm_label, QtCore.Qt.AlignLeft)
+
+        self.excellon_format_lower_mm_entry = IntEntry()
+        self.excellon_format_lower_mm_entry.setMaxLength(1)
+        self.excellon_format_lower_mm_entry.setAlignment(QtCore.Qt.AlignRight)
+        self.excellon_format_lower_mm_entry.setFixedWidth(30)
+        self.excellon_format_lower_mm_entry.setToolTip(
+            "This numbers signify the number of digits in\n"
+            "the decimal part of Excellon coordinates."
+        )
+        hlay2.addWidget(self.excellon_format_lower_mm_entry, QtCore.Qt.AlignLeft)
+        hlay2.addStretch()
+
+        hlay3 = QtWidgets.QHBoxLayout()
+        self.layout.addLayout(hlay3)
+
+        self.excellon_zeros_label = QtWidgets.QLabel('Excellon <b>Zeros</b> Type:')
+        self.excellon_zeros_label.setAlignment(QtCore.Qt.AlignLeft)
+        self.excellon_zeros_label.setToolTip(
+            "This sets the type of excellon zeros.\n"
+            "If LZ then Leading Zeros are kept and\n"
+            "Trailing Zeros are removed.\n"
+            "If TZ is checked then Trailing Zeros are kept\n"
+            "and Leading Zeros are removed."
+        )
+        hlay3.addWidget(self.excellon_zeros_label)
+
+        self.excellon_zeros_radio = RadioSet([{'label': 'LZ', 'value': 'L'},
+                                     {'label': 'TZ', 'value': 'T'}])
+        self.excellon_zeros_radio.setToolTip(
+            "This sets the type of excellon zeros.\n"
+            "If LZ then Leading Zeros are kept and\n"
+            "Trailing Zeros are removed.\n"
+            "If TZ is checked then Trailing Zeros are kept\n"
+            "and Leading Zeros are removed."
+        )
+        hlay3.addStretch()
+        hlay3.addWidget(self.excellon_zeros_radio, QtCore.Qt.AlignRight)
+
+        hlay4 = QtWidgets.QHBoxLayout()
+        self.layout.addLayout(hlay4)
+
+        self.excellon_units_label = QtWidgets.QLabel('Excellon <b>Units</b> Type:')
+        self.excellon_units_label.setAlignment(QtCore.Qt.AlignLeft)
+        self.excellon_units_label.setToolTip(
+            "This sets the units of Excellon files.\n"
+            "Some Excellon files don't have an header\n"
+            "therefore this parameter will be used.\n"
+        )
+        hlay4.addWidget(self.excellon_units_label)
+
+        self.excellon_units_radio = RadioSet([{'label': 'INCH', 'value': 'INCH'},
+                                              {'label': 'MM', 'value': 'METRIC'}])
+        self.excellon_units_radio.setToolTip(
+            "This sets the units of Excellon files.\n"
+            "Some Excellon files don't have an header\n"
+            "therefore this parameter will be used.\n"
+        )
+        hlay4.addStretch()
+        hlay4.addWidget(self.excellon_units_radio, QtCore.Qt.AlignRight)
+
+        hlay5 = QtWidgets.QVBoxLayout()
+        self.layout.addLayout(hlay5)
+
+        self.empty_label = QtWidgets.QLabel("")
+        hlay5.addWidget(self.empty_label)
+
+        hlay6 = QtWidgets.QVBoxLayout()
+        self.layout.addLayout(hlay6)
+
+        self.excellon_general_label = QtWidgets.QLabel("<b>Excellon Optimization:</b>")
+        hlay6.addWidget(self.excellon_general_label)
+
+        # Create a form layout for the Excellon general settings
+        form_box_excellon = QtWidgets.QFormLayout()
+        hlay6.addLayout(form_box_excellon)
+
+        self.excellon_optimization_label = QtWidgets.QLabel('Path Optimization:   ')
+        self.excellon_optimization_label.setAlignment(QtCore.Qt.AlignLeft)
+        self.excellon_optimization_label.setToolTip(
+            "This sets the optimization type for the Excellon drill path.\n"
+            "If MH is checked then Google OR-Tools algorithm with MetaHeuristic\n"
+            "Guided Local Path is used. Default search time is 3sec.\n"
+            "Use set_sys excellon_search_time value Tcl Command to set other values.\n"
+            "If Basic is checked then Google OR-Tools Basic algorithm is used.\n"
+            "\n"
+            "If DISABLED, then FlatCAM works in 32bit mode and it uses \n"
+            "Travelling Salesman algorithm for path optimization."
+        )
+
+        self.excellon_optimization_radio = RadioSet([{'label': 'MH', 'value': 'M'},
+                                     {'label': 'Basic', 'value': 'B'}])
+        self.excellon_optimization_radio.setToolTip(
+            "This sets the optimization type for the Excellon drill path.\n"
+            "If MH is checked then Google OR-Tools algorithm with MetaHeuristic\n"
+            "Guided Local Path is used. Default search time is 3sec.\n"
+            "Use set_sys excellon_search_time value Tcl Command to set other values.\n"
+            "If Basic is checked then Google OR-Tools Basic algorithm is used.\n"
+            "\n"
+            "If DISABLED, then FlatCAM works in 32bit mode and it uses \n"
+            "Travelling Salesman algorithm for path optimization."
+        )
+
+        form_box_excellon.addRow(self.excellon_optimization_label, self.excellon_optimization_radio)
+
+        current_platform = platform.architecture()[0]
+        if current_platform == '64bit':
+            self.excellon_optimization_label.setDisabled(False)
+            self.excellon_optimization_radio.setDisabled(False)
+        else:
+            self.excellon_optimization_label.setDisabled(True)
+            self.excellon_optimization_radio.setDisabled(True)
+
+        ## Create CNC Job
+        self.cncjob_label = QtWidgets.QLabel('<b>Create CNC Job</b>')
+        self.cncjob_label.setToolTip(
+            "Create a CNC Job object\n"
+            "for this drill object."
+        )
+        self.layout.addWidget(self.cncjob_label)
+
+        grid2 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid2)
+
+        cutzlabel = QtWidgets.QLabel('Cut Z:')
+        cutzlabel.setToolTip(
+            "Drill depth (negative)\n"
+            "below the copper surface."
+        )
+        grid2.addWidget(cutzlabel, 0, 0)
+        self.cutz_entry = LengthEntry()
+        grid2.addWidget(self.cutz_entry, 0, 1)
+
+        travelzlabel = QtWidgets.QLabel('Travel Z:')
+        travelzlabel.setToolTip(
+            "Tool height when travelling\n"
+            "across the XY plane."
+        )
+        grid2.addWidget(travelzlabel, 1, 0)
+        self.travelz_entry = LengthEntry()
+        grid2.addWidget(self.travelz_entry, 1, 1)
+
+        # Tool change:
+        toolchlabel = QtWidgets.QLabel("Tool change:")
+        toolchlabel.setToolTip(
+            "Include tool-change sequence\n"
+            "in G-Code (Pause for tool change)."
+        )
+        self.toolchange_cb = FCCheckBox()
+        grid2.addWidget(toolchlabel, 2, 0)
+        grid2.addWidget(self.toolchange_cb, 2, 1)
+
+        toolchangezlabel = QtWidgets.QLabel('Toolchange Z:')
+        toolchangezlabel.setToolTip(
+            "Toolchange Z position."
+        )
+        grid2.addWidget(toolchangezlabel, 3, 0)
+        self.toolchangez_entry = LengthEntry()
+        grid2.addWidget(self.toolchangez_entry, 3, 1)
+
+        toolchange_xy_label = QtWidgets.QLabel('Toolchange X,Y:')
+        toolchange_xy_label.setToolTip(
+            "Toolchange X,Y position."
+        )
+        grid2.addWidget(toolchange_xy_label, 4, 0)
+        self.toolchangexy_entry = FCEntry()
+        grid2.addWidget(self.toolchangexy_entry, 4, 1)
+
+        startzlabel = QtWidgets.QLabel('Start move Z:')
+        startzlabel.setToolTip(
+            "Height of the tool just after start.\n"
+            "Delete the value if you don't need this feature."
+        )
+        grid2.addWidget(startzlabel, 5, 0)
+        self.estartz_entry = FloatEntry()
+        grid2.addWidget(self.estartz_entry, 5, 1)
+
+        endzlabel = QtWidgets.QLabel('End move Z:')
+        endzlabel.setToolTip(
+            "Tool Z where user can change drill bit."
+        )
+        grid2.addWidget(endzlabel, 6, 0)
+        self.eendz_entry = LengthEntry()
+        grid2.addWidget(self.eendz_entry, 6, 1)
+
+        frlabel = QtWidgets.QLabel('Feedrate (Plunge):')
+        frlabel.setToolTip(
+            "Tool speed while drilling\n"
+            "(in units per minute)."
+        )
+        grid2.addWidget(frlabel, 7, 0)
+        self.feedrate_entry = LengthEntry()
+        grid2.addWidget(self.feedrate_entry, 7, 1)
+
+        fr_rapid_label = QtWidgets.QLabel('Feedrate Rapids:')
+        fr_rapid_label.setToolTip(
+            "Tool speed while drilling\n"
+            "with rapid move\n"
+            "(in units per minute)."
+        )
+        grid2.addWidget(fr_rapid_label, 8, 0)
+        self.feedrate_rapid_entry = LengthEntry()
+        grid2.addWidget(self.feedrate_rapid_entry, 8, 1)
+
+        # Spindle speed
+        spdlabel = QtWidgets.QLabel('Spindle speed:')
+        spdlabel.setToolTip(
+            "Speed of the spindle\n"
+            "in RPM (optional)"
+        )
+        grid2.addWidget(spdlabel, 9, 0)
+        self.spindlespeed_entry = IntEntry(allow_empty=True)
+        grid2.addWidget(self.spindlespeed_entry, 9, 1)
+
+        # Dwell
+        dwelllabel = QtWidgets.QLabel('Dwell:')
+        dwelllabel.setToolTip(
+            "Pause to allow the spindle to reach its\n"
+            "speed before cutting."
+        )
+        dwelltime = QtWidgets.QLabel('Duration [m-sec.]:')
+        dwelltime.setToolTip(
+            "Number of milliseconds for spindle to dwell."
+        )
+        self.dwell_cb = FCCheckBox()
+        self.dwelltime_entry = FCEntry()
+        grid2.addWidget(dwelllabel, 10, 0)
+        grid2.addWidget(self.dwell_cb, 10, 1)
+        grid2.addWidget(dwelltime, 11, 0)
+        grid2.addWidget(self.dwelltime_entry, 11, 1)
+
+        self.ois_dwell_exc = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
+
+        # postprocessor selection
+        pp_excellon_label = QtWidgets.QLabel("Postprocessor")
+        pp_excellon_label.setToolTip(
+            "The postprocessor file that dictates\n"
+            "gcode output."
+        )
+        grid2.addWidget(pp_excellon_label, 12, 0)
+        self.pp_excellon_name_cb = FCComboBox()
+        self.pp_excellon_name_cb.setFocusPolicy(Qt.StrongFocus)
+        grid2.addWidget(self.pp_excellon_name_cb, 12, 1)
+
+        #### Choose what to use for Gcode creation: Drills, Slots or Both
+        excellon_gcode_type_label = QtWidgets.QLabel('<b>Gcode:    </b>')
+        excellon_gcode_type_label.setToolTip(
+            "Choose what to use for GCode generation:\n"
+            "'Drills', 'Slots' or 'Both'.\n"
+            "When choosing 'Slots' or 'Both', slots will be\n"
+            "converted to drills."
+        )
+        self.excellon_gcode_type_radio = RadioSet([{'label': 'Drills', 'value': 'drills'},
+                                          {'label': 'Slots', 'value': 'slots'},
+                                          {'label': 'Both', 'value': 'both'}])
+        grid2.addWidget(excellon_gcode_type_label, 13, 0)
+        grid2.addWidget(self.excellon_gcode_type_radio, 13, 1)
+
+        #### Milling Holes ####
+        self.mill_hole_label = QtWidgets.QLabel('<b>Mill Holes</b>')
+        self.mill_hole_label.setToolTip(
+            "Create Geometry for milling holes."
+        )
+        self.layout.addWidget(self.mill_hole_label)
+
+        grid3 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid3)
+        tdlabel = QtWidgets.QLabel('Drill Tool dia:')
+        tdlabel.setToolTip(
+            "Diameter of the cutting tool."
+        )
+        grid3.addWidget(tdlabel, 0, 0)
+        self.tooldia_entry = LengthEntry()
+        grid3.addWidget(self.tooldia_entry, 0, 1)
+        stdlabel = QtWidgets.QLabel('Slot Tool dia:')
+        stdlabel.setToolTip(
+            "Diameter of the cutting tool\n"
+            "when milling slots."
+        )
+        grid3.addWidget(stdlabel, 1, 0)
+        self.slot_tooldia_entry = LengthEntry()
+        grid3.addWidget(self.slot_tooldia_entry, 1, 1)
+
+        grid4 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid4)
+
+        # Adding the Excellon Format Defaults Button
+        self.excellon_defaults_button = QtWidgets.QPushButton()
+        self.excellon_defaults_button.setText(str("Defaults"))
+        self.excellon_defaults_button.setFixedWidth(80)
+        grid4.addWidget(self.excellon_defaults_button, 0, 0, QtCore.Qt.AlignRight)
+
+        self.layout.addStretch()
+
+
+class GeometryPrefGroupUI(OptionsGroupUI):
+    def __init__(self, parent=None):
+        # OptionsGroupUI.__init__(self, "Geometry Options", parent=parent)
+        super(GeometryPrefGroupUI, self).__init__(self)
+
+        self.setTitle(str("Geometry Options"))
+
+        ## Plot options
+        self.plot_options_label = QtWidgets.QLabel("<b>Plot Options:</b>")
+        self.layout.addWidget(self.plot_options_label)
+
+        # Plot CB
+        self.plot_cb = FCCheckBox(label='Plot')
+        self.plot_cb.setToolTip(
+            "Plot (show) this object."
+        )
+        self.layout.addWidget(self.plot_cb)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+        # Number of circle steps for circular aperture linear approximation
+        self.circle_steps_label = QtWidgets.QLabel("Circle Steps:")
+        self.circle_steps_label.setToolTip(
+            "The number of circle steps for <b>Geometry</b> \n"
+            "circle and arc shapes linear approximation."
+        )
+        grid0.addWidget(self.circle_steps_label, 1, 0)
+        self.circle_steps_entry = IntEntry()
+        grid0.addWidget(self.circle_steps_entry, 1, 1)
+
+        # Tools
+        self.tools_label = QtWidgets.QLabel("<b>Tools</b>")
+        self.layout.addWidget(self.tools_label)
+
+        grid1 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid1)
+
+        # Tooldia
+        tdlabel = QtWidgets.QLabel('Tool dia:                   ')
+        tdlabel.setToolTip(
+            "The diameter of the cutting\n"
+            "tool (just for display)."
+        )
+        grid1.addWidget(tdlabel, 0, 0)
+        self.cnctooldia_entry = LengthEntry()
+        grid1.addWidget(self.cnctooldia_entry, 0, 1)
+
+        # ------------------------------
+        ## Create CNC Job
+        # ------------------------------
+        self.cncjob_label = QtWidgets.QLabel('<b>Create CNC Job:</b>')
+        self.cncjob_label.setToolTip(
+            "Create a CNC Job object\n"
+            "tracing the contours of this\n"
+            "Geometry object."
+        )
+        self.layout.addWidget(self.cncjob_label)
+
+        grid2 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid2)
+
+        # Cut Z
+        cutzlabel = QtWidgets.QLabel('Cut Z:')
+        cutzlabel.setToolTip(
+            "Cutting depth (negative)\n"
+            "below the copper surface."
+        )
+        grid2.addWidget(cutzlabel, 0, 0)
+        self.cutz_entry = LengthEntry()
+        grid2.addWidget(self.cutz_entry, 0, 1)
+
+        # Multidepth CheckBox
+        self.multidepth_cb = FCCheckBox(label='Multidepth')
+        self.multidepth_cb.setToolTip(
+            "Multidepth usage: True or False."
+        )
+        grid2.addWidget(self.multidepth_cb, 1, 0)
+
+        # Depth/pass
+        dplabel = QtWidgets.QLabel('Depth/Pass:')
+        dplabel.setToolTip(
+            "The depth to cut on each pass,\n"
+            "when multidepth is enabled."
+        )
+
+        grid2.addWidget(dplabel, 2, 0)
+        self.depthperpass_entry = LengthEntry()
+        grid2.addWidget(self.depthperpass_entry, 2, 1)
+
+        self.ois_multidepth = OptionalInputSection(self.multidepth_cb, [self.depthperpass_entry])
+
+        # Travel Z
+        travelzlabel = QtWidgets.QLabel('Travel Z:')
+        travelzlabel.setToolTip(
+            "Height of the tool when\n"
+            "moving without cutting."
+        )
+        grid2.addWidget(travelzlabel, 3, 0)
+        self.travelz_entry = LengthEntry()
+        grid2.addWidget(self.travelz_entry, 3, 1)
+
+        # Tool change:
+        toolchlabel = QtWidgets.QLabel("Tool change:")
+        toolchlabel.setToolTip(
+            "Include tool-change sequence\n"
+            "in G-Code (Pause for tool change)."
+        )
+        self.toolchange_cb = FCCheckBox()
+        grid2.addWidget(toolchlabel, 4, 0)
+        grid2.addWidget(self.toolchange_cb, 4, 1)
+
+        # Toolchange Z
+        toolchangezlabel = QtWidgets.QLabel('Toolchange Z:')
+        toolchangezlabel.setToolTip(
+            "Toolchange Z position."
+        )
+        grid2.addWidget(toolchangezlabel, 5, 0)
+        self.toolchangez_entry = LengthEntry()
+        grid2.addWidget(self.toolchangez_entry, 5, 1)
+
+        # Toolchange X,Y
+        toolchange_xy_label = QtWidgets.QLabel('Toolchange X,Y:')
+        toolchange_xy_label.setToolTip(
+            "Toolchange X,Y position."
+        )
+        grid2.addWidget(toolchange_xy_label, 6, 0)
+        self.toolchangexy_entry = FCEntry()
+        grid2.addWidget(self.toolchangexy_entry, 6, 1)
+
+        # Start move Z
+        startzlabel = QtWidgets.QLabel('Start move Z:')
+        startzlabel.setToolTip(
+            "Height of the tool just\n"
+            "after starting the work.\n"
+            "Delete the value if you don't need this feature."
+        )
+        grid2.addWidget(startzlabel, 7, 0)
+        self.gstartz_entry = FloatEntry()
+        grid2.addWidget(self.gstartz_entry, 7, 1)
+
+        # End move Z
+        endzlabel = QtWidgets.QLabel('End move Z:')
+        endzlabel.setToolTip(
+            "Height of the tool after\n"
+            " the last move."
+        )
+        grid2.addWidget(endzlabel, 8, 0)
+        self.gendz_entry = LengthEntry()
+        grid2.addWidget(self.gendz_entry, 8, 1)
+
+        # Feedrate X-Y
+        frlabel = QtWidgets.QLabel('Feed Rate X-Y:')
+        frlabel.setToolTip(
+            "Cutting speed in the XY\n"
+            "plane in units per minute"
+        )
+        grid2.addWidget(frlabel, 9, 0)
+        self.cncfeedrate_entry = LengthEntry()
+        grid2.addWidget(self.cncfeedrate_entry, 9, 1)
+
+        # Feedrate Z (Plunge)
+        frz_label = QtWidgets.QLabel('Feed Rate Z (Plunge):')
+        frz_label.setToolTip(
+            "Cutting speed in the XY\n"
+            "plane in units per minute"
+        )
+        grid2.addWidget(frz_label, 10, 0)
+        self.cncplunge_entry = LengthEntry()
+        grid2.addWidget(self.cncplunge_entry, 10, 1)
+
+        # Feedrate rapids
+        fr_rapid_label = QtWidgets.QLabel('Feed Rate Rapids:')
+        fr_rapid_label.setToolTip(
+            "Cutting speed in the XY\n"
+            "plane in units per minute"
+        )
+        grid2.addWidget(fr_rapid_label, 11, 0)
+        self.cncfeedrate_rapid_entry = LengthEntry()
+        grid2.addWidget(self.cncfeedrate_rapid_entry, 11, 1)
+
+        # End move extra cut
+        self.extracut_cb = FCCheckBox(label='Cut over 1st pt.')
+        self.extracut_cb.setToolTip(
+            "In order to remove possible\n"
+            "copper leftovers where first cut\n"
+            "meet with last cut, we generate an\n"
+            "extended cut over the first cut section."
+        )
+        grid2.addWidget(self.extracut_cb, 12, 0)
+
+        # Spindle Speed
+        spdlabel = QtWidgets.QLabel('Spindle speed:')
+        spdlabel.setToolTip(
+            "Speed of the spindle\n"
+            "in RPM (optional)"
+        )
+        grid2.addWidget(spdlabel, 13, 0)
+        self.cncspindlespeed_entry = IntEntry(allow_empty=True)
+        grid2.addWidget(self.cncspindlespeed_entry, 13, 1)
+
+        # Dwell
+        self.dwell_cb = FCCheckBox(label='Dwell:')
+        self.dwell_cb.setToolTip(
+            "Pause to allow the spindle to reach its\n"
+            "speed before cutting."
+        )
+        dwelltime = QtWidgets.QLabel('Duration [m-sec.]:')
+        dwelltime.setToolTip(
+            "Number of milliseconds for spindle to dwell."
+        )
+        self.dwelltime_entry = FCEntry()
+        grid2.addWidget(self.dwell_cb, 14, 0)
+        grid2.addWidget(dwelltime, 15, 0)
+        grid2.addWidget(self.dwelltime_entry, 15, 1)
+
+        grid3 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid3)
+
+        self.ois_dwell = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
+
+        # postprocessor selection
+        pp_label = QtWidgets.QLabel("Postprocessor")
+        pp_label.setToolTip(
+            "The postprocessor file that dictates\n"
+            "gcode output."
+        )
+        grid3.addWidget(pp_label)
+        self.pp_geometry_name_cb = FCComboBox()
+        self.pp_geometry_name_cb.setFocusPolicy(Qt.StrongFocus)
+        grid3.addWidget(self.pp_geometry_name_cb)
+
+        # ------------------------------
+        ## Paint area
+        # ------------------------------
+        self.paint_label = QtWidgets.QLabel('<b>Paint Area:</b>')
+        self.paint_label.setToolTip(
+            "Creates tool paths to cover the\n"
+            "whole area of a polygon (remove\n"
+            "all copper). You will be asked\n"
+            "to click on the desired polygon."
+        )
+        self.layout.addWidget(self.paint_label)
+
+        grid4 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid4)
+
+        # Tool dia
+        ptdlabel = QtWidgets.QLabel('Tool dia:')
+        ptdlabel.setToolTip(
+            "Diameter of the tool to\n"
+            "be used in the operation."
+        )
+        grid4.addWidget(ptdlabel, 0, 0)
+
+        self.painttooldia_entry = LengthEntry()
+        grid4.addWidget(self.painttooldia_entry, 0, 1)
+
+        # Overlap
+        ovlabel = QtWidgets.QLabel('Overlap:')
+        ovlabel.setToolTip(
+            "How much (fraction) of the tool\n"
+            "width to overlap each tool pass."
+        )
+        grid4.addWidget(ovlabel, 1, 0)
+        self.paintoverlap_entry = LengthEntry()
+        grid4.addWidget(self.paintoverlap_entry, 1, 1)
+
+        # Margin
+        marginlabel = QtWidgets.QLabel('Margin:')
+        marginlabel.setToolTip(
+            "Distance by which to avoid\n"
+            "the edges of the polygon to\n"
+            "be painted."
+        )
+        grid4.addWidget(marginlabel, 2, 0)
+        self.paintmargin_entry = LengthEntry()
+        grid4.addWidget(self.paintmargin_entry, 2, 1)
+
+        # Method
+        methodlabel = QtWidgets.QLabel('Method:')
+        methodlabel.setToolTip(
+            "Algorithm to paint the polygon:<BR>"
+            "<B>Standard</B>: Fixed step inwards.<BR>"
+            "<B>Seed-based</B>: Outwards from seed."
+        )
+        grid4.addWidget(methodlabel, 3, 0)
+        self.paintmethod_combo = RadioSet([
+            {"label": "Standard", "value": "standard"},
+            {"label": "Seed-based", "value": "seed"},
+            {"label": "Straight lines", "value": "lines"}
+        ], orientation='vertical', stretch=False)
+        grid4.addWidget(self.paintmethod_combo, 3, 1)
+
+        # Connect lines
+        pathconnectlabel = QtWidgets.QLabel("Connect:")
+        pathconnectlabel.setToolTip(
+            "Draw lines between resulting\n"
+            "segments to minimize tool lifts."
+        )
+        grid4.addWidget(pathconnectlabel, 4, 0)
+        self.pathconnect_cb = FCCheckBox()
+        grid4.addWidget(self.pathconnect_cb, 4, 1)
+
+        # Paint contour
+        contourlabel = QtWidgets.QLabel("Contour:")
+        contourlabel.setToolTip(
+            "Cut around the perimeter of the polygon\n"
+            "to trim rough edges."
+        )
+        grid4.addWidget(contourlabel, 5, 0)
+        self.contour_cb = FCCheckBox()
+        grid4.addWidget(self.contour_cb, 5, 1)
+
+        # Polygon selection
+        selectlabel = QtWidgets.QLabel('Selection:')
+        selectlabel.setToolTip(
+            "How to select the polygons to paint."
+        )
+        grid4.addWidget(selectlabel, 6, 0)
+        self.selectmethod_combo = RadioSet([
+            {"label": "Single", "value": "single"},
+            {"label": "All", "value": "all"},
+            # {"label": "Rectangle", "value": "rectangle"}
+        ])
+        grid4.addWidget(self.selectmethod_combo, 6, 1)
+
+        self.layout.addStretch()
+
+
+class CNCJobPrefGroupUI(OptionsGroupUI):
+    def __init__(self, parent=None):
+        # OptionsGroupUI.__init__(self, "CNC Job Options", parent=None)
+        super(CNCJobPrefGroupUI, self).__init__(self)
+
+        self.setTitle(str("CNC Job Options"))
+
+        ## Plot options
+        self.plot_options_label = QtWidgets.QLabel("<b>Plot Options:</b>")
+        self.layout.addWidget(self.plot_options_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+
+        # Plot CB
+        # self.plot_cb = QtWidgets.QCheckBox('Plot')
+        self.plot_cb = FCCheckBox('Plot')
+        self.plot_cb.setToolTip(
+            "Plot (show) this object."
+        )
+        grid0.addWidget(self.plot_cb, 0, 0)
+
+        # Number of circle steps for circular aperture linear approximation
+        self.steps_per_circle_label = QtWidgets.QLabel("Circle Steps:")
+        self.steps_per_circle_label.setToolTip(
+            "The number of circle steps for <b>GCode</b> \n"
+            "circle and arc shapes linear approximation."
+        )
+        grid0.addWidget(self.steps_per_circle_label, 1, 0)
+        self.steps_per_circle_entry = IntEntry()
+        grid0.addWidget(self.steps_per_circle_entry, 1, 1)
+
+        # Tool dia for plot
+        tdlabel = QtWidgets.QLabel('Tool dia:')
+        tdlabel.setToolTip(
+            "Diameter of the tool to be\n"
+            "rendered in the plot."
+        )
+        grid0.addWidget(tdlabel, 2, 0)
+        self.tooldia_entry = LengthEntry()
+        grid0.addWidget(self.tooldia_entry, 2, 1)
+
+        # Number of decimals to use in GCODE coordinates
+        cdeclabel = QtWidgets.QLabel('Coords decimals:')
+        cdeclabel.setToolTip(
+            "The number of decimals to be used for \n"
+            "the X, Y, Z coordinates in CNC code (GCODE, etc.)"
+        )
+        grid0.addWidget(cdeclabel, 3, 0)
+        self.coords_dec_entry = IntEntry()
+        grid0.addWidget(self.coords_dec_entry, 3, 1)
+
+        # Number of decimals to use in GCODE feedrate
+        frdeclabel = QtWidgets.QLabel('Feedrate decimals:')
+        frdeclabel.setToolTip(
+            "The number of decimals to be used for \n"
+            "the feedrate in CNC code (GCODE, etc.)"
+        )
+        grid0.addWidget(frdeclabel, 4, 0)
+        self.fr_dec_entry = IntEntry()
+        grid0.addWidget(self.fr_dec_entry, 4, 1)
+
+        ## Export G-Code
+        self.export_gcode_label = QtWidgets.QLabel("<b>Export G-Code:</b>")
+        self.export_gcode_label.setToolTip(
+            "Export and save G-Code to\n"
+            "make this object to a file."
+        )
+        self.layout.addWidget(self.export_gcode_label)
+
+        # Prepend to G-Code
+        prependlabel = QtWidgets.QLabel('Prepend to G-Code:')
+        prependlabel.setToolTip(
+            "Type here any G-Code commands you would\n"
+            "like to add at the beginning of the G-Code file."
+        )
+        self.layout.addWidget(prependlabel)
+
+        self.prepend_text = FCTextArea()
+        self.layout.addWidget(self.prepend_text)
+
+        # Append text to G-Code
+        appendlabel = QtWidgets.QLabel('Append to G-Code:')
+        appendlabel.setToolTip(
+            "Type here any G-Code commands you would\n"
+            "like to append to the generated file.\n"
+            "I.e.: M2 (End of program)"
+        )
+        self.layout.addWidget(appendlabel)
+
+        self.append_text = FCTextArea()
+        self.layout.addWidget(self.append_text)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+
+
+class FlatCAMActivityView(QtWidgets.QWidget):
+
+    def __init__(self, parent=None):
+        super().__init__(parent=parent)
+
+        self.setMinimumWidth(200)
+
+        self.icon = QtWidgets.QLabel(self)
+        self.icon.setGeometry(0, 0, 16, 12)
+        self.movie = QtGui.QMovie("share/active.gif")
+        self.icon.setMovie(self.movie)
+        # self.movie.start()
+
+        layout = QtWidgets.QHBoxLayout()
+        layout.setContentsMargins(5, 0, 5, 0)
+        layout.setAlignment(QtCore.Qt.AlignLeft)
+        self.setLayout(layout)
+
+        layout.addWidget(self.icon)
+        self.text = QtWidgets.QLabel(self)
+        self.text.setText("Idle.")
+
+        layout.addWidget(self.text)
+
+    def set_idle(self):
+        self.movie.stop()
+        self.text.setText("Idle.")
+
+    def set_busy(self, msg):
+        self.movie.start()
+        self.text.setText(msg)
+
+
+class FlatCAMInfoBar(QtWidgets.QWidget):
+
+    def __init__(self, parent=None):
+        super(FlatCAMInfoBar, self).__init__(parent=parent)
+
+        self.icon = QtWidgets.QLabel(self)
+        self.icon.setGeometry(0, 0, 12, 12)
+        self.pmap = QtGui.QPixmap('share/graylight12.png')
+        self.icon.setPixmap(self.pmap)
+
+        layout = QtWidgets.QHBoxLayout()
+        layout.setContentsMargins(5, 0, 5, 0)
+        self.setLayout(layout)
+
+        layout.addWidget(self.icon)
+
+        self.text = QtWidgets.QLabel(self)
+        self.text.setText("Hello!")
+        self.text.setToolTip("Hello!")
+
+        layout.addWidget(self.text)
+
+        layout.addStretch()
+
+    def set_text_(self, text):
+        self.text.setText(text)
+        self.text.setToolTip(text)
+
+    def set_status(self, text, level="info"):
+        level = str(level)
+        self.pmap.fill()
+        if level == "error" or level == "error_notcl":
+            self.pmap = QtGui.QPixmap('share/redlight12.png')
+        elif level == "success":
+            self.pmap = QtGui.QPixmap('share/greenlight12.png')
+        elif level == "warning" or level == "warning_notcl":
+            self.pmap = QtGui.QPixmap('share/yellowlight12.png')
+        else:
+            self.pmap = QtGui.QPixmap('share/graylight12.png')
+
+        self.icon.setPixmap(self.pmap)
+        self.set_text_(text)
+# end of file

+ 4004 - 0
FlatCAMObj.py

@@ -0,0 +1,4004 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+from io import StringIO
+from PyQt5 import QtCore, QtGui
+from PyQt5.QtCore import Qt
+from copy import copy, deepcopy
+import inspect  # TODO: For debugging only.
+from shapely.geometry.base import JOIN_STYLE
+from datetime import datetime
+
+import FlatCAMApp
+from ObjectUI import *
+from FlatCAMCommon import LoudDict
+from FlatCAMEditor import FlatCAMGeoEditor
+from camlib import *
+from VisPyVisuals import ShapeCollectionVisual
+import itertools
+
+
+# Interrupts plotting process if FlatCAMObj has been deleted
+class ObjectDeleted(Exception):
+    pass
+
+
+class ValidationError(Exception):
+    def __init__(self, message, errors):
+        super().__init__(message)
+
+        self.errors = errors
+
+########################################
+##            FlatCAMObj              ##
+########################################
+class FlatCAMObj(QtCore.QObject):
+    """
+    Base type of objects handled in FlatCAM. These become interactive
+    in the GUI, can be plotted, and their options can be modified
+    by the user in their respective forms.
+    """
+
+    # Instance of the application to which these are related.
+    # The app should set this value.
+    app = None
+
+    def __init__(self, name):
+        """
+        Constructor.
+
+        :param name: Name of the object given by the user.
+        :return: FlatCAMObj
+        """
+        QtCore.QObject.__init__(self)
+
+        # View
+        self.ui = None
+
+        self.options = LoudDict(name=name)
+        self.options.set_change_callback(self.on_options_change)
+
+        self.form_fields = {}
+
+        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.item = None  # Link with project view item
+
+        self.muted_ui = False
+        self.deleted = False
+
+        self._drawing_tolerance = 0.01
+
+        # assert isinstance(self.ui, ObjectUI)
+        # self.ui.name_entry.returnPressed.connect(self.on_name_activate)
+        # 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 __str__(self):
+        return "<FlatCAMObj({:12s}): {:20s}>".format(self.kind, self.options["name"])
+
+    def from_dict(self, d):
+        """
+        This supersedes ``from_dict`` in derived classes. Derived classes
+        must inherit from FlatCAMObj first, then from derivatives of Geometry.
+
+        ``self.options`` is only updated, not overwritten. This ensures that
+        options set by the app do not vanish when reading the objects
+        from a project file.
+
+        :param d: Dictionary with attributes to set.
+        :return: None
+        """
+
+        for attr in self.ser_attrs:
+
+            if attr == 'options':
+                self.options.update(d[attr])
+            else:
+                setattr(self, attr, d[attr])
+
+    def on_options_change(self, key):
+        # Update form on programmatically options change
+        self.set_form_item(key)
+
+        # Set object visibility
+        if key == 'plot':
+            self.visible = self.options['plot']
+
+        # self.emit(QtCore.SIGNAL("optionChanged"), key)
+        self.optionChanged.emit(key)
+
+    def set_ui(self, ui):
+        self.ui = ui
+
+        self.form_fields = {"name": self.ui.name_entry}
+
+        assert isinstance(self.ui, ObjectUI)
+        self.ui.name_entry.returnPressed.connect(self.on_name_activate)
+        self.ui.offset_button.clicked.connect(self.on_offset_button_click)
+        self.ui.scale_button.clicked.connect(self.on_scale_button_click)
+
+        self.ui.offsetvector_entry.returnPressed.connect(self.on_offset_button_click)
+        self.ui.scale_entry.returnPressed.connect(self.on_scale_button_click)
+        # self.ui.skew_button.clicked.connect(self.on_skew_button_click)
+
+    def build_ui(self):
+        """
+        Sets up the UI/form for this object. Show the UI
+        in the App.
+
+        :return: None
+        :rtype: None
+        """
+
+        self.muted_ui = True
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.build_ui()")
+
+        # Remove anything else in the box
+        # box_children = self.app.ui.notebook.selected_contents.get_children()
+        # for child in box_children:
+        #     self.app.ui.notebook.selected_contents.remove(child)
+        # while self.app.ui.selected_layout.count():
+        #     self.app.ui.selected_layout.takeAt(0)
+
+        # Put in the UI
+        # box_selected.pack_start(sw, True, True, 0)
+        # self.app.ui.notebook.selected_contents.add(self.ui)
+        # self.app.ui.selected_layout.addWidget(self.ui)
+        try:
+            self.app.ui.selected_scroll_area.takeWidget()
+        except:
+            self.app.log.debug("Nothing to remove")
+        self.app.ui.selected_scroll_area.setWidget(self.ui)
+
+        self.muted_ui = False
+
+    def on_name_activate(self):
+        old_name = copy(self.options["name"])
+        new_name = self.ui.name_entry.get_value()
+
+        # update the SHELL auto-completer model data
+        try:
+            self.app.myKeywords.remove(old_name)
+            self.app.myKeywords.append(new_name)
+            self.app.shell._edit.set_model_data(self.app.myKeywords)
+        except:
+            log.debug("on_name_activate() --> Could not remove the old object name from auto-completer model list")
+
+        self.options["name"] = self.ui.name_entry.get_value()
+        self.app.inform.emit("[success]Name changed from %s to %s" % (old_name, new_name))
+
+    def on_offset_button_click(self):
+        self.app.report_usage("obj_on_offset_button")
+
+        self.read_form()
+        vect = self.ui.offsetvector_entry.get_value()
+        self.offset(vect)
+        self.plot()
+        self.app.object_changed.emit(self)
+
+    def on_scale_button_click(self):
+        self.app.report_usage("obj_on_scale_button")
+        self.read_form()
+        factor = self.ui.scale_entry.get_value()
+        self.scale(factor)
+        self.plot()
+        self.app.object_changed.emit(self)
+
+    def on_skew_button_click(self):
+        self.app.report_usage("obj_on_skew_button")
+        self.read_form()
+        xangle = self.ui.xangle_entry.get_value()
+        yangle = self.ui.yangle_entry.get_value()
+        self.skew(xangle, yangle)
+        self.plot()
+        self.app.object_changed.emit(self)
+
+    def to_form(self):
+        """
+        Copies options to the UI form.
+
+        :return: None
+        """
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMObj.to_form()")
+        for option in self.options:
+            try:
+                self.set_form_item(option)
+            except:
+                self.app.log.warning("Unexpected error:", sys.exc_info())
+
+    def read_form(self):
+        """
+        Reads form into ``self.options``.
+
+        :return: None
+        :rtype: None
+        """
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.read_form()")
+        for option in self.options:
+            try:
+                self.read_form_item(option)
+            except:
+                self.app.log.warning("Unexpected error:", sys.exc_info())
+
+    def set_form_item(self, option):
+        """
+        Copies the specified option to the UI form.
+
+        :param option: Name of the option (Key in ``self.options``).
+        :type option: str
+        :return: None
+        """
+
+        try:
+            self.form_fields[option].set_value(self.options[option])
+        except KeyError:
+            # self.app.log.warn("Tried to set an option or field that does not exist: %s" % option)
+            pass
+
+    def read_form_item(self, option):
+        """
+        Reads the specified option from the UI form into ``self.options``.
+
+        :param option: Name of the option.
+        :type option: str
+        :return: None
+        """
+
+        try:
+            self.options[option] = self.form_fields[option].get_value()
+        except KeyError:
+            self.app.log.warning("Failed to read option from field: %s" % option)
+
+    def plot(self):
+        """
+        Plot this object (Extend this method to implement the actual plotting).
+        Call this in descendants before doing the plotting.
+
+        :return: Whether to continue plotting or not depending on the "plot" option.
+        :rtype: bool
+        """
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMObj.plot()")
+
+        if self.deleted:
+            return False
+
+        self.clear()
+
+
+        return True
+
+    def serialize(self):
+        """
+        Returns a representation of the object as a dictionary so
+        it can be later exported as JSON. Override this method.
+
+        :return: Dictionary representing the object
+        :rtype: dict
+        """
+        return
+
+    def deserialize(self, obj_dict):
+        """
+        Re-builds an object from its serialized version.
+
+        :param obj_dict: Dictionary representing a FlatCAMObj
+        :type obj_dict: dict
+        :return: None
+        """
+        return
+
+    def add_shape(self, **kwargs):
+        if self.deleted:
+            raise ObjectDeleted()
+        else:
+            key = self.shapes.add(tolerance=self.drawing_tolerance, **kwargs)
+        return key
+
+    @property
+    def visible(self):
+        return self.shapes.visible
+
+    @visible.setter
+    def visible(self, value):
+        self.shapes.visible = value
+
+        # Not all object types has annotations
+        try:
+            self.annotation.visible = value
+        except AttributeError:
+            pass
+
+    @property
+    def drawing_tolerance(self):
+        return self._drawing_tolerance if self.units == 'MM' or not self.units else self._drawing_tolerance / 25.4
+
+    @drawing_tolerance.setter
+    def drawing_tolerance(self, value):
+        self._drawing_tolerance = value if self.units == 'MM' or not self.units else value / 25.4
+
+    def clear(self, update=False):
+        self.shapes.clear(update)
+
+        # Not all object types has annotations
+        try:
+            self.annotation.clear(update)
+        except AttributeError:
+            pass
+
+    def delete(self):
+        # Free resources
+        del self.ui
+        del self.options
+
+        # Set flag
+        self.deleted = True
+
+
+class FlatCAMGerber(FlatCAMObj, Gerber):
+    """
+    Represents Gerber code.
+    """
+    optionChanged = QtCore.pyqtSignal(str)
+    ui_type = GerberObjectUI
+
+    @staticmethod
+    def merge(grb_list, grb_final):
+        """
+        Merges the geometry of objects in geo_list into
+        the geometry of geo_final.
+
+        :param grb_list: List of FlatCAMGerber Objects to join.
+        :param grb_final: Destination FlatCAMGeometry object.
+        :return: None
+        """
+
+        if grb_final.solid_geometry is None:
+            grb_final.solid_geometry = []
+        if type(grb_final.solid_geometry) is not list:
+            grb_final.solid_geometry = [grb_final.solid_geometry]
+
+        for grb in grb_list:
+
+            # Expand lists
+            if type(grb) is list:
+                FlatCAMGerber.merge(grb, grb_final)
+
+            # If not list, just append
+            else:
+                grb_final.solid_geometry.append(grb.solid_geometry)
+
+    def __init__(self, name):
+        Gerber.__init__(self, steps_per_circle=self.app.defaults["gerber_circle_steps"])
+        FlatCAMObj.__init__(self, name)
+
+        self.kind = "gerber"
+
+        # The 'name' is already in self.options from FlatCAMObj
+        # Automatically updates the UI
+        self.options.update({
+            "plot": True,
+            "multicolored": False,
+            "solid": False,
+            "isotooldia": 0.016,
+            "isopasses": 1,
+            "isooverlap": 0.15,
+            "milling_type": "cl",
+            "combine_passes": True,
+            "ncctools": "1.0, 0.5",
+            "nccoverlap": 0.4,
+            "nccmargin": 1,
+            "noncoppermargin": 0.0,
+            "noncopperrounded": False,
+            "bboxmargin": 0.0,
+            "bboxrounded": False
+        })
+
+        # type of isolation: 0 = exteriors, 1 = interiors, 2 = complete isolation (both interiors and exteriors)
+        self.iso_type = 2
+
+        # Attributes to be included in serialization
+        # Always append to it because it carries contents
+        # from predecessors.
+        self.ser_attrs += ['options', 'kind']
+
+        # assert isinstance(self.ui, GerberObjectUI)
+        # self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
+        # self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
+        # self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
+        # self.ui.generate_iso_button.clicked.connect(self.on_iso_button_click)
+        # self.ui.generate_cutout_button.clicked.connect(self.on_generatecutout_button_click)
+        # self.ui.generate_bb_button.clicked.connect(self.on_generatebb_button_click)
+        # self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click)
+
+    def set_ui(self, ui):
+        """
+        Maps options with GUI inputs.
+        Connects GUI events to methods.
+
+        :param ui: GUI object.
+        :type ui: GerberObjectUI
+        :return: None
+        """
+        FlatCAMObj.set_ui(self, ui)
+
+        FlatCAMApp.App.log.debug("FlatCAMGerber.set_ui()")
+
+        self.form_fields.update({
+            "plot": self.ui.plot_cb,
+            "multicolored": self.ui.multicolored_cb,
+            "solid": self.ui.solid_cb,
+            "isotooldia": self.ui.iso_tool_dia_entry,
+            "isopasses": self.ui.iso_width_entry,
+            "isooverlap": self.ui.iso_overlap_entry,
+            "milling_type": self.ui.milling_type_radio,
+            "combine_passes": self.ui.combine_passes_cb,
+            "noncoppermargin": self.ui.noncopper_margin_entry,
+            "noncopperrounded": self.ui.noncopper_rounded_cb,
+            "bboxmargin": self.ui.bbmargin_entry,
+            "bboxrounded": self.ui.bbrounded_cb
+        })
+
+        # Fill form fields only on object create
+        self.to_form()
+
+        assert isinstance(self.ui, GerberObjectUI)
+        self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
+        self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
+        self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
+        self.ui.generate_ext_iso_button.clicked.connect(self.on_ext_iso_button_click)
+        self.ui.generate_int_iso_button.clicked.connect(self.on_int_iso_button_click)
+        self.ui.generate_iso_button.clicked.connect(self.on_iso_button_click)
+        self.ui.generate_ncc_button.clicked.connect(self.app.ncclear_tool.run)
+        self.ui.generate_cutout_button.clicked.connect(self.app.cutout_tool.run)
+        self.ui.generate_bb_button.clicked.connect(self.on_generatebb_button_click)
+        self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click)
+
+    def on_generatenoncopper_button_click(self, *args):
+        self.app.report_usage("gerber_on_generatenoncopper_button")
+
+        self.read_form()
+        name = self.options["name"] + "_noncopper"
+
+        def geo_init(geo_obj, app_obj):
+            assert isinstance(geo_obj, FlatCAMGeometry)
+            bounding_box = self.solid_geometry.envelope.buffer(self.options["noncoppermargin"])
+            if not self.options["noncopperrounded"]:
+                bounding_box = bounding_box.envelope
+            non_copper = bounding_box.difference(self.solid_geometry)
+            geo_obj.solid_geometry = non_copper
+
+        # TODO: Check for None
+        self.app.new_object("geometry", name, geo_init)
+
+    def on_generatebb_button_click(self, *args):
+        self.app.report_usage("gerber_on_generatebb_button")
+        self.read_form()
+        name = self.options["name"] + "_bbox"
+
+        def geo_init(geo_obj, app_obj):
+            assert isinstance(geo_obj, FlatCAMGeometry)
+            # Bounding box with rounded corners
+            bounding_box = self.solid_geometry.envelope.buffer(self.options["bboxmargin"])
+            if not self.options["bboxrounded"]:  # Remove rounded corners
+                bounding_box = bounding_box.envelope
+            geo_obj.solid_geometry = bounding_box
+
+        self.app.new_object("geometry", name, geo_init)
+
+    def on_ext_iso_button_click(self, *args):
+
+        if self.ui.follow_cb.get_value() == True:
+            obj = self.app.collection.get_active()
+            obj.follow()
+            # in the end toggle the visibility of the origin object so we can see the generated Geometry
+            obj.ui.plot_cb.toggle()
+        else:
+            self.app.report_usage("gerber_on_iso_button")
+            self.read_form()
+            self.isolate(iso_type=0)
+
+    def on_int_iso_button_click(self, *args):
+
+        if self.ui.follow_cb.get_value() == True:
+            obj = self.app.collection.get_active()
+            obj.follow()
+            # in the end toggle the visibility of the origin object so we can see the generated Geometry
+            obj.ui.plot_cb.toggle()
+        else:
+            self.app.report_usage("gerber_on_iso_button")
+            self.read_form()
+            self.isolate(iso_type=1)
+
+    def on_iso_button_click(self, *args):
+
+        if self.ui.follow_cb.get_value() == True:
+            obj = self.app.collection.get_active()
+            obj.follow()
+            # in the end toggle the visibility of the origin object so we can see the generated Geometry
+            obj.ui.plot_cb.toggle()
+        else:
+            self.app.report_usage("gerber_on_iso_button")
+            self.read_form()
+            self.isolate()
+
+    def follow(self, outname=None):
+        """
+        Creates a geometry object "following" the gerber paths.
+
+        :return: None
+        """
+
+        # default_name = self.options["name"] + "_follow"
+        # follow_name = outname or default_name
+
+        if outname is None:
+            follow_name = self.options["name"] + "_follow"
+        else:
+            follow_name = outname
+
+        def follow_init(follow_obj, app):
+            # Propagate options
+            follow_obj.options["cnctooldia"] = self.options["isotooldia"]
+            follow_obj.solid_geometry = self.solid_geometry
+
+        # TODO: Do something if this is None. Offer changing name?
+        try:
+            self.app.new_object("geometry", follow_name, follow_init)
+        except Exception as e:
+            return "Operation failed: %s" % str(e)
+
+    def isolate(self, iso_type=None, dia=None, passes=None, overlap=None,
+                outname=None, combine=None, milling_type=None):
+        """
+        Creates an isolation routing geometry object in the project.
+
+        :param iso_type: type of isolation to be done: 0 = exteriors, 1 = interiors and 2 = both
+        :param dia: Tool diameter
+        :param passes: Number of tool widths to cut
+        :param overlap: Overlap between passes in fraction of tool diameter
+        :param outname: Base name of the output object
+        :return: None
+        """
+        if dia is None:
+            dia = self.options["isotooldia"]
+        if passes is None:
+            passes = int(self.options["isopasses"])
+        if overlap is None:
+            overlap = self.options["isooverlap"]
+        if combine is None:
+            combine = self.options["combine_passes"]
+        else:
+            combine = bool(combine)
+        if milling_type is None:
+            milling_type = self.options["milling_type"]
+        if iso_type is None:
+            self.iso_type = 2
+        else:
+            self.iso_type = iso_type
+
+        base_name = self.options["name"] + "_iso"
+        base_name = outname or base_name
+
+        def generate_envelope(offset, invert, envelope_iso_type=2):
+            # isolation_geometry produces an envelope that is going on the left of the geometry
+            # (the copper features). To leave the least amount of burrs on the features
+            # the tool needs to travel on the right side of the features (this is called conventional milling)
+            # the first pass is the one cutting all of the features, so it needs to be reversed
+            # the other passes overlap preceding ones and cut the left over copper. It is better for them
+            # to cut on the right side of the left over copper i.e on the left side of the features.
+            try:
+                geom = self.isolation_geometry(offset, iso_type=envelope_iso_type)
+            except Exception as e:
+                log.debug(str(e))
+
+            if invert:
+                try:
+                    if type(geom) is MultiPolygon:
+                        pl = []
+                        for p in geom:
+                            pl.append(Polygon(p.exterior.coords[::-1], p.interiors))
+                        geom = MultiPolygon(pl)
+                    elif type(geom) is Polygon:
+                        geom = Polygon(geom.exterior.coords[::-1], geom.interiors)
+                except Exception as e:
+                    s = str("Unexpected Geometry")
+            return geom
+
+        if combine:
+
+            if self.iso_type == 0:
+                iso_name = self.options["name"] + "_ext_iso"
+            elif self.iso_type == 1:
+                iso_name = self.options["name"] + "_int_iso"
+            else:
+                iso_name = base_name
+
+            # TODO: This is ugly. Create way to pass data into init function.
+            def iso_init(geo_obj, app_obj):
+                # Propagate options
+                geo_obj.options["cnctooldia"] = self.options["isotooldia"]
+                geo_obj.solid_geometry = []
+                for i in range(passes):
+                    offset = (((2 * i + 1) / 2.0) * dia) - (i * overlap * dia)
+
+                    # if milling type is climb then the move is counter-clockwise around features
+                    if milling_type == 'cl':
+                        # geom = generate_envelope (offset, i == 0)
+                        geom = generate_envelope(offset, 1, envelope_iso_type=self.iso_type)
+                    else:
+                        geom = generate_envelope(offset, 0, envelope_iso_type=self.iso_type)
+                    geo_obj.solid_geometry.append(geom)
+
+                # detect if solid_geometry is empty and this require list flattening which is "heavy"
+                # or just looking in the lists (they are one level depth) and if any is not empty
+                # proceed with object creation, if there are empty and the number of them is the length
+                # of the list then we have an empty solid_geometry which should raise a Custom Exception
+                empty_cnt = 0
+                if not isinstance(geo_obj.solid_geometry, list):
+                    geo_obj.solid_geometry = [geo_obj.solid_geometry]
+
+                for g in geo_obj.solid_geometry:
+                    if g:
+                        app_obj.inform.emit("[success]Isolation geometry created: %s" % geo_obj.options["name"])
+                        break
+                    else:
+                        empty_cnt += 1
+                if empty_cnt == len(geo_obj.solid_geometry):
+                    raise ValidationError("Empty Geometry", None)
+                geo_obj.multigeo = False
+
+            # TODO: Do something if this is None. Offer changing name?
+            self.app.new_object("geometry", iso_name, iso_init)
+        else:
+            for i in range(passes):
+
+                offset = (2 * i + 1) / 2.0 * dia - i * overlap * dia
+                if passes > 1:
+                    if self.iso_type == 0:
+                        iso_name = self.options["name"] + "_ext_iso" + str(i + 1)
+                    elif self.iso_type == 1:
+                        iso_name = self.options["name"] + "_int_iso" + str(i + 1)
+                    else:
+                        iso_name = base_name + str(i + 1)
+                else:
+                    if self.iso_type == 0:
+                        iso_name = self.options["name"] + "_ext_iso"
+                    elif self.iso_type == 1:
+                        iso_name = self.options["name"] + "_int_iso"
+                    else:
+                        iso_name = base_name
+
+                # TODO: This is ugly. Create way to pass data into init function.
+                def iso_init(geo_obj, app_obj):
+                    # Propagate options
+                    geo_obj.options["cnctooldia"] = self.options["isotooldia"]
+
+                    # if milling type is climb then the move is counter-clockwise around features
+                    if milling_type == 'cl':
+                        # geo_obj.solid_geometry = generate_envelope(offset, i == 0)
+                        geo_obj.solid_geometry = generate_envelope(offset, 1, envelope_iso_type=self.iso_type)
+                    else:
+                        geo_obj.solid_geometry = generate_envelope(offset, 0, envelope_iso_type=self.iso_type)
+
+                    # detect if solid_geometry is empty and this require list flattening which is "heavy"
+                    # or just looking in the lists (they are one level depth) and if any is not empty
+                    # proceed with object creation, if there are empty and the number of them is the length
+                    # of the list then we have an empty solid_geometry which should raise a Custom Exception
+                    empty_cnt = 0
+                    if not isinstance(geo_obj.solid_geometry, list):
+                        geo_obj.solid_geometry = [geo_obj.solid_geometry]
+
+                    for g in geo_obj.solid_geometry:
+                        if g:
+                            app_obj.inform.emit("[success]Isolation geometry created: %s" % geo_obj.options["name"])
+                            break
+                        else:
+                            empty_cnt += 1
+                    if empty_cnt == len(geo_obj.solid_geometry):
+                        raise ValidationError("Empty Geometry", None)
+                    geo_obj.multigeo = False
+
+                # TODO: Do something if this is None. Offer changing name?
+                self.app.new_object("geometry", iso_name, iso_init)
+
+    def on_plot_cb_click(self, *args):
+        if self.muted_ui:
+            return
+        self.read_form_item('plot')
+
+    def on_solid_cb_click(self, *args):
+        if self.muted_ui:
+            return
+        self.read_form_item('solid')
+        self.plot()
+
+    def on_multicolored_cb_click(self, *args):
+        if self.muted_ui:
+            return
+        self.read_form_item('multicolored')
+        self.plot()
+
+    def convert_units(self, units):
+        """
+        Converts the units of the object by scaling dimensions in all geometry
+        and options.
+
+        :param units: Units to which to convert the object: "IN" or "MM".
+        :type units: str
+        :return: None
+        :rtype: None
+        """
+
+        factor = Gerber.convert_units(self, units)
+
+        self.options['isotooldia'] *= factor
+        self.options['cutoutmargin'] *= factor
+        self.options['cutoutgapsize'] *= factor
+        self.options['noncoppermargin'] *= factor
+        self.options['bboxmargin'] *= factor
+
+    def plot(self, **kwargs):
+        """
+
+        :param kwargs: color and face_color
+        :return:
+        """
+
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMGerber.plot()")
+
+        # Does all the required setup and returns False
+        # if the 'ptint' option is set to False.
+        if not FlatCAMObj.plot(self):
+            return
+
+        if 'color' in kwargs:
+            color = kwargs['color']
+        else:
+            color = self.app.defaults['global_plot_line']
+        if 'face_color' in kwargs:
+            face_color = kwargs['face_color']
+        else:
+            face_color = self.app.defaults['global_plot_fill']
+
+        geometry = self.solid_geometry
+
+        # Make sure geometry is iterable.
+        try:
+            _ = iter(geometry)
+        except TypeError:
+            geometry = [geometry]
+
+        def random_color():
+            color = np.random.rand(4)
+            color[3] = 1
+            return color
+
+        try:
+            if self.options["solid"]:
+                for g in geometry:
+                    if type(g) == Polygon or type(g) == LineString:
+                        self.add_shape(shape=g, color=color,
+                                       face_color=random_color() if self.options['multicolored']
+                                       else face_color, visible=self.options['plot'])
+                    else:
+                        for el in g:
+                            self.add_shape(shape=el, color=color,
+                                           face_color=random_color() if self.options['multicolored']
+                                           else face_color, visible=self.options['plot'])
+            else:
+                for g in geometry:
+                    if type(g) == Polygon or type(g) == LineString:
+                        self.add_shape(shape=g, color=random_color() if self.options['multicolored'] else 'black',
+                                       visible=self.options['plot'])
+                    else:
+                        for el in g:
+                            self.add_shape(shape=el, color=random_color() if self.options['multicolored'] else 'black',
+                                           visible=self.options['plot'])
+            self.shapes.redraw()
+        except (ObjectDeleted, AttributeError):
+            self.shapes.clear(update=True)
+
+    def serialize(self):
+        return {
+            "options": self.options,
+            "kind": self.kind
+        }
+
+
+class FlatCAMExcellon(FlatCAMObj, Excellon):
+    """
+    Represents Excellon/Drill code.
+    """
+
+    ui_type = ExcellonObjectUI
+    optionChanged = QtCore.pyqtSignal(str)
+
+    def __init__(self, name):
+        Excellon.__init__(self, geo_steps_per_circle=self.app.defaults["geometry_circle_steps"])
+        FlatCAMObj.__init__(self, name)
+
+        self.kind = "excellon"
+
+        self.options.update({
+            "plot": True,
+            "solid": False,
+            "drillz": -0.1,
+            "travelz": 0.1,
+            "feedrate": 5.0,
+            "feedrate_rapid": 5.0,
+            "tooldia": 0.1,
+            "slot_tooldia": 0.1,
+            "toolchange": False,
+            "toolchangez": 1.0,
+            "toolchangexy": "0.0, 0.0",
+            "endz": 2.0,
+            "startz": None,
+            "spindlespeed": None,
+            "dwell": True,
+            "dwelltime": 1000,
+            "ppname_e": 'defaults',
+            "optimization_type": "R",
+            "gcode_type": "drills"
+        })
+
+        # TODO: Document this.
+        self.tool_cbs = {}
+
+        # Attributes to be included in serialization
+        # Always append to it because it carries contents
+        # from predecessors.
+        self.ser_attrs += ['options', 'kind']
+
+        # variable to store the total amount of drills per job
+        self.tot_drill_cnt = 0
+        self.tool_row = 0
+
+        # variable to store the total amount of slots per job
+        self.tot_slot_cnt = 0
+        self.tool_row_slots = 0
+
+
+    @staticmethod
+    def merge(exc_list, exc_final):
+        """
+        Merge Excellon objects found in exc_list parameter into exc_final object.
+        Options are always copied from source .
+
+        Tools are disregarded, what is taken in consideration is the unique drill diameters found as values in the
+        exc_list tools dict's. In the reconstruction section for each unique tool diameter it will be created a
+        tool_name to be used in the final Excellon object, exc_final.
+
+        If only one object is in exc_list parameter then this function will copy that object in the exc_final
+
+        :param exc_list: List or one object of FlatCAMExcellon Objects to join.
+        :param exc_final: Destination FlatCAMExcellon object.
+        :return: None
+        """
+
+        try:
+            flattened_list = list(itertools.chain(*exc_list))
+        except TypeError:
+            flattened_list = exc_list
+
+        # this dict will hold the unique tool diameters found in the exc_list objects as the dict keys and the dict
+        # values will be list of Shapely Points
+        custom_dict = {}
+
+        for exc in flattened_list:
+            # copy options of the current excellon obj to the final excellon obj
+            for option in exc.options:
+                if option is not 'name':
+                    try:
+                        exc_final.options[option] = exc.options[option]
+                    except:
+                        exc.app.log.warning("Failed to copy option.", option)
+
+            for drill in exc.drills:
+                exc_tool_dia = float('%.3f' % exc.tools[drill['tool']]['C'])
+
+                if exc_tool_dia not in custom_dict:
+                    custom_dict[exc_tool_dia] = [drill['point']]
+                else:
+                    custom_dict[exc_tool_dia].append(drill['point'])
+
+                # add the zeros and units to the exc_final object
+            exc_final.zeros = exc.zeros
+            exc_final.units = exc.units
+
+        # variable to make tool_name for the tools
+        current_tool = 0
+
+        # Here we add data to the exc_final object
+        # the tools diameter are now the keys in the drill_dia dict and the values are the Shapely Points
+        for tool_dia in custom_dict:
+            # we create a tool name for each key in the drill_dia dict (the key is a unique drill diameter)
+            current_tool += 1
+
+            tool_name = str(current_tool)
+            spec = {"C": float(tool_dia)}
+            exc_final.tools[tool_name] = spec
+
+            # rebuild the drills list of dict's that belong to the exc_final object
+            for point in custom_dict[tool_dia]:
+                exc_final.drills.append(
+                    {
+                        "point": point,
+                        "tool": str(current_tool)
+                    }
+                )
+
+        # create the geometry for the exc_final object
+        exc_final.create_geometry()
+
+    def build_ui(self):
+        FlatCAMObj.build_ui(self)
+
+        n = len(self.tools)
+        # we have (n+2) rows because there are 'n' tools, each a row, plus the last 2 rows for totals.
+        self.ui.tools_table.setRowCount(n + 2)
+
+        self.tot_drill_cnt = 0
+        self.tot_slot_cnt = 0
+
+        self.tool_row = 0
+
+        sort = []
+        for k, v in list(self.tools.items()):
+            sort.append((k, v.get('C')))
+        sorted_tools = sorted(sort, key=lambda t1: t1[1])
+        tools = [i[0] for i in sorted_tools]
+
+        for tool_no in tools:
+
+            drill_cnt = 0  # variable to store the nr of drills per tool
+            slot_cnt = 0  # variable to store the nr of slots per tool
+
+            # Find no of drills for the current tool
+            for drill in self.drills:
+                if drill['tool'] == tool_no:
+                    drill_cnt += 1
+
+            self.tot_drill_cnt += drill_cnt
+
+            # Find no of slots for the current tool
+            for slot in self.slots:
+                if slot['tool'] == tool_no:
+                    slot_cnt += 1
+
+            self.tot_slot_cnt += slot_cnt
+
+            id = QtWidgets.QTableWidgetItem('%d' % int(tool_no))
+            id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.tools_table.setItem(self.tool_row, 0, id)  # Tool name/id
+
+            # Make sure that the drill diameter when in MM is with no more than 2 decimals
+            # There are no drill bits in MM with more than 3 decimals diameter
+            # For INCH the decimals should be no more than 3. There are no drills under 10mils
+            if self.units == 'MM':
+                dia = QtWidgets.QTableWidgetItem('%.2f' % (self.tools[tool_no]['C']))
+            else:
+                dia = QtWidgets.QTableWidgetItem('%.3f' % (self.tools[tool_no]['C']))
+
+            dia.setFlags(QtCore.Qt.ItemIsEnabled)
+
+            drill_count = QtWidgets.QTableWidgetItem('%d' % drill_cnt)
+            drill_count.setFlags(QtCore.Qt.ItemIsEnabled)
+
+            # if the slot number is zero is better to not clutter the GUI with zero's so we print a space
+            if slot_cnt > 0:
+                slot_count = QtWidgets.QTableWidgetItem('%d' % slot_cnt)
+            else:
+                slot_count = QtWidgets.QTableWidgetItem('')
+            slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
+
+            self.ui.tools_table.setItem(self.tool_row, 1, dia)  # Diameter
+            self.ui.tools_table.setItem(self.tool_row, 2, drill_count)  # Number of drills per tool
+            self.ui.tools_table.setItem(self.tool_row, 3, slot_count)  # Number of drills per tool
+            self.tool_row += 1
+
+        # add a last row with the Total number of drills
+        empty = QtWidgets.QTableWidgetItem('')
+        empty.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+
+        label_tot_drill_count = QtWidgets.QTableWidgetItem('Total Drills')
+        tot_drill_count = QtWidgets.QTableWidgetItem('%d' % self.tot_drill_cnt)
+        label_tot_drill_count.setFlags(QtCore.Qt.ItemIsEnabled)
+        tot_drill_count.setFlags(QtCore.Qt.ItemIsEnabled)
+
+        self.ui.tools_table.setItem(self.tool_row, 0, empty)
+        self.ui.tools_table.setItem(self.tool_row, 1, label_tot_drill_count)
+        self.ui.tools_table.setItem(self.tool_row, 2, tot_drill_count)  # Total number of drills
+
+        font = QtGui.QFont()
+        font.setBold(True)
+        font.setWeight(75)
+
+        for k in [1, 2]:
+            self.ui.tools_table.item(self.tool_row, k).setForeground(QtGui.QColor(127, 0, 255))
+            self.ui.tools_table.item(self.tool_row, k).setFont(font)
+
+        self.tool_row += 1
+
+        # add a last row with the Total number of slots
+        empty_2 = QtWidgets.QTableWidgetItem('')
+        empty_2.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        empty_3 = QtWidgets.QTableWidgetItem('')
+        empty_3.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+
+        label_tot_slot_count = QtWidgets.QTableWidgetItem('Total Slots')
+        tot_slot_count = QtWidgets.QTableWidgetItem('%d' % self.tot_slot_cnt)
+        label_tot_slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
+        tot_slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
+
+        self.ui.tools_table.setItem(self.tool_row, 0, empty_2)
+        self.ui.tools_table.setItem(self.tool_row, 1, label_tot_slot_count)
+        self.ui.tools_table.setItem(self.tool_row, 2, empty_3)
+        self.ui.tools_table.setItem(self.tool_row, 3, tot_slot_count)  # Total number of slots
+
+        for kl in [1, 2, 3]:
+            self.ui.tools_table.item(self.tool_row, kl).setFont(font)
+            self.ui.tools_table.item(self.tool_row, kl).setForeground(QtGui.QColor(0, 70, 255))
+
+        # sort the tool diameter column
+        self.ui.tools_table.sortItems(1)
+        # all the tools are selected by default
+        self.ui.tools_table.selectColumn(0)
+        #
+        self.ui.tools_table.resizeColumnsToContents()
+        self.ui.tools_table.resizeRowsToContents()
+
+        vertical_header = self.ui.tools_table.verticalHeader()
+        # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
+        vertical_header.hide()
+        self.ui.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header = self.ui.tools_table.horizontalHeader()
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+        horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
+        # horizontal_header.setStretchLastSection(True)
+
+        self.ui.tools_table.setSortingEnabled(True)
+
+        self.ui.tools_table.setMinimumHeight(self.ui.tools_table.getHeight())
+        self.ui.tools_table.setMaximumHeight(self.ui.tools_table.getHeight())
+
+        if not self.drills:
+            self.ui.tdlabel.hide()
+            self.ui.tooldia_entry.hide()
+            self.ui.generate_milling_button.hide()
+        else:
+            self.ui.tdlabel.show()
+            self.ui.tooldia_entry.show()
+            self.ui.generate_milling_button.show()
+
+        if not self.slots:
+            self.ui.stdlabel.hide()
+            self.ui.slot_tooldia_entry.hide()
+            self.ui.generate_milling_slots_button.hide()
+        else:
+            self.ui.stdlabel.show()
+            self.ui.slot_tooldia_entry.show()
+            self.ui.generate_milling_slots_button.show()
+
+    def set_ui(self, ui):
+        """
+        Configures the user interface for this object.
+        Connects options to form fields.
+
+        :param ui: User interface object.
+        :type ui: ExcellonObjectUI
+        :return: None
+        """
+        FlatCAMObj.set_ui(self, ui)
+
+        FlatCAMApp.App.log.debug("FlatCAMExcellon.set_ui()")
+
+        self.form_fields.update({
+            "plot": self.ui.plot_cb,
+            "solid": self.ui.solid_cb,
+            "drillz": self.ui.cutz_entry,
+            "travelz": self.ui.travelz_entry,
+            "feedrate": self.ui.feedrate_entry,
+            "feedrate_rapid": self.ui.feedrate_rapid_entry,
+            "tooldia": self.ui.tooldia_entry,
+            "slot_tooldia": self.ui.slot_tooldia_entry,
+            "toolchange": self.ui.toolchange_cb,
+            "toolchangez": self.ui.toolchangez_entry,
+            "spindlespeed": self.ui.spindlespeed_entry,
+            "dwell": self.ui.dwell_cb,
+            "dwelltime": self.ui.dwelltime_entry,
+            "startz": self.ui.estartz_entry,
+            "endz": self.ui.eendz_entry,
+            "ppname_e": self.ui.pp_excellon_name_cb,
+            "gcode_type": self.ui.excellon_gcode_type_radio
+        })
+
+        for name in list(self.app.postprocessors.keys()):
+            self.ui.pp_excellon_name_cb.addItem(name)
+
+        # Fill form fields
+        self.to_form()
+
+        assert isinstance(self.ui, ExcellonObjectUI), \
+            "Expected a ExcellonObjectUI, got %s" % type(self.ui)
+        self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
+        self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
+        self.ui.generate_cnc_button.clicked.connect(self.on_create_cncjob_button_click)
+        self.ui.generate_milling_button.clicked.connect(self.on_generate_milling_button_click)
+        self.ui.generate_milling_slots_button.clicked.connect(self.on_generate_milling_slots_button_click)
+
+    def get_selected_tools_list(self):
+        """
+        Returns the keys to the self.tools dictionary corresponding
+        to the selections on the tool list in the GUI.
+
+        :return: List of tools.
+        :rtype: list
+        """
+
+        return [str(x.text()) for x in self.ui.tools_table.selectedItems()]
+
+    def get_selected_tools_table_items(self):
+        """
+        Returns a list of lists, each list in the list is made out of row elements
+
+        :return: List of table_tools items.
+        :rtype: list
+        """
+        table_tools_items = []
+        for x in self.ui.tools_table.selectedItems():
+            table_tools_items.append([self.ui.tools_table.item(x.row(), column).text()
+                                      for column in range(0, self.ui.tools_table.columnCount())])
+        for item in table_tools_items:
+            item[0] = str(item[0])
+        return table_tools_items
+
+    def export_excellon(self):
+        """
+        Returns two values, first is a boolean , if 1 then the file has slots and second contain the Excellon code
+        :return: has_slots and Excellon_code
+        """
+
+        excellon_code = ''
+        units = self.app.general_options_form.general_group.units_radio.get_value().upper()
+
+        # store here if the file has slots, return 1 if any slots, 0 if only drills
+        has_slots = 0
+
+        # drills processing
+        try:
+            for tool in self.tools:
+                if int(tool) < 10:
+                    excellon_code += 'T0' + str(tool) + '\n'
+                else:
+                    excellon_code += 'T' + str(tool) + '\n'
+
+                for drill in self.drills:
+                    if tool == drill['tool']:
+                        if units == 'MM':
+                            excellon_code += 'X' + '%.3f' % drill['point'].x + 'Y' + '%.3f' % drill['point'].y + '\n'
+                        else:
+                            excellon_code += 'X' + '%.4f' % drill['point'].x + 'Y' + '%.4f' % drill['point'].y + '\n'
+        except Exception as e:
+            log.debug(str(e))
+
+        # slots processing
+        try:
+            if self.slots:
+                has_slots = 1
+                for tool in self.tools:
+                    if int(tool) < 10:
+                        excellon_code += 'T0' + str(tool) + '\n'
+                    else:
+                        excellon_code += 'T' + str(tool) + '\n'
+
+                    for slot in self.slots:
+                        if tool == slot['tool']:
+                            if units == 'MM':
+                                excellon_code += 'G00' + 'X' + '%.3f' % slot['start'].x + 'Y' + \
+                                                 '%.3f' % slot['start'].y + '\n'
+                                excellon_code += 'M15\n'
+                                excellon_code += 'G01' + 'X' + '%.3f' % slot['stop'].x + 'Y' + \
+                                                 '%.3f' % slot['stop'].y + '\n'
+                                excellon_code += 'M16\n'
+                            else:
+                                excellon_code += 'G00' + 'X' + '%.4f' % slot['start'].x + 'Y' + \
+                                                 '%.4f' % slot['start'].y + '\n'
+                                excellon_code += 'M15\n'
+                                excellon_code += 'G01' + 'X' + '%.4f' % slot['stop'].x + 'Y' + \
+                                                 '%.4f' % slot['stop'].y + '\n'
+                                excellon_code += 'M16\n'
+        except Exception as e:
+            log.debug(str(e))
+
+        return has_slots, excellon_code
+
+    def export_excellon_altium(self):
+        """
+        Returns two values, first is a boolean , if 1 then the file has slots and second contain the Excellon code
+        :return: has_slots and Excellon_code
+        """
+
+        excellon_code = ''
+        units = self.app.general_options_form.general_group.units_radio.get_value().upper()
+
+        # store here if the file has slots, return 1 if any slots, 0 if only drills
+        has_slots = 0
+
+        # drills processing
+        try:
+            for tool in self.tools:
+                if int(tool) < 10:
+                    excellon_code += 'T0' + str(tool) + '\n'
+                else:
+                    excellon_code += 'T' + str(tool) + '\n'
+
+                for drill in self.drills:
+                    if tool == drill['tool']:
+                        drill_x = drill['point'].x
+                        drill_y = drill['point'].y
+                        if units == 'MM':
+                            drill_x /= 25.4
+                            drill_y /= 25.4
+                        exc_x_formatted = ('%.4f' % drill_x).replace('.', '')
+                        if drill_x < 10:
+                            exc_x_formatted = '0' + exc_x_formatted
+
+                        exc_y_formatted = ('%.4f' % drill_y).replace('.', '')
+                        if drill_y < 10:
+                            exc_y_formatted = '0' + exc_y_formatted
+
+                        excellon_code += 'X' + exc_x_formatted + 'Y' + exc_y_formatted + '\n'
+        except Exception as e:
+            log.debug(str(e))
+
+        # slots processing
+        try:
+            if self.slots:
+                has_slots = 1
+                for tool in self.tools:
+                    if int(tool) < 10:
+                        excellon_code += 'T0' + str(tool) + '\n'
+                    else:
+                        excellon_code += 'T' + str(tool) + '\n'
+
+                    for slot in self.slots:
+                        if tool == slot['tool']:
+                            start_slot_x = slot['start'].x
+                            start_slot_y = slot['start'].y
+                            stop_slot_x = slot['stop'].x
+                            stop_slot_y = slot['stop'].y
+                            if units == 'MM':
+                                start_slot_x /= 25.4
+                                start_slot_y /= 25.4
+                                stop_slot_x /= 25.4
+                                stop_slot_y /= 25.4
+
+                            start_slot_x_formatted = ('%.4f' % start_slot_x).replace('.', '')
+                            if start_slot_x < 10:
+                                start_slot_x_formatted = '0' + start_slot_x_formatted
+
+                            start_slot_y_formatted = ('%.4f' % start_slot_y).replace('.', '')
+                            if start_slot_y < 10:
+                                start_slot_y_formatted = '0' + start_slot_y_formatted
+
+                            stop_slot_x_formatted = ('%.4f' % stop_slot_x).replace('.', '')
+                            if stop_slot_x < 10:
+                                stop_slot_x_formatted = '0' + stop_slot_x_formatted
+
+                            stop_slot_y_formatted = ('%.4f' % stop_slot_y).replace('.', '')
+                            if stop_slot_y < 10:
+                                stop_slot_y_formatted = '0' + stop_slot_y_formatted
+
+                            excellon_code += 'G00' + 'X' + start_slot_x_formatted + 'Y' + \
+                                             start_slot_y_formatted + '\n'
+                            excellon_code += 'M15\n'
+                            excellon_code += 'G01' + 'X' + stop_slot_x_formatted + 'Y' + \
+                                             stop_slot_y_formatted + '\n'
+                            excellon_code += 'M16\n'
+        except Exception as e:
+            log.debug(str(e))
+
+        return has_slots, excellon_code
+
+    def generate_milling_drills(self, tools=None, outname=None, tooldia=None, use_thread=False):
+        """
+        Note: This method is a good template for generic operations as
+        it takes it's options from parameters or otherwise from the
+        object's options and returns a (success, msg) tuple as feedback
+        for shell operations.
+
+        :return: Success/failure condition tuple (bool, str).
+        :rtype: tuple
+        """
+
+        # Get the tools from the list. These are keys
+        # to self.tools
+        if tools is None:
+            tools = self.get_selected_tools_list()
+
+        if outname is None:
+            outname = self.options["name"] + "_mill"
+
+        if tooldia is None:
+            tooldia = self.options["tooldia"]
+
+        # Sort tools by diameter. items() -> [('name', diameter), ...]
+        # sorted_tools = sorted(list(self.tools.items()), key=lambda tl: tl[1]) # no longer works in Python3
+
+        sort = []
+        for k, v in self.tools.items():
+            sort.append((k, v.get('C')))
+        sorted_tools = sorted(sort, key=lambda t1: t1[1])
+
+        if tools == "all":
+            tools = [i[0] for i in sorted_tools]  # List if ordered tool names.
+            log.debug("Tools 'all' and sorted are: %s" % str(tools))
+
+        if len(tools) == 0:
+            self.app.inform.emit("[error_notcl]Please select one or more tools from the list and try again.")
+            return False, "Error: No tools."
+
+        for tool in tools:
+            if tooldia > self.tools[tool]["C"]:
+                self.app.inform.emit("[error_notcl] Milling tool for DRILLS is larger than hole size. Cancelled.")
+                return False, "Error: Milling tool is larger than hole."
+
+        def geo_init(geo_obj, app_obj):
+            assert isinstance(geo_obj, FlatCAMGeometry), \
+                "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
+            app_obj.progress.emit(20)
+
+            ### Add properties to the object
+
+            # get the tool_table items in a list of row items
+            tool_table_items = self.get_selected_tools_table_items()
+            # insert an information only element in the front
+            tool_table_items.insert(0, ["Tool_nr", "Diameter", "Drills_Nr", "Slots_Nr"])
+
+            geo_obj.options['Tools_in_use'] = tool_table_items
+            geo_obj.options['type'] = 'Excellon Geometry'
+
+            geo_obj.solid_geometry = []
+
+            # in case that the tool used has the same diameter with the hole, and since the maximum resolution
+            # for FlatCAM is 6 decimals,
+            # we add a tenth of the minimum value, meaning 0.0000001, which from our point of view is "almost zero"
+            for hole in self.drills:
+                if hole['tool'] in tools:
+                    buffer_value = self.tools[hole['tool']]["C"] / 2 - tooldia / 2
+                    if buffer_value == 0:
+                        geo_obj.solid_geometry.append(
+                            Point(hole['point']).buffer(0.0000001).exterior)
+                    else:
+                        geo_obj.solid_geometry.append(
+                            Point(hole['point']).buffer(buffer_value).exterior)
+        if use_thread:
+            def geo_thread(app_obj):
+                app_obj.new_object("geometry", outname, geo_init)
+                app_obj.progress.emit(100)
+
+            # Create a promise with the new name
+            self.app.collection.promise(outname)
+
+            # Send to worker
+            self.app.worker_task.emit({'fcn': geo_thread, 'params': [self.app]})
+        else:
+            self.app.new_object("geometry", outname, geo_init)
+
+        return True, ""
+
+    def generate_milling_slots(self, tools=None, outname=None, tooldia=None, use_thread=False):
+        """
+        Note: This method is a good template for generic operations as
+        it takes it's options from parameters or otherwise from the
+        object's options and returns a (success, msg) tuple as feedback
+        for shell operations.
+
+        :return: Success/failure condition tuple (bool, str).
+        :rtype: tuple
+        """
+
+        # Get the tools from the list. These are keys
+        # to self.tools
+        if tools is None:
+            tools = self.get_selected_tools_list()
+
+        if outname is None:
+            outname = self.options["name"] + "_mill"
+
+        if tooldia is None:
+            tooldia = self.options["slot_tooldia"]
+
+        # Sort tools by diameter. items() -> [('name', diameter), ...]
+        # sorted_tools = sorted(list(self.tools.items()), key=lambda tl: tl[1]) # no longer works in Python3
+
+        sort = []
+        for k, v in self.tools.items():
+            sort.append((k, v.get('C')))
+        sorted_tools = sorted(sort, key=lambda t1: t1[1])
+
+        if tools == "all":
+            tools = [i[0] for i in sorted_tools]  # List if ordered tool names.
+            log.debug("Tools 'all' and sorted are: %s" % str(tools))
+
+        if len(tools) == 0:
+            self.app.inform.emit("[error_notcl]Please select one or more tools from the list and try again.")
+            return False, "Error: No tools."
+
+        for tool in tools:
+            if tooldia > self.tools[tool]["C"]:
+                self.app.inform.emit("[error_notcl] Milling tool for SLOTS is larger than hole size. Cancelled.")
+                return False, "Error: Milling tool is larger than hole."
+
+        def geo_init(geo_obj, app_obj):
+            assert isinstance(geo_obj, FlatCAMGeometry), \
+                "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
+            app_obj.progress.emit(20)
+
+            ### Add properties to the object
+
+            # get the tool_table items in a list of row items
+            tool_table_items = self.get_selected_tools_table_items()
+            # insert an information only element in the front
+            tool_table_items.insert(0, ["Tool_nr", "Diameter", "Drills_Nr", "Slots_Nr"])
+
+            geo_obj.options['Tools_in_use'] = tool_table_items
+            geo_obj.options['type'] = 'Excellon Geometry'
+
+            geo_obj.solid_geometry = []
+
+            # in case that the tool used has the same diameter with the hole, and since the maximum resolution
+            # for FlatCAM is 6 decimals,
+            # we add a tenth of the minimum value, meaning 0.0000001, which from our point of view is "almost zero"
+            for slot in self.slots:
+                if slot['tool'] in tools:
+                    buffer_value = self.tools[slot['tool']]["C"] / 2 - tooldia / 2
+                    if buffer_value == 0:
+                        start = slot['start']
+                        stop = slot['stop']
+
+                        lines_string = LineString([start, stop])
+                        poly = lines_string.buffer(0.0000001, self.geo_steps_per_circle).exterior
+                        geo_obj.solid_geometry.append(poly)
+                    else:
+                        start = slot['start']
+                        stop = slot['stop']
+
+                        lines_string = LineString([start, stop])
+                        poly = lines_string.buffer(buffer_value, self.geo_steps_per_circle).exterior
+                        geo_obj.solid_geometry.append(poly)
+
+        if use_thread:
+            def geo_thread(app_obj):
+                app_obj.new_object("geometry", outname + '_slot', geo_init)
+                app_obj.progress.emit(100)
+
+            # Create a promise with the new name
+            self.app.collection.promise(outname)
+
+            # Send to worker
+            self.app.worker_task.emit({'fcn': geo_thread, 'params': [self.app]})
+        else:
+            self.app.new_object("geometry", outname + '_slot', geo_init)
+
+        return True, ""
+
+    def on_generate_milling_button_click(self, *args):
+        self.app.report_usage("excellon_on_create_milling_drills button")
+        self.read_form()
+
+        self.generate_milling_drills(use_thread=False)
+
+    def on_generate_milling_slots_button_click(self, *args):
+        self.app.report_usage("excellon_on_create_milling_slots_button")
+        self.read_form()
+
+        self.generate_milling_slots(use_thread=False)
+
+    def on_create_cncjob_button_click(self, *args):
+        self.app.report_usage("excellon_on_create_cncjob_button")
+        self.read_form()
+
+        # Get the tools from the list
+        tools = self.get_selected_tools_list()
+
+        if len(tools) == 0:
+            self.app.inform.emit("[error_notcl]Please select one or more tools from the list and try again.")
+            return
+
+        job_name = self.options["name"] + "_cnc"
+        pp_excellon_name = self.options["ppname_e"]
+
+        # Object initialization function for app.new_object()
+        def job_init(job_obj, app_obj):
+            assert isinstance(job_obj, FlatCAMCNCjob), \
+                "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj)
+
+            # get the tool_table items in a list of row items
+            tool_table_items = self.get_selected_tools_table_items()
+            # insert an information only element in the front
+            tool_table_items.insert(0, ["Tool_nr", "Diameter", "Drills_Nr", "Slots_Nr"])
+
+            ### Add properties to the object
+
+            job_obj.options['Tools_in_use'] = tool_table_items
+            job_obj.options['type'] = 'Excellon'
+
+            app_obj.progress.emit(20)
+            job_obj.z_cut = self.options["drillz"]
+            job_obj.z_move = self.options["travelz"]
+            job_obj.feedrate = self.options["feedrate"]
+            job_obj.feedrate_rapid = self.options["feedrate_rapid"]
+            job_obj.spindlespeed = self.options["spindlespeed"]
+            job_obj.dwell = self.options["dwell"]
+            job_obj.dwelltime = self.options["dwelltime"]
+            job_obj.pp_excellon_name = pp_excellon_name
+            job_obj.toolchange_xy = "excellon"
+            job_obj.coords_decimals = int(self.app.defaults["cncjob_coords_decimals"])
+            job_obj.fr_decimals = int(self.app.defaults["cncjob_fr_decimals"])
+
+            # There could be more than one drill size...
+            # job_obj.tooldia =   # TODO: duplicate variable!
+            # job_obj.options["tooldia"] =
+
+            tools_csv = ','.join(tools)
+            job_obj.generate_from_excellon_by_tool(self, tools_csv,
+                                                   drillz=self.options['drillz'],
+                                                   toolchange=self.options["toolchange"],
+                                                   toolchangez=self.options["toolchangez"],
+                                                   toolchangexy=self.options["toolchangexy"],
+                                                   startz=self.options["startz"],
+                                                   endz=self.options["endz"],
+                                                   excellon_optimization_type=self.options["optimization_type"])
+
+            app_obj.progress.emit(50)
+            job_obj.gcode_parse()
+
+            app_obj.progress.emit(60)
+            job_obj.create_geometry()
+
+            app_obj.progress.emit(80)
+
+        # To be run in separate thread
+        def job_thread(app_obj):
+            with self.app.proc_container.new("Generating CNC Code"):
+                app_obj.new_object("cncjob", job_name, job_init)
+                app_obj.progress.emit(100)
+
+        # Create promise for the new name.
+        self.app.collection.promise(job_name)
+
+        # Send to worker
+        # self.app.worker.add_task(job_thread, [self.app])
+        self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
+
+    def on_plot_cb_click(self, *args):
+        if self.muted_ui:
+            return
+        self.read_form_item('plot')
+
+    def on_solid_cb_click(self, *args):
+        if self.muted_ui:
+            return
+        self.read_form_item('solid')
+        self.plot()
+
+    def convert_units(self, units):
+        factor = Excellon.convert_units(self, units)
+
+        self.options['drillz'] *= factor
+        self.options['travelz'] *= factor
+        self.options['feedrate'] *= factor
+        self.options['feedrate_rapid'] *= factor
+        self.options['toolchangez'] *= factor
+
+        coords_xy = [float(eval(coord)) for coord in self.app.defaults["excellon_toolchangexy"].split(",")]
+        coords_xy[0] *= factor
+        coords_xy[1] *= factor
+        self.options['toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1])
+
+        if self.options['startz'] is not None:
+            self.options['startz'] *= factor
+        self.options['endz'] *= factor
+
+    def plot(self):
+
+        # Does all the required setup and returns False
+        # if the 'ptint' option is set to False.
+        if not FlatCAMObj.plot(self):
+            return
+
+        try:
+            _ = iter(self.solid_geometry)
+        except TypeError:
+            self.solid_geometry = [self.solid_geometry]
+
+        try:
+            # Plot excellon (All polygons?)
+            if self.options["solid"]:
+                for geo in self.solid_geometry:
+                    self.add_shape(shape=geo, color='#750000BF', face_color='#C40000BF', visible=self.options['plot'],
+                                   layer=2)
+            else:
+                for geo in self.solid_geometry:
+                    self.add_shape(shape=geo.exterior, color='red', visible=self.options['plot'])
+                    for ints in geo.interiors:
+                        self.add_shape(shape=ints, color='green', visible=self.options['plot'])
+
+            self.shapes.redraw()
+        except (ObjectDeleted, AttributeError):
+            self.shapes.clear(update=True)
+
+        # try:
+        #     # Plot excellon (All polygons?)
+        #     if self.options["solid"]:
+        #         for geo_type in self.solid_geometry:
+        #             if geo_type is not None:
+        #                 if type(geo_type) is dict:
+        #                     for tooldia in geo_type:
+        #                         geo_list = geo_type[tooldia]
+        #                         for geo in geo_list:
+        #                             self.add_shape(shape=geo, color='#750000BF', face_color='#C40000BF',
+        #                                            visible=self.options['plot'],
+        #                                            layer=2)
+        #                 else:
+        #                     self.add_shape(shape=geo_type, color='#750000BF', face_color='#C40000BF',
+        #                                    visible=self.options['plot'],
+        #                                    layer=2)
+        #     else:
+        #         for geo_type in self.solid_geometry:
+        #             if geo_type is not None:
+        #                 if type(geo_type) is dict:
+        #                     for tooldia in geo_type:
+        #                         geo_list = geo_type[tooldia]
+        #                         for geo in geo_list:
+        #                             self.add_shape(shape=geo.exterior, color='red', visible=self.options['plot'])
+        #                             for ints in geo.interiors:
+        #                                 self.add_shape(shape=ints, color='green', visible=self.options['plot'])
+        #                 else:
+        #                     self.add_shape(shape=geo_type.exterior, color='red', visible=self.options['plot'])
+        #                     for ints in geo_type.interiors:
+        #                         self.add_shape(shape=ints, color='green', visible=self.options['plot'])
+        #     self.shapes.redraw()
+        # except (ObjectDeleted, AttributeError):
+        #     self.shapes.clear(update=True)
+
+
+class FlatCAMGeometry(FlatCAMObj, Geometry):
+    """
+    Geometric object not associated with a specific
+    format.
+    """
+    optionChanged = QtCore.pyqtSignal(str)
+    ui_type = GeometryObjectUI
+
+    @staticmethod
+    def merge(geo_list, geo_final, multigeo=None):
+        """
+        Merges the geometry of objects in grb_list into
+        the geometry of geo_final.
+
+        :param geo_list: List of FlatCAMGerber Objects to join.
+        :param geo_final: Destination FlatCAMGerber object.
+        :return: None
+        """
+
+        if geo_final.solid_geometry is None:
+            geo_final.solid_geometry = []
+
+        if type(geo_final.solid_geometry) is not list:
+            geo_final.solid_geometry = [geo_final.solid_geometry]
+
+        for geo in geo_list:
+            # Expand lists
+            if type(geo) is list:
+                FlatCAMGeometry.merge(geo, geo_final)
+
+            # If not list, just append
+            else:
+                # merge solid_geometry, useful for singletool geometry, for multitool each is empty
+                if multigeo is None or multigeo == False:
+                    geo_final.multigeo = False
+                    try:
+                        geo_final.solid_geometry.append(geo.solid_geometry)
+                    except Exception as e:
+                        log.debug("FlatCAMGeometry.merge() --> %s" % str(e))
+                else:
+                    geo_final.multigeo = True
+                    # if multigeo the solid_geometry is empty in the object attributes because it now lives in the
+                    # tools object attribute, as a key value
+                    geo_final.solid_geometry = []
+
+                # find the tool_uid maximum value in the geo_final
+                geo_final_uid_list = []
+                for key in geo_final.tools:
+                    geo_final_uid_list.append(int(key))
+                try:
+                    max_uid = max(geo_final_uid_list, key=int)
+                except ValueError:
+                    max_uid = 0
+
+                # add and merge tools
+                for tool_uid in geo.tools:
+                    max_uid += 1
+                    geo_final.tools[max_uid] = dict(geo.tools[tool_uid])
+
+    @staticmethod
+    def get_pts(o):
+        """
+        Returns a list of all points in the object, where
+        the object can be a MultiPolygon, Polygon, Not a polygon, or a list
+        of such. Search is done recursively.
+
+        :param: geometric object
+        :return: List of points
+        :rtype: list
+        """
+        pts = []
+
+        ## Iterable: descend into each item.
+        try:
+            for subo in o:
+                pts += FlatCAMGeometry.get_pts(subo)
+
+        ## Non-iterable
+        except TypeError:
+            if o is not None:
+                if type(o) == MultiPolygon:
+                    for poly in o:
+                        pts += FlatCAMGeometry.get_pts(poly)
+                ## Descend into .exerior and .interiors
+                elif type(o) == Polygon:
+                    pts += FlatCAMGeometry.get_pts(o.exterior)
+                    for i in o.interiors:
+                        pts += FlatCAMGeometry.get_pts(i)
+                elif type(o) == MultiLineString:
+                    for line in o:
+                        pts += FlatCAMGeometry.get_pts(line)
+                ## Has .coords: list them.
+                else:
+                    pts += list(o.coords)
+            else:
+                return
+        return pts
+
+    def __init__(self, name):
+        FlatCAMObj.__init__(self, name)
+        Geometry.__init__(self, geo_steps_per_circle=self.app.defaults["geometry_circle_steps"])
+
+        self.kind = "geometry"
+
+        self.options.update({
+            "plot": True,
+            "cutz": -0.002,
+            "vtipdia": 0.1,
+            "vtipangle": 30,
+            "travelz": 0.1,
+            "feedrate": 5.0,
+            "feedrate_z": 5.0,
+            "feedrate_rapid": 5.0,
+            "spindlespeed": None,
+            "dwell": True,
+            "dwelltime": 1000,
+            "multidepth": False,
+            "depthperpass": 0.002,
+            "extracut": False,
+            "endz": 2.0,
+            "toolchange": False,
+            "toolchangez": 1.0,
+            "toolchangexy": "0.0, 0.0",
+            "startz": None,
+            "ppname_g": 'default',
+        })
+
+        if "cnctooldia" not in self.options:
+            self.options["cnctooldia"] =  self.app.defaults["geometry_cnctooldia"]
+
+        self.options["startz"] = self.app.defaults["geometry_startz"]
+
+        # this will hold the tool unique ID that is useful when having multiple tools with same diameter
+        self.tooluid = 0
+
+        '''
+            self.tools = {}
+            This is a dictionary. Each dict key is associated with a tool used in geo_tools_table. The key is the 
+            tool_id of the tools and the value is another dict that will hold the data under the following form:
+                {tooluid:   {
+                            'tooldia': 1,
+                            'offset': 'Path',
+                            'offset_value': 0.0
+                            'type': 'Rough',
+                            'tool_type': 'C1',
+                            'data': self.default_tool_data
+                            'solid_geometry': []
+                            }
+                }
+        '''
+        self.tools = {}
+
+        # this dict is to store those elements (tools) of self.tools that are selected in the self.geo_tools_table
+        # those elements are the ones used for generating GCode
+        self.sel_tools = {}
+
+        self.offset_item_options = ["Path", "In", "Out", "Custom"]
+        self.type_item_options = ["Iso", "Rough", "Finish"]
+        self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
+
+        # flag to store if the V-Shape tool is selected in self.ui.geo_tools_table
+        self.v_tool_type = None
+
+        self.multigeo = False
+
+        # Attributes to be included in serialization
+        # Always append to it because it carries contents
+        # from predecessors.
+        self.ser_attrs += ['options', 'kind', 'tools', 'multigeo']
+
+    def build_ui(self):
+
+        self.ui_disconnect()
+
+        FlatCAMObj.build_ui(self)
+
+        offset = 0
+        tool_idx = 0
+
+        n = len(self.tools)
+        self.ui.geo_tools_table.setRowCount(n)
+
+        for tooluid_key, tooluid_value in self.tools.items():
+            tool_idx += 1
+            row_no = tool_idx - 1
+
+            id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
+            id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.geo_tools_table.setItem(row_no, 0, id)  # Tool name/id
+
+            # Make sure that the tool diameter when in MM is with no more than 2 decimals.
+            # There are no tool bits in MM with more than 3 decimals diameter.
+            # For INCH the decimals should be no more than 3. There are no tools under 10mils.
+            if self.units == 'MM':
+                dia_item = QtWidgets.QTableWidgetItem('%.2f' % float(tooluid_value['tooldia']))
+            else:
+                dia_item = QtWidgets.QTableWidgetItem('%.4f' % float(tooluid_value['tooldia']))
+
+            dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
+
+            offset_item = QtWidgets.QComboBox()
+            for item in self.offset_item_options:
+                offset_item.addItem(item)
+            offset_item.setStyleSheet('background-color: rgb(255,255,255)')
+            idx = offset_item.findText(tooluid_value['offset'])
+            offset_item.setCurrentIndex(idx)
+
+            type_item = QtWidgets.QComboBox()
+            for item in self.type_item_options:
+                type_item.addItem(item)
+            type_item.setStyleSheet('background-color: rgb(255,255,255)')
+            idx = type_item.findText(tooluid_value['type'])
+            type_item.setCurrentIndex(idx)
+
+            tool_type_item = QtWidgets.QComboBox()
+            for item in self.tool_type_item_options:
+                tool_type_item.addItem(item)
+                tool_type_item.setStyleSheet('background-color: rgb(255,255,255)')
+            idx = tool_type_item.findText(tooluid_value['tool_type'])
+            tool_type_item.setCurrentIndex(idx)
+
+            tool_uid_item = QtWidgets.QTableWidgetItem(str(tooluid_key))
+
+            plot_item = FCCheckBox()
+            plot_item.setLayoutDirection(QtCore.Qt.RightToLeft)
+            if self.ui.plot_cb.isChecked():
+                plot_item.setChecked(True)
+
+            self.ui.geo_tools_table.setItem(row_no, 1, dia_item)  # Diameter
+            self.ui.geo_tools_table.setCellWidget(row_no, 2, offset_item)
+            self.ui.geo_tools_table.setCellWidget(row_no, 3, type_item)
+            self.ui.geo_tools_table.setCellWidget(row_no, 4, tool_type_item)
+
+            ### REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY ###
+            self.ui.geo_tools_table.setItem(row_no, 5, tool_uid_item)  # Tool unique ID
+            self.ui.geo_tools_table.setCellWidget(row_no, 6, plot_item)
+
+            try:
+                self.ui.tool_offset_entry.set_value(tooluid_value['offset_value'])
+            except:
+                log.debug("build_ui() --> Could not set the 'offset_value' key in self.tools")
+
+        # make the diameter column editable
+        for row in range(tool_idx):
+            self.ui.geo_tools_table.item(row, 1).setFlags(QtCore.Qt.ItemIsSelectable |
+                                                          QtCore.Qt.ItemIsEditable |
+                                                          QtCore.Qt.ItemIsEnabled)
+
+        # sort the tool diameter column
+        # self.ui.geo_tools_table.sortItems(1)
+        # all the tools are selected by default
+        # self.ui.geo_tools_table.selectColumn(0)
+
+        self.ui.geo_tools_table.resizeColumnsToContents()
+        self.ui.geo_tools_table.resizeRowsToContents()
+
+        vertical_header = self.ui.geo_tools_table.verticalHeader()
+        # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
+        vertical_header.hide()
+        self.ui.geo_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header = self.ui.geo_tools_table.horizontalHeader()
+        horizontal_header.setMinimumSectionSize(10)
+        horizontal_header.setDefaultSectionSize(70)
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(0, 20)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+        # horizontal_header.setColumnWidth(2, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(4, 40)
+        horizontal_header.setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(4, 17)
+        # horizontal_header.setStretchLastSection(True)
+        self.ui.geo_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        self.ui.geo_tools_table.setColumnWidth(0, 20)
+        self.ui.geo_tools_table.setColumnWidth(4, 40)
+        self.ui.geo_tools_table.setColumnWidth(6, 17)
+
+        # self.ui.geo_tools_table.setSortingEnabled(True)
+
+        self.ui.geo_tools_table.setMinimumHeight(self.ui.geo_tools_table.getHeight())
+        self.ui.geo_tools_table.setMaximumHeight(self.ui.geo_tools_table.getHeight())
+
+        # update UI for all rows - useful after units conversion but only if there is at least one row
+        row_cnt = self.ui.geo_tools_table.rowCount()
+        if row_cnt > 0:
+            for r in range(row_cnt):
+                self.update_ui(r)
+
+        # select only the first tool / row
+        selected_row = 0
+        try:
+            self.select_tools_table_row(selected_row, clearsel=True)
+            # update the Geometry UI
+            self.update_ui()
+        except Exception as e:
+            # when the tools table is empty there will be this error but once the table is populated it will go away
+            log.debug(str(e))
+
+        # disable the Plot column in Tool Table if the geometry is SingleGeo as it is not needed
+        # and can create some problems
+        if self.multigeo is False:
+            self.ui.geo_tools_table.setColumnHidden(6, True)
+        else:
+            self.ui.geo_tools_table.setColumnHidden(6, False)
+
+        self.set_tool_offset_visibility(selected_row)
+        self.ui_connect()
+
+    def set_ui(self, ui):
+        FlatCAMObj.set_ui(self, ui)
+
+        log.debug("FlatCAMGeometry.set_ui()")
+
+        assert isinstance(self.ui, GeometryObjectUI), \
+            "Expected a GeometryObjectUI, got %s" % type(self.ui)
+
+        # populate postprocessor names in the combobox
+        for name in list(self.app.postprocessors.keys()):
+            self.ui.pp_geometry_name_cb.addItem(name)
+
+        self.form_fields.update({
+            "plot": self.ui.plot_cb,
+            "cutz": self.ui.cutz_entry,
+            "vtipdia": self.ui.tipdia_entry,
+            "vtipangle": self.ui.tipangle_entry,
+            "travelz": self.ui.travelz_entry,
+            "feedrate": self.ui.cncfeedrate_entry,
+            "feedrate_z": self.ui.cncplunge_entry,
+            "feedrate_rapid": self.ui.cncfeedrate_rapid_entry,
+            "spindlespeed": self.ui.cncspindlespeed_entry,
+            "dwell": self.ui.dwell_cb,
+            "dwelltime": self.ui.dwelltime_entry,
+            "multidepth": self.ui.mpass_cb,
+            "ppname_g": self.ui.pp_geometry_name_cb,
+            "depthperpass": self.ui.maxdepth_entry,
+            "extracut": self.ui.extracut_cb,
+            "toolchange": self.ui.toolchangeg_cb,
+            "toolchangez": self.ui.toolchangez_entry,
+            "endz": self.ui.gendz_entry,
+        })
+
+        # Fill form fields only on object create
+        self.to_form()
+
+        self.ui.tipdialabel.hide()
+        self.ui.tipdia_entry.hide()
+        self.ui.tipanglelabel.hide()
+        self.ui.tipangle_entry.hide()
+        self.ui.cutz_entry.setDisabled(False)
+
+        # store here the default data for Geometry Data
+        self.default_data = {}
+        self.default_data.update({
+            "name": None,
+            "plot": None,
+            "cutz": None,
+            "vtipdia": None,
+            "vtipangle": None,
+            "travelz": None,
+            "feedrate": None,
+            "feedrate_z": None,
+            "feedrate_rapid": None,
+            "dwell": None,
+            "dwelltime": None,
+            "multidepth": None,
+            "ppname_g": None,
+            "depthperpass": None,
+            "extracut": None,
+            "toolchange": None,
+            "toolchangez": None,
+            "endz": None,
+            "spindlespeed": None,
+            "toolchangexy": None,
+            "startz": None
+        })
+
+        # fill in self.default_data values from self.options
+        for def_key in self.default_data:
+            for opt_key, opt_val in self.options.items():
+                if def_key == opt_key:
+                    self.default_data[def_key] = opt_val
+
+        self.tooluid += 1
+        if not self.tools:
+            self.tools.update({
+                self.tooluid: {
+                    'tooldia': self.options["cnctooldia"],
+                    'offset': 'Path',
+                    'offset_value': 0.0,
+                    'type': 'Rough',
+                    'tool_type': 'C1',
+                    'data': self.default_data,
+                    'solid_geometry': []
+                }
+            })
+        else:
+            # if self.tools is not empty then it can safely be assumed that it comes from an opened project.
+            # Because of the serialization the self.tools list on project save, the dict keys (members of self.tools
+            # are each a dict) are turned into strings so we rebuild the self.tools elements so the keys are
+            # again float type; dict's don't like having keys changed when iterated through therefore the need for the
+            # following convoluted way of changing the keys from string to float type
+            temp_tools = {}
+            new_key = 0.0
+            for tooluid_key in self.tools:
+                val =  dict(self.tools[tooluid_key])
+                new_key = deepcopy(int(tooluid_key))
+                temp_tools[new_key] = val
+
+            self.tools.clear()
+            self.tools = dict(temp_tools)
+
+        self.ui.tool_offset_entry.hide()
+        self.ui.tool_offset_lbl.hide()
+
+        assert isinstance(self.ui, GeometryObjectUI), \
+            "Expected a GeometryObjectUI, got %s" % type(self.ui)
+
+        self.ui.geo_tools_table.setupContextMenu()
+        self.ui.geo_tools_table.addContextMenu(
+            "Copy", self.on_tool_copy, icon=QtGui.QIcon("share/copy16.png"))
+        self.ui.geo_tools_table.addContextMenu(
+            "Delete", lambda: self.on_tool_delete(all=None), icon=QtGui.QIcon("share/delete32.png"))
+
+        self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
+        self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click)
+        self.ui.paint_tool_button.clicked.connect(self.app.paint_tool.run)
+
+    def set_tool_offset_visibility(self, current_row):
+        if current_row is None:
+            return
+        try:
+            tool_offset = self.ui.geo_tools_table.cellWidget(current_row, 2)
+            if tool_offset is not None:
+                tool_offset_txt = tool_offset.currentText()
+                if tool_offset_txt == 'Custom':
+                    self.ui.tool_offset_entry.show()
+                    self.ui.tool_offset_lbl.show()
+                else:
+                    self.ui.tool_offset_entry.hide()
+                    self.ui.tool_offset_lbl.hide()
+        except Exception as e:
+            log.debug("set_tool_offset_visibility() --> " + str(e))
+            return
+
+    def on_offset_value_edited(self):
+        '''
+        This will save the offset_value into self.tools storage whenever the oofset value is edited
+        :return:
+        '''
+        for current_row in self.ui.geo_tools_table.selectedItems():
+            # sometime the header get selected and it has row number -1
+            # we don't want to do anything with the header :)
+            if current_row.row() < 0:
+                continue
+            tool_uid = int(self.ui.geo_tools_table.item(current_row.row(), 5).text())
+            self.set_tool_offset_visibility(current_row.row())
+
+            for tooluid_key, tooluid_value in self.tools.items():
+                if int(tooluid_key) == tool_uid:
+                    tooluid_value['offset_value'] = self.ui.tool_offset_entry.get_value()
+
+    def ui_connect(self):
+
+        # on any change to the widgets that matter it will be called self.gui_form_to_storage which will save the
+        # changes in geometry UI
+        for i in range(self.ui.grid3.count()):
+            try:
+                # works for CheckBoxes
+                self.ui.grid3.itemAt(i).widget().stateChanged.connect(self.gui_form_to_storage)
+            except:
+                # works for ComboBoxes
+                try:
+                    self.ui.grid3.itemAt(i).widget().currentIndexChanged.connect(self.gui_form_to_storage)
+                except:
+                    # works for Entry
+                    try:
+                        self.ui.grid3.itemAt(i).widget().editingFinished.connect(self.gui_form_to_storage)
+                    except:
+                        pass
+
+        for row in range(self.ui.geo_tools_table.rowCount()):
+            for col in [2, 3, 4]:
+                self.ui.geo_tools_table.cellWidget(row, col).currentIndexChanged.connect(
+                    self.on_tooltable_cellwidget_change)
+
+        # I use lambda's because the connected functions have parameters that could be used in certain scenarios
+        self.ui.addtool_btn.clicked.connect(lambda: self.on_tool_add())
+        self.ui.copytool_btn.clicked.connect(lambda: self.on_tool_copy())
+        self.ui.deltool_btn.clicked.connect(lambda: self.on_tool_delete())
+
+        self.ui.geo_tools_table.currentItemChanged.connect(self.on_row_selection_change)
+        self.ui.geo_tools_table.itemChanged.connect(self.on_tool_edit)
+        self.ui.tool_offset_entry.editingFinished.connect(self.on_offset_value_edited)
+
+        for row in range(self.ui.geo_tools_table.rowCount()):
+            self.ui.geo_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table)
+        self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
+
+    def ui_disconnect(self):
+
+        try:
+            # on any change to the widgets that matter it will be called self.gui_form_to_storage which will save the
+            # changes in geometry UI
+            for i in range(self.ui.grid3.count()):
+                if isinstance(self.ui.grid3.itemAt(i).widget(), FCCheckBox):
+                    self.ui.grid3.itemAt(i).widget().stateChanged.disconnect()
+
+                if isinstance(self.ui.grid3.itemAt(i).widget(), FCComboBox):
+                    self.ui.grid3.itemAt(i).widget().currentIndexChanged.disconnect()
+
+                if isinstance(self.ui.grid3.itemAt(i).widget(), LengthEntry) or \
+                        isinstance(self.ui.grid3.itemAt(i), IntEntry) or \
+                        isinstance(self.ui.grid3.itemAt(i), FCEntry):
+                    self.ui.grid3.itemAt(i).widget().editingFinished.disconnect()
+        except:
+            pass
+
+        try:
+            for row in range(self.ui.geo_tools_table.rowCount()):
+                for col in [2, 3, 4]:
+                    self.ui.geo_tools_table.cellWidget(row, col).currentIndexChanged.disconnect()
+        except:
+            pass
+
+        # I use lambda's because the connected functions have parameters that could be used in certain scenarios
+        try:
+            self.ui.addtool_btn.clicked.disconnect()
+        except:
+            pass
+
+        try:
+            self.ui.copytool_btn.clicked.disconnect()
+        except:
+            pass
+
+        try:
+            self.ui.deltool_btn.clicked.disconnect()
+        except:
+            pass
+
+        try:
+            self.ui.geo_tools_table.currentItemChanged.disconnect()
+        except:
+            pass
+
+        try:
+            self.ui.geo_tools_table.itemChanged.disconnect()
+        except:
+            pass
+
+        try:
+            self.ui.tool_offset_entry.editingFinished.disconnect()
+        except:
+            pass
+
+        for row in range(self.ui.geo_tools_table.rowCount()):
+            try:
+                self.ui.geo_tools_table.cellWidget(row, 6).clicked.disconnect()
+            except:
+                pass
+
+        try:
+            self.ui.plot_cb.stateChanged.disconnect()
+        except:
+            pass
+
+    def on_tool_add(self, dia=None):
+        self.ui_disconnect()
+
+        last_offset = None
+        last_offset_value = None
+        last_type = None
+        last_tool_type = None
+        last_data = None
+        last_solid_geometry = []
+
+        if dia is not None:
+            tooldia = dia
+        else:
+            tooldia = self.ui.addtool_entry.get_value()
+            if tooldia is None:
+                self.build_ui()
+                self.app.inform.emit("[error_notcl] Please enter the desired tool diameter in Float format.")
+                return
+
+        # construct a list of all 'tooluid' in the self.tools
+        tool_uid_list = []
+        for tooluid_key in self.tools:
+            tool_uid_item = int(tooluid_key)
+            tool_uid_list.append(tool_uid_item)
+
+        # find maximum from the temp_uid, add 1 and this is the new 'tooluid'
+        if not tool_uid_list:
+            max_uid = 0
+        else:
+            max_uid = max(tool_uid_list)
+        self.tooluid = max_uid + 1
+
+        if self.units == 'IN':
+            tooldia = float('%.4f' % tooldia)
+        else:
+            tooldia = float('%.2f' % tooldia)
+
+        # here we actually add the new tool; if there is no tool in the tool table we add a tool with default data
+        # otherwise we add a tool with data copied from last tool
+        if not self.tools:
+            self.tools.update({
+                self.tooluid: {
+                    'tooldia': tooldia,
+                    'offset': 'Path',
+                    'offset_value': 0.0,
+                    'type': 'Rough',
+                    'tool_type': 'C1',
+                    'data': dict(self.default_data),
+                    'solid_geometry': []
+                }
+            })
+        else:
+            # print("LAST", self.tools[maxuid])
+            last_data = self.tools[max_uid]['data']
+            last_offset = self.tools[max_uid]['offset']
+            last_offset_value = self.tools[max_uid]['offset_value']
+            last_type = self.tools[max_uid]['type']
+            last_tool_type = self.tools[max_uid]['tool_type']
+            last_solid_geometry = self.tools[max_uid]['solid_geometry']
+
+            self.tools.update({
+                self.tooluid: {
+                    'tooldia': tooldia,
+                    'offset': last_offset,
+                    'offset_value': last_offset_value,
+                    'type': last_type,
+                    'tool_type': last_tool_type,
+                    'data': dict(last_data),
+                    'solid_geometry': deepcopy(last_solid_geometry)
+                }
+            })
+            # print("CURRENT", self.tools[-1])
+
+        self.ui.tool_offset_entry.hide()
+        self.ui.tool_offset_lbl.hide()
+
+        # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list
+        try:
+            self.ser_attrs.remove('tools')
+        except:
+            pass
+        self.ser_attrs.append('tools')
+
+        self.app.inform.emit("[success] Tool added in Tool Table.")
+        self.build_ui()
+
+    def on_tool_copy(self, all=None):
+        self.ui_disconnect()
+
+        # find the tool_uid maximum value in the self.tools
+        uid_list = []
+        for key in self.tools:
+            uid_list.append(int(key))
+        try:
+            max_uid = max(uid_list, key=int)
+        except ValueError:
+            max_uid = 0
+
+        if all is None:
+            if self.ui.geo_tools_table.selectedItems():
+                for current_row in self.ui.geo_tools_table.selectedItems():
+                    # sometime the header get selected and it has row number -1
+                    # we don't want to do anything with the header :)
+                    if current_row.row() < 0:
+                        continue
+                    try:
+                        tooluid_copy = int(self.ui.geo_tools_table.item(current_row.row(), 5).text())
+                        self.set_tool_offset_visibility(current_row.row())
+                        max_uid += 1
+                        self.tools[int(max_uid)] = dict(self.tools[tooluid_copy])
+                    except AttributeError:
+                        self.app.inform.emit("[warning_notcl]Failed. Select a tool to copy.")
+                        self.build_ui()
+                        return
+                    except Exception as e:
+                        log.debug("on_tool_copy() --> " + str(e))
+                # deselect the table
+                # self.ui.geo_tools_table.clearSelection()
+            else:
+                self.app.inform.emit("[warning_notcl]Failed. Select a tool to copy.")
+                self.build_ui()
+                return
+        else:
+            # we copy all tools in geo_tools_table
+            try:
+                temp_tools = dict(self.tools)
+                max_uid += 1
+                for tooluid in temp_tools:
+                    self.tools[int(max_uid)] = dict(temp_tools[tooluid])
+                temp_tools.clear()
+            except Exception as e:
+                log.debug("on_tool_copy() --> " + str(e))
+
+        # if there are no more tools in geo tools table then hide the tool offset
+        if not self.tools:
+            self.ui.tool_offset_entry.hide()
+            self.ui.tool_offset_lbl.hide()
+
+        # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list
+        try:
+            self.ser_attrs.remove('tools')
+        except:
+            pass
+        self.ser_attrs.append('tools')
+
+        self.build_ui()
+        self.app.inform.emit("[success] Tool was copied in Tool Table.")
+
+    def on_tool_edit(self, current_item):
+
+        self.ui_disconnect()
+
+        current_row = current_item.row()
+        tool_dia = float('%.4f' % float(self.ui.geo_tools_table.item(current_row, 1).text()))
+        tooluid = int(self.ui.geo_tools_table.item(current_row, 5).text())
+
+        self.tools[tooluid]['tooldia'] = tool_dia
+
+        try:
+            self.ser_attrs.remove('tools')
+            self.ser_attrs.append('tools')
+        except:
+            pass
+
+        self.app.inform.emit("[success] Tool was edited in Tool Table.")
+        self.build_ui()
+
+    def on_tool_delete(self, all=None):
+
+        self.ui_disconnect()
+
+        if all is None:
+            if self.ui.geo_tools_table.selectedItems():
+                for current_row in self.ui.geo_tools_table.selectedItems():
+                    # sometime the header get selected and it has row number -1
+                    # we don't want to do anything with the header :)
+                    if current_row.row() < 0:
+                        continue
+                    try:
+                        tooluid_del = int(self.ui.geo_tools_table.item(current_row.row(), 5).text())
+                        self.set_tool_offset_visibility(current_row.row())
+
+                        temp_tools = dict(self.tools)
+                        for tooluid_key in self.tools:
+                            if int(tooluid_key) == tooluid_del:
+                                temp_tools.pop(tooluid_del, None)
+                        self.tools = dict(temp_tools)
+                        temp_tools.clear()
+                    except AttributeError:
+                        self.app.inform.emit("[warning_notcl]Failed. Select a tool to delete.")
+                        self.build_ui()
+                        return
+                    except Exception as e:
+                        log.debug("on_tool_delete() --> " + str(e))
+                # deselect the table
+                # self.ui.geo_tools_table.clearSelection()
+            else:
+                self.app.inform.emit("[warning_notcl]Failed. Select a tool to delete.")
+                self.build_ui()
+                return
+        else:
+            # we delete all tools in geo_tools_table
+            self.tools.clear()
+
+        self.app.plot_all()
+
+        # if there are no more tools in geo tools table then hide the tool offset
+        if not self.tools:
+            self.ui.tool_offset_entry.hide()
+            self.ui.tool_offset_lbl.hide()
+
+        # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list
+        try:
+            self.ser_attrs.remove('tools')
+        except:
+            pass
+        self.ser_attrs.append('tools')
+
+        self.build_ui()
+        self.app.inform.emit("[success] Tool was deleted in Tool Table.")
+
+        obj_active = self.app.collection.get_active()
+        # if the object was MultiGeo and now it has no tool at all (therefore no geometry)
+        # we make it back SingleGeo
+        if self.ui.geo_tools_table.rowCount() <= 0:
+            obj_active.multigeo = False
+            obj_active.options['xmin'] = 0
+            obj_active.options['ymin'] = 0
+            obj_active.options['xmax'] = 0
+            obj_active.options['ymax'] = 0
+
+        if obj_active.multigeo is True:
+            try:
+                xmin, ymin, xmax, ymax = obj_active.bounds()
+                obj_active.options['xmin'] = xmin
+                obj_active.options['ymin'] = ymin
+                obj_active.options['xmax'] = xmax
+                obj_active.options['ymax'] = ymax
+            except:
+                obj_active.options['xmin'] = 0
+                obj_active.options['ymin'] = 0
+                obj_active.options['xmax'] = 0
+                obj_active.options['ymax'] = 0
+
+    def on_row_selection_change(self):
+        self.update_ui()
+
+    def update_ui(self, row=None):
+        self.ui_disconnect()
+
+        if row is None:
+            try:
+                current_row = self.ui.geo_tools_table.currentRow()
+            except:
+                current_row = 0
+        else:
+            current_row = row
+
+        if current_row < 0:
+            current_row = 0
+
+        self.set_tool_offset_visibility(current_row)
+
+        # populate the form with the data from the tool associated with the row parameter
+        try:
+            tooluid = int(self.ui.geo_tools_table.item(current_row, 5).text())
+        except Exception as e:
+            log.debug("Tool missing. Add a tool in Geo Tool Table. %s" % str(e))
+            return
+
+        # update the form with the V-Shape fields if V-Shape selected in the geo_tool_table
+        # also modify the Cut Z form entry to reflect the calculated Cut Z from values got from V-Shape Fields
+        try:
+            tool_type_txt = self.ui.geo_tools_table.cellWidget(current_row, 4).currentText()
+            self.ui_update_v_shape(tool_type_txt=tool_type_txt)
+        except Exception as e:
+            log.debug("Tool missing. Add a tool in Geo Tool Table. %s" % str(e))
+            return
+
+        try:
+            # set the form with data from the newly selected tool
+            for tooluid_key, tooluid_value in self.tools.items():
+                if int(tooluid_key) == tooluid:
+                    for key, value in tooluid_value.items():
+                        if key == 'data':
+                            form_value_storage = tooluid_value[key]
+                            self.update_form(form_value_storage)
+                        if key == 'offset_value':
+                            # update the offset value in the entry even if the entry is hidden
+                            self.ui.tool_offset_entry.set_value(tooluid_value[key])
+
+                        if key == 'tool_type' and value == 'V':
+                            self.update_cutz()
+        except Exception as e:
+            log.debug("FlatCAMObj ---> update_ui() " + str(e))
+
+        self.ui_connect()
+
+    def ui_update_v_shape(self, tool_type_txt):
+        if tool_type_txt == 'V':
+            self.ui.tipdialabel.show()
+            self.ui.tipdia_entry.show()
+            self.ui.tipanglelabel.show()
+            self.ui.tipangle_entry.show()
+            self.ui.cutz_entry.setDisabled(True)
+
+            self.update_cutz()
+        else:
+            self.ui.tipdialabel.hide()
+            self.ui.tipdia_entry.hide()
+            self.ui.tipanglelabel.hide()
+            self.ui.tipangle_entry.hide()
+            self.ui.cutz_entry.setDisabled(False)
+
+    def update_cutz(self):
+        vdia = float(self.ui.tipdia_entry.get_value())
+        half_vangle = float(self.ui.tipangle_entry.get_value()) / 2
+
+        row = self.ui.geo_tools_table.currentRow()
+        tool_uid = int(self.ui.geo_tools_table.item(row, 5).text())
+
+        tooldia = float(self.ui.geo_tools_table.item(row, 1).text())
+        new_cutz = (tooldia - vdia) / (2 * math.tan(math.radians(half_vangle)))
+        new_cutz = float('%.4f' % -new_cutz)
+        self.ui.cutz_entry.set_value(new_cutz)
+
+        # store the new CutZ value into storage (self.tools)
+        for tooluid_key, tooluid_value in self.tools.items():
+            if int(tooluid_key) == tool_uid:
+                tooluid_value['data']['cutz'] = new_cutz
+
+    def on_tooltable_cellwidget_change(self):
+        cw = self.sender()
+        cw_index = self.ui.geo_tools_table.indexAt(cw.pos())
+        cw_row = cw_index.row()
+        cw_col = cw_index.column()
+        current_uid = int(self.ui.geo_tools_table.item(cw_row, 5).text())
+
+        # store the text of the cellWidget that changed it's index in the self.tools
+        for tooluid_key, tooluid_value in self.tools.items():
+            if int(tooluid_key) == current_uid:
+                cb_txt = cw.currentText()
+                if cw_col == 2:
+                    tooluid_value['offset'] = cb_txt
+                    if cb_txt == 'Custom':
+                        self.ui.tool_offset_entry.show()
+                        self.ui.tool_offset_lbl.show()
+                    else:
+                        self.ui.tool_offset_entry.hide()
+                        self.ui.tool_offset_lbl.hide()
+                        # reset the offset_value in storage self.tools
+                        tooluid_value['offset_value'] = 0.0
+                elif cw_col == 3:
+                    # force toolpath type as 'Iso' if the tool type is V-Shape
+                    if self.ui.geo_tools_table.cellWidget(cw_row, 4).currentText() == 'V':
+                        tooluid_value['type'] = 'Iso'
+                        idx = self.ui.geo_tools_table.cellWidget(cw_row, 3).findText('Iso')
+                        self.ui.geo_tools_table.cellWidget(cw_row, 3).setCurrentIndex(idx)
+                    else:
+                        tooluid_value['type'] = cb_txt
+                elif cw_col == 4:
+                    tooluid_value['tool_type'] = cb_txt
+
+                    # if the tool_type selected is V-Shape then autoselect the toolpath type as Iso
+                    if cb_txt == 'V':
+                        idx = self.ui.geo_tools_table.cellWidget(cw_row, 3).findText('Iso')
+                        self.ui.geo_tools_table.cellWidget(cw_row, 3).setCurrentIndex(idx)
+                self.ui_update_v_shape(tool_type_txt=self.ui.geo_tools_table.cellWidget(cw_row, 4).currentText())
+
+    def update_form(self, dict_storage):
+        for form_key in self.form_fields:
+            for storage_key in dict_storage:
+                if form_key == storage_key:
+                    try:
+                        self.form_fields[form_key].set_value(dict_storage[form_key])
+                    except Exception as e:
+                        log.debug(str(e))
+
+        # this is done here because those buttons control through OptionalInputSelection if some entry's are Enabled
+        # or not. But due of using the ui_disconnect() status is no longer updated and I had to do it here
+        self.ui.ois_dwell_geo.on_cb_change()
+        self.ui.ois_mpass_geo.on_cb_change()
+        self.ui.ois_tcz_geo.on_cb_change()
+
+    def gui_form_to_storage(self):
+
+        self.ui_disconnect()
+        widget_changed = self.sender()
+        try:
+            widget_idx = self.ui.grid3.indexOf(widget_changed)
+        except:
+            return
+
+        # those are the indexes for the V-Tip Dia and V-Tip Angle, if edited calculate the new Cut Z
+        if widget_idx == 1 or widget_idx == 3:
+            self.update_cutz()
+
+        # the original connect() function of the OptionalInpuSelection is no longer working because of the
+        # ui_diconnect() so I use this 'hack'
+        if isinstance(widget_changed, FCCheckBox):
+            if widget_changed.text() == 'Multi-Depth:':
+                self.ui.ois_mpass_geo.on_cb_change()
+
+            if widget_changed.text() == 'Tool change':
+                self.ui.ois_tcz_geo.on_cb_change()
+
+            if widget_changed.text() == 'Dwell:':
+                self.ui.ois_dwell_geo.on_cb_change()
+
+        row = self.ui.geo_tools_table.currentRow()
+        if row < 0:
+            row = 0
+
+        # store all the data associated with the row parameter to the self.tools storage
+        tooldia_item = float(self.ui.geo_tools_table.item(row, 1).text())
+        offset_item = self.ui.geo_tools_table.cellWidget(row, 2).currentText()
+        type_item = self.ui.geo_tools_table.cellWidget(row, 3).currentText()
+        tool_type_item = self.ui.geo_tools_table.cellWidget(row, 4).currentText()
+        tooluid_item = int(self.ui.geo_tools_table.item(row, 5).text())
+
+        offset_value_item = self.ui.tool_offset_entry.get_value()
+
+        # this new dict will hold the actual useful data, another dict that is the value of key 'data'
+        temp_tools = {}
+        temp_dia = {}
+        temp_data = {}
+
+        for tooluid_key, tooluid_value in self.tools.items():
+            if int(tooluid_key) == tooluid_item:
+                for key, value in tooluid_value.items():
+                    if key == 'tooldia':
+                        temp_dia[key] = tooldia_item
+                    # update the 'offset', 'type' and 'tool_type' sections
+                    if key == 'offset':
+                        temp_dia[key] = offset_item
+                    if key == 'type':
+                        temp_dia[key] = type_item
+                    if key == 'tool_type':
+                        temp_dia[key] = tool_type_item
+                    if key == 'offset_value':
+                        temp_dia[key] = offset_value_item
+
+                    if key == 'data':
+                        # update the 'data' section
+                        for data_key in tooluid_value[key].keys():
+                            for form_key, form_value in self.form_fields.items():
+                                if form_key == data_key:
+                                    temp_data[data_key] = form_value.get_value()
+                            # make sure we make a copy of the keys not in the form (we may use 'data' keys that are
+                            # updated from self.app.defaults
+                            if data_key not in self.form_fields:
+                                temp_data[data_key] = value[data_key]
+                        temp_dia[key] = dict(temp_data)
+                        temp_data.clear()
+
+                    if key == 'solid_geometry':
+                        temp_dia[key] = deepcopy(self.tools[tooluid_key]['solid_geometry'])
+
+                    temp_tools[tooluid_key] = dict(temp_dia)
+
+            else:
+                temp_tools[tooluid_key] = dict(tooluid_value)
+
+        self.tools.clear()
+        self.tools = dict(temp_tools)
+        temp_tools.clear()
+        self.ui_connect()
+
+    def select_tools_table_row(self, row, clearsel=None):
+        if clearsel:
+            self.ui.geo_tools_table.clearSelection()
+
+        if self.ui.geo_tools_table.rowCount() > 0:
+            # self.ui.geo_tools_table.item(row, 0).setSelected(True)
+            self.ui.geo_tools_table.setCurrentItem(self.ui.geo_tools_table.item(row, 0))
+
+    def export_dxf(self):
+        units = self.app.general_options_form.general_group.units_radio.get_value().upper()
+        dwg = None
+        try:
+            dwg = ezdxf.new('R2010')
+            msp = dwg.modelspace()
+
+            def g2dxf(dxf_space, geo):
+                if isinstance(geo, MultiPolygon):
+                    for poly in geo:
+                        ext_points = list(poly.exterior.coords)
+                        dxf_space.add_lwpolyline(ext_points)
+                        for interior in poly.interiors:
+                            dxf_space.add_lwpolyline(list(interior.coords))
+                if isinstance(geo, Polygon):
+                    ext_points = list(geo.exterior.coords)
+                    dxf_space.add_lwpolyline(ext_points)
+                    for interior in geo.interiors:
+                        dxf_space.add_lwpolyline(list(interior.coords))
+                if isinstance(geo, MultiLineString):
+                    for line in geo:
+                        dxf_space.add_lwpolyline(list(line.coords))
+                if isinstance(geo, LineString) or isinstance(geo, LinearRing):
+                    dxf_space.add_lwpolyline(list(geo.coords))
+
+            multigeo_solid_geometry = []
+            if self.multigeo:
+                for tool in self.tools:
+                    multigeo_solid_geometry += self.tools[tool]['solid_geometry']
+            else:
+                    multigeo_solid_geometry = self.solid_geometry
+
+            for geo in multigeo_solid_geometry:
+                if type(geo) == list:
+                    for g in geo:
+                        g2dxf(msp, g)
+                else:
+                    g2dxf(msp, geo)
+
+                # points = FlatCAMGeometry.get_pts(geo)
+                # msp.add_lwpolyline(points)
+        except Exception as e:
+            log.debug(str(e))
+
+        return dwg
+
+    def on_generatecnc_button_click(self, *args):
+
+        self.app.report_usage("geometry_on_generatecnc_button")
+        self.read_form()
+
+        # test to see if we have tools available in the tool table
+        if self.ui.geo_tools_table.selectedItems():
+            for x in self.ui.geo_tools_table.selectedItems():
+                tooldia = float(self.ui.geo_tools_table.item(x.row(), 1).text())
+                tooluid = int(self.ui.geo_tools_table.item(x.row(), 5).text())
+
+                for tooluid_key, tooluid_value in self.tools.items():
+                    if int(tooluid_key) == tooluid:
+                        self.sel_tools.update({
+                            tooluid: dict(tooluid_value)
+                        })
+            self.mtool_gen_cncjob()
+
+            self.ui.geo_tools_table.clearSelection()
+        else:
+            self.app.inform.emit("[error_notcl] Failed. No tool selected in the tool table ...")
+
+    def mtool_gen_cncjob(self, use_thread=True):
+        """
+        Creates a multi-tool CNCJob out of this Geometry object.
+        The actual work is done by the target FlatCAMCNCjob object's
+        `generate_from_geometry_2()` method.
+
+        :param z_cut: Cut depth (negative)
+        :param z_move: Hight of the tool when travelling (not cutting)
+        :param feedrate: Feed rate while cutting on X - Y plane
+        :param feedrate_z: Feed rate while cutting on Z plane
+        :param feedrate_rapid: Feed rate while moving with rapids
+        :param tooldia: Tool diameter
+        :param outname: Name of the new object
+        :param spindlespeed: Spindle speed (RPM)
+        :param ppname_g Name of the postprocessor
+        :return: None
+        """
+
+        offset_str = ''
+        multitool_gcode = ''
+
+        # use the name of the first tool selected in self.geo_tools_table which has the diameter passed as tool_dia
+        outname = "%s_%s" % (self.options["name"], 'cnc')
+
+        # Object initialization function for app.new_object()
+        # RUNNING ON SEPARATE THREAD!
+        def job_init_single_geometry(job_obj, app_obj):
+            assert isinstance(job_obj, FlatCAMCNCjob), \
+                "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj)
+
+            # count the tools
+            tool_cnt = 0
+
+            dia_cnc_dict = {}
+
+            # this turn on the FlatCAMCNCJob plot for multiple tools
+            job_obj.multitool = True
+            job_obj.multigeo = False
+            job_obj.cnc_tools.clear()
+            # job_obj.create_geometry()
+
+            for tooluid_key in self.sel_tools:
+                tool_cnt += 1
+                app_obj.progress.emit(20)
+
+                for diadict_key, diadict_value in self.sel_tools[tooluid_key].items():
+                    if diadict_key == 'tooldia':
+                        tooldia_val = float('%.4f' % float(diadict_value))
+                        dia_cnc_dict.update({
+                            diadict_key: tooldia_val
+                        })
+                    if diadict_key == 'offset':
+                        o_val = diadict_value.lower()
+                        dia_cnc_dict.update({
+                            diadict_key: o_val
+                        })
+
+                    if diadict_key == 'type':
+                        t_val = diadict_value
+                        dia_cnc_dict.update({
+                            diadict_key: t_val
+                        })
+
+                    if diadict_key == 'tool_type':
+                        tt_val = diadict_value
+                        dia_cnc_dict.update({
+                            diadict_key: tt_val
+                        })
+
+                    if diadict_key == 'data':
+                        for data_key, data_value in diadict_value.items():
+                            if data_key ==  "multidepth":
+                                multidepth = data_value
+                            if data_key == "depthperpass":
+                                depthpercut = data_value
+
+                            if data_key == "extracut":
+                                extracut = data_value
+                            if data_key == "startz":
+                                startz = data_value
+                            if data_key == "endz":
+                                endz = data_value
+
+                            if data_key == "toolchangez":
+                                toolchangez =data_value
+                            if data_key == "toolchangexy":
+                                toolchangexy = data_value
+                            if data_key == "toolchange":
+                                toolchange = data_value
+
+                            if data_key == "cutz":
+                                z_cut = data_value
+                            if data_key == "travelz":
+                                z_move = data_value
+
+                            if data_key == "feedrate":
+                                feedrate = data_value
+                            if data_key == "feedrate_z":
+                                feedrate_z = data_value
+                            if data_key == "feedrate_rapid":
+                                feedrate_rapid = data_value
+
+                            if data_key == "ppname_g":
+                                pp_geometry_name = data_value
+
+                            if data_key == "spindlespeed":
+                                spindlespeed = data_value
+                            if data_key == "dwell":
+                                dwell = data_value
+                            if data_key == "dwelltime":
+                                dwelltime = data_value
+
+                        datadict = dict(diadict_value)
+                        dia_cnc_dict.update({
+                            diadict_key: datadict
+                        })
+
+                if dia_cnc_dict['offset'] == 'in':
+                    tool_offset = -dia_cnc_dict['tooldia'] / 2
+                    offset_str = 'inside'
+                elif dia_cnc_dict['offset'].lower() == 'out':
+                    tool_offset = dia_cnc_dict['tooldia']  / 2
+                    offset_str = 'outside'
+                elif dia_cnc_dict['offset'].lower() == 'path':
+                    offset_str = 'onpath'
+                    tool_offset = 0.0
+                else:
+                    offset_str = 'custom'
+                    offset_value = self.ui.tool_offset_entry.get_value()
+                    if offset_value:
+                        tool_offset = float(offset_value)
+                    else:
+                        self.app.inform.emit(
+                            "[warning] Tool Offset is selected in Tool Table but no value is provided.\n"
+                            "Add a Tool Offset or change the Offset Type."
+                        )
+                        return
+                dia_cnc_dict.update({
+                    'offset_value': tool_offset
+                })
+
+                job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"]
+                job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"]
+
+                # Propagate options
+                job_obj.options["tooldia"] = tooldia_val
+                job_obj.options['type'] = 'Geometry'
+                job_obj.options['tool_dia'] = tooldia_val
+
+                app_obj.progress.emit(40)
+
+                dia_cnc_dict['gcode'] = job_obj.generate_from_geometry_2(
+                    self, tooldia=tooldia_val, offset=tool_offset, tolerance=0.0005,
+                    z_cut=z_cut, z_move=z_move,
+                    feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid,
+                    spindlespeed=spindlespeed, dwell=dwell, dwelltime=dwelltime,
+                    multidepth=multidepth, depthpercut=depthpercut,
+                    extracut=extracut, startz=startz, endz=endz,
+                    toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy,
+                    pp_geometry_name=pp_geometry_name,
+                    tool_no=tool_cnt)
+
+                app_obj.progress.emit(50)
+                # tell gcode_parse from which point to start drawing the lines depending on what kind of
+                # object is the source of gcode
+                job_obj.toolchange_xy = "geometry"
+
+                dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse()
+
+                # TODO this serve for bounding box creation only; should be optimized
+                dia_cnc_dict['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_cnc_dict['gcode_parsed']])
+
+                app_obj.progress.emit(80)
+
+                job_obj.cnc_tools.update({
+                    tooluid_key: dict(dia_cnc_dict)
+                })
+                dia_cnc_dict.clear()
+
+        # Object initialization function for app.new_object()
+        # RUNNING ON SEPARATE THREAD!
+        def job_init_multi_geometry(job_obj, app_obj):
+            assert isinstance(job_obj, FlatCAMCNCjob), \
+                "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj)
+
+            # count the tools
+            tool_cnt = 0
+
+            dia_cnc_dict = {}
+
+            current_uid = int(1)
+
+            # this turn on the FlatCAMCNCJob plot for multiple tools
+            job_obj.multitool = True
+            job_obj.multigeo = True
+            job_obj.cnc_tools.clear()
+
+            for tooluid_key in self.sel_tools:
+                tool_cnt += 1
+                app_obj.progress.emit(20)
+
+                # find the tool_dia associated with the tooluid_key
+                sel_tool_dia = self.sel_tools[tooluid_key]['tooldia']
+
+                # search in the self.tools for the sel_tool_dia and when found see what tooluid has
+                # on the found tooluid in self.tools we also have the solid_geometry that interest us
+                for k, v in self.tools.items():
+                    if float('%.4f' % float(v['tooldia'])) == float('%.4f' % float(sel_tool_dia)):
+                        current_uid = int(k)
+                        break
+
+                for diadict_key, diadict_value in self.sel_tools[tooluid_key].items():
+                    if diadict_key == 'tooldia':
+                        tooldia_val = float('%.4f' % float(diadict_value))
+                        dia_cnc_dict.update({
+                            diadict_key: tooldia_val
+                        })
+                    if diadict_key == 'offset':
+                        o_val = diadict_value.lower()
+                        dia_cnc_dict.update({
+                            diadict_key: o_val
+                        })
+
+                    if diadict_key == 'type':
+                        t_val = diadict_value
+                        dia_cnc_dict.update({
+                            diadict_key: t_val
+                        })
+
+                    if diadict_key == 'tool_type':
+                        tt_val = diadict_value
+                        dia_cnc_dict.update({
+                            diadict_key: tt_val
+                        })
+
+                    if diadict_key == 'data':
+                        for data_key, data_value in diadict_value.items():
+                            if data_key ==  "multidepth":
+                                multidepth = data_value
+                            if data_key == "depthperpass":
+                                depthpercut = data_value
+
+                            if data_key == "extracut":
+                                extracut = data_value
+                            if data_key == "startz":
+                                startz = data_value
+                            if data_key == "endz":
+                                endz = data_value
+
+                            if data_key == "toolchangez":
+                                toolchangez =data_value
+                            if data_key == "toolchangexy":
+                                toolchangexy = data_value
+                            if data_key == "toolchange":
+                                toolchange = data_value
+
+                            if data_key == "cutz":
+                                z_cut = data_value
+                            if data_key == "travelz":
+                                z_move = data_value
+
+                            if data_key == "feedrate":
+                                feedrate = data_value
+                            if data_key == "feedrate_z":
+                                feedrate_z = data_value
+                            if data_key == "feedrate_rapid":
+                                feedrate_rapid = data_value
+
+                            if data_key == "ppname_g":
+                                pp_geometry_name = data_value
+
+                            if data_key == "spindlespeed":
+                                spindlespeed = data_value
+                            if data_key == "dwell":
+                                dwell = data_value
+                            if data_key == "dwelltime":
+                                dwelltime = data_value
+
+                        datadict = dict(diadict_value)
+                        dia_cnc_dict.update({
+                            diadict_key: datadict
+                        })
+
+                if dia_cnc_dict['offset'] == 'in':
+                    tool_offset = -dia_cnc_dict['tooldia'] / 2
+                    offset_str = 'inside'
+                elif dia_cnc_dict['offset'].lower() == 'out':
+                    tool_offset = dia_cnc_dict['tooldia']  / 2
+                    offset_str = 'outside'
+                elif dia_cnc_dict['offset'].lower() == 'path':
+                    offset_str = 'onpath'
+                    tool_offset = 0.0
+                else:
+                    offset_str = 'custom'
+                    offset_value = self.ui.tool_offset_entry.get_value()
+                    if offset_value:
+                        tool_offset = float(offset_value)
+                    else:
+                        self.app.inform.emit(
+                            "[warning] Tool Offset is selected in Tool Table but no value is provided.\n"
+                            "Add a Tool Offset or change the Offset Type."
+                        )
+                        return
+                dia_cnc_dict.update({
+                    'offset_value': tool_offset
+                })
+
+                job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"]
+                job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"]
+
+                # Propagate options
+                job_obj.options["tooldia"] = tooldia_val
+                job_obj.options['type'] = 'Geometry'
+                job_obj.options['tool_dia'] = tooldia_val
+
+                app_obj.progress.emit(40)
+
+                tool_solid_geometry = self.tools[current_uid]['solid_geometry']
+                dia_cnc_dict['gcode'] = job_obj.generate_from_multitool_geometry(
+                    tool_solid_geometry, tooldia=tooldia_val, offset=tool_offset,
+                    tolerance=0.0005, z_cut=z_cut, z_move=z_move,
+                    feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid,
+                    spindlespeed=spindlespeed, dwell=dwell, dwelltime=dwelltime,
+                    multidepth=multidepth, depthpercut=depthpercut,
+                    extracut=extracut, startz=startz, endz=endz,
+                    toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy,
+                    pp_geometry_name=pp_geometry_name,
+                    tool_no=tool_cnt)
+
+                dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse()
+
+                # TODO this serve for bounding box creation only; should be optimized
+                dia_cnc_dict['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_cnc_dict['gcode_parsed']])
+
+                # tell gcode_parse from which point to start drawing the lines depending on what kind of
+                # object is the source of gcode
+                job_obj.toolchange_xy = "geometry"
+
+                app_obj.progress.emit(80)
+
+                job_obj.cnc_tools.update({
+                    tooluid_key: dict(dia_cnc_dict)
+                })
+                dia_cnc_dict.clear()
+
+        if use_thread:
+            # To be run in separate thread
+            # The idea is that if there is a solid_geometry in the file "root" then most likely thare are no
+            # separate solid_geometry in the self.tools dictionary
+            def job_thread(app_obj):
+                if self.solid_geometry:
+                    with self.app.proc_container.new("Generating CNC Code"):
+                        app_obj.new_object("cncjob", outname, job_init_single_geometry)
+                        app_obj.inform.emit("[success]CNCjob created: %s" % outname)
+                        app_obj.progress.emit(100)
+                else:
+                    with self.app.proc_container.new("Generating CNC Code"):
+                        app_obj.new_object("cncjob", outname, job_init_multi_geometry)
+                        app_obj.inform.emit("[success]CNCjob created: %s" % outname)
+                        app_obj.progress.emit(100)
+
+            # Create a promise with the name
+            self.app.collection.promise(outname)
+            # Send to worker
+            self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
+        else:
+            if self.solid_geometry:
+                self.app.new_object("cncjob", outname, job_init_single_geometry)
+            else:
+                self.app.new_object("cncjob", outname, job_init_multi_geometry)
+
+
+    def generatecncjob(self, outname=None,
+                       tooldia=None, offset=None,
+                       z_cut=None, z_move=None,
+                       feedrate=None, feedrate_z=None, feedrate_rapid=None,
+                       spindlespeed=None, dwell=None, dwelltime=None,
+                       multidepth=None, depthperpass=None,
+                       toolchange=None, toolchangez=None, toolchangexy=None,
+                       extracut=None, startz=None, endz=None,
+                       ppname_g=None,
+                       use_thread=True):
+        """
+        Only used for TCL Command.
+        Creates a CNCJob out of this Geometry object. The actual
+        work is done by the target FlatCAMCNCjob object's
+        `generate_from_geometry_2()` method.
+
+        :param z_cut: Cut depth (negative)
+        :param z_move: Hight of the tool when travelling (not cutting)
+        :param feedrate: Feed rate while cutting on X - Y plane
+        :param feedrate_z: Feed rate while cutting on Z plane
+        :param feedrate_rapid: Feed rate while moving with rapids
+        :param tooldia: Tool diameter
+        :param outname: Name of the new object
+        :param spindlespeed: Spindle speed (RPM)
+        :param ppname_g Name of the postprocessor
+        :return: None
+        """
+        tooldia = tooldia if tooldia else self.options["cnctooldia"]
+        outname = outname if outname is not None else self.options["name"]
+
+        z_cut = z_cut if z_cut is not None else self.options["cutz"]
+        z_move = z_move if z_move is not None else self.options["travelz"]
+
+        feedrate = feedrate if feedrate is not None else self.options["feedrate"]
+        feedrate_z = feedrate_z if feedrate_z is not None else self.options["feedrate_z"]
+        feedrate_rapid = feedrate_rapid if feedrate_rapid is not None else self.options["feedrate_rapid"]
+
+        multidepth = multidepth if multidepth is not None else self.options["multidepth"]
+        depthperpass = depthperpass if depthperpass is not None else self.options["depthperpass"]
+
+        extracut = extracut if extracut is not None else self.options["extracut"]
+        startz = startz if startz is not None else self.options["startz"]
+        endz = endz if endz is not None else self.options["endz"]
+
+        toolchangez = toolchangez if toolchangez else self.options["toolchangez"]
+        toolchangexy = toolchangexy if toolchangexy else self.options["toolchangexy"]
+        toolchange = toolchange if toolchange else self.options["toolchange"]
+
+        offset = offset if offset else 0.0
+
+        # int or None.
+        spindlespeed = spindlespeed if spindlespeed else self.options['spindlespeed']
+        dwell = dwell if dwell else self.options["dwell"]
+        dwelltime = dwelltime if dwelltime else self.options["dwelltime"]
+
+        ppname_g = ppname_g if ppname_g else self.options["ppname_g"]
+
+        print(self.tools)
+
+        # Object initialization function for app.new_object()
+        # RUNNING ON SEPARATE THREAD!
+        def job_init(job_obj, app_obj):
+            assert isinstance(job_obj, FlatCAMCNCjob), "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj)
+
+            # Propagate options
+            job_obj.options["tooldia"] = tooldia
+
+            app_obj.progress.emit(20)
+            job_obj.z_cut = z_cut
+            job_obj.z_move = z_move
+            job_obj.feedrate = feedrate
+            job_obj.feedrate_z = feedrate_z
+            job_obj.feedrate_rapid = feedrate_rapid
+            job_obj.pp_geometry_name = ppname_g
+            job_obj.spindlespeed = spindlespeed
+            job_obj.dwell = dwell
+            job_obj.dwelltime = dwelltime
+            job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"]
+            job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"]
+            app_obj.progress.emit(40)
+
+            job_obj.options['type'] = 'Geometry'
+            job_obj.options['tool_dia'] = tooldia
+
+            job_obj.toolchange = self.options["toolchange"]
+            job_obj.toolchangez = self.options["toolchangez"]
+            job_obj.toolchangexy = self.options["toolchangexy"]
+
+            # TODO: The tolerance should not be hard coded. Just for testing.
+            job_obj.generate_from_geometry_2(self, tooldia=tooldia, offset=offset,
+                                             multidepth=multidepth, depthpercut=depthperpass,
+                                             tolerance=0.0005,
+                                             extracut=extracut,  endz=endz, startz=startz,
+                                             toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy)
+
+            app_obj.progress.emit(50)
+            # tell gcode_parse from which point to start drawing the lines depending on what kind of object is the
+            # source of gcode
+            job_obj.toolchange_xy = "geometry"
+            job_obj.gcode_parse()
+
+            app_obj.progress.emit(80)
+
+        if use_thread:
+            # To be run in separate thread
+            def job_thread(app_obj):
+                with self.app.proc_container.new("Generating CNC Code"):
+                    app_obj.new_object("cncjob", outname, job_init)
+                    app_obj.inform.emit("[success]CNCjob created: %s" % outname)
+                    app_obj.progress.emit(100)
+
+            # Create a promise with the name
+            self.app.collection.promise(outname)
+            # Send to worker
+            self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
+        else:
+            self.app.new_object("cncjob", outname, job_init)
+
+    # def on_plot_cb_click(self, *args):  # TODO: args not needed
+    #     if self.muted_ui:
+    #         return
+    #     self.read_form_item('plot')
+
+    def scale(self, xfactor, yfactor=None, point=None):
+        """
+        Scales all geometry by a given factor.
+
+        :param xfactor: Factor by which to scale the object's geometry/
+        :type xfactor: float
+        :param yfactor: Factor by which to scale the object's geometry/
+        :type yfactor: float
+        :return: None
+        :rtype: None
+        """
+
+        if yfactor is None:
+            yfactor = xfactor
+
+        if point is None:
+            px = 0
+            py = 0
+        else:
+            px, py = point
+
+        if type(self.solid_geometry) == list:
+            geo_list =  self.flatten(self.solid_geometry)
+            self.solid_geometry = []
+            # for g in geo_list:
+            #     self.solid_geometry.append(affinity.scale(g, xfactor, yfactor, origin=(px, py)))
+            self.solid_geometry = [affinity.scale(g, xfactor, yfactor, origin=(px, py))
+                                   for g in geo_list]
+        else:
+            self.solid_geometry = affinity.scale(self.solid_geometry, xfactor, yfactor,
+                                                 origin=(px, py))
+
+    def offset(self, vect):
+        """
+        Offsets all geometry by a given vector/
+
+        :param vect: (x, y) vector by which to offset the object's geometry.
+        :type vect: tuple
+        :return: None
+        :rtype: None
+        """
+
+        dx, dy = vect
+
+        def translate_recursion(geom):
+            if type(geom) == list:
+                geoms=list()
+                for local_geom in geom:
+                    geoms.append(translate_recursion(local_geom))
+                return geoms
+            else:
+                return  affinity.translate(geom, xoff=dx, yoff=dy)
+
+        if self.multigeo is True:
+            for tool in self.tools:
+                self.tools[tool]['solid_geometry'] = translate_recursion(self.tools[tool]['solid_geometry'])
+        else:
+            self.solid_geometry=translate_recursion(self.solid_geometry)
+
+    def convert_units(self, units):
+        self.ui_disconnect()
+
+        factor = Geometry.convert_units(self, units)
+
+        self.options['cutz'] *= factor
+        self.options['depthperpass'] *= factor
+        self.options['travelz'] *= factor
+        self.options['feedrate'] *= factor
+        self.options['feedrate_z'] *= factor
+        self.options['feedrate_rapid'] *= factor
+        self.options['endz'] *= factor
+        # self.options['cnctooldia'] *= factor
+        self.options['painttooldia'] *= factor
+        self.options['paintmargin'] *= factor
+        self.options['paintoverlap'] *= factor
+
+        self.options["toolchangez"] *= factor
+
+        coords_xy = [float(eval(coord)) for coord in self.app.defaults["geometry_toolchangexy"].split(",")]
+        coords_xy[0] *= factor
+        coords_xy[1] *= factor
+        self.options['toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1])
+
+        if self.options['startz'] is not None:
+            self.options['startz'] *= factor
+
+        param_list = ['cutz', 'depthperpass', 'travelz', 'feedrate', 'feedrate_z', 'feedrate_rapid',
+                      'endz', 'toolchangez']
+
+        temp_tools_dict = {}
+        tool_dia_copy = {}
+        data_copy = {}
+        for tooluid_key, tooluid_value in self.tools.items():
+            for dia_key, dia_value in tooluid_value.items():
+                if dia_key == 'tooldia':
+                    dia_value *= factor
+                    dia_value = float('%.4f' % dia_value)
+                    tool_dia_copy[dia_key] = dia_value
+                if dia_key == 'offset':
+                    tool_dia_copy[dia_key] = dia_value
+                if dia_key == 'offset_value':
+                    dia_value *= factor
+                    tool_dia_copy[dia_key] = dia_value
+
+                    # convert the value in the Custom Tool Offset entry in UI
+                    custom_offset = self.ui.tool_offset_entry.get_value()
+                    if custom_offset:
+                        custom_offset *= factor
+                        self.ui.tool_offset_entry.set_value(custom_offset)
+
+                if dia_key == 'type':
+                    tool_dia_copy[dia_key] = dia_value
+                if dia_key == 'tool_type':
+                    tool_dia_copy[dia_key] = dia_value
+                if dia_key == 'data':
+                    for data_key, data_value in dia_value.items():
+                        # convert the form fields that are convertible
+                        for param in param_list:
+                            if data_key == param and data_value is not None:
+                                data_copy[data_key] = data_value * factor
+                        # copy the other dict entries that are not convertible
+                        if data_key not in param_list:
+                            data_copy[data_key] = data_value
+                    tool_dia_copy[dia_key] = dict(data_copy)
+                    data_copy.clear()
+
+            temp_tools_dict.update({
+                tooluid_key: dict(tool_dia_copy)
+            })
+            tool_dia_copy.clear()
+
+
+        self.tools.clear()
+        self.tools = dict(temp_tools_dict)
+
+        # if there is a value in the new tool field then convert that one too
+        tooldia = self.ui.addtool_entry.get_value()
+        if tooldia:
+            tooldia *= factor
+            # limit the decimals to 2 for METRIC and 3 for INCH
+            if units.lower() == 'in':
+                tooldia = float('%.4f' % tooldia)
+            else:
+                tooldia = float('%.2f' % tooldia)
+
+            self.ui.addtool_entry.set_value(tooldia)
+
+        return factor
+
+    def plot_element(self, element, color='red', visible=None):
+
+        visible = visible if visible else self.options['plot']
+
+        try:
+            for sub_el in element:
+                self.plot_element(sub_el)
+
+        except TypeError:  # Element is not iterable...
+            self.add_shape(shape=element, color=color, visible=visible, layer=0)
+
+    def plot(self, visible=None):
+        """
+        Adds the object into collection.
+
+        :return: None
+        """
+
+        # Does all the required setup and returns False
+        # if the 'ptint' option is set to False.
+        if not FlatCAMObj.plot(self):
+            return
+
+        try:
+            # plot solid geometries found as members of self.tools attribute dict
+            # for MultiGeo
+            if self.multigeo == True: # geo multi tool usage
+                for tooluid_key in self.tools:
+                    solid_geometry = self.tools[tooluid_key]['solid_geometry']
+                    self.plot_element(solid_geometry, visible=visible)
+
+            # plot solid geometry that may be an direct attribute of the geometry object
+            # for SingleGeo
+            if self.solid_geometry:
+                self.plot_element(self.solid_geometry, visible=visible)
+
+            # self.plot_element(self.solid_geometry, visible=self.options['plot'])
+            self.shapes.redraw()
+        except (ObjectDeleted, AttributeError):
+            self.shapes.clear(update=True)
+
+    def on_plot_cb_click(self, *args):
+        if self.muted_ui:
+            return
+        self.plot()
+        self.read_form_item('plot')
+
+        self.ui_disconnect()
+        cb_flag = self.ui.plot_cb.isChecked()
+        for row in range(self.ui.geo_tools_table.rowCount()):
+            table_cb = self.ui.geo_tools_table.cellWidget(row, 6)
+            if cb_flag:
+                table_cb.setChecked(True)
+            else:
+                table_cb.setChecked(False)
+        self.ui_connect()
+
+    def on_plot_cb_click_table(self):
+        # self.ui.cnc_tools_table.cellWidget(row, 2).widget().setCheckState(QtCore.Qt.Unchecked)
+        self.ui_disconnect()
+        cw = self.sender()
+        cw_index = self.ui.geo_tools_table.indexAt(cw.pos())
+        cw_row = cw_index.row()
+        check_row = 0
+
+        self.shapes.clear(update=True)
+        for tooluid_key in self.tools:
+            solid_geometry = self.tools[tooluid_key]['solid_geometry']
+
+            # find the geo_tool_table row associated with the tooluid_key
+            for row in range(self.ui.geo_tools_table.rowCount()):
+                tooluid_item = int(self.ui.geo_tools_table.item(row, 5).text())
+                if tooluid_item == int(tooluid_key):
+                    check_row = row
+                    break
+            if self.ui.geo_tools_table.cellWidget(check_row, 6).isChecked():
+                self.plot_element(element=solid_geometry, visible=True)
+        self.shapes.redraw()
+
+        # make sure that the general plot is disabled if one of the row plot's are disabled and
+        # if all the row plot's are enabled also enable the general plot checkbox
+        cb_cnt = 0
+        total_row = self.ui.geo_tools_table.rowCount()
+        for row in range(total_row):
+            if self.ui.geo_tools_table.cellWidget(row, 6).isChecked():
+                cb_cnt += 1
+            else:
+                cb_cnt -= 1
+        if cb_cnt < total_row:
+            self.ui.plot_cb.setChecked(False)
+        else:
+            self.ui.plot_cb.setChecked(True)
+        self.ui_connect()
+
+class FlatCAMCNCjob(FlatCAMObj, CNCjob):
+    """
+    Represents G-Code.
+    """
+    optionChanged = QtCore.pyqtSignal(str)
+    ui_type = CNCObjectUI
+
+    def __init__(self, name, units="in", kind="generic", z_move=0.1,
+                 feedrate=3.0, feedrate_rapid=3.0, z_cut=-0.002, tooldia=0.0,
+                 spindlespeed=None):
+
+        FlatCAMApp.App.log.debug("Creating CNCJob object...")
+
+        CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
+                        feedrate=feedrate, feedrate_rapid=feedrate_rapid, z_cut=z_cut, tooldia=tooldia,
+                        spindlespeed=spindlespeed, steps_per_circle=self.app.defaults["cncjob_steps_per_circle"])
+
+        FlatCAMObj.__init__(self, name)
+
+        self.kind = "cncjob"
+
+        self.options.update({
+            "plot": True,
+            "tooldia": 0.03937,  # 0.4mm in inches
+            "append": "",
+            "prepend": "",
+            "dwell": False,
+            "dwelltime": 1,
+            "type": 'Geometry'
+        })
+
+        '''
+            This is a dict of dictionaries. Each dict is associated with a tool present in the file. The key is the 
+            diameter of the tools and the value is another dict that will hold the data under the following form:
+               {tooldia:   {
+                           'tooluid': 1,
+                           'offset': 'Path',
+                           'type_item': 'Rough',
+                           'tool_type': 'C1',
+                           'data': {} # a dict to hold the parameters
+                           'gcode': "" # a string with the actual GCODE
+                           'gcode_parsed': {} # dictionary holding the CNCJob geometry and type of geometry (cut or move)
+                           'solid_geometry': []
+                           },
+                           ...
+               }
+            It is populated in the FlatCAMGeometry.mtool_gen_cncjob()
+            BEWARE: I rely on the ordered nature of the Python 3.7 dictionary. Things might change ...
+        '''
+        self.cnc_tools = {}
+
+        # for now it show if the plot will be done for multi-tool CNCJob (True) or for single tool
+        # (like the one in the TCL Command), False
+        self.multitool = False
+
+        # used for parsing the GCode lines to adjust the offset when the GCode was offseted
+        offsetx_re_string = r'(?=.*(X[-\+]?\d*\.\d*))'
+        self.g_offsetx_re = re.compile(offsetx_re_string)
+        offsety_re_string = r'(?=.*(Y[-\+]?\d*\.\d*))'
+        self.g_offsety_re = re.compile(offsety_re_string)
+        # Attributes to be included in serialization
+        # Always append to it because it carries contents
+        # from predecessors.
+        self.ser_attrs += ['options', 'kind', 'cnc_tools', 'multitool']
+
+        self.annotation = self.app.plotcanvas.new_text_group()
+
+    def build_ui(self):
+        self.ui_disconnect()
+
+        FlatCAMObj.build_ui(self)
+
+        offset = 0
+        tool_idx = 0
+
+        n = len(self.cnc_tools)
+        self.ui.cnc_tools_table.setRowCount(n)
+
+        for dia_key, dia_value in self.cnc_tools.items():
+
+            tool_idx += 1
+            row_no = tool_idx - 1
+
+            id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
+            # id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.cnc_tools_table.setItem(row_no, 0, id)  # Tool name/id
+
+            # Make sure that the tool diameter when in MM is with no more than 2 decimals.
+            # There are no tool bits in MM with more than 2 decimals diameter.
+            # For INCH the decimals should be no more than 4. There are no tools under 10mils.
+            if self.units == 'MM':
+                dia_item = QtWidgets.QTableWidgetItem('%.2f' % float(dia_value['tooldia']))
+            else:
+                dia_item = QtWidgets.QTableWidgetItem('%.4f' % float(dia_value['tooldia']))
+
+            offset_txt = list(str(dia_value['offset']))
+            offset_txt[0] = offset_txt[0].upper()
+            offset_item = QtWidgets.QTableWidgetItem(''.join(offset_txt))
+            type_item = QtWidgets.QTableWidgetItem(str(dia_value['type']))
+            tool_type_item = QtWidgets.QTableWidgetItem(str(dia_value['tool_type']))
+
+            id.setFlags(QtCore.Qt.ItemIsEnabled)
+            dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
+            offset_item.setFlags(QtCore.Qt.ItemIsEnabled)
+            type_item.setFlags(QtCore.Qt.ItemIsEnabled)
+            tool_type_item.setFlags(QtCore.Qt.ItemIsEnabled)
+
+            # hack so the checkbox stay centered in the table cell
+            # used this:
+            # https://stackoverflow.com/questions/32458111/pyqt-allign-checkbox-and-put-it-in-every-row
+            # plot_item = QtWidgets.QWidget()
+            # checkbox = FCCheckBox()
+            # checkbox.setCheckState(QtCore.Qt.Checked)
+            # qhboxlayout = QtWidgets.QHBoxLayout(plot_item)
+            # qhboxlayout.addWidget(checkbox)
+            # qhboxlayout.setAlignment(QtCore.Qt.AlignCenter)
+            # qhboxlayout.setContentsMargins(0, 0, 0, 0)
+            plot_item = FCCheckBox()
+            plot_item.setLayoutDirection(QtCore.Qt.RightToLeft)
+            tool_uid_item = QtWidgets.QTableWidgetItem(str(dia_key))
+            if self.ui.plot_cb.isChecked():
+                plot_item.setChecked(True)
+
+            self.ui.cnc_tools_table.setItem(row_no, 1, dia_item)  # Diameter
+            self.ui.cnc_tools_table.setItem(row_no, 2, offset_item)  # Offset
+            self.ui.cnc_tools_table.setItem(row_no, 3, type_item)  # Toolpath Type
+            self.ui.cnc_tools_table.setItem(row_no, 4, tool_type_item)  # Tool Type
+
+            ### REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY ###
+            self.ui.cnc_tools_table.setItem(row_no, 5, tool_uid_item)  # Tool unique ID)
+            self.ui.cnc_tools_table.setCellWidget(row_no, 6, plot_item)
+
+        # make the diameter column editable
+        # for row in range(tool_idx):
+        #     self.ui.cnc_tools_table.item(row, 1).setFlags(QtCore.Qt.ItemIsSelectable |
+        #                                                   QtCore.Qt.ItemIsEnabled)
+
+        for row in range(tool_idx):
+            self.ui.cnc_tools_table.item(row, 0).setFlags(
+                self.ui.cnc_tools_table.item(row, 0).flags() ^ QtCore.Qt.ItemIsSelectable)
+
+        self.ui.cnc_tools_table.resizeColumnsToContents()
+        self.ui.cnc_tools_table.resizeRowsToContents()
+
+        vertical_header = self.ui.cnc_tools_table.verticalHeader()
+        # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
+        vertical_header.hide()
+        self.ui.cnc_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header = self.ui.cnc_tools_table.horizontalHeader()
+        horizontal_header.setMinimumSectionSize(10)
+        horizontal_header.setDefaultSectionSize(70)
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(0, 20)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(4, 40)
+        horizontal_header.setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(4, 17)
+        # horizontal_header.setStretchLastSection(True)
+        self.ui.cnc_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        self.ui.cnc_tools_table.setColumnWidth(0, 20)
+        self.ui.cnc_tools_table.setColumnWidth(4, 40)
+        self.ui.cnc_tools_table.setColumnWidth(6, 17)
+
+        # self.ui.geo_tools_table.setSortingEnabled(True)
+
+        self.ui.cnc_tools_table.setMinimumHeight(self.ui.cnc_tools_table.getHeight())
+        self.ui.cnc_tools_table.setMaximumHeight(self.ui.cnc_tools_table.getHeight())
+
+        self.ui_connect()
+
+    def set_ui(self, ui):
+        FlatCAMObj.set_ui(self, ui)
+
+        FlatCAMApp.App.log.debug("FlatCAMCNCJob.set_ui()")
+
+        assert isinstance(self.ui, CNCObjectUI), \
+            "Expected a CNCObjectUI, got %s" % type(self.ui)
+
+        self.form_fields.update({
+            "plot": self.ui.plot_cb,
+            # "tooldia": self.ui.tooldia_entry,
+            "append": self.ui.append_text,
+            "prepend": self.ui.prepend_text,
+        })
+
+        # Fill form fields only on object create
+        self.to_form()
+
+        self.ui.updateplot_button.clicked.connect(self.on_updateplot_button_click)
+        self.ui.export_gcode_button.clicked.connect(self.on_exportgcode_button_click)
+        self.ui.modify_gcode_button.clicked.connect(self.on_modifygcode_button_click)
+
+    def ui_connect(self):
+        for row in range(self.ui.cnc_tools_table.rowCount()):
+            self.ui.cnc_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table)
+        self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
+
+
+    def ui_disconnect(self):
+        for row in range(self.ui.cnc_tools_table.rowCount()):
+            self.ui.cnc_tools_table.cellWidget(row, 6).clicked.disconnect(self.on_plot_cb_click_table)
+        try:
+            self.ui.plot_cb.stateChanged.disconnect(self.on_plot_cb_click)
+        except:
+            pass
+
+    def on_updateplot_button_click(self, *args):
+        """
+        Callback for the "Updata Plot" button. Reads the form for updates
+        and plots the object.
+        """
+        self.read_form()
+        self.plot()
+
+    def on_exportgcode_button_click(self, *args):
+        self.app.report_usage("cncjob_on_exportgcode_button")
+
+        self.read_form()
+
+        if 'Roland' in self.pp_excellon_name or 'Roland' in self.pp_geometry_name:
+            _filter_ = "RML1 Files (*.rol);;" \
+                       "All Files (*.*)"
+        else:
+            _filter_ = "G-Code Files (*.nc);;G-Code Files (*.txt);;G-Code Files (*.tap);;G-Code Files (*.cnc);;" \
+                       "G-Code Files (*.g-code);;All Files (*.*)"
+        try:
+            filename = str(QtWidgets.QFileDialog.getSaveFileName(
+                caption="Export G-Code ...", directory=self.app.get_last_save_folder(), filter=_filter_)[0])
+        except TypeError:
+            filename = str(QtWidgets.QFileDialog.getSaveFileName(caption="Export G-Code ...", filter=_filter_)[0])
+
+        preamble = str(self.ui.prepend_text.get_value())
+        postamble = str(self.ui.append_text.get_value())
+
+        self.export_gcode(filename, preamble=preamble, postamble=postamble)
+        self.app.file_saved.emit("gcode", filename)
+        self.app.inform.emit("[success] G-Code file saved to: %s" % filename)
+
+    def on_modifygcode_button_click(self, *args):
+        # add the tab if it was closed
+        self.app.ui.plot_tab_area.addTab(self.app.ui.cncjob_tab, "CNC Code Editor")
+
+        # delete the absolute and relative position and messages in the infobar
+        self.app.ui.position_label.setText("")
+        self.app.ui.rel_position_label.setText("")
+
+        # Switch plot_area to CNCJob tab
+        self.app.ui.plot_tab_area.setCurrentWidget(self.app.ui.cncjob_tab)
+
+        preamble = str(self.ui.prepend_text.get_value())
+        postamble = str(self.ui.append_text.get_value())
+        self.app.gcode_edited = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True)
+
+        # first clear previous text in text editor (if any)
+        self.app.ui.code_editor.clear()
+
+        # then append the text from GCode to the text editor
+        for line in self.app.gcode_edited:
+            proc_line = str(line).strip('\n')
+            self.app.ui.code_editor.append(proc_line)
+
+        self.app.ui.code_editor.moveCursor(QtGui.QTextCursor.Start)
+
+        self.app.handleTextChanged()
+        self.app.ui.show()
+
+    def gcode_header(self):
+        time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
+        marlin = False
+        try:
+            for key in self.cnc_tools[0]:
+                if self.cnc_tools[0][key]['data']['ppname_g'] == 'marlin':
+                    marlin = True
+                    break
+        except:
+            try:
+                for key in self.cnc_tools[0]:
+                    if self.cnc_tools[0][key]['data']['ppname_e'] == 'marlin':
+                        marlin = True
+                        break
+            except:
+                pass
+
+        if marlin is False:
+
+            gcode = '(G-CODE GENERATED BY FLATCAM - www.flatcam.org 2018)\n' + '\n'
+
+            gcode += '(Name: ' + str(self.options['name']) + ')\n'
+            gcode += '(Type: ' + "G-code from " + str(self.options['type']) + ')\n'
+
+            # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
+            #     gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
+
+            gcode += '(Units: ' + self.units.upper() + ')\n' + "\n"
+            gcode += '(Created on ' + time_str + ')\n' + '\n'
+
+        else:
+            gcode = ';G-CODE GENERATED BY FLATCAM - www.flatcam.org 2018\n' + '\n'
+
+            gcode += ';Name: ' + str(self.options['name']) + '\n'
+            gcode += ';Type: ' + "G-code from " + str(p['options']['type']) + '\n'
+
+            # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
+            #     gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
+
+            gcode += ';Units: ' + self.units.upper() + '\n' + "\n"
+            gcode += ';Created on ' + time_str + '\n' + '\n'
+
+        return gcode
+
+    def export_gcode(self, filename=None, preamble='', postamble='', to_file=False):
+        gcode = ''
+        roland = False
+
+        # detect if using Roland postprocessor
+        try:
+            for key in self.cnc_tools:
+                if self.cnc_tools[key]['data']['ppname_g'] == 'Roland_MDX_20':
+                    roland = True
+                    break
+        except:
+            try:
+                for key in self.cnc_tools:
+                    if self.cnc_tools[key]['data']['ppname_e'] == 'Roland_MDX_20':
+                        roland = True
+                        break
+            except:
+                pass
+
+        # do not add gcode_header when using the Roland postprocessor, add it for every other postprocessor
+        if roland is False:
+            gcode = self.gcode_header()
+
+        # detect if using multi-tool and make the Gcode summation correctly for each case
+        if self.multitool is True:
+            for tooluid_key in self.cnc_tools:
+                for key, value in self.cnc_tools[tooluid_key].items():
+                    if key == 'gcode':
+                        gcode += value
+                        break
+        else:
+            gcode += self.gcode
+
+        if roland is True:
+            g = preamble + gcode + postamble
+        else:
+            # fix so the preamble gets inserted in between the comments header and the actual start of GCODE
+            g_idx = gcode.rfind('G20')
+
+            # if it did not find 'G20' then search for 'G21'
+            if g_idx == -1:
+                g_idx = gcode.rfind('G21')
+
+            # if it did not find 'G20' and it did not find 'G21' then there is an error and return
+            if g_idx == -1:
+                self.app.inform.emit("[error_notcl] G-code does not have a units code: either G20 or G21")
+                return
+
+            g = gcode[:g_idx] + preamble + '\n' + gcode[g_idx:] + postamble
+
+        # lines = StringIO(self.gcode)
+        lines = StringIO(g)
+
+        ## Write
+        if filename is not None:
+            try:
+                with open(filename, 'w') as f:
+                    for line in lines:
+                        f.write(line)
+
+            except FileNotFoundError:
+                self.app.inform.emit("[warning_notcl] No such file or directory")
+                return
+        elif to_file is False:
+            # Just for adding it to the recent files list.
+            self.app.file_opened.emit("cncjob", filename)
+
+            self.app.inform.emit("[success] Saved to: " + filename)
+        else:
+            return lines
+
+    def get_gcode(self, preamble='', postamble=''):
+        #we need this to be able get_gcode separatelly for shell command export_gcode
+        return preamble + '\n' + self.gcode + "\n" + postamble
+
+    def get_svg(self):
+        # we need this to be able get_svg separately for shell command export_svg
+        pass
+
+    def on_plot_cb_click(self, *args):
+        if self.muted_ui:
+            return
+        self.plot()
+        self.read_form_item('plot')
+
+        self.ui_disconnect()
+        cb_flag = self.ui.plot_cb.isChecked()
+        for row in range(self.ui.cnc_tools_table.rowCount()):
+            table_cb = self.ui.cnc_tools_table.cellWidget(row, 6)
+            if cb_flag:
+                table_cb.setChecked(True)
+            else:
+                table_cb.setChecked(False)
+        self.ui_connect()
+
+    def on_plot_cb_click_table(self):
+        # self.ui.cnc_tools_table.cellWidget(row, 2).widget().setCheckState(QtCore.Qt.Unchecked)
+        self.ui_disconnect()
+        cw = self.sender()
+        cw_index = self.ui.cnc_tools_table.indexAt(cw.pos())
+        cw_row = cw_index.row()
+
+        self.shapes.clear(update=True)
+        for tooluid_key in self.cnc_tools:
+            tooldia = float('%.4f' % float(self.cnc_tools[tooluid_key]['tooldia']))
+            gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
+            # tool_uid = int(self.ui.cnc_tools_table.item(cw_row, 3).text())
+
+            if self.ui.cnc_tools_table.cellWidget((tooluid_key - 1), 6).isChecked():
+                self.plot2(tooldia=tooldia, obj=self, visible=True, gcode_parsed=gcode_parsed)
+
+        self.shapes.redraw()
+
+        # make sure that the general plot is disabled if one of the row plot's are disabled and
+        # if all the row plot's are enabled also enable the general plot checkbox
+        cb_cnt = 0
+        total_row = self.ui.cnc_tools_table.rowCount()
+        for row in range(total_row):
+            if self.ui.cnc_tools_table.cellWidget(row, 6).isChecked():
+                cb_cnt += 1
+            else:
+                cb_cnt -= 1
+        if cb_cnt < total_row:
+            self.ui.plot_cb.setChecked(False)
+        else:
+            self.ui.plot_cb.setChecked(True)
+        self.ui_connect()
+
+
+    def plot(self, visible=None):
+
+        # Does all the required setup and returns False
+        # if the 'ptint' option is set to False.
+        if not FlatCAMObj.plot(self):
+            return
+
+        visible = visible if visible else self.options['plot']
+
+        try:
+            if self.multitool is False: # single tool usage
+                self.plot2(tooldia=self.options["tooldia"], obj=self, visible=visible)
+            else:
+                # multiple tools usage
+                for tooluid_key in self.cnc_tools:
+                    tooldia = float('%.4f' % float(self.cnc_tools[tooluid_key]['tooldia']))
+                    gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
+                    self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed)
+            self.shapes.redraw()
+        except (ObjectDeleted, AttributeError):
+            self.shapes.clear(update=True)
+            self.annotation.clear(update=True)
+
+    def convert_units(self, units):
+        factor = CNCjob.convert_units(self, units)
+        FlatCAMApp.App.log.debug("FlatCAMCNCjob.convert_units()")
+        self.options["tooldia"] *= factor
+
+# end of file

+ 28 - 0
FlatCAMPool.py

@@ -0,0 +1,28 @@
+from PyQt5 import QtCore
+from multiprocessing import Pool
+import dill
+
+def run_dill_encoded(what):
+    fun, args = dill.loads(what)
+    print("load", fun, args)
+    return fun(*args)
+
+def apply_async(pool, fun, args):
+    print("...", fun, args)
+    print("dumps", dill.dumps((fun, args)))
+    return pool.map_async(run_dill_encoded, (dill.dumps((fun, args)),))
+
+def func1():
+    print("func")
+
+class WorkerPool(QtCore.QObject):
+
+    def __init__(self):
+        super(WorkerPool, self).__init__()
+        self.pool = Pool(2)
+
+    def add_task(self, task):
+        print("adding task", task)
+        # task['fcn'](*task['params'])
+        # print self.pool.map(task['fcn'], task['params'])
+        apply_async(self.pool, func1, ())

+ 78 - 0
FlatCAMPostProc.py

@@ -0,0 +1,78 @@
+from importlib.machinery import SourceFileLoader
+import os
+from abc import ABCMeta, abstractmethod
+from datetime import datetime
+import math
+
+#module-root dictionary of postprocessors
+import FlatCAMApp
+
+postprocessors = {}
+
+
+class ABCPostProcRegister(ABCMeta):
+    #handles postprocessors registration on instantation
+    def __new__(cls, clsname, bases, attrs):
+        newclass = super(ABCPostProcRegister, cls).__new__(cls, clsname, bases, attrs)
+        if object not in bases:
+            if newclass.__name__ in postprocessors:
+                FlatCAMApp.App.log.warning('Postprocessor %s has been overriden'%(newclass.__name__))
+            postprocessors[newclass.__name__] = newclass()  # here is your register function
+        return newclass
+
+class FlatCAMPostProc(object, metaclass=ABCPostProcRegister):
+    @abstractmethod
+    def start_code(self, p):
+        pass
+
+    @abstractmethod
+    def lift_code(self, p):
+        pass
+
+    @abstractmethod
+    def down_code(self, p):
+        pass
+
+    @abstractmethod
+    def toolchange_code(self, p):
+        pass
+
+    @abstractmethod
+    def up_to_zero_code(self, p):
+        pass
+
+    @abstractmethod
+    def rapid_code(self, p):
+        pass
+
+    @abstractmethod
+    def linear_code(self, p):
+        pass
+
+    @abstractmethod
+    def end_code(self, p):
+        pass
+
+    @abstractmethod
+    def feedrate_code(self, p):
+        pass
+
+    @abstractmethod
+    def spindle_code(self,p):
+        pass
+
+    @abstractmethod
+    def spindle_stop_code(self,p):
+        pass
+
+def load_postprocessors(app):
+    postprocessors_path_search = [os.path.join(app.data_path,'postprocessors','*.py'),
+                                  os.path.join('postprocessors', '*.py')]
+    import glob
+    for path_search in postprocessors_path_search:
+        for file in glob.glob(path_search):
+            try:
+                SourceFileLoader('FlatCAMPostProcessor', file).load_module()
+            except Exception as e:
+                app.log.error(str(e))
+    return postprocessors

+ 156 - 0
FlatCAMProcess.py

@@ -0,0 +1,156 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+from FlatCAMGUI import FlatCAMActivityView
+from PyQt5 import QtCore
+import weakref
+
+
+# import logging
+
+# log = logging.getLogger('base2')
+# #log.setLevel(logging.DEBUG)
+# log.setLevel(logging.WARNING)
+# #log.setLevel(logging.INFO)
+# formatter = logging.Formatter('[%(levelname)s] %(message)s')
+# handler = logging.StreamHandler()
+# handler.setFormatter(formatter)
+# log.addHandler(handler)
+
+
+class FCProcess(object):
+
+    app = None
+
+    def __init__(self, descr):
+        self.callbacks = {
+            "done": []
+        }
+        self.descr = descr
+        self.status = "Active"
+
+    def __del__(self):
+        self.done()
+
+    def __enter__(self):
+        pass
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        if exc_type is not None:
+            self.app.log.error("Abnormal termination of process!")
+            self.app.log.error(exc_type)
+            self.app.log.error(exc_val)
+            self.app.log.error(exc_tb)
+
+        self.done()
+
+    def done(self):
+        for fcn in self.callbacks["done"]:
+            fcn(self)
+
+    def connect(self, callback, event="done"):
+        if callback not in self.callbacks[event]:
+            self.callbacks[event].append(callback)
+
+    def disconnect(self, callback, event="done"):
+        try:
+            self.callbacks[event].remove(callback)
+        except ValueError:
+            pass
+
+    def set_status(self, status_string):
+        self.status = status_string
+
+    def status_msg(self):
+        return self.descr
+
+
+class FCProcessContainer(object):
+    """
+    This is the process container, or controller (as in MVC)
+    of the Process/Activity tracking.
+
+    FCProcessContainer keeps weak references to the FCProcess'es
+    such that their __del__ method is called when the user
+    looses track of their reference.
+    """
+
+    app = None
+
+    def __init__(self):
+
+        self.procs = []
+
+    def add(self, proc):
+
+        self.procs.append(weakref.ref(proc))
+
+    def new(self, descr):
+        proc = FCProcess(descr)
+
+        proc.connect(self.on_done, event="done")
+
+        self.add(proc)
+
+        self.on_change(proc)
+
+        return proc
+
+    def on_change(self, proc):
+        pass
+
+    def on_done(self, proc):
+        self.remove(proc)
+
+    def remove(self, proc):
+
+        to_be_removed = []
+
+        for pref in self.procs:
+            if pref() == proc or pref() is None:
+                to_be_removed.append(pref)
+
+        for pref in to_be_removed:
+            self.procs.remove(pref)
+
+
+class FCVisibleProcessContainer(QtCore.QObject, FCProcessContainer):
+    something_changed = QtCore.pyqtSignal()
+
+    def __init__(self, view):
+        assert isinstance(view, FlatCAMActivityView), \
+            "Expected a FlatCAMActivityView, got %s" % type(view)
+
+        FCProcessContainer.__init__(self)
+        QtCore.QObject.__init__(self)
+
+        self.view = view
+
+        self.something_changed.connect(self.update_view)
+
+    def on_done(self, proc):
+        self.app.log.debug("FCVisibleProcessContainer.on_done()")
+        super(FCVisibleProcessContainer, self).on_done(proc)
+
+        self.something_changed.emit()
+
+    def on_change(self, proc):
+        self.app.log.debug("FCVisibleProcessContainer.on_change()")
+        super(FCVisibleProcessContainer, self).on_change(proc)
+
+        self.something_changed.emit()
+
+    def update_view(self):
+        if len(self.procs) == 0:
+            self.view.set_idle()
+
+        elif len(self.procs) == 1:
+            self.view.set_busy(self.procs[0]().status_msg())
+
+        else:
+            self.view.set_busy("%d processes running." % len(self.procs))

+ 85 - 0
FlatCAMTool.py

@@ -0,0 +1,85 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+from PyQt5 import QtGui, QtCore, QtWidgets, QtWidgets
+from PyQt5.QtCore import Qt
+
+
+class FlatCAMTool(QtWidgets.QWidget):
+
+    toolName = "FlatCAM Generic Tool"
+
+    def __init__(self, app, parent=None):
+        """
+
+        :param app: The application this tool will run in.
+        :type app: App
+        :param parent: Qt Parent
+        :return: FlatCAMTool
+        """
+        QtWidgets.QWidget.__init__(self, parent)
+
+        # self.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum)
+
+        self.layout = QtWidgets.QVBoxLayout()
+        self.setLayout(self.layout)
+
+        self.app = app
+
+        self.menuAction = None
+
+    def install(self, icon=None, separator=None, **kwargs):
+        before = None
+
+        # 'pos' is the menu where the Action has to be installed
+        # if no 'pos' kwarg is provided then by default our Action will be installed in the menutool
+        # as it previously was
+        if 'pos' in kwargs:
+            pos = kwargs['pos']
+        else:
+            pos = self.app.ui.menutool
+
+        # 'before' is the Action in the menu stated by 'pos' kwarg, before which we want our Action to be installed
+        # if 'before' kwarg is not provided, by default our Action will be added in the last place.
+        if 'before' in kwargs:
+            before = (kwargs['before'])
+
+        # create the new Action
+        self.menuAction = QtWidgets.QAction(self)
+        # if provided, add an icon to this Action
+        if icon is not None:
+            self.menuAction.setIcon(icon)
+        # set the text name of the Action, which will be displayed in the menu
+        self.menuAction.setText(self.toolName)
+        # add a ToolTip to the new Action
+        # self.menuAction.setToolTip(self.toolTip) # currently not available
+
+        # insert the action in the position specified by 'before' and 'pos' kwargs
+        pos.insertAction(before, self.menuAction)
+
+        # if separator parameter is True add a Separator after the newly created Action
+        if separator is True:
+            pos.addSeparator()
+
+        self.menuAction.triggered.connect(self.run)
+
+    def run(self):
+
+        if self.app.tool_tab_locked is True:
+            return
+        # Remove anything else in the GUI
+        self.app.ui.tool_scroll_area.takeWidget()
+
+        # Put ourself in the GUI
+        self.app.ui.tool_scroll_area.setWidget(self)
+
+        # Switch notebook to tool page
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+
+        self.show()
+

+ 67 - 0
FlatCAMWorker.py

@@ -0,0 +1,67 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+from PyQt5 import QtCore
+
+
+class Worker(QtCore.QObject):
+    """
+    Implements a queue of tasks to be carried out in order
+    in a single independent thread.
+    """
+
+    # avoid multiple tests  for debug availability
+    pydevd_failed = False
+    task_completed = QtCore.pyqtSignal(str)
+
+    def __init__(self, app, name=None):
+        super(Worker, self).__init__()
+        self.app = app
+        self.name = name
+
+    def allow_debug(self):
+        """
+         allow debuging/breakpoints in this threads
+         should work from PyCharm and PyDev
+        :return:
+        """
+
+        if not self.pydevd_failed:
+            try:
+                import pydevd
+                pydevd.settrace(suspend=False, trace_only_current_thread=True)
+            except ImportError:
+                self.pydevd_failed=True
+
+    def run(self):
+
+        # self.app.log.debug("Worker Started!")
+
+        self.allow_debug()
+
+        # Tasks are queued in the event listener.
+        self.app.worker_task.connect(self.do_worker_task)
+
+    def do_worker_task(self, task):
+
+        # self.app.log.debug("Running task: %s" % str(task))
+
+        self.allow_debug()
+
+        if ('worker_name' in task and task['worker_name'] == self.name) or \
+                ('worker_name' not in task and self.name is None):
+
+            try:
+                task['fcn'](*task['params'])
+            except Exception as e:
+                self.app.thread_exception.emit(e)
+                raise e
+            finally:
+                self.task_completed.emit(self.name)
+
+        # self.app.log.debug("Task ignored.")

+ 44 - 0
FlatCAMWorkerStack.py

@@ -0,0 +1,44 @@
+from PyQt5 import QtCore
+from FlatCAMWorker import Worker
+import multiprocessing
+
+
+class WorkerStack(QtCore.QObject):
+
+    worker_task = QtCore.pyqtSignal(dict)               # 'worker_name', 'func', 'params'
+    thread_exception = QtCore.pyqtSignal(object)
+
+    def __init__(self):
+        super(WorkerStack, self).__init__()
+
+        self.workers = []
+        self.threads = []
+        self.load = {}                                  # {'worker_name': tasks_count}
+
+        # Create workers crew
+        for i in range(0, 2):
+            worker = Worker(self, 'Slogger-' + str(i))
+            thread = QtCore.QThread()
+
+            worker.moveToThread(thread)
+            # worker.connect(thread, QtCore.SIGNAL("started()"), worker.run)
+            thread.started.connect(worker.run)
+            worker.task_completed.connect(self.on_task_completed)
+
+            thread.start()
+
+            self.workers.append(worker)
+            self.threads.append(thread)
+            self.load[worker.name] = 0
+
+    def __del__(self):
+        for thread in self.threads:
+            thread.terminate()
+
+    def add_task(self, task):
+        worker_name = min(self.load, key=self.load.get)
+        self.load[worker_name] += 1
+        self.worker_task.emit({'worker_name': worker_name, 'fcn': task['fcn'], 'params': task['params']})
+
+    def on_task_completed(self, worker_name):
+        self.load[str(worker_name)] -= 1

+ 710 - 0
GUIElements.py

@@ -0,0 +1,710 @@
+from PyQt5 import QtGui, QtCore, QtWidgets, QtWidgets
+from copy import copy
+import re
+import logging
+
+log = logging.getLogger('base')
+
+EDIT_SIZE_HINT = 70
+
+class RadioSet(QtWidgets.QWidget):
+    activated_custom = QtCore.pyqtSignal()
+
+    def __init__(self, choices, orientation='horizontal', parent=None, stretch=None):
+        """
+        The choices are specified as a list of dictionaries containing:
+
+        * 'label': Shown in the UI
+        * 'value': The value returned is selected
+
+        :param choices: List of choices. See description.
+        :param orientation: 'horizontal' (default) of 'vertical'.
+        :param parent: Qt parent widget.
+        :type choices: list
+        """
+        super(RadioSet, self).__init__(parent)
+        self.choices = copy(choices)
+        if orientation == 'horizontal':
+            layout = QtWidgets.QHBoxLayout()
+        else:
+            layout = QtWidgets.QVBoxLayout()
+
+        group = QtWidgets.QButtonGroup(self)
+
+        for choice in self.choices:
+            choice['radio'] = QtWidgets.QRadioButton(choice['label'])
+            group.addButton(choice['radio'])
+            layout.addWidget(choice['radio'], stretch=0)
+            choice['radio'].toggled.connect(self.on_toggle)
+
+        layout.setContentsMargins(0, 0, 0, 0)
+
+        if stretch is False:
+            pass
+        else:
+            layout.addStretch()
+
+        self.setLayout(layout)
+
+        self.group_toggle_fn = lambda: None
+
+    def on_toggle(self):
+        # log.debug("Radio toggled")
+        radio = self.sender()
+        if radio.isChecked():
+            self.group_toggle_fn()
+            self.activated_custom.emit()
+        return
+
+    def get_value(self):
+        for choice in self.choices:
+            if choice['radio'].isChecked():
+                return choice['value']
+        log.error("No button was toggled in RadioSet.")
+        return None
+
+    def set_value(self, val):
+        for choice in self.choices:
+            if choice['value'] == val:
+                choice['radio'].setChecked(True)
+                return
+        log.error("Value given is not part of this RadioSet: %s" % str(val))
+
+
+# class RadioGroupChoice(QtWidgets.QWidget):
+#     def __init__(self, label_1, label_2, to_check, hide_list, show_list, parent=None):
+#         """
+#         The choices are specified as a list of dictionaries containing:
+#
+#         * 'label': Shown in the UI
+#         * 'value': The value returned is selected
+#
+#         :param choices: List of choices. See description.
+#         :param orientation: 'horizontal' (default) of 'vertical'.
+#         :param parent: Qt parent widget.
+#         :type choices: list
+#         """
+#         super().__init__(parent)
+#
+#         group = QtGui.QButtonGroup(self)
+#
+#         self.lbl1 = label_1
+#         self.lbl2 = label_2
+#         self.hide_list = hide_list
+#         self.show_list = show_list
+#
+#         self.btn1 = QtGui.QRadioButton(str(label_1))
+#         self.btn2 = QtGui.QRadioButton(str(label_2))
+#         group.addButton(self.btn1)
+#         group.addButton(self.btn2)
+#
+#         if to_check == 1:
+#             self.btn1.setChecked(True)
+#         else:
+#             self.btn2.setChecked(True)
+#
+#         self.btn1.toggled.connect(lambda: self.btn_state(self.btn1))
+#         self.btn2.toggled.connect(lambda: self.btn_state(self.btn2))
+#
+#     def btn_state(self, btn):
+#         if btn.text() == self.lbl1:
+#             if btn.isChecked() is True:
+#                 self.show_widgets(self.show_list)
+#                 self.hide_widgets(self.hide_list)
+#             else:
+#                 self.show_widgets(self.hide_list)
+#                 self.hide_widgets(self.show_list)
+#
+#     def hide_widgets(self, lst):
+#         for wgt in lst:
+#             wgt.hide()
+#
+#     def show_widgets(self, lst):
+#         for wgt in lst:
+#             wgt.show()
+
+
+class LengthEntry(QtWidgets.QLineEdit):
+    def __init__(self, output_units='IN', parent=None):
+        super(LengthEntry, self).__init__(parent)
+
+        self.output_units = output_units
+        self.format_re = re.compile(r"^([^\s]+)(?:\s([a-zA-Z]+))?$")
+
+        # Unit conversion table OUTPUT-INPUT
+        self.scales = {
+            'IN': {'IN': 1.0,
+                   'MM': 1/25.4},
+            'MM': {'IN': 25.4,
+                   'MM': 1.0}
+        }
+        self.readyToEdit = True
+
+    def mousePressEvent(self, e, Parent=None):
+        super(LengthEntry, self).mousePressEvent(e)  # required to deselect on 2e click
+        if self.readyToEdit:
+            self.selectAll()
+            self.readyToEdit = False
+
+    def focusOutEvent(self, e):
+        super(LengthEntry, self).focusOutEvent(e)  # required to remove cursor on focusOut
+        self.deselect()
+        self.readyToEdit = True
+
+    def returnPressed(self, *args, **kwargs):
+        val = self.get_value()
+        if val is not None:
+            self.set_text(str(val))
+        else:
+            log.warning("Could not interpret entry: %s" % self.get_text())
+
+    def get_value(self):
+        raw = str(self.text()).strip(' ')
+        # match = self.format_re.search(raw)
+
+        try:
+            units = raw[-2:]
+            units = self.scales[self.output_units][units.upper()]
+            value = raw[:-2]
+            return float(eval(value))*units
+        except IndexError:
+            value = raw
+            return float(eval(value))
+        except KeyError:
+            value = raw
+            return float(eval(value))
+        except:
+            log.warning("Could not parse value in entry: %s" % str(raw))
+            return None
+
+    def set_value(self, val):
+        self.setText(str('%.4f' % val))
+
+    def sizeHint(self):
+        default_hint_size = super(LengthEntry, self).sizeHint()
+        return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+
+class FloatEntry(QtWidgets.QLineEdit):
+    def __init__(self, parent=None):
+        super(FloatEntry, self).__init__(parent)
+        self.readyToEdit = True
+
+    def mousePressEvent(self, e, Parent=None):
+        super(FloatEntry, self).mousePressEvent(e)  # required to deselect on 2e click
+        if self.readyToEdit:
+            self.selectAll()
+            self.readyToEdit = False
+
+    def focusOutEvent(self, e):
+        super(FloatEntry, self).focusOutEvent(e)  # required to remove cursor on focusOut
+        self.deselect()
+        self.readyToEdit = True
+
+    def returnPressed(self, *args, **kwargs):
+        val = self.get_value()
+        if val is not None:
+            self.set_text(str(val))
+        else:
+            log.warning("Could not interpret entry: %s" % self.text())
+
+    def get_value(self):
+        raw = str(self.text()).strip(' ')
+        evaled = 0.0
+
+        try:
+            evaled = eval(raw)
+        except:
+            if evaled is not None:
+                log.error("Could not evaluate: %s" % str(raw))
+            return None
+
+        return float(evaled)
+
+    def set_value(self, val):
+        if val is not None:
+            self.setText("%.6f" % val)
+        else:
+            self.setText("")
+
+
+    def sizeHint(self):
+        default_hint_size = super(FloatEntry, self).sizeHint()
+        return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+
+class FloatEntry2(QtWidgets.QLineEdit):
+    def __init__(self, parent=None):
+        super(FloatEntry2, self).__init__(parent)
+        self.readyToEdit = True
+
+    def mousePressEvent(self, e, Parent=None):
+        super(FloatEntry2, self).mousePressEvent(e)  # required to deselect on 2e click
+        if self.readyToEdit:
+            self.selectAll()
+            self.readyToEdit = False
+
+    def focusOutEvent(self, e):
+        super(FloatEntry2, self).focusOutEvent(e)  # required to remove cursor on focusOut
+        self.deselect()
+        self.readyToEdit = True
+
+    def get_value(self):
+        raw = str(self.text()).strip(' ')
+        evaled = 0.0
+        try:
+            evaled = eval(raw)
+        except:
+            if evaled is not None:
+                log.error("Could not evaluate: %s" % str(raw))
+            return None
+
+        return float(evaled)
+
+    def set_value(self, val):
+        self.setText("%.6f" % val)
+
+    def sizeHint(self):
+        default_hint_size = super(FloatEntry2, self).sizeHint()
+        return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+
+class IntEntry(QtWidgets.QLineEdit):
+
+    def __init__(self, parent=None, allow_empty=False, empty_val=None):
+        super(IntEntry, self).__init__(parent)
+        self.allow_empty = allow_empty
+        self.empty_val = empty_val
+        self.readyToEdit = True
+
+    def mousePressEvent(self, e, Parent=None):
+        super(IntEntry, self).mousePressEvent(e)  # required to deselect on 2e click
+        if self.readyToEdit:
+            self.selectAll()
+            self.readyToEdit = False
+
+    def focusOutEvent(self, e):
+        super(IntEntry, self).focusOutEvent(e)  # required to remove cursor on focusOut
+        self.deselect()
+        self.readyToEdit = True
+
+    def get_value(self):
+
+        if self.allow_empty:
+            if str(self.text()) == "":
+                return self.empty_val
+        # make the text() first a float and then int because if text is a float type,
+        # the int() can't convert directly a "text float" into a int type.
+        ret_val = float(self.text())
+        ret_val = int(ret_val)
+        return ret_val
+
+    def set_value(self, val):
+
+        if val == self.empty_val and self.allow_empty:
+            self.setText("")
+            return
+
+        self.setText(str(val))
+
+    def sizeHint(self):
+        default_hint_size = super(IntEntry, self).sizeHint()
+        return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+
+class FCEntry(QtWidgets.QLineEdit):
+    def __init__(self, parent=None):
+        super(FCEntry, self).__init__(parent)
+        self.readyToEdit = True
+
+    def mousePressEvent(self, e, Parent=None):
+        super(FCEntry, self).mousePressEvent(e)  # required to deselect on 2e click
+        if self.readyToEdit:
+            self.selectAll()
+            self.readyToEdit = False
+
+    def focusOutEvent(self, e):
+        super(FCEntry, self).focusOutEvent(e)  # required to remove cursor on focusOut
+        self.deselect()
+        self.readyToEdit = True
+
+    def get_value(self):
+        return str(self.text())
+
+    def set_value(self, val):
+        self.setText(str(val))
+
+    def sizeHint(self):
+        default_hint_size = super(FCEntry, self).sizeHint()
+        return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+
+class FCEntry2(FCEntry):
+    def __init__(self, parent=None):
+        super(FCEntry2, self).__init__(parent)
+        self.readyToEdit = True
+
+    def set_value(self, val):
+        self.setText('%.5f' % float(val))
+
+
+class EvalEntry(QtWidgets.QLineEdit):
+    def __init__(self, parent=None):
+        super(EvalEntry, self).__init__(parent)
+        self.readyToEdit = True
+
+    def mousePressEvent(self, e, Parent=None):
+        super(EvalEntry, self).mousePressEvent(e)  # required to deselect on 2e click
+        if self.readyToEdit:
+            self.selectAll()
+            self.readyToEdit = False
+
+    def focusOutEvent(self, e):
+        super(EvalEntry, self).focusOutEvent(e)  # required to remove cursor on focusOut
+        self.deselect()
+        self.readyToEdit = True
+
+    def returnPressed(self, *args, **kwargs):
+        val = self.get_value()
+        if val is not None:
+            self.setText(str(val))
+        else:
+            log.warning("Could not interpret entry: %s" % self.get_text())
+
+    def get_value(self):
+        raw = str(self.text()).strip(' ')
+        evaled = 0.0
+        try:
+            evaled = eval(raw)
+        except:
+            if evaled is not None:
+                log.error("Could not evaluate: %s" % str(raw))
+            return None
+        return evaled
+
+    def set_value(self, val):
+        self.setText(str(val))
+
+    def sizeHint(self):
+        default_hint_size = super(EvalEntry, self).sizeHint()
+        return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+
+class EvalEntry2(QtWidgets.QLineEdit):
+    def __init__(self, parent=None):
+        super(EvalEntry2, self).__init__(parent)
+        self.readyToEdit = True
+
+    def mousePressEvent(self, e, Parent=None):
+        super(EvalEntry2, self).mousePressEvent(e)  # required to deselect on 2e click
+        if self.readyToEdit:
+            self.selectAll()
+            self.readyToEdit = False
+
+    def focusOutEvent(self, e):
+        super(EvalEntry2, self).focusOutEvent(e)  # required to remove cursor on focusOut
+        self.deselect()
+        self.readyToEdit = True
+
+    def get_value(self):
+        raw = str(self.text()).strip(' ')
+        evaled = 0.0
+        try:
+            evaled = eval(raw)
+        except:
+            if evaled is not None:
+                log.error("Could not evaluate: %s" % str(raw))
+            return None
+        return evaled
+
+    def set_value(self, val):
+        self.setText(str(val))
+
+    def sizeHint(self):
+        default_hint_size = super(EvalEntry2, self).sizeHint()
+        return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+
+class FCCheckBox(QtWidgets.QCheckBox):
+    def __init__(self, label='', parent=None):
+        super(FCCheckBox, self).__init__(str(label), parent)
+
+    def get_value(self):
+        return self.isChecked()
+
+    def set_value(self, val):
+        self.setChecked(val)
+
+    def toggle(self):
+        self.set_value(not self.get_value())
+
+
+class FCTextArea(QtWidgets.QPlainTextEdit):
+    def __init__(self, parent=None):
+        super(FCTextArea, self).__init__(parent)
+
+    def set_value(self, val):
+        self.setPlainText(val)
+
+    def get_value(self):
+        return str(self.toPlainText())
+
+    def sizeHint(self):
+        default_hint_size = super(FCTextArea, self).sizeHint()
+        return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+
+class FCTextAreaRich(QtWidgets.QTextEdit):
+    def __init__(self, parent=None):
+        super(FCTextAreaRich, self).__init__(parent)
+
+    def set_value(self, val):
+        self.setText(val)
+
+    def get_value(self):
+        return str(self.toPlainText())
+
+    def sizeHint(self):
+        default_hint_size = super(FCTextAreaRich, self).sizeHint()
+        return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+class FCComboBox(QtWidgets.QComboBox):
+    def __init__(self, parent=None):
+        super(FCComboBox, self).__init__(parent)
+        self.setFocusPolicy(QtCore.Qt.StrongFocus)
+
+    def wheelEvent(self, *args, **kwargs):
+        pass
+
+    def get_value(self):
+        return str(self.currentText())
+
+    def set_value(self, val):
+        self.setCurrentIndex(self.findText(str(val)))
+
+
+class FCInputDialog(QtWidgets.QInputDialog):
+    def __init__(self, parent=None, ok=False, val=None, title=None, text=None, min=None, max=None, decimals=None):
+        super(FCInputDialog, self).__init__(parent)
+        self.allow_empty = ok
+        self.empty_val = val
+        if title is None:
+            self.title = 'title'
+        else:
+            self.title = title
+        if text is None:
+            self.text = 'text'
+        else:
+            self.text = text
+        if min is None:
+            self.min = 0
+        else:
+            self.min = min
+        if max is None:
+            self.max = 0
+        else:
+            self.max = max
+        if decimals is None:
+            self.decimals = 6
+        else:
+            self.decimals = decimals
+
+
+    def get_value(self):
+        self.val,self.ok = self.getDouble(self, self.title, self.text, min=self.min,
+                                                      max=self.max, decimals=self.decimals)
+        return [self.val, self.ok]
+
+    # "Transform", "Enter the Angle value:"
+    def set_value(self, val):
+        pass
+
+
+class FCButton(QtWidgets.QPushButton):
+    def __init__(self, parent=None):
+        super(FCButton, self).__init__(parent)
+
+    def get_value(self):
+        return self.isChecked()
+
+    def set_value(self, val):
+        self.setText(str(val))
+
+
+class FCTab(QtWidgets.QTabWidget):
+    def __init__(self, parent=None):
+        super(FCTab, self).__init__(parent)
+        self.setTabsClosable(True)
+        self.tabCloseRequested.connect(self.closeTab)
+
+    def deleteTab(self, currentIndex):
+        widget = self.widget(currentIndex)
+        if widget is not None:
+            widget.deleteLater()
+        self.removeTab(currentIndex)
+
+    def closeTab(self, currentIndex):
+        self.removeTab(currentIndex)
+
+    def protectTab(self, currentIndex):
+        self.tabBar().setTabButton(currentIndex, QtWidgets.QTabBar.RightSide, None)
+
+
+class VerticalScrollArea(QtWidgets.QScrollArea):
+    """
+    This widget extends QtGui.QScrollArea to make a vertical-only
+    scroll area that also expands horizontally to accomodate
+    its contents.
+    """
+    def __init__(self, parent=None):
+        QtWidgets.QScrollArea.__init__(self, parent=parent)
+        self.setWidgetResizable(True)
+        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+
+    def eventFilter(self, source, event):
+        """
+        The event filter gets automatically installed when setWidget()
+        is called.
+
+        :param source:
+        :param event:
+        :return:
+        """
+        if event.type() == QtCore.QEvent.Resize and source == self.widget():
+            # log.debug("VerticalScrollArea: Widget resized:")
+            # log.debug(" minimumSizeHint().width() = %d" % self.widget().minimumSizeHint().width())
+            # log.debug(" verticalScrollBar().width() = %d" % self.verticalScrollBar().width())
+
+            self.setMinimumWidth(self.widget().sizeHint().width() +
+                                 self.verticalScrollBar().sizeHint().width())
+
+            # if self.verticalScrollBar().isVisible():
+            #     log.debug(" Scroll bar visible")
+            #     self.setMinimumWidth(self.widget().minimumSizeHint().width() +
+            #                          self.verticalScrollBar().width())
+            # else:
+            #     log.debug(" Scroll bar hidden")
+            #     self.setMinimumWidth(self.widget().minimumSizeHint().width())
+        return QtWidgets.QWidget.eventFilter(self, source, event)
+
+
+class OptionalInputSection:
+
+    def __init__(self, cb, optinputs, logic=True):
+        """
+        Associates the a checkbox with a set of inputs.
+
+        :param cb: Checkbox that enables the optional inputs.
+        :param optinputs: List of widgets that are optional.
+        :param logic: When True the logic is normal, when False the logic is in reverse
+        It means that for logic=True, when the checkbox is checked the widgets are Enabled, and
+        for logic=False, when the checkbox is checked the widgets are Disabled
+        :return:
+        """
+        assert isinstance(cb, FCCheckBox), \
+            "Expected an FCCheckBox, got %s" % type(cb)
+
+        self.cb = cb
+        self.optinputs = optinputs
+        self.logic = logic
+
+        self.on_cb_change()
+        self.cb.stateChanged.connect(self.on_cb_change)
+
+    def on_cb_change(self):
+
+        if self.cb.checkState():
+            for widget in self.optinputs:
+                if self.logic is True:
+                    widget.setEnabled(True)
+                else:
+                    widget.setEnabled(False)
+        else:
+            for widget in self.optinputs:
+                if self.logic is True:
+                    widget.setEnabled(False)
+                else:
+                    widget.setEnabled(True)
+
+
+class FCTable(QtWidgets.QTableWidget):
+    def __init__(self, parent=None):
+        super(FCTable, self).__init__(parent)
+
+    def sizeHint(self):
+        default_hint_size = super(FCTable, self).sizeHint()
+        return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+    def getHeight(self):
+        height = self.horizontalHeader().height()
+        for i in range(self.rowCount()):
+            height += self.rowHeight(i)
+        return height
+
+    def getWidth(self):
+        width = self.verticalHeader().width()
+        for i in range(self.columnCount()):
+            width += self.columnWidth(i)
+        return width
+
+    # color is in format QtGui.Qcolor(r, g, b, alfa) with or without alfa
+    def setColortoRow(self, rowIndex, color):
+        for j in range(self.columnCount()):
+            self.item(rowIndex, j).setBackground(color)
+
+    # if user is clicking an blank area inside the QTableWidget it will deselect currently selected rows
+    def mousePressEvent(self, event):
+        if self.itemAt(event.pos()) is None:
+            self.clearSelection()
+        else:
+            QtWidgets.QTableWidget.mousePressEvent(self, event)
+
+    def setupContextMenu(self):
+        self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
+
+    def addContextMenu(self, entry, call_function, icon=None):
+        action_name = str(entry)
+        action = QtWidgets.QAction(self)
+        action.setText(action_name)
+        if icon:
+            assert isinstance(icon, QtGui.QIcon), \
+                "Expected the argument to be QtGui.QIcon. Instead it is %s" % type(icon)
+            action.setIcon(icon)
+        self.addAction(action)
+        action.triggered.connect(call_function)
+
+class FCSpinner(QtWidgets.QSpinBox):
+    def __init__(self, parent=None):
+        super(FCSpinner, self).__init__(parent)
+
+    def get_value(self):
+        return str(self.value())
+
+    def set_value(self, val):
+        try:
+            k = int(val)
+        except Exception as e:
+            raise e
+        self.setValue(k)
+
+    # def sizeHint(self):
+    #     default_hint_size = super(FCSpinner, self).sizeHint()
+    #     return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+class Dialog_box(QtWidgets.QWidget):
+    def __init__(self, title=None, label=None):
+        """
+
+        :param title: string with the window title
+        :param label: string with the message inside the dialog box
+        """
+        super(Dialog_box, self).__init__()
+        self.location = (0, 0)
+        self.ok = False
+
+        dialog_box = QtWidgets.QInputDialog()
+        dialog_box.setFixedWidth(270)
+
+        self.location, self.ok = dialog_box.getText(self, title, label)
+

+ 9 - 0
LICENSE

@@ -0,0 +1,9 @@
+The MIT License (MIT)
+
+Copyright (c) 2014-2018 Juan Pablo Caram
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 815 - 0
ObjectCollection.py

@@ -0,0 +1,815 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+# from PyQt5.QtCore import QModelIndex
+from FlatCAMObj import *
+import inspect  # TODO: Remove
+import FlatCAMApp
+from PyQt5 import QtGui, QtCore, QtWidgets
+from PyQt5.QtCore import Qt
+
+
+class KeySensitiveListView(QtWidgets.QTreeView):
+    """
+    QtGui.QListView extended to emit a signal on key press.
+    """
+
+    def __init__(self, app, parent=None):
+        super(KeySensitiveListView, self).__init__(parent)
+        self.setHeaderHidden(True)
+        self.setEditTriggers(QtWidgets.QTreeView.SelectedClicked)
+
+        # self.setRootIsDecorated(False)
+        # self.setExpandsOnDoubleClick(False)
+
+        # Enable dragging and dropping onto the GUI
+        self.setAcceptDrops(True)
+        self.filename = ""
+        self.app = app
+
+    keyPressed = QtCore.pyqtSignal(int)
+
+    def keyPressEvent(self, event):
+        super(KeySensitiveListView, self).keyPressEvent(event)
+        self.keyPressed.emit(event.key())
+
+    def dragEnterEvent(self, event):
+        if event.mimeData().hasUrls:
+            event.accept()
+        else:
+            event.ignore()
+
+    def dragMoveEvent(self, event):
+        if event.mimeData().hasUrls:
+            event.accept()
+        else:
+            event.ignore()
+
+    def dropEvent(self, event):
+        if event.mimeData().hasUrls:
+            event.setDropAction(QtCore.Qt.CopyAction)
+            event.accept()
+            for url in event.mimeData().urls():
+                self.filename = str(url.toLocalFile())
+
+            if self.filename == "":
+                self.app.inform.emit("Open cancelled.")
+            else:
+                if self.filename.lower().rpartition('.')[-1] in self.app.grb_list:
+                    self.app.worker_task.emit({'fcn': self.app.open_gerber,
+                                               'params': [self.filename]})
+                else:
+                    event.ignore()
+
+                if self.filename.lower().rpartition('.')[-1] in self.app.exc_list:
+                    self.app.worker_task.emit({'fcn': self.app.open_excellon,
+                                               'params': [self.filename]})
+                else:
+                    event.ignore()
+
+                if self.filename.lower().rpartition('.')[-1] in self.app.gcode_list:
+                    self.app.worker_task.emit({'fcn': self.app.open_gcode,
+                                               'params': [self.filename]})
+                else:
+                    event.ignore()
+
+                if self.filename.lower().rpartition('.')[-1] in self.app.svg_list:
+                    object_type = 'geometry'
+                    self.app.worker_task.emit({'fcn': self.app.import_svg,
+                                               'params': [self.filename, object_type, None]})
+
+                if self.filename.lower().rpartition('.')[-1] in self.app.dxf_list:
+                    object_type = 'geometry'
+                    self.app.worker_task.emit({'fcn': self.app.import_dxf,
+                                               'params': [self.filename, object_type, None]})
+
+                if self.filename.lower().rpartition('.')[-1] in self.app.prj_list:
+                    # self.app.open_project() is not Thread Safe
+                    self.app.open_project(self.filename)
+                else:
+                    event.ignore()
+        else:
+            event.ignore()
+
+class TreeItem:
+    """
+    Item of a tree model
+    """
+
+    def __init__(self, data, icon=None, obj=None, parent_item=None):
+
+        self.parent_item = parent_item
+        self.item_data = data  # Columns string data
+        self.icon = icon  # Decoration
+        self.obj = obj  # FlatCAMObj
+
+        self.child_items = []
+
+        if parent_item:
+            parent_item.append_child(self)
+
+    def append_child(self, item):
+        self.child_items.append(item)
+        item.set_parent_item(self)
+
+    def remove_child(self, item):
+        child = self.child_items.pop(self.child_items.index(item))
+        child.obj.clear(True)
+        child.obj.delete()
+        del child.obj
+        del child
+
+    def remove_children(self):
+        for child in self.child_items:
+            child.obj.clear()
+            child.obj.delete()
+            del child.obj
+            del child
+
+        self.child_items = []
+
+    def child(self, row):
+        return self.child_items[row]
+
+    def child_count(self):
+        return len(self.child_items)
+
+    def column_count(self):
+        return len(self.item_data)
+
+    def data(self, column):
+        return self.item_data[column]
+
+    def row(self):
+        return self.parent_item.child_items.index(self)
+
+    def set_parent_item(self, parent_item):
+        self.parent_item = parent_item
+
+    def __del__(self):
+        del self.icon
+
+
+class ObjectCollection(QtCore.QAbstractItemModel):
+    """
+    Object storage and management.
+    """
+
+    groups = [
+        ("gerber", "Gerber"),
+        ("excellon", "Excellon"),
+        ("geometry", "Geometry"),
+        ("cncjob", "CNC Job")
+    ]
+
+    classdict = {
+        "gerber": FlatCAMGerber,
+        "excellon": FlatCAMExcellon,
+        "cncjob": FlatCAMCNCjob,
+        "geometry": FlatCAMGeometry
+    }
+
+    icon_files = {
+        "gerber": "share/flatcam_icon16.png",
+        "excellon": "share/drill16.png",
+        "cncjob": "share/cnc16.png",
+        "geometry": "share/geometry16.png"
+    }
+
+    root_item = None
+    # app = None
+
+    def __init__(self, app, parent=None):
+
+        QtCore.QAbstractItemModel.__init__(self)
+
+        ### Icons for the list view
+        self.icons = {}
+        for kind in ObjectCollection.icon_files:
+            self.icons[kind] = QtGui.QPixmap(ObjectCollection.icon_files[kind])
+
+        # Create root tree view item
+        self.root_item = TreeItem(["root"])
+
+        # Create group items
+        self.group_items = {}
+        for kind, title in ObjectCollection.groups:
+            item = TreeItem([title], self.icons[kind])
+            self.group_items[kind] = item
+            self.root_item.append_child(item)
+
+        # Create test sub-items
+        # for i in self.root_item.m_child_items:
+        #     print i.data(0)
+        #     i.append_child(TreeItem(["empty"]))
+
+        ### Data ###
+        self.checked_indexes = []
+
+        # Names of objects that are expected to become available.
+        # For example, when the creation of a new object will run
+        # in the background and will complete some time in the
+        # future. This is a way to reserve the name and to let other
+        # tasks know that they have to wait until available.
+        self.promises = set()
+
+        ### View
+        self.view = KeySensitiveListView(app)
+        self.view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+        self.view.setModel(self)
+        self.view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+        font = QtGui.QFont()
+        font.setPixelSize(12)
+        font.setFamily("Seagoe UI")
+        self.view.setFont(font)
+
+        ## GUI Events
+        self.view.selectionModel().selectionChanged.connect(self.on_list_selection_change)
+        self.view.activated.connect(self.on_item_activated)
+        self.view.keyPressed.connect(self.on_key)
+        self.view.clicked.connect(self.on_mouse_down)
+        self.view.customContextMenuRequested.connect(self.on_menu_request)
+
+        self.click_modifier = None
+
+    def promise(self, obj_name):
+        FlatCAMApp.App.log.debug("Object %s has been promised." % obj_name)
+        self.promises.add(obj_name)
+
+    def has_promises(self):
+        return len(self.promises) > 0
+
+    def on_key(self, key):
+        modifiers = QtWidgets.QApplication.keyboardModifiers()
+        active = self.get_active()
+        selected = self.get_selected()
+
+        if modifiers == QtCore.Qt.ControlModifier:
+            if key == QtCore.Qt.Key_A:
+                self.app.on_selectall()
+
+            if key == QtCore.Qt.Key_C:
+                self.app.on_copy_object()
+
+            if key == QtCore.Qt.Key_E:
+                self.app.on_fileopenexcellon()
+
+            if key == QtCore.Qt.Key_G:
+                self.app.on_fileopengerber()
+
+            if key == QtCore.Qt.Key_M:
+                self.app.measurement_tool.run()
+            if key == QtCore.Qt.Key_O:
+                self.app.on_file_openproject()
+
+            if key == QtCore.Qt.Key_S:
+                self.app.on_file_saveproject()
+            return
+        elif modifiers == QtCore.Qt.ShiftModifier:
+
+            # Toggle axis
+            if key == QtCore.Qt.Key_G:
+                if self.toggle_axis is False:
+                    self.app.plotcanvas.v_line.set_data(color=(0.70, 0.3, 0.3, 1.0))
+                    self.app.plotcanvas.h_line.set_data(color=(0.70, 0.3, 0.3, 1.0))
+                    self.app.plotcanvas.redraw()
+                    self.app.toggle_axis = True
+                else:
+                    self.app.plotcanvas.v_line.set_data(color=(0.0, 0.0, 0.0, 0.0))
+
+                    self.app.plotcanvas.h_line.set_data(color=(0.0, 0.0, 0.0, 0.0))
+                    self.appplotcanvas.redraw()
+                    self.app.toggle_axis = False
+
+            # Rotate Object by 90 degree CCW
+            if key == QtCore.Qt.Key_R:
+                self.app.on_rotate(silent=True, preset=-90)
+                return
+
+        else:
+            # Zoom Fit
+            if key == QtCore.Qt.Key_1:
+                self.app.on_zoom_fit(None)
+
+            # Zoom In
+            if key == QtCore.Qt.Key_2:
+                self.app.plotcanvas.zoom(1 / self.app.defaults['zoom_ratio'], self.app.mouse)
+
+            # Zoom Out
+            if key == QtCore.Qt.Key_3:
+                self.app.plotcanvas.zoom(self.app.defaults['zoom_ratio'], self.app.mouse)
+
+            # Delete
+            if key == QtCore.Qt.Key_Delete and active:
+                # Delete via the application to
+                # ensure cleanup of the GUI
+                active.app.on_delete()
+
+            # Space = Toggle Active/Inactive
+            if key == QtCore.Qt.Key_Space:
+                for select in selected:
+                    select.ui.plot_cb.toggle()
+                self.app.delete_selection_shape()
+
+            # Copy Object Name
+            if key == QtCore.Qt.Key_C:
+                self.app.on_copy_name()
+
+            # Copy Object Name
+            if key == QtCore.Qt.Key_E:
+                self.app.object2editor()
+
+            # Grid toggle
+            if key == QtCore.Qt.Key_G:
+                self.app.geo_editor.grid_snap_btn.trigger()
+
+            # Jump to coords
+            if key == QtCore.Qt.Key_J:
+                self.app.on_jump_to()
+
+            # Move tool toggle
+            if key == QtCore.Qt.Key_M:
+                self.app.move_tool.toggle()
+
+            # New Geometry
+            if key == QtCore.Qt.Key_N:
+                self.app.on_new_geometry()
+
+            # Change Units
+            if key == QtCore.Qt.Key_Q:
+                if self.app.options["units"] == 'MM':
+                    self.app.general_options_form.general_group.units_radio.set_value("IN")
+                else:
+                    self.app.general_options_form.general_group.units_radio.set_value("MM")
+                self.app.on_toggle_units()
+
+            # Rotate Object by 90 degree CW
+            if key == QtCore.Qt.Key_R:
+                self.app.on_rotate(silent=True, preset=90)
+
+            # Shell toggle
+            if key == QtCore.Qt.Key_S:
+                self.app.on_toggle_shell()
+
+            # Transform Tool
+            if key == QtCore.Qt.Key_T:
+                self.app.transform_tool.run()
+
+            # Zoom Fit
+            if key == QtCore.Qt.Key_V:
+                self.app.on_zoom_fit(None)
+
+            # Mirror on X the selected object(s)
+            if key == QtCore.Qt.Key_X:
+                self.app.on_flipx()
+
+            # Mirror on Y the selected object(s)
+            if key == QtCore.Qt.Key_Y:
+                self.app.on_flipy()
+
+            # Show shortcut list
+            if key == QtCore.Qt.Key_Ampersand:
+                self.app.on_shortcut_list()
+
+            if key == QtCore.Qt.Key_QuoteLeft:
+                self.app.on_shortcut_list()
+            return
+
+    def on_mouse_down(self, event):
+        FlatCAMApp.App.log.debug("Mouse button pressed on list")
+
+    def on_menu_request(self, pos):
+
+        sel = len(self.view.selectedIndexes()) > 0
+        self.app.ui.menuprojectenable.setEnabled(sel)
+        self.app.ui.menuprojectdisable.setEnabled(sel)
+        self.app.ui.menuprojectdelete.setEnabled(sel)
+
+        if sel:
+            self.app.ui.menuprojectgeneratecnc.setVisible(True)
+            for obj in self.get_selected():
+                if type(obj) != FlatCAMGeometry:
+                    self.app.ui.menuprojectgeneratecnc.setVisible(False)
+        else:
+            self.app.ui.menuprojectgeneratecnc.setVisible(False)
+
+        self.app.ui.menuproject.popup(self.view.mapToGlobal(pos))
+
+    def index(self, row, column=0, parent=None, *args, **kwargs):
+        if not self.hasIndex(row, column, parent):
+            return QtCore.QModelIndex()
+
+        if not parent.isValid():
+            parent_item = self.root_item
+        else:
+            parent_item = parent.internalPointer()
+
+        child_item = parent_item.child(row)
+
+        if child_item:
+            return self.createIndex(row, column, child_item)
+        else:
+            return QtCore.QModelIndex()
+
+    def parent(self, index=None):
+        if not index.isValid():
+            return QtCore.QModelIndex()
+
+        parent_item = index.internalPointer().parent_item
+
+        if parent_item == self.root_item:
+            return QtCore.QModelIndex()
+
+        return self.createIndex(parent_item.row(), 0, parent_item)
+
+    def rowCount(self, index=None, *args, **kwargs):
+        if index.column() > 0:
+            return 0
+
+        if not index.isValid():
+            parent_item = self.root_item
+        else:
+            parent_item = index.internalPointer()
+
+        return parent_item.child_count()
+
+    def columnCount(self, index=None, *args, **kwargs):
+        if index.isValid():
+            return index.internalPointer().column_count()
+        else:
+            return self.root_item.column_count()
+
+    def data(self, index, role=None):
+        if not index.isValid():
+            return None
+
+        if role in [Qt.DisplayRole, Qt.EditRole]:
+            obj = index.internalPointer().obj
+            if obj:
+                return obj.options["name"]
+            else:
+                return index.internalPointer().data(index.column())
+
+        if role == Qt.ForegroundRole:
+            obj = index.internalPointer().obj
+            if obj:
+                return QtGui.QBrush(QtCore.Qt.black) if obj.options["plot"] else QtGui.QBrush(QtCore.Qt.darkGray)
+            else:
+                return index.internalPointer().data(index.column())
+
+        elif role == Qt.DecorationRole:
+            icon = index.internalPointer().icon
+            if icon:
+                return icon
+            else:
+                return QtGui.QPixmap()
+        else:
+            return None
+
+    def setData(self, index, data, role=None):
+        if index.isValid():
+            obj = index.internalPointer().obj
+            if obj:
+                old_name = obj.options['name']
+                # rename the object
+                obj.options["name"] = str(data)
+                new_name = obj.options['name']
+
+                # update the SHELL auto-completer model data
+                try:
+                    self.app.myKeywords.remove(old_name)
+                    self.app.myKeywords.append(new_name)
+                    self.app.shell._edit.set_model_data(self.app.myKeywords)
+                except:
+                    log.debug(
+                        "setData() --> Could not remove the old object name from auto-completer model list")
+
+                obj.build_ui()
+            self.app.inform.emit("Object renamed from %s to %s" % (old_name, new_name))
+
+        return True
+
+    def flags(self, index):
+        if not index.isValid():
+            return 0
+
+        # Prevent groups from selection
+        if not index.internalPointer().obj:
+            return Qt.ItemIsEnabled
+        else:
+            return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
+
+        return QtWidgets.QAbstractItemModel.flags(self, index)
+
+    # def data(self, index, role=Qt.Qt.DisplayRole):
+    #     if not index.isValid() or not 0 <= index.row() < self.rowCount():
+    #         return QtCore.QVariant()
+    #     row = index.row()
+    #     if role == Qt.Qt.DisplayRole:
+    #         return self.object_list[row].options["name"]
+    #     if role == Qt.Qt.DecorationRole:
+    #         return self.icons[self.object_list[row].kind]
+    #     # if role == Qt.Qt.CheckStateRole:
+    #     #     if row in self.checked_indexes:
+    #     #         return Qt.Qt.Checked
+    #     #     else:
+    #     #         return Qt.Qt.Unchecked
+
+    def print_list(self):
+        for obj in self.get_list():
+            print(obj)
+
+    def append(self, obj, active=False):
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> OC.append()")
+
+        name = obj.options["name"]
+
+        # Check promises and clear if exists
+        if name in self.promises:
+            self.promises.remove(name)
+            FlatCAMApp.App.log.debug("Promised object %s became available." % name)
+            FlatCAMApp.App.log.debug("%d promised objects remaining." % len(self.promises))
+        # Prevent same name
+        while name in self.get_names():
+            ## Create a new name
+            # Ends with number?
+            FlatCAMApp.App.log.debug("new_object(): Object name (%s) exists, changing." % name)
+            match = re.search(r'(.*[^\d])?(\d+)$', name)
+            if match:  # Yes: Increment the number!
+                base = match.group(1) or ''
+                num = int(match.group(2))
+                name = base + str(num + 1)
+            else:  # No: add a number!
+                name += "_1"
+        obj.options["name"] = name
+
+        obj.set_ui(obj.ui_type())
+
+        # Required before appending (Qt MVC)
+        group = self.group_items[obj.kind]
+        group_index = self.index(group.row(), 0, QtCore.QModelIndex())
+        self.beginInsertRows(group_index, group.child_count(), group.child_count())
+
+        # Append new item
+        obj.item = TreeItem(None, self.icons[obj.kind], obj, group)
+
+        # Required after appending (Qt MVC)
+        self.endInsertRows()
+
+        # Expand group
+        if group.child_count() is 1:
+            self.view.setExpanded(group_index, True)
+
+    def get_names(self):
+        """
+        Gets a list of the names of all objects in the collection.
+
+        :return: List of names.
+        :rtype: list
+        """
+
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> OC.get_names()")
+        return [x.options['name'] for x in self.get_list()]
+
+    def get_bounds(self):
+        """
+        Finds coordinates bounding all objects in the collection.
+
+        :return: [xmin, ymin, xmax, ymax]
+        :rtype: list
+        """
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_bounds()")
+
+        # TODO: Move the operation out of here.
+
+        xmin = Inf
+        ymin = Inf
+        xmax = -Inf
+        ymax = -Inf
+
+        # for obj in self.object_list:
+        for obj in self.get_list():
+            try:
+                gxmin, gymin, gxmax, gymax = obj.bounds()
+                xmin = min([xmin, gxmin])
+                ymin = min([ymin, gymin])
+                xmax = max([xmax, gxmax])
+                ymax = max([ymax, gymax])
+            except:
+                FlatCAMApp.App.log.warning("DEV WARNING: Tried to get bounds of empty geometry.")
+
+        return [xmin, ymin, xmax, ymax]
+
+    def get_by_name(self, name, isCaseSensitive=None):
+        """
+        Fetches the FlatCAMObj with the given `name`.
+
+        :param name: The name of the object.
+        :type name: str
+        :return: The requested object or None if no such object.
+        :rtype: FlatCAMObj or None
+        """
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_by_name()")
+
+
+        if isCaseSensitive is None or isCaseSensitive is True:
+            for obj in self.get_list():
+                if obj.options['name'] == name:
+                    return obj
+        else:
+            for obj in self.get_list():
+                if obj.options['name'].lower() == name.lower():
+                    return obj
+        return None
+
+    def delete_active(self):
+        selections = self.view.selectedIndexes()
+        if len(selections) == 0:
+            return
+
+        active = selections[0].internalPointer()
+        group = active.parent_item
+
+        # update the SHELL auto-completer model data
+        name = active.obj.options['name']
+        try:
+            self.app.myKeywords.remove(name)
+            self.app.shell._edit.set_model_data(self.app.myKeywords)
+        except:
+            log.debug(
+                "delete_active() --> Could not remove the old object name from auto-completer model list")
+
+
+        self.beginRemoveRows(self.index(group.row(), 0, QtCore.QModelIndex()), active.row(), active.row())
+
+        group.remove_child(active)
+
+        # after deletion of object store the current list of objects into the self.app.all_objects_list
+        self.app.all_objects_list = self.get_list()
+
+        self.endRemoveRows()
+
+        # always go to the Project Tab after object deletion as it may be done with a shortcut key
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
+
+    def delete_all(self):
+        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.delete_all()")
+
+        self.beginResetModel()
+
+        self.checked_indexes = []
+        for group in self.root_item.child_items:
+            group.remove_children()
+
+        self.endResetModel()
+
+        self.app.plotcanvas.redraw()
+
+        self.app.all_objects_list.clear()
+
+        self.app.geo_editor.clear()
+
+        self.app.exc_editor.clear()
+
+        self.app.dblsidedtool.reset_fields()
+
+        self.app.panelize_tool.reset_fields()
+
+        self.app.cutout_tool.reset_fields()
+
+        self.app.film_tool.reset_fields()
+
+    def get_active(self):
+        """
+        Returns the active object or None
+
+        :return: FlatCAMObj or None
+        """
+        selections = self.view.selectedIndexes()
+        if len(selections) == 0:
+            return None
+
+        return selections[0].internalPointer().obj
+
+    def get_selected(self):
+        """
+        Returns list of objects selected in the view.
+
+        :return: List of objects
+        """
+        return [sel.internalPointer().obj for sel in self.view.selectedIndexes()]
+
+    def get_non_selected(self):
+        """
+        Returns list of objects non-selected in the view.
+
+        :return: List of objects
+        """
+
+        l = self.get_list()
+
+        for sel in self.get_selected():
+            l.remove(sel)
+
+        return l
+
+    def set_active(self, name):
+        """
+        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
+        """
+        try:
+            obj = self.get_by_name(name)
+            item = obj.item
+            group = self.group_items[obj.kind]
+
+            group_index = self.index(group.row(), 0, QtCore.QModelIndex())
+            item_index = self.index(item.row(), 0, group_index)
+
+            self.view.selectionModel().select(item_index, QtCore.QItemSelectionModel.Select)
+        except Exception as e:
+            log.error("[ERROR] Cause: %s" % str(e))
+            raise
+
+    def set_inactive(self, name):
+        """
+        Unselect 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
+        """
+        obj = self.get_by_name(name)
+        item = obj.item
+        group = self.group_items[obj.kind]
+
+        group_index = self.index(group.row(), 0, QtCore.QModelIndex())
+        item_index = self.index(item.row(), 0, group_index)
+
+        self.view.selectionModel().select(item_index, QtCore.QItemSelectionModel.Deselect)
+
+    def set_all_inactive(self):
+        """
+        Unselect all objects from the project list. This triggers the
+        list_selection_changed event and call on_list_selection_changed.
+
+        :return: None
+        """
+        for name in self.get_names():
+            self.set_inactive(name)
+
+    def on_list_selection_change(self, current, previous):
+        FlatCAMApp.App.log.debug("on_list_selection_change()")
+        FlatCAMApp.App.log.debug("Current: %s, Previous %s" % (str(current), str(previous)))
+
+        try:
+            obj = current.indexes()[0].internalPointer().obj
+        except IndexError:
+            FlatCAMApp.App.log.debug("on_list_selection_change(): Index Error (Nothing selected?)")
+
+            try:
+                self.app.ui.selected_scroll_area.takeWidget()
+            except:
+                FlatCAMApp.App.log.debug("Nothing to remove")
+
+            self.app.setup_component_editor()
+            return
+
+        if obj:
+            obj.build_ui()
+
+    def on_item_activated(self, index):
+        """
+        Double-click or Enter on item.
+
+        :param index: Index of the item in the list.
+        :return: None
+        """
+        a_idx = index.internalPointer().obj
+        if a_idx is None:
+            return
+        else:
+            try:
+                a_idx.build_ui()
+            except Exception as e:
+                self.app.inform.emit("[ERROR] Cause of error: %s" % str(e))
+                raise
+
+    def get_list(self):
+        obj_list = []
+        for group in self.root_item.child_items:
+            for item in group.child_items:
+                obj_list.append(item.obj)
+
+        return obj_list
+
+    def update_view(self):
+        self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())

+ 1166 - 0
ObjectUI.py

@@ -0,0 +1,1166 @@
+import sys
+from PyQt5 import QtGui, QtCore, QtWidgets, QtWidgets
+from GUIElements import FCEntry, FloatEntry, EvalEntry, FCCheckBox, FCTable, \
+    LengthEntry, FCTextArea, IntEntry, RadioSet, OptionalInputSection, FCComboBox, FloatEntry2, EvalEntry2
+from camlib import Excellon
+
+class ObjectUI(QtWidgets.QWidget):
+    """
+    Base class for the UI of FlatCAM objects. Deriving classes should
+    put UI elements in ObjectUI.custom_box (QtWidgets.QLayout).
+    """
+
+    def __init__(self, icon_file='share/flatcam_icon32.png', title='FlatCAM Object', parent=None):
+        QtWidgets.QWidget.__init__(self, parent=parent)
+
+        layout = QtWidgets.QVBoxLayout()
+        self.setLayout(layout)
+
+        ## Page Title box (spacing between children)
+        self.title_box = QtWidgets.QHBoxLayout()
+        layout.addLayout(self.title_box)
+
+        ## Page Title icon
+        pixmap = QtGui.QPixmap(icon_file)
+        self.icon = QtWidgets.QLabel()
+        self.icon.setPixmap(pixmap)
+        self.title_box.addWidget(self.icon, stretch=0)
+
+        ## Title label
+        self.title_label = QtWidgets.QLabel("<font size=5><b>" + title + "</b></font>")
+        self.title_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.title_box.addWidget(self.title_label, stretch=1)
+
+        ## Object name
+        self.name_box = QtWidgets.QHBoxLayout()
+        layout.addLayout(self.name_box)
+        name_label = QtWidgets.QLabel("Name:")
+        self.name_box.addWidget(name_label)
+        self.name_entry = FCEntry()
+        self.name_box.addWidget(self.name_entry)
+
+        ## Box box for custom widgets
+        # This gets populated in offspring implementations.
+        self.custom_box = QtWidgets.QVBoxLayout()
+        layout.addLayout(self.custom_box)
+
+        ###########################
+        ## Common to all objects ##
+        ###########################
+
+        #### Scale ####
+        self.scale_label = QtWidgets.QLabel('<b>Scale:</b>')
+        self.scale_label.setToolTip(
+            "Change the size of the object."
+        )
+        layout.addWidget(self.scale_label)
+
+        self.scale_grid = QtWidgets.QGridLayout()
+        layout.addLayout(self.scale_grid)
+
+        # Factor
+        faclabel = QtWidgets.QLabel('Factor:')
+        faclabel.setToolTip(
+            "Factor by which to multiply\n"
+            "geometric features of this object."
+        )
+        self.scale_grid.addWidget(faclabel, 0, 0)
+        self.scale_entry = FloatEntry2()
+        self.scale_entry.set_value(1.0)
+        self.scale_grid.addWidget(self.scale_entry, 0, 1)
+
+        # GO Button
+        self.scale_button = QtWidgets.QPushButton('Scale')
+        self.scale_button.setToolTip(
+            "Perform scaling operation."
+        )
+        self.scale_button.setFixedWidth(40)
+        self.scale_grid.addWidget(self.scale_button, 0, 2)
+
+        #### Offset ####
+        self.offset_label = QtWidgets.QLabel('<b>Offset:</b>')
+        self.offset_label.setToolTip(
+            "Change the position of this object."
+        )
+        layout.addWidget(self.offset_label)
+
+        self.offset_grid = QtWidgets.QGridLayout()
+        layout.addLayout(self.offset_grid)
+
+        self.offset_vectorlabel = QtWidgets.QLabel('Vector:')
+        self.offset_vectorlabel.setToolTip(
+            "Amount by which to move the object\n"
+            "in the x and y axes in (x, y) format."
+        )
+        self.offset_grid.addWidget(self.offset_vectorlabel, 0, 0)
+        self.offsetvector_entry = EvalEntry2()
+        self.offsetvector_entry.setText("(0.0, 0.0)")
+        self.offset_grid.addWidget(self.offsetvector_entry, 0, 1)
+
+        self.offset_button = QtWidgets.QPushButton('Offset')
+        self.offset_button.setToolTip(
+            "Perform the offset operation."
+        )
+        self.offset_button.setFixedWidth(40)
+        self.offset_grid.addWidget(self.offset_button, 0, 2)
+
+        layout.addStretch()
+
+
+class GerberObjectUI(ObjectUI):
+    """
+    User interface for Gerber objects.
+    """
+
+    def __init__(self, parent=None):
+        ObjectUI.__init__(self, title='Gerber Object', parent=parent)
+
+        # Plot options
+        self.plot_options_label = QtWidgets.QLabel("<b>Plot Options:</b>")
+        self.custom_box.addWidget(self.plot_options_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        grid0.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.custom_box.addLayout(grid0)
+
+        # Plot CB
+        self.plot_cb = FCCheckBox(label='Plot   ')
+        self.plot_options_label.setToolTip(
+            "Plot (show) this object."
+        )
+        self.plot_cb.setFixedWidth(50)
+        grid0.addWidget(self.plot_cb, 0, 0)
+
+        # Solid CB
+        self.solid_cb = FCCheckBox(label='Solid   ')
+        self.solid_cb.setToolTip(
+            "Solid color polygons."
+        )
+        self.solid_cb.setFixedWidth(50)
+        grid0.addWidget(self.solid_cb, 0, 1)
+
+        # Multicolored CB
+        self.multicolored_cb = FCCheckBox(label='M-Color   ')
+        self.multicolored_cb.setToolTip(
+            "Draw polygons in different colors."
+        )
+        self.multicolored_cb.setFixedWidth(55)
+        grid0.addWidget(self.multicolored_cb, 0, 2)
+
+        # Isolation Routing
+        self.isolation_routing_label = QtWidgets.QLabel("<b>Isolation Routing:</b>")
+        self.isolation_routing_label.setToolTip(
+            "Create a Geometry object with\n"
+            "toolpaths to cut outside polygons."
+        )
+        self.custom_box.addWidget(self.isolation_routing_label)
+
+        grid1 = QtWidgets.QGridLayout()
+        self.custom_box.addLayout(grid1)
+        tdlabel = QtWidgets.QLabel('Tool dia:')
+        tdlabel.setToolTip(
+            "Diameter of the cutting tool.\n"
+            "If you want to have an isolation path\n"
+            "inside the actual shape of the Gerber\n"
+            "feature, use a negative value for\n"
+            "this parameter."
+        )
+        grid1.addWidget(tdlabel, 0, 0)
+        self.iso_tool_dia_entry = LengthEntry()
+        grid1.addWidget(self.iso_tool_dia_entry, 0, 1)
+
+        passlabel = QtWidgets.QLabel('Passes:')
+        passlabel.setToolTip(
+            "Width of the isolation gap in\n"
+            "number (integer) of tool widths."
+        )
+        grid1.addWidget(passlabel, 1, 0)
+        self.iso_width_entry = IntEntry()
+        grid1.addWidget(self.iso_width_entry, 1, 1)
+
+        overlabel = QtWidgets.QLabel('Pass overlap:')
+        overlabel.setToolTip(
+            "How much (fraction) of the tool width to overlap each tool pass.\n"
+            "Example:\n"
+            "A value here of 0.25 means an overlap of 25% from the tool diameter found above."
+        )
+        grid1.addWidget(overlabel, 2, 0)
+        self.iso_overlap_entry = FloatEntry()
+        grid1.addWidget(self.iso_overlap_entry, 2, 1)
+
+        # Milling Type Radio Button
+        milling_type_label = QtWidgets.QLabel('Milling Type:')
+        milling_type_label.setToolTip(
+            "Milling type:\n"
+            "- climb / best for precision milling and to reduce tool usage\n"
+            "- conventional / useful when there is no backlash compensation"
+        )
+        grid1.addWidget(milling_type_label, 3, 0)
+        self.milling_type_radio = RadioSet([{'label': 'Climb', 'value': 'cl'},
+                                    {'label': 'Conv.', 'value': 'cv'}])
+        grid1.addWidget(self.milling_type_radio, 3, 1)
+
+        # combine all passes CB
+        self.combine_passes_cb = FCCheckBox(label='Combine')
+        self.combine_passes_cb.setToolTip(
+            "Combine all passes into one object"
+        )
+        grid1.addWidget(self.combine_passes_cb, 4, 0)
+
+        # generate follow
+        self.follow_cb = FCCheckBox(label='"Follow" Geo')
+        self.follow_cb.setToolTip(
+            "Generate a 'Follow' geometry.\n"
+            "This means that it will cut through\n"
+            "the middle of the trace.\n"
+            "Requires that the Gerber file to be\n"
+            "loaded with 'follow' parameter."
+        )
+        grid1.addWidget(self.follow_cb, 4, 1)
+
+        self.gen_iso_label = QtWidgets.QLabel("<b>Generate Isolation Geometry:</b>")
+        self.gen_iso_label.setToolTip(
+            "Create a Geometry object with toolpaths to cut \n"
+            "isolation outside, inside or on both sides of the\n"
+            "object. For a Gerber object outside means outside\n"
+            "of the Gerber feature and inside means inside of\n"
+            "the Gerber feature, if possible at all. This means\n"
+            "that only if the Gerber feature has openings inside, they\n"
+            "will be isolated. If what is wanted is to cut isolation\n"
+            "inside the actual Gerber feature, use a negative tool\n"
+            "diameter above."
+        )
+        self.custom_box.addWidget(self.gen_iso_label)
+
+        hlay_1 = QtWidgets.QHBoxLayout()
+        self.custom_box.addLayout(hlay_1)
+
+        hlay_1.addStretch()
+
+        self.generate_ext_iso_button = QtWidgets.QPushButton('Ext Geo')
+        self.generate_ext_iso_button.setToolTip(
+            "Create the Geometry Object\n"
+            "for isolation routing containing\n"
+            "only the exteriors geometry."
+        )
+        self.generate_ext_iso_button.setFixedWidth(60)
+        hlay_1.addWidget(self.generate_ext_iso_button)
+
+        self.generate_int_iso_button = QtWidgets.QPushButton('Int Geo')
+        self.generate_int_iso_button.setToolTip(
+            "Create the Geometry Object\n"
+            "for isolation routing containing\n"
+            "only the interiors geometry."
+        )
+        self.generate_int_iso_button.setFixedWidth(60)
+        hlay_1.addWidget(self.generate_int_iso_button)
+
+        self.generate_iso_button = QtWidgets.QPushButton('FULL Geo')
+        self.generate_iso_button.setToolTip(
+            "Create the Geometry Object\n"
+            "for isolation routing. It contains both\n"
+            "the interiors and exteriors geometry."
+        )
+        self.generate_iso_button.setFixedWidth(80)
+        hlay_1.addWidget(self.generate_iso_button)
+
+        # when the follow checkbox is checked then the exteriors and interiors isolation generation buttons
+        # are disabled as is doesn't make sense to have them enabled due of the nature of "follow"
+        self.ois_iso = OptionalInputSection(self.follow_cb,
+                                            [self.generate_int_iso_button, self.generate_ext_iso_button], logic=False)
+
+        ## Clear non-copper regions
+        self.clearcopper_label = QtWidgets.QLabel("<b>Clear non-copper:</b>")
+        self.clearcopper_label.setToolTip(
+            "Create a Geometry object with\n"
+            "toolpaths to cut all non-copper regions."
+        )
+        self.custom_box.addWidget(self.clearcopper_label)
+
+        self.generate_ncc_button = QtWidgets.QPushButton('Non-Copper Clear Tool')
+        self.generate_ncc_button.setToolTip(
+            "Create the Geometry Object\n"
+            "for non-copper routing."
+        )
+        self.custom_box.addWidget(self.generate_ncc_button)
+
+        ## Board cutout
+        self.board_cutout_label = QtWidgets.QLabel("<b>Board cutout:</b>")
+        self.board_cutout_label.setToolTip(
+            "Create toolpaths to cut around\n"
+            "the PCB and separate it from\n"
+            "the original board."
+        )
+        self.custom_box.addWidget(self.board_cutout_label)
+
+        self.generate_cutout_button = QtWidgets.QPushButton('Cutout Tool')
+        self.generate_cutout_button.setToolTip(
+            "Generate the geometry for\n"
+            "the board cutout."
+        )
+        self.custom_box.addWidget(self.generate_cutout_button)
+
+        ## Non-copper regions
+        self.noncopper_label = QtWidgets.QLabel("<b>Non-copper regions:</b>")
+        self.noncopper_label.setToolTip(
+            "Create polygons covering the\n"
+            "areas without copper on the PCB.\n"
+            "Equivalent to the inverse of this\n"
+            "object. Can be used to remove all\n"
+            "copper from a specified region."
+        )
+        self.custom_box.addWidget(self.noncopper_label)
+
+        grid4 = QtWidgets.QGridLayout()
+        self.custom_box.addLayout(grid4)
+
+        # Margin
+        bmlabel = QtWidgets.QLabel('Boundary Margin:')
+        bmlabel.setToolTip(
+            "Specify the edge of the PCB\n"
+            "by drawing a box around all\n"
+            "objects with this minimum\n"
+            "distance."
+        )
+        grid4.addWidget(bmlabel, 0, 0)
+        self.noncopper_margin_entry = LengthEntry()
+        grid4.addWidget(self.noncopper_margin_entry, 0, 1)
+
+        # Rounded corners
+        self.noncopper_rounded_cb = FCCheckBox(label="Rounded corners")
+        self.noncopper_rounded_cb.setToolTip(
+            "Creates a Geometry objects with polygons\n"
+            "covering the copper-free areas of the PCB."
+        )
+        grid4.addWidget(self.noncopper_rounded_cb, 1, 0, 1, 2)
+
+        self.generate_noncopper_button = QtWidgets.QPushButton('Generate Geometry')
+        self.custom_box.addWidget(self.generate_noncopper_button)
+
+        ## Bounding box
+        self.boundingbox_label = QtWidgets.QLabel('<b>Bounding Box:</b>')
+        self.custom_box.addWidget(self.boundingbox_label)
+
+        grid5 = QtWidgets.QGridLayout()
+        self.custom_box.addLayout(grid5)
+
+        bbmargin = QtWidgets.QLabel('Boundary Margin:')
+        bbmargin.setToolTip(
+            "Distance of the edges of the box\n"
+            "to the nearest polygon."
+        )
+        grid5.addWidget(bbmargin, 0, 0)
+        self.bbmargin_entry = LengthEntry()
+        grid5.addWidget(self.bbmargin_entry, 0, 1)
+
+        self.bbrounded_cb = FCCheckBox(label="Rounded corners")
+        self.bbrounded_cb.setToolTip(
+            "If the bounding box is \n"
+            "to have rounded corners\n"
+            "their radius is equal to\n"
+            "the margin."
+        )
+        grid5.addWidget(self.bbrounded_cb, 1, 0, 1, 2)
+
+        self.generate_bb_button = QtWidgets.QPushButton('Generate Geometry')
+        self.generate_bb_button.setToolTip(
+            "Genrate the Geometry object."
+        )
+        self.custom_box.addWidget(self.generate_bb_button)
+
+
+class ExcellonObjectUI(ObjectUI):
+    """
+    User interface for Excellon objects.
+    """
+
+    def __init__(self, parent=None):
+        ObjectUI.__init__(self, title='Excellon Object',
+                          icon_file='share/drill32.png',
+                          parent=parent)
+
+        #### Plot options ####
+
+        self.plot_options_label = QtWidgets.QLabel("<b>Plot Options:</b>")
+        self.custom_box.addWidget(self.plot_options_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.custom_box.addLayout(grid0)
+        self.plot_cb = FCCheckBox(label='Plot')
+        self.plot_cb.setToolTip(
+            "Plot (show) this object."
+        )
+        grid0.addWidget(self.plot_cb, 0, 0)
+        self.solid_cb = FCCheckBox(label='Solid')
+        self.solid_cb.setToolTip(
+            "Solid circles."
+        )
+        grid0.addWidget(self.solid_cb, 0, 1)
+
+        # add a frame and inside add a vertical box layout. Inside this vbox layout I add all the Drills widgets
+        # this way I can hide/show the frame
+        self.drills_frame = QtWidgets.QFrame()
+        self.drills_frame.setContentsMargins(0, 0, 0, 0)
+        self.custom_box.addWidget(self.drills_frame)
+        self.tools_box = QtWidgets.QVBoxLayout()
+        self.tools_box.setContentsMargins(0, 0, 0, 0)
+        self.drills_frame.setLayout(self.tools_box)
+
+        #### Tools Drills ####
+        self.tools_table_label = QtWidgets.QLabel('<b>Tools Table</b>')
+        self.tools_table_label.setToolTip(
+            "Tools in this Excellon object\n"
+            "when are used for drilling."
+        )
+        self.tools_box.addWidget(self.tools_table_label)
+
+        self.tools_table = FCTable()
+        self.tools_box.addWidget(self.tools_table)
+
+        self.tools_table.setColumnCount(4)
+        self.tools_table.setHorizontalHeaderLabels(['#', 'Diameter', 'D', 'S'])
+        self.tools_table.setSortingEnabled(False)
+
+        self.empty_label = QtWidgets.QLabel('')
+        self.tools_box.addWidget(self.empty_label)
+
+        #### Create CNC Job ####
+        self.cncjob_label = QtWidgets.QLabel('<b>Create CNC Job</b>')
+        self.cncjob_label.setToolTip(
+            "Create a CNC Job object\n"
+            "for this drill object."
+        )
+        self.tools_box.addWidget(self.cncjob_label)
+
+        grid1 = QtWidgets.QGridLayout()
+        self.tools_box.addLayout(grid1)
+
+        # Cut Z
+        cutzlabel = QtWidgets.QLabel('Cut Z:')
+        cutzlabel.setToolTip(
+            "Drill depth (negative)\n"
+            "below the copper surface."
+        )
+        grid1.addWidget(cutzlabel, 0, 0)
+        self.cutz_entry = LengthEntry()
+        grid1.addWidget(self.cutz_entry, 0, 1)
+
+        # Travel Z (z_move)
+        travelzlabel = QtWidgets.QLabel('Travel Z:')
+        travelzlabel.setToolTip(
+            "Tool height when travelling\n"
+            "across the XY plane."
+        )
+        grid1.addWidget(travelzlabel, 1, 0)
+        self.travelz_entry = LengthEntry()
+        grid1.addWidget(self.travelz_entry, 1, 1)
+
+        # Tool change:
+        self.toolchange_cb = FCCheckBox("Tool change")
+        self.toolchange_cb.setToolTip(
+            "Include tool-change sequence\n"
+            "in G-Code (Pause for tool change)."
+        )
+        grid1.addWidget(self.toolchange_cb, 2, 0)
+
+        # Tool change Z:
+        toolchzlabel = QtWidgets.QLabel("Tool change Z:")
+        toolchzlabel.setToolTip(
+            "Z-axis position (height) for\n"
+            "tool change."
+        )
+        grid1.addWidget(toolchzlabel, 3, 0)
+        self.toolchangez_entry = LengthEntry()
+        grid1.addWidget(self.toolchangez_entry, 3, 1)
+        self.ois_tcz_e = OptionalInputSection(self.toolchange_cb, [self.toolchangez_entry])
+
+        # Start move Z:
+        startzlabel = QtWidgets.QLabel("Start move Z:")
+        startzlabel.setToolTip(
+            "Tool height just before starting the work.\n"
+            "Delete the value if you don't need this feature."
+        )
+        grid1.addWidget(startzlabel, 4, 0)
+        self.estartz_entry = FloatEntry()
+        grid1.addWidget(self.estartz_entry, 4, 1)
+
+        # End move Z:
+        endzlabel = QtWidgets.QLabel("End move Z:")
+        endzlabel.setToolTip(
+            "Z-axis position (height) for\n"
+            "the last move."
+        )
+        grid1.addWidget(endzlabel, 5, 0)
+        self.eendz_entry = LengthEntry()
+        grid1.addWidget(self.eendz_entry, 5, 1)
+
+        # Excellon Feedrate
+        frlabel = QtWidgets.QLabel('Feedrate (Plunge):')
+        frlabel.setToolTip(
+            "Tool speed while drilling\n"
+            "(in units per minute).\n"
+            "This is for linear move G01."
+        )
+        grid1.addWidget(frlabel, 6, 0)
+        self.feedrate_entry = LengthEntry()
+        grid1.addWidget(self.feedrate_entry, 6, 1)
+
+        # Excellon Rapid Feedrate
+        fr_rapid_label = QtWidgets.QLabel('Feedrate Rapids:')
+        fr_rapid_label.setToolTip(
+            "Tool speed while drilling\n"
+            "(in units per minute).\n"
+            "This is for the rapid move G00."
+        )
+        grid1.addWidget(fr_rapid_label, 7, 0)
+        self.feedrate_rapid_entry = LengthEntry()
+        grid1.addWidget(self.feedrate_rapid_entry, 7, 1)
+
+        # Spindlespeed
+        spdlabel = QtWidgets.QLabel('Spindle speed:')
+        spdlabel.setToolTip(
+            "Speed of the spindle\n"
+            "in RPM (optional)"
+        )
+        grid1.addWidget(spdlabel, 8, 0)
+        self.spindlespeed_entry = IntEntry(allow_empty=True)
+        grid1.addWidget(self.spindlespeed_entry, 8, 1)
+
+        # Dwell
+        self.dwell_cb = FCCheckBox('Dwell:')
+        self.dwell_cb.setToolTip(
+            "Pause to allow the spindle to reach its\n"
+            "speed before cutting."
+        )
+        self.dwelltime_entry = FCEntry()
+        self.dwelltime_entry.setToolTip(
+            "Number of milliseconds for spindle to dwell."
+        )
+        grid1.addWidget(self.dwell_cb, 9, 0)
+        grid1.addWidget(self.dwelltime_entry, 9, 1)
+
+        self.ois_dwell = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
+
+        # postprocessor selection
+        pp_excellon_label = QtWidgets.QLabel("Postprocessor")
+        pp_excellon_label.setToolTip(
+            "The json file that dictates\n"
+            "gcode output."
+        )
+        self.tools_box.addWidget(pp_excellon_label)
+        self.pp_excellon_name_cb = FCComboBox()
+        self.pp_excellon_name_cb.setFocusPolicy(QtCore.Qt.StrongFocus)
+        self.tools_box.addWidget(self.pp_excellon_name_cb)
+
+        choose_tools_label = QtWidgets.QLabel(
+            "Select from the Tools Table above\n"
+            "the tools you want to include."
+        )
+        self.tools_box.addWidget(choose_tools_label)
+
+        #### Choose what to use for Gcode creation: Drills, Slots or Both
+        gcode_box = QtWidgets.QFormLayout()
+        gcode_type_label = QtWidgets.QLabel('<b>Type:    </b>')
+        gcode_type_label.setToolTip(
+            "Choose what to use for GCode generation:\n"
+            "'Drills', 'Slots' or 'Both'.\n"
+            "When choosing 'Slots' or 'Both', slots will be\n"
+            "converted to a series of drills."
+        )
+        self.excellon_gcode_type_radio = RadioSet([{'label': 'Drills', 'value': 'drills'},
+                                    {'label': 'Slots', 'value': 'slots'},
+                                    {'label': 'Both', 'value': 'both'}])
+        gcode_box.addRow(gcode_type_label, self.excellon_gcode_type_radio)
+        self.tools_box.addLayout(gcode_box)
+
+        # temporary action until I finish the feature
+        self.excellon_gcode_type_radio.setEnabled(False)
+
+        self.generate_cnc_button = QtWidgets.QPushButton('Create GCode')
+        self.generate_cnc_button.setToolTip(
+            "Generate the CNC Job."
+        )
+        self.tools_box.addWidget(self.generate_cnc_button)
+
+        #### Milling Holes Drills####
+        self.mill_hole_label = QtWidgets.QLabel('<b>Mill Holes</b>')
+        self.mill_hole_label.setToolTip(
+            "Create Geometry for milling holes."
+        )
+        self.tools_box.addWidget(self.mill_hole_label)
+
+        self.choose_tools_label2 = QtWidgets.QLabel(
+            "Select from the Tools Table above\n"
+            " the hole dias that are to be milled."
+        )
+        self.tools_box.addWidget(self.choose_tools_label2)
+
+        grid2 = QtWidgets.QGridLayout()
+        self.tools_box.addLayout(grid2)
+        self.tdlabel = QtWidgets.QLabel('Drills Tool dia:')
+        self.tdlabel.setToolTip(
+            "Diameter of the cutting tool."
+        )
+        grid2.addWidget(self.tdlabel, 0, 0)
+        self.tooldia_entry = LengthEntry()
+        grid2.addWidget(self.tooldia_entry, 0, 1)
+        self.generate_milling_button = QtWidgets.QPushButton('Mill Drills Geo')
+        self.generate_milling_button.setToolTip(
+            "Create the Geometry Object\n"
+            "for milling DRILLS toolpaths."
+        )
+        grid2.addWidget(self.generate_milling_button, 0, 2)
+
+        grid3 = QtWidgets.QGridLayout()
+        self.custom_box.addLayout(grid3)
+        self.stdlabel = QtWidgets.QLabel('Slots Tool dia:')
+        self.stdlabel.setToolTip(
+            "Diameter of the cutting tool."
+        )
+        grid3.addWidget(self.stdlabel, 0, 0)
+        self.slot_tooldia_entry = LengthEntry()
+        grid3.addWidget(self.slot_tooldia_entry, 0, 1)
+        self.generate_milling_slots_button = QtWidgets.QPushButton('Mill Slots Geo')
+        self.generate_milling_slots_button.setToolTip(
+            "Create the Geometry Object\n"
+            "for milling SLOTS toolpaths."
+        )
+        grid3.addWidget(self.generate_milling_slots_button, 0, 2)
+
+    def hide_drills(self, state=True):
+        if state is True:
+            self.drills_frame.hide()
+        else:
+            self.drills_frame.show()
+
+
+class GeometryObjectUI(ObjectUI):
+    """
+    User interface for Geometry objects.
+    """
+
+    def __init__(self, parent=None):
+        super(GeometryObjectUI, self).__init__(title='Geometry Object', icon_file='share/geometry32.png', parent=parent)
+
+        # Plot options
+        # self.plot_options_label = QtWidgets.QLabel("<b>Plot Options:</b>")
+        # self.custom_box.addWidget(self.plot_options_label)
+
+        # add a frame and inside add a vertical box layout. Inside this vbox layout I add all the Tools widgets
+        # this way I can hide/show the frame
+        self.geo_tools_frame = QtWidgets.QFrame()
+        self.geo_tools_frame.setContentsMargins(0, 0, 0, 0)
+        self.custom_box.addWidget(self.geo_tools_frame)
+        self.geo_tools_box = QtWidgets.QVBoxLayout()
+        self.geo_tools_box.setContentsMargins(0, 0, 0, 0)
+        self.geo_tools_frame.setLayout(self.geo_tools_box)
+
+        hlay_plot = QtWidgets.QHBoxLayout()
+        self.geo_tools_box.addLayout(hlay_plot)
+
+        #### Tools ####
+        self.tools_table_label = QtWidgets.QLabel('<b>Tools Table</b>')
+        self.tools_table_label.setToolTip(
+            "Tools in this Geometry object used for cutting.\n"
+            "The 'Offset' entry will set an offset for the cut.\n"
+            "'Offset' can be inside, outside, on path (none) and custom.\n"
+            "'Type' entry is only informative and it allow to know the \n"
+            "intent of using the current tool. \n"
+            "It can be Rough(ing), Finish(ing) or Iso(lation).\n"
+            "The 'Tool type'(TT) can be circular with 1 to 4 teeths(C1..C4),\n"
+            "ball(B), or V-Shaped(V). \n"
+            "When V-shaped is selected the 'Type' entry is automatically \n"
+            "set to Isolation, the CutZ parameter in the UI form is\n"
+            "grayed out and Cut Z is automatically calculated from the newly \n"
+            "showed UI form entries named V-Tip Dia and V-Tip Angle."
+        )
+        hlay_plot.addWidget(self.tools_table_label)
+
+        # Plot CB
+        # self.plot_cb = QtWidgets.QCheckBox('Plot')
+        self.plot_cb = FCCheckBox('Plot Object')
+        self.plot_cb.setToolTip(
+            "Plot (show) this object."
+        )
+        self.plot_cb.setLayoutDirection(QtCore.Qt.RightToLeft)
+        hlay_plot.addStretch()
+        hlay_plot.addWidget(self.plot_cb)
+
+        self.geo_tools_table = FCTable()
+        self.geo_tools_box.addWidget(self.geo_tools_table)
+        self.geo_tools_table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
+
+        self.geo_tools_table.setColumnCount(7)
+        self.geo_tools_table.setColumnWidth(0, 20)
+        self.geo_tools_table.setHorizontalHeaderLabels(['#', 'Dia', 'Offset', 'Type', 'TT', '', 'P'])
+        self.geo_tools_table.setColumnHidden(5, True)
+
+        self.geo_tools_table.horizontalHeaderItem(0).setToolTip(
+            "This is the Tool Number.\n"
+            "When ToolChange is checked, on toolchange event this value\n"
+            "will be showed as a T1, T2 ... Tn")
+        self.geo_tools_table.horizontalHeaderItem(1).setToolTip(
+            "Tool Diameter. It's value (in current FlatCAM units) \n"
+            "is the cut width into the material.")
+        self.geo_tools_table.horizontalHeaderItem(2).setToolTip(
+            "The value for the Offset can be:\n"
+            "- Path -> There is no offset, the tool cut will be done through the geometry line.\n"
+            "- In(side) -> The tool cut will follow the geometry inside. It will create a 'pocket'.\n"
+            "- Out(side) -> The tool cut will follow the geometry line on the outside.")
+        self.geo_tools_table.horizontalHeaderItem(3).setToolTip(
+            "The (Operation) Type has only informative value. Usually the UI form values \n"
+            "are choosed based on the operation type and this will serve as a reminder.\n"
+            "Can be 'Roughing', 'Finishing' or 'Isolation'.\n"
+            "For Roughing we may choose a lower Feedrate and multiDepth cut.\n"
+            "For Finishing we may choose a higher Feedrate, without multiDepth.\n"
+            "For Isolation we need a lower Feedrate as it use a milling bit with a fine tip.")
+        self.geo_tools_table.horizontalHeaderItem(4).setToolTip(
+            "The Tool Type (TT) can be:\n"
+            "- Circular with 1 ... 4 teeth -> it is informative only. Being circular the cut width in material\n"
+            "is exactly the tool diameter.\n"
+            "- Ball -> informative only and make reference to the Ball type endmill.\n"
+            "- V-Shape -> it will disable de Z-Cut parameter in the UI form and enable two additional UI form\n"
+            "fields: V-Tip Dia and V-Tip Angle. Adjusting those two values will adjust the Z-Cut parameter such\n"
+            "as the cut width into material will be equal with the value in the Tool Diameter column of this table.\n"
+            "Choosing the V-Shape Tool Type automatically will select the Operation Type as Isolation.")
+        self.geo_tools_table.horizontalHeaderItem(6).setToolTip(
+            "Plot column. It is visible only for MultiGeo geometries, meaning geometries that holds the geometry\n"
+            "data into the tools. For those geometries, deleting the tool will delete the geometry data also,\n"
+            "so be WARNED. From the checkboxes on each row it can be enabled/disabled the plot on canvas\n"
+            "for the corresponding tool.")
+
+        # self.geo_tools_table.setSortingEnabled(False)
+        # self.geo_tools_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+
+        # Tool Offset
+        self.grid1 = QtWidgets.QGridLayout()
+        self.geo_tools_box.addLayout(self.grid1)
+
+        self.tool_offset_lbl = QtWidgets.QLabel('Tool Offset:')
+        self.tool_offset_lbl.setToolTip(
+            "The value to offset the cut when \n"
+            "the Offset type selected is 'Offset'.\n"
+            "The value can be positive for 'outside'\n"
+            "cut and negative for 'inside' cut."
+        )
+        self.grid1.addWidget(self.tool_offset_lbl, 0, 0)
+        self.tool_offset_entry = FloatEntry()
+        spacer_lbl = QtWidgets.QLabel(" ")
+        spacer_lbl.setFixedWidth(80)
+
+        self.grid1.addWidget(self.tool_offset_entry, 0, 1)
+        self.grid1.addWidget(spacer_lbl, 0, 2)
+
+        #### Add a new Tool ####
+        hlay = QtWidgets.QHBoxLayout()
+        self.geo_tools_box.addLayout(hlay)
+
+        # self.addtool_label = QtWidgets.QLabel('<b>Tool</b>')
+        # self.addtool_label.setToolTip(
+        #     "Add/Copy/Delete a tool to the tool list."
+        # )
+        self.addtool_entry_lbl = QtWidgets.QLabel('<b>Tool Dia:</b>')
+        self.addtool_entry_lbl.setToolTip(
+            "Diameter for the new tool"
+        )
+        self.addtool_entry = FloatEntry()
+
+        # hlay.addWidget(self.addtool_label)
+        # hlay.addStretch()
+        hlay.addWidget(self.addtool_entry_lbl)
+        hlay.addWidget(self.addtool_entry)
+
+        grid2 = QtWidgets.QGridLayout()
+        self.geo_tools_box.addLayout(grid2)
+
+        self.addtool_btn = QtWidgets.QPushButton('Add')
+        self.addtool_btn.setToolTip(
+            "Add a new tool to the Tool Table\n"
+            "with the diameter specified above."
+        )
+
+        self.copytool_btn = QtWidgets.QPushButton('Copy')
+        self.copytool_btn.setToolTip(
+            "Copy a selection of tools in the Tool Table\n"
+            "by first selecting a row in the Tool Table."
+        )
+
+        self.deltool_btn = QtWidgets.QPushButton('Delete')
+        self.deltool_btn.setToolTip(
+            "Delete a selection of tools in the Tool Table\n"
+            "by first selecting a row in the Tool Table."
+        )
+
+        grid2.addWidget(self.addtool_btn, 0, 0)
+        grid2.addWidget(self.copytool_btn, 0, 1)
+        grid2.addWidget(self.deltool_btn, 0,2)
+
+        self.empty_label = QtWidgets.QLabel('')
+        self.geo_tools_box.addWidget(self.empty_label)
+
+        #-----------------------------------
+        # Create CNC Job
+        #-----------------------------------
+        #### Tools Data ####
+        self.tool_data_label = QtWidgets.QLabel('<b>Tool Data</b>')
+        self.tool_data_label.setToolTip(
+            "The data used for creating GCode.\n"
+            "Each tool store it's own set of such data."
+        )
+        self.geo_tools_box.addWidget(self.tool_data_label)
+
+        self.grid3 = QtWidgets.QGridLayout()
+        self.geo_tools_box.addLayout(self.grid3)
+
+        # Tip Dia
+        self.tipdialabel = QtWidgets.QLabel('V-Tip Dia:')
+        self.tipdialabel.setToolTip(
+            "The tip diameter for V-Shape Tool"
+        )
+        self.grid3.addWidget(self.tipdialabel, 1, 0)
+        self.tipdia_entry = LengthEntry()
+        self.grid3.addWidget(self.tipdia_entry, 1, 1)
+
+        # Tip Angle
+        self.tipanglelabel = QtWidgets.QLabel('V-Tip Angle:')
+        self.tipanglelabel.setToolTip(
+            "The tip angle for V-Shape Tool.\n"
+            "In degree."
+        )
+        self.grid3.addWidget(self.tipanglelabel, 2, 0)
+        self.tipangle_entry = LengthEntry()
+        self.grid3.addWidget(self.tipangle_entry, 2, 1)
+
+        # Cut Z
+        cutzlabel = QtWidgets.QLabel('Cut Z:')
+        cutzlabel.setToolTip(
+            "Cutting depth (negative)\n"
+            "below the copper surface."
+        )
+        self.grid3.addWidget(cutzlabel, 3, 0)
+        self.cutz_entry = LengthEntry()
+        self.grid3.addWidget(self.cutz_entry, 3, 1)
+
+        # Multi-pass
+        self.mpass_cb = FCCheckBox("Multi-Depth:")
+        self.mpass_cb.setToolTip(
+            "Use multiple passes to limit\n"
+            "the cut depth in each pass. Will\n"
+            "cut multiple times until Cut Z is\n"
+            "reached.\n"
+            "To the right, input the depth of \n"
+            "each pass (positive value)."
+        )
+        self.grid3.addWidget(self.mpass_cb, 4, 0)
+
+
+        self.maxdepth_entry = LengthEntry()
+        self.maxdepth_entry.setToolTip(
+            "Depth of each pass (positive)."
+        )
+        self.grid3.addWidget(self.maxdepth_entry, 4, 1)
+
+        self.ois_mpass_geo = OptionalInputSection(self.mpass_cb, [self.maxdepth_entry])
+
+        # Travel Z
+        travelzlabel = QtWidgets.QLabel('Travel Z:')
+        travelzlabel.setToolTip(
+            "Height of the tool when\n"
+            "moving without cutting."
+        )
+        self.grid3.addWidget(travelzlabel, 5, 0)
+        self.travelz_entry = LengthEntry()
+        self.grid3.addWidget(self.travelz_entry, 5, 1)
+
+        # Tool change:
+
+        self.toolchzlabel = QtWidgets.QLabel("Tool change Z:")
+        self.toolchzlabel.setToolTip(
+            "Z-axis position (height) for\n"
+            "tool change."
+        )
+        self.toolchangeg_cb = FCCheckBox("Tool change")
+        self.toolchangeg_cb.setToolTip(
+            "Include tool-change sequence\n"
+            "in G-Code (Pause for tool change)."
+        )
+        self.toolchangez_entry = LengthEntry()
+
+        self.grid3.addWidget(self.toolchangeg_cb, 6, 0)
+        self.grid3.addWidget(self.toolchzlabel, 7, 0)
+        self.grid3.addWidget(self.toolchangez_entry, 7, 1)
+        self.ois_tcz_geo = OptionalInputSection(self.toolchangeg_cb, [self.toolchangez_entry])
+
+        # The Z value for the start move
+        # startzlabel = QtWidgets.QLabel('Start move Z:')
+        # startzlabel.setToolTip(
+        #     "Tool height just before starting the work.\n"
+        #     "Delete the value if you don't need this feature."
+        #
+        # )
+        # self.grid3.addWidget(startzlabel, 8, 0)
+        # self.gstartz_entry = FloatEntry()
+        # self.grid3.addWidget(self.gstartz_entry, 8, 1)
+
+        # The Z value for the end move
+        endzlabel = QtWidgets.QLabel('End move Z:')
+        endzlabel.setToolTip(
+            "This is the height (Z) at which the CNC\n"
+            "will go as the last move."
+        )
+        self.grid3.addWidget(endzlabel, 9, 0)
+        self.gendz_entry = LengthEntry()
+        self.grid3.addWidget(self.gendz_entry, 9, 1)
+
+        # Feedrate X-Y
+        frlabel = QtWidgets.QLabel('Feed Rate X-Y:')
+        frlabel.setToolTip(
+            "Cutting speed in the XY\n"
+            "plane in units per minute"
+        )
+        self.grid3.addWidget(frlabel, 10, 0)
+        self.cncfeedrate_entry = LengthEntry()
+        self.grid3.addWidget(self.cncfeedrate_entry, 10, 1)
+
+        # Feedrate Z (Plunge)
+        frzlabel = QtWidgets.QLabel('Feed Rate Z (Plunge):')
+        frzlabel.setToolTip(
+            "Cutting speed in the Z\n"
+            "plane in units per minute"
+        )
+        self.grid3.addWidget(frzlabel, 11, 0)
+        self.cncplunge_entry = LengthEntry()
+        self.grid3.addWidget(self.cncplunge_entry, 11, 1)
+
+        # Feedrate rapids
+        fr_rapidlabel = QtWidgets.QLabel('Feed Rate Rapids:')
+        fr_rapidlabel.setToolTip(
+            "Cutting speed in the XY\n"
+            "plane in units per minute\n"
+            "for the rapid movements"
+        )
+        self.grid3.addWidget(fr_rapidlabel, 12, 0)
+        self.cncfeedrate_rapid_entry = LengthEntry()
+        self.grid3.addWidget(self.cncfeedrate_rapid_entry, 12, 1)
+
+        # Cut over 1st point in path
+        self.extracut_cb = FCCheckBox('Cut over 1st pt')
+        self.extracut_cb.setToolTip(
+            "In order to remove possible\n"
+            "copper leftovers where first cut\n"
+            "meet with last cut, we generate an\n"
+            "extended cut over the first cut section."
+        )
+        self.grid3.addWidget(self.extracut_cb, 13, 0)
+
+        # Spindlespeed
+        spdlabel = QtWidgets.QLabel('Spindle speed:')
+        spdlabel.setToolTip(
+            "Speed of the spindle\n"
+            "in RPM (optional)"
+        )
+        self.grid3.addWidget(spdlabel, 14, 0)
+        self.cncspindlespeed_entry = IntEntry(allow_empty=True)
+        self.grid3.addWidget(self.cncspindlespeed_entry, 14, 1)
+
+        # Dwell
+        self.dwell_cb = FCCheckBox('Dwell:')
+        self.dwell_cb.setToolTip(
+            "Pause to allow the spindle to reach its\n"
+            "speed before cutting."
+        )
+        self.dwelltime_entry = FCEntry()
+        self.dwelltime_entry.setToolTip(
+            "Number of milliseconds for spindle to dwell."
+        )
+        self.grid3.addWidget(self.dwell_cb, 15, 0)
+        self.grid3.addWidget(self.dwelltime_entry, 15, 1)
+
+        self.ois_dwell_geo = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
+
+        # postprocessor selection
+        pp_label = QtWidgets.QLabel("PostProcessor:")
+        pp_label.setToolTip(
+            "The Postprocessor file that dictates\n"
+            "Gcode output."
+        )
+        self.grid3.addWidget(pp_label, 16, 0)
+        self.pp_geometry_name_cb = FCComboBox()
+        self.pp_geometry_name_cb.setFocusPolicy(QtCore.Qt.StrongFocus)
+        self.grid3.addWidget(self.pp_geometry_name_cb, 16, 1)
+
+        warning_lbl = QtWidgets.QLabel(
+            "Add at least one tool in the tool-table.\n"
+            "Click the header to select all, or Ctrl + LMB\n"
+            "for custom selection of tools.")
+        self.grid3.addWidget(warning_lbl, 17, 0, 1, 2)
+
+        # Button
+        self.generate_cnc_button = QtWidgets.QPushButton('Generate')
+        self.generate_cnc_button.setToolTip(
+            "Generate the CNC Job object."
+        )
+        self.geo_tools_box.addWidget(self.generate_cnc_button)
+
+        #------------------------------
+        # Paint area
+        #------------------------------
+        self.paint_label = QtWidgets.QLabel('<b>Paint Area:</b>')
+        self.paint_label.setToolTip(
+            "Creates tool paths to cover the\n"
+            "whole area of a polygon (remove\n"
+            "all copper). You will be asked\n"
+            "to click on the desired polygon."
+        )
+        self.geo_tools_box.addWidget(self.paint_label)
+
+        # GO Button
+        self.paint_tool_button = QtWidgets.QPushButton('Paint Tool')
+        self.paint_tool_button.setToolTip(
+            "Launch Paint Tool in Tools Tab."
+        )
+        self.geo_tools_box.addWidget(self.paint_tool_button)
+
+
+class CNCObjectUI(ObjectUI):
+    """
+    User interface for CNCJob objects.
+    """
+
+    def __init__(self, parent=None):
+        """
+        Creates the user interface for CNCJob objects. GUI elements should
+        be placed in ``self.custom_box`` to preserve the layout.
+        """
+
+        ObjectUI.__init__(self, title='CNC Job Object', icon_file='share/cnc32.png', parent=parent)
+
+        # Scale and offset ans skew are not available for CNCJob objects.
+        # Hiding from the GUI.
+        for i in range(0, self.scale_grid.count()):
+            self.scale_grid.itemAt(i).widget().hide()
+        self.scale_label.hide()
+        self.scale_button.hide()
+
+
+        for i in range(0, self.offset_grid.count()):
+            self.offset_grid.itemAt(i).widget().hide()
+        self.offset_label.hide()
+        self.offset_button.hide()
+
+        ## Plot options
+        self.plot_options_label = QtWidgets.QLabel("<b>Plot Options:</b>")
+        self.custom_box.addWidget(self.plot_options_label)
+
+        # # Tool dia for plot
+        # tdlabel = QtWidgets.QLabel('Tool dia:')
+        # tdlabel.setToolTip(
+        #     "Diameter of the tool to be\n"
+        #     "rendered in the plot."
+        # )
+        # grid0.addWidget(tdlabel, 1, 0)
+        # self.tooldia_entry = LengthEntry()
+        # grid0.addWidget(self.tooldia_entry, 1, 1)
+
+        hlay = QtWidgets.QHBoxLayout()
+        self.custom_box.addLayout(hlay)
+
+        # CNC Tools Table for plot
+        self.cnc_tools_table_label = QtWidgets.QLabel('<b>CNC Tools Table</b>')
+        self.cnc_tools_table_label.setToolTip(
+            "Tools in this CNCJob object used for cutting.\n"
+            "The tool diameter is used for plotting on canvas.\n"
+            "The 'Offset' entry will set an offset for the cut.\n"
+            "'Offset' can be inside, outside, on path (none) and custom.\n"
+            "'Type' entry is only informative and it allow to know the \n"
+            "intent of using the current tool. \n"
+            "It can be Rough(ing), Finish(ing) or Iso(lation).\n"
+            "The 'Tool type'(TT) can be circular with 1 to 4 teeths(C1..C4),\n"
+            "ball(B), or V-Shaped(V)."
+        )
+        hlay.addWidget(self.cnc_tools_table_label)
+
+        # Plot CB
+        # self.plot_cb = QtWidgets.QCheckBox('Plot')
+        self.plot_cb = FCCheckBox('Plot Object')
+        self.plot_cb.setToolTip(
+            "Plot (show) this object."
+        )
+        self.plot_cb.setLayoutDirection(QtCore.Qt.RightToLeft)
+        hlay.addStretch()
+        hlay.addWidget(self.plot_cb)
+
+        self.cnc_tools_table = FCTable()
+        self.custom_box.addWidget(self.cnc_tools_table)
+
+        # self.cnc_tools_table.setColumnCount(4)
+        # self.cnc_tools_table.setHorizontalHeaderLabels(['#', 'Dia', 'Plot', ''])
+        # self.cnc_tools_table.setColumnHidden(3, True)
+        self.cnc_tools_table.setColumnCount(7)
+        self.cnc_tools_table.setColumnWidth(0, 20)
+        self.cnc_tools_table.setHorizontalHeaderLabels(['#', 'Dia', 'Offset', 'Type', 'TT', '', 'P'])
+        self.cnc_tools_table.setColumnHidden(5, True)
+
+        # Update plot button
+        self.updateplot_button = QtWidgets.QPushButton('Update Plot')
+        self.updateplot_button.setToolTip(
+            "Update the plot."
+        )
+        self.custom_box.addWidget(self.updateplot_button)
+
+        ##################
+        ## Export G-Code
+        ##################
+        self.export_gcode_label = QtWidgets.QLabel("<b>Export CNC Code:</b>")
+        self.export_gcode_label.setToolTip(
+            "Export and save G-Code to\n"
+            "make this object to a file."
+        )
+        self.custom_box.addWidget(self.export_gcode_label)
+
+        # Prepend text to Gerber
+        prependlabel = QtWidgets.QLabel('Prepend to CNC Code:')
+        prependlabel.setToolTip(
+            "Type here any G-Code commands you would\n"
+            "like to add to the beginning of the generated file."
+        )
+        self.custom_box.addWidget(prependlabel)
+
+        self.prepend_text = FCTextArea()
+        self.custom_box.addWidget(self.prepend_text)
+
+        # Append text to Gerber
+        appendlabel = QtWidgets.QLabel('Append to CNC Code')
+        appendlabel.setToolTip(
+            "Type here any G-Code commands you would\n"
+            "like to append to the generated file.\n"
+            "I.e.: M2 (End of program)"
+        )
+        self.custom_box.addWidget(appendlabel)
+
+        self.append_text = FCTextArea()
+        self.custom_box.addWidget(self.append_text)
+
+        h_lay = QtWidgets.QHBoxLayout()
+        h_lay.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.custom_box.addLayout(h_lay)
+
+        # Edit GCode Button
+        self.modify_gcode_button = QtWidgets.QPushButton('Edit CNC Code')
+        self.modify_gcode_button.setToolTip(
+            "Opens TAB to modify/print G-Code\n"
+            "file."
+        )
+
+        # GO Button
+        self.export_gcode_button = QtWidgets.QPushButton('Save CNC Code')
+        self.export_gcode_button.setToolTip(
+            "Opens dialog to save G-Code\n"
+            "file."
+        )
+
+        h_lay.addWidget(self.modify_gcode_button)
+        h_lay.addWidget(self.export_gcode_button)
+        # self.custom_box.addWidget(self.export_gcode_button)
+
+# end of file

+ 453 - 0
ParseDXF.py

@@ -0,0 +1,453 @@
+import re
+import itertools
+import math
+import ezdxf
+
+from shapely.geometry import LinearRing, LineString, Point, Polygon
+from shapely.affinity import translate, rotate, scale, skew, affine_transform
+import numpy
+import logging
+
+log = logging.getLogger('base2')
+import FlatCAMApp
+
+from ParseFont import *
+from ParseDXF_Spline import *
+
+
+def distance(pt1, pt2):
+    return math.sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2)
+
+
+def dxfpoint2shapely(point):
+
+    geo = Point(point.dxf.location).buffer(0.01)
+    return geo
+
+def dxfline2shapely(line):
+
+    try:
+        start = (line.dxf.start[0], line.dxf.start[1])
+        stop = (line.dxf.end[0], line.dxf.end[1])
+
+    except Exception as e:
+        log.debug(str(e))
+        return None
+
+    geo = LineString([start, stop])
+
+    return geo
+
+def dxfcircle2shapely(circle, n_points=100):
+
+    ocs = circle.ocs()
+    # if the extrusion attribute is not (0, 0, 1) then we have to change the coordinate system from OCS to WCS
+    if circle.dxf.extrusion != (0, 0, 1):
+        center_pt = ocs.to_wcs(circle.dxf.center)
+    else:
+        center_pt = circle.dxf.center
+
+    radius = circle.dxf.radius
+    geo = Point(center_pt).buffer(radius, int(n_points / 4))
+
+    return geo
+
+
+def dxfarc2shapely(arc, n_points=100):
+    # ocs = arc.ocs()
+    # # if the extrusion attribute is not (0, 0, 1) then we have to change the coordinate system from OCS to WCS
+    # if arc.dxf.extrusion != (0, 0, 1):
+    #     arc_center = ocs.to_wcs(arc.dxf.center)
+    #     start_angle = math.radians(arc.dxf.start_angle) + math.pi
+    #     end_angle = math.radians(arc.dxf.end_angle) + math.pi
+    #     dir = 'CW'
+    # else:
+    #     arc_center = arc.dxf.center
+    #     start_angle = math.radians(arc.dxf.start_angle)
+    #     end_angle = math.radians(arc.dxf.end_angle)
+    #     dir = 'CCW'
+    #
+    # center_x = arc_center[0]
+    # center_y = arc_center[1]
+    # radius = arc.dxf.radius
+    #
+    # point_list = []
+    #
+    # if start_angle > end_angle:
+    #     start_angle +=  2 * math.pi
+    #
+    # line_seg = int((n_points * (end_angle - start_angle)) / math.pi)
+    # step_angle = (end_angle - start_angle) / float(line_seg)
+    #
+    # angle = start_angle
+    # for step in range(line_seg + 1):
+    #     if dir == 'CCW':
+    #         x = center_x + radius * math.cos(angle)
+    #         y = center_y + radius * math.sin(angle)
+    #     else:
+    #         x = center_x + radius * math.cos(-angle)
+    #         y = center_y + radius * math.sin(-angle)
+    #     point_list.append((x, y))
+    #     angle += step_angle
+    #
+    #
+    # log.debug("X = %.3f, Y = %.3f, Radius = %.3f, start_angle = %.1f, stop_angle = %.1f, step_angle = %.3f, dir=%s" %
+    #           (center_x, center_y, radius, start_angle, end_angle, step_angle, dir))
+    #
+    # geo = LineString(point_list)
+    # return geo
+
+    ocs = arc.ocs()
+    # if the extrusion attribute is not (0, 0, 1) then we have to change the coordinate system from OCS to WCS
+    if arc.dxf.extrusion != (0, 0, 1):
+        arc_center = ocs.to_wcs(arc.dxf.center)
+        start_angle = arc.dxf.start_angle + 180
+        end_angle = arc.dxf.end_angle + 180
+        dir = 'CW'
+    else:
+        arc_center = arc.dxf.center
+        start_angle = arc.dxf.start_angle
+        end_angle = arc.dxf.end_angle
+        dir = 'CCW'
+
+    center_x = arc_center[0]
+    center_y = arc_center[1]
+    radius = arc.dxf.radius
+
+    point_list = []
+
+    if start_angle > end_angle:
+        start_angle = start_angle - 360
+    angle = start_angle
+
+    step_angle = float(abs(end_angle - start_angle) / n_points)
+
+    while angle <= end_angle:
+        if dir == 'CCW':
+            x = center_x + radius * math.cos(math.radians(angle))
+            y = center_y + radius * math.sin(math.radians(angle))
+        else:
+            x = center_x + radius * math.cos(math.radians(-angle))
+            y = center_y + radius * math.sin(math.radians(-angle))
+        point_list.append((x, y))
+        angle += abs(step_angle)
+
+    # in case the number of segments do not cover everything until the end of the arc
+    if angle != end_angle:
+        if dir == 'CCW':
+            x = center_x + radius * math.cos(math.radians(end_angle))
+            y = center_y + radius * math.sin(math.radians(end_angle))
+        else:
+            x = center_x + radius * math.cos(math.radians(- end_angle))
+            y = center_y + radius * math.sin(math.radians(- end_angle))
+        point_list.append((x, y))
+
+    # log.debug("X = %.3f, Y = %.3f, Radius = %.3f, start_angle = %.1f, stop_angle = %.1f, step_angle = %.3f" %
+    #           (center_x, center_y, radius, start_angle, end_angle, step_angle))
+
+    geo = LineString(point_list)
+    return geo
+
+
+def dxfellipse2shapely(ellipse, ellipse_segments=100):
+    # center = ellipse.dxf.center
+    # start_angle = ellipse.dxf.start_param
+    # end_angle = ellipse.dxf.end_param
+
+    ocs = ellipse.ocs()
+    # if the extrusion attribute is not (0, 0, 1) then we have to change the coordinate system from OCS to WCS
+    if ellipse.dxf.extrusion != (0, 0, 1):
+        center = ocs.to_wcs(ellipse.dxf.center)
+        start_angle = ocs.to_wcs(ellipse.dxf.start_param)
+        end_angle = ocs.to_wcs(ellipse.dxf.end_param)
+        dir = 'CW'
+    else:
+        center = ellipse.dxf.center
+        start_angle = ellipse.dxf.start_param
+        end_angle = ellipse.dxf.end_param
+        dir = 'CCW'
+
+    # print("Dir = %s" % dir)
+    major_axis = ellipse.dxf.major_axis
+    ratio = ellipse.dxf.ratio
+
+    points_list = []
+
+    major_axis = Vector(major_axis)
+
+    major_x = major_axis[0]
+    major_y = major_axis[1]
+
+    if start_angle >= end_angle:
+        end_angle += 2.0 * math.pi
+
+    line_seg = int((ellipse_segments * (end_angle - start_angle)) / math.pi)
+    step_angle = abs(end_angle - start_angle) / float(line_seg)
+
+    angle = start_angle
+    for step in range(line_seg + 1):
+        if dir == 'CW':
+            major_dim = normalize_2(major_axis)
+            minor_dim = normalize_2(Vector([ratio * k for k in major_axis]))
+            vx = (major_dim[0] + major_dim[1]) * math.cos(angle)
+            vy = (minor_dim[0] - minor_dim[1]) * math.sin(angle)
+            x = center[0] + major_x * vx - major_y * vy
+            y = center[1] + major_y * vx + major_x * vy
+            angle += step_angle
+        else:
+            major_dim = normalize_2(major_axis)
+            minor_dim = (Vector([ratio * k for k in major_dim]))
+            vx = (major_dim[0] + major_dim[1]) * math.cos(angle)
+            vy = (minor_dim[0] + minor_dim[1]) * math.sin(angle)
+            x = center[0] + major_x * vx + major_y * vy
+            y = center[1] + major_y * vx + major_x * vy
+            angle += step_angle
+
+        points_list.append((x, y))
+
+    geo = LineString(points_list)
+    return geo
+
+
+def dxfpolyline2shapely(polyline):
+    final_pts = []
+    pts = polyline.points()
+    for i in pts:
+        final_pts.append((i[0], i[1]))
+    if polyline.is_closed:
+        final_pts.append(final_pts[0])
+
+    geo = LineString(final_pts)
+    return geo
+
+
+def dxflwpolyline2shapely(lwpolyline):
+    final_pts = []
+
+    for point in lwpolyline:
+        x, y, _, _, _ = point
+        final_pts.append((x, y))
+    if lwpolyline.closed:
+        final_pts.append(final_pts[0])
+
+    geo = LineString(final_pts)
+    return geo
+
+
+def dxfsolid2shapely(solid):
+    iterator = 0
+    corner_list = []
+    try:
+        corner_list.append(solid[iterator])
+        iterator += 1
+    except:
+        return Polygon(corner_list)
+
+
+def dxfspline2shapely(spline):
+    with spline.edit_data() as spline_data:
+        ctrl_points = spline_data.control_points
+        knot_values = spline_data.knot_values
+    is_closed = spline.closed
+    degree = spline.dxf.degree
+
+    x_list, y_list, _ = spline2Polyline(ctrl_points, degree=degree, closed=is_closed, segments=20, knots=knot_values)
+    points_list = zip(x_list, y_list)
+
+    geo = LineString(points_list)
+    return geo
+
+
+def dxftrace2shapely(trace):
+    iterator = 0
+    corner_list = []
+    try:
+        corner_list.append(trace[iterator])
+        iterator += 1
+    except:
+        return Polygon(corner_list)
+
+
+def getdxfgeo(dxf_object):
+
+    msp = dxf_object.modelspace()
+    geos = get_geo(dxf_object, msp)
+
+    # geo_block = get_geo_from_block(dxf_object)
+
+    return geos
+
+
+def get_geo_from_insert(dxf_object, insert):
+    geo_block_transformed = []
+
+    phi = insert.dxf.rotation
+    tr = insert.dxf.insert
+    sx = insert.dxf.xscale
+    sy = insert.dxf.yscale
+    r_count = insert.dxf.row_count
+    r_spacing = insert.dxf.row_spacing
+    c_count = insert.dxf.column_count
+    c_spacing = insert.dxf.column_spacing
+
+    # print(phi, tr)
+
+    # identify the block given the 'INSERT' type entity name
+    block = dxf_object.blocks[insert.dxf.name]
+    block_coords = (block.block.dxf.base_point[0], block.block.dxf.base_point[1])
+
+    # get a list of geometries found in the block
+    geo_block = get_geo(dxf_object, block)
+
+    # iterate over the geometries found and apply any transformation found in the 'INSERT' entity attributes
+    for geo in geo_block:
+
+        # get the bounds of the geometry
+        # minx, miny, maxx, maxy = geo.bounds
+
+        if tr[0] != 0 or tr[1] != 0:
+            geo = translate(geo, (tr[0] - block_coords[0]), (tr[1] - block_coords[1]))
+
+        # support for array block insertions
+        if r_count > 1:
+            for r in range(r_count):
+                geo_block_transformed.append(translate(geo, (tr[0] + (r * r_spacing) - block_coords[0]), 0))
+        if c_count > 1:
+            for c in range(c_count):
+                geo_block_transformed.append(translate(geo, 0, (tr[1] + (c * c_spacing) - block_coords[1])))
+
+        if sx != 1 or sy != 1:
+            geo = scale(geo, sx, sy)
+        if phi != 0:
+            geo = rotate(geo, phi, origin=tr)
+
+        geo_block_transformed.append(geo)
+    return geo_block_transformed
+
+
+def get_geo(dxf_object, container):
+    # store shapely geometry here
+    geo = []
+
+    for dxf_entity in container:
+        g = []
+        # print("Entity", dxf_entity.dxftype())
+        if dxf_entity.dxftype() == 'POINT':
+            g = dxfpoint2shapely(dxf_entity,)
+        elif dxf_entity.dxftype() == 'LINE':
+            g = dxfline2shapely(dxf_entity,)
+        elif dxf_entity.dxftype() == 'CIRCLE':
+            g = dxfcircle2shapely(dxf_entity)
+        elif dxf_entity.dxftype() == 'ARC':
+            g = dxfarc2shapely(dxf_entity)
+        elif dxf_entity.dxftype() == 'ELLIPSE':
+            g = dxfellipse2shapely(dxf_entity)
+        elif dxf_entity.dxftype() == 'LWPOLYLINE':
+            g = dxflwpolyline2shapely(dxf_entity)
+        elif dxf_entity.dxftype() == 'POLYLINE':
+            g = dxfpolyline2shapely(dxf_entity)
+        elif dxf_entity.dxftype() == 'SOLID':
+            g = dxfsolid2shapely(dxf_entity)
+        elif dxf_entity.dxftype() == 'TRACE':
+            g = dxftrace2shapely(dxf_entity)
+        elif dxf_entity.dxftype() == 'SPLINE':
+            g = dxfspline2shapely(dxf_entity)
+        elif dxf_entity.dxftype() == 'INSERT':
+            g = get_geo_from_insert(dxf_object, dxf_entity)
+        else:
+            log.debug(" %s is not supported yet." % dxf_entity.dxftype())
+
+        if g is not None:
+            if type(g) == list:
+                for subg in g:
+                    geo.append(subg)
+            else:
+                geo.append(g)
+
+    return geo
+
+def getdxftext(exf_object, object_type, units=None):
+    pass
+
+# def get_geo_from_block(dxf_object):
+#     geo_block_transformed = []
+#
+#     msp = dxf_object.modelspace()
+#     # iterate through all 'INSERT' entities found in modelspace msp
+#     for insert in msp.query('INSERT'):
+#         phi = insert.dxf.rotation
+#         tr = insert.dxf.insert
+#         sx = insert.dxf.xscale
+#         sy = insert.dxf.yscale
+#         r_count = insert.dxf.row_count
+#         r_spacing = insert.dxf.row_spacing
+#         c_count = insert.dxf.column_count
+#         c_spacing = insert.dxf.column_spacing
+#
+#         # print(phi, tr)
+#
+#         # identify the block given the 'INSERT' type entity name
+#         print(insert.dxf.name)
+#         block = dxf_object.blocks[insert.dxf.name]
+#         block_coords = (block.block.dxf.base_point[0], block.block.dxf.base_point[1])
+#
+#         # get a list of geometries found in the block
+#         # store shapely geometry here
+#         geo_block = []
+#
+#         for dxf_entity in block:
+#             g = []
+#             # print("Entity", dxf_entity.dxftype())
+#             if dxf_entity.dxftype() == 'POINT':
+#                 g = dxfpoint2shapely(dxf_entity, )
+#             elif dxf_entity.dxftype() == 'LINE':
+#                 g = dxfline2shapely(dxf_entity, )
+#             elif dxf_entity.dxftype() == 'CIRCLE':
+#                 g = dxfcircle2shapely(dxf_entity)
+#             elif dxf_entity.dxftype() == 'ARC':
+#                 g = dxfarc2shapely(dxf_entity)
+#             elif dxf_entity.dxftype() == 'ELLIPSE':
+#                 g = dxfellipse2shapely(dxf_entity)
+#             elif dxf_entity.dxftype() == 'LWPOLYLINE':
+#                 g = dxflwpolyline2shapely(dxf_entity)
+#             elif dxf_entity.dxftype() == 'POLYLINE':
+#                 g = dxfpolyline2shapely(dxf_entity)
+#             elif dxf_entity.dxftype() == 'SOLID':
+#                 g = dxfsolid2shapely(dxf_entity)
+#             elif dxf_entity.dxftype() == 'TRACE':
+#                 g = dxftrace2shapely(dxf_entity)
+#             elif dxf_entity.dxftype() == 'SPLINE':
+#                 g = dxfspline2shapely(dxf_entity)
+#             elif dxf_entity.dxftype() == 'INSERT':
+#                 log.debug("Not supported yet.")
+#             else:
+#                 log.debug("Not supported yet.")
+#
+#             if g is not None:
+#                 if type(g) == list:
+#                     for subg in g:
+#                         geo_block.append(subg)
+#                 else:
+#                     geo_block.append(g)
+#
+#         # iterate over the geometries found and apply any transformation found in the 'INSERT' entity attributes
+#         for geo in geo_block:
+#             if tr[0] != 0 or tr[1] != 0:
+#                 geo = translate(geo, (tr[0] - block_coords[0]), (tr[1] - block_coords[1]))
+#
+#             # support for array block insertions
+#             if r_count > 1:
+#                 for r in range(r_count):
+#                     geo_block_transformed.append(translate(geo, (tr[0] + (r * r_spacing) - block_coords[0]), 0))
+#
+#             if c_count > 1:
+#                 for c in range(c_count):
+#                     geo_block_transformed.append(translate(geo, 0, (tr[1] + (c * c_spacing) - block_coords[1])))
+#
+#             if sx != 1 or sy != 1:
+#                 geo = scale(geo, sx, sy)
+#             if phi != 0:
+#                 geo = rotate(geo, phi, origin=tr)
+#
+#             geo_block_transformed.append(geo)
+#     return geo_block_transformed

+ 809 - 0
ParseDXF_Spline.py

@@ -0,0 +1,809 @@
+# Author: vvlachoudis@gmail.com
+# Vasilis Vlachoudis
+# Date: 20-Oct-2015
+
+import math
+import sys
+
+def norm(v):
+    return math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
+
+def normalize_2(v):
+    m = norm(v)
+    return [v[0]/m, v[1]/m, v[2]/m]
+
+# ------------------------------------------------------------------------------
+# Convert a B-spline to polyline with a fixed number of segments
+#
+# FIXME to become adaptive
+# ------------------------------------------------------------------------------
+def spline2Polyline(xyz, degree, closed, segments, knots):
+    '''
+    :param xyz: DXF spline control points
+    :param degree: degree of the Spline curve
+    :param closed: closed Spline
+    :type closed: bool
+    :param segments: how many lines to use for Spline approximation
+    :param knots: DXF spline knots
+    :return: x,y,z coordinates (each is a list)
+    '''
+
+    # Check if last point coincide with the first one
+    if (Vector(xyz[0]) - Vector(xyz[-1])).length2() < 1e-10:
+        # it is already closed, treat it as open
+        closed = False
+        # FIXME we should verify if it is periodic,.... but...
+        #       I am not sure :)
+
+    if closed:
+        xyz.extend(xyz[:degree])
+        knots = None
+    else:
+        # make base-1
+        knots.insert(0, 0)
+
+    npts = len(xyz)
+
+    if degree<1 or degree>3:
+        #print "invalid degree"
+        return None,None,None
+
+    # order:
+    k = degree+1
+
+    if npts < k:
+        #print "not enough control points"
+        return None,None,None
+
+    # resolution:
+    nseg = segments * npts
+
+    # WARNING: base 1
+    b = [0.0]*(npts*3+1)        # polygon points
+    h = [1.0]*(npts+1)        # set all homogeneous weighting factors to 1.0
+    p = [0.0]*(nseg*3+1)        # returned curved points
+
+    i = 1
+    for pt in xyz:
+        b[i]   = pt[0]
+        b[i+1] = pt[1]
+        b[i+2] = pt[2]
+        i +=3
+
+    #if periodic:
+    if closed:
+        _rbsplinu(npts, k, nseg, b, h, p, knots)
+    else:
+        _rbspline(npts, k, nseg, b, h, p, knots)
+
+    x = []
+    y = []
+    z = []
+    for i in range(1,3*nseg+1,3):
+        x.append(p[i])
+        y.append(p[i+1])
+        z.append(p[i+2])
+
+#    for i,xyz in enumerate(zip(x,y,z)):
+#        print i,xyz
+
+    return x,y,z
+
+# ------------------------------------------------------------------------------
+# Subroutine to generate a B-spline open knot vector with multiplicity
+# equal to the order at the ends.
+#    c            = order of the basis function
+#    n            = the number of defining polygon vertices
+#    n+2          = index of x[] for the first occurence of the maximum knot vector value
+#    n+order      = maximum value of the knot vector -- $n + c$
+#    x[]          = array containing the knot vector
+# ------------------------------------------------------------------------------
+def _knot(n, order):
+    x = [0.0]*(n+order+1)
+    for i in range(2, n+order+1):
+        if i>order and i<n+2:
+            x[i] = x[i-1] + 1.0
+        else:
+            x[i] = x[i-1]
+    return x
+
+# ------------------------------------------------------------------------------
+# Subroutine to generate a B-spline uniform (periodic) knot vector.
+#
+# order        = order of the basis function
+# n            = the number of defining polygon vertices
+# n+order      = maximum value of the knot vector -- $n + order$
+# x[]          = array containing the knot vector
+# ------------------------------------------------------------------------------
+def _knotu(n, order):
+    x = [0]*(n+order+1)
+    for i in range(2, n+order+1):
+        x[i] = float(i-1)
+    return x
+
+# ------------------------------------------------------------------------------
+# Subroutine to generate rational B-spline basis functions--open knot vector
+
+# C code for An Introduction to NURBS
+# by David F. Rogers. Copyright (C) 2000 David F. Rogers,
+# All rights reserved.
+
+# Name: rbasis
+# Subroutines called: none
+# Book reference: Chapter 4, Sec. 4. , p 296
+
+#   c        = order of the B-spline basis function
+#   d        = first term of the basis function recursion relation
+#   e        = second term of the basis function recursion relation
+#   h[]      = array containing the homogeneous weights
+#   npts     = number of defining polygon vertices
+#   nplusc   = constant -- npts + c -- maximum number of knot values
+#   r[]      = array containing the rational basis functions
+#              r[1] contains the basis function associated with B1 etc.
+#   t        = parameter value
+#   temp[]   = temporary array
+#   x[]      = knot vector
+# ------------------------------------------------------------------------------
+def _rbasis(c, t, npts, x, h, r):
+    nplusc = npts + c
+    temp = [0.0]*(nplusc+1)
+
+    # calculate the first order non-rational basis functions n[i]
+    for i in range(1, nplusc):
+        if x[i] <= t < x[i+1]:
+            temp[i] = 1.0
+        else:
+            temp[i] = 0.0
+
+    # calculate the higher order non-rational basis functions
+    for k in range(2,c+1):
+        for i in range(1,nplusc-k+1):
+            # if the lower order basis function is zero skip the calculation
+            if temp[i] != 0.0:
+                d = ((t-x[i])*temp[i])/(x[i+k-1]-x[i])
+            else:
+                d = 0.0
+
+            # if the lower order basis function is zero skip the calculation
+            if temp[i+1] != 0.0:
+                e = ((x[i+k]-t)*temp[i+1])/(x[i+k]-x[i+1])
+            else:
+                e = 0.0
+            temp[i] = d + e
+
+    # pick up last point
+    if t >= x[nplusc]:
+        temp[npts] = 1.0
+
+    # calculate sum for denominator of rational basis functions
+    s = 0.0
+    for i in range(1,npts+1):
+        s += temp[i]*h[i]
+
+    # form rational basis functions and put in r vector
+    for i in range(1, npts+1):
+        if s != 0.0:
+            r[i] = (temp[i]*h[i])/s
+        else:
+            r[i] = 0
+
+# ------------------------------------------------------------------------------
+# Generates a rational B-spline curve using a uniform open knot vector.
+#
+# C code for An Introduction to NURBS
+# by David F. Rogers. Copyright (C) 2000 David F. Rogers,
+# All rights reserved.
+#
+# Name: rbspline.c
+# Subroutines called: _knot, rbasis
+# Book reference: Chapter 4, Alg. p. 297
+#
+#    b           = array containing the defining polygon vertices
+#                  b[1] contains the x-component of the vertex
+#                  b[2] contains the y-component of the vertex
+#                  b[3] contains the z-component of the vertex
+#    h           = array containing the homogeneous weighting factors
+#    k           = order of the B-spline basis function
+#    nbasis      = array containing the basis functions for a single value of t
+#    nplusc      = number of knot values
+#    npts        = number of defining polygon vertices
+#    p[,]        = array containing the curve points
+#                  p[1] contains the x-component of the point
+#                  p[2] contains the y-component of the point
+#                  p[3] contains the z-component of the point
+#    p1          = number of points to be calculated on the curve
+#    t           = parameter value 0 <= t <= npts - k + 1
+#    x[]         = array containing the knot vector
+# ------------------------------------------------------------------------------
+def _rbspline(npts, k, p1, b, h, p, x):
+    nplusc = npts + k
+    nbasis = [0.0]*(npts+1)        # zero and re-dimension the basis array
+
+    # generate the uniform open knot vector
+    if x is None or len(x) != nplusc+1:
+        x = _knot(npts, k)
+    icount = 0
+    # calculate the points on the rational B-spline curve
+    t = 0
+    step = float(x[nplusc])/float(p1-1)
+    for i1 in range(1, p1+1):
+        if x[nplusc] - t < 5e-6:
+            t = x[nplusc]
+        # generate the basis function for this value of t
+        nbasis = [0.0]*(npts+1)    # zero and re-dimension the knot vector and the basis array
+        _rbasis(k, t, npts, x, h, nbasis)
+        # generate a point on the curve
+        for j in range(1, 4):
+            jcount = j
+            p[icount+j] = 0.0
+            # Do local matrix multiplication
+            for i in range(1, npts+1):
+                p[icount+j] +=  nbasis[i]*b[jcount]
+                jcount += 3
+        icount += 3
+        t += step
+
+# ------------------------------------------------------------------------------
+# Subroutine to generate a rational B-spline curve using an uniform periodic knot vector
+#
+# C code for An Introduction to NURBS
+# by David F. Rogers. Copyright (C) 2000 David F. Rogers,
+# All rights reserved.
+#
+# Name: rbsplinu.c
+# Subroutines called: _knotu, _rbasis
+# Book reference: Chapter 4, Alg. p. 298
+#
+#   b[]         = array containing the defining polygon vertices
+#                 b[1] contains the x-component of the vertex
+#                 b[2] contains the y-component of the vertex
+#                 b[3] contains the z-component of the vertex
+#   h[]         = array containing the homogeneous weighting factors
+#   k           = order of the B-spline basis function
+#   nbasis      = array containing the basis functions for a single value of t
+#   nplusc      = number of knot values
+#   npts        = number of defining polygon vertices
+#   p[,]        = array containing the curve points
+#                 p[1] contains the x-component of the point
+#                 p[2] contains the y-component of the point
+#                 p[3] contains the z-component of the point
+#   p1          = number of points to be calculated on the curve
+#   t           = parameter value 0 <= t <= npts - k + 1
+#   x[]         = array containing the knot vector
+# ------------------------------------------------------------------------------
+def _rbsplinu(npts, k, p1, b, h, p, x=None):
+    nplusc = npts + k
+    nbasis = [0.0]*(npts+1)        # zero and re-dimension the basis array
+    # generate the uniform periodic knot vector
+    if x is None or len(x) != nplusc+1:
+        # zero and re dimension the knot vector and the basis array
+        x = _knotu(npts, k)
+    icount = 0
+    # calculate the points on the rational B-spline curve
+    t = k-1
+    step = (float(npts)-(k-1))/float(p1-1)
+    for i1 in range(1, p1+1):
+        if x[nplusc] - t < 5e-6:
+            t = x[nplusc]
+        # generate the basis function for this value of t
+        nbasis = [0.0]*(npts+1)
+        _rbasis(k, t, npts, x, h, nbasis)
+        # generate a point on the curve
+        for j in range(1,4):
+            jcount = j
+            p[icount+j] = 0.0
+            #  Do local matrix multiplication
+            for i in range(1,npts+1):
+                p[icount+j] += nbasis[i]*b[jcount]
+                jcount += 3
+        icount += 3
+        t += step
+
+# Accuracy for comparison operators
+_accuracy = 1E-15
+
+
+def Cmp0(x):
+    """Compare against zero within _accuracy"""
+    return abs(x)<_accuracy
+
+
+def gauss(A, B):
+    """Solve A*X = B using the Gauss elimination method"""
+
+    n = len(A)
+    s = [0.0] * n
+    X = [0.0] * n
+
+    p = [i for i in range(n)]
+    for i in range(n):
+        s[i] = max([abs(x) for x in A[i]])
+
+    for k in range(n - 1):
+        # select j>=k so that
+        # |A[p[j]][k]| / s[p[i]] >= |A[p[i]][k]| / s[p[i]] for i = k,k+1,...,n
+        j = k
+        ap = abs(A[p[j]][k]) / s[p[j]]
+        for i in range(k + 1, n):
+            api = abs(A[p[i]][k]) / s[p[i]]
+            if api > ap:
+                j = i
+                ap = api
+
+        if j != k: p[k], p[j] = p[j], p[k]  # Swap values
+
+        for i in range(k + 1, n):
+            z = A[p[i]][k] / A[p[k]][k]
+            A[p[i]][k] = z
+            for j in range(k + 1, n):
+                A[p[i]][j] -= z * A[p[k]][j]
+
+    for k in range(n - 1):
+        for i in range(k + 1, n):
+            B[p[i]] -= A[p[i]][k] * B[p[k]]
+
+    for i in range(n - 1, -1, -1):
+        X[i] = B[p[i]]
+        for j in range(i + 1, n):
+            X[i] -= A[p[i]][j] * X[j]
+        X[i] /= A[p[i]][i]
+
+    return X
+
+
+# Vector class
+# Inherits from List
+class Vector(list):
+    """Vector class"""
+
+    def __init__(self, x=3, *args):
+        """Create a new vector,
+        Vector(size), Vector(list), Vector(x,y,z,...)"""
+        list.__init__(self)
+
+        if isinstance(x, int) and not args:
+            for i in range(x):
+                self.append(0.0)
+        elif isinstance(x, (list, tuple)):
+            for i in x:
+                self.append(float(i))
+        else:
+            self.append(float(x))
+            for i in args:
+                self.append(float(i))
+
+    # ----------------------------------------------------------------------
+    def set(self, x, y, z=None):
+        """Set vector"""
+        self[0] = x
+        self[1] = y
+        if z: self[2] = z
+
+    # ----------------------------------------------------------------------
+    def __repr__(self):
+        return "[%s]" % (", ".join([repr(x) for x in self]))
+
+    # ----------------------------------------------------------------------
+    def __str__(self):
+        return "[%s]" % (", ".join([("%15g" % (x)).strip() for x in self]))
+
+    # ----------------------------------------------------------------------
+    def eq(self, v, acc=_accuracy):
+        """Test for equality with vector v within accuracy"""
+        if len(self) != len(v): return False
+        s2 = 0.0
+        for a, b in zip(self, v):
+            s2 += (a - b) ** 2
+        return s2 <= acc ** 2
+
+    def __eq__(self, v):
+        return self.eq(v)
+
+    # ----------------------------------------------------------------------
+    def __neg__(self):
+        """Negate vector"""
+        new = Vector(len(self))
+        for i, s in enumerate(self):
+            new[i] = -s
+        return new
+
+    # ----------------------------------------------------------------------
+    def __add__(self, v):
+        """Add 2 vectors"""
+        size = min(len(self), len(v))
+        new = Vector(size)
+        for i in range(size):
+            new[i] = self[i] + v[i]
+        return new
+
+    # ----------------------------------------------------------------------
+    def __iadd__(self, v):
+        """Add vector v to self"""
+        for i in range(min(len(self), len(v))):
+            self[i] += v[i]
+        return self
+
+    # ----------------------------------------------------------------------
+    def __sub__(self, v):
+        """Subtract 2 vectors"""
+        size = min(len(self), len(v))
+        new = Vector(size)
+        for i in range(size):
+            new[i] = self[i] - v[i]
+        return new
+
+    # ----------------------------------------------------------------------
+    def __isub__(self, v):
+        """Subtract vector v from self"""
+        for i in range(min(len(self), len(v))):
+            self[i] -= v[i]
+        return self
+
+    # ----------------------------------------------------------------------
+    # Scale or Dot product
+    # ----------------------------------------------------------------------
+    def __mul__(self, v):
+        """scale*Vector() or Vector()*Vector() - Scale vector or dot product"""
+        if isinstance(v, list):
+            return self.dot(v)
+        else:
+            return Vector([x * v for x in self])
+
+    # ----------------------------------------------------------------------
+    # Scale or Dot product
+    # ----------------------------------------------------------------------
+    def __rmul__(self, v):
+        """scale*Vector() or Vector()*Vector() - Scale vector or dot product"""
+        if isinstance(v, Vector):
+            return self.dot(v)
+        else:
+            return Vector([x * v for x in self])
+
+    # ----------------------------------------------------------------------
+    # Divide by floating point
+    # ----------------------------------------------------------------------
+    def __div__(self, b):
+        return Vector([x / b for x in self])
+
+    # ----------------------------------------------------------------------
+    def __xor__(self, v):
+        """Cross product"""
+        return self.cross(v)
+
+    # ----------------------------------------------------------------------
+    def dot(self, v):
+        """Dot product of 2 vectors"""
+        s = 0.0
+        for a, b in zip(self, v):
+            s += a * b
+        return s
+
+    # ----------------------------------------------------------------------
+    def cross(self, v):
+        """Cross product of 2 vectors"""
+        if len(self) == 3:
+            return Vector(self[1] * v[2] - self[2] * v[1],
+                          self[2] * v[0] - self[0] * v[2],
+                          self[0] * v[1] - self[1] * v[0])
+        elif len(self) == 2:
+            return self[0] * v[1] - self[1] * v[0]
+        else:
+            raise Exception("Cross product needs 2d or 3d vectors")
+
+    # ----------------------------------------------------------------------
+    def length2(self):
+        """Return length squared of vector"""
+        s2 = 0.0
+        for s in self:
+            s2 += s ** 2
+        return s2
+
+    # ----------------------------------------------------------------------
+    def length(self):
+        """Return length of vector"""
+        s2 = 0.0
+        for s in self:
+            s2 += s ** 2
+        return math.sqrt(s2)
+
+    __abs__ = length
+
+    # ----------------------------------------------------------------------
+    def arg(self):
+        """return vector angle"""
+        return math.atan2(self[1], self[0])
+
+    # ----------------------------------------------------------------------
+    def norm(self):
+        """Normalize vector and return length"""
+        l = self.length()
+        if l > 0.0:
+            invlen = 1.0 / l
+            for i in range(len(self)):
+                self[i] *= invlen
+        return l
+
+    normalize = norm
+
+    # ----------------------------------------------------------------------
+    def unit(self):
+        """return a unit vector"""
+        v = self.clone()
+        v.norm()
+        return v
+
+    # ----------------------------------------------------------------------
+    def clone(self):
+        """Clone vector"""
+        return Vector(self)
+
+    # ----------------------------------------------------------------------
+    def x(self):
+        return self[0]
+
+    def y(self):
+        return self[1]
+
+    def z(self):
+        return self[2]
+
+    # ----------------------------------------------------------------------
+    def orthogonal(self):
+        """return a vector orthogonal to self"""
+        xx = abs(self.x())
+        yy = abs(self.y())
+
+        if len(self) >= 3:
+            zz = abs(self.z())
+            if xx < yy:
+                if xx < zz:
+                    return Vector(0.0, self.z(), -self.y())
+                else:
+                    return Vector(self.y(), -self.x(), 0.0)
+            else:
+                if yy < zz:
+                    return Vector(-self.z(), 0.0, self.x())
+                else:
+                    return Vector(self.y(), -self.x(), 0.0)
+        else:
+            return Vector(-self.y(), self.x())
+
+    # ----------------------------------------------------------------------
+    def direction(self, zero=_accuracy):
+        """return containing the direction if normalized with any of the axis"""
+
+        v = self.clone()
+        l = v.norm()
+        if abs(l) <= zero: return "O"
+
+        if abs(v[0] - 1.0) < zero:
+            return "X"
+        elif abs(v[0] + 1.0) < zero:
+            return "-X"
+        elif abs(v[1] - 1.0) < zero:
+            return "Y"
+        elif abs(v[1] + 1.0) < zero:
+            return "-Y"
+        elif abs(v[2] - 1.0) < zero:
+            return "Z"
+        elif abs(v[2] + 1.0) < zero:
+            return "-Z"
+        else:
+            # nothing special about the direction, return N
+            return "N"
+
+    # ----------------------------------------------------------------------
+    # Set the vector directly in polar coordinates
+    # @param ma magnitude of vector
+    # @param ph azimuthal angle in radians
+    # @param th polar angle in radians
+    # ----------------------------------------------------------------------
+    def setPolar(self, ma, ph, th):
+        """Set the vector directly in polar coordinates"""
+        sf = math.sin(ph)
+        cf = math.cos(ph)
+        st = math.sin(th)
+        ct = math.cos(th)
+        self[0] = ma * st * cf
+        self[1] = ma * st * sf
+        self[2] = ma * ct
+
+    # ----------------------------------------------------------------------
+    def phi(self):
+        """return the azimuth angle."""
+        if Cmp0(self.x()) and Cmp0(self.y()):
+            return 0.0
+        return math.atan2(self.y(), self.x())
+
+    # ----------------------------------------------------------------------
+    def theta(self):
+        """return the polar angle."""
+        if Cmp0(self.x()) and Cmp0(self.y()) and Cmp0(self.z()):
+            return 0.0
+        return math.atan2(self.perp(), self.z())
+
+    # ----------------------------------------------------------------------
+    def cosTheta(self):
+        """return cosine of the polar angle."""
+        ptot = self.length()
+        if Cmp0(ptot):
+            return 1.0
+        else:
+            return self.z() / ptot
+
+    # ----------------------------------------------------------------------
+    def perp2(self):
+        """return the transverse component squared
+        (R^2 in cylindrical coordinate system)."""
+        return self.x() * self.x() + self.y() * self.y()
+
+    # ----------------------------------------------------------------------
+    def perp(self):
+        """@return the transverse component
+        (R in cylindrical coordinate system)."""
+        return math.sqrt(self.perp2())
+
+    # ----------------------------------------------------------------------
+    # Return a random 3D vector
+    # ----------------------------------------------------------------------
+    # @staticmethod
+    # def random():
+    #     cosTheta = 2.0 * random.random() - 1.0
+    #     sinTheta = math.sqrt(1.0 - cosTheta ** 2)
+    #     phi = 2.0 * math.pi * random.random()
+    #     return Vector(math.cos(phi) * sinTheta, math.sin(phi) * sinTheta, cosTheta)
+
+# #===============================================================================
+# # Cardinal cubic spline class
+# #===============================================================================
+# class CardinalSpline:
+#     def __init__(self, A=0.5):
+#         # The default matrix is the Catmull-Rom spline
+#         # which is equal to Cardinal matrix
+#         # for A = 0.5
+#         #
+#         # Note: Vasilis
+#         #    The A parameter should be the fraction in t where
+#         #    the second derivative is zero
+#         self.setMatrix(A)
+#
+#     #-----------------------------------------------------------------------
+#     # Set the matrix according to Cardinal
+#     #-----------------------------------------------------------------------
+#     def setMatrix(self, A=0.5):
+#         self.M = []
+#         self.M.append([  -A,  2.-A,    A-2.,   A ])
+#         self.M.append([2.*A,  A-3., 3.-2.*A,  -A ])
+#         self.M.append([  -A,    0.,       A,   0.])
+#         self.M.append([  0.,    1.,       0,   0.])
+#
+#     #-----------------------------------------------------------------------
+#     # Evaluate Cardinal spline at position t
+#     # @param P      list or tuple with 4 points y positions
+#     # @param t [0..1] fraction of interval from points 1..2
+#     # @param k      index of starting 4 elements in P
+#     # @return spline evaluation
+#     #-----------------------------------------------------------------------
+#     def __call__(self, P, t, k=1):
+#         T = [t*t*t, t*t, t, 1.0]
+#         R = [0.0]*4
+#         for i in range(4):
+#             for j in range(4):
+#                 R[i] += T[j] * self.M[j][i]
+#         y = 0.0
+#         for i in range(4):
+#             y += R[i]*P[k+i-1]
+#
+#         return y
+#
+#     #-----------------------------------------------------------------------
+#     # Return the coefficients of a 3rd degree polynomial
+#     #     f(x) = a t^3 + b t^2 + c t + d
+#     # @return [a, b, c, d]
+#     #-----------------------------------------------------------------------
+#     def coefficients(self, P, k=1):
+#         C = [0.0]*4
+#         for i in range(4):
+#             for j in range(4):
+#                 C[i] += self.M[i][j] * P[k+j-1]
+#         return C
+#
+#     #-----------------------------------------------------------------------
+#     # Evaluate the value of the spline using the coefficients
+#     #-----------------------------------------------------------------------
+#     def evaluate(self, C, t):
+#         return ((C[0]*t + C[1])*t + C[2])*t + C[3]
+#
+# #===============================================================================
+# # Cubic spline ensuring that the first and second derivative are continuous
+# # adapted from Penelope Manual Appending B.1
+# # It requires all the points (xi,yi) and the assumption on how to deal
+# # with the second derivative on the extremities
+# # Option 1: assume zero as second derivative on both ends
+# # Option 2: assume the same as the next or previous one
+# #===============================================================================
+# class CubicSpline:
+#     def __init__(self, X, Y):
+#         self.X = X
+#         self.Y = Y
+#         self.n = len(X)
+#
+#         # Option #1
+#         s1 = 0.0    # zero based = s0
+#         sN = 0.0    # zero based = sN-1
+#
+#         # Construct the tri-diagonal matrix
+#         A = []
+#         B = [0.0] * (self.n-2)
+#         for i in range(self.n-2):
+#             A.append([0.0] * (self.n-2))
+#
+#         for i in range(1,self.n-1):
+#             hi = self.h(i)
+#             Hi = 2.0*(self.h(i-1) + hi)
+#             j = i-1
+#             A[j][j] = Hi
+#             if i+1<self.n-1:
+#                 A[j][j+1] = A[j+1][j] = hi
+#
+#             if i==1:
+#                 B[j] = 6.*(self.d(i) - self.d(j)) - hi*s1
+#             elif i<self.n-2:
+#                 B[j] = 6.*(self.d(i) - self.d(j))
+#             else:
+#                 B[j] = 6.*(self.d(i) - self.d(j)) - hi*sN
+#
+#
+#         self.s = gauss(A,B)
+#         self.s.insert(0,s1)
+#         self.s.append(sN)
+# #        print ">> s <<"
+# #        pprint(self.s)
+#
+#     #-----------------------------------------------------------------------
+#     def h(self, i):
+#         return self.X[i+1] - self.X[i]
+#
+#     #-----------------------------------------------------------------------
+#     def d(self, i):
+#         return (self.Y[i+1] - self.Y[i]) / (self.X[i+1] - self.X[i])
+#
+#     #-----------------------------------------------------------------------
+#     def coefficients(self, i):
+#         """return coefficients of cubic spline for interval i a*x**3+b*x**2+c*x+d"""
+#         hi  = self.h(i)
+#         si  = self.s[i]
+#         si1 = self.s[i+1]
+#         xi  = self.X[i]
+#         xi1 = self.X[i+1]
+#         fi  = self.Y[i]
+#         fi1 = self.Y[i+1]
+#
+#         a = 1./(6.*hi)*(si*xi1**3 - si1*xi**3 + 6.*(fi*xi1 - fi1*xi)) + hi/6.*(si1*xi - si*xi1)
+#         b = 1./(2.*hi)*(si1*xi**2 - si*xi1**2 + 2*(fi1 - fi)) + hi/6.*(si - si1)
+#         c = 1./(2.*hi)*(si*xi1 - si1*xi)
+#         d = 1./(6.*hi)*(si1-si)
+#
+#         return [d,c,b,a]
+#
+#     #-----------------------------------------------------------------------
+#     def __call__(self, i, x):
+#         # FIXME should interpolate to find the interval
+#         C = self.coefficients(i)
+#         return ((C[0]*x + C[1])*x + C[2])*x + C[3]
+#
+#     #-----------------------------------------------------------------------
+#     # @return evaluation of cubic spline at x using coefficients C
+#     #-----------------------------------------------------------------------
+#     def evaluate(self, C, x):
+#         return ((C[0]*x + C[1])*x + C[2])*x + C[3]
+#
+#     #-----------------------------------------------------------------------
+#     # Return evaluated derivative at x using coefficients C
+#     #-----------------------------------------------------------------------
+#     def derivative(self, C, x):
+#         a = 3.0*C[0]            # derivative coefficients
+#         b = 2.0*C[1]            # ... for sampling with rejection
+#         c =     C[2]
+#         return (3.0*C[0]*x + 2.0*C[1])*x + C[2]
+#

+ 332 - 0
ParseFont.py

@@ -0,0 +1,332 @@
+#########################################################################
+### Borrowed code from 'https://github.com/gddc/ttfquery/blob/master/ ###
+### and made it work with Python 3                          #############
+#########################################################################
+
+import re, os, sys, glob
+import itertools
+
+from shapely.geometry import Point, Polygon
+from shapely.affinity import translate, scale, rotate
+from shapely.geometry import MultiPolygon
+from shapely.geometry.base import BaseGeometry
+
+import freetype as ft
+from fontTools import ttLib
+
+import logging
+
+log = logging.getLogger('base2')
+
+
+class ParseFont():
+
+    FONT_SPECIFIER_NAME_ID = 4
+    FONT_SPECIFIER_FAMILY_ID = 1
+
+    @staticmethod
+    def get_win32_font_path():
+        """Get User-specific font directory on Win32"""
+        try:
+            import winreg
+        except ImportError:
+            return os.path.join(os.environ['WINDIR'], 'Fonts')
+        else:
+            k = winreg.OpenKey(
+                winreg.HKEY_CURRENT_USER,
+                r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders")
+            try:
+                # should check that k is valid? How?
+                return winreg.QueryValueEx(k, "Fonts")[0]
+            finally:
+                winreg.CloseKey(k)
+
+    @staticmethod
+    def get_linux_font_paths():
+        """Get system font directories on Linux/Unix
+
+        Uses /usr/sbin/chkfontpath to get the list
+        of system-font directories, note that many
+        of these will *not* be truetype font directories.
+
+        If /usr/sbin/chkfontpath isn't available, uses
+        returns a set of common Linux/Unix paths
+        """
+        executable = '/usr/sbin/chkfontpath'
+        if os.path.isfile(executable):
+            data = os.popen(executable).readlines()
+            match = re.compile('\d+: (.+)')
+            set = []
+            for line in data:
+                result = match.match(line)
+                if result:
+                    set.append(result.group(1))
+            return set
+        else:
+            directories = [
+                # what seems to be the standard installation point
+                "/usr/X11R6/lib/X11/fonts/TTF/",
+                # common application, not really useful
+                "/usr/lib/openoffice/share/fonts/truetype/",
+                # documented as a good place to install new fonts...
+                "/usr/share/fonts",
+                "/usr/local/share/fonts",
+                # seems to be where fonts are installed for an individual user?
+                "~/.fonts",
+            ]
+
+            dir_set = []
+
+            for directory in directories:
+                directory = directory = os.path.expanduser(os.path.expandvars(directory))
+                try:
+                    if os.path.isdir(directory):
+                        for path, children, files in os.walk(directory):
+                            dir_set.append(path)
+                except (IOError, OSError, TypeError, ValueError):
+                    pass
+            return dir_set
+
+    @staticmethod
+    def get_mac_font_paths():
+        """Get system font directories on MacOS
+        """
+        directories = [
+            # okay, now the OS X variants...
+            "~/Library/Fonts/",
+            "/Library/Fonts/",
+            "/Network/Library/Fonts/",
+            "/System/Library/Fonts/",
+            "System Folder:Fonts:",
+        ]
+
+        dir_set = []
+
+        for directory in directories:
+            directory = directory = os.path.expanduser(os.path.expandvars(directory))
+            try:
+                if os.path.isdir(directory):
+                    for path, children, files in os.walk(directory):
+                        dir_set.append(path)
+            except (IOError, OSError, TypeError, ValueError):
+                pass
+        return dir_set
+
+    @staticmethod
+    def get_win32_fonts(font_directory=None):
+        """Get list of explicitly *installed* font names"""
+
+        import winreg
+        if font_directory is None:
+            font_directory = ParseFont.get_win32_font_path()
+        k = None
+
+        items = {}
+        for keyName in (
+                r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts",
+                r"SOFTWARE\Microsoft\Windows\CurrentVersion\Fonts",
+        ):
+            try:
+                k = winreg.OpenKey(
+                    winreg.HKEY_LOCAL_MACHINE,
+                    keyName
+                )
+            except OSError as err:
+                pass
+
+        if not k:
+            # couldn't open either WinNT or Win98 key???
+            return glob.glob(os.path.join(font_directory, '*.ttf'))
+
+        try:
+            # should check that k is valid? How?
+            for index in range(winreg.QueryInfoKey(k)[1]):
+                key, value, _ = winreg.EnumValue(k, index)
+                if not os.path.dirname(value):
+                    value = os.path.join(font_directory, value)
+                value = os.path.abspath(value).lower()
+                if value[-4:] == '.ttf':
+                    items[value] = 1
+            return list(items.keys())
+        finally:
+            winreg.CloseKey(k)
+
+    @staticmethod
+    def get_font_name(font_path):
+        """
+        Get the short name from the font's names table
+        From 'https://github.com/gddc/ttfquery/blob/master/ttfquery/describe.py'
+        and
+        http://www.starrhorne.com/2012/01/18/
+        how-to-extract-font-names-from-ttf-files-using-python-and-our-old-friend-the-command-line.html
+        ported to Python 3 here: https://gist.github.com/pklaus/dce37521579513c574d0
+        """
+        name = ""
+        family = ""
+
+        font = ttLib.TTFont(font_path)
+
+        for record in font['name'].names:
+            if b'\x00' in record.string:
+                name_str = record.string.decode('utf-16-be')
+            else:
+                # name_str = record.string.decode('utf-8')
+                name_str = record.string.decode('latin-1')
+
+            if record.nameID == ParseFont.FONT_SPECIFIER_NAME_ID and not name:
+                name = name_str
+            elif record.nameID == ParseFont.FONT_SPECIFIER_FAMILY_ID and not family:
+                family = name_str
+
+            if name and family:
+                break
+        return name, family
+
+    def __init__(self, parent=None):
+        super(ParseFont, self).__init__()
+
+        # regular fonts
+        self.regular_f = {}
+        # bold fonts
+        self.bold_f = {}
+        # italic fonts
+        self.italic_f = {}
+        # bold and italic fonts
+        self.bold_italic_f = {}
+
+    def get_fonts(self, paths=None):
+        """
+        Find fonts in paths, or the system paths if not given
+        """
+        files = {}
+        if paths is None:
+            if sys.platform == 'win32':
+                font_directory = ParseFont.get_win32_font_path()
+                paths = [font_directory,]
+
+                # now get all installed fonts directly...
+                for f in self.get_win32_fonts(font_directory):
+                    files[f] = 1
+            elif sys.platform == 'linux':
+                paths = ParseFont.get_linux_font_paths()
+            else:
+                paths = ParseFont.get_mac_font_paths()
+        elif isinstance(paths, str):
+            paths = [paths]
+
+        for path in paths:
+            for file in glob.glob(os.path.join(path, '*.ttf')):
+                files[os.path.abspath(file)] = 1
+
+        return list(files.keys())
+
+    def get_fonts_by_types(self):
+
+        system_fonts = self.get_fonts()
+
+        # split the installed fonts by type: regular, bold, italic (oblique), bold-italic and
+        # store them in separate dictionaries {name: file_path/filename.ttf}
+        for font in system_fonts:
+            name, family = ParseFont.get_font_name(font)
+
+            if 'Bold' in name and 'Italic' in name:
+                name = name.replace(" Bold Italic", '')
+                self.bold_italic_f.update({name: font})
+            elif 'Bold' in name and 'Oblique' in name:
+                name = name.replace(" Bold Oblique", '')
+                self.bold_italic_f.update({name: font})
+            elif 'Bold' in name:
+                name = name.replace(" Bold", '')
+                self.bold_f.update({name: font})
+            elif 'SemiBold' in name:
+                name = name.replace(" SemiBold", '')
+                self.bold_f.update({name: font})
+            elif 'DemiBold' in name:
+                name = name.replace(" DemiBold", '')
+                self.bold_f.update({name: font})
+            elif 'Demi' in name:
+                name = name.replace(" Demi", '')
+                self.bold_f.update({name: font})
+            elif 'Italic' in name:
+                name = name.replace(" Italic", '')
+                self.italic_f.update({name: font})
+            elif 'Oblique' in name:
+                name = name.replace(" Italic", '')
+                self.italic_f.update({name: font})
+            else:
+                try:
+                    name = name.replace(" Regular", '')
+                except:
+                    pass
+                self.regular_f.update({name: font})
+        log.debug("Font parsing is finished.")
+
+    def font_to_geometry(self, char_string, font_name, font_type, font_size, units='MM', coordx=0, coordy=0):
+        path = []
+        scaled_path = []
+        path_filename = ""
+
+        regular_dict = self.regular_f
+        bold_dict = self.bold_f
+        italic_dict = self.italic_f
+        bold_italic_dict = self.bold_italic_f
+
+        try:
+            if font_type == 'bi':
+                path_filename = bold_italic_dict[font_name]
+            elif font_type == 'bold':
+                path_filename = bold_dict[font_name]
+            elif font_type == 'italic':
+                path_filename = italic_dict[font_name]
+            elif font_type == 'regular':
+                path_filename = regular_dict[font_name]
+        except Exception as e:
+            log.debug("[error_notcl] Font Loading: %s" % str(e))
+            return"[ERROR] Font Loading: %s" % str(e)
+
+        face = ft.Face(path_filename)
+        face.set_char_size(int(font_size) * 64)
+
+        pen_x = coordx
+        previous = 0
+
+        # done as here: https://www.freetype.org/freetype2/docs/tutorial/step2.html
+        for char in char_string:
+            glyph_index = face.get_char_index(char)
+
+            try:
+                if previous > 0 and glyph_index > 0:
+                    delta = face.get_kerning(previous, glyph_index)
+                    pen_x += delta.x
+            except:
+                pass
+
+            face.load_glyph(glyph_index)
+            # face.load_char(char, flags=8)
+
+            slot = face.glyph
+            outline = slot.outline
+
+            start, end = 0, 0
+            for i in range(len(outline.contours)):
+                end = outline.contours[i]
+                points = outline.points[start:end + 1]
+                points.append(points[0])
+
+                char_geo = Polygon(points)
+                char_geo = translate(char_geo, xoff=pen_x, yoff=coordy)
+
+                path.append(char_geo)
+
+                start = end + 1
+
+            pen_x += slot.advance.x
+            previous = glyph_index
+
+        for item in path:
+            if units == 'MM':
+                scaled_path.append(scale(item, 0.0080187969924812, 0.0080187969924812, origin=(coordx, coordy)))
+            else:
+                scaled_path.append(scale(item, 0.00031570066, 0.00031570066, origin=(coordx, coordy)))
+
+        return MultiPolygon(scaled_path)

+ 652 - 0
ParseSVG.py

@@ -0,0 +1,652 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 12/18/2015                                         #
+# MIT Licence                                              #
+#                                                          #
+# SVG Features supported:                                  #
+#  * Groups                                                #
+#  * Rectangles (w/ rounded corners)                       #
+#  * Circles                                               #
+#  * Ellipses                                              #
+#  * Polygons                                              #
+#  * Polylines                                             #
+#  * Lines                                                 #
+#  * Paths                                                 #
+#  * All transformations                                   #
+#                                                          #
+#  Reference: www.w3.org/TR/SVG/Overview.html              #
+############################################################
+
+# import xml.etree.ElementTree as ET
+from lxml import etree as ET
+import re
+import itertools
+from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path
+from svg.path.path import Move
+from shapely.geometry import LinearRing, LineString, Point, Polygon
+from shapely.affinity import translate, rotate, scale, skew, affine_transform
+import numpy as np
+import logging
+
+from ParseFont import *
+
+log = logging.getLogger('base2')
+
+
+def svgparselength(lengthstr):
+    """
+    Parse an SVG length string into a float and a units
+    string, if any.
+
+    :param lengthstr: SVG length string.
+    :return: Number and units pair.
+    :rtype: tuple(float, str|None)
+    """
+
+    integer_re_str = r'[+-]?[0-9]+'
+    number_re_str = r'(?:[+-]?[0-9]*\.[0-9]+(?:[Ee]' + integer_re_str + ')?' + r')|' + \
+                    r'(?:' + integer_re_str + r'(?:[Ee]' + integer_re_str + r')?)'
+    length_re_str = r'(' + number_re_str + r')(em|ex|px|in|cm|mm|pt|pc|%)?'
+
+    match = re.search(length_re_str, lengthstr)
+    if match:
+        return float(match.group(1)), match.group(2)
+
+    return
+
+
+def path2shapely(path, object_type, res=1.0):
+    """
+    Converts an svg.path.Path into a Shapely
+    LinearRing or LinearString.
+
+    :rtype : LinearRing
+    :rtype : LineString
+    :param path: svg.path.Path instance
+    :param res: Resolution (minimum step along path)
+    :return: Shapely geometry object
+    """
+
+    points = []
+    geometry = []
+    geo_element = None
+
+    for component in path:
+
+        # Line
+        if isinstance(component, Line):
+            start = component.start
+            x, y = start.real, start.imag
+            if len(points) == 0 or points[-1] != (x, y):
+                points.append((x, y))
+            end = component.end
+            points.append((end.real, end.imag))
+            continue
+
+        # Arc, CubicBezier or QuadraticBezier
+        if isinstance(component, Arc) or \
+           isinstance(component, CubicBezier) or \
+           isinstance(component, QuadraticBezier):
+
+            # How many points to use in the discrete representation.
+            length = component.length(res / 10.0)
+            steps = int(length / res + 0.5)
+
+            # solve error when step is below 1,
+            # it may cause other problems, but LineString needs at least  two points
+            if steps == 0:
+                steps = 1
+
+            frac = 1.0 / steps
+
+            # print length, steps, frac
+            for i in range(steps):
+                point = component.point(i * frac)
+                x, y = point.real, point.imag
+                if len(points) == 0 or points[-1] != (x, y):
+                    points.append((x, y))
+            end = component.point(1.0)
+            points.append((end.real, end.imag))
+            continue
+
+        # Move
+        if isinstance(component, Move):
+            if object_type == 'geometry':
+                geo_element = LineString(points)
+            elif object_type == 'gerber':
+                # Didn't worked out using Polygon because if there is a large outline it will envelope everything
+                # and create issues with intersections. I will let the parameter obj_type present though
+                # geo_element = Polygon(points)
+                geo_element = LineString(points)
+            else:
+                log.error("[error]: Not a valid target object.")
+            if not points:
+                continue
+            else:
+                geometry.append(geo_element)
+                points = []
+            continue
+        log.warning("I don't know what this is: %s" % str(component))
+        continue
+
+    # if there are still points in points then add them as a LineString
+    if points:
+        geo_element = LineString(points)
+        geometry.append(geo_element)
+        points = []
+
+    # if path.closed:
+    #     return Polygon(points).buffer(0)
+    #     # return LinearRing(points)
+    # else:
+    #     return LineString(points)
+    return geometry
+
+def svgrect2shapely(rect, n_points=32):
+    """
+    Converts an SVG rect into Shapely geometry.
+
+    :param rect: Rect Element
+    :type rect: xml.etree.ElementTree.Element
+    :return: shapely.geometry.polygon.LinearRing
+    """
+    w = svgparselength(rect.get('width'))[0]
+    h = svgparselength(rect.get('height'))[0]
+    x_obj = rect.get('x')
+    if x_obj is not None:
+        x = svgparselength(x_obj)[0]
+    else:
+        x = 0
+    y_obj = rect.get('y')
+    if y_obj is not None:
+        y = svgparselength(y_obj)[0]
+    else:
+        y = 0
+    rxstr = rect.get('rx')
+    rystr = rect.get('ry')
+
+    if rxstr is None and rystr is None:  # Sharp corners
+        pts = [
+            (x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)
+        ]
+
+    else:  # Rounded corners
+        rx = 0.0 if rxstr is None else svgparselength(rxstr)[0]
+        ry = 0.0 if rystr is None else svgparselength(rystr)[0]
+
+        n_points = int(n_points / 4 + 0.5)
+        t = np.arange(n_points, dtype=float) / n_points / 4
+
+        x_ = (x + w - rx) + rx * np.cos(2 * np.pi * (t + 0.75))
+        y_ = (y + ry) + ry * np.sin(2 * np.pi * (t + 0.75))
+
+        lower_right = [(x_[i], y_[i]) for i in range(n_points)]
+
+        x_ = (x + w - rx) + rx * np.cos(2 * np.pi * t)
+        y_ = (y + h - ry) + ry * np.sin(2 * np.pi * t)
+
+        upper_right = [(x_[i], y_[i]) for i in range(n_points)]
+
+        x_ = (x + rx) + rx * np.cos(2 * np.pi * (t + 0.25))
+        y_ = (y + h - ry) + ry * np.sin(2 * np.pi * (t + 0.25))
+
+        upper_left = [(x_[i], y_[i]) for i in range(n_points)]
+
+        x_ = (x + rx) + rx * np.cos(2 * np.pi * (t + 0.5))
+        y_ = (y + ry) + ry * np.sin(2 * np.pi * (t + 0.5))
+
+        lower_left = [(x_[i], y_[i]) for i in range(n_points)]
+
+        pts = [(x + rx, y), (x - rx + w, y)] + \
+            lower_right + \
+            [(x + w, y + ry), (x + w, y + h - ry)] + \
+            upper_right + \
+            [(x + w - rx, y + h), (x + rx, y + h)] + \
+            upper_left + \
+            [(x, y + h - ry), (x, y + ry)] + \
+            lower_left
+
+    return Polygon(pts).buffer(0)
+    # return LinearRing(pts)
+
+
+def svgcircle2shapely(circle):
+    """
+    Converts an SVG circle into Shapely geometry.
+
+    :param circle: Circle Element
+    :type circle: xml.etree.ElementTree.Element
+    :return: Shapely representation of the circle.
+    :rtype: shapely.geometry.polygon.LinearRing
+    """
+    # cx = float(circle.get('cx'))
+    # cy = float(circle.get('cy'))
+    # r = float(circle.get('r'))
+    cx = svgparselength(circle.get('cx'))[0]  # TODO: No units support yet
+    cy = svgparselength(circle.get('cy'))[0]  # TODO: No units support yet
+    r = svgparselength(circle.get('r'))[0]  # TODO: No units support yet
+
+    # TODO: No resolution specified.
+    return Point(cx, cy).buffer(r)
+
+
+def svgellipse2shapely(ellipse, n_points=64):
+    """
+    Converts an SVG ellipse into Shapely geometry
+
+    :param ellipse: Ellipse Element
+    :type ellipse: xml.etree.ElementTree.Element
+    :param n_points: Number of discrete points in output.
+    :return: Shapely representation of the ellipse.
+    :rtype: shapely.geometry.polygon.LinearRing
+    """
+
+    cx = svgparselength(ellipse.get('cx'))[0]  # TODO: No units support yet
+    cy = svgparselength(ellipse.get('cy'))[0]  # TODO: No units support yet
+
+    rx = svgparselength(ellipse.get('rx'))[0]  # TODO: No units support yet
+    ry = svgparselength(ellipse.get('ry'))[0]  # TODO: No units support yet
+
+    t = np.arange(n_points, dtype=float) / n_points
+    x = cx + rx * np.cos(2 * np.pi * t)
+    y = cy + ry * np.sin(2 * np.pi * t)
+    pts = [(x[i], y[i]) for i in range(n_points)]
+
+    return Polygon(pts).buffer(0)
+    # return LinearRing(pts)
+
+
+def svgline2shapely(line):
+    """
+
+    :param line: Line element
+    :type line: xml.etree.ElementTree.Element
+    :return: Shapely representation on the line.
+    :rtype: shapely.geometry.polygon.LinearRing
+    """
+
+    x1 = svgparselength(line.get('x1'))[0]
+    y1 = svgparselength(line.get('y1'))[0]
+    x2 = svgparselength(line.get('x2'))[0]
+    y2 = svgparselength(line.get('y2'))[0]
+
+    return LineString([(x1, y1), (x2, y2)])
+
+
+def svgpolyline2shapely(polyline):
+
+    ptliststr = polyline.get('points')
+    points = parse_svg_point_list(ptliststr)
+
+    return LineString(points)
+
+
+def svgpolygon2shapely(polygon):
+
+    ptliststr = polygon.get('points')
+    points = parse_svg_point_list(ptliststr)
+
+    return Polygon(points).buffer(0)
+    # return LinearRing(points)
+
+
+def getsvggeo(node, object_type):
+    """
+    Extracts and flattens all geometry from an SVG node
+    into a list of Shapely geometry.
+
+    :param node: xml.etree.ElementTree.Element
+    :return: List of Shapely geometry
+    :rtype: list
+    """
+    kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
+    geo = []
+
+    # Recurse
+    if len(node) > 0:
+        for child in node:
+            subgeo = getsvggeo(child, object_type)
+            if subgeo is not None:
+                geo += subgeo
+
+    # Parse
+    elif kind == 'path':
+        log.debug("***PATH***")
+        P = parse_path(node.get('d'))
+        P = path2shapely(P, object_type)
+        # for path, the resulting geometry is already a list so no need to create a new one
+        geo = P
+
+    elif kind == 'rect':
+        log.debug("***RECT***")
+        R = svgrect2shapely(node)
+        geo = [R]
+
+    elif kind == 'circle':
+        log.debug("***CIRCLE***")
+        C = svgcircle2shapely(node)
+        geo = [C]
+
+    elif kind == 'ellipse':
+        log.debug("***ELLIPSE***")
+        E = svgellipse2shapely(node)
+        geo = [E]
+
+    elif kind == 'polygon':
+        log.debug("***POLYGON***")
+        poly = svgpolygon2shapely(node)
+        geo = [poly]
+
+    elif kind == 'line':
+        log.debug("***LINE***")
+        line = svgline2shapely(node)
+        geo = [line]
+
+    elif kind == 'polyline':
+        log.debug("***POLYLINE***")
+        pline = svgpolyline2shapely(node)
+        geo = [pline]
+
+    else:
+        log.warning("Unknown kind: " + kind)
+        geo = None
+
+    # ignore transformation for unknown kind
+    if geo is not None:
+        # Transformations
+        if 'transform' in node.attrib:
+            trstr = node.get('transform')
+            trlist = parse_svg_transform(trstr)
+            # log.debug(trlist)
+
+            # Transformations are applied in reverse order
+            for tr in trlist[::-1]:
+                if tr[0] == 'translate':
+                    geo = [translate(geoi, tr[1], tr[2]) for geoi in geo]
+                elif tr[0] == 'scale':
+                    geo = [scale(geoi, tr[0], tr[1], origin=(0, 0))
+                           for geoi in geo]
+                elif tr[0] == 'rotate':
+                    geo = [rotate(geoi, tr[1], origin=(tr[2], tr[3]))
+                           for geoi in geo]
+                elif tr[0] == 'skew':
+                    geo = [skew(geoi, tr[1], tr[2], origin=(0, 0))
+                           for geoi in geo]
+                elif tr[0] == 'matrix':
+                    geo = [affine_transform(geoi, tr[1:]) for geoi in geo]
+                else:
+                    raise Exception('Unknown transformation: %s', tr)
+
+    return geo
+
+
+def getsvgtext(node, object_type, units='MM'):
+    """
+    Extracts and flattens all geometry from an SVG node
+    into a list of Shapely geometry.
+
+    :param node: xml.etree.ElementTree.Element
+    :return: List of Shapely geometry
+    :rtype: list
+    """
+    kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
+    geo = []
+
+    # Recurse
+    if len(node) > 0:
+        for child in node:
+            subgeo = getsvgtext(child, object_type, units=units)
+            if subgeo is not None:
+                geo += subgeo
+
+    # Parse
+    elif kind == 'tspan':
+        current_attrib = node.attrib
+        txt = node.text
+        style_dict = {}
+        parrent_attrib = node.getparent().attrib
+        style = parrent_attrib['style']
+
+        try:
+            style_list = style.split(';')
+            for css in style_list:
+                style_dict[css.rpartition(':')[0]] = css.rpartition(':')[-1]
+
+            pos_x = float(current_attrib['x'])
+            pos_y = float(current_attrib['y'])
+
+            # should have used the instance from FlatCAMApp.App but how? without reworking everything ...
+            pf = ParseFont()
+            pf.get_fonts_by_types()
+            font_name = style_dict['font-family'].replace("'", '')
+
+            if style_dict['font-style'] == 'italic' and style_dict['font-weight'] == 'bold':
+                font_type = 'bi'
+            elif style_dict['font-weight'] == 'bold':
+                font_type = 'bold'
+            elif style_dict['font-style'] == 'italic':
+                font_type = 'italic'
+            else:
+                font_type = 'regular'
+
+            # value of 2.2 should have been 2.83 (conversion value from pixels to points)
+            # but the dimensions from Inkscape did not corelate with the ones after importing in FlatCAM
+            # so I adjusted this
+            font_size = svgparselength(style_dict['font-size'])[0] * 2.2
+            geo = [pf.font_to_geometry(txt,
+                                       font_name=font_name,
+                                       font_size=font_size,
+                                       font_type=font_type,
+                                       units=units,
+                                       coordx=pos_x,
+                                       coordy=pos_y)
+                   ]
+
+            geo = [(scale(g, 1.0, -1.0)) for g in geo]
+        except Exception as e:
+            log.debug(str(e))
+    else:
+        geo = None
+
+    # ignore transformation for unknown kind
+    if geo is not None:
+        # Transformations
+        if 'transform' in node.attrib:
+            trstr = node.get('transform')
+            trlist = parse_svg_transform(trstr)
+            # log.debug(trlist)
+
+            # Transformations are applied in reverse order
+            for tr in trlist[::-1]:
+                if tr[0] == 'translate':
+                    geo = [translate(geoi, tr[1], tr[2]) for geoi in geo]
+                elif tr[0] == 'scale':
+                    geo = [scale(geoi, tr[0], tr[1], origin=(0, 0))
+                           for geoi in geo]
+                elif tr[0] == 'rotate':
+                    geo = [rotate(geoi, tr[1], origin=(tr[2], tr[3]))
+                           for geoi in geo]
+                elif tr[0] == 'skew':
+                    geo = [skew(geoi, tr[1], tr[2], origin=(0, 0))
+                           for geoi in geo]
+                elif tr[0] == 'matrix':
+                    geo = [affine_transform(geoi, tr[1:]) for geoi in geo]
+                else:
+                    raise Exception('Unknown transformation: %s', tr)
+
+    return geo
+
+def parse_svg_point_list(ptliststr):
+    """
+    Returns a list of coordinate pairs extracted from the "points"
+    attribute in SVG polygons and polyline's.
+
+    :param ptliststr: "points" attribute string in polygon or polyline.
+    :return: List of tuples with coordinates.
+    """
+
+    pairs = []
+    last = None
+    pos = 0
+    i = 0
+
+    for match in re.finditer(r'(\s*,\s*)|(\s+)', ptliststr.strip(' ')):
+
+        val = float(ptliststr[pos:match.start()])
+
+        if i % 2 == 1:
+            pairs.append((last, val))
+        else:
+            last = val
+
+        pos = match.end()
+        i += 1
+
+    # Check for last element
+    val = float(ptliststr[pos:])
+    if i % 2 == 1:
+        pairs.append((last, val))
+    else:
+        log.warning("Incomplete coordinates.")
+
+    return pairs
+
+
+def parse_svg_transform(trstr):
+    """
+    Parses an SVG transform string into a list
+    of transform names and their parameters.
+
+    Possible transformations are:
+
+    * Translate: translate(<tx> [<ty>]), which specifies
+      a translation by tx and ty. If <ty> is not provided,
+      it is assumed to be zero. Result is
+      ['translate', tx, ty]
+
+    * Scale: scale(<sx> [<sy>]), which specifies a scale operation
+      by sx and sy. If <sy> is not provided, it is assumed to be
+      equal to <sx>. Result is: ['scale', sx, sy]
+
+    * Rotate: rotate(<rotate-angle> [<cx> <cy>]), which specifies
+      a rotation by <rotate-angle> degrees about a given point.
+      If optional parameters <cx> and <cy> are not supplied,
+      the rotate is about the origin of the current user coordinate
+      system. Result is: ['rotate', rotate-angle, cx, cy]
+
+    * Skew: skewX(<skew-angle>), which specifies a skew
+      transformation along the x-axis. skewY(<skew-angle>), which
+      specifies a skew transformation along the y-axis.
+      Result is ['skew', angle-x, angle-y]
+
+    * Matrix: matrix(<a> <b> <c> <d> <e> <f>), which specifies a
+      transformation in the form of a transformation matrix of six
+      values. matrix(a,b,c,d,e,f) is equivalent to applying the
+      transformation matrix [a b c d e f]. Result is
+      ['matrix', a, b, c, d, e, f]
+
+    Note: All parameters to the transformations are "numbers",
+    i.e. no units present.
+
+    :param trstr: SVG transform string.
+    :type trstr: str
+    :return: List of transforms.
+    :rtype: list
+    """
+    trlist = []
+
+    assert isinstance(trstr, str)
+    trstr = trstr.strip(' ')
+
+    integer_re_str = r'[+-]?[0-9]+'
+    number_re_str = r'(?:[+-]?[0-9]*\.[0-9]+(?:[Ee]' + integer_re_str + ')?' + r')|' + \
+                    r'(?:' + integer_re_str + r'(?:[Ee]' + integer_re_str + r')?)'
+
+    # num_re_str = r'[\+\-]?[0-9\.e]+'  # TODO: Negative exponents missing
+    comma_or_space_re_str = r'(?:(?:\s+)|(?:\s*,\s*))'
+    translate_re_str = r'translate\s*\(\s*(' + \
+                       number_re_str + r')(?:' + \
+                       comma_or_space_re_str + \
+                       r'(' + number_re_str + r'))?\s*\)'
+    scale_re_str = r'scale\s*\(\s*(' + \
+                   number_re_str + r')' + \
+                   r'(?:' + comma_or_space_re_str + \
+                   r'(' + number_re_str + r'))?\s*\)'
+    skew_re_str = r'skew([XY])\s*\(\s*(' + \
+                  number_re_str + r')\s*\)'
+    rotate_re_str = r'rotate\s*\(\s*(' + \
+                    number_re_str + r')' + \
+                    r'(?:' + comma_or_space_re_str + \
+                    r'(' + number_re_str + r')' + \
+                    comma_or_space_re_str + \
+                    r'(' + number_re_str + r'))?\s*\)'
+    matrix_re_str = r'matrix\s*\(\s*' + \
+                    r'(' + number_re_str + r')' + comma_or_space_re_str + \
+                    r'(' + number_re_str + r')' + comma_or_space_re_str + \
+                    r'(' + number_re_str + r')' + comma_or_space_re_str + \
+                    r'(' + number_re_str + r')' + comma_or_space_re_str + \
+                    r'(' + number_re_str + r')' + comma_or_space_re_str + \
+                    r'(' + number_re_str + r')\s*\)'
+
+    while len(trstr) > 0:
+        match = re.search(r'^' + translate_re_str, trstr)
+        if match:
+            trlist.append([
+                'translate',
+                float(match.group(1)),
+                float(match.group(2)) if match.group else 0.0
+            ])
+            trstr = trstr[len(match.group(0)):].strip(' ')
+            continue
+
+        match = re.search(r'^' + scale_re_str, trstr)
+        if match:
+            trlist.append([
+                'translate',
+                float(match.group(1)),
+                float(match.group(2)) if not None else float(match.group(1))
+            ])
+            trstr = trstr[len(match.group(0)):].strip(' ')
+            continue
+
+        match = re.search(r'^' + skew_re_str, trstr)
+        if match:
+            trlist.append([
+                'skew',
+                float(match.group(2)) if match.group(1) == 'X' else 0.0,
+                float(match.group(2)) if match.group(1) == 'Y' else 0.0
+            ])
+            trstr = trstr[len(match.group(0)):].strip(' ')
+            continue
+
+        match = re.search(r'^' + rotate_re_str, trstr)
+        if match:
+            trlist.append([
+                'rotate',
+                float(match.group(1)),
+                float(match.group(2)) if match.group(2) else 0.0,
+                float(match.group(3)) if match.group(3) else 0.0
+            ])
+            trstr = trstr[len(match.group(0)):].strip(' ')
+            continue
+
+        match = re.search(r'^' + matrix_re_str, trstr)
+        if match:
+            trlist.append(['matrix'] + [float(x) for x in match.groups()])
+            trstr = trstr[len(match.group(0)):].strip(' ')
+            continue
+
+        # raise Exception("Don't know how to parse: %s" % trstr)
+        log.error("[error] Don't know how to parse: %s" % trstr)
+
+    return trlist
+
+# if __name__ == "__main__":
+#     tree = ET.parse('tests/svg/drawing.svg')
+#     root = tree.getroot()
+#     ns = re.search(r'\{(.*)\}', root.tag).group(1)
+#     print(ns)
+#     for geo in getsvggeo(root):
+#         print(geo)

+ 228 - 0
PlotCanvas.py

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

+ 1407 - 0
README.md

@@ -0,0 +1,1407 @@
+FlatCAM: 2D Computer-Aided PCB Manufacturing
+=================================================
+
+(c) 2014-2018 Juan Pablo Caram
+
+FlatCAM is a program for preparing CNC jobs for making PCBs on a CNC router.
+Among other things, it can take a Gerber file generated by your favorite PCB
+CAD program, and create G-Code for Isolation routing.
+
+=================================================
+
+3.01.2019
+
+- initial merge into FlatCAM regular
+
+28.12.2018
+
+- changed the workspace drawing from 'gl' to 'agg'. 'gl' has better performance but it messes with the overlapping graphics
+- removed the initial obj.build_ui() in App.editor2object()
+
+25.12.2018
+
+- fixed bugs in Excellon Editor due of PyQt5 port
+- fixed bug when loading Gerber with follow
+- fixed bug that when a Gerber was loaded with -follow parameter it could not be isolated external and full
+- changed multiple status bar messages
+- changed some assertions to (status error message + return) combo
+- fixed issues in 32bit installers
+- added protection against using Excellon joining on different kind of objects
+- fixed bug in ToolCutout where the Rectangular Cutout used the Type of Gaps from Freeform Cutout
+- fixed bug that didn't allowed saving SVG file from a Gerber file
+- modified setup_ubuntu.sh file for PyQt5 packages
+
+23.12.2018
+
+- added move (as in Tool Move) capability for CNCJob object and the GCode is updated on each move --> finished both for Gcode loaded and for CNCJob generated in the app
+- fixed some errors related to DialogOpen widget that I've missed in PyQt5 porting
+- added a bounds() method for CNCJob class in camlib (perhaps overdone as it worked well with the one inherited)
+- small changes in Paint Tool - the rest machining is working only partially
+- added more columns in CNCjob Tool Table showing more info about the present tools
+- make the columns in CNCJob Tool Table not editable as it has no sense
+
+22.12.2018
+
+- fixed issues in Transform Tool regarding the message boxes
+- fixed more error in Double Sided Tool and added some more information's in ToolTips
+- added more information's in CutOut Tool ToolTips
+- updated the tooltips in amost all FlatCAM tools; in Tool Tables added column header ToolTips
+- fixed NCC rest machining in NCC Tool; added status message and stop object creation if there is no geometry on any tool
+- fixed version number: now it will made of a number in format main_version.secondary_version/working_version
+- modified the makefile for windows builds to accommodate both 32bit and 64bit executable generation
+
+21.12.2018
+
+- added shortcut "SHIFT + W" for workspace toggle
+- updated the list of shortcuts
+- forbid editing for the MultiGeo type of Geometry because the Geometry Editor is not prepared for this
+- finished a "sort" of rest-machining for Non Copper Clearing Tool but it's time consuming operation
+- reworked the NCC Tool as it was fundamental wrong - still has issues on the rest machining
+- added a parameter reset for each run of Paint Tool and NCC Tool
+
+20.12.2018
+
+- porting application to PyQt5
+- adjusted the level of many status bar messages
+- created new bounds() methods for Excellon and Gerber objects as the one inherited from Geometry failed in conjunction with PyQt5
+- fixed some small bugs where a string was divided by a float finally casting the result to an integer
+- removed the 'raise' conditions everywhere I could and make protections against loading files in the wrong place
+- fixed a "PyCharm stupid paste on the previous tab level even after one free line " in Excellon.bounds()
+- in Geometry object fixed error in tool_delete regarding deletion while iterating a dict
+- started to rework the NCC Tool to generate one file only
+- in Geometry Tool Table added checkboxes for individual plot of tools in case of MultiGeo Geometry
+- rework of NCC Tool UI
+- added a automatic selector: if the system is 32bit the OR-tools imports are not done and the OR-tools drill path optimizations are replaced by a default Travelling Salesman drill path optimization
+- created a Win32 make file to generate a Win32 executable
+- disabled the Plot column in Geometry Tool Table when the geometry is SingleGeo as it is not needed
+- solved a issue when doing isolation, if the solid_geometry is not a list will make it a list
+- added tooltips in the Geometry Tool Table headers explaining each column
+- added a new Tcl Command: clear. It clears the Tcl Shell of all text and restore it to the original state
+- fixed Properties Tool area calculation; added status bar messages if there is no object selected show an error and successful showing properties is confirmed in status bar
+- when Preferences are saved, now the default values are instantly propagated within the application
+- when a geometry is MultiGeo and all the tools are deleted, it will have no geometry at all therefore all that it's plotted on canvas that used to belong to it has to be deleted and because now it is an empty object we demote it to SingleGeo so it can be edited
+
+19.12.2018
+
+- fixed SVG_export for MultiGeo Geometries
+- fixed DXF_export for MultiGeo Geometries
+- fixed SingleGeo to MultiGeo conversion plotting bug
+
+18.12.2018
+
+- small changes in FlatCAMGeometry.plot()
+- updated the FlatCAMGeometry.merge() function and the Join Geometry feature to accommodate the different types of geometries: singlegeo and multigeo type
+- added Conversion submenu in Edit where I moved the Join features and added the Convert from MultiGeo to SingleGeo type and the reverse
+- added Copy Tool (on a selection of tools) feature in Geometry Object UI 
+- fixed the bounds() method for the MultiGeo geometry object so the canvas selection is working and also the Properties Tool
+- fixed Move Tool to support MultiGeo geometry objects moving
+- added tool edit in Geometry Object Tool Table
+- added Tool Table context menu in Geometry Object and in Paint Tool
+- modified some Status Bar messages in Geometry Object
+
+17.12.2018
+
+- added support for multiple solid_geometry in a geometry object; each tool can now have it's own geometry. Plot, project save/load are OK.
+- added support for single GCode file generation from multi-tool PaintTool job
+- added protection for results of Paint Tool job that do not have geometry at all. An Error will be issued. It can happen if the combination of Paint parameters is not good enough
+- solved a small bug that didn't allow the Paint Job to be done with lines when the results were geometries not iterable 
+- added protection for the case when trying to run the cncjob Tcl Command on a Geometry object that do not have solid geometry or one that is multi-tool
+- Paint Tool Table: now it is possible to edit a tool to a new diameter and then edit another tool to the former diameter of the first edited tool
+- added a new type of warning, [warning_notcl]
+- fixed conflict with "space" keyboard shortcut for CNC job
+
+16.12.2018
+
+- redone the Options menu; removed the Transfer Options as they were not used
+- deleted some folders in the project structure that were never used
+- Paint polygon Single works only for left mouse click allowing mouse panning
+- added ability to print errors in status bar without raising Tcl Shell
+- fixed small bug: when doing interiors isolation on a Gerber that don't allow it, no object is created now and an error in the status bar is issued
+- fixed bug in Paint All for Geometry made from exteriors Gerber isolation
+- fixed the join geometry: when the geometries has different tools the join will fail with a status bar message (as it should). Allow joining of geometries that have no tool. // Reverted on 18.12.2018
+- changed the error messages that are simple to the kind that do not open the TCl shell
+- fixed some issues in Geometry Object
+- Paint Tool - reworked the UI and made it compatible with the Geometry Object UI
+- Paint Tool - tool edit functional
+- added Clear action in the Context menu of the TCl Shell
+
+14.12.2018
+
+- fixed typo in setup_ubuntu.sh
+- minor changes in Excellon Object UI
+- added Tool Table in Paint Tool
+- now in Paint Tool and Non Copper Clearing Tool a selection of tools can be deleted (not only one by one)
+- minor GUI changes (added/corrected tooltips)
+- optimized vispy startup time from about >6 sec to ~3 seconds
+- removed vispy text collection starting in plotcanvas as it did nothing // RESTORED 18.12.2018 as it messed the graphical presentation
+- fixed cncjob TclCommand for the new type of Geometry
+- make sure that when using the TclCommands, the object names are case insensitive
+- updated the TCL Shell auto-complete function; now it will index also the names of objects created or loaded in the application
+- on object removal the name is removed from the Shell auto-complete model
+
+13.12.2018
+
+NEW Geometry Object and CNC Object architecture (3rd attempt) which allow multiple tools for one geometry
+
+- fixed issue with cumulative G-code after successive delete/create of a CNCJob on the same geometry (some references were kept after deletion of CNCJob object which kept the deleted tools data and added it to a new one)
+- fixed plot and export G-code in new format
+- fixed project save/load in the new format for geometry
+- added new feature in CNCJob Object UI: since we may have multiple tools per CNCJob object due of having multiple tool in Geometry Object,
+now there is a Tool Table in CNC Object UI and each tool GCode can be enabled or disabled
+
+12.12.2018
+
+- Geometry Tool Table: when the Offset type is 'custom' each tool it's storing the value and it is updated on UI when that tool is selected in UI table
+- Geometry Tool Table: fixed tool offset conversion when the Offset in Tool Table UI is set to Custom
+
+11.12.2018
+
+- cleaned up the generatecncjob() function in FlatCAMObj
+- created a new function for generating cncjob out of multitool geometry, mtool_generate_cncjob()
+- cleaned up the generate_from_geometry_2() method in camlib
+- Geometry Tool Table: new tool added copy all the form fields (data) from the last tool
+- finished work on generation of a single CNC Job file (therefore a single GCODE file) even for multiple tools in Geo Tool Table
+- GCode header is added only on saving the file therefore the time generation will be reflected in the file
+- modified postprocessors to accommodate the new CNC Job file with multiple tools
+- modified postprocessors so the last X,Y move will be to the toolchange X,Y pos (set in Preferences)
+- save_project and load_project now work with the new type of multitool geometry and cncjob objects
+
+10.12.2018
+
+- added new feature in Geometry Tool Table: if the Offset type in tool table is 'Offset' then a new entry is unhidden and the user can use custom offset
+- Geometry Tool Table: fixed add new tool with diameter with many decimals
+- Geometry Tool Table: when editing the tip dia or tip angle for the V Shape tool, the CutZ is automatically calculated
+
+9.12.2018
+
+- new Geometry Tool Table has functional unit conversion
+- when entering a float number in Spindle Speed now there is no error and only the integer part is used, the decimals are discarded
+- finished the Geometry Tool Table in the form that generates only multiple files
+- if tool type is V-Shape ('V') then the Cut Z entry is disabled and new 'Tip Dia' and 'Tip Angle' fields are showed. The values entered will calculate the Cut Z parameter
+
+5.12.2018
+
+- remade the Geometry Tool Table, before this change each tool could not store it's own set of data in case of multiple tools with same diameter
+- added a new column in Geo Tool Table where to specify which type of tool to use: C for circular, B for Ball and V for V-shape
+
+4.12.2018
+
+- new geometry/excellon object name is now only "new_g"/"new_e" as the type is clear from the category is into (and the associated icon)
+- always autoselect the first tool in the Geometry Tool table
+- issue error message if the user is trying to generate CNCJob without a tool selected in Geometry Tool Table
+- add the whole data from Geometry Object GUI as dict in the geometry tool dict so each tool (file) will have it's own set of data
+
+3.12.2018
+
+- Geometry Tool table: delete multiple tools with same diameter = DONE
+- Geometry Tool table: possibility to cut a path inside or outside or on path = DONE
+- Geometry Tool table: fixed situation when user tries to add a tool but there is no tool diameter entered
+- if a geometry is a closed shape then create a Polygon out of it
+- some fixes in Non Copper Clearing Tool
+- Geometry Tool table: added option to delete_tool function for delete_all
+- Geometry Tool table: added ability to delete even the last tool in tool_table and added an warning if the user try to generate a CNC Job without a tool in tool table
+- if a geometry is painted inside the Geometry Editor then it will store the tool diameter used for this painting. Only one tool cn be stored (the last one) so if multiple paintings are done with different tools in the same geometry it will store only the last used tool.
+- if multiple geometries have different tool diameters associated (contain a paint geometry) they aren't allowed to be joined and a message is displayed letting the user know
+
+2.12.2018
+
+- started to work on a geometry Tool Table
+- renamed FlatCAMShell as ToolShell and moved it (and termwidget) to flatcamTools folder
+- cleaned up the ToolShell by removing the termwidget separate file and added those classes to ToolShell
+- added autocomplete for TCL Shell - the autocomplete key is 'TAB'
+- covered some possible exceptions in rotate/skew/mirror functions
+- Geometry Tool table: add/delete tools = DONE
+- Geometry Tool table: add multiple tools with same diameter = DONE
+
+1.12.2018
+
+- fixed Gerber parser so now the Gerber regions that have D02 operation code just before the end of the region will be processed correctly. Autotrax Dex Gerbers are now loaded
+- fixed an issue with temporary geo storage "geo" being referenced before assignment
+- moved all FlatCAM Tools into a single directory
+
+30.11.2018
+
+- remade the CutOut Tool. I've put together the former Freeform Cutout tool and the Cutout Object fount in Gerber Object GUI and left only a link in the Gerber Object GUI. This tidy the GUI a bit.
+- created a Paint Tool and replaced the Paint Area section in Geometry Object GUI with a link to this tool.
+- fixed bug in former Paint Area and in the new Paint Tool that made the paint method not to be saved in App preferences
+- solved a bug in Gerber parser: in case that an operation code D? was encountered alone it was not remembered - fixed
+- fixed bug related to the newly entered toolchange feature for Geometry: it was trying to evaluate toolchange_z as a comma separated value like for toolchange x,y
+- fixed bug in scaling units in CNC Job which made the unit change between INCH and MM not possible if a CNC Job was present in the project objects
+
+29.11.2018
+
+- added checks for using a Z Cut with positive value. The Z Cut parameter has to be negative so if the app will detect a positive value it will automatically convert it to negative
+- started to implement rest-machining for Non Copper clearing Tool - for now the results are not great
+- added Toolchange X,Y position parameters and modified the default and manual_toolchange postprocessor file to use them
+For now they are used only for Excellon objects who do have toolchange events
+- added Toolchange event selection for Geometry objects; for now it is as before, single tool on each file
+- remade the GUI for objects and in Preferences to have uniformity
+- fixed bug: after editing a newly created excellon/geometry object the object UI used to not keep the original settings
+- fixed some bugs in Tool Add feature of the new Non Copper Clear Tool
+- added some messages in the Non Copper Clear Tool
+- added parameters for coordinates no of decimals and for feedrate no of decimals used in the resulting GCODE. They are in EDIT -> Preferences -> CNC Job Options
+- modified the postprocessors to use the "decimals" parameters
+
+28.11.2018
+
+- added different methods of copper clearing (standard, seed, line_based) and "connect", "contour" options found in Paint function
+- remake of the non-copper clearing tool as a separate tool
+- modified the "About" menu entry to mention the main contributors to FlatCAM 3000 
+- modified Marlin postprocessor according to modifications made by @redbull0174 user from FlatCAM.org forum
+- modified Move Tool so it will detect if there is no object to move and issue a message
+
+27.11.2018
+
+- fixed bug in isolation with multiple passes
+- cosmetic changes in Buffer and Paint tool from Geometry Editor
+- changed the way selection box is working in Geometry Editor; now cumulative selection is done with modifier key (SHIFT or CONTROL) - before it was done by default
+- changed the default value for CNCJob tooldia to 1mm
+
+25.11.2018
+
+- each Tool change the name of the Tools tab to it's name
+- all open objects are no longer autoselected upon creation. Only on new Geometry/Excellon object creation it will be autoselected
+
+24.11.2018
+
+- restored the selection method in Geometry Editor to the original one found in FlatCAM 8.5
+- minor changes in Clear Copper function
+- minor changes in some postprocessors
+- change Join Geometry menu entry to Join Geo/Gerber
+- added menu entry for Toggle Axis in Menu -> View
+- added menu entry for Toggle Workspace in Menu -> View
+- added Bounding box area to the Properties (when metric units, in cm2)
+- non-copper clearing function optimization
+- fixed Z_toolchange value in the GCODE header
+
+21.11.2018
+
+- not very precise jump to location function
+- added shortcut key for jump to coordinates (J) and for Tool Transform (T)
+- some work in shortcut key
+
+19.11.2018
+
+- fixed issue with nested comment in postprocessors
+- fixed issue in Paint All; reverted changes
+
+18.11.2018
+
+- renamed FlatCAM 2018 to FlatCAM 3000
+- added new entries in the Help menu; one will show shortcut list and the other will start a YouTube webpage with a playlist where I will publish future training videos for this version of FlatCAM
+- if a Gerber region has issues the file will be loaded bypassing the error but there will be a TCL message letting the user know that there are parser errors. 
+
+17.11.2018
+
+- added Excellon parser support for units defined outside header
+
+
+12.11.2018
+
+- fixed bug in Paint Single Polygon
+- added spindle speed in laser postprocessor
+- added Z start move parameter. It controls the height at which the tool travel on the fist move in the job. Leave it blank if you don't need it.
+
+9.11.2018
+
+- fixed a reported bug generated by a typo for feedrate_z object in camlib.py. Because of that, the project could not be saved.
+- fixed a G01 usage (should be G1) in Marlin postprocessor.
+- changed the position of the Tool Dia entry in the Object UI and in FlatCAMGUI
+- fixed issues in the installer
+
+30.10.2018
+
+- fixed a bug in Freeform Cutout Tool - it was missing a change in the name of an object
+
+29.10.2018
+
+- added Excellon export menu entry and functionality that can export in fixed format 2:4 LZ INCH (format that Altium can load and it is a more generic format).
+It will be usefull for those who need FlatCAM to only convert the Excellon to a more useful format and visualize Gerbers.
+The other Excellon Export menu entry is exporting in units either Metric or INCH depending on the current units in FlatCAM, but it will always use the decimal format which may not be loaded in all cases.
+- disabled the Selected Tab while in Geometry Editor; the user is not supposed to have access to those functions while in Geometry Editor
+- added an menu entry in Menu -> File -> Recent Files named Clear Recent files which does exactly that
+- fixed issue: when a New Project is created but there is a Geometry still in Geometry Editor (or Excellon Editor) not saved, now that geometry is deleted
+- fixed problem when doing Clear Copper with Cut over 1st point option active. When the shape is not closed then it may cut over copper features. Originally the feature was meant to be used only with isolation geometry which is closed. Fixed
+
+28.10.2018
+
+- fixed Excellon Editor shortcut messages; also fixed differences in messages between usage by shortcuts and usage by menu toolbar actions
+- fixed Excellon Editor bug: it was triggering exceptions when the user selected a tool in tooltable and then tried to add a drill (or array) by clicking on canvas
+Clicking on canvas by default clear all the used tools, therefore the action could not be done. Fixed.
+- fixed bug Excellon Editor: when all the drills from a tool are resized, after resize they can't be selected.
+- Excellon Editor: added ability to delete multiple tools at once by doing multiple selection on the tooltable
+- Excellon Editor: if there are no more drills to a tool after doing drills resize then delete that tool from the tooltable
+- Excellon Editor: always select the last tool added to the tooltable
+- Excellon Editor: added a small canvas context menu for Excellon Editor
+
+27.10.2018
+
+- added a Paint tool toolbar icon and added shortcut key 'I' for Paint Tool
+- fixed unreliable multiple selection in Geometry Editor; some clicks were not registered
+- added utility geometry for Add Drill Array in Excellon Editor
+- fixed bug Excellon Editor: drills in drill array start now from the array start point (x, y); previously array start point was used only for calculating the radius
+- fixed bug Excellon Editor: Measurement Tool was not acting correctly in Exc Editor regarding connect/disconnect of events
+- in Excellon Editor every time a tool is clicked (except Select which is the default) the focus will return to Selected tab
+- added protection in Excellon Editor: if there is no tool/drill selected no operation over drills can be performed and a status bar message will be displayed
+- Excellon Editor: added relevant messages for all actions
+- fixed bug Excellon Editor: multiple selection with key modifier pressed (CTRL/SHIFT) either by simple click or through selection box is now working
+- fixed dwell parameter for Excellon in Preferences to be default Off
+
+26.10.2018
+
+- when objects are disabled they can't be selected
+- added Feedrate_z (Plunge) parameter for Geometry Object
+- fixed bug in units convert for Geometry Tab; added some missing parameters to the conversion list
+- fixed bug in isolation Geometry when the isolated Gerber was a single Polygon
+- updated the Paint function in Geometry Editor
+
+25.10.2018
+
+- added a verification on project saving to make sure that the project was saved successfully. If not, a message will be displayed in the status bar saying so.
+
+20.10.2018
+
+- fixed the SVG import as Gerber. But unfortunately, when there is a ground pour in a imported PCB SVG, the ground pour will be isolated inside
+instead to be isolated outside like every other feature. That's no way around this. The end result will be thinner features
+for the ground pour and if one is relying on those thin connections as GND links then it will not work as intended ,they may be broken.
+Of course one can edit the isolation geometry and delete the isolation for the ground pour.
+- delete selection shapes on double clicking on object as we may not want to have selection shape while Selected tab is active
+
+19.10.2018
+
+- solved some value update bugs in tool_table in Excellon Editor when editing tools followed by deleting another tool,
+and then re-adding the just-deleted tool.
+- added support for chaining blocks in DXF Import
+- fixed the DXF arc import
+- added support for a type of Gerber files generated by OrCAD where the Number format is combined with G74 on the same line
+- in Geometry Editor added the possibility for buffer to use different kinds of corners
+- added protection against loading an GCODE file as Excellon through drag & drop on canvas or file open dialog
+- added shortcut key 'B' for buffer operation inside Geometry Editor
+- added shell message in case the Font used in Text Tool in Geometry editor is not supported. Only Regular, Bold, Italic adn BoldItalic are supported as of yet.
+- added shortcut key 'T' for Text Tool inside Geometry Editor
+- added possibility for Drag & Drop on FlatCAM GUI with multiple files at once 
+
+18.10.2018
+
+- fixed DXF arc import in case of extrusion enabled
+- added on Geo Editor Toolbar the button for Buffer Geometry; added the possibility to create exterior and interior buffer
+- fixed a numpy import error
+
+17.10.2018
+
+- added Spline support and Ellipse (chord) support in DXF Import: chord might have issues
+(borrowed from the work of Vasilis Vlachoudis, https://github.com/vlachoudis/bCNC)
+- added Block support in DXF Import - no support yet for chained blocks (INSERT in block)
+- support for repasted block insertions
+
+16.10.2018
+
+- added persistent toolbar view: the enabled toolbars will be active at the next app startup while those that are not enabled will not be
+enabled at the next app startup. To enable/disable toolbars right click on the toolbar.
+
+15.10.2018
+
+- DXF Export works now also for Exteriors only and Interiors only geometry generated from Gerber Object
+- when a Geometry is edited, now the interiors and exterior of a Polygon that is part of the Geometry can be selected individually. In practice, if
+doing full isolation geometry, now both external and internal trace can be selected individually.
+
+13.10.2018
+
+- solved issue in CNC Code Editor: it appended text to the previous one even if the CNC Code Editor was closed
+- added .GBD Gerber extension to the lists
+- added support for closed polylines/lwpolylines in Import DXF; now PCB patterns found in PDF format can be imported in INKSCAPE
+and saved as DXF. FlatCAM can import DXF as Gerber and the user now can do isolation on it.
+
+12.10.2018
+
+- added zoom in, zoom out and zoom fit buttons on the View toolbar
+- fixed bug that on Double Sided Tool when a Excellon Alignment is created does not reset the list of Alignment drills
+- added a message warning the user to add Point coordinates in case the reference used in Double Sided Tool is Point
+- added new feature: DXF Export for Geometry
+
+10.10.2018
+
+- fixed a small bug in Setup Recent Files
+- small fix in Freeform Cutout Tool regarding objects populating the combo boxes
+- Excellon object name will reflect the number of edits performed on it
+
+9.10.2018
+
+- In Geometry Editor, now Path and Polygon draw mode can be finished not only with shortcut key Enter but also with right click on canvas
+- fixes regarding of circle linear approximation - final touch
+- fix for interference between Geo Editor and Excellon Editor
+- fixed Cut action in Geometry Editor so it can now be done multiple times on the target geometry without need for saving in between.
+- initial work on DXF import; made the GUI interface and functional structure
+- added import functions for DXF import
+- finished DXF Import (no blocks support, no SPLINE support for now)
+
+8.10.2018
+
+- completed toggle canvas selection when there is only one object under click position for the case when clicking the object is done
+while other object is already selected.
+- added static utility geometry just upon activating an Editor function
+- changed the way the canvas is showed on FlatCAM startup
+
+7.10.2018
+
+- solved mouse click not setting relative measurement origin to zero
+- solved bug that always added one drill when copying a selection of drills in the EXCELLON EDITOR
+- solved bug that the number of copied drills in Excellon Editor was not updated in the tool table
+- work in the Excellon Editor: found useful to change the diameter of one tool to another already in the list;
+could help for all those tools that are a fraction difference that comes from imperial to mm (or reverse) conversion,
+to reduce the tool changes - Done
+- in Excellon Editor, always auto-select the last tool added
+- in Excellon Editor fixed shortcuts for drill add and drill_array add: they were reversed. Now key 'A' is for array add
+and key 'D' is for drill add
+- solved a small bug in Excellon export: even when there were no slots in the file, it always added the tools list that
+acted as unnecessary toolchanges
+- after Move action, all objects are deselected
+
+
+6.10.2018
+
+- Added basic support for SVG text in SVG import. Will not work if some letters in a word have different style (italic bold or both)
+- added toggle selection to the canvas selection if there is only one object under the click position
+- added support for "repeat" command in Excellon file
+- added support for Allegro Gerber and Excellon files
+- Python 3.7 is used again; solved bug where the activity icon was not playing when FlatCAM active
+
+5.10.2018
+
+- fixed undesired setting focus to Project Tab when doing the SHIFT + LMB combo (to capture the click coordinates)
+
+4.10.2018
+
+- Excellon Editor: finished Add Drill Array - Linear type action
+- Excellon Editor: finished Add Drill Array - Circular type action
+- detected bug in shortcuts: Fixed
+- Excellon Editor: added constrain for adding circular array, if the number of drills multiplied by angle is more than 360
+the app will return with an message
+- solved sorting bug in the Excellon Editor tool table
+- solved bug in Menu -> Edit -> Sort Origin ; the selection box was not updated after offset
+- added Excellon Export in Menu -> File -> Export -> Export Excellon
+- added support to save the slots in the Excellon file in case there were some in the original file
+- fixed Double Sided Tool for the case of using the box as mirroring reference.
+
+2.10.2018
+
+- made slots persistent after edit
+- bug detected: in Excellon Editor if new tool added diameter is bigger than 10 it mess things up: SOLVED
+- Excellon Editor: finished Drill Resize action
+- after an object is deleted from the Project list, if the current tab in notebook is not Project,
+always focus in the Project Tab (deletion can be done by shortcut key also)
+- changed the initial view to include the possible enabled workspace guides
+
+1.10.2018
+
+- added GUI for Excellon Editor in the Tool Tab
+- Excellon Editor: created and populated the tool list
+- Excellon Editor: added possibility to add new tools in the list
+- Excellon Editor: added possibility to delete a tool (and the drills that it contain) by selecting a row in the tool table and 
+clicking the Delete Tool button
+- Excellon Editor: added possibility to change the tool diameter in the tool list for existing tool diameters.
+- Excellon Editor: when selecting a drill, it will highlight the tool in the Tool table
+- Excellon Editor: optimized single click selection
+- Excellon Editor: added selection for all drills with same diameter upon tool selection in tool table; fix in tool_edit
+- Excellon Editor: added constrain to selection by single click, it will select if within a certain area around the drill
+- Excellon Editor: finished Add Drill action
+- Excellon Editor: finished Move Drill action
+- Excellon Editor: finished Copy Drill action
+
+- fixed issue: when an object is selected before entering the Editor mode, now the selecting shape is deleted before entry 
+in the Editor (be it Geometry or Excellon).
+- fixed a few glitches regarding the units change
+- when an object is deselected on the Plot Area, the notebook will switch to Project Tab
+- changed the selection behavior for the dragging rectangle selection box in Editor (Geometry, Excellon): by dragging a
+selection box and selecting is cumulative: it just adds. To remove from selection press key Ctrl (or Shift depending of 
+the setting in the Preferences) and drag the rectangle across the objects you want to deselect.
+
+29.09.2018
+
+- optimized the combobox item population in Panelization Tool and in Film Tool
+- FlatCAM now remember the last path for saving files not only for opening
+- small fix in GUI
+- work on Excellon Editor. Excellon editor working functions are: loading an Excellon object into Editor, 
+saving an Excellon object from editor to FlatCAM, selecting drills by left click, selection of drills by dragging rectangle, deletion of drills.
+- fixed Excellon merge
+- added more Gcode details (depthperpass parameter in Gcode header) in postprocessors
+- deleted the Tool informations from header in postprocessors due to Mach3 not liking the lot of square brackets
+- more corrections in postprocessors
+
+
+28.09.2018
+
+- added a save_defaults() call on App exit from action on Menu -> File -> Exit
+- solved a small bug in Measurement Tool
+- disabled right mouse click functions when Measurement Tools is active so the user can do panning and find the destination point easily
+- added a new button named "Measure" in Measurement Tool that allow easy access to Measurement Tool from within the tool
+- fixed a bug in Gerber parser that when there was a rectangular aperture used within a region, some artifacts were generated.
+- some more work on Excellon Editor
+
+27.09.2018
+
+- fixed bug when creating a new project, if a previous object was selected on screen, the selection shape
+survived the creation of a new project
+- added compatibility with old type of FlatCAM projects
+- reverted modifications to the way that Excellon geometry was stored to the old way.
+- added exceptions for Paint functions so the user can know if something failed.
+- modified confirmation messages to use the color coded messages (error = red, success = green, warning = yellow)
+- restored activity icon
+
+26.09.2018
+
+- disabled selection of objects in Project Tab when in Editor
+- the Editor Toolbar is hidden in normal mode and it is showed when Editor
+is activated. I may change this behaviour back.
+- changed names in classes, functions to prepare for the Excellon editor
+
+- fixed bugs in Paint All function
+- fixed a bug in ParseSVG module in parse_svg_transform(), related to 'scale'
+
+- moved all the Editor menu/toolbar creation to FlatCAMUI where they belong
+- fixed a Gerber parse number issue when Gerber zeros are TZ (keep trailing zeros)
+
+- changed the way of how the solid_geometry for Excellon files is stored
+and plotted. Before everything was put in the same "container". Now,
+the geometries of drills and slots are organized into dictionaries having
+as keys the tool diameters and as values list of Shapely objects (polygons)
+- fix for Excellon plotting for newly created empty Excellon Object
+- fixed geometry.bounds() in camlib to work with the new format of the Excellon geometry (list of dicts)
+
+24.09.2018
+
+- added packages in the Requirements and setup_ubuntu.sh. Tested in Ubuntu and
+it's OK
+- added Replace (All) feature in the CNC Code Editor
+- made CNC Code generation for Excellon to show progress
+- added information about transforms in the object properties (like skew
+and how much, if it was mirrored and so on)
+- made all the transforms threaded and make them show progress in the progress bar
+- made FlatCAM project saving, threaded.
+ 
+23.09.2018
+
+- added support for "header-less" Excellon files. It seems that Mentor PADS does generate such
+non-standard Excellon files. The user will have to guess: units (IN/MM), type of zero suppression LZ/TZ 
+(leading zeros or trailing zeros are kept) and Excellon number format(digits and decimals). 
+All of those can be adjusted in Menu -> Edit -> Preferences -> Excellon Object -> Excellon format
+- fixed svgparse for Path. Now PCB rasted images can traced in Inkscape or PDF's can be converted
+and then saved as SVG files which can be imported into FlatCAM. This is a convolute way to convert a PDF
+to Gerber file.
+
+22.09.2018
+
+- added Drag & Drop capability. Now the user can drag and drop to FlatCAM GUI interface a file 
+(with the right extension) that can be a FlatCAM project file (.FlatPrj) a Gerber file, 
+an Excellon file, a G-Code file or a SVG file.
+- made the Move Tool command threaded
+- added Image import into FlatCAM
+
+21.09.2018
+
+- added new information's in the object properties: all used Tool-Table items
+are included in a new entry in self.options dictionary
+- modified the postprocessor files so they now include information's about
+how many drills (or slots) are for each tool. The Gcode will have this
+information displayed on the message from ToolChange.
+- removed some log.debug and add new log.debug especially for moments when some process is finished
+- fixed the utility geometry for Font geometry in Geometry Editor
+- work on selection in Geometry Editor
+- added multiple selection key as a Preference in Menu -> Edit -> Preferences
+It can be either Shift or Ctrl.
+- fixed bug in Gerber Object -> Copper Clearing.
+- added more comprehensive tooltips in Non-copper Clearing as advice on how to proceed.
+- adjusted make_win32.py file so it will work with Python 3.7 (cx_freeze can't copy OpenGL files, so
+it has to be done manually)
+
+19.09.2018
+
+- optimized loading FlatCAM project by double clicking on project file; there is no need to clean up everything by using 
+the function not Thread Safe: on_file_new() because there is nothing to clean since FlatCAM just started.
+
+- added a workspace delimitation with sizes A3, A4 and landscape or portrait format
+- The Workspace checkbox in Preferences GUI is doing toggle on the workspace
+- made the workspace app default state = False
+- made the workspace to resize when units are changed
+- disabled automatic defaults save (might create SSD wear)
+- added an automatic defaults save on FlatCAM application close
+- made the draw method for the Workspace lines 'agg' so the quality of the FC objects will not be affected
+
+- added Area constrain to the Panelization Tool: if the resulting area is too big to fit within constrains, the number
+of columns and/or rows will be reduced to the maximum that still fits is.
+- removed the Flip command from Panelization Tools because Flipping (Mirroring) should be done properly with the 
+Transform Tool or using the provided shortcut keys.
+
+- made Font parsing threaded so the application will not wait for the font parsing to complete therefore the app start
+is faster
+
+
+17.09.2018
+
+- fixed Measuring Tool not working when grid is turned OFF
+- fixed Roland MDX20 postprocessor
+- added a .GBR extension in the open_gerber filter
+- added ability to Scale and Offset (for all types of objects) to just
+press Enter after entering a value in the Entry just like in Tool Transform
+- added capability in Tool Transform to mirror(flip) around a certain Point.
+The point coordinates can either be entered by hand or they can be captured
+by left clicking while pressing key "SHIFT" and then clicking the Add button
+- added the .ROL extension when saving Machine Code
+- replaced strings that reference to G-Code from G-Code to CNC Code
+- added capability to open a project by serving the path/project_name.FlatPrj as a parameter
+to FlatCAM.py
+
+15.09.2018
+
+- removed dwell line generator and included dwell generation in the postprocessor files
+- added a proposed RML1 Roland_MDX20 postprocessor file.
+- added a limit of 15mm/sec (900mm/min) to the feedrate and to the feedrate_rapid. Anything faster than this
+will be capped to 900mm/min regardless what is entered in the program GUI. This is because Roland MDX-20 has
+a mechanical limit of the speed to 15mm/sec (900mm/min in GUI)
+
+14.09.2018
+- remade the Double Sided Tool so it now include mirroring of Excellon and Geometry Objects along Gerber.
+Made adding points easier by adding buttons to GUI that allow adding the coordinates captured by
+left mouse click + SHIFT key
+- added a few fixes in code to the other FlatCAM tools regarding reset_fields() function. The issue
+was present when clicking New Project entry in Menu -> File.
+- FIXED: fix adding/updating bounding box coords for the mirrored objects in Double side Tool.
+- FIXED: fix the bounding box values from within FlatCAM objects, upon units change.
+- fixed issue with running again the constructor of the drawing tools after the tool action was complete,
+in Geometry Editor
+- fixed issue with Tool tab not closed after Text Input tool is finished.
+- fixed issue with TEXT to GEOMETRY tool, the resulting geometry was not scaled depending of current units
+- fixed case when user is clicking on the canvas to place a Font Geometry without clicking apply button first
+or the Font Geometry is empty, in Geometry Editor - > Text Input tool
+- reworked Measuring Tool by adding more information's (START, STOP point coordinates) and remade the 
+strings
+- added to Double Sided Tool the ability to use as reference box Excellon and Geometry Objects
+
+12.09.2018
+
+- fixed Excellon Object class such that Excellon files that have both drills and slots are supported
+- remade the GUI interface for the Excellon Object in a more compact way; added a column with slots numbers
+(if any) along the drills numbers so now there is only one tool table for drills and slots.
+- remade the GUI in Preferences and removed unwanted stretch that was broken the layout.
+- if for a certain tool, the slots number is zero it will not be displayed
+- reworked Text to Geometry feature to work in Linux and MacOS
+- remade the Text to Geometry so font collection process is done once at app start-up improving the performance
+
+
+09.09.2018
+
+- added TEXT ENTRY SUPPORT in Geometry Editor. It will convert strings of True Type Fonts to geometry. 
+The actual dimensions are approximations because font size is in points and not in metric or inch units.
+For now full support is limited to Windows. In Linux/MacOS only the fonts for which the font name is the same 
+as the font filename are supported. Italic and Bold functions may not work in Linux/MacOS.
+- solved bug: some Drawing menu entries not having connected functions
+
+28.08.2018
+
+- fixed Gerber parser so now G01 "moving" rectangular 
+aperture is supported.
+- fixed import_svg function; it can import SVG as geometry (solved bug)
+- fixed import_svg function; it can import SVG as Gerber (it did not work previously)
+- added menu entry's for SVG import as Gerber and separated import as Geometry
+
+27.08.2018
+
+- fixed Gerber parser so now FlatCAM can load Gerber files generated by Mentor Graphics EDA programs.
+
+26.08.2018
+
+- added awareness for missing coordinates in Gerber parsing. It will try to use the previous coordinates but if there
+are not any those lines will be ignored and an Warning will be printed in Tcl Shell.
+- fixed TCL commands AlignDrillGrid and DrilCncJob
+- added TCL script file load_and_run support in GUI
+- made the tool_table in Excellon to automatically adjust the table height depending on the number of rows such that
+all the rows will be displayed.
+- structural changes in the Excellon build_ui()
+- icon changes and menu compress
+
+23.08.2018
+
+- added Excellon routing support
+- solved a small bug that crippled Excellon slot G85 support when the coordinates
+are with period.
+- changed the way selection is done in Geometry Editor; now it should work
+in all cases (although the method used may be computationally intensive,
+because sometimes you have to click twice to make selection if you do it too fast)
+
+21.08.2018
+
+- added Excellon slots support when using G85 command for generation of
+the slots file. Inspired from the work of @mgix. Thanks.
+Routing format support for slots will follow. 
+- minor bug solved: option "Cut over 1st pt" now has same name both in
+Preferences -> Geometry Options and in Selected tab -> Geomety Object.
+Solves #3
+- added option to select Climb or Conventional Milling in Gerber Object options
+Solves #4
+- made "Combine passes" option to be saved as an app preference
+- added Generate Exteriors Geo and Generate Interiors Geo buttons in the
+Gerber Object properties
+- added configuration for the number of steps used for Gerber circular aperture
+linear approximation. The option is in Preferences -> Gerber Options
+- added configuration for the number of steps used for Gcode circular aperture
+linear approximation. The option is in Preferences -> CNCjob Options
+- added configuration for the number of steps used for Geometry circular aperture
+linear approximation. The option is in Preferences -> Geometry Options. It is used 
+on circles/arcs made in Geometry Editor and for other types of geometries generated in 
+the app.
+
+
+17.07.2018
+
+- added the required packages in Requirements.txt file
+- added required packages in setup_ubuntu.sh file
+- added color control over almost all the colors in the application; those
+settings are in Menu -> Edit -> Preferences -> General Tab
+- added configuration of which mouse button to be used when panning (MMB or RMB)
+- fixed bug with missing 'drillz' parameter in function generate_from_excellon_by_tool()
+(credits for finding it goes to Stefan Smith https://bitbucket.org/stefan064/)
+- load Factory defaults in Preferences will load the defaults that are used just after
+first install. Load Defaults option in Preferences will load the User saved Defaults.
+
+03.07.2018
+
+- fixed bug in rotate function that didn't update the bounding box of the
+modified object (rotated) due of not emitting the right signal parameter.
+- removed the Options tab from the Notebook (the left area where is located
+also the Project tab). Replaced it with the Preferences Tab launched with
+Menu -> Edit -> Preferences
+- when FlatCAM is used under MacOS, multiple selection of shapes in Editor
+mode is done using SHIFT key instead of CTRL key due of MacOS interpreting
+CTRL+LMB_click as a RMB click
+- when in Editor, clicking not on a shape, reset the index of selected shapes
+to zero
+- added a new Tab in the Plot Area named Gcode Editor. It allow the user to
+edit the Gcode and then Save it or Print it.
+- added a fix so the 'preamble' Gcode is correctly inserted between the
+comments header and the actual GCODE
+- added Find function in G-Code Editor
+
+
+27.06.2018
+
+- the Plot Area tab is changing name to "Editor Area" when the Editor is
+activated and returns to the "Plot Area" name upon exiting the Editor
+- made the labels shorter in Transform Tool in anticipation of
+Options Tab removal from Notebook and replacing it with Preferences
+- the Excellon Editor is not finished (not even started yet) so the
+Plot Area title should stay "Plot Area" not change to "Editor Area" when
+attempting to edit an Excellon file. Solved.
+- added a header comment block in the generated Gcode with useful
+information's
+- fixed issue that did not allow the Nightly's to be run in
+Windows 7 x64. The reason was an outdated DLL file (freetype.dll) used
+by Vispy python module.
+
+
+25.06.2018
+
+- "New" menu entry in Menu -> File is renamed to "New Project"
+- on "New Project" action, all the Tools are reinitialized so the Tools
+tab will work as expected
+- fixed issue in Film Tool when generating black film
+- fixed Measurement Tool acquiring and releasing the mouse/key events
+- fixed cursor shape is updated on grid_toggle
+- added some infobar messages to show the user when the Editor was
+activated and when it was closed (control returned to App).
+- added thread usage for Film tool; now the App is no longer blocked on
+film generation and there is a visual clue that the App is working
+
+22.06.2018
+
+- added export PNG image functionality and menu entry in
+Menu -> File -> Export PNG ...
+- added a command to set focus on canvas inside the mouve move event
+handler; once the mouse is moved the focus is moved to canvas so the
+shortcuts work immediatly.
+- solved a small bug when using the 'C' key to copy name of the selected
+object to clipboard
+
+- fixed millholes() function and isolate() so now it works even when the
+tool diameter is the same as the hole diameter.
+
+Actually if the passed value to  the buffer() function is zero, I
+artificially add a value of 0.0000001 (FlatCAM has a precision of
+6 decimals so I use a tenth of that value as a pseudo "zero")
+because the value has to be positive. This may have solved for some use
+cases the user complaints that on clearing the areas of copper there is
+still copper leftovers.
+
+- added shortcut "SHIFT+G" to toggle the axis presence. Useful when one
+wants to save a PNG file.
+- changed color of the grid from 'gray' to 'dimgray'
+
+- the selection shape is deleted when the object is deleted
+
+- the plot area is now in a TAB.
+- solved bug that allowed middle button click to create selection
+- fixed issue with main window geometry restore (hopefully).
+- made view toolbar to be hidden by default as it is not really needed
+(we have the functions in menu, zoom is done with mouse wheel, and there
+is also the canvas context menu that holds the functionality)
+- remade the GUIElements.FCInput() and made a GUIElements.FCTab()
+- on visibility plot toogle the selection shape is deleted
+
+- made sure that on panning in Geometry editor, the context menu is not
+displayed
+- disabled App shortcut keys on entry in Geometry Editor so only the
+local shortcut keys are working
+
+- deleted metric units in canvas context menu
+- added protection so object deletion can't be done until Geometry
+Editor session is finished. Solved bug when the shapes on Geometry
+Editor were not transfered to the New_geometry object yet and the
+New_Geometry object is deleted. In this case the drawn shapes are left
+in a intermediary state on canvas.
+
+- added selection shape drawing in Geometry Editor preserving the
+current behavior: click to select, click on canvas clear selection,
+CTRL+click add to selection new shape but remove from selection
+if already selected. Drag LMB from left to right select enclosed
+shapes, drag LMB from right to left select touching shapes. Now the
+selection is made based on
+- added info message to be displayed in infobar, when a object is
+renamed
+
+20.06.2018
+
+- there are two types of mouse drag selection (rectangle selection)
+If there is a rectangle selection from left to right, the color of the
+selection rectangle is blue and the selection is "enclosing" - this
+means that the object to be selected has to be enclosed by the selecting
+blue rectangle shape.
+If there is a rectangle selection fro right to left, the color of the
+selection rectangle is green and the selection is "touching" - this
+means that it's enough to touch with the selecting green rectangle the
+object(s) to be selected so they become selected
+- changed the modifier key required to be pressed when LMB is ckicked
+over canvas in order to copy to clipboard the coordinates of the click,
+from CTRL to SHIFT. CTRL will be used for multiple selection.
+- change the entry names in the canvas context menu
+- disconnected the app mouse event functions while in geometry editor
+since the geometry editor has it's own mouse event functions and there
+was interference between object and geometry items. Exception for the
+mouse release event so the canvas context menu still work.
+- solved a bug that did not update the obj.options after a geometry
+object was edited in geometry editor
+- solved a bug in the signal that saved the position and dimensions of
+the application window.
+- solved a bug in app.on_preferences() that created an error when run
+in Linux
+
+18.06.2018 Update 1
+
+- reverted the 'units' parameter change to 'global_units' due of a bug
+that did not allow saving of the project
+- modified the camlib transform (rotate, mirror, scale etc) functions
+so now they work with Gerber file loaded with 'follow' parameter
+
+18.06.2018
+
+- reworked the Properties context menu option to a Tool that displays
+more informations on the selected object(s)
+- remade the FlatCAM project extension as .FlatPrj
+- rearranged the toolbar menu entries to a more properly order
+- objects can now be selected on canvas, a blue polygon is drawn around
+when selected
+- reworked the Tool Move so it will work with the new canvas selection
+- reworked the Measurement Tool so it will work with the new canvas
+selection
+- canvas selection can now be done by dragging left mouse boutton and
+creating a selection box over the objects
+- when the objects are overlapped on canvas, the mouse click
+selection works in a circular way, selecting the first, then the second,
+then ..., then the last and then again the first and so on.
+- double click on a object on canvas will open the Selected Tab
+- each object store the bounding box coordinates in the options dict
+- the bbox coordinates are updated on the obj options when the object
+is modified by a transform function (rotate, scale etc)
+
+
+15.06.2018
+
+- the selection marker when moving is now a semitransparent Polygon
+with a blue border
+- rectified a small typo in the ToolTip for Excellon Format for
+Diptrace excellon format; from 4:2 to 5:2
+- corrected an error that cause no Gcode could be saved
+
+
+14.06.2018
+
+- more work on the contextual menu
+- added Draw context menu
+- added a new tool that bring together all the transformations, named
+Transformation Tool (Rotate, Skew, Scale, Offset, Flip)
+- added shorcut key 'Q' which toggle the units between IN and MM
+- remade the Move tool, there is now a selection box to show where the
+move is done
+- remade the Measurement tool, there is now a line between the start
+point of measurement and the end point of the measurement.
+- renamed most of the system variables that have a global app effect to
+global_name where name is the parameter (variable)
+
+
+9.06.2018
+
+- reverted to PyQt4. PyQt5 require too much software rewrite
+- added calculators: units_calculator and V-shape Tool calculator
+- solved bug in Join Excellon
+- added right click menu over canvas
+
+6.06.2018 Update
+
+- fixed bug: G-Code could not be saved
+- fixed bug: double clicking a category in Project Tab made the app to
+crash
+- remade the bounds() function to work with nested lists of objects as
+per advice from JP which made the operation less performance taxing.
+- added shortcut Shift+R that is complement to 'R'
+- shorcuts 'R' and 'SHIFT+R' are working now in steps of 90 degrees
+instead of previous 45 degrees.
+- added filters in the open ... FlatCAM projects are saved automatically
+as *.flat, the Gerber files have few categories. So the Excellons and
+G-Code and SVG.
+
+6.06.2018
+
+- remade the transform functions (rotate, flip, skew) so they are now
+working for joined objects, too
+- modified the Skew and Rotate comamands: if they are applied over a
+selection of objects than the origin point will be the center of the
+biggest bounding box. That allow for perfect sync between the selected
+objects
+- started to modify the program so the exceptions are handled correctly
+- solved bug where a crash occur when ObjCollection.setData didn't
+return a bool value
+- work in progress for handling situations when a different file is
+loaded as another (like loading a Gerber file using Open Excellon
+ commands.
+- added filters on open_gerber and open_excellon Dialogs. There is still
+the ability to select All Files but this should reduce the cases when
+the user is trying to oprn a file from a wrong place.
+
+4.06.2018
+
+- finished PyQt4 to PyQt4 port on the Vispy variant (there were some changes
+compared with the Matplotlib version for which the port was finished
+some time ago)
+- added Ctrl+S shortcut for the Geometry Editor. When is activated it will
+save de geometry ("update") and return to the main App.
+- modified the Mirror command for the case when multiple objects are
+selected and we want to mirror all together. In this case they should mirror
+around a bounding box to fill all.
+
+3.06.2018
+
+- removed the current drill path optimizations as they are inefficient
+- implemented Google OR-tools drill path optimization in 2 flavors;
+Basic OR-tools TSP algorithm and OR-Tools Metaheuristics Guided Local Path
+- Move tool is moved to Menu -> Edit under the name Move Object
+
+- solved some internal bugs (info command was creating an non-fatal
+error in PyQt, regarding using QPixMaps outside GUI thread
+- reworked camlib number parsing (still had some bugs)
+- working in porting the application from usage of PyQt4 to PyQt4
+- added TclCommands save_sys and list_sys. save_sys is saving all the
+system default parameters and list_sys is listing them by the first
+letters. listsys with no arguments will list all the system parameters.
+
+29.05.2018
+
+- modified the labels for the X,Y and Dx,Dy coordinates
+- modified the menu entries, added more icons
+- added initial work on a Excellon Editor
+- modified the behavior of when clicking on canvas the coordinates were
+copied to cliboard: now it is required to press CTRL key for this to
+happen, and it will only happen just for left mouse button click
+- removed the autocopy of the object name on new object creation
+- remade the Tcl commands drillcncjob and cncjob
+- added fix so the canvas is focused on the start of the program,
+therefore the shortcuts work without the need for doing first a click
+on canvas.
+
+
+
+28.05.2018
+
+- added total drill count column in Excellon Tool Table which displays the
+total number of drills
+- added aliases in panelize Tool (pan and panel should work)
+- modified generate_milling method which had issues from the Python3 port
+(it could not sort the tools due of dict to dict comparison no longer
+possible).
+- modified the 'default' postprocessor in order to include a space
+between the value of Xcoord and the following Y
+- made optional the using of threads for the milling command; by default
+it is OFF (False) because in the current configuration it creates issues
+when it is using threads
+- modified the Panelize function and Tcl command Panelize. It was having
+issues due to multithreading (kept trying to modify a dictionary in
+redraw() method)and automatically selecting the last created object
+(feature introduced by me). I've added a parameter to
+the new_object method, named autoselected (by default it is True) and
+in the panelize method I initialized it with False.
+By initializing the plot parameter with False for the temporary objects,
+I have increased dramatically the  generation speed of the panel because
+now the temporary object are no longer ploted which consumed time.
+- replaced log.warn() with log.warning() in camlib.py. Reason: deprecated
+- fixed the issue that the "Defaults" button was having no effect when
+clicked and Options Combo was in Project Options
+- fixed issue with Tcl Shell loosing focus after each command, therefore
+needing to click in the edit line before we type a new command (borrowed
+from @brainstorm
+- added a header in the postprocessor files mentioning that the GCODE
+files were generated by FlatCAM.
+- modified the number of decimals in some of the line entries to 4.
+- added an alias for the millholes Tcl Command: 'mill'
+
+27.04.2018
+
+- modified the Gerber.scale() function from camlib.py in order to
+allow loading Gerber files with 'follow' parameter in other units
+than the current ones
+- snap_max_entry is disabled when the DRAW toolbar is disabled (previous
+fix didn't work)
+- added drill count column in Excellon Tool Table which displays the
+total number of drills for each tool
+
+- added a new menu entry in Menu -> EDIT named "Join Excellon". It will
+merge a selection of Excellon files into a new Excellon file
+- added menu stubs for other Excellon based actions
+
+- solved bug that was not possible to generate film from joined geometry
+- improved toggle active/inactive of the object through SPACE key. Now
+the command works not only for one object but also for a selection
+
+26.05.2018
+
+- made conversion to Python3
+- added Rtree Indexing drill path optimization
+- added a checkbox in Options Tab -> App Defaults -> Excellon
+Group named Excellon Optim. Type from which it can be selected
+the default optimization type: TS stands for Travelling
+Salesman algorithm and Rtree stands for Rtree Indexing
+- added a checkbox on the Grid Toolbar that when checked
+(default status is checked) whatever value entered in the GridX entry
+will be used instead of the now disabled GridY entry
+- modified the default behavior on when a line_entry is clicked.
+Now, on each click on a line_entry, the content is automatically
+selected.
+- snap_max_entry is disabled when the DRAW toolbar is disabled
+
+24.05.2015
+
+- in Geometry Editor added a initial form of Rotate Geometry command in
+toolbar
+- changed the way the geometry is finished if it requires a key: before
+it was using key 'Space' now it uses 'Enter'
+- added Shortcut for Rotate Geometry to key 'Space'
+- after using a tool in Geometry Editor it automatically defaults to
+'Select Tool'
+
+23.05.2018
+
+Added key shortcut's in FlatCAMApp and in Geometry Editor.
+
+FlatCAMApp shortcut list:
+1      Zoom Fit
+2      Zoom Out
+3      Zoom In
+C      Copy Obj_Name
+E      Edit Geometry (if selected)
+G      Grid On/Off
+M      Move Obj
+
+N      New Geometry
+R      Rotate
+S      Shell Toggle
+V      View Fit
+X      Flip on X_axis
+Y      Flip on Y_axis
+~      Show Shortcut List
+
+Space:   En(Dis)able Obj Plot
+CTRL+A   Select All
+CTRL+C   Copy Obj
+CTRL+E   Open Excellon File
+CTRL+G   Open Gerber File
+CTRL+M   Measurement Tool
+CTRL+O   Open Project
+CTRL+S   Save Project As
+Delete   Delete Obj'''
+
+
+Geometry Editor Key shortcut list:
+A       Add an 'Arc'
+C       Copy Geo Item
+G       Grid Snap On/Off
+K       Corner Snap On/Off
+M       Move Geo Item
+
+N       Add an 'Polygon'
+O       Add a 'Circle'
+P       Add a 'Path'
+R       Add an 'Rectangle'
+S       Select Tool Active
+
+
+~        Show Shortcut List
+Space:   Rotate Geometry
+Enter:   Finish Current Action
+Escape:  Abort Current Action
+Delete:  Delete Obj
+
+22.05.2018
+
+- Added Marlin postprocessor
+- Added a new entry into the Geometry and Excellon Object's UI:
+Feedrate rapid: the purpose is to set a feedrate for the G0
+command that some firmwares like Marlin don't intepret as
+'move with highest speed'
+- FlatCAM was not making the conversion from one type of units to
+another for a lot of parameters. Corrected that.
+- Modified the Marlin Postprocessor so it will generate the required
+GCODE.
+
+21.05.2018
+
+- added new icons for menu entries
+- added shortcuts that work on the Project tab but also over
+Plot. Shorcut list is accesed with shortcut key '~' sau '`'
+- small GUI modification: on each "New File" command it will switch to
+the Project Tab regardless on which tab we were.
+
+- removed the global shear entries and checkbox as they can be
+damaging and it will build effect upon effect, which is not good
+- solved bug in that the Edit -> Shear on X (Y)axis could adjust
+only in integers. Now the angle can be adjusted in float with
+3 decimals.
+- changed the tile of QInputDialog to a more general one
+- changed the "follow" Tcl command to the new format
+- added a new entry in the Menu -> File, to open a Gerber with
+the follow parameter = True
+- added a new checkbox in the Gerber Object Selection Tab that
+when checked it will create a "follow" geometry
+- added a few lines in Mill Holes Tcl command to check if there are
+promises and raise an Tcl error if there are any.
+- started to modify the Export_Svg Tcl command
+
+20.05.2018
+
+- changed the interpretation of the axis for the rotate and skew commands.
+Actually I reversed them to reflect reality.
+- for the rotate command a positive angle now rotates CW. It was reversed.
+- added shortcuts (for outside CANVAS; the CANVAS has it's own set of shortcuts)
+CTRL+C will copy to clipboard the name of the selected object
+CTRL+A will Select All objects
+
+"X" key will flip the selected objects on X axis
+
+"Y" key will flip the selected objects on Y axis
+
+"R" key will rotate CW with a 45 degrees step
+- changed the layout for the top of th Options page. Added a checkbox and entries
+for parameters for skew command. When the checkbox is checked it will save (and
+load at the next startup of the program) the option that at each CNCJob generation
+(be it from Excellon or Geometry) it will perform the Skew command with the 
+parametrs set in the nearby field boxes (Skew X and Skey Y angles).
+It is useful in case the CNC router is not perfectly alligned between the X and Y axis
+
+- added some protection in case the skew command receive a None parameter
+
+- BUG solved: made an UGLY (really UGLY) HACK so now, when there is a panel geometry
+generated from GUI, the project WILL save. I had to create a copy of the generated 
+panel geometry and delete the original panel geometry. This way there is no complain
+from JSON module about circular reference.
+
+Supplimentary:
+- removed the Save buttons previously added on each Group in Application Defaults.
+Replaced them with a single Save button that stays always on top of the Options TAB
+- added settings for defaults for the Grid that are persistent
+- changed the default view at FlatCAM startup: now the origin is in the center of the screen
+
+
+19.05.2018
+
+- last object that is opened (created) is always automatically selected and
+the name of the object is automatically copied to clipboard; useful when
+using the TCL command :)
+
+- added new commands in MENU -> EDIT named: "Copy Object" and
+"Copy Obj as Geom". The first command will duplicate any object (Geometry,
+Gerber, Excellon).
+The second command will duplicate the object as a geometry. For example,
+holes in Excello now are just circles that can be "painted" if one wants it.
+
+- added new Tool named ToolFreeformCutout. It does what it says, it will
+make a board cutout from a "any shape" Gerber or Geometry file
+
+- solved bug in the TCL command "drillcncjob" that always used the endz
+parameter value as the toolchangez parameter value and for the endz value
+used a default value = 1
+
+- added postprocessor name into the TCL command "drillcncjob" parameters
+
+- when adding a new geometry the default name is now: "New_Geometry" instead
+of "New Geometry". TCL commands don't handle the spaces inside the name and
+require adding quotes.
+
+- solved bug in "cncjob" TCL command in which it used multidepth parameter as
+always True regardless of the argument provided
+
+- added a checkbox for Multidepth in the Options Tab -> Application Defaults
+
+
+18.05.2018
+
+- added an "Defaults" button in Excellon Defaults Group; it loads the
+following configuration (Excellon_format_in 2:4, Excellon_format_mm 3:3,
+Excellon_zeros LZ)
+- added Save buttons for each Defaults Group; in the future more 
+parameters will be propagated in the app, for now they are a few
+- added functions for Skew on X axis and for Skew on Y menu stubs.
+Now, clicking on those Menu -> Options -> Transform Object menu entries
+will trigger those functions
+- added a CheckBox button in the Options Tab -> Application Defaults that control
+the behaviour of the TCL shell: checking it will make the TCL shell window visible
+at each start-up, unchecking it the TCL shell window will be hidden until needed
+- Depth/pass parameter from Geometry Object CNC Job is now in the
+defaults and it will keep it's value until changed in the Application
+Defaults.
+
+17.05.2018
+
+- added messages box for the Flip commands to show error in case there
+is no object selected when the command is executed
+- added field entries in the Options TAB - > Application Defaults for the
+following newly introduced parameters: 
+excellon_format_upper_in
+excellon_format_lower_in
+excellon_format_upper_mm
+excellon_format_lower_mm
+
+The ones with upper indicate how many digits are allocated for the units
+and the ones with lower indicate how many digits from coordinates are 
+alocated for the decimals.
+
+[  Eg: Excellon format 2:4 in INCH
+   excellon_format_upper_in = 2
+   excellon_format_lower_in = 4
+where the first 2 digits are for units and the last 4 digits are
+decimals so from a number like 235589 we will get a coordinate 23.5589
+]
+
+- added Radio button in the Options TAB - > Application Defaults for the
+Excellon_zeros parameter
+
+After each change of those parameters the user will have to press 
+"Save defaults" from File menu in order to propagate the new values, or
+wait for the autosave to kick in (each 20sec).
+
+Those parameters can be set in the set_sys TCL command.
+
+15.05.2018
+- modified SetSys TCL command: now it can change units
+- modified SetSys TCL command: now it can set new parameters:
+excellon_format_mm and excellon_format_in. the first one is when the
+excellon units are MM and the second is for when the excellon units are
+in INCH. Those parameters can be set with a number between 1 and 5 and it
+signify how many digits are before coma.
+- added new GUI command in EDIT -> Select All. It will select all
+objects on the first mouse click and on the second will deselect all
+(toggle action)
+- added new GUI commands in Options -> Transform object. Added Rotate selection,
+Flip on X axis of the selection and Flip on Y axis of the selection
+For the Rotate selection command, negative numbers means rotation CCW and
+positive numbers means rotation CW.
+
+- cleaned up a bit the module imports
+- worked on the excellon parsing for the case of trailing zeros.
+If there are more than 6digits in the 
+coordinates, in case that there is no period, now the software will 
+identify the issue and attempt to correct it by dividing the coordinate 
+further by 10 for each additional digit over 6. If the number of digits
+is less than 6 then the software will multiply by 10 the coordinates
+
+14.05.2018
+
+- fixed bug in Geometry CNCJob generation that prevented generating 
+the object
+- added GRBL 1.1 postprocessor and Laser postprocessor (adapted from 
+the work of MARCO A QUEZADA)
+
+
+13.05.2018
+
+- added postprocessing in correct form
+- added the possibility to select an postprocessor for Excellon Object
+- added a new postprocessor, manual_toolchange.py. It allows to change 
+the tools and adjust the drill tip to touch the surface manually, always
+in the X=0, Y=0, Z = toolchangeZ coordinates.
+- fixed drillcncjob TCL command by adding toolchangeZ parameter
+- fixed the posprocessor file template 'default.py' in the toolchange
+command section
+- after I created a feature that the message in infobar is cleared by
+moving mouse on canvas, it generated a bug in TCL shell: everytime 
+mouse was moved it will add a space into the TCL read only section.
+Now this bug is fixed.
+- added an EndZ parameter for the drillcncjob and cncjob TCL commands: it
+will specify at what Z value to park the CNC when job ends
+- the spindle will be turned on after the toolchange and it will be turned off
+just before the final end move.
+
+Previously:
+- added GRID based working of FLATCAM
+- added Set Origin command
+- added FilmTool, PanelizeTool GUI, MoveTool
+- and others
+
+
+24.04.2018
+
+- Remade the Measurement Tool: it now ask for the Start point of the measurement and then for the Stop point. After it will display the measurement until we left click again on the canvas and so on. Previously you clicked the start point and reset the X and Y coords displayed and then you moved the mouse pointer wherever you wanted to measure, but moving the mouse from there you lost the measurement.
+- Added Relative measurement on the main plot
+- Now both the measuring tool and the relative measurement will work only with the left click of the mouse button because middle mouse click and right mouse click are used for panning
+- Renamed the tools files starting with Tool so they are grouped (in the future they may have their own folder like for TCL Commands)
+
+- Commented some shortcut keys and functions for features that are not present anymore or they are planned to be in the future but unfinished (like buffer tool, paint tool)
+- minor corrections regarding PEP8 (Pycharm complains about the m)
+- solved bug in TclCommandsSetSys.py Everytime that the command was executed it complain about the parameter not being in the list (something like this). There was a missing “else:”
+- when using the command “set_sys excellon_zeros” with parameter in lower case (either ‘l’ or ‘t’) now it is always written in the defaults file as capital letter
+
+- solved a bug introduced by me: when apertures macros were detected in Excellon file, FlatCam will complain about missing dictionary key “size”. Now it first check if the aperture is a macro and perform the check for zero value only for apertures with “size” key
+- solved a bug that didn't allowed FC to detect if Excellon file has leading zeros or trailing zeros
+- solved a bug that FC was searching for char ‘%’ that signal end of Excellon header even in commented lines (latest versions of Eagle end the commented line with a ‘%’)
+
+
+============================================
+
+This fork features:
+
+- Added buttons in the menu bar for opening of Gerber and Excellon files;
+- Reduced number of decimals for drill bits to two decimals;
+- Updated make_win32.py so it will work with cx_freeze 5.0.1 
+- Added capability so FlatCAM can now read Gerber files with traces having zero value (aperture size is zero);
+- Added Paint All / Seed based Paint functions from the JP's FlatCAM;
+- Added Excellon move optimization (travelling salesman algorithm) cherry-picked from David Kahler: https://bitbucket.org/dakahler/flatcam
+- Updated make_win32.py so it will work with cx_freeze 5.0.1 Corrected small typo in DblSidedTool.py
+- Added the TCL commands in the new format. Picked from FLATCAM master.
+- Hack to fix the issue with geometry not being updated after a TCL command was executed. Now after each TCL command the plot_all() function is executed and the canvas is refreshed.
+- Added GUI for panelization TCL command
+- Added GUI tool for the panelization TCL command: Changed some ToolTips.
+
+
+============================================
+
+Previously added features by Dennis
+
+- "Clear non-copper" feature, supporting multi-tool work.
+- Groups in Project view.
+- Pan view by dragging in visualizer window with pressed MMB.
+- OpenGL-based visualizer.
+

+ 167 - 0
VisPyCanvas.py

@@ -0,0 +1,167 @@
+import numpy as np
+from PyQt5.QtGui import QPalette
+import vispy.scene as scene
+from vispy.scene.cameras.base_camera import BaseCamera
+from vispy.color import Color
+import time
+
+white = Color("#ffffff" )
+black = Color("#000000")
+
+
+class VisPyCanvas(scene.SceneCanvas):
+
+    def __init__(self, config=None):
+        scene.SceneCanvas.__init__(self, keys=None, config=config)
+
+        self.unfreeze()
+
+        back_color = str(QPalette().color(QPalette.Window).name())
+
+        self.central_widget.bgcolor = back_color
+        self.central_widget.border_color = back_color
+
+        self.grid_widget = self.central_widget.add_grid(margin=10)
+        self.grid_widget.spacing = 0
+
+        top_padding = self.grid_widget.add_widget(row=0, col=0, col_span=2)
+        top_padding.height_max = 0
+
+        self.yaxis = scene.AxisWidget(orientation='left', axis_color='black', text_color='black', font_size=8)
+        self.yaxis.width_max = 55
+        self.grid_widget.add_widget(self.yaxis, row=1, col=0)
+
+        self.xaxis = scene.AxisWidget(orientation='bottom', axis_color='black', text_color='black', font_size=8)
+        self.xaxis.height_max = 25
+        self.grid_widget.add_widget(self.xaxis, row=2, col=1)
+
+        right_padding = self.grid_widget.add_widget(row=0, col=2, row_span=2)
+        # right_padding.width_max = 24
+        right_padding.width_max = 0
+
+        view = self.grid_widget.add_view(row=1, col=1, border_color='black', bgcolor='white')
+        view.camera = Camera(aspect=1, rect=(-100,-100,500,500))
+
+        # Following function was removed from 'prepare_draw()' of 'Grid' class by patch,
+        # it is necessary to call manually
+        self.grid_widget._update_child_widget_dim()
+
+        self.xaxis.link_view(view)
+        self.yaxis.link_view(view)
+
+        grid1 = scene.GridLines(parent=view.scene, color='dimgray')
+        grid1.set_gl_state(depth_test=False)
+
+        self.view = view
+        self.grid = grid1
+
+        self.freeze()
+
+        # self.measure_fps()
+
+    def translate_coords(self, pos):
+        tr = self.grid.get_transform('canvas', 'visual')
+        return tr.map(pos)
+
+    def translate_coords_2(self, pos):
+        tr = self.grid.get_transform('visual', 'document')
+        return tr.map(pos)
+
+
+class Camera(scene.PanZoomCamera):
+
+    def __init__(self, **kwargs):
+        super(Camera, self).__init__(**kwargs)
+
+        self.minimum_scene_size = 0.01
+        self.maximum_scene_size = 10000
+
+        self.last_event = None
+        self.last_time = 0
+
+        # Default mouse button for panning is RMB
+        self.pan_button_setting = "2"
+
+    def zoom(self, factor, center=None):
+        center = center if (center is not None) else self.center
+        super(Camera, self).zoom(factor, center)
+
+    def viewbox_mouse_event(self, event):
+        """
+        The SubScene received a mouse event; update transform
+        accordingly.
+
+        Parameters
+        ----------
+        event : instance of Event
+            The event.
+        """
+        if event.handled or not self.interactive:
+            return
+
+        # Limit mouse move events
+        last_event = event.last_event
+        t = time.time()
+        if t - self.last_time > 0.015:
+            self.last_time = t
+            if self.last_event:
+                last_event = self.last_event
+                self.last_event = None
+        else:
+            if not self.last_event:
+                self.last_event = last_event
+            event.handled = True
+            return
+
+        # Scrolling
+        BaseCamera.viewbox_mouse_event(self, event)
+
+        if event.type == 'mouse_wheel':
+            center = self._scene_transform.imap(event.pos)
+            scale = (1 + self.zoom_factor) ** (-event.delta[1] * 30)
+            self.limited_zoom(scale, center)
+            event.handled = True
+
+        elif event.type == 'mouse_move':
+            if event.press_event is None:
+                return
+
+            modifiers = event.mouse_event.modifiers
+
+            # self.pan_button_setting is actually self.FlatCAM.APP.defaults['global_pan_button']
+            if event.button == int(self.pan_button_setting) and not modifiers:
+                # Translate
+                p1 = np.array(last_event.pos)[:2]
+                p2 = np.array(event.pos)[:2]
+                p1s = self._transform.imap(p1)
+                p2s = self._transform.imap(p2)
+                self.pan(p1s-p2s)
+                event.handled = True
+            elif event.button in [2, 3] and 'Shift' in modifiers:
+                # Zoom
+                p1c = np.array(last_event.pos)[:2]
+                p2c = np.array(event.pos)[:2]
+                scale = ((1 + self.zoom_factor) **
+                         ((p1c-p2c) * np.array([1, -1])))
+                center = self._transform.imap(event.press_event.pos[:2])
+                self.limited_zoom(scale, center)
+                event.handled = True
+            else:
+                event.handled = False
+        elif event.type == 'mouse_press':
+            # accept the event if it is button 1 or 2.
+            # This is required in order to receive future events
+            event.handled = event.button in [1, 2, 3]
+        else:
+            event.handled = False
+
+    def limited_zoom(self, scale, center):
+
+        try:
+            zoom_in = scale[1] < 1
+        except IndexError:
+            zoom_in = scale < 1
+
+        if (not zoom_in and self.rect.width < self.maximum_scene_size) \
+                or (zoom_in and self.rect.width > self.minimum_scene_size):
+            self.zoom(scale, center)

+ 126 - 0
VisPyPatches.py

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

+ 90 - 0
VisPyTesselators.py

@@ -0,0 +1,90 @@
+from OpenGL import GLU
+
+
+class GLUTess:
+    def __init__(self):
+        """
+        OpenGL GLU triangulation class
+        """
+        self.tris = []
+        self.pts = []
+        self.vertex_index = 0
+
+    def _on_begin_primitive(self, type):
+        pass
+
+    def _on_new_vertex(self, vertex):
+        self.tris.append(vertex)
+
+    # Force GLU to return separate triangles (GLU_TRIANGLES)
+    def _on_edge_flag(self, flag):
+        pass
+
+    def _on_combine(self, coords, data, weight):
+        return (coords[0], coords[1], coords[2])
+
+    def _on_error(self, errno):
+        print("GLUTess error:", errno)
+
+    def _on_end_primitive(self):
+        pass
+
+    def triangulate(self, polygon):
+        """
+        Triangulates polygon
+        :param polygon: shapely.geometry.polygon
+            Polygon to tessellate
+        :return: list, list
+            Array of triangle vertex indices [t0i0, t0i1, t0i2, t1i0, t1i1, ... ]
+            Array of polygon points [(x0, y0), (x1, y1), ... ]
+        """
+        # Create tessellation object
+        tess = GLU.gluNewTess()
+
+        # Setup callbacks
+        GLU.gluTessCallback(tess, GLU.GLU_TESS_BEGIN, self._on_begin_primitive)
+        GLU.gluTessCallback(tess, GLU.GLU_TESS_VERTEX, self._on_new_vertex)
+        GLU.gluTessCallback(tess, GLU.GLU_TESS_EDGE_FLAG, self._on_edge_flag)
+        GLU.gluTessCallback(tess, GLU.GLU_TESS_COMBINE, self._on_combine)
+        GLU.gluTessCallback(tess, GLU.GLU_TESS_ERROR, self._on_error)
+        GLU.gluTessCallback(tess, GLU.GLU_TESS_END, self._on_end_primitive)
+
+        # Reset data
+        del self.tris[:]
+        del self.pts[:]
+        self.vertex_index = 0
+
+        # Define polygon
+        GLU.gluTessBeginPolygon(tess, None)
+
+        def define_contour(contour):
+            vertices = list(contour.coords)             # Get vertices coordinates
+            if vertices[0] == vertices[-1]:             # Open ring
+                vertices = vertices[:-1]
+
+            self.pts += vertices
+
+            GLU.gluTessBeginContour(tess)               # Start contour
+
+            # Set vertices
+            for vertex in vertices:
+                point = (vertex[0], vertex[1], 0)
+                GLU.gluTessVertex(tess, point, self.vertex_index)
+                self.vertex_index += 1
+
+            GLU.gluTessEndContour(tess)                 # End contour
+
+        # Polygon exterior
+        define_contour(polygon.exterior)
+
+        # Interiors
+        for interior in polygon.interiors:
+            define_contour(interior)
+
+        # Start tessellation
+        GLU.gluTessEndPolygon(tess)
+
+        # Free resources
+        GLU.gluDeleteTess(tess)
+
+        return self.tris, self.pts

+ 596 - 0
VisPyVisuals.py

@@ -0,0 +1,596 @@
+from vispy.visuals import CompoundVisual, LineVisual, MeshVisual, TextVisual, MarkersVisual
+from vispy.scene.visuals import VisualNode, generate_docstring, visuals
+from vispy.gloo import set_state
+from vispy.color import Color
+from shapely.geometry import Polygon, LineString, LinearRing
+import threading
+import numpy as np
+from VisPyTesselators import GLUTess
+
+
+class FlatCAMLineVisual(LineVisual):
+    def __init__(self, pos=None, color=(0.5, 0.5, 0.5, 1), width=1, connect='strip',
+                            method='gl', antialias=False):
+        LineVisual.__init__(self, pos=None, color=(0.5, 0.5, 0.5, 1), width=1, connect='strip',
+                            method='gl', antialias=True)
+
+    def clear_data(self):
+        self._bounds = None
+        self._pos = None
+        self._changed['pos'] = True
+        self.update()
+
+
+def _update_shape_buffers(data, triangulation='glu'):
+    """
+    Translates Shapely geometry to internal buffers for speedup redraws
+    :param data: dict
+        Input shape data
+    :param triangulation: str
+        Triangulation engine
+    """
+    mesh_vertices = []                                              # Vertices for mesh
+    mesh_tris = []                                                  # Faces for mesh
+    mesh_colors = []                                                # Face colors
+    line_pts = []                                                   # Vertices for line
+    line_colors = []                                                # Line color
+
+    geo, color, face_color, tolerance = data['geometry'], data['color'], data['face_color'], data['tolerance']
+
+    if geo is not None and not geo.is_empty:
+        simple = geo.simplify(tolerance) if tolerance else geo      # Simplified shape
+        pts = []                                                    # Shape line points
+        tri_pts = []                                                # Mesh vertices
+        tri_tris = []                                               # Mesh faces
+
+        if type(geo) == LineString:
+            # Prepare lines
+            pts = _linestring_to_segments(list(simple.coords))
+
+        elif type(geo) == LinearRing:
+            # Prepare lines
+            pts = _linearring_to_segments(list(simple.coords))
+
+        elif type(geo) == Polygon:
+            # Prepare polygon faces
+            if face_color is not None:
+                if triangulation == 'glu':
+                    gt = GLUTess()
+                    tri_tris, tri_pts = gt.triangulate(simple)
+                else:
+                    print("Triangulation type '%s' isn't implemented. Drawing only edges." % triangulation)
+
+            # Prepare polygon edges
+            if color is not None:
+                pts = _linearring_to_segments(list(simple.exterior.coords))
+                for ints in simple.interiors:
+                    pts += _linearring_to_segments(list(ints.coords))
+
+        # Appending data for mesh
+        if len(tri_pts) > 0 and len(tri_tris) > 0:
+            mesh_tris += tri_tris
+            mesh_vertices += tri_pts
+            mesh_colors += [Color(face_color).rgba] * (len(tri_tris) // 3)
+
+        # Appending data for line
+        if len(pts) > 0:
+            line_pts += pts
+            line_colors += [Color(color).rgba] * len(pts)
+
+    # Store buffers
+    data['line_pts'] = line_pts
+    data['line_colors'] = line_colors
+    data['mesh_vertices'] = mesh_vertices
+    data['mesh_tris'] = mesh_tris
+    data['mesh_colors'] = mesh_colors
+
+    # Clear shapely geometry
+    del data['geometry']
+
+    return data
+
+
+def _linearring_to_segments(arr):
+    # Close linear ring
+    """
+    Translates linear ring to line segments
+    :param arr: numpy.array
+        Array of linear ring vertices
+    :return: numpy.array
+        Line segments
+    """
+    if arr[0] != arr[-1]:
+        arr.append(arr[0])
+
+    return _linestring_to_segments(arr)
+
+
+def _linestring_to_segments(arr):
+    """
+    Translates line strip to segments
+    :param arr: numpy.array
+        Array of line strip vertices
+    :return: numpy.array
+        Line segments
+    """
+    return [arr[i // 2] for i in range(0, len(arr) * 2)][1:-1]
+
+
+class ShapeGroup(object):
+    def __init__(self, collection):
+        """
+        Represents group of shapes in collection
+        :param collection: ShapeCollection
+            Collection to work with
+        """
+        self._collection = collection
+        self._indexes = []
+        self._visible = True
+        self._color = None
+
+    def add(self, **kwargs):
+        """
+        Adds shape to collection and store index in group
+        :param kwargs: keyword arguments
+            Arguments for ShapeCollection.add function
+        """
+        self._indexes.append(self._collection.add(**kwargs))
+
+    def clear(self, update=False):
+        """
+        Removes group shapes from collection, clear indexes
+        :param update: bool
+            Set True to redraw collection
+        """
+        for i in self._indexes:
+            self._collection.remove(i, False)
+
+        del self._indexes[:]
+
+        if update:
+            self._collection.redraw([])             # Skip waiting results
+
+    def redraw(self):
+        """
+        Redraws shape collection
+        """
+        self._collection.redraw(self._indexes)
+
+    @property
+    def visible(self):
+        """
+        Visibility of group
+        :return: bool
+        """
+        return self._visible
+
+    @visible.setter
+    def visible(self, value):
+        """
+        Visibility of group
+        :param value: bool
+        """
+        self._visible = value
+        for i in self._indexes:
+            self._collection.data[i]['visible'] = value
+
+        self._collection.redraw([])
+
+
+class ShapeCollectionVisual(CompoundVisual):
+
+    def __init__(self, line_width=1, triangulation='gpc', layers=3, pool=None, **kwargs):
+        """
+        Represents collection of shapes to draw on VisPy scene
+        :param line_width: float
+            Width of lines/edges
+        :param triangulation: str
+            Triangulation method used for polygons translation
+            'vispy' - VisPy lib triangulation
+            'gpc' - Polygon2 lib
+        :param layers: int
+            Layers count
+            Each layer adds 2 visuals on VisPy scene. Be careful: more layers cause less fps
+        :param kwargs:
+        """
+        self.data = {}
+        self.last_key = -1
+
+        # Thread locks
+        self.key_lock = threading.Lock()
+        self.results_lock = threading.Lock()
+        self.update_lock = threading.Lock()
+
+        # Process pool
+        self.pool = pool
+        self.results = {}
+
+        self._meshes = [MeshVisual() for _ in range(0, layers)]
+        # self._lines = [LineVisual(antialias=True) for _ in range(0, layers)]
+        self._lines = [FlatCAMLineVisual(antialias=True) for _ in range(0, layers)]
+
+        self._line_width = line_width
+        self._triangulation = triangulation
+
+        visuals_ = [self._lines[i // 2] if i % 2 else self._meshes[i // 2] for i in range(0, layers * 2)]
+
+        CompoundVisual.__init__(self, visuals_, **kwargs)
+
+        for m in self._meshes:
+            pass
+            m.set_gl_state(polygon_offset_fill=True, polygon_offset=(1, 1), cull_face=False)
+
+        for l in self._lines:
+            pass
+            l.set_gl_state(blend=True)
+
+        self.freeze()
+
+    def add(self, shape=None, color=None, face_color=None, alpha=None, visible=True,
+            update=False, layer=1, tolerance=0.01):
+        """
+        Adds shape to collection
+        :return:
+        :param shape: shapely.geometry
+            Shapely geometry object
+        :param color: str, tuple
+            Line/edge color
+        :param face_color: str, tuple
+            Polygon face color
+        :param visible: bool
+            Shape visibility
+        :param update: bool
+            Set True to redraw collection
+        :param layer: int
+            Layer number. 0 - lowest.
+        :param tolerance: float
+            Geometry simplifying tolerance
+        :return: int
+            Index of shape
+        """
+        # Get new key
+        self.key_lock.acquire(True)
+        self.last_key += 1
+        key = self.last_key
+        self.key_lock.release()
+
+        # Prepare data for translation
+        self.data[key] = {'geometry': shape, 'color': color, 'alpha': alpha, 'face_color': face_color,
+                          'visible': visible, 'layer': layer, 'tolerance': tolerance}
+
+        # Add data to process pool if pool exists
+        try:
+            self.results[key] = self.pool.map_async(_update_shape_buffers, [self.data[key]])
+        except:
+            self.data[key] = _update_shape_buffers(self.data[key])
+
+        if update:
+            self.redraw()                       # redraw() waits for pool process end
+
+        return key
+
+    def remove(self, key, update=False):
+        """
+        Removes shape from collection
+        :param key: int
+            Shape index to remove
+        :param update:
+            Set True to redraw collection
+        """
+        # Remove process result
+        self.results_lock.acquire(True)
+        if key in list(self.results.copy().keys()):
+            del self.results[key]
+        self.results_lock.release()
+
+        # Remove data
+        del self.data[key]
+
+        if update:
+            self.__update()
+
+    def clear(self, update=False):
+        """
+        Removes all shapes from collection
+        :param update: bool
+            Set True to redraw collection
+        """
+        self.data.clear()
+        if update:
+            self.__update()
+
+    def __update(self):
+        """
+        Merges internal buffers, sets data to visuals, redraws collection on scene
+        """
+        mesh_vertices = [[] for _ in range(0, len(self._meshes))]       # Vertices for mesh
+        mesh_tris = [[] for _ in range(0, len(self._meshes))]           # Faces for mesh
+        mesh_colors = [[] for _ in range(0, len(self._meshes))]         # Face colors
+        line_pts = [[] for _ in range(0, len(self._lines))]             # Vertices for line
+        line_colors = [[] for _ in range(0, len(self._lines))]          # Line color
+
+        # Lock sub-visuals updates
+        self.update_lock.acquire(True)
+
+        # Merge shapes buffers
+        for data in list(self.data.values()):
+            if data['visible'] and 'line_pts' in data:
+                try:
+                    line_pts[data['layer']] += data['line_pts']
+                    line_colors[data['layer']] += data['line_colors']
+                    mesh_tris[data['layer']] += [x + len(mesh_vertices[data['layer']])
+                                                 for x in data['mesh_tris']]
+
+                    mesh_vertices[data['layer']] += data['mesh_vertices']
+                    mesh_colors[data['layer']] += data['mesh_colors']
+                except Exception as e:
+                    print("Data error", e)
+
+        # Updating meshes
+        for i, mesh in enumerate(self._meshes):
+            if len(mesh_vertices[i]) > 0:
+                set_state(polygon_offset_fill=False)
+                mesh.set_data(np.asarray(mesh_vertices[i]), np.asarray(mesh_tris[i], dtype=np.uint32)
+                              .reshape((-1, 3)), face_colors=np.asarray(mesh_colors[i]))
+            else:
+                mesh.set_data()
+
+            mesh._bounds_changed()
+
+        # Updating lines
+        for i, line in enumerate(self._lines):
+            if len(line_pts[i]) > 0:
+                line.set_data(np.asarray(line_pts[i]), np.asarray(line_colors[i]), self._line_width, 'segments')
+            else:
+                line.clear_data()
+
+            line._bounds_changed()
+
+        self._bounds_changed()
+
+        self.update_lock.release()
+
+    def redraw(self, indexes=None):
+        """
+        Redraws collection
+        :param indexes: list
+            Shape indexes to get from process pool
+        """
+        # Only one thread can update data
+        self.results_lock.acquire(True)
+
+        for i in list(self.data.copy().keys()) if not indexes else indexes:
+            if i in list(self.results.copy().keys()):
+                try:
+                    self.results[i].wait()                                  # Wait for process results
+                    if i in self.data:
+                        self.data[i] = self.results[i].get()[0]             # Store translated data
+                        del self.results[i]
+                except Exception as e:
+                    print(e, indexes)
+
+        self.results_lock.release()
+
+        self.__update()
+
+    def lock_updates(self):
+        self.update_lock.acquire(True)
+
+    def unlock_updates(self):
+        self.update_lock.release()
+
+
+class TextGroup(object):
+    def __init__(self, collection):
+        self._collection = collection
+        self._index = None
+        self._visible = None
+
+    def set(self, **kwargs):
+        """
+        Adds text to collection and store index
+        :param kwargs: keyword arguments
+            Arguments for TextCollection.add function
+        """
+        self._index = self._collection.add(**kwargs)
+
+    def clear(self, update=False):
+        """
+        Removes text from collection, clear index
+        :param update: bool
+            Set True to redraw collection
+        """
+
+        if self._index is not None:
+            self._collection.remove(self._index, False)
+            self._index = None
+
+        if update:
+            self._collection.redraw()
+
+    def redraw(self):
+        """
+        Redraws text collection
+        """
+        self._collection.redraw()
+
+    @property
+    def visible(self):
+        """
+        Visibility of group
+        :return: bool
+        """
+        return self._visible
+
+    @visible.setter
+    def visible(self, value):
+        """
+        Visibility of group
+        :param value: bool
+        """
+        self._visible = value
+        self._collection.data[self._index]['visible'] = value
+
+        self._collection.redraw()
+
+
+class TextCollectionVisual(TextVisual):
+
+    def __init__(self, **kwargs):
+        """
+        Represents collection of shapes to draw on VisPy scene
+        :param kwargs: keyword arguments
+            Arguments to pass for TextVisual
+        """
+        self.data = {}
+        self.last_key = -1
+        self.lock = threading.Lock()
+
+        super(TextCollectionVisual, self).__init__(**kwargs)
+
+        self.freeze()
+
+    def add(self, text, pos, visible=True, update=True):
+        """
+        Adds array of text to collection
+        :param text: list
+            Array of strings ['str1', 'str2', ... ]
+        :param pos: list
+            Array of string positions   [(0, 0), (10, 10), ... ]
+        :param update: bool
+            Set True to redraw collection
+        :return: int
+            Index of array
+        """
+        # Get new key
+        self.lock.acquire(True)
+        self.last_key += 1
+        key = self.last_key
+        self.lock.release()
+
+        # Prepare data for translation
+        self.data[key] = {'text': text, 'pos': pos, 'visible': visible}
+
+        if update:
+            self.redraw()
+
+        return key
+
+    def remove(self, key, update=False):
+        """
+        Removes shape from collection
+        :param key: int
+            Shape index to remove
+        :param update:
+            Set True to redraw collection
+        """
+        del self.data[key]
+
+        if update:
+            self.__update()
+
+    def clear(self, update=False):
+        """
+        Removes all shapes from colleciton
+        :param update: bool
+            Set True to redraw collection
+        """
+        self.data.clear()
+        if update:
+            self.__update()
+
+    def __update(self):
+        """
+        Merges internal buffers, sets data to visuals, redraws collection on scene
+        """
+        labels = []
+        pos = []
+
+        # Merge buffers
+        for data in list(self.data.values()):
+            if data['visible']:
+                try:
+                    labels += data['text']
+                    pos += data['pos']
+                except Exception as e:
+                    print("Data error", e)
+
+        # Updating text
+        if len(labels) > 0:
+            self.text = labels
+            self.pos = pos
+        else:
+            self.text = None
+            self.pos = (0, 0)
+
+        self._bounds_changed()
+
+    def redraw(self):
+        """
+        Redraws collection
+        """
+        self.__update()
+
+
+# Add 'enabled' property to visual nodes
+def create_fast_node(subclass):
+    # Create a new subclass of Node.
+
+    # Decide on new class name
+    clsname = subclass.__name__
+    if not (clsname.endswith('Visual') and
+            issubclass(subclass, visuals.BaseVisual)):
+        raise RuntimeError('Class "%s" must end with Visual, and must '
+                           'subclass BaseVisual' % clsname)
+    clsname = clsname[:-6]
+
+    # Generate new docstring based on visual docstring
+    try:
+        doc = generate_docstring(subclass, clsname)
+    except Exception:
+        # If parsing fails, just return the original Visual docstring
+        doc = subclass.__doc__
+
+    # New __init__ method
+    def __init__(self, *args, **kwargs):
+        parent = kwargs.pop('parent', None)
+        name = kwargs.pop('name', None)
+        self.name = name  # to allow __str__ before Node.__init__
+        self._visual_superclass = subclass
+
+        # parent: property,
+        # _parent: attribute of Node class
+        # __parent: attribute of fast_node class
+        self.__parent = parent
+        self._enabled = False
+
+        subclass.__init__(self, *args, **kwargs)
+        self.unfreeze()
+        VisualNode.__init__(self, parent=parent, name=name)
+        self.freeze()
+
+    # Create new class
+    cls = type(clsname, (VisualNode, subclass),
+               {'__init__': __init__, '__doc__': doc})
+
+    # 'Enabled' property clears/restores 'parent' property of Node class
+    # Scene will be painted quicker than when using 'visible' property
+    def get_enabled(self):
+        return self._enabled
+
+    def set_enabled(self, enabled):
+        if enabled:
+            self.parent = self.__parent                 # Restore parent
+        else:
+            if self.parent:                             # Store parent
+                self.__parent = self.parent
+            self.parent = None
+
+    cls.enabled = property(get_enabled, set_enabled)
+
+    return cls
+
+
+ShapeCollection = create_fast_node(ShapeCollectionVisual)
+TextCollection = create_fast_node(TextCollectionVisual)
+Cursor = create_fast_node(MarkersVisual)

+ 6435 - 0
camlib.py

@@ -0,0 +1,6435 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+#import traceback
+
+from io import StringIO
+from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos, dot, float32, \
+    transpose
+from numpy.linalg import solve, norm
+import re
+import sys
+import traceback
+from decimal import Decimal
+
+import collections
+
+from rtree import index as rtindex
+
+# See: http://toblerity.org/shapely/manual.html
+from shapely.geometry import Polygon, LineString, Point, LinearRing, MultiLineString
+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 shapely.geometry import shape
+
+from collections import Iterable
+
+import numpy as np
+import rasterio
+from rasterio.features import shapes
+
+# TODO: Commented for FlatCAM packaging with cx_freeze
+
+from xml.dom.minidom import parseString as parse_xml_string
+
+from ParseSVG import *
+from ParseDXF import *
+
+import logging
+import os
+# import pprint
+import platform
+import FlatCAMApp
+
+import math
+
+if platform.architecture()[0] == '64bit':
+    from ortools.constraint_solver import pywrapcp
+    from ortools.constraint_solver import routing_enums_pb2
+
+
+log = logging.getLogger('base2')
+log.setLevel(logging.DEBUG)
+# log.setLevel(logging.WARNING)
+# log.setLevel(logging.INFO)
+formatter = logging.Formatter('[%(levelname)s] %(message)s')
+handler = logging.StreamHandler()
+handler.setFormatter(formatter)
+log.addHandler(handler)
+
+
+class ParseError(Exception):
+    pass
+
+
+class Geometry(object):
+    """
+    Base geometry class.
+    """
+
+    defaults = {
+        "units": 'in',
+        "geo_steps_per_circle": 64
+    }
+
+    def __init__(self, geo_steps_per_circle=None):
+        # Units (in or mm)
+        self.units = Geometry.defaults["units"]
+        
+        # Final geometry: MultiPolygon or list (of geometry constructs)
+        self.solid_geometry = None
+
+        # Attributes to be included in serialization
+        self.ser_attrs = ["units", 'solid_geometry']
+
+        # Flattened geometry (list of paths only)
+        self.flat_geometry = []
+
+        # Index
+        self.index = None
+
+        self.geo_steps_per_circle = geo_steps_per_circle
+
+        if geo_steps_per_circle is None:
+            geo_steps_per_circle = Geometry.defaults["geo_steps_per_circle"]
+        self.geo_steps_per_circle = geo_steps_per_circle
+
+    def make_index(self):
+        self.flatten()
+        self.index = FlatCAMRTree()
+
+        for i, g in enumerate(self.flat_geometry):
+            self.index.insert(i, g)
+
+    def add_circle(self, origin, radius):
+        """
+        Adds a circle to the object.
+
+        :param origin: Center of the circle.
+        :param radius: Radius of the circle.
+        :return: None
+        """
+        # TODO: Decide what solid_geometry is supposed to be and how we append to it.
+
+        if self.solid_geometry is None:
+            self.solid_geometry = []
+
+        if type(self.solid_geometry) is list:
+            self.solid_geometry.append(Point(origin).buffer(radius, int(int(self.geo_steps_per_circle) / 4)))
+            return
+
+        try:
+            self.solid_geometry = self.solid_geometry.union(Point(origin).buffer(radius,
+                                                                                 int(int(self.geo_steps_per_circle) / 4)))
+        except:
+            #print "Failed to run union on polygons."
+            log.error("Failed to run union on polygons.")
+            return
+
+    def add_polygon(self, points):
+        """
+        Adds a polygon to the object (by union)
+
+        :param points: The vertices of the polygon.
+        :return: None
+        """
+        if self.solid_geometry is None:
+            self.solid_geometry = []
+
+        if type(self.solid_geometry) is list:
+            self.solid_geometry.append(Polygon(points))
+            return
+
+        try:
+            self.solid_geometry = self.solid_geometry.union(Polygon(points))
+        except:
+            #print "Failed to run union on polygons."
+            log.error("Failed to run union on polygons.")
+            return
+
+    def add_polyline(self, points):
+        """
+        Adds a polyline to the object (by union)
+
+        :param points: The vertices of the polyline.
+        :return: None
+        """
+        if self.solid_geometry is None:
+            self.solid_geometry = []
+
+        if type(self.solid_geometry) is list:
+            self.solid_geometry.append(LineString(points))
+            return
+
+        try:
+            self.solid_geometry = self.solid_geometry.union(LineString(points))
+        except:
+            #print "Failed to run union on polygons."
+            log.error("Failed to run union on polylines.")
+            return
+
+    def is_empty(self):
+
+        if isinstance(self.solid_geometry, BaseGeometry):
+            return self.solid_geometry.is_empty
+
+        if isinstance(self.solid_geometry, list):
+            return len(self.solid_geometry) == 0
+
+        self.app.inform.emit("[error_notcl] self.solid_geometry is neither BaseGeometry or list.")
+        return
+
+    def subtract_polygon(self, points):
+        """
+        Subtract polygon from the given object. This only operates on the paths in the original geometry, i.e. it converts polygons into paths.
+
+        :param points: The vertices of the polygon.
+        :return: none
+        """
+        if self.solid_geometry is None:
+            self.solid_geometry = []
+
+        #pathonly should be allways True, otherwise polygons are not subtracted
+        flat_geometry = self.flatten(pathonly=True)
+        log.debug("%d paths" % len(flat_geometry))
+        polygon=Polygon(points)
+        toolgeo=cascaded_union(polygon)
+        diffs=[]
+        for target in flat_geometry:
+            if type(target) == LineString or type(target) == LinearRing:
+                diffs.append(target.difference(toolgeo))
+            else:
+                log.warning("Not implemented.")
+        self.solid_geometry=cascaded_union(diffs)
+
+    def bounds(self):
+        """
+        Returns coordinates of rectangular bounds
+        of geometry: (xmin, ymin, xmax, ymax).
+        """
+        # fixed issue of getting bounds only for one level lists of objects
+        # now it can get bounds for nested lists of objects
+
+        log.debug("Geometry->bounds()")
+        if self.solid_geometry is None:
+            log.debug("solid_geometry is None")
+            return 0, 0, 0, 0
+
+        def bounds_rec(obj):
+            if type(obj) is list:
+                minx = Inf
+                miny = Inf
+                maxx = -Inf
+                maxy = -Inf
+
+                for k in obj:
+                    if type(k) is dict:
+                        for key in k:
+                            minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
+                            minx = min(minx, minx_)
+                            miny = min(miny, miny_)
+                            maxx = max(maxx, maxx_)
+                            maxy = max(maxy, maxy_)
+                    else:
+                        minx_, miny_, maxx_, maxy_ = bounds_rec(k)
+                        minx = min(minx, minx_)
+                        miny = min(miny, miny_)
+                        maxx = max(maxx, maxx_)
+                        maxy = max(maxy, maxy_)
+                return minx, miny, maxx, maxy
+            else:
+                # it's a Shapely object, return it's bounds
+                return obj.bounds
+
+        if self.multigeo is True:
+            minx_list = []
+            miny_list = []
+            maxx_list = []
+            maxy_list = []
+
+            for tool in self.tools:
+                minx, miny, maxx, maxy = bounds_rec(self.tools[tool]['solid_geometry'])
+                minx_list.append(minx)
+                miny_list.append(miny)
+                maxx_list.append(maxx)
+                maxy_list.append(maxy)
+
+            return(min(minx_list), min(miny_list), max(maxx_list), max(maxy_list))
+        else:
+            bounds_coords = bounds_rec(self.solid_geometry)
+            return bounds_coords
+
+        # try:
+        #     # from here: http://rightfootin.blogspot.com/2006/09/more-on-python-flatten.html
+        #     def flatten(l, ltypes=(list, tuple)):
+        #         ltype = type(l)
+        #         l = list(l)
+        #         i = 0
+        #         while i < len(l):
+        #             while isinstance(l[i], ltypes):
+        #                 if not l[i]:
+        #                     l.pop(i)
+        #                     i -= 1
+        #                     break
+        #                 else:
+        #                     l[i:i + 1] = l[i]
+        #             i += 1
+        #         return ltype(l)
+        #
+        #     log.debug("Geometry->bounds()")
+        #     if self.solid_geometry is None:
+        #         log.debug("solid_geometry is None")
+        #         return 0, 0, 0, 0
+        #
+        #     if type(self.solid_geometry) is list:
+        #         # TODO: This can be done faster. See comment from Shapely mailing lists.
+        #         if len(self.solid_geometry) == 0:
+        #             log.debug('solid_geometry is empty []')
+        #             return 0, 0, 0, 0
+        #         return cascaded_union(flatten(self.solid_geometry)).bounds
+        #     else:
+        #         return self.solid_geometry.bounds
+        # except Exception as e:
+        #     self.app.inform.emit("[error_notcl] Error cause: %s" % str(e))
+
+        # log.debug("Geometry->bounds()")
+        # if self.solid_geometry is None:
+        #     log.debug("solid_geometry is None")
+        #     return 0, 0, 0, 0
+        #
+        # if type(self.solid_geometry) is list:
+        #     # TODO: This can be done faster. See comment from Shapely mailing lists.
+        #     if len(self.solid_geometry) == 0:
+        #         log.debug('solid_geometry is empty []')
+        #         return 0, 0, 0, 0
+        #     return cascaded_union(self.solid_geometry).bounds
+        # else:
+        #     return self.solid_geometry.bounds
+
+    def find_polygon(self, point, geoset=None):
+        """
+        Find an object that object.contains(Point(point)) in
+        poly, which can can be iterable, contain iterable of, or
+        be itself an implementer of .contains().
+
+        :param poly: See description
+        :return: Polygon containing point or None.
+        """
+
+        if geoset is None:
+            geoset = self.solid_geometry
+
+        try:  # Iterable
+            for sub_geo in geoset:
+                p = self.find_polygon(point, geoset=sub_geo)
+                if p is not None:
+                    return p
+        except TypeError:  # Non-iterable
+            try:  # Implements .contains()
+                if isinstance(geoset, LinearRing):
+                    geoset = Polygon(geoset)
+                if geoset.contains(Point(point)):
+                    return geoset
+            except AttributeError:  # Does not implement .contains()
+                return None
+
+        return None
+
+    def get_interiors(self, geometry=None):
+
+        interiors = []
+
+        if geometry is None:
+            geometry = self.solid_geometry
+
+        ## If iterable, expand recursively.
+        try:
+            for geo in geometry:
+                interiors.extend(self.get_interiors(geometry=geo))
+
+        ## Not iterable, get the interiors if polygon.
+        except TypeError:
+            if type(geometry) == Polygon:
+                interiors.extend(geometry.interiors)
+
+        return interiors
+
+    def get_exteriors(self, geometry=None):
+        """
+        Returns all exteriors of polygons in geometry. Uses
+        ``self.solid_geometry`` if geometry is not provided.
+
+        :param geometry: Shapely type or list or list of list of such.
+        :return: List of paths constituting the exteriors
+           of polygons in geometry.
+        """
+
+        exteriors = []
+
+        if geometry is None:
+            geometry = self.solid_geometry
+
+        ## If iterable, expand recursively.
+        try:
+            for geo in geometry:
+                exteriors.extend(self.get_exteriors(geometry=geo))
+
+        ## Not iterable, get the exterior if polygon.
+        except TypeError:
+            if type(geometry) == Polygon:
+                exteriors.append(geometry.exterior)
+
+        return exteriors
+
+    def flatten(self, geometry=None, reset=True, pathonly=False):
+        """
+        Creates a list of non-iterable linear geometry objects.
+        Polygons are expanded into its exterior and interiors if specified.
+
+        Results are placed in self.flat_geometry
+
+        :param geometry: Shapely type or list or list of list of such.
+        :param reset: Clears the contents of self.flat_geometry.
+        :param pathonly: Expands polygons into linear elements.
+        """
+
+        if geometry is None:
+            geometry = self.solid_geometry
+
+        if reset:
+            self.flat_geometry = []
+
+        ## If iterable, expand recursively.
+        try:
+            for geo in geometry:
+                if geo is not None:
+                    self.flatten(geometry=geo,
+                                 reset=False,
+                                 pathonly=pathonly)
+
+        ## Not iterable, do the actual indexing and add.
+        except TypeError:
+            if pathonly and type(geometry) == Polygon:
+                self.flat_geometry.append(geometry.exterior)
+                self.flatten(geometry=geometry.interiors,
+                             reset=False,
+                             pathonly=True)
+            else:
+                self.flat_geometry.append(geometry)
+
+        return self.flat_geometry
+
+    # def make2Dstorage(self):
+    #
+    #     self.flatten()
+    #
+    #     def get_pts(o):
+    #         pts = []
+    #         if type(o) == Polygon:
+    #             g = o.exterior
+    #             pts += list(g.coords)
+    #             for i in o.interiors:
+    #                 pts += list(i.coords)
+    #         else:
+    #             pts += list(o.coords)
+    #         return pts
+    #
+    #     storage = FlatCAMRTreeStorage()
+    #     storage.get_points = get_pts
+    #     for shape in self.flat_geometry:
+    #         storage.insert(shape)
+    #     return storage
+
+    # def flatten_to_paths(self, geometry=None, reset=True):
+    #     """
+    #     Creates a list of non-iterable linear geometry elements and
+    #     indexes them in rtree.
+    #
+    #     :param geometry: Iterable geometry
+    #     :param reset: Wether to clear (True) or append (False) to self.flat_geometry
+    #     :return: self.flat_geometry, self.flat_geometry_rtree
+    #     """
+    #
+    #     if geometry is None:
+    #         geometry = self.solid_geometry
+    #
+    #     if reset:
+    #         self.flat_geometry = []
+    #
+    #     ## If iterable, expand recursively.
+    #     try:
+    #         for geo in geometry:
+    #             self.flatten_to_paths(geometry=geo, reset=False)
+    #
+    #     ## Not iterable, do the actual indexing and add.
+    #     except TypeError:
+    #         if type(geometry) == Polygon:
+    #             g = geometry.exterior
+    #             self.flat_geometry.append(g)
+    #
+    #             ## Add first and last points of the path to the index.
+    #             self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
+    #             self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
+    #
+    #             for interior in geometry.interiors:
+    #                 g = interior
+    #                 self.flat_geometry.append(g)
+    #                 self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
+    #                 self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
+    #         else:
+    #             g = geometry
+    #             self.flat_geometry.append(g)
+    #             self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
+    #             self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
+    #
+    #     return self.flat_geometry, self.flat_geometry_rtree
+
+    def isolation_geometry(self, offset, iso_type=2):
+        """
+        Creates contours around geometry at a given
+        offset distance.
+
+        :param offset: Offset distance.
+        :type offset: float
+        :param iso_type: type of isolation, can be 0 = exteriors or 1 = interiors or 2 = both (complete)
+        :type integer
+        :return: The buffered geometry.
+        :rtype: Shapely.MultiPolygon or Shapely.Polygon
+        """
+
+        # geo_iso = []
+        # In case that the offset value is zero we don't use the buffer as the resulting geometry is actually the
+        # original solid_geometry
+        # if offset == 0:
+        #     geo_iso = self.solid_geometry
+        # else:
+        #     flattened_geo = self.flatten_list(self.solid_geometry)
+        #     try:
+        #         for mp_geo in flattened_geo:
+        #             geo_iso.append(mp_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4)))
+        #     except TypeError:
+        #         geo_iso.append(self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4)))
+
+        geo_iso = []
+        flattened_geo = self.flatten_list(self.solid_geometry)
+        try:
+            for mp_geo in flattened_geo:
+                geo_iso.append(mp_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4)))
+        except TypeError:
+            geo_iso.append(self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4)))
+
+        if iso_type == 2:
+            return geo_iso
+        elif iso_type == 0:
+            return self.get_exteriors(geo_iso)
+        elif iso_type == 1:
+            return self.get_interiors(geo_iso)
+        else:
+            log.debug("Geometry.isolation_geometry() --> Type of isolation not supported")
+            return "fail"
+
+    def flatten_list(self, list):
+        for item in list:
+            if isinstance(item, Iterable) and not isinstance(item, (str, bytes)):
+                yield from self.flatten_list(item)
+            else:
+                yield item
+
+    def import_svg(self, filename, object_type=None, flip=True, units='MM'):
+        """
+        Imports shapes from an SVG file into the object's geometry.
+
+        :param filename: Path to the SVG file.
+        :type filename: str
+        :param flip: Flip the vertically.
+        :type flip: bool
+        :return: None
+        """
+
+        # Parse into list of shapely objects
+        svg_tree = ET.parse(filename)
+        svg_root = svg_tree.getroot()
+
+        # Change origin to bottom left
+        # h = float(svg_root.get('height'))
+        # w = float(svg_root.get('width'))
+        h = svgparselength(svg_root.get('height'))[0]  # TODO: No units support yet
+        geos = getsvggeo(svg_root, object_type)
+        if flip:
+            geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]
+
+        # Add to object
+        if self.solid_geometry is None:
+            self.solid_geometry = []
+
+        if type(self.solid_geometry) is list:
+            # self.solid_geometry.append(cascaded_union(geos))
+            if type(geos) is list:
+                self.solid_geometry += geos
+            else:
+                self.solid_geometry.append(geos)
+        else:  # It's shapely geometry
+            # self.solid_geometry = cascaded_union([self.solid_geometry,
+            #                                       cascaded_union(geos)])
+            self.solid_geometry = [self.solid_geometry, geos]
+
+        # flatten the self.solid_geometry list for import_svg() to import SVG as Gerber
+        self.solid_geometry = list(self.flatten_list(self.solid_geometry))
+        self.solid_geometry = cascaded_union(self.solid_geometry)
+
+        geos_text = getsvgtext(svg_root, object_type, units=units)
+        if geos_text is not None:
+            geos_text_f = []
+            if flip:
+                # Change origin to bottom left
+                for i in geos_text:
+                    _, minimy, _, maximy = i.bounds
+                    h2 = (maximy - minimy) * 0.5
+                    geos_text_f.append(translate(scale(i, 1.0, -1.0, origin=(0, 0)), yoff=(h + h2)))
+            self.solid_geometry = [self.solid_geometry, geos_text_f]
+
+    def import_dxf(self, filename, object_type=None, units='MM'):
+        """
+        Imports shapes from an DXF file into the object's geometry.
+
+        :param filename: Path to the DXF file.
+        :type filename: str
+        :param units: Application units
+        :type flip: str
+        :return: None
+        """
+
+        # Parse into list of shapely objects
+        dxf = ezdxf.readfile(filename)
+        geos = getdxfgeo(dxf)
+
+        # Add to object
+        if self.solid_geometry is None:
+            self.solid_geometry = []
+
+        if type(self.solid_geometry) is list:
+            if type(geos) is list:
+                self.solid_geometry += geos
+            else:
+                self.solid_geometry.append(geos)
+        else:  # It's shapely geometry
+            self.solid_geometry = [self.solid_geometry, geos]
+
+        # flatten the self.solid_geometry list for import_dxf() to import DXF as Gerber
+        self.solid_geometry = list(self.flatten_list(self.solid_geometry))
+        if self.solid_geometry is not None:
+            self.solid_geometry = cascaded_union(self.solid_geometry)
+        else:
+            return
+
+        # commented until this function is ready
+        # geos_text = getdxftext(dxf, object_type, units=units)
+        # if geos_text is not None:
+        #     geos_text_f = []
+        #     self.solid_geometry = [self.solid_geometry, geos_text_f]
+
+    def import_image(self, filename, flip=True, units='MM', dpi=96, mode='black', mask=[128, 128, 128, 128]):
+        """
+        Imports shapes from an IMAGE file into the object's geometry.
+
+        :param filename: Path to the IMAGE file.
+        :type filename: str
+        :param flip: Flip the object vertically.
+        :type flip: bool
+        :return: None
+        """
+        scale_factor = 0.264583333
+
+        if units.lower() == 'mm':
+            scale_factor = 25.4 / dpi
+        else:
+            scale_factor = 1 / dpi
+
+
+        geos = []
+        unscaled_geos = []
+
+        with rasterio.open(filename) as src:
+            # if filename.lower().rpartition('.')[-1] == 'bmp':
+            #     red = green = blue = src.read(1)
+            #     print("BMP")
+            # elif filename.lower().rpartition('.')[-1] == 'png':
+            #     red, green, blue, alpha = src.read()
+            # elif filename.lower().rpartition('.')[-1] == 'jpg':
+            #     red, green, blue = src.read()
+
+            red = green = blue = src.read(1)
+
+            try:
+                green = src.read(2)
+            except:
+                pass
+
+            try:
+                blue= src.read(3)
+            except:
+                pass
+
+        if mode == 'black':
+            mask_setting = red <= mask[0]
+            total = red
+            log.debug("Image import as monochrome.")
+        else:
+            mask_setting = (red <= mask[1]) + (green <= mask[2]) + (blue <= mask[3])
+            total = np.zeros(red.shape, dtype=float32)
+            for band in red, green, blue:
+                total += band
+            total /= 3
+            log.debug("Image import as colored. Thresholds are: R = %s , G = %s, B = %s" %
+                      (str(mask[1]), str(mask[2]), str(mask[3])))
+
+        for geom, val in shapes(total, mask=mask_setting):
+            unscaled_geos.append(shape(geom))
+
+        for g in unscaled_geos:
+            geos.append(scale(g, scale_factor, scale_factor, origin=(0, 0)))
+
+        if flip:
+            geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0))) for g in geos]
+
+        # Add to object
+        if self.solid_geometry is None:
+            self.solid_geometry = []
+
+        if type(self.solid_geometry) is list:
+            # self.solid_geometry.append(cascaded_union(geos))
+            if type(geos) is list:
+                self.solid_geometry += geos
+            else:
+                self.solid_geometry.append(geos)
+        else:  # It's shapely geometry
+            self.solid_geometry = [self.solid_geometry, geos]
+
+        # flatten the self.solid_geometry list for import_svg() to import SVG as Gerber
+        self.solid_geometry = list(self.flatten_list(self.solid_geometry))
+        self.solid_geometry = cascaded_union(self.solid_geometry)
+
+        # self.solid_geometry = MultiPolygon(self.solid_geometry)
+        # self.solid_geometry = self.solid_geometry.buffer(0.00000001)
+        # self.solid_geometry = self.solid_geometry.buffer(-0.00000001)
+
+    def size(self):
+        """
+        Returns (width, height) of rectangular
+        bounds of geometry.
+        """
+        if self.solid_geometry is None:
+            log.warning("Solid_geometry not computed yet.")
+            return 0
+        bounds = self.bounds()
+        return bounds[2] - bounds[0], bounds[3] - bounds[1]
+        
+    def get_empty_area(self, boundary=None):
+        """
+        Returns the complement of self.solid_geometry within
+        the given boundary polygon. If not specified, it defaults to
+        the rectangular bounding box of self.solid_geometry.
+        """
+        if boundary is None:
+            boundary = self.solid_geometry.envelope
+        return boundary.difference(self.solid_geometry)
+        
+    @staticmethod
+    def clear_polygon(polygon, tooldia, steps_per_circle, overlap=0.15, connect=True,
+                        contour=True):
+        """
+        Creates geometry inside a polygon for a tool to cover
+        the whole area.
+
+        This algorithm shrinks the edges of the polygon and takes
+        the resulting edges as toolpaths.
+
+        :param polygon: Polygon to clear.
+        :param tooldia: Diameter of the tool.
+        :param overlap: Overlap of toolpasses.
+        :param connect: Draw lines between disjoint segments to
+                        minimize tool lifts.
+        :param contour: Paint around the edges. Inconsequential in
+                        this painting method.
+        :return:
+        """
+
+        # log.debug("camlib.clear_polygon()")
+        assert type(polygon) == Polygon or type(polygon) == MultiPolygon, \
+            "Expected a Polygon or MultiPolygon, got %s" % type(polygon)
+
+        ## The toolpaths
+        # Index first and last points in paths
+        def get_pts(o):
+            return [o.coords[0], o.coords[-1]]
+
+        geoms = FlatCAMRTreeStorage()
+        geoms.get_points = get_pts
+
+        # Can only result in a Polygon or MultiPolygon
+        # NOTE: The resulting polygon can be "empty".
+        current = polygon.buffer((-tooldia / 1.999999), int(steps_per_circle / 4))
+        if current.area == 0:
+            # Otherwise, trying to to insert current.exterior == None
+            # into the FlatCAMStorage will fail.
+            # print("Area is None")
+            return None
+
+        # current can be a MultiPolygon
+        try:
+            for p in current:
+                geoms.insert(p.exterior)
+                for i in p.interiors:
+                    geoms.insert(i)
+
+        # Not a Multipolygon. Must be a Polygon
+        except TypeError:
+            geoms.insert(current.exterior)
+            for i in current.interiors:
+                geoms.insert(i)
+
+        while True:
+
+            # Can only result in a Polygon or MultiPolygon
+            current = current.buffer(-tooldia * (1 - overlap), int(steps_per_circle / 4))
+            if current.area > 0:
+
+                # current can be a MultiPolygon
+                try:
+                    for p in current:
+                        geoms.insert(p.exterior)
+                        for i in p.interiors:
+                            geoms.insert(i)
+
+                # Not a Multipolygon. Must be a Polygon
+                except TypeError:
+                    geoms.insert(current.exterior)
+                    for i in current.interiors:
+                        geoms.insert(i)
+            else:
+                print("Current Area is zero")
+                break
+
+        # Optimization: Reduce lifts
+        if connect:
+            # log.debug("Reducing tool lifts...")
+            geoms = Geometry.paint_connect(geoms, polygon, tooldia, steps_per_circle)
+
+        return geoms
+
+    @staticmethod
+    def clear_polygon2(polygon_to_clear, tooldia, steps_per_circle, seedpoint=None, overlap=0.15,
+                       connect=True, contour=True):
+        """
+        Creates geometry inside a polygon for a tool to cover
+        the whole area.
+
+        This algorithm starts with a seed point inside the polygon
+        and draws circles around it. Arcs inside the polygons are
+        valid cuts. Finalizes by cutting around the inside edge of
+        the polygon.
+
+        :param polygon_to_clear: Shapely.geometry.Polygon
+        :param tooldia: Diameter of the tool
+        :param seedpoint: Shapely.geometry.Point or None
+        :param overlap: Tool fraction overlap bewteen passes
+        :param connect: Connect disjoint segment to minumize tool lifts
+        :param contour: Cut countour inside the polygon.
+        :return: List of toolpaths covering polygon.
+        :rtype: FlatCAMRTreeStorage | None
+        """
+
+        # log.debug("camlib.clear_polygon2()")
+
+        # Current buffer radius
+        radius = tooldia / 2 * (1 - overlap)
+
+        ## The toolpaths
+        # Index first and last points in paths
+        def get_pts(o):
+            return [o.coords[0], o.coords[-1]]
+        geoms = FlatCAMRTreeStorage()
+        geoms.get_points = get_pts
+
+        # Path margin
+        path_margin = polygon_to_clear.buffer(-tooldia / 2, int(steps_per_circle / 4))
+
+        if path_margin.is_empty or path_margin is None:
+            return
+
+        # Estimate good seedpoint if not provided.
+        if seedpoint is None:
+            seedpoint = path_margin.representative_point()
+
+        # Grow from seed until outside the box. The polygons will
+        # never have an interior, so take the exterior LinearRing.
+        while 1:
+            path = Point(seedpoint).buffer(radius, int(steps_per_circle / 4)).exterior
+            path = path.intersection(path_margin)
+
+            # Touches polygon?
+            if path.is_empty:
+                break
+            else:
+                #geoms.append(path)
+                #geoms.insert(path)
+                # path can be a collection of paths.
+                try:
+                    for p in path:
+                        geoms.insert(p)
+                except TypeError:
+                    geoms.insert(path)
+
+            radius += tooldia * (1 - overlap)
+
+        # Clean inside edges (contours) of the original polygon
+        if contour:
+            outer_edges = [x.exterior for x in autolist(polygon_to_clear.buffer(-tooldia / 2, int(steps_per_circle / 4)))]
+            inner_edges = []
+            for x in autolist(polygon_to_clear.buffer(-tooldia / 2, int(steps_per_circle / 4))):  # Over resulting polygons
+                for y in x.interiors:  # Over interiors of each polygon
+                    inner_edges.append(y)
+            #geoms += outer_edges + inner_edges
+            for g in outer_edges + inner_edges:
+                geoms.insert(g)
+
+        # Optimization connect touching paths
+        # log.debug("Connecting paths...")
+        # geoms = Geometry.path_connect(geoms)
+
+        # Optimization: Reduce lifts
+        if connect:
+            # log.debug("Reducing tool lifts...")
+            geoms = Geometry.paint_connect(geoms, polygon_to_clear, tooldia, steps_per_circle)
+
+        return geoms
+
+    @staticmethod
+    def clear_polygon3(polygon, tooldia, steps_per_circle, overlap=0.15, connect=True,
+                       contour=True):
+        """
+        Creates geometry inside a polygon for a tool to cover
+        the whole area.
+
+        This algorithm draws horizontal lines inside the polygon.
+
+        :param polygon: The polygon being painted.
+        :type polygon: shapely.geometry.Polygon
+        :param tooldia: Tool diameter.
+        :param overlap: Tool path overlap percentage.
+        :param connect: Connect lines to avoid tool lifts.
+        :param contour: Paint around the edges.
+        :return:
+        """
+
+        # log.debug("camlib.clear_polygon3()")
+
+        ## The toolpaths
+        # Index first and last points in paths
+        def get_pts(o):
+            return [o.coords[0], o.coords[-1]]
+
+        geoms = FlatCAMRTreeStorage()
+        geoms.get_points = get_pts
+
+        lines = []
+
+        # Bounding box
+        left, bot, right, top = polygon.bounds
+
+        # First line
+        y = top - tooldia / 1.99999999
+        while y > bot + tooldia / 1.999999999:
+            line = LineString([(left, y), (right, y)])
+            lines.append(line)
+            y -= tooldia * (1 - overlap)
+
+        # Last line
+        y = bot + tooldia / 2
+        line = LineString([(left, y), (right, y)])
+        lines.append(line)
+
+        # Combine
+        linesgeo = unary_union(lines)
+
+        # Trim to the polygon
+        margin_poly = polygon.buffer(-tooldia / 1.99999999, (int(steps_per_circle)))
+        lines_trimmed = linesgeo.intersection(margin_poly)
+
+        # Add lines to storage
+        try:
+            for line in lines_trimmed:
+                geoms.insert(line)
+        except TypeError:
+            # in case lines_trimmed are not iterable (Linestring, LinearRing)
+            geoms.insert(lines_trimmed)
+
+        # Add margin (contour) to storage
+        if contour:
+            geoms.insert(margin_poly.exterior)
+            for ints in margin_poly.interiors:
+                geoms.insert(ints)
+
+        # Optimization: Reduce lifts
+        if connect:
+            # log.debug("Reducing tool lifts...")
+            geoms = Geometry.paint_connect(geoms, polygon, tooldia, steps_per_circle)
+
+        return geoms
+
+    def scale(self, xfactor, yfactor, point=None):
+        """
+        Scales all of the object's geometry by a given factor. Override
+        this method.
+        :param factor: Number by which to scale.
+        :type factor: float
+        :return: None
+        :rtype: None
+        """
+        return
+
+    def offset(self, vect):
+        """
+        Offset the geometry by the given vector. Override this method.
+
+        :param vect: (x, y) vector by which to offset the object.
+        :type vect: tuple
+        :return: None
+        """
+        return
+
+    @staticmethod
+    def paint_connect(storage, boundary, tooldia, steps_per_circle, max_walk=None):
+        """
+        Connects paths that results in a connection segment that is
+        within the paint area. This avoids unnecessary tool lifting.
+
+        :param storage: Geometry to be optimized.
+        :type storage: FlatCAMRTreeStorage
+        :param boundary: Polygon defining the limits of the paintable area.
+        :type boundary: Polygon
+        :param tooldia: Tool diameter.
+        :rtype tooldia: float
+        :param max_walk: Maximum allowable distance without lifting tool.
+        :type max_walk: float or None
+        :return: Optimized geometry.
+        :rtype: FlatCAMRTreeStorage
+        """
+
+        # If max_walk is not specified, the maximum allowed is
+        # 10 times the tool diameter
+        max_walk = max_walk or 10 * tooldia
+
+        # Assuming geolist is a flat list of flat elements
+
+        ## Index first and last points in paths
+        def get_pts(o):
+            return [o.coords[0], o.coords[-1]]
+
+        # storage = FlatCAMRTreeStorage()
+        # storage.get_points = get_pts
+        #
+        # for shape in geolist:
+        #     if shape is not None:  # TODO: This shouldn't have happened.
+        #         # Make LlinearRings into linestrings otherwise
+        #         # When chaining the coordinates path is messed up.
+        #         storage.insert(LineString(shape))
+        #         #storage.insert(shape)
+
+        ## Iterate over geometry paths getting the nearest each time.
+        #optimized_paths = []
+        optimized_paths = FlatCAMRTreeStorage()
+        optimized_paths.get_points = get_pts
+        path_count = 0
+        current_pt = (0, 0)
+        pt, geo = storage.nearest(current_pt)
+        storage.remove(geo)
+        geo = LineString(geo)
+        current_pt = geo.coords[-1]
+        try:
+            while True:
+                path_count += 1
+                #log.debug("Path %d" % path_count)
+
+                pt, candidate = storage.nearest(current_pt)
+                storage.remove(candidate)
+                candidate = LineString(candidate)
+
+                # If last point in geometry is the nearest
+                # then reverse coordinates.
+                # but prefer the first one if last == first
+                if pt != candidate.coords[0] and pt == candidate.coords[-1]:
+                    candidate.coords = list(candidate.coords)[::-1]
+
+                # Straight line from current_pt to pt.
+                # Is the toolpath inside the geometry?
+                walk_path = LineString([current_pt, pt])
+                walk_cut = walk_path.buffer(tooldia / 2, int(steps_per_circle / 4))
+
+                if walk_cut.within(boundary) and walk_path.length < max_walk:
+                    #log.debug("Walk to path #%d is inside. Joining." % path_count)
+
+                    # Completely inside. Append...
+                    geo.coords = list(geo.coords) + list(candidate.coords)
+                    # try:
+                    #     last = optimized_paths[-1]
+                    #     last.coords = list(last.coords) + list(geo.coords)
+                    # except IndexError:
+                    #     optimized_paths.append(geo)
+
+                else:
+
+                    # Have to lift tool. End path.
+                    #log.debug("Path #%d not within boundary. Next." % path_count)
+                    #optimized_paths.append(geo)
+                    optimized_paths.insert(geo)
+                    geo = candidate
+
+                current_pt = geo.coords[-1]
+
+                # Next
+                #pt, geo = storage.nearest(current_pt)
+
+        except StopIteration:  # Nothing left in storage.
+            #pass
+            optimized_paths.insert(geo)
+
+        return optimized_paths
+
+    @staticmethod
+    def path_connect(storage, origin=(0, 0)):
+        """
+        Simplifies paths in the FlatCAMRTreeStorage storage by
+        connecting paths that touch on their enpoints.
+
+        :param storage: Storage containing the initial paths.
+        :rtype storage: FlatCAMRTreeStorage
+        :return: Simplified storage.
+        :rtype: FlatCAMRTreeStorage
+        """
+
+        log.debug("path_connect()")
+
+        ## Index first and last points in paths
+        def get_pts(o):
+            return [o.coords[0], o.coords[-1]]
+        #
+        # storage = FlatCAMRTreeStorage()
+        # storage.get_points = get_pts
+        #
+        # for shape in pathlist:
+        #     if shape is not None:  # TODO: This shouldn't have happened.
+        #         storage.insert(shape)
+
+        path_count = 0
+        pt, geo = storage.nearest(origin)
+        storage.remove(geo)
+        #optimized_geometry = [geo]
+        optimized_geometry = FlatCAMRTreeStorage()
+        optimized_geometry.get_points = get_pts
+        #optimized_geometry.insert(geo)
+        try:
+            while True:
+                path_count += 1
+
+                #print "geo is", geo
+
+                _, left = storage.nearest(geo.coords[0])
+                #print "left is", left
+
+                # If left touches geo, remove left from original
+                # storage and append to geo.
+                if type(left) == LineString:
+                    if left.coords[0] == geo.coords[0]:
+                        storage.remove(left)
+                        geo.coords = list(geo.coords)[::-1] + list(left.coords)
+                        continue
+
+                    if left.coords[-1] == geo.coords[0]:
+                        storage.remove(left)
+                        geo.coords = list(left.coords) + list(geo.coords)
+                        continue
+
+                    if left.coords[0] == geo.coords[-1]:
+                        storage.remove(left)
+                        geo.coords = list(geo.coords) + list(left.coords)
+                        continue
+
+                    if left.coords[-1] == geo.coords[-1]:
+                        storage.remove(left)
+                        geo.coords = list(geo.coords) + list(left.coords)[::-1]
+                        continue
+
+                _, right = storage.nearest(geo.coords[-1])
+                #print "right is", right
+
+                # If right touches geo, remove left from original
+                # storage and append to geo.
+                if type(right) == LineString:
+                    if right.coords[0] == geo.coords[-1]:
+                        storage.remove(right)
+                        geo.coords = list(geo.coords) + list(right.coords)
+                        continue
+
+                    if right.coords[-1] == geo.coords[-1]:
+                        storage.remove(right)
+                        geo.coords = list(geo.coords) + list(right.coords)[::-1]
+                        continue
+
+                    if right.coords[0] == geo.coords[0]:
+                        storage.remove(right)
+                        geo.coords = list(geo.coords)[::-1] + list(right.coords)
+                        continue
+
+                    if right.coords[-1] == geo.coords[0]:
+                        storage.remove(right)
+                        geo.coords = list(left.coords) + list(geo.coords)
+                        continue
+
+                # right is either a LinearRing or it does not connect
+                # to geo (nothing left to connect to geo), so we continue
+                # with right as geo.
+                storage.remove(right)
+
+                if type(right) == LinearRing:
+                    optimized_geometry.insert(right)
+                else:
+                    # Cannot exteng geo any further. Put it away.
+                    optimized_geometry.insert(geo)
+
+                    # Continue with right.
+                    geo = right
+
+        except StopIteration:  # Nothing found in storage.
+            optimized_geometry.insert(geo)
+
+        #print path_count
+        log.debug("path_count = %d" % path_count)
+
+        return optimized_geometry
+
+    def convert_units(self, units):
+        """
+        Converts the units of the object to ``units`` by scaling all
+        the geometry appropriately. This call ``scale()``. Don't call
+        it again in descendents.
+
+        :param units: "IN" or "MM"
+        :type units: str
+        :return: Scaling factor resulting from unit change.
+        :rtype: float
+        """
+        log.debug("Geometry.convert_units()")
+
+        if units.upper() == self.units.upper():
+            return 1.0
+
+        if units.upper() == "MM":
+            factor = 25.4
+        elif units.upper() == "IN":
+            factor = 1 / 25.4
+        else:
+            log.error("Unsupported units: %s" % str(units))
+            return 1.0
+
+        self.units = units
+        self.scale(factor)
+        return factor
+
+    def to_dict(self):
+        """
+        Returns a respresentation of the object as a dictionary.
+        Attributes to include are listed in ``self.ser_attrs``.
+
+        :return: A dictionary-encoded copy of the object.
+        :rtype: dict
+        """
+        d = {}
+        for attr in self.ser_attrs:
+            d[attr] = getattr(self, attr)
+        return d
+
+    def from_dict(self, d):
+        """
+        Sets object's attributes from a dictionary.
+        Attributes to include are listed in ``self.ser_attrs``.
+        This method will look only for only and all the
+        attributes in ``self.ser_attrs``. They must all
+        be present. Use only for deserializing saved
+        objects.
+
+        :param d: Dictionary of attributes to set in the object.
+        :type d: dict
+        :return: None
+        """
+        for attr in self.ser_attrs:
+            setattr(self, attr, d[attr])
+
+    def union(self):
+        """
+        Runs a cascaded union on the list of objects in
+        solid_geometry.
+
+        :return: None
+        """
+        self.solid_geometry = [cascaded_union(self.solid_geometry)]
+
+    def export_svg(self, scale_factor=0.00):
+        """
+        Exports the Geometry Object as a SVG Element
+
+        :return: SVG Element
+        """
+
+        # Make sure we see a Shapely Geometry class and not a list
+
+        if str(type(self)) == "<class 'FlatCAMObj.FlatCAMGeometry'>":
+            flat_geo = []
+            if self.multigeo:
+                for tool in self.tools:
+                    flat_geo += self.flatten(self.tools[tool]['solid_geometry'])
+                geom = cascaded_union(flat_geo)
+            else:
+                geom = cascaded_union(self.flatten())
+        else:
+            geom = cascaded_union(self.flatten())
+
+        # scale_factor is a multiplication factor for the SVG stroke-width used within shapely's svg export
+
+        # If 0 or less which is invalid then default to 0.05
+        # This value appears to work for zooming, and getting the output svg line width
+        # to match that viewed on screen with FlatCam
+        # MS: I choose a factor of 0.01 so the scale is right for PCB UV film
+        if scale_factor <= 0:
+            scale_factor = 0.01
+
+        # Convert to a SVG
+        svg_elem = geom.svg(scale_factor=scale_factor)
+        return svg_elem
+
+    def mirror(self, axis, point):
+        """
+        Mirrors the object around a specified axis passign through
+        the given point.
+
+        :param axis: "X" or "Y" indicates around which axis to mirror.
+        :type axis: str
+        :param point: [x, y] point belonging to the mirror axis.
+        :type point: list
+        :return: None
+        """
+
+        px, py = point
+        xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
+
+        def mirror_geom(obj):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(mirror_geom(g))
+                return new_obj
+            else:
+                return affinity.scale(obj, xscale, yscale, origin=(px,py))
+
+        try:
+            self.solid_geometry = mirror_geom(self.solid_geometry)
+            self.app.inform.emit('[success]Object was mirrored ...')
+        except AttributeError:
+            self.app.inform.emit("[error_notcl] Failed to mirror. No object selected")
+
+    def rotate(self, angle, point):
+        """
+        Rotate an object by an angle (in degrees) around the provided coordinates.
+
+        Parameters
+        ----------
+        The angle of rotation are specified in degrees (default). Positive angles are
+        counter-clockwise and negative are clockwise rotations.
+
+        The point of origin can be a keyword 'center' for the bounding box
+        center (default), 'centroid' for the geometry's centroid, a Point object
+        or a coordinate tuple (x0, y0).
+
+        See shapely manual for more information:
+        http://toblerity.org/shapely/manual.html#affine-transformations
+        """
+
+        px, py = point
+
+        def rotate_geom(obj):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(rotate_geom(g))
+                return new_obj
+            else:
+                return affinity.rotate(obj, angle, origin=(px, py))
+
+        try:
+            self.solid_geometry = rotate_geom(self.solid_geometry)
+            self.app.inform.emit('[success]Object was rotated ...')
+        except AttributeError:
+            self.app.inform.emit("[error_notcl] Failed to rotate. No object selected")
+
+    def skew(self, angle_x, angle_y, point):
+        """
+        Shear/Skew the geometries of an object by angles along x and y dimensions.
+
+        Parameters
+        ----------
+        angle_x, angle_y : float, float
+            The shear angle(s) for the x and y axes respectively. These can be
+            specified in either degrees (default) or radians by setting
+            use_radians=True.
+        point: tuple of coordinates (x,y)
+
+        See shapely manual for more information:
+        http://toblerity.org/shapely/manual.html#affine-transformations
+        """
+        px, py = point
+
+        def skew_geom(obj):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(skew_geom(g))
+                return new_obj
+            else:
+                return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
+
+        try:
+            self.solid_geometry = skew_geom(self.solid_geometry)
+            self.app.inform.emit('[success]Object was skewed ...')
+        except AttributeError:
+            self.app.inform.emit("[error_notcl] Failed to skew. No object selected")
+
+        # if type(self.solid_geometry) == list:
+        #     self.solid_geometry = [affinity.skew(g, angle_x, angle_y, origin=(px, py))
+        #                            for g in self.solid_geometry]
+        # else:
+        #     self.solid_geometry = affinity.skew(self.solid_geometry, angle_x, angle_y,
+        #                                         origin=(px, py))
+
+
+class ApertureMacro:
+    """
+    Syntax of aperture macros.
+
+    <AM command>:           AM<Aperture macro name>*<Macro content>
+    <Macro content>:        {{<Variable definition>*}{<Primitive>*}}
+    <Variable definition>:  $K=<Arithmetic expression>
+    <Primitive>:            <Primitive code>,<Modifier>{,<Modifier>}|<Comment>
+    <Modifier>:             $M|< Arithmetic expression>
+    <Comment>:              0 <Text>
+    """
+
+    ## Regular expressions
+    am1_re = re.compile(r'^%AM([^\*]+)\*(.+)?(%)?$')
+    am2_re = re.compile(r'(.*)%$')
+    amcomm_re = re.compile(r'^0(.*)')
+    amprim_re = re.compile(r'^[1-9].*')
+    amvar_re = re.compile(r'^\$([0-9a-zA-z]+)=(.*)')
+
+    def __init__(self, name=None):
+        self.name = name
+        self.raw = ""
+
+        ## These below are recomputed for every aperture
+        ## definition, in other words, are temporary variables.
+        self.primitives = []
+        self.locvars = {}
+        self.geometry = None
+
+    def to_dict(self):
+        """
+        Returns the object in a serializable form. Only the name and
+        raw are required.
+
+        :return: Dictionary representing the object. JSON ready.
+        :rtype: dict
+        """
+
+        return {
+            'name': self.name,
+            'raw': self.raw
+        }
+
+    def from_dict(self, d):
+        """
+        Populates the object from a serial representation created
+        with ``self.to_dict()``.
+
+        :param d: Serial representation of an ApertureMacro object.
+        :return: None
+        """
+        for attr in ['name', 'raw']:
+            setattr(self, attr, d[attr])
+
+    def parse_content(self):
+        """
+        Creates numerical lists for all primitives in the aperture
+        macro (in ``self.raw``) by replacing all variables by their
+        values iteratively and evaluating expressions. Results
+        are stored in ``self.primitives``.
+
+        :return: None
+        """
+        # Cleanup
+        self.raw = self.raw.replace('\n', '').replace('\r', '').strip(" *")
+        self.primitives = []
+
+        # Separate parts
+        parts = self.raw.split('*')
+
+        #### Every part in the macro ####
+        for part in parts:
+            ### Comments. Ignored.
+            match = ApertureMacro.amcomm_re.search(part)
+            if match:
+                continue
+
+            ### Variables
+            # These are variables defined locally inside the macro. They can be
+            # numerical constant or defind in terms of previously define
+            # variables, which can be defined locally or in an aperture
+            # definition. All replacements ocurr here.
+            match = ApertureMacro.amvar_re.search(part)
+            if match:
+                var = match.group(1)
+                val = match.group(2)
+
+                # Replace variables in value
+                for v in self.locvars:
+                    # replaced the following line with the next to fix Mentor custom apertures not parsed OK
+                    # val = re.sub((r'\$'+str(v)+r'(?![0-9a-zA-Z])'), str(self.locvars[v]), val)
+                    val = val.replace('$' + str(v), str(self.locvars[v]))
+
+                # Make all others 0
+                val = re.sub(r'\$[0-9a-zA-Z](?![0-9a-zA-Z])', "0", val)
+                # Change x with *
+                val = re.sub(r'[xX]', "*", val)
+
+                # Eval() and store.
+                self.locvars[var] = eval(val)
+                continue
+
+            ### Primitives
+            # Each is an array. The first identifies the primitive, while the
+            # rest depend on the primitive. All are strings representing a
+            # number and may contain variable definition. The values of these
+            # variables are defined in an aperture definition.
+            match = ApertureMacro.amprim_re.search(part)
+            if match:
+                ## Replace all variables
+                for v in self.locvars:
+                    # replaced the following line with the next to fix Mentor custom apertures not parsed OK
+                    # part = re.sub(r'\$' + str(v) + r'(?![0-9a-zA-Z])', str(self.locvars[v]), part)
+                    part = part.replace('$' + str(v), str(self.locvars[v]))
+
+                # Make all others 0
+                part = re.sub(r'\$[0-9a-zA-Z](?![0-9a-zA-Z])', "0", part)
+
+                # Change x with *
+                part = re.sub(r'[xX]', "*", part)
+
+                ## Store
+                elements = part.split(",")
+                self.primitives.append([eval(x) for x in elements])
+                continue
+
+            log.warning("Unknown syntax of aperture macro part: %s" % str(part))
+
+    def append(self, data):
+        """
+        Appends a string to the raw macro.
+
+        :param data: Part of the macro.
+        :type data: str
+        :return: None
+        """
+        self.raw += data
+
+    @staticmethod
+    def default2zero(n, mods):
+        """
+        Pads the ``mods`` list with zeros resulting in an
+        list of length n.
+
+        :param n: Length of the resulting list.
+        :type n: int
+        :param mods: List to be padded.
+        :type mods: list
+        :return: Zero-padded list.
+        :rtype: list
+        """
+        x = [0.0] * n
+        na = len(mods)
+        x[0:na] = mods
+        return x
+
+    @staticmethod
+    def make_circle(mods):
+        """
+
+        :param mods: (Exposure 0/1, Diameter >=0, X-coord, Y-coord)
+        :return:
+        """
+
+        pol, dia, x, y = ApertureMacro.default2zero(4, mods)
+
+        return {"pol": int(pol), "geometry": Point(x, y).buffer(dia/2)}
+
+    @staticmethod
+    def make_vectorline(mods):
+        """
+
+        :param mods: (Exposure 0/1, Line width >= 0, X-start, Y-start, X-end, Y-end,
+            rotation angle around origin in degrees)
+        :return:
+        """
+        pol, width, xs, ys, xe, ye, angle = ApertureMacro.default2zero(7, mods)
+
+        line = LineString([(xs, ys), (xe, ye)])
+        box = line.buffer(width/2, cap_style=2)
+        box_rotated = affinity.rotate(box, angle, origin=(0, 0))
+
+        return {"pol": int(pol), "geometry": box_rotated}
+
+    @staticmethod
+    def make_centerline(mods):
+        """
+
+        :param mods: (Exposure 0/1, width >=0, height >=0, x-center, y-center,
+            rotation angle around origin in degrees)
+        :return:
+        """
+
+        pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
+
+        box = shply_box(x-width/2, y-height/2, x+width/2, y+height/2)
+        box_rotated = affinity.rotate(box, angle, origin=(0, 0))
+
+        return {"pol": int(pol), "geometry": box_rotated}
+
+    @staticmethod
+    def make_lowerleftline(mods):
+        """
+
+        :param mods: (exposure 0/1, width >=0, height >=0, x-lowerleft, y-lowerleft,
+            rotation angle around origin in degrees)
+        :return:
+        """
+
+        pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
+
+        box = shply_box(x, y, x+width, y+height)
+        box_rotated = affinity.rotate(box, angle, origin=(0, 0))
+
+        return {"pol": int(pol), "geometry": box_rotated}
+
+    @staticmethod
+    def make_outline(mods):
+        """
+
+        :param mods:
+        :return:
+        """
+
+        pol = mods[0]
+        n = mods[1]
+        points = [(0, 0)]*(n+1)
+
+        for i in range(n+1):
+            points[i] = mods[2*i + 2:2*i + 4]
+
+        angle = mods[2*n + 4]
+
+        poly = Polygon(points)
+        poly_rotated = affinity.rotate(poly, angle, origin=(0, 0))
+
+        return {"pol": int(pol), "geometry": poly_rotated}
+
+    @staticmethod
+    def make_polygon(mods):
+        """
+        Note: Specs indicate that rotation is only allowed if the center
+        (x, y) == (0, 0). I will tolerate breaking this rule.
+
+        :param mods: (exposure 0/1, n_verts 3<=n<=12, x-center, y-center,
+            diameter of circumscribed circle >=0, rotation angle around origin)
+        :return:
+        """
+
+        pol, nverts, x, y, dia, angle = ApertureMacro.default2zero(6, mods)
+        points = [(0, 0)]*nverts
+
+        for i in range(nverts):
+            points[i] = (x + 0.5 * dia * cos(2*pi * i/nverts),
+                         y + 0.5 * dia * sin(2*pi * i/nverts))
+
+        poly = Polygon(points)
+        poly_rotated = affinity.rotate(poly, angle, origin=(0, 0))
+
+        return {"pol": int(pol), "geometry": poly_rotated}
+
+    @staticmethod
+    def make_moire(mods):
+        """
+        Note: Specs indicate that rotation is only allowed if the center
+        (x, y) == (0, 0). I will tolerate breaking this rule.
+
+        :param mods: (x-center, y-center, outer_dia_outer_ring, ring thickness,
+            gap, max_rings, crosshair_thickness, crosshair_len, rotation
+            angle around origin in degrees)
+        :return:
+        """
+
+        x, y, dia, thickness, gap, nrings, cross_th, cross_len, angle = ApertureMacro.default2zero(9, mods)
+
+        r = dia/2 - thickness/2
+        result = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0)
+        ring = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0)  # Need a copy!
+
+        i = 1  # Number of rings created so far
+
+        ## If the ring does not have an interior it means that it is
+        ## a disk. Then stop.
+        while len(ring.interiors) > 0 and i < nrings:
+            r -= thickness + gap
+            if r <= 0:
+                break
+            ring = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0)
+            result = cascaded_union([result, ring])
+            i += 1
+
+        ## Crosshair
+        hor = LineString([(x - cross_len, y), (x + cross_len, y)]).buffer(cross_th/2.0, cap_style=2)
+        ver = LineString([(x, y-cross_len), (x, y + cross_len)]).buffer(cross_th/2.0, cap_style=2)
+        result = cascaded_union([result, hor, ver])
+
+        return {"pol": 1, "geometry": result}
+
+    @staticmethod
+    def make_thermal(mods):
+        """
+        Note: Specs indicate that rotation is only allowed if the center
+        (x, y) == (0, 0). I will tolerate breaking this rule.
+
+        :param mods: [x-center, y-center, diameter-outside, diameter-inside,
+            gap-thickness, rotation angle around origin]
+        :return:
+        """
+
+        x, y, dout, din, t, angle = ApertureMacro.default2zero(6, mods)
+
+        ring = Point((x, y)).buffer(dout/2.0).difference(Point((x, y)).buffer(din/2.0))
+        hline = LineString([(x - dout/2.0, y), (x + dout/2.0, y)]).buffer(t/2.0, cap_style=3)
+        vline = LineString([(x, y - dout/2.0), (x, y + dout/2.0)]).buffer(t/2.0, cap_style=3)
+        thermal = ring.difference(hline.union(vline))
+
+        return {"pol": 1, "geometry": thermal}
+
+    def make_geometry(self, modifiers):
+        """
+        Runs the macro for the given modifiers and generates
+        the corresponding geometry.
+
+        :param modifiers: Modifiers (parameters) for this macro
+        :type modifiers: list
+        :return: Shapely geometry
+        :rtype: shapely.geometry.polygon
+        """
+
+        ## Primitive makers
+        makers = {
+            "1": ApertureMacro.make_circle,
+            "2": ApertureMacro.make_vectorline,
+            "20": ApertureMacro.make_vectorline,
+            "21": ApertureMacro.make_centerline,
+            "22": ApertureMacro.make_lowerleftline,
+            "4": ApertureMacro.make_outline,
+            "5": ApertureMacro.make_polygon,
+            "6": ApertureMacro.make_moire,
+            "7": ApertureMacro.make_thermal
+        }
+
+        ## Store modifiers as local variables
+        modifiers = modifiers or []
+        modifiers = [float(m) for m in modifiers]
+        self.locvars = {}
+        for i in range(0, len(modifiers)):
+            self.locvars[str(i + 1)] = modifiers[i]
+
+        ## Parse
+        self.primitives = []  # Cleanup
+        self.geometry = Polygon()
+        self.parse_content()
+
+        ## Make the geometry
+        for primitive in self.primitives:
+            # Make the primitive
+            prim_geo = makers[str(int(primitive[0]))](primitive[1:])
+
+            # Add it (according to polarity)
+            # if self.geometry is None and prim_geo['pol'] == 1:
+            #     self.geometry = prim_geo['geometry']
+            #     continue
+            if prim_geo['pol'] == 1:
+                self.geometry = self.geometry.union(prim_geo['geometry'])
+                continue
+            if prim_geo['pol'] == 0:
+                self.geometry = self.geometry.difference(prim_geo['geometry'])
+                continue
+
+        return self.geometry
+
+
+class Gerber (Geometry):
+    """
+    **ATTRIBUTES**
+
+    * ``apertures`` (dict): The keys are names/identifiers of each aperture.
+      The values are dictionaries key/value pairs which describe the aperture. The
+      type key is always present and the rest depend on the key:
+
+    +-----------+-----------------------------------+
+    | Key       | Value                             |
+    +===========+===================================+
+    | type      | (str) "C", "R", "O", "P", or "AP" |
+    +-----------+-----------------------------------+
+    | others    | Depend on ``type``                |
+    +-----------+-----------------------------------+
+
+    * ``aperture_macros`` (dictionary): Are predefined geometrical structures
+      that can be instanciated with different parameters in an aperture
+      definition. See ``apertures`` above. The key is the name of the macro,
+      and the macro itself, the value, is a ``Aperture_Macro`` object.
+
+    * ``flash_geometry`` (list): List of (Shapely) geometric object resulting
+      from ``flashes``. These are generated from ``flashes`` in ``do_flashes()``.
+
+    * ``buffered_paths`` (list): List of (Shapely) polygons resulting from
+      *buffering* (or thickening) the ``paths`` with the aperture. These are
+      generated from ``paths`` in ``buffer_paths()``.
+
+    **USAGE**::
+
+        g = Gerber()
+        g.parse_file(filename)
+        g.create_geometry()
+        do_something(s.solid_geometry)
+
+    """
+
+    defaults = {
+        "steps_per_circle": 56,
+        "use_buffer_for_union": True
+    }
+
+    def __init__(self, steps_per_circle=None):
+        """
+        The constructor takes no parameters. Use ``gerber.parse_files()``
+        or ``gerber.parse_lines()`` to populate the object from Gerber source.
+
+        :return: Gerber object
+        :rtype: Gerber
+        """
+
+        # How to discretize a circle.
+        if steps_per_circle is None:
+            steps_per_circle = Gerber.defaults['steps_per_circle']
+        self.steps_per_circle = steps_per_circle
+
+        # Initialize parent
+        Geometry.__init__(self, geo_steps_per_circle=steps_per_circle)
+
+        self.solid_geometry = Polygon()
+
+        # Number format
+        self.int_digits = 3
+        """Number of integer digits in Gerber numbers. Used during parsing."""
+
+        self.frac_digits = 4
+        """Number of fraction digits in Gerber numbers. Used during parsing."""
+
+        self.gerber_zeros = 'L'
+        """Zeros in Gerber numbers. If 'L' then remove leading zeros, if 'T' remove trailing zeros. Used during parsing.
+        """
+        
+        ## Gerber elements ##
+        # Apertures {'id':{'type':chr, 
+        #             ['size':float], ['width':float],
+        #             ['height':float]}, ...}
+        self.apertures = {}
+
+        # Aperture Macros
+        self.aperture_macros = {}
+
+        # Attributes to be included in serialization
+        # Always append to it because it carries contents
+        # from Geometry.
+        self.ser_attrs += ['int_digits', 'frac_digits', 'apertures',
+                           'aperture_macros', 'solid_geometry']
+
+        #### Parser patterns ####
+        # FS - Format Specification
+        # The format of X and Y must be the same!
+        # L-omit leading zeros, T-omit trailing zeros
+        # A-absolute notation, I-incremental notation
+        self.fmt_re = re.compile(r'%FS([LT])([AI])X(\d)(\d)Y\d\d\*%$')
+        self.fmt_re_alt = re.compile(r'%FS([LT])([AI])X(\d)(\d)Y\d\d\*MO(IN|MM)\*%$')
+        self.fmt_re_orcad = re.compile(r'(G\d+)*\**%FS([LT])([AI]).*X(\d)(\d)Y\d\d\*%$')
+
+        # Mode (IN/MM)
+        self.mode_re = re.compile(r'^%MO(IN|MM)\*%$')
+
+        # Comment G04|G4
+        self.comm_re = re.compile(r'^G0?4(.*)$')
+
+        # AD - Aperture definition
+        # Aperture Macro names: Name = [a-zA-Z_.$]{[a-zA-Z_.0-9]+}
+        # NOTE: Adding "-" to support output from Upverter.
+        self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z_$\.][a-zA-Z0-9_$\.\-]*)(?:,(.*))?\*%$')
+
+        # AM - Aperture Macro
+        # Beginning of macro (Ends with *%):
+        #self.am_re = re.compile(r'^%AM([a-zA-Z0-9]*)\*')
+
+        # Tool change
+        # May begin with G54 but that is deprecated
+        self.tool_re = re.compile(r'^(?:G54)?D(\d\d+)\*$')
+
+        # G01... - Linear interpolation plus flashes with coordinates
+        # Operation code (D0x) missing is deprecated... oh well I will support it.
+        self.lin_re = re.compile(r'^(?:G0?(1))?(?=.*X([\+-]?\d+))?(?=.*Y([\+-]?\d+))?[XY][^DIJ]*(?:D0?([123]))?\*$')
+
+        # Operation code alone, usually just D03 (Flash)
+        self.opcode_re = re.compile(r'^D0?([123])\*$')
+
+        # G02/3... - Circular interpolation with coordinates
+        # 2-clockwise, 3-counterclockwise
+        # Operation code (D0x) missing is deprecated... oh well I will support it.
+        # Optional start with G02 or G03, optional end with D01 or D02 with
+        # optional coordinates but at least one in any order.
+        self.circ_re = re.compile(r'^(?:G0?([23]))?(?=.*X([\+-]?\d+))?(?=.*Y([\+-]?\d+))' +
+                                  '?(?=.*I([\+-]?\d+))?(?=.*J([\+-]?\d+))?[XYIJ][^D]*(?:D0([12]))?\*$')
+
+        # G01/2/3 Occurring without coordinates
+        self.interp_re = re.compile(r'^(?:G0?([123]))\*')
+
+        # Single G74 or multi G75 quadrant for circular interpolation
+        self.quad_re = re.compile(r'^G7([45]).*\*$')
+
+        # Region mode on
+        # In region mode, D01 starts a region
+        # and D02 ends it. A new region can be started again
+        # with D01. All contours must be closed before
+        # D02 or G37.
+        self.regionon_re = re.compile(r'^G36\*$')
+
+        # Region mode off
+        # Will end a region and come off region mode.
+        # All contours must be closed before D02 or G37.
+        self.regionoff_re = re.compile(r'^G37\*$')
+
+        # End of file
+        self.eof_re = re.compile(r'^M02\*')
+
+        # IP - Image polarity
+        self.pol_re = re.compile(r'^%IP(POS|NEG)\*%$')
+
+        # LP - Level polarity
+        self.lpol_re = re.compile(r'^%LP([DC])\*%$')
+
+        # Units (OBSOLETE)
+        self.units_re = re.compile(r'^G7([01])\*$')
+
+        # Absolute/Relative G90/1 (OBSOLETE)
+        self.absrel_re = re.compile(r'^G9([01])\*$')
+
+        # Aperture macros
+        self.am1_re = re.compile(r'^%AM([^\*]+)\*([^%]+)?(%)?$')
+        self.am2_re = re.compile(r'(.*)%$')
+
+        self.use_buffer_for_union = self.defaults["use_buffer_for_union"]
+
+    def aperture_parse(self, apertureId, apertureType, apParameters):
+        """
+        Parse gerber aperture definition into dictionary of apertures.
+        The following kinds and their attributes are supported:
+
+        * *Circular (C)*: size (float)
+        * *Rectangle (R)*: width (float), height (float)
+        * *Obround (O)*: width (float), height (float).
+        * *Polygon (P)*: diameter(float), vertices(int), [rotation(float)]
+        * *Aperture Macro (AM)*: macro (ApertureMacro), modifiers (list)
+
+        :param apertureId: Id of the aperture being defined.
+        :param apertureType: Type of the aperture.
+        :param apParameters: Parameters of the aperture.
+        :type apertureId: str
+        :type apertureType: str
+        :type apParameters: str
+        :return: Identifier of the aperture.
+        :rtype: str
+        """
+
+        # Found some Gerber with a leading zero in the aperture id and the
+        # referenced it without the zero, so this is a hack to handle that.
+        apid = str(int(apertureId))
+
+        try:  # Could be empty for aperture macros
+            paramList = apParameters.split('X')
+        except:
+            paramList = None
+
+        if apertureType == "C":  # Circle, example: %ADD11C,0.1*%
+            self.apertures[apid] = {"type": "C",
+                                    "size": float(paramList[0])}
+            return apid
+        
+        if apertureType == "R":  # Rectangle, example: %ADD15R,0.05X0.12*%
+            self.apertures[apid] = {"type": "R",
+                                    "width": float(paramList[0]),
+                                    "height": float(paramList[1]),
+                                    "size": sqrt(float(paramList[0])**2 + float(paramList[1])**2)}  # Hack
+            return apid
+
+        if apertureType == "O":  # Obround
+            self.apertures[apid] = {"type": "O",
+                                    "width": float(paramList[0]),
+                                    "height": float(paramList[1]),
+                                    "size": sqrt(float(paramList[0])**2 + float(paramList[1])**2)}  # Hack
+            return apid
+        
+        if apertureType == "P":  # Polygon (regular)
+            self.apertures[apid] = {"type": "P",
+                                    "diam": float(paramList[0]),
+                                    "nVertices": int(paramList[1]),
+                                    "size": float(paramList[0])}  # Hack
+            if len(paramList) >= 3:
+                self.apertures[apid]["rotation"] = float(paramList[2])
+            return apid
+
+        if apertureType in self.aperture_macros:
+            self.apertures[apid] = {"type": "AM",
+                                    "macro": self.aperture_macros[apertureType],
+                                    "modifiers": paramList}
+            return apid
+
+        log.warning("Aperture not implemented: %s" % str(apertureType))
+        return None
+        
+    def parse_file(self, filename, follow=False):
+        """
+        Calls Gerber.parse_lines() with generator of lines
+        read from the given file. Will split the lines if multiple
+        statements are found in a single original line.
+
+        The following line is split into two::
+
+            G54D11*G36*
+
+        First is ``G54D11*`` and seconds is ``G36*``.
+
+        :param filename: Gerber file to parse.
+        :type filename: str
+        :param follow: If true, will not create polygons, just lines
+            following the gerber path.
+        :type follow: bool
+        :return: None
+        """
+
+        with open(filename, 'r') as gfile:
+
+            def line_generator():
+                for line in gfile:
+                    line = line.strip(' \r\n')
+                    while len(line) > 0:
+
+                        # If ends with '%' leave as is.
+                        if line[-1] == '%':
+                            yield line
+                            break
+
+                        # Split after '*' if any.
+                        starpos = line.find('*')
+                        if starpos > -1:
+                            cleanline = line[:starpos + 1]
+                            yield cleanline
+                            line = line[starpos + 1:]
+
+                        # Otherwise leave as is.
+                        else:
+                            # yield cleanline
+                            yield line
+                            break
+
+            self.parse_lines(line_generator(), follow=follow)
+
+    #@profile
+    def parse_lines(self, glines, follow=False):
+        """
+        Main Gerber parser. Reads Gerber and populates ``self.paths``, ``self.apertures``,
+        ``self.flashes``, ``self.regions`` and ``self.units``.
+
+        :param glines: Gerber code as list of strings, each element being
+            one line of the source file.
+        :type glines: list
+        :param follow: If true, will not create polygons, just lines
+            following the gerber path.
+        :type follow: bool
+        :return: None
+        :rtype: None
+        """
+
+        # Coordinates of the current path, each is [x, y]
+        path = []
+
+        # this is for temporary storage of geometry until it is added to poly_buffer
+        geo = None
+
+        # Polygons are stored here until there is a change in polarity.
+        # Only then they are combined via cascaded_union and added or
+        # subtracted from solid_geometry. This is ~100 times faster than
+        # applying a union for every new polygon.
+        poly_buffer = []
+
+        last_path_aperture = None
+        current_aperture = None
+
+        # 1,2 or 3 from "G01", "G02" or "G03"
+        current_interpolation_mode = None
+
+        # 1 or 2 from "D01" or "D02"
+        # Note this is to support deprecated Gerber not putting
+        # an operation code at the end of every coordinate line.
+        current_operation_code = None
+
+        # Current coordinates
+        current_x = None
+        current_y = None
+        previous_x = None
+        previous_y = None
+
+        current_d = None
+
+        # Absolute or Relative/Incremental coordinates
+        # Not implemented
+        absolute = True
+
+        # How to interpret circular interpolation: SINGLE or MULTI
+        quadrant_mode = None
+
+        # Indicates we are parsing an aperture macro
+        current_macro = None
+
+        # Indicates the current polarity: D-Dark, C-Clear
+        current_polarity = 'D'
+
+        # If a region is being defined
+        making_region = False
+
+        #### Parsing starts here ####
+        line_num = 0
+        gline = ""
+        try:
+            for gline in glines:
+                line_num += 1
+
+                ### Cleanup
+                gline = gline.strip(' \r\n')
+                # log.debug("Line=%3s %s" % (line_num, gline))
+
+                #### Ignored lines
+                ## Comments
+                match = self.comm_re.search(gline)
+                if match:
+                    continue
+
+                ### Polarity change
+                # Example: %LPD*% or %LPC*%
+                # If polarity changes, creates geometry from current
+                # buffer, then adds or subtracts accordingly.
+                match = self.lpol_re.search(gline)
+                if match:
+                    if len(path) > 1 and current_polarity != match.group(1):
+
+                        # --- Buffered ----
+                        width = self.apertures[last_path_aperture]["size"]
+
+                        if follow:
+                            geo = LineString(path)
+                        else:
+                            geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                        if not geo.is_empty:
+                            poly_buffer.append(geo)
+
+                        path = [path[-1]]
+
+                    # --- Apply buffer ---
+                    # If added for testing of bug #83
+                    # TODO: Remove when bug fixed
+                    if len(poly_buffer) > 0:
+                        if current_polarity == 'D':
+                            self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer))
+                        else:
+                            self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer))
+                        poly_buffer = []
+
+                    current_polarity = match.group(1)
+                    continue
+
+                ### Number format
+                # Example: %FSLAX24Y24*%
+                # TODO: This is ignoring most of the format. Implement the rest.
+                match = self.fmt_re.search(gline)
+                if match:
+                    absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(2)]
+                    self.gerber_zeros = match.group(1)
+                    self.int_digits = int(match.group(3))
+                    self.frac_digits = int(match.group(4))
+                    log.debug("Gerber format found. (%s) " % str(gline))
+
+                    log.debug(
+                        "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros)" %
+                        self.gerber_zeros)
+                    log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute)
+                    continue
+
+                ### Mode (IN/MM)
+                # Example: %MOIN*%
+                match = self.mode_re.search(gline)
+                if match:
+                    gerber_units = match.group(1)
+                    log.debug("Gerber units found = %s" % gerber_units)
+                    # Changed for issue #80
+                    self.convert_units(match.group(1))
+                    continue
+
+                ### Combined Number format and Mode --- Allegro does this
+                match = self.fmt_re_alt.search(gline)
+                if match:
+                    absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(2)]
+                    self.gerber_zeros = match.group(1)
+                    self.int_digits = int(match.group(3))
+                    self.frac_digits = int(match.group(4))
+                    log.debug("Gerber format found. (%s) " % str(gline))
+                    log.debug(
+                        "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros)" %
+                        self.gerber_zeros)
+                    log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute)
+
+                    gerber_units = match.group(1)
+                    log.debug("Gerber units found = %s" % gerber_units)
+                    # Changed for issue #80
+                    self.convert_units(match.group(5))
+                    continue
+
+                ### Search for OrCAD way for having Number format
+                match = self.fmt_re_orcad.search(gline)
+                if match:
+                    if match.group(1) is not None:
+                        if match.group(1) == 'G74':
+                            quadrant_mode = 'SINGLE'
+                        elif match.group(1) == 'G75':
+                            quadrant_mode = 'MULTI'
+                        absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(3)]
+                        self.gerber_zeros = match.group(2)
+                        self.int_digits = int(match.group(4))
+                        self.frac_digits = int(match.group(5))
+                        log.debug("Gerber format found. (%s) " % str(gline))
+                        log.debug(
+                            "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros)" %
+                            self.gerber_zeros)
+                        log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute)
+
+                        gerber_units = match.group(1)
+                        log.debug("Gerber units found = %s" % gerber_units)
+                        # Changed for issue #80
+                        self.convert_units(match.group(5))
+                        continue
+
+                ### Units (G70/1) OBSOLETE
+                match = self.units_re.search(gline)
+                if match:
+                    obs_gerber_units = {'0': 'IN', '1': 'MM'}[match.group(1)]
+                    log.warning("Gerber obsolete units found = %s" % obs_gerber_units)
+                    # Changed for issue #80
+                    self.convert_units({'0': 'IN', '1': 'MM'}[match.group(1)])
+                    continue
+
+                ### Absolute/relative coordinates G90/1 OBSOLETE
+                match = self.absrel_re.search(gline)
+                if match:
+                    absolute = {'0': "Absolute", '1': "Relative"}[match.group(1)]
+                    log.warning("Gerber obsolete coordinates type found = %s (Absolute or Relative) " % absolute)
+                    continue
+
+                ### Aperture Macros
+                # Having this at the beginning will slow things down
+                # but macros can have complicated statements than could
+                # be caught by other patterns.
+                if current_macro is None:  # No macro started yet
+                    match = self.am1_re.search(gline)
+                    # Start macro if match, else not an AM, carry on.
+                    if match:
+                        log.debug("Starting macro. Line %d: %s" % (line_num, gline))
+                        current_macro = match.group(1)
+                        self.aperture_macros[current_macro] = ApertureMacro(name=current_macro)
+                        if match.group(2):  # Append
+                            self.aperture_macros[current_macro].append(match.group(2))
+                        if match.group(3):  # Finish macro
+                            #self.aperture_macros[current_macro].parse_content()
+                            current_macro = None
+                            log.debug("Macro complete in 1 line.")
+                        continue
+                else:  # Continue macro
+                    log.debug("Continuing macro. Line %d." % line_num)
+                    match = self.am2_re.search(gline)
+                    if match:  # Finish macro
+                        log.debug("End of macro. Line %d." % line_num)
+                        self.aperture_macros[current_macro].append(match.group(1))
+                        #self.aperture_macros[current_macro].parse_content()
+                        current_macro = None
+                    else:  # Append
+                        self.aperture_macros[current_macro].append(gline)
+                    continue
+
+                ### Aperture definitions %ADD...
+                match = self.ad_re.search(gline)
+                if match:
+                    log.info("Found aperture definition. Line %d: %s" % (line_num, gline))
+                    self.aperture_parse(match.group(1), match.group(2), match.group(3))
+                    continue
+
+                ### Operation code alone
+                # Operation code alone, usually just D03 (Flash)
+                # self.opcode_re = re.compile(r'^D0?([123])\*$')
+                match = self.opcode_re.search(gline)
+                if match:
+                    current_operation_code = int(match.group(1))
+                    current_d = current_operation_code
+
+                    if current_operation_code == 3:
+
+                        ## --- Buffered ---
+                        try:
+                            log.debug("Bare op-code %d." % current_operation_code)
+                            # flash = Gerber.create_flash_geometry(Point(path[-1]),
+                            #                                      self.apertures[current_aperture])
+                            if follow:
+                                continue
+                            flash = Gerber.create_flash_geometry(
+                                Point(current_x, current_y), self.apertures[current_aperture],
+                                int(self.steps_per_circle))
+                            if not flash.is_empty:
+                                poly_buffer.append(flash)
+                        except IndexError:
+                            log.warning("Line %d: %s -> Nothing there to flash!" % (line_num, gline))
+
+                    continue
+
+                ### Tool/aperture change
+                # Example: D12*
+                match = self.tool_re.search(gline)
+                if match:
+                    current_aperture = match.group(1)
+                    log.debug("Line %d: Aperture change to (%s)" % (line_num, match.group(1)))
+
+                    # If the aperture value is zero then make it something quite small but with a non-zero value
+                    # so it can be processed by FlatCAM.
+                    # But first test to see if the aperture type is "aperture macro". In that case
+                    # we should not test for "size" key as it does not exist in this case.
+                    if self.apertures[current_aperture]["type"] is not "AM":
+                        if self.apertures[current_aperture]["size"] == 0:
+                            self.apertures[current_aperture]["size"] = 1e-12
+                    log.debug(self.apertures[current_aperture])
+
+                    # Take care of the current path with the previous tool
+                    if len(path) > 1:
+                        if self.apertures[last_path_aperture]["type"] == 'R':
+                            # do nothing because 'R' type moving aperture is none at once
+                            pass
+                        else:
+                            # --- Buffered ----
+                            width = self.apertures[last_path_aperture]["size"]
+
+                            if follow:
+                                geo = LineString(path)
+                            else:
+                                geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                            if not geo.is_empty:
+                                poly_buffer.append(geo)
+
+                            path = [path[-1]]
+
+                    continue
+
+                ### G36* - Begin region
+                if self.regionon_re.search(gline):
+                    if len(path) > 1:
+                        # Take care of what is left in the path
+
+                        ## --- Buffered ---
+                        width = self.apertures[last_path_aperture]["size"]
+
+                        if follow:
+                            geo = LineString(path)
+                        else:
+                            geo = LineString(path).buffer(width/1.999, int(self.steps_per_circle / 4))
+                        if not geo.is_empty:
+                            poly_buffer.append(geo)
+
+                        path = [path[-1]]
+
+                    making_region = True
+                    continue
+
+                ### G37* - End region
+                if self.regionoff_re.search(gline):
+                    making_region = False
+
+                    # if D02 happened before G37 we now have a path with 1 element only so we have to add the current
+                    # geo to the poly_buffer otherwise we loose it
+                    if current_operation_code == 2:
+                        if geo:
+                            if not geo.is_empty:
+                                poly_buffer.append(geo)
+                            continue
+
+                    # Only one path defines region?
+                    # This can happen if D02 happened before G37 and
+                    # is not and error.
+                    if len(path) < 3:
+                        # print "ERROR: Path contains less than 3 points:"
+                        # print path
+                        # print "Line (%d): " % line_num, gline
+                        # path = []
+                        #path = [[current_x, current_y]]
+                        continue
+
+                    # For regions we may ignore an aperture that is None
+                    # self.regions.append({"polygon": Polygon(path),
+                    #                      "aperture": last_path_aperture})
+
+                    # --- Buffered ---
+                    if follow:
+                        region = Polygon()
+                    else:
+                        region = Polygon(path)
+                    if not region.is_valid:
+                        if not follow:
+                            region = region.buffer(0, int(self.steps_per_circle / 4))
+                    if not region.is_empty:
+                        poly_buffer.append(region)
+
+                    path = [[current_x, current_y]]  # Start new path
+                    continue
+
+                ### G01/2/3* - Interpolation mode change
+                # Can occur along with coordinates and operation code but
+                # sometimes by itself (handled here).
+                # Example: G01*
+                match = self.interp_re.search(gline)
+                if match:
+                    current_interpolation_mode = int(match.group(1))
+                    continue
+
+                ### G01 - Linear interpolation plus flashes
+                # Operation code (D0x) missing is deprecated... oh well I will support it.
+                # REGEX: r'^(?:G0?(1))?(?:X(-?\d+))?(?:Y(-?\d+))?(?:D0([123]))?\*$'
+                match = self.lin_re.search(gline)
+                if match:
+                    # Dxx alone?
+                    # if match.group(1) is None and match.group(2) is None and match.group(3) is None:
+                    #     try:
+                    #         current_operation_code = int(match.group(4))
+                    #     except:
+                    #         pass  # A line with just * will match too.
+                    #     continue
+                    # NOTE: Letting it continue allows it to react to the
+                    #       operation code.
+
+                    # Parse coordinates
+                    if match.group(2) is not None:
+                        linear_x = parse_gerber_number(match.group(2),
+                                                       self.int_digits, self.frac_digits, self.gerber_zeros)
+                        current_x = linear_x
+                    else:
+                        linear_x = current_x
+                    if match.group(3) is not None:
+                        linear_y = parse_gerber_number(match.group(3),
+                                                       self.int_digits, self.frac_digits, self.gerber_zeros)
+                        current_y = linear_y
+                    else:
+                        linear_y = current_y
+
+                    # Parse operation code
+                    if match.group(4) is not None:
+                        current_operation_code = int(match.group(4))
+
+                    # Pen down: add segment
+                    if current_operation_code == 1:
+                        # if linear_x or linear_y are None, ignore those
+                        if linear_x is not None and linear_y is not None:
+                            # only add the point if it's a new one otherwise skip it (harder to process)
+                            if path[-1] != [linear_x, linear_y]:
+                                path.append([linear_x, linear_y])
+
+                            if follow == 0 and making_region is False:
+                                # if the aperture is rectangle then add a rectangular shape having as parameters the
+                                # coordinates of the start and end point and also the width and height
+                                # of the 'R' aperture
+                                try:
+                                    if self.apertures[current_aperture]["type"] == 'R':
+                                        width = self.apertures[current_aperture]['width']
+                                        height = self.apertures[current_aperture]['height']
+                                        minx = min(path[0][0], path[1][0]) - width / 2
+                                        maxx = max(path[0][0], path[1][0]) + width / 2
+                                        miny = min(path[0][1], path[1][1]) - height / 2
+                                        maxy = max(path[0][1], path[1][1]) + height / 2
+                                        log.debug("Coords: %s - %s - %s - %s" % (minx, miny, maxx, maxy))
+                                        poly_buffer.append(shply_box(minx, miny, maxx, maxy))
+                                except:
+                                    pass
+                            last_path_aperture = current_aperture
+                        else:
+                            self.app.inform.emit("[warning] Coordinates missing, line ignored: %s" % str(gline))
+                            self.app.inform.emit("[warning_notcl] GERBER file might be CORRUPT. Check the file !!!")
+
+                    elif current_operation_code == 2:
+                        if len(path) > 1:
+                            geo = None
+
+                            ## --- BUFFERED ---
+                            if making_region:
+                                if follow:
+                                    geo = Polygon()
+                                else:
+                                    elem = [linear_x, linear_y]
+                                    if elem != path[-1]:
+                                        path.append([linear_x, linear_y])
+                                    try:
+                                        geo = Polygon(path)
+                                    except ValueError:
+                                        log.warning("Problem %s %s" % (gline, line_num))
+                                        self.app.inform.emit("[error] Region does not have enough points. "
+                                                             "File will be processed but there are parser errors. "
+                                                             "Line number: %s" % str(line_num))
+                            else:
+                                if last_path_aperture is None:
+                                    log.warning("No aperture defined for curent path. (%d)" % line_num)
+                                width = self.apertures[last_path_aperture]["size"]  # TODO: WARNING this should fail!
+                                #log.debug("Line %d: Setting aperture to %s before buffering." % (line_num, last_path_aperture))
+                                if follow:
+                                    geo = LineString(path)
+                                else:
+                                    geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+
+                            try:
+                                if self.apertures[last_path_aperture]["type"] != 'R':
+                                    if not geo.is_empty:
+                                        poly_buffer.append(geo)
+                            except:
+                                poly_buffer.append(geo)
+
+                        # if linear_x or linear_y are None, ignore those
+                        if linear_x is not None and linear_y is not None:
+                            path = [[linear_x, linear_y]]  # Start new path
+                        else:
+                            self.app.inform.emit("[warning] Coordinates missing, line ignored: %s" % str(gline))
+                            self.app.inform.emit("[warning_notcl] GERBER file might be CORRUPT. Check the file !!!")
+
+                    # Flash
+                    # Not allowed in region mode.
+                    elif current_operation_code == 3:
+
+                        # Create path draw so far.
+                        if len(path) > 1:
+                            # --- Buffered ----
+                            width = self.apertures[last_path_aperture]["size"]
+
+                            if follow:
+                                geo = LineString(path)
+                            else:
+                                geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+
+                            if not geo.is_empty:
+                                try:
+                                    if self.apertures[current_aperture]["type"] != 'R':
+                                        poly_buffer.append(geo)
+                                    else:
+                                        pass
+                                except:
+                                    poly_buffer.append(geo)
+
+                        # Reset path starting point
+                        path = [[linear_x, linear_y]]
+
+                        # --- BUFFERED ---
+                        # Draw the flash
+                        if follow:
+                            continue
+                        flash = Gerber.create_flash_geometry(
+                            Point(
+                                [linear_x, linear_y]),
+                            self.apertures[current_aperture],
+                            int(self.steps_per_circle)
+                        )
+                        if not flash.is_empty:
+                            poly_buffer.append(flash)
+
+                    # maybe those lines are not exactly needed but it is easier to read the program as those coordinates
+                    # are used in case that circular interpolation is encountered within the Gerber file
+                    current_x = linear_x
+                    current_y = linear_y
+
+                    # log.debug("Line_number=%3s X=%s Y=%s (%s)" % (line_num, linear_x, linear_y, gline))
+                    continue
+
+                ### G74/75* - Single or multiple quadrant arcs
+                match = self.quad_re.search(gline)
+                if match:
+                    if match.group(1) == '4':
+                        quadrant_mode = 'SINGLE'
+                    else:
+                        quadrant_mode = 'MULTI'
+                    continue
+
+                ### G02/3 - Circular interpolation
+                # 2-clockwise, 3-counterclockwise
+                # Ex. format: G03 X0 Y50 I-50 J0 where the X, Y coords are the coords of the End Point
+                match = self.circ_re.search(gline)
+                if match:
+                    arcdir = [None, None, "cw", "ccw"]
+
+                    mode, circular_x, circular_y, i, j, d = match.groups()
+
+                    try:
+                        circular_x = parse_gerber_number(circular_x,
+                                                         self.int_digits, self.frac_digits, self.gerber_zeros)
+                    except:
+                        circular_x = current_x
+
+                    try:
+                        circular_y = parse_gerber_number(circular_y,
+                                                         self.int_digits, self.frac_digits, self.gerber_zeros)
+                    except:
+                        circular_y = current_y
+
+                    # According to Gerber specification i and j are not modal, which means that when i or j are missing,
+                    # they are to be interpreted as being zero
+                    try:
+                        i = parse_gerber_number(i, self.int_digits, self.frac_digits, self.gerber_zeros)
+                    except:
+                        i = 0
+
+                    try:
+                        j = parse_gerber_number(j, self.int_digits, self.frac_digits, self.gerber_zeros)
+                    except:
+                        j = 0
+
+                    if quadrant_mode is None:
+                        log.error("Found arc without preceding quadrant specification G74 or G75. (%d)" % line_num)
+                        log.error(gline)
+                        continue
+
+                    if mode is None and current_interpolation_mode not in [2, 3]:
+                        log.error("Found arc without circular interpolation mode defined. (%d)" % line_num)
+                        log.error(gline)
+                        continue
+                    elif mode is not None:
+                        current_interpolation_mode = int(mode)
+
+                    # Set operation code if provided
+                    try:
+                        current_operation_code = int(d)
+                        current_d = current_operation_code
+                    except:
+                        current_operation_code = current_d
+
+                    # Nothing created! Pen Up.
+                    if current_operation_code == 2:
+                        log.warning("Arc with D2. (%d)" % line_num)
+                        if len(path) > 1:
+                            if last_path_aperture is None:
+                                log.warning("No aperture defined for curent path. (%d)" % line_num)
+
+                            # --- BUFFERED ---
+                            width = self.apertures[last_path_aperture]["size"]
+
+                            if follow:
+                                buffered = LineString(path)
+                            else:
+                                buffered = LineString(path).buffer(width / 1.999, int(self.steps_per_circle))
+                            if not buffered.is_empty:
+                                poly_buffer.append(buffered)
+
+                        current_x = circular_x
+                        current_y = circular_y
+                        path = [[current_x, current_y]]  # Start new path
+                        continue
+
+                    # Flash should not happen here
+                    if current_operation_code == 3:
+                        log.error("Trying to flash within arc. (%d)" % line_num)
+                        continue
+
+                    if quadrant_mode == 'MULTI':
+                        center = [i + current_x, j + current_y]
+                        radius = sqrt(i ** 2 + j ** 2)
+                        start = arctan2(-j, -i)  # Start angle
+                        # Numerical errors might prevent start == stop therefore
+                        # we check ahead of time. This should result in a
+                        # 360 degree arc.
+                        if current_x == circular_x and current_y == circular_y:
+                            stop = start
+                        else:
+                            stop = arctan2(-center[1] + circular_y, -center[0] + circular_x)  # Stop angle
+
+                        this_arc = arc(center, radius, start, stop,
+                                       arcdir[current_interpolation_mode],
+                                       int(self.steps_per_circle))
+
+                        # The last point in the computed arc can have
+                        # numerical errors. The exact final point is the
+                        # specified (x, y). Replace.
+                        this_arc[-1] = (circular_x, circular_y)
+
+                        # Last point in path is current point
+                        # current_x = this_arc[-1][0]
+                        # current_y = this_arc[-1][1]
+                        current_x, current_y = circular_x, circular_y
+
+                        # Append
+                        path += this_arc
+
+                        last_path_aperture = current_aperture
+
+                        continue
+
+                    if quadrant_mode == 'SINGLE':
+
+                        center_candidates = [
+                            [i + current_x, j + current_y],
+                            [-i + current_x, j + current_y],
+                            [i + current_x, -j + current_y],
+                            [-i + current_x, -j + current_y]
+                        ]
+
+                        valid = False
+                        log.debug("I: %f  J: %f" % (i, j))
+                        for center in center_candidates:
+                            radius = sqrt(i ** 2 + j ** 2)
+
+                            # Make sure radius to start is the same as radius to end.
+                            radius2 = sqrt((center[0] - circular_x) ** 2 + (center[1] - circular_y) ** 2)
+                            if radius2 < radius * 0.95 or radius2 > radius * 1.05:
+                                continue  # Not a valid center.
+
+                            # Correct i and j and continue as with multi-quadrant.
+                            i = center[0] - current_x
+                            j = center[1] - current_y
+
+                            start = arctan2(-j, -i)  # Start angle
+                            stop = arctan2(-center[1] + circular_y, -center[0] + circular_x)  # Stop angle
+                            angle = abs(arc_angle(start, stop, arcdir[current_interpolation_mode]))
+                            log.debug("ARC START: %f, %f  CENTER: %f, %f  STOP: %f, %f" %
+                                      (current_x, current_y, center[0], center[1], circular_x, circular_y))
+                            log.debug("START Ang: %f, STOP Ang: %f, DIR: %s, ABS: %.12f <= %.12f: %s" %
+                                      (start * 180 / pi, stop * 180 / pi, arcdir[current_interpolation_mode],
+                                       angle * 180 / pi, pi / 2 * 180 / pi, angle <= (pi + 1e-6) / 2))
+
+                            if angle <= (pi + 1e-6) / 2:
+                                log.debug("########## ACCEPTING ARC ############")
+                                this_arc = arc(center, radius, start, stop,
+                                               arcdir[current_interpolation_mode],
+                                               int(self.steps_per_circle))
+
+                                # Replace with exact values
+                                this_arc[-1] = (circular_x, circular_y)
+
+                                # current_x = this_arc[-1][0]
+                                # current_y = this_arc[-1][1]
+                                current_x, current_y = circular_x, circular_y
+
+                                path += this_arc
+                                last_path_aperture = current_aperture
+                                valid = True
+                                break
+
+                        if valid:
+                            continue
+                        else:
+                            log.warning("Invalid arc in line %d." % line_num)
+
+                ## EOF
+                match = self.eof_re.search(gline)
+                if match:
+                    continue
+
+                ### Line did not match any pattern. Warn user.
+                log.warning("Line ignored (%d): %s" % (line_num, gline))
+
+            if len(path) > 1:
+                # In case that G01 (moving) aperture is rectangular, there is no need to still create
+                # another geo since we already created a shapely box using the start and end coordinates found in
+                # path variable. We do it only for other apertures than 'R' type
+                if self.apertures[last_path_aperture]["type"] == 'R':
+                    pass
+                else:
+                    # EOF, create shapely LineString if something still in path
+                    ## --- Buffered ---
+                    width = self.apertures[last_path_aperture]["size"]
+                    if follow:
+                        geo = LineString(path)
+                    else:
+                        geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                    if not geo.is_empty:
+                        poly_buffer.append(geo)
+
+            # --- Apply buffer ---
+            if follow:
+                self.solid_geometry = poly_buffer
+                return
+
+            log.warning("Joining %d polygons." % len(poly_buffer))
+
+            if len(poly_buffer) == 0:
+                log.error("Object is not Gerber file or empty. Aborting Object creation.")
+                return
+
+            if self.use_buffer_for_union:
+                log.debug("Union by buffer...")
+                new_poly = MultiPolygon(poly_buffer)
+                new_poly = new_poly.buffer(0.00000001)
+                new_poly = new_poly.buffer(-0.00000001)
+                log.warning("Union(buffer) done.")
+            else:
+                log.debug("Union by union()...")
+                new_poly = cascaded_union(poly_buffer)
+                new_poly = new_poly.buffer(0, int(self.steps_per_circle / 4))
+                log.warning("Union done.")
+            if current_polarity == 'D':
+                self.solid_geometry = self.solid_geometry.union(new_poly)
+            else:
+                self.solid_geometry = self.solid_geometry.difference(new_poly)
+
+        except Exception as err:
+            ex_type, ex, tb = sys.exc_info()
+            traceback.print_tb(tb)
+            #print traceback.format_exc()
+
+            log.error("PARSING FAILED. Line %d: %s" % (line_num, gline))
+            self.app.inform.emit("[error] Gerber Parser ERROR.\n Line %d: %s" % (line_num, gline), repr(err))
+
+    @staticmethod
+    def create_flash_geometry(location, aperture, steps_per_circle=None):
+
+        # log.debug('Flashing @%s, Aperture: %s' % (location, aperture))
+
+        if steps_per_circle is None:
+            steps_per_circle = 64
+
+        if type(location) == list:
+            location = Point(location)
+
+        if aperture['type'] == 'C':  # Circles
+            return location.buffer(aperture['size'] / 2, int(steps_per_circle / 4))
+
+        if aperture['type'] == 'R':  # Rectangles
+            loc = location.coords[0]
+            width = aperture['width']
+            height = aperture['height']
+            minx = loc[0] - width / 2
+            maxx = loc[0] + width / 2
+            miny = loc[1] - height / 2
+            maxy = loc[1] + height / 2
+            return shply_box(minx, miny, maxx, maxy)
+
+        if aperture['type'] == 'O':  # Obround
+            loc = location.coords[0]
+            width = aperture['width']
+            height = aperture['height']
+            if width > height:
+                p1 = Point(loc[0] + 0.5 * (width - height), loc[1])
+                p2 = Point(loc[0] - 0.5 * (width - height), loc[1])
+                c1 = p1.buffer(height * 0.5, int(steps_per_circle / 4))
+                c2 = p2.buffer(height * 0.5, int(steps_per_circle / 4))
+            else:
+                p1 = Point(loc[0], loc[1] + 0.5 * (height - width))
+                p2 = Point(loc[0], loc[1] - 0.5 * (height - width))
+                c1 = p1.buffer(width * 0.5, int(steps_per_circle / 4))
+                c2 = p2.buffer(width * 0.5, int(steps_per_circle / 4))
+            return cascaded_union([c1, c2]).convex_hull
+
+        if aperture['type'] == 'P':  # Regular polygon
+            loc = location.coords[0]
+            diam = aperture['diam']
+            n_vertices = aperture['nVertices']
+            points = []
+            for i in range(0, n_vertices):
+                x = loc[0] + 0.5 * diam * (cos(2 * pi * i / n_vertices))
+                y = loc[1] + 0.5 * diam * (sin(2 * pi * i / n_vertices))
+                points.append((x, y))
+            ply = Polygon(points)
+            if 'rotation' in aperture:
+                ply = affinity.rotate(ply, aperture['rotation'])
+            return ply
+
+        if aperture['type'] == 'AM':  # Aperture Macro
+            loc = location.coords[0]
+            flash_geo = aperture['macro'].make_geometry(aperture['modifiers'])
+            if flash_geo.is_empty:
+                log.warning("Empty geometry for Aperture Macro: %s" % str(aperture['macro'].name))
+            return affinity.translate(flash_geo, xoff=loc[0], yoff=loc[1])
+
+        log.warning("Unknown aperture type: %s" % aperture['type'])
+        return None
+    
+    def create_geometry(self):
+        """
+        Geometry from a Gerber file is made up entirely of polygons.
+        Every stroke (linear or circular) has an aperture which gives
+        it thickness. Additionally, aperture strokes have non-zero area,
+        and regions naturally do as well.
+
+        :rtype : None
+        :return: None
+        """
+
+        # self.buffer_paths()
+        #
+        # self.fix_regions()
+        #
+        # self.do_flashes()
+        #
+        # self.solid_geometry = cascaded_union(self.buffered_paths +
+        #                                      [poly['polygon'] for poly in self.regions] +
+        #                                      self.flash_geometry)
+
+    def get_bounding_box(self, margin=0.0, rounded=False):
+        """
+        Creates and returns a rectangular polygon bounding at a distance of
+        margin from the object's ``solid_geometry``. If margin > 0, the polygon
+        can optionally have rounded corners of radius equal to margin.
+
+        :param margin: Distance to enlarge the rectangular bounding
+         box in both positive and negative, x and y axes.
+        :type margin: float
+        :param rounded: Wether or not to have rounded corners.
+        :type rounded: bool
+        :return: The bounding box.
+        :rtype: Shapely.Polygon
+        """
+
+        bbox = self.solid_geometry.envelope.buffer(margin)
+        if not rounded:
+            bbox = bbox.envelope
+        return bbox
+
+    def bounds(self):
+        """
+        Returns coordinates of rectangular bounds
+        of Gerber geometry: (xmin, ymin, xmax, ymax).
+        """
+        # fixed issue of getting bounds only for one level lists of objects
+        # now it can get bounds for nested lists of objects
+
+        log.debug("Gerber->bounds()")
+        if self.solid_geometry is None:
+            log.debug("solid_geometry is None")
+            return 0, 0, 0, 0
+
+        def bounds_rec(obj):
+            if type(obj) is list:
+                minx = Inf
+                miny = Inf
+                maxx = -Inf
+                maxy = -Inf
+
+                for k in obj:
+                    if type(k) is dict:
+                        for key in k:
+                            minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
+                            minx = min(minx, minx_)
+                            miny = min(miny, miny_)
+                            maxx = max(maxx, maxx_)
+                            maxy = max(maxy, maxy_)
+                    else:
+                        minx_, miny_, maxx_, maxy_ = bounds_rec(k)
+                        minx = min(minx, minx_)
+                        miny = min(miny, miny_)
+                        maxx = max(maxx, maxx_)
+                        maxy = max(maxy, maxy_)
+                return minx, miny, maxx, maxy
+            else:
+                # it's a Shapely object, return it's bounds
+                return obj.bounds
+
+        bounds_coords = bounds_rec(self.solid_geometry)
+        return bounds_coords
+
+    def scale(self, xfactor, yfactor=None, point=None):
+        """
+        Scales the objects' geometry on the XY plane by a given factor.
+        These are:
+
+        * ``buffered_paths``
+        * ``flash_geometry``
+        * ``solid_geometry``
+        * ``regions``
+
+        NOTE:
+        Does not modify the data used to create these elements. If these
+        are recreated, the scaling will be lost. This behavior was modified
+        because of the complexity reached in this class.
+
+        :param factor: Number by which to scale.
+        :type factor: float
+        :rtype : None
+        """
+
+        if yfactor is None:
+            yfactor = xfactor
+
+        if point is None:
+            px = 0
+            py = 0
+        else:
+            px, py = point
+
+        def scale_geom(obj):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(scale_geom(g))
+                return new_obj
+            else:
+                return affinity.scale(obj, xfactor,
+                                             yfactor, origin=(px, py))
+
+        self.solid_geometry = scale_geom(self.solid_geometry)
+
+        ## solid_geometry ???
+        #  It's a cascaded union of objects.
+        # self.solid_geometry = affinity.scale(self.solid_geometry, factor,
+        #                                      factor, origin=(0, 0))
+
+        # # Now buffered_paths, flash_geometry and solid_geometry
+        # self.create_geometry()
+
+    def offset(self, vect):
+        """
+        Offsets the objects' geometry on the XY plane by a given vector.
+        These are:
+
+        * ``buffered_paths``
+        * ``flash_geometry``
+        * ``solid_geometry``
+        * ``regions``
+
+        NOTE:
+        Does not modify the data used to create these elements. If these
+        are recreated, the scaling will be lost. This behavior was modified
+        because of the complexity reached in this class.
+
+        :param vect: (x, y) offset vector.
+        :type vect: tuple
+        :return: None
+        """
+
+        dx, dy = vect
+
+        def offset_geom(obj):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(offset_geom(g))
+                return new_obj
+            else:
+                return affinity.translate(obj, xoff=dx, yoff=dy)
+
+        ## Solid geometry
+        # self.solid_geometry = affinity.translate(self.solid_geometry, xoff=dx, yoff=dy)
+        self.solid_geometry = offset_geom(self.solid_geometry)
+
+    def mirror(self, axis, point):
+        """
+        Mirrors the object around a specified axis passing through
+        the given point. What is affected:
+
+        * ``buffered_paths``
+        * ``flash_geometry``
+        * ``solid_geometry``
+        * ``regions``
+
+        NOTE:
+        Does not modify the data used to create these elements. If these
+        are recreated, the scaling will be lost. This behavior was modified
+        because of the complexity reached in this class.
+
+        :param axis: "X" or "Y" indicates around which axis to mirror.
+        :type axis: str
+        :param point: [x, y] point belonging to the mirror axis.
+        :type point: list
+        :return: None
+        """
+
+        px, py = point
+        xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
+
+        def mirror_geom(obj):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(mirror_geom(g))
+                return new_obj
+            else:
+                return affinity.scale(obj, xscale, yscale, origin=(px, py))
+
+        self.solid_geometry = mirror_geom(self.solid_geometry)
+
+        #  It's a cascaded union of objects.
+        # self.solid_geometry = affinity.scale(self.solid_geometry,
+        #                                      xscale, yscale, origin=(px, py))
+
+
+    def skew(self, angle_x, angle_y, point):
+        """
+        Shear/Skew the geometries of an object by angles along x and y dimensions.
+
+        Parameters
+        ----------
+        xs, ys : float, float
+            The shear angle(s) for the x and y axes respectively. These can be
+            specified in either degrees (default) or radians by setting
+            use_radians=True.
+
+        See shapely manual for more information:
+        http://toblerity.org/shapely/manual.html#affine-transformations
+        """
+
+        px, py = point
+
+        def skew_geom(obj):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(skew_geom(g))
+                return new_obj
+            else:
+                return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
+
+        self.solid_geometry = skew_geom(self.solid_geometry)
+
+        # self.solid_geometry = affinity.skew(self.solid_geometry, angle_x, angle_y, origin=(px, py))
+
+    def rotate(self, angle, point):
+        """
+        Rotate an object by a given angle around given coords (point)
+        :param angle:
+        :param point:
+        :return:
+        """
+
+        px, py = point
+
+        def rotate_geom(obj):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(rotate_geom(g))
+                return new_obj
+            else:
+                return affinity.rotate(obj, angle, origin=(px, py))
+
+        self.solid_geometry = rotate_geom(self.solid_geometry)
+
+        # self.solid_geometry = affinity.rotate(self.solid_geometry, angle, origin=(px, py))
+
+
+class Excellon(Geometry):
+    """
+    *ATTRIBUTES*
+
+    * ``tools`` (dict): The key is the tool name and the value is
+      a dictionary specifying the tool:
+
+    ================  ====================================
+    Key               Value
+    ================  ====================================
+    C                 Diameter of the tool
+    Others            Not supported (Ignored).
+    ================  ====================================
+
+    * ``drills`` (list): Each is a dictionary:
+
+    ================  ====================================
+    Key               Value
+    ================  ====================================
+    point             (Shapely.Point) Where to drill
+    tool              (str) A key in ``tools``
+    ================  ====================================
+
+    * ``slots`` (list): Each is a dictionary
+
+    ================  ====================================
+    Key               Value
+    ================  ====================================
+    start             (Shapely.Point) Start point of the slot
+    start             (Shapely.Point) Stop point of the slot
+    tool              (str) A key in ``tools``
+    ================  ====================================
+    """
+
+    defaults = {
+        "zeros": "L",
+        "excellon_format_upper_mm": '3',
+        "excellon_format_lower_mm": '3',
+        "excellon_format_upper_in": '2',
+        "excellon_format_lower_in": '4',
+        "excellon_units": 'INCH',
+        "geo_steps_per_circle": '64'
+    }
+
+    def __init__(self, zeros=None, excellon_format_upper_mm=None, excellon_format_lower_mm=None,
+                 excellon_format_upper_in=None, excellon_format_lower_in=None, excellon_units=None,
+                 geo_steps_per_circle=None):
+        """
+        The constructor takes no parameters.
+
+        :return: Excellon object.
+        :rtype: Excellon
+        """
+
+        if geo_steps_per_circle is None:
+            geo_steps_per_circle = Excellon.defaults['geo_steps_per_circle']
+        self.geo_steps_per_circle = geo_steps_per_circle
+
+        Geometry.__init__(self, geo_steps_per_circle=geo_steps_per_circle)
+
+        # dictionary to store tools, see above for description
+        self.tools = {}
+        # list to store the drills, see above for description
+        self.drills = []
+
+        # self.slots (list) to store the slots; each is a dictionary
+        self.slots = []
+
+        # it serve to flag if a start routing or a stop routing was encountered
+        # if a stop is encounter and this flag is still 0 (so there is no stop for a previous start) issue error
+        self.routing_flag = 1
+
+        self.match_routing_start = None
+        self.match_routing_stop = None
+
+        self.num_tools = []  # List for keeping the tools sorted
+        self.index_per_tool = {}  # Dictionary to store the indexed points for each tool
+
+        ## IN|MM -> Units are inherited from Geometry
+        #self.units = units
+
+        # Trailing "T" or leading "L" (default)
+        #self.zeros = "T"
+        self.zeros = zeros or self.defaults["zeros"]
+        self.zeros_found = self.zeros
+        self.units_found = self.units
+
+        # Excellon format
+        self.excellon_format_upper_in = excellon_format_upper_in or self.defaults["excellon_format_upper_in"]
+        self.excellon_format_lower_in = excellon_format_lower_in or self.defaults["excellon_format_lower_in"]
+        self.excellon_format_upper_mm = excellon_format_upper_mm or self.defaults["excellon_format_upper_mm"]
+        self.excellon_format_lower_mm = excellon_format_lower_mm or self.defaults["excellon_format_lower_mm"]
+        self.excellon_units = excellon_units or self.defaults["excellon_units"]
+
+        # Attributes to be included in serialization
+        # Always append to it because it carries contents
+        # from Geometry.
+        self.ser_attrs += ['tools', 'drills', 'zeros', 'excellon_format_upper_mm', 'excellon_format_lower_mm',
+                           'excellon_format_upper_in', 'excellon_format_lower_in', 'excellon_units', 'slots']
+
+        #### Patterns ####
+        # Regex basics:
+        # ^ - beginning
+        # $ - end
+        # *: 0 or more, +: 1 or more, ?: 0 or 1
+
+        # M48 - Beginning of Part Program Header
+        self.hbegin_re = re.compile(r'^M48$')
+
+        # ;HEADER - Beginning of Allegro Program Header
+        self.allegro_hbegin_re = re.compile(r'\;\s*(HEADER)')
+
+        # M95 or % - End of Part Program Header
+        # NOTE: % has different meaning in the body
+        self.hend_re = re.compile(r'^(?:M95|%)$')
+
+        # FMAT Excellon format
+        # Ignored in the parser
+        #self.fmat_re = re.compile(r'^FMAT,([12])$')
+
+        # Number format and units
+        # INCH uses 6 digits
+        # METRIC uses 5/6
+        self.units_re = re.compile(r'^(INCH|METRIC)(?:,([TL])Z)?$')
+
+        # Tool definition/parameters (?= is look-ahead
+        # NOTE: This might be an overkill!
+        # self.toolset_re = re.compile(r'^T(0?\d|\d\d)(?=.*C(\d*\.?\d*))?' +
+        #                              r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' +
+        #                              r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' +
+        #                              r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]')
+        self.toolset_re = re.compile(r'^T(\d+)(?=.*C(\d*\.?\d*))?' +
+                                     r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' +
+                                     r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' +
+                                     r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]')
+
+        self.detect_gcode_re = re.compile(r'^G2([01])$')
+
+        # Tool select
+        # Can have additional data after tool number but
+        # is ignored if present in the header.
+        # Warning: This will match toolset_re too.
+        # self.toolsel_re = re.compile(r'^T((?:\d\d)|(?:\d))')
+        self.toolsel_re = re.compile(r'^T(\d+)')
+
+        # Headerless toolset
+        self.toolset_hl_re = re.compile(r'^T(\d+)(?=.*C(\d*\.?\d*))')
+
+        # Comment
+        self.comm_re = re.compile(r'^;(.*)$')
+
+        # Absolute/Incremental G90/G91
+        self.absinc_re = re.compile(r'^G9([01])$')
+
+        # Modes of operation
+        # 1-linear, 2-circCW, 3-cirCCW, 4-vardwell, 5-Drill
+        self.modes_re = re.compile(r'^G0([012345])')
+
+        # Measuring mode
+        # 1-metric, 2-inch
+        self.meas_re = re.compile(r'^M7([12])$')
+
+        # Coordinates
+        # self.xcoord_re = re.compile(r'^X(\d*\.?\d*)(?:Y\d*\.?\d*)?$')
+        # self.ycoord_re = re.compile(r'^(?:X\d*\.?\d*)?Y(\d*\.?\d*)$')
+        coordsperiod_re_string = r'(?=.*X([-\+]?\d*\.\d*))?(?=.*Y([-\+]?\d*\.\d*))?[XY]'
+        self.coordsperiod_re = re.compile(coordsperiod_re_string)
+
+        coordsnoperiod_re_string = r'(?!.*\.)(?=.*X([-\+]?\d*))?(?=.*Y([-\+]?\d*))?[XY]'
+        self.coordsnoperiod_re = re.compile(coordsnoperiod_re_string)
+
+        # Slots parsing
+        slots_re_string = r'^([^G]+)G85(.*)$'
+        self.slots_re = re.compile(slots_re_string)
+
+        # R - Repeat hole (# times, X offset, Y offset)
+        self.rep_re = re.compile(r'^R(\d+)(?=.*[XY])+(?:X([-\+]?\d*\.?\d*))?(?:Y([-\+]?\d*\.?\d*))?$')
+
+        # Various stop/pause commands
+        self.stop_re = re.compile(r'^((G04)|(M09)|(M06)|(M00)|(M30))')
+
+        # Allegro Excellon format support
+        self.tool_units_re = re.compile(r'(\;\s*Holesize \d+.\s*\=\s*(\d+.\d+).*(MILS|MM))')
+
+        # Parse coordinates
+        self.leadingzeros_re = re.compile(r'^[-\+]?(0*)(\d*)')
+
+        # Repeating command
+        self.repeat_re = re.compile(r'R(\d+)')
+
+    def parse_file(self, filename):
+        """
+        Reads the specified file as array of lines as
+        passes it to ``parse_lines()``.
+
+        :param filename: The file to be read and parsed.
+        :type filename: str
+        :return: None
+        """
+        efile = open(filename, 'r')
+        estr = efile.readlines()
+        efile.close()
+        try:
+            self.parse_lines(estr)
+        except:
+            return "fail"
+
+    def parse_lines(self, elines):
+        """
+        Main Excellon parser.
+
+        :param elines: List of strings, each being a line of Excellon code.
+        :type elines: list
+        :return: None
+        """
+
+        # State variables
+        current_tool = ""
+        in_header = False
+        headerless = False
+        current_x = None
+        current_y = None
+
+        slot_current_x = None
+        slot_current_y = None
+
+        name_tool = 0
+        allegro_warning = False
+        line_units_found = False
+
+        repeating_x = 0
+        repeating_y = 0
+        repeat = 0
+
+        line_units = ''
+
+        #### Parsing starts here ####
+        line_num = 0  # Line number
+        eline = ""
+        try:
+            for eline in elines:
+                line_num += 1
+                # log.debug("%3d %s" % (line_num, str(eline)))
+
+                # Cleanup lines
+                eline = eline.strip(' \r\n')
+
+                # Excellon files and Gcode share some extensions therefore if we detect G20 or G21 it's GCODe
+                # and we need to exit from here
+                if self.detect_gcode_re.search(eline):
+                    log.warning("This is GCODE mark: %s" % eline)
+                    self.app.inform.emit('[error_notcl] This is GCODE mark: %s' % eline)
+                    return
+
+                # Header Begin (M48) #
+                if self.hbegin_re.search(eline):
+                    in_header = True
+                    log.warning("Found start of the header: %s" % eline)
+                    continue
+
+                # Allegro Header Begin (;HEADER) #
+                if self.allegro_hbegin_re.search(eline):
+                    in_header = True
+                    allegro_warning = True
+                    log.warning("Found ALLEGRO start of the header: %s" % eline)
+                    continue
+
+                # Header End #
+                # Since there might be comments in the header that include char % or M95
+                # we ignore the lines starting with ';' which show they are comments
+                if self.comm_re.search(eline):
+                    match = self.tool_units_re.search(eline)
+                    if match:
+                        if line_units_found is False:
+                            line_units_found = True
+                            line_units = match.group(3)
+                            self.convert_units({"MILS": "IN", "MM": "MM"}[line_units])
+                            log.warning("Type of Allegro UNITS found inline: %s" % line_units)
+
+                        if match.group(2):
+                            name_tool += 1
+                            if line_units == 'MILS':
+                                spec = {"C": (float(match.group(2)) / 1000)}
+                                self.tools[str(name_tool)] = spec
+                                log.debug("  Tool definition: %s %s" % (name_tool, spec))
+                            else:
+                                spec = {"C": float(match.group(2))}
+                                self.tools[str(name_tool)] = spec
+                                log.debug("  Tool definition: %s %s" % (name_tool, spec))
+                            continue
+                    else:
+                        log.warning("Line ignored, it's a comment: %s" % eline)
+
+                else:
+                    if self.hend_re.search(eline):
+                        if in_header is False:
+                            log.warning("Found end of the header but there is no header: %s" % eline)
+                            log.warning("The only useful data in header are tools, units and format.")
+                            log.warning("Therefore we will create units and format based on defaults.")
+                            headerless = True
+                            try:
+                                self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.excellon_units])
+                                print("Units converted .............................. %s" % self.excellon_units)
+                            except Exception as e:
+                                log.warning("Units could not be converted: %s" % str(e))
+
+                        in_header = False
+                        # for Allegro type of Excellons we reset name_tool variable so we can reuse it for toolchange
+                        if allegro_warning is True:
+                            name_tool = 0
+                        log.warning("Found end of the header: %s" % eline)
+                        continue
+
+                ## Alternative units format M71/M72
+                # Supposed to be just in the body (yes, the body)
+                # but some put it in the header (PADS for example).
+                # Will detect anywhere. Occurrence will change the
+                # object's units.
+                match = self.meas_re.match(eline)
+                if match:
+                    #self.units = {"1": "MM", "2": "IN"}[match.group(1)]
+
+                    # Modified for issue #80
+                    self.convert_units({"1": "MM", "2": "IN"}[match.group(1)])
+                    log.debug("  Units: %s" % self.units)
+                    if self.units == 'MM':
+                        log.warning("Excellon format preset is: %s" % self.excellon_format_upper_mm + \
+                                    ':' + str(self.excellon_format_lower_mm))
+                    else:
+                        log.warning("Excellon format preset is: %s" % self.excellon_format_upper_in + \
+                        ':' + str(self.excellon_format_lower_in))
+                    continue
+
+                #### Body ####
+                if not in_header:
+
+                    ## Tool change ##
+                    match = self.toolsel_re.search(eline)
+                    if match:
+                        current_tool = str(int(match.group(1)))
+                        log.debug("Tool change: %s" % current_tool)
+                        if headerless is True:
+                            match = self.toolset_hl_re.search(eline)
+                            if match:
+                                name = str(int(match.group(1)))
+                                spec = {
+                                    "C": float(match.group(2)),
+                                }
+                                self.tools[name] = spec
+                                log.debug("  Tool definition out of header: %s %s" % (name, spec))
+
+                        continue
+
+                    ## Allegro Type Tool change ##
+                    if allegro_warning is True:
+                        match = self.absinc_re.search(eline)
+                        match1 = self.stop_re.search(eline)
+                        if match or match1:
+                            name_tool += 1
+                            current_tool = str(name_tool)
+                            log.debug(" Tool change for Allegro type of Excellon: %s" % current_tool)
+                            continue
+
+                    ## Slots parsing for drilled slots (contain G85)
+                    # a Excellon drilled slot line may look like this:
+                    # X01125Y0022244G85Y0027756
+                    match = self.slots_re.search(eline)
+                    if match:
+                        # signal that there are milling slots operations
+                        self.defaults['excellon_drills'] = False
+
+                        # the slot start coordinates group is to the left of G85 command (group(1) )
+                        # the slot stop coordinates group is to the right of G85 command (group(2) )
+                        start_coords_match = match.group(1)
+                        stop_coords_match = match.group(2)
+
+                        # Slot coordinates without period ##
+                        # get the coordinates for slot start and for slot stop into variables
+                        start_coords_noperiod = self.coordsnoperiod_re.search(start_coords_match)
+                        stop_coords_noperiod = self.coordsnoperiod_re.search(stop_coords_match)
+                        if start_coords_noperiod:
+                            try:
+                                slot_start_x = self.parse_number(start_coords_noperiod.group(1))
+                                slot_current_x = slot_start_x
+                            except TypeError:
+                                slot_start_x = slot_current_x
+                            except:
+                                return
+
+                            try:
+                                slot_start_y = self.parse_number(start_coords_noperiod.group(2))
+                                slot_current_y = slot_start_y
+                            except TypeError:
+                                slot_start_y = slot_current_y
+                            except:
+                                return
+
+                            try:
+                                slot_stop_x = self.parse_number(stop_coords_noperiod.group(1))
+                                slot_current_x = slot_stop_x
+                            except TypeError:
+                                slot_stop_x = slot_current_x
+                            except:
+                                return
+
+                            try:
+                                slot_stop_y = self.parse_number(stop_coords_noperiod.group(2))
+                                slot_current_y = slot_stop_y
+                            except TypeError:
+                                slot_stop_y = slot_current_y
+                            except:
+                                return
+
+                            if (slot_start_x is None or slot_start_y is None or
+                                                slot_stop_x is None or slot_stop_y is None):
+                                log.error("Slots are missing some or all coordinates.")
+                                continue
+
+                            # we have a slot
+                            log.debug('Parsed a slot with coordinates: ' + str([slot_start_x,
+                                                                               slot_start_y, slot_stop_x,
+                                                                               slot_stop_y]))
+
+                            # store current tool diameter as slot diameter
+                            slot_dia = 0.05
+                            try:
+                                slot_dia = float(self.tools[current_tool]['C'])
+                            except:
+                                pass
+                            log.debug(
+                                'Milling/Drilling slot with tool %s, diam=%f' % (
+                                    current_tool,
+                                    slot_dia
+                                )
+                            )
+
+                            self.slots.append(
+                                {
+                                    'start': Point(slot_start_x, slot_start_y),
+                                    'stop': Point(slot_stop_x, slot_stop_y),
+                                    'tool': current_tool
+                                }
+                            )
+                            continue
+
+                        # Slot coordinates with period: Use literally. ##
+                        # get the coordinates for slot start and for slot stop into variables
+                        start_coords_period = self.coordsperiod_re.search(start_coords_match)
+                        stop_coords_period = self.coordsperiod_re.search(stop_coords_match)
+                        if start_coords_period:
+
+                            try:
+                                slot_start_x = float(start_coords_period.group(1))
+                                slot_current_x = slot_start_x
+                            except TypeError:
+                                slot_start_x = slot_current_x
+                            except:
+                                return
+
+                            try:
+                                slot_start_y = float(start_coords_period.group(2))
+                                slot_current_y = slot_start_y
+                            except TypeError:
+                                slot_start_y = slot_current_y
+                            except:
+                                return
+
+                            try:
+                                slot_stop_x = float(stop_coords_period.group(1))
+                                slot_current_x = slot_stop_x
+                            except TypeError:
+                                slot_stop_x = slot_current_x
+                            except:
+                                return
+
+                            try:
+                                slot_stop_y = float(stop_coords_period.group(2))
+                                slot_current_y = slot_stop_y
+                            except TypeError:
+                                slot_stop_y = slot_current_y
+                            except:
+                                return
+
+                            if (slot_start_x is None or slot_start_y is None or
+                                                slot_stop_x is None or slot_stop_y is None):
+                                log.error("Slots are missing some or all coordinates.")
+                                continue
+
+                            # we have a slot
+                            log.debug('Parsed a slot with coordinates: ' + str([slot_start_x,
+                                                                            slot_start_y, slot_stop_x, slot_stop_y]))
+
+                            # store current tool diameter as slot diameter
+                            slot_dia = 0.05
+                            try:
+                                slot_dia = float(self.tools[current_tool]['C'])
+                            except:
+                                pass
+                            log.debug(
+                                'Milling/Drilling slot with tool %s, diam=%f' % (
+                                    current_tool,
+                                    slot_dia
+                                )
+                            )
+
+                            self.slots.append(
+                                {
+                                    'start': Point(slot_start_x, slot_start_y),
+                                    'stop': Point(slot_stop_x, slot_stop_y),
+                                    'tool': current_tool
+                                }
+                            )
+                        continue
+
+                    ## Coordinates without period ##
+                    match = self.coordsnoperiod_re.search(eline)
+                    if match:
+                        matchr = self.repeat_re.search(eline)
+                        if matchr:
+                            repeat = int(matchr.group(1))
+
+                        try:
+                            x = self.parse_number(match.group(1))
+                            repeating_x = current_x
+                            current_x = x
+                        except TypeError:
+                            x = current_x
+                            repeating_x = 0
+                        except:
+                            return
+
+                        try:
+                            y = self.parse_number(match.group(2))
+                            repeating_y = current_y
+                            current_y = y
+                        except TypeError:
+                            y = current_y
+                            repeating_y = 0
+                        except:
+                            return
+
+                        if x is None or y is None:
+                            log.error("Missing coordinates")
+                            continue
+
+                        ## Excellon Routing parse
+                        if len(re.findall("G00", eline)) > 0:
+                            self.match_routing_start = 'G00'
+
+                            # signal that there are milling slots operations
+                            self.defaults['excellon_drills'] = False
+
+                            self.routing_flag = 0
+                            slot_start_x = x
+                            slot_start_y = y
+                            continue
+
+                        if self.routing_flag == 0:
+                            if len(re.findall("G01", eline)) > 0:
+                                self.match_routing_stop = 'G01'
+
+                                # signal that there are milling slots operations
+                                self.defaults['excellon_drills'] = False
+
+                                self.routing_flag = 1
+                                slot_stop_x = x
+                                slot_stop_y = y
+                                self.slots.append(
+                                    {
+                                        'start': Point(slot_start_x, slot_start_y),
+                                        'stop': Point(slot_stop_x, slot_stop_y),
+                                        'tool': current_tool
+                                    }
+                                )
+                                continue
+
+                        if self.match_routing_start is None and self.match_routing_stop is None:
+                            if repeat == 0:
+                                # signal that there are drill operations
+                                self.defaults['excellon_drills'] = True
+                                self.drills.append({'point': Point((x, y)), 'tool': current_tool})
+                            else:
+                                coordx = x
+                                coordy = y
+                                while repeat > 0:
+                                    if repeating_x:
+                                        coordx = (repeat * x) + repeating_x
+                                    if repeating_y:
+                                        coordy = (repeat * y) + repeating_y
+                                    self.drills.append({'point': Point((coordx, coordy)), 'tool': current_tool})
+                                    repeat -= 1
+                            repeating_x = repeating_y = 0
+                            log.debug("{:15} {:8} {:8}".format(eline, x, y))
+                            continue
+
+                    ## Coordinates with period: Use literally. ##
+                    match = self.coordsperiod_re.search(eline)
+                    if match:
+                        matchr = self.repeat_re.search(eline)
+                        if matchr:
+                            repeat = int(matchr.group(1))
+
+                    if match:
+                        # signal that there are drill operations
+                        self.defaults['excellon_drills'] = True
+
+                        try:
+                            x = float(match.group(1))
+                            repeating_x = current_x
+                            current_x = x
+                        except TypeError:
+                            x = current_x
+                            repeating_x = 0
+
+                        try:
+                            y = float(match.group(2))
+                            repeating_y = current_y
+                            current_y = y
+                        except TypeError:
+                            y = current_y
+                            repeating_y = 0
+
+                        if x is None or y is None:
+                            log.error("Missing coordinates")
+                            continue
+
+                        ## Excellon Routing parse
+                        if len(re.findall("G00", eline)) > 0:
+                            self.match_routing_start = 'G00'
+
+                            # signal that there are milling slots operations
+                            self.defaults['excellon_drills'] = False
+
+                            self.routing_flag = 0
+                            slot_start_x = x
+                            slot_start_y = y
+                            continue
+
+                        if self.routing_flag == 0:
+                            if len(re.findall("G01", eline)) > 0:
+                                self.match_routing_stop = 'G01'
+
+                                # signal that there are milling slots operations
+                                self.defaults['excellon_drills'] = False
+
+                                self.routing_flag = 1
+                                slot_stop_x = x
+                                slot_stop_y = y
+                                self.slots.append(
+                                    {
+                                        'start': Point(slot_start_x, slot_start_y),
+                                        'stop': Point(slot_stop_x, slot_stop_y),
+                                        'tool': current_tool
+                                    }
+                                )
+                                continue
+
+                        if self.match_routing_start is None and self.match_routing_stop is None:
+                            # signal that there are drill operations
+                            if repeat == 0:
+                                # signal that there are drill operations
+                                self.defaults['excellon_drills'] = True
+                                self.drills.append({'point': Point((x, y)), 'tool': current_tool})
+                            else:
+                                coordx = x
+                                coordy = y
+                                while repeat > 0:
+                                    if repeating_x:
+                                        coordx = (repeat * x) + repeating_x
+                                    if repeating_y:
+                                        coordy = (repeat * y) + repeating_y
+                                    self.drills.append({'point': Point((coordx, coordy)), 'tool': current_tool})
+                                    repeat -= 1
+                            repeating_x = repeating_y = 0
+                            log.debug("{:15} {:8} {:8}".format(eline, x, y))
+                            continue
+
+                #### Header ####
+                if in_header:
+
+                    ## Tool definitions ##
+                    match = self.toolset_re.search(eline)
+                    if match:
+
+                        name = str(int(match.group(1)))
+                        spec = {
+                            "C": float(match.group(2)),
+                            # "F": float(match.group(3)),
+                            # "S": float(match.group(4)),
+                            # "B": float(match.group(5)),
+                            # "H": float(match.group(6)),
+                            # "Z": float(match.group(7))
+                        }
+                        self.tools[name] = spec
+                        log.debug("  Tool definition: %s %s" % (name, spec))
+                        continue
+
+                    ## Units and number format ##
+                    match = self.units_re.match(eline)
+                    if match:
+                        self.units_found = match.group(1)
+                        self.zeros = match.group(2)  # "T" or "L". Might be empty
+
+                        # self.units = {"INCH": "IN", "METRIC": "MM"}[match.group(1)]
+
+                        # Modified for issue #80
+                        self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.units_found])
+                        # log.warning("  Units/Format: %s %s" % (self.units, self.zeros))
+                        log.warning("Units: %s" % self.units)
+                        if self.units == 'MM':
+                            log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) +
+                                        ':' + str(self.excellon_format_lower_mm))
+                        else:
+                            log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) +
+                                        ':' + str(self.excellon_format_lower_in))
+                        log.warning("Type of zeros found inline: %s" % self.zeros)
+                        continue
+
+                    # Search for units type again it might be alone on the line
+                    if "INCH" in eline:
+                        line_units = "INCH"
+                        # Modified for issue #80
+                        self.convert_units({"INCH": "IN", "METRIC": "MM"}[line_units])
+                        log.warning("Type of UNITS found inline: %s" % line_units)
+                        log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) +
+                                    ':' + str(self.excellon_format_lower_in))
+                        # TODO: not working
+                        #FlatCAMApp.App.inform.emit("Detected INLINE: %s" % str(eline))
+                        continue
+                    elif "METRIC" in eline:
+                        line_units = "METRIC"
+                        # Modified for issue #80
+                        self.convert_units({"INCH": "IN", "METRIC": "MM"}[line_units])
+                        log.warning("Type of UNITS found inline: %s" % line_units)
+                        log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) +
+                                    ':' + str(self.excellon_format_lower_mm))
+                        # TODO: not working
+                        #FlatCAMApp.App.inform.emit("Detected INLINE: %s" % str(eline))
+                        continue
+
+
+                    # Search for zeros type again because it might be alone on the line
+                    match = re.search(r'[LT]Z',eline)
+                    if match:
+                        self.zeros = match.group()
+                        log.warning("Type of zeros found: %s" % self.zeros)
+                        continue
+
+                ## Units and number format outside header##
+                match = self.units_re.match(eline)
+                if match:
+                    self.units_found = match.group(1)
+                    self.zeros = match.group(2)  # "T" or "L". Might be empty
+
+                    # self.units = {"INCH": "IN", "METRIC": "MM"}[match.group(1)]
+
+                    # Modified for issue #80
+                    self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.units_found])
+                    # log.warning("  Units/Format: %s %s" % (self.units, self.zeros))
+                    log.warning("Units: %s" % self.units)
+                    if self.units == 'MM':
+                        log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) +
+                                    ':' + str(self.excellon_format_lower_mm))
+                    else:
+                        log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) +
+                                    ':' + str(self.excellon_format_lower_in))
+                    log.warning("Type of zeros found outside header, inline: %s" % self.zeros)
+
+                    log.warning("UNITS found outside header")
+                    continue
+
+                log.warning("Line ignored: %s" % eline)
+
+            # make sure that since we are in headerless mode, we convert the tools only after the file parsing
+            # is finished since the tools definitions are spread in the Excellon body. We use as units the value
+            # from self.defaults['excellon_units']
+            log.info("Zeros: %s, Units %s." % (self.zeros, self.units))
+
+
+        except Exception as e:
+            log.error("PARSING FAILED. Line %d: %s" % (line_num, eline))
+            self.app.inform.emit('[error] Excellon Parser ERROR.\nPARSING FAILED. Line %d: %s' % (line_num, eline))
+            return "fail"
+        
+    def parse_number(self, number_str):
+        """
+        Parses coordinate numbers without period.
+
+        :param number_str: String representing the numerical value.
+        :type number_str: str
+        :return: Floating point representation of the number
+        :rtype: float
+        """
+
+        match = self.leadingzeros_re.search(number_str)
+        nr_length = len(match.group(1)) + len(match.group(2))
+        try:
+            if self.zeros == "L" or self.zeros == "LZ":
+                # With leading zeros, when you type in a coordinate,
+                # the leading zeros must always be included.  Trailing zeros
+                # are unneeded and may be left off. The CNC-7 will automatically add them.
+                # r'^[-\+]?(0*)(\d*)'
+                # 6 digits are divided by 10^4
+                # If less than size digits, they are automatically added,
+                # 5 digits then are divided by 10^3 and so on.
+
+                if self.units.lower() == "in":
+                    result = float(number_str) / (10 ** (float(nr_length) - float(self.excellon_format_upper_in)))
+                else:
+                    result = float(number_str) / (10 ** (float(nr_length) - float(self.excellon_format_upper_mm)))
+                return result
+            else:  # Trailing
+                # You must show all zeros to the right of the number and can omit
+                # all zeros to the left of the number. The CNC-7 will count the number
+                # of digits you typed and automatically fill in the missing zeros.
+                ## flatCAM expects 6digits
+                # flatCAM expects the number of digits entered into the defaults
+
+                if self.units.lower() == "in":  # Inches is 00.0000
+                    result = float(number_str) / (10 ** (float(self.excellon_format_lower_in)))
+                else:   # Metric is 000.000
+                    result = float(number_str) / (10 ** (float(self.excellon_format_lower_mm)))
+                return result
+        except Exception as e:
+            log.error("Aborted. Operation could not be completed due of %s" % str(e))
+            return
+
+    def create_geometry(self):
+        """
+        Creates circles of the tool diameter at every point
+        specified in ``self.drills``. Also creates geometries (polygons)
+        for the slots as specified in ``self.slots``
+        All the resulting geometry is stored into self.solid_geometry list.
+        The list self.solid_geometry has 2 elements: first is a dict with the drills geometry,
+        and second element is another similar dict that contain the slots geometry.
+
+        Each dict has as keys the tool diameters and as values lists with Shapely objects, the geometries
+        ================  ====================================
+        Key               Value
+        ================  ====================================
+        tool_diameter     list of (Shapely.Point) Where to drill
+        ================  ====================================
+
+        :return: None
+        """
+        self.solid_geometry = []
+        try:
+            for drill in self.drills:
+                # poly = drill['point'].buffer(self.tools[drill['tool']]["C"]/2.0)
+                tooldia = self.tools[drill['tool']]['C']
+                poly = drill['point'].buffer(tooldia / 2.0, int(int(self.geo_steps_per_circle) / 4))
+                self.solid_geometry.append(poly)
+
+            for slot in self.slots:
+                slot_tooldia = self.tools[slot['tool']]['C']
+                start = slot['start']
+                stop = slot['stop']
+
+                lines_string = LineString([start, stop])
+                poly = lines_string.buffer(slot_tooldia / 2.0, int(int(self.geo_steps_per_circle) / 4))
+                self.solid_geometry.append(poly)
+        except:
+            return "fail"
+        # drill_geometry = {}
+        # slot_geometry = {}
+        #
+        # def insertIntoDataStruct(dia, drill_geo, aDict):
+        #     if not dia in aDict:
+        #         aDict[dia] = [drill_geo]
+        #     else:
+        #         aDict[dia].append(drill_geo)
+        #
+        # for tool in self.tools:
+        #     tooldia = self.tools[tool]['C']
+        #     for drill in self.drills:
+        #         if drill['tool'] == tool:
+        #             poly = drill['point'].buffer(tooldia / 2.0)
+        #             insertIntoDataStruct(tooldia, poly, drill_geometry)
+        #
+        # for tool in self.tools:
+        #     slot_tooldia = self.tools[tool]['C']
+        #     for slot in self.slots:
+        #         if slot['tool'] == tool:
+        #             start = slot['start']
+        #             stop = slot['stop']
+        #             lines_string = LineString([start, stop])
+        #             poly = lines_string.buffer(slot_tooldia/2.0, self.geo_steps_per_circle)
+        #             insertIntoDataStruct(slot_tooldia, poly, drill_geometry)
+        #
+        # self.solid_geometry = [drill_geometry, slot_geometry]
+
+    def bounds(self):
+        """
+        Returns coordinates of rectangular bounds
+        of Gerber geometry: (xmin, ymin, xmax, ymax).
+        """
+        # fixed issue of getting bounds only for one level lists of objects
+        # now it can get bounds for nested lists of objects
+
+        log.debug("Excellon() -> bounds()")
+        if self.solid_geometry is None:
+            log.debug("solid_geometry is None")
+            return 0, 0, 0, 0
+
+        def bounds_rec(obj):
+            if type(obj) is list:
+                minx = Inf
+                miny = Inf
+                maxx = -Inf
+                maxy = -Inf
+
+                for k in obj:
+                    if type(k) is dict:
+                        for key in k:
+                            minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
+                            minx = min(minx, minx_)
+                            miny = min(miny, miny_)
+                            maxx = max(maxx, maxx_)
+                            maxy = max(maxy, maxy_)
+                    else:
+                        minx_, miny_, maxx_, maxy_ = bounds_rec(k)
+                        minx = min(minx, minx_)
+                        miny = min(miny, miny_)
+                        maxx = max(maxx, maxx_)
+                        maxy = max(maxy, maxy_)
+                return minx, miny, maxx, maxy
+            else:
+                # it's a Shapely object, return it's bounds
+                return obj.bounds
+
+        bounds_coords = bounds_rec(self.solid_geometry)
+        return bounds_coords
+
+    def convert_units(self, units):
+        """
+        This function first convert to the the units found in the Excellon file but it converts tools that
+        are not there yet so it has no effect other than it signal that the units are the ones in the file.
+
+        On object creation, in new_object(), true conversion is done because this is done at the end of the
+        Excellon file parsing, the tools are inside and self.tools is really converted from the units found
+        inside the file to the FlatCAM units.
+
+        Kind of convolute way to make the conversion and it is based on the assumption that the Excellon file
+        will have detected the units before the tools are parsed and stored in self.tools
+        :param units:
+        :type str: IN or MM
+        :return:
+        """
+        factor = Geometry.convert_units(self, units)
+
+        # Tools
+        for tname in self.tools:
+            self.tools[tname]["C"] *= factor
+
+        self.create_geometry()
+
+        return factor
+
+    def scale(self, xfactor, yfactor=None, point=None):
+        """
+        Scales geometry on the XY plane in the object by a given factor.
+        Tool sizes, feedrates an Z-plane dimensions are untouched.
+
+        :param factor: Number by which to scale the object.
+        :type factor: float
+        :return: None
+        :rtype: NOne
+        """
+        if yfactor is None:
+            yfactor = xfactor
+
+        if point is None:
+            px = 0
+            py = 0
+        else:
+            px, py = point
+
+        # Drills
+        for drill in self.drills:
+            drill['point'] = affinity.scale(drill['point'], xfactor, yfactor, origin=(px, py))
+
+        # Slots
+        for slot in self.slots:
+            slot['stop'] = affinity.scale(slot['stop'], xfactor, yfactor, origin=(px, py))
+            slot['start'] = affinity.scale(slot['start'], xfactor, yfactor, origin=(px, py))
+
+        self.create_geometry()
+
+    def offset(self, vect):
+        """
+        Offsets geometry on the XY plane in the object by a given vector.
+
+        :param vect: (x, y) offset vector.
+        :type vect: tuple
+        :return: None
+        """
+
+        dx, dy = vect
+
+        # Drills
+        for drill in self.drills:
+            drill['point'] = affinity.translate(drill['point'], xoff=dx, yoff=dy)
+
+        # Slots
+        for slot in self.slots:
+            slot['stop'] = affinity.translate(slot['stop'], xoff=dx, yoff=dy)
+            slot['start'] = affinity.translate(slot['start'],xoff=dx, yoff=dy)
+
+        # Recreate geometry
+        self.create_geometry()
+
+    def mirror(self, axis, point):
+        """
+
+        :param axis: "X" or "Y" indicates around which axis to mirror.
+        :type axis: str
+        :param point: [x, y] point belonging to the mirror axis.
+        :type point: list
+        :return: None
+        """
+        px, py = point
+        xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
+
+        # Modify data
+        # Drills
+        for drill in self.drills:
+            drill['point'] = affinity.scale(drill['point'], xscale, yscale, origin=(px, py))
+
+        # Slots
+        for slot in self.slots:
+            slot['stop'] = affinity.scale(slot['stop'], xscale, yscale, origin=(px, py))
+            slot['start'] = affinity.scale(slot['start'], xscale, yscale, origin=(px, py))
+
+        # Recreate geometry
+        self.create_geometry()
+
+    def skew(self, angle_x=None, angle_y=None, point=None):
+        """
+        Shear/Skew the geometries of an object by angles along x and y dimensions.
+        Tool sizes, feedrates an Z-plane dimensions are untouched.
+
+        Parameters
+        ----------
+        xs, ys : float, float
+            The shear angle(s) for the x and y axes respectively. These can be
+            specified in either degrees (default) or radians by setting
+            use_radians=True.
+
+        See shapely manual for more information:
+        http://toblerity.org/shapely/manual.html#affine-transformations
+        """
+        if angle_x is None:
+            angle_x = 0.0
+
+        if angle_y is None:
+            angle_y = 0.0
+
+        if point is None:
+            # Drills
+            for drill in self.drills:
+                drill['point'] = affinity.skew(drill['point'], angle_x, angle_y,
+                                               origin=(0, 0))
+
+            # Slots
+            for slot in self.slots:
+                slot['stop'] = affinity.skew(slot['stop'], angle_x, angle_y, origin=(0, 0))
+                slot['start'] = affinity.skew(slot['start'], angle_x, angle_y, origin=(0, 0))
+        else:
+            px, py = point
+            # Drills
+            for drill in self.drills:
+                drill['point'] = affinity.skew(drill['point'], angle_x, angle_y,
+                                               origin=(px, py))
+
+            # Slots
+            for slot in self.slots:
+                slot['stop'] = affinity.skew(slot['stop'], angle_x, angle_y, origin=(px, py))
+                slot['start'] = affinity.skew(slot['start'], angle_x, angle_y, origin=(px, py))
+
+        self.create_geometry()
+
+    def rotate(self, angle, point=None):
+        """
+        Rotate the geometry of an object by an angle around the 'point' coordinates
+        :param angle:
+        :param point: tuple of coordinates (x, y)
+        :return:
+        """
+        if point is None:
+            # Drills
+            for drill in self.drills:
+                drill['point'] = affinity.rotate(drill['point'], angle, origin='center')
+
+            # Slots
+            for slot in self.slots:
+                slot['stop'] = affinity.rotate(slot['stop'], angle, origin='center')
+                slot['start'] = affinity.rotate(slot['start'], angle, origin='center')
+        else:
+            px, py = point
+            # Drills
+            for drill in self.drills:
+                drill['point'] = affinity.rotate(drill['point'], angle, origin=(px, py))
+
+            # Slots
+            for slot in self.slots:
+                slot['stop'] = affinity.rotate(slot['stop'], angle, origin=(px, py))
+                slot['start'] = affinity.rotate(slot['start'], angle, origin=(px, py))
+
+        self.create_geometry()
+
+
+class AttrDict(dict):
+    def __init__(self, *args, **kwargs):
+        super(AttrDict, self).__init__(*args, **kwargs)
+        self.__dict__ = self
+
+
+class CNCjob(Geometry):
+    """
+    Represents work to be done by a CNC machine.
+
+    *ATTRIBUTES*
+
+    * ``gcode_parsed`` (list): Each is a dictionary:
+
+    =====================  =========================================
+    Key                    Value
+    =====================  =========================================
+    geom                   (Shapely.LineString) Tool path (XY plane)
+    kind                   (string) "AB", A is "T" (travel) or
+                           "C" (cut). B is "F" (fast) or "S" (slow).
+    =====================  =========================================
+    """
+
+    defaults = {
+        "global_zdownrate": None,
+        "pp_geometry_name":'default',
+        "pp_excellon_name":'default',
+        "excellon_optimization_type": "B",
+        "steps_per_circle": 64
+    }
+
+    def __init__(self,
+                 units="in", kind="generic", tooldia=0.0,
+                 z_cut=-0.002, z_move=0.1,
+                 feedrate=3.0, feedrate_z=3.0, feedrate_rapid=3.0,
+                 pp_geometry_name='default', pp_excellon_name='default',
+                 depthpercut = 0.1,
+                 spindlespeed=None, dwell=True, dwelltime=1000,
+                 toolchangez=0.787402,
+                 endz=2.0,
+                 steps_per_circle=None):
+
+        # Used when parsing G-code arcs
+        if steps_per_circle is None:
+            steps_per_circle = CNCjob.defaults["steps_per_circle"]
+        self.steps_per_circle = steps_per_circle
+
+        Geometry.__init__(self, geo_steps_per_circle=steps_per_circle)
+
+        self.kind = kind
+        self.units = units
+
+        self.z_cut = z_cut
+        self.z_move = z_move
+
+        self.feedrate = feedrate
+        self.feedrate_z = feedrate_z
+        self.feedrate_rapid = feedrate_rapid
+
+        self.tooldia = tooldia
+        self.toolchangez = toolchangez
+        self.toolchange_xy = None
+
+        self.endz = endz
+        self.depthpercut = depthpercut
+
+        self.unitcode = {"IN": "G20", "MM": "G21"}
+
+        self.feedminutecode = "G94"
+        self.absolutecode = "G90"
+
+        self.gcode = ""
+        self.gcode_parsed = None
+
+        self.pp_geometry_name = pp_geometry_name
+        self.pp_geometry = self.app.postprocessors[self.pp_geometry_name]
+
+        self.pp_excellon_name = pp_excellon_name
+        self.pp_excellon = self.app.postprocessors[self.pp_excellon_name]
+
+        self.spindlespeed = spindlespeed
+        self.dwell = dwell
+        self.dwelltime = dwelltime
+
+        self.input_geometry_bounds = None
+
+        # Attributes to be included in serialization
+        # Always append to it because it carries contents
+        # from Geometry.
+        self.ser_attrs += ['kind', 'z_cut', 'z_move', 'toolchangez', 'feedrate', 'feedrate_z', 'feedrate_rapid',
+                           'tooldia', 'gcode', 'input_geometry_bounds', 'gcode_parsed', 'steps_per_circle',
+                           'depthpercut', 'spindlespeed', 'dwell', 'dwelltime']
+
+    @property
+    def postdata(self):
+        return self.__dict__
+
+    def convert_units(self, units):
+        factor = Geometry.convert_units(self, units)
+        log.debug("CNCjob.convert_units()")
+
+        self.z_cut = float(self.z_cut) * factor
+        self.z_move *= factor
+        self.feedrate *= factor
+        self.feedrate_z *= factor
+        self.feedrate_rapid *= factor
+        self.tooldia *= factor
+        self.toolchangez *= factor
+        self.endz *= factor
+        self.depthpercut = float(self.depthpercut) * factor
+
+        return factor
+
+    def doformat(self, fun, **kwargs):
+        return self.doformat2(fun, **kwargs) + "\n"
+
+    def doformat2(self, fun, **kwargs):
+        attributes = AttrDict()
+        attributes.update(self.postdata)
+        attributes.update(kwargs)
+        try:
+            returnvalue = fun(attributes)
+            return returnvalue
+        except Exception as e:
+            self.app.log.error('Exception ocurred inside a postprocessor: ' + traceback.format_exc())
+            return ''
+
+    def optimized_travelling_salesman(self, points, start=None):
+        """
+        As solving the problem in the brute force way is too slow,
+        this function implements a simple heuristic: always
+        go to the nearest city.
+
+        Even if this algorithm is extremely simple, it works pretty well
+        giving a solution only about 25% longer than the optimal one (cit. Wikipedia),
+        and runs very fast in O(N^2) time complexity.
+
+        >>> optimized_travelling_salesman([[i,j] for i in range(5) for j in range(5)])
+        [[0, 0], [0, 1], [0, 2], [0, 3], [0, 4], [1, 4], [1, 3], [1, 2], [1, 1], [1, 0], [2, 0], [2, 1], [2, 2],
+        [2, 3], [2, 4], [3, 4], [3, 3], [3, 2], [3, 1], [3, 0], [4, 0], [4, 1], [4, 2], [4, 3], [4, 4]]
+        >>> optimized_travelling_salesman([[0,0],[10,0],[6,0]])
+        [[0, 0], [6, 0], [10, 0]]
+        """
+        if start is None:
+            start = points[0]
+        must_visit = points
+        path = [start]
+        # must_visit.remove(start)
+        while must_visit:
+            nearest = min(must_visit, key=lambda x: distance(path[-1], x))
+            path.append(nearest)
+            must_visit.remove(nearest)
+        return path
+
+    def generate_from_excellon_by_tool(self, exobj, tools="all", drillz = 3.0,
+                                       toolchange=False, toolchangez=0.1, toolchangexy="0.0, 0.0",
+                                       endz=2.0, startz=None,
+                                       excellon_optimization_type='B'):
+        """
+        Creates gcode for this object from an Excellon object
+        for the specified tools.
+
+        :param exobj: Excellon object to process
+        :type exobj: Excellon
+        :param tools: Comma separated tool names
+        :type: tools: str
+        :param drillz: drill Z depth
+        :type drillz: float
+        :param toolchange: Use tool change sequence between tools.
+        :type toolchange: bool
+        :param toolchangez: Height at which to perform the tool change.
+        :type toolchangez: float
+        :param toolchangexy: Toolchange X,Y position
+        :type toolchangexy: String containing 2 floats separated by comma
+        :param startz: Z position just before starting the job
+        :type startz: float
+        :param endz: final Z position to move to at the end of the CNC job
+        :type endz: float
+        :param excellon_optimization_type: Single character that defines which drill re-ordering optimisation algorithm
+        is to be used: 'M' for meta-heuristic and 'B' for basic
+        :type excellon_optimization_type: string
+        :return: None
+        :rtype: None
+        """
+        if drillz > 0:
+            self.app.inform.emit("[warning] The Cut Z parameter has positive value. "
+                                 "It is the depth value to drill into material.\n"
+                                 "The Cut Z parameter needs to have a negative value, assuming it is a typo "
+                                 "therefore the app will convert the value to negative. "
+                                 "Check the resulting CNC code (Gcode etc).")
+            self.z_cut = -drillz
+        elif drillz == 0:
+            self.app.inform.emit("[warning] The Cut Z parameter is zero. "
+                                 "There will be no cut, skipping %s file" % exobj.options['name'])
+            return
+        else:
+            self.z_cut = drillz
+
+        self.toolchangez = toolchangez
+        self.toolchange_xy = [float(eval(a)) for a in toolchangexy.split(",")]
+
+        self.startz = startz
+        self.endz = endz
+
+        log.debug("Creating CNC Job from Excellon...")
+
+        # Tools
+        
+        # sort the tools list by the second item in tuple (here we have a dict with diameter of the tool)
+        # so we actually are sorting the tools by diameter
+        #sorted_tools = sorted(exobj.tools.items(), key=lambda t1: t1['C'])
+
+        sort = []
+        for k, v in list(exobj.tools.items()):
+            sort.append((k, v.get('C')))
+        sorted_tools = sorted(sort,key=lambda t1: t1[1])
+
+        if tools == "all":
+            tools = [i[0] for i in sorted_tools]   # we get a array of ordered tools
+            log.debug("Tools 'all' and sorted are: %s" % str(tools))
+        else:
+            selected_tools = [x.strip() for x in tools.split(",")]  # we strip spaces and also separate the tools by ','
+            selected_tools = [t1 for t1 in selected_tools if t1 in selected_tools]
+
+            # Create a sorted list of selected tools from the sorted_tools list
+            tools = [i for i, j in sorted_tools for k in selected_tools if i == k]
+            log.debug("Tools selected and sorted are: %s" % str(tools))
+
+        # Points (Group by tool)
+        points = {}
+        for drill in exobj.drills:
+            if drill['tool'] in tools:
+                try:
+                    points[drill['tool']].append(drill['point'])
+                except KeyError:
+                    points[drill['tool']] = [drill['point']]
+
+        #log.debug("Found %d drills." % len(points))
+
+        self.gcode = []
+
+        # Basic G-Code macros
+        self.pp_excellon = self.app.postprocessors[self.pp_excellon_name]
+        p = self.pp_excellon
+
+        # Initialization
+        gcode = self.doformat(p.start_code)
+        gcode += self.doformat(p.feedrate_code)
+        gcode += self.doformat(p.lift_code, x=0, y=0)
+        gcode += self.doformat(p.startz_code)
+
+        # Distance callback
+        class CreateDistanceCallback(object):
+            """Create callback to calculate distances between points."""
+
+            def __init__(self):
+                """Initialize distance array."""
+                locations = create_data_array()
+                size = len(locations)
+                self.matrix = {}
+
+                for from_node in range(size):
+                    self.matrix[from_node] = {}
+                    for to_node in range(size):
+                        if from_node == to_node:
+                            self.matrix[from_node][to_node] = 0
+                        else:
+                            x1 = locations[from_node][0]
+                            y1 = locations[from_node][1]
+                            x2 = locations[to_node][0]
+                            y2 = locations[to_node][1]
+                            self.matrix[from_node][to_node] = distance_euclidian(x1, y1, x2, y2)
+
+            def Distance(self, from_node, to_node):
+                return int(self.matrix[from_node][to_node])
+
+        # Create the data.
+        def create_data_array():
+            locations = []
+            for point in points[tool]:
+                locations.append((point.coords.xy[0][0], point.coords.xy[1][0]))
+            return locations
+
+        current_platform = platform.architecture()[0]
+        if current_platform == '64bit':
+            if excellon_optimization_type == 'M':
+                log.debug("Using OR-Tools Metaheuristic Guided Local Search drill path optimization.")
+                for tool in tools:
+                    self.tool=tool
+                    self.postdata['toolC']=exobj.tools[tool]["C"]
+
+                    ################################################
+                    # Create the data.
+                    node_list = []
+                    locations = create_data_array()
+                    tsp_size = len(locations)
+                    num_routes = 1  # The number of routes, which is 1 in the TSP.
+                    # Nodes are indexed from 0 to tsp_size - 1. The depot is the starting node of the route.
+                    depot = 0
+                    # Create routing model.
+                    if tsp_size > 0:
+                        routing = pywrapcp.RoutingModel(tsp_size, num_routes, depot)
+                        search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters()
+                        search_parameters.local_search_metaheuristic = (
+                            routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)
+
+                        # Set search time limit in milliseconds.
+                        if float(self.app.defaults["excellon_search_time"]) != 0:
+                            search_parameters.time_limit_ms = int(float(self.app.defaults["excellon_search_time"]) * 1000)
+                        else:
+                            search_parameters.time_limit_ms = 3000
+
+                        # Callback to the distance function. The callback takes two
+                        # arguments (the from and to node indices) and returns the distance between them.
+                        dist_between_locations = CreateDistanceCallback()
+                        dist_callback = dist_between_locations.Distance
+                        routing.SetArcCostEvaluatorOfAllVehicles(dist_callback)
+
+                        # Solve, returns a solution if any.
+                        assignment = routing.SolveWithParameters(search_parameters)
+
+                        if assignment:
+                            # Solution cost.
+                            log.info("Total distance: " + str(assignment.ObjectiveValue()))
+
+                            # Inspect solution.
+                            # Only one route here; otherwise iterate from 0 to routing.vehicles() - 1.
+                            route_number = 0
+                            node = routing.Start(route_number)
+                            start_node = node
+
+                            while not routing.IsEnd(node):
+                                node_list.append(node)
+                                node = assignment.Value(routing.NextVar(node))
+                        else:
+                            log.warning('No solution found.')
+                    else:
+                        log.warning('Specify an instance greater than 0.')
+                    ################################################
+
+                    # Only if tool has points.
+                    if tool in points:
+                        # Tool change sequence (optional)
+                        if toolchange:
+                            gcode += self.doformat(p.toolchange_code)
+                            gcode += self.doformat(p.spindle_code)  # Spindle start
+                            if self.dwell is True:
+                                gcode += self.doformat(p.dwell_code)  # Dwell time
+                        else:
+                            gcode += self.doformat(p.spindle_code)
+                            if self.dwell is True:
+                                gcode += self.doformat(p.dwell_code)  # Dwell time
+
+                        # Drillling!
+                        oldx = 0
+                        oldy = 0
+                        measured_distance = 0
+                        for k in node_list:
+                            locx = locations[k][0]
+                            locy = locations[k][1]
+                            gcode += self.doformat(p.rapid_code, x=locx, y=locy)
+                            gcode += self.doformat(p.down_code, x=locx, y=locy)
+                            gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
+                            gcode += self.doformat(p.lift_code, x=locx, y=locy)
+                            measured_distance += abs(distance_euclidian(locx, locy, oldx, oldy))
+                            oldx = locx
+                            oldy = locy
+                        log.debug("The total travel distance with Metaheuristics is: %s" % str(measured_distance) + '\n')
+            elif excellon_optimization_type == 'B':
+                log.debug("Using OR-Tools Basic drill path optimization.")
+                for tool in tools:
+                    self.tool=tool
+                    self.postdata['toolC']=exobj.tools[tool]["C"]
+
+                    ################################################
+                    node_list = []
+                    locations = create_data_array()
+                    tsp_size = len(locations)
+                    num_routes = 1  # The number of routes, which is 1 in the TSP.
+
+                    # Nodes are indexed from 0 to tsp_size - 1. The depot is the starting node of the route.
+                    depot = 0
+
+                    # Create routing model.
+                    if tsp_size > 0:
+                        routing = pywrapcp.RoutingModel(tsp_size, num_routes, depot)
+                        search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters()
+
+                        # Callback to the distance function. The callback takes two
+                        # arguments (the from and to node indices) and returns the distance between them.
+                        dist_between_locations = CreateDistanceCallback()
+                        dist_callback = dist_between_locations.Distance
+                        routing.SetArcCostEvaluatorOfAllVehicles(dist_callback)
+
+                        # Solve, returns a solution if any.
+                        assignment = routing.SolveWithParameters(search_parameters)
+
+                        if assignment:
+                            # Solution cost.
+                            log.info("Total distance: " + str(assignment.ObjectiveValue()))
+
+                            # Inspect solution.
+                            # Only one route here; otherwise iterate from 0 to routing.vehicles() - 1.
+                            route_number = 0
+                            node = routing.Start(route_number)
+                            start_node = node
+
+                            while not routing.IsEnd(node):
+                                node_list.append(node)
+                                node = assignment.Value(routing.NextVar(node))
+                        else:
+                            log.warning('No solution found.')
+                    else:
+                        log.warning('Specify an instance greater than 0.')
+                    ################################################
+
+                    # Only if tool has points.
+                    if tool in points:
+                        # Tool change sequence (optional)
+                        if toolchange:
+                            gcode += self.doformat(p.toolchange_code)
+                            gcode += self.doformat(p.spindle_code)  # Spindle start)
+                            if self.dwell is True:
+                                gcode += self.doformat(p.dwell_code)  # Dwell time
+                        else:
+                            gcode += self.doformat(p.spindle_code)
+                            if self.dwell is True:
+                                gcode += self.doformat(p.dwell_code)  # Dwell time
+
+                        # Drillling!
+                        oldx = 0
+                        oldy = 0
+                        measured_distance = 0
+                        for k in node_list:
+                            locx = locations[k][0]
+                            locy = locations[k][1]
+                            gcode += self.doformat(p.rapid_code, x=locx, y=locy)
+                            gcode += self.doformat(p.down_code, x=locx, y=locy)
+                            gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
+                            gcode += self.doformat(p.lift_code, x=locx, y=locy)
+                            measured_distance += abs(distance_euclidian(locx, locy, oldx, oldy))
+                            oldx = locx
+                            oldy = locy
+                        log.debug("The total travel distance with Basic Algorithm is: %s" % str(measured_distance) + '\n')
+            else:
+                self.app.inform.emit("[error_notcl] Wrong optimization type selected.")
+                return
+        else:
+            log.debug("Using Travelling Salesman drill path optimization.")
+            for tool in tools:
+                self.tool = tool
+                self.postdata['toolC'] = exobj.tools[tool]["C"]
+
+                # Only if tool has points.
+                if tool in points:
+                    # Tool change sequence (optional)
+                    if toolchange:
+                        gcode += self.doformat(p.toolchange_code)
+                        gcode += self.doformat(p.spindle_code)  # Spindle start)
+                        if self.dwell is True:
+                            gcode += self.doformat(p.dwell_code)  # Dwell time
+                    else:
+                        gcode += self.doformat(p.spindle_code)
+                        if self.dwell is True:
+                            gcode += self.doformat(p.dwell_code)  # Dwell time
+
+                    # Drillling!
+                    altPoints = []
+                    for point in points[tool]:
+                        altPoints.append((point.coords.xy[0][0], point.coords.xy[1][0]))
+
+                    for point in self.optimized_travelling_salesman(altPoints):
+                        gcode += self.doformat(p.rapid_code, x=point[0], y=point[1])
+                        gcode += self.doformat(p.down_code, x=point[0], y=point[1])
+                        gcode += self.doformat(p.up_to_zero_code, x=point[0], y=point[1])
+                        gcode += self.doformat(p.lift_code, x=point[0], y=point[1])
+
+        gcode += self.doformat(p.spindle_stop_code)  # Spindle stop
+        gcode += self.doformat(p.end_code, x=0, y=0)
+
+        self.gcode = gcode
+
+    def generate_from_multitool_geometry(self, geometry, append=True,
+                                 tooldia=None, offset=0.0, tolerance=0,
+                                 z_cut=1.0, z_move=2.0,
+                                 feedrate=2.0, feedrate_z=2.0, feedrate_rapid=30,
+                                 spindlespeed=None, dwell=False, dwelltime=1.0,
+                                 multidepth=False, depthpercut=None,
+                                 toolchange=False, toolchangez=1.0, toolchangexy="0.0, 0.0",
+                                 extracut=False, startz=None, endz=2.0,
+                                 pp_geometry_name=None, tool_no=1):
+        """
+        Second algorithm to generate from Geometry.
+
+        Algorithm description:
+        ----------------------
+        Uses RTree to find the nearest path to follow.
+
+        :param geometry:
+        :param append:
+        :param tooldia:
+        :param tolerance:
+        :param multidepth: If True, use multiple passes to reach
+           the desired depth.
+        :param depthpercut: Maximum depth in each pass.
+        :param extracut: Adds (or not) an extra cut at the end of each path
+            overlapping the first point in path to ensure complete copper removal
+        :return: None
+        """
+
+        log.debug("Generate_from_geometry_2()")
+
+        temp_solid_geometry = []
+        if offset != 0.0:
+            for it in geometry:
+                # if the geometry is a closed shape then create a Polygon out of it
+                if isinstance(it, LineString):
+                    c = it.coords
+                    if c[0] == c[-1]:
+                        it = Polygon(it)
+                temp_solid_geometry.append(it.buffer(offset, join_style=2))
+        else:
+            temp_solid_geometry = geometry
+
+        ## Flatten the geometry. Only linear elements (no polygons) remain.
+        flat_geometry = self.flatten(temp_solid_geometry, pathonly=True)
+        log.debug("%d paths" % len(flat_geometry))
+
+        self.tooldia = tooldia
+        self.z_cut = z_cut
+        self.z_move = z_move
+
+        self.feedrate = feedrate
+        self.feedrate_z = feedrate_z
+        self.feedrate_rapid = feedrate_rapid
+
+        self.spindlespeed = spindlespeed
+        self.dwell = dwell
+        self.dwelltime = dwelltime
+
+        self.startz = startz
+        self.endz = endz
+
+        self.depthpercut = depthpercut
+        self.multidepth = multidepth
+
+        self.toolchangez = toolchangez
+        self.toolchange_xy = [float(eval(a)) for a in toolchangexy.split(",")]
+
+        self.pp_geometry_name = pp_geometry_name if pp_geometry_name else 'default'
+
+        if self.z_cut > 0:
+            self.app.inform.emit("[warning] The Cut Z parameter has positive value. "
+                                 "It is the depth value to cut into material.\n"
+                                 "The Cut Z parameter needs to have a negative value, assuming it is a typo "
+                                 "therefore the app will convert the value to negative."
+                                 "Check the resulting CNC code (Gcode etc).")
+            self.z_cut = -self.z_cut
+        elif self.z_cut == 0:
+            self.app.inform.emit("[warning] The Cut Z parameter is zero. "
+                                 "There will be no cut, skipping %s file" % self.options['name'])
+
+        ## Index first and last points in paths
+        # What points to index.
+        def get_pts(o):
+            return [o.coords[0], o.coords[-1]]
+
+        # Create the indexed storage.
+        storage = FlatCAMRTreeStorage()
+        storage.get_points = get_pts
+
+        # Store the geometry
+        log.debug("Indexing geometry before generating G-Code...")
+        for shape in flat_geometry:
+            if shape is not None:  # TODO: This shouldn't have happened.
+                storage.insert(shape)
+
+        # self.input_geometry_bounds = geometry.bounds()
+
+        if not append:
+            self.gcode = ""
+
+        # tell postprocessor the number of tool (for toolchange)
+        self.tool = tool_no
+
+        # this is the tool diameter, it is used as such to accommodate the postprocessor who need the tool diameter
+        # given under the name 'toolC'
+        self.postdata['toolC'] = self.tooldia
+
+        # Initial G-Code
+        self.pp_geometry = self.app.postprocessors[self.pp_geometry_name]
+        p = self.pp_geometry
+
+        self.gcode = self.doformat(p.start_code)
+
+        self.gcode += self.doformat(p.feedrate_code)        # sets the feed rate
+        self.gcode += self.doformat(p.lift_code, x=0, y=0)  # Move (up) to travel height
+        self.gcode += self.doformat(p.startz_code)
+
+        if toolchange:
+            self.gcode += self.doformat(p.toolchange_code)
+            self.gcode += self.doformat(p.spindle_code)     # Spindle start
+            if self.dwell is True:
+                self.gcode += self.doformat(p.dwell_code)   # Dwell time
+        else:
+            self.gcode += self.doformat(p.spindle_code)     # Spindle start
+            if self.dwell is True:
+                self.gcode += self.doformat(p.dwell_code)   # Dwell time
+
+        ## Iterate over geometry paths getting the nearest each time.
+        log.debug("Starting G-Code...")
+        path_count = 0
+        current_pt = (0, 0)
+        pt, geo = storage.nearest(current_pt)
+        try:
+            while True:
+                path_count += 1
+
+                # Remove before modifying, otherwise deletion will fail.
+                storage.remove(geo)
+
+                # If last point in geometry is the nearest but prefer the first one if last point == first point
+                # then reverse coordinates.
+                if pt != geo.coords[0] and pt == geo.coords[-1]:
+                    geo.coords = list(geo.coords)[::-1]
+
+                #---------- Single depth/pass --------
+                if not multidepth:
+                    self.gcode += self.create_gcode_single_pass(geo, extracut, tolerance)
+
+                #--------- Multi-pass ---------
+                else:
+                    self.gcode += self.create_gcode_multi_pass(geo, extracut, tolerance,
+                                                               postproc=p, current_point=current_pt)
+
+                current_pt = geo.coords[-1]
+                pt, geo = storage.nearest(current_pt) # Next
+
+        except StopIteration:  # Nothing found in storage.
+            pass
+
+        log.debug("Finishing G-Code... %s paths traced." % path_count)
+
+        # Finish
+        self.gcode += self.doformat(p.spindle_stop_code)
+        self.gcode += self.doformat(p.lift_code, x=current_pt[0], y=current_pt[1])
+        self.gcode += self.doformat(p.end_code, x=0, y=0)
+
+        return self.gcode
+
+    def generate_from_geometry_2(self, geometry, append=True,
+                                 tooldia=None, offset=0.0, tolerance=0,
+                                 z_cut=1.0, z_move=2.0,
+                                 feedrate=2.0, feedrate_z=2.0, feedrate_rapid=30,
+                                 spindlespeed=None, dwell=False, dwelltime=1.0,
+                                 multidepth=False, depthpercut=None,
+                                 toolchange=False, toolchangez=1.0, toolchangexy="0.0, 0.0",
+                                 extracut=False, startz=None, endz=2.0,
+                                 pp_geometry_name=None, tool_no=1):
+        """
+        Second algorithm to generate from Geometry.
+
+        Algorithm description:
+        ----------------------
+        Uses RTree to find the nearest path to follow.
+
+        :param geometry:
+        :param append:
+        :param tooldia:
+        :param tolerance:
+        :param multidepth: If True, use multiple passes to reach
+           the desired depth.
+        :param depthpercut: Maximum depth in each pass.
+        :param extracut: Adds (or not) an extra cut at the end of each path
+            overlapping the first point in path to ensure complete copper removal
+        :return: None
+        """
+        assert isinstance(geometry, Geometry), "Expected a Geometry, got %s" % type(geometry)
+        log.debug("Generate_from_geometry_2()")
+
+        # if solid_geometry is empty raise an exception
+        if not geometry.solid_geometry:
+            self.app.inform.emit("[error_notcl]Trying to generate a CNC Job "
+                                 "from a Geometry object without solid_geometry.")
+
+        temp_solid_geometry = []
+        if offset != 0.0:
+            for it in geometry.solid_geometry:
+                # if the geometry is a closed shape then create a Polygon out of it
+                if isinstance(it, LineString):
+                    c = it.coords
+                    if c[0] == c[-1]:
+                        it = Polygon(it)
+                temp_solid_geometry.append(it.buffer(offset, join_style=2))
+        else:
+            temp_solid_geometry = geometry.solid_geometry
+
+        ## Flatten the geometry. Only linear elements (no polygons) remain.
+        flat_geometry = self.flatten(temp_solid_geometry, pathonly=True)
+        log.debug("%d paths" % len(flat_geometry))
+
+        self.tooldia = tooldia
+        self.z_cut = z_cut
+        self.z_move = z_move
+
+        self.feedrate = feedrate
+        self.feedrate_z = feedrate_z
+        self.feedrate_rapid = feedrate_rapid
+
+        self.spindlespeed = spindlespeed
+        self.dwell = dwell
+        self.dwelltime = dwelltime
+
+        self.startz = startz
+        self.endz = endz
+
+        self.depthpercut = depthpercut
+        self.multidepth = multidepth
+
+        self.toolchangez = toolchangez
+        self.toolchange_xy = [float(eval(a)) for a in toolchangexy.split(",")]
+
+        self.pp_geometry_name = pp_geometry_name if pp_geometry_name else 'default'
+
+        if self.z_cut > 0:
+            self.app.inform.emit("[warning] The Cut Z parameter has positive value. "
+                                 "It is the depth value to cut into material.\n"
+                                 "The Cut Z parameter needs to have a negative value, assuming it is a typo "
+                                 "therefore the app will convert the value to negative."
+                                 "Check the resulting CNC code (Gcode etc).")
+            self.z_cut = -self.z_cut
+        elif self.z_cut == 0:
+            self.app.inform.emit("[warning] The Cut Z parameter is zero. "
+                                 "There will be no cut, skipping %s file" % geometry.options['name'])
+
+        ## Index first and last points in paths
+        # What points to index.
+        def get_pts(o):
+            return [o.coords[0], o.coords[-1]]
+
+        # Create the indexed storage.
+        storage = FlatCAMRTreeStorage()
+        storage.get_points = get_pts
+
+        # Store the geometry
+        log.debug("Indexing geometry before generating G-Code...")
+        for shape in flat_geometry:
+            if shape is not None:  # TODO: This shouldn't have happened.
+                storage.insert(shape)
+
+        # self.input_geometry_bounds = geometry.bounds()
+
+        if not append:
+            self.gcode = ""
+
+        # tell postprocessor the number of tool (for toolchange)
+        self.tool = tool_no
+
+        # this is the tool diameter, it is used as such to accommodate the postprocessor who need the tool diameter
+        # given under the name 'toolC'
+        self.postdata['toolC'] = self.tooldia
+
+        # Initial G-Code
+        self.pp_geometry = self.app.postprocessors[self.pp_geometry_name]
+        p = self.pp_geometry
+
+        self.gcode = self.doformat(p.start_code)
+
+        self.gcode += self.doformat(p.feedrate_code)        # sets the feed rate
+        self.gcode += self.doformat(p.lift_code, x=0, y=0)  # Move (up) to travel height
+        self.gcode += self.doformat(p.startz_code)
+
+        if toolchange:
+            self.gcode += self.doformat(p.toolchange_code)
+            self.gcode += self.doformat(p.spindle_code)     # Spindle start
+            if self.dwell is True:
+                self.gcode += self.doformat(p.dwell_code)   # Dwell time
+        else:
+            self.gcode += self.doformat(p.spindle_code)     # Spindle start
+            if self.dwell is True:
+                self.gcode += self.doformat(p.dwell_code)   # Dwell time
+
+        ## Iterate over geometry paths getting the nearest each time.
+        log.debug("Starting G-Code...")
+        path_count = 0
+        current_pt = (0, 0)
+        pt, geo = storage.nearest(current_pt)
+        try:
+            while True:
+                path_count += 1
+
+                # Remove before modifying, otherwise deletion will fail.
+                storage.remove(geo)
+
+                # If last point in geometry is the nearest but prefer the first one if last point == first point
+                # then reverse coordinates.
+                if pt != geo.coords[0] and pt == geo.coords[-1]:
+                    geo.coords = list(geo.coords)[::-1]
+
+                #---------- Single depth/pass --------
+                if not multidepth:
+                    self.gcode += self.create_gcode_single_pass(geo, extracut, tolerance)
+
+                #--------- Multi-pass ---------
+                else:
+                    self.gcode += self.create_gcode_multi_pass(geo, extracut, tolerance,
+                                                               postproc=p, current_point=current_pt)
+
+                current_pt = geo.coords[-1]
+                pt, geo = storage.nearest(current_pt) # Next
+
+        except StopIteration:  # Nothing found in storage.
+            pass
+
+        log.debug("Finishing G-Code... %s paths traced." % path_count)
+
+        # Finish
+        self.gcode += self.doformat(p.spindle_stop_code)
+        self.gcode += self.doformat(p.lift_code, x=current_pt[0], y=current_pt[1])
+        self.gcode += self.doformat(p.end_code, x=0, y=0)
+
+        return self.gcode
+
+    def create_gcode_single_pass(self, geometry, extracut, tolerance):
+        # G-code. Note: self.linear2gcode() and self.point2gcode() will lower and raise the tool every time.
+        gcode_single_pass = ''
+
+        if type(geometry) == LineString or type(geometry) == LinearRing:
+            if extracut is False:
+                gcode_single_pass = self.linear2gcode(geometry, tolerance=tolerance, )
+            else:
+                if geometry.is_ring:
+                    gcode_single_pass = self.linear2gcode_extra(geometry, tolerance=tolerance)
+                else:
+                    gcode_single_pass = self.linear2gcode(geometry, tolerance=tolerance)
+        elif type(geometry) == Point:
+            gcode_single_pass = self.point2gcode(geometry)
+        else:
+            log.warning("G-code generation not implemented for %s" % (str(type(geometry))))
+            return
+
+        return gcode_single_pass
+
+    def create_gcode_multi_pass(self, geometry, extracut, tolerance, postproc, current_point):
+
+        gcode_multi_pass = ''
+
+        if isinstance(self.z_cut, Decimal):
+            z_cut = self.z_cut
+        else:
+            z_cut = Decimal(self.z_cut).quantize(Decimal('0.000000001'))
+
+        if self.depthpercut is None:
+            self.depthpercut = z_cut
+        elif not isinstance(self.depthpercut, Decimal):
+            self.depthpercut = Decimal(self.depthpercut).quantize(Decimal('0.000000001'))
+
+        depth = 0
+        reverse = False
+        while depth > z_cut:
+
+            # Increase depth. Limit to z_cut.
+            depth -= self.depthpercut
+            if depth < z_cut:
+                depth = z_cut
+
+            # Cut at specific depth and do not lift the tool.
+            # Note: linear2gcode() will use G00 to move to the first point in the path, but it should be already
+            # at the first point if the tool is down (in the material).  So, an extra G00 should show up but
+            # is inconsequential.
+            if type(geometry) == LineString or type(geometry) == LinearRing:
+                if extracut is False:
+                    gcode_multi_pass += self.linear2gcode(geometry, tolerance=tolerance, z_cut=depth, up=False)
+                else:
+                    if geometry.is_ring:
+                        gcode_multi_pass += self.linear2gcode_extra(geometry, tolerance=tolerance, z_cut=depth, up=False)
+                    else:
+                        gcode_multi_pass += self.linear2gcode(geometry, tolerance=tolerance, z_cut=depth, up=False)
+
+            # Ignore multi-pass for points.
+            elif type(geometry) == Point:
+                gcode_multi_pass += self.point2gcode(geometry)
+                break  # Ignoring ...
+            else:
+                log.warning("G-code generation not implemented for %s" % (str(type(geometry))))
+
+            # Reverse coordinates if not a loop so we can continue cutting without returning to the beginning.
+            if type(geometry) == LineString:
+                geometry.coords = list(geometry.coords)[::-1]
+                reverse = True
+
+        # If geometry is reversed, revert.
+        if reverse:
+            if type(geometry) == LineString:
+                geometry.coords = list(geometry.coords)[::-1]
+
+        # Lift the tool
+        gcode_multi_pass += self.doformat(postproc.lift_code, x=current_point[0], y=current_point[1])
+        return gcode_multi_pass
+
+    def codes_split(self, gline):
+        """
+        Parses a line of G-Code such as "G01 X1234 Y987" into
+        a dictionary: {'G': 1.0, 'X': 1234.0, 'Y': 987.0}
+
+        :param gline: G-Code line string
+        :return: Dictionary with parsed line.
+        """
+
+        command = {}
+
+        if 'Roland' in self.pp_excellon_name or 'Roland' in self.pp_geometry_name:
+            match_z = re.search(r"^Z(\s*-?\d+\.\d+?),(\s*\s*-?\d+\.\d+?),(\s*\s*-?\d+\.\d+?)*;$", gline)
+            if match_z:
+                command['G'] = 0
+                command['X'] = float(match_z.group(1).replace(" ", "")) * 0.025
+                command['Y'] = float(match_z.group(2).replace(" ", "")) * 0.025
+                command['Z'] = float(match_z.group(3).replace(" ", "")) * 0.025
+
+        else:
+            match = re.search(r'^\s*([A-Z])\s*([\+\-\.\d\s]+)', gline)
+            while match:
+                command[match.group(1)] = float(match.group(2).replace(" ", ""))
+                gline = gline[match.end():]
+                match = re.search(r'^\s*([A-Z])\s*([\+\-\.\d\s]+)', gline)
+        return command
+
+    def gcode_parse(self):
+        """
+        G-Code parser (from self.gcode). Generates dictionary with
+        single-segment LineString's and "kind" indicating cut or travel,
+        fast or feedrate speed.
+        """
+
+        kind = ["C", "F"]  # T=travel, C=cut, F=fast, S=slow
+
+        # Results go here
+        geometry = []        
+
+        # Last known instruction
+        current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0}
+
+        # Current path: temporary storage until tool is
+        # lifted or lowered.
+        if self.toolchange_xy == "excellon":
+            pos_xy = [float(eval(a)) for a in self.app.defaults["excellon_toolchangexy"].split(",")]
+        else:
+            pos_xy = [float(eval(a)) for a in self.app.defaults["geometry_toolchangexy"].split(",")]
+        path = [pos_xy]
+        # path = [(0, 0)]
+
+        # Process every instruction
+        for line in StringIO(self.gcode):
+            if '%MO' in line or '%' in line:
+                return "fail"
+
+            gobj = self.codes_split(line)
+
+            ## Units
+            if 'G' in gobj and (gobj['G'] == 20.0 or gobj['G'] == 21.0):
+                self.units = {20.0: "IN", 21.0: "MM"}[gobj['G']]
+                continue
+
+            ## Changing height
+            if 'Z' in gobj:
+                if 'Roland' in self.pp_excellon_name or 'Roland' in self.pp_geometry_name:
+                    pass
+                elif ('X' in gobj or 'Y' in gobj) and gobj['Z'] != current['Z']:
+                    log.warning("Non-orthogonal motion: From %s" % str(current))
+                    log.warning("  To: %s" % str(gobj))
+
+                current['Z'] = gobj['Z']
+                # Store the path into geometry and reset path
+                if len(path) > 1:
+                    geometry.append({"geom": LineString(path),
+                                     "kind": kind})
+                    path = [path[-1]]  # Start with the last point of last path.
+
+            if 'G' in gobj:
+                current['G'] = int(gobj['G'])
+                
+            if 'X' in gobj or 'Y' in gobj:
+                # TODO: I think there is a problem here, current['X] (and the rest of current[...] are not initialized
+                if 'X' in gobj:
+                    x = gobj['X']
+                    # current['X'] = x
+                else:
+                    x = current['X']
+                
+                if 'Y' in gobj:
+                    y = gobj['Y']
+                else:
+                    y = current['Y']
+
+                kind = ["C", "F"]  # T=travel, C=cut, F=fast, S=slow
+
+                if current['Z'] > 0:
+                    kind[0] = 'T'
+                if current['G'] > 0:
+                    kind[1] = 'S'
+
+                if current['G'] in [0, 1]:  # line
+                    path.append((x, y))
+
+                arcdir = [None, None, "cw", "ccw"]
+                if current['G'] in [2, 3]:  # arc
+                    center = [gobj['I'] + current['X'], gobj['J'] + current['Y']]
+                    radius = sqrt(gobj['I']**2 + gobj['J']**2)
+                    start = arctan2(-gobj['J'], -gobj['I'])
+                    stop = arctan2(-center[1] + y, -center[0] + x)
+                    path += arc(center, radius, start, stop,
+                                arcdir[current['G']],
+                                int(self.steps_per_circle / 4))
+
+            # Update current instruction
+            for code in gobj:
+                current[code] = gobj[code]
+
+        # There might not be a change in height at the
+        # end, therefore, see here too if there is
+        # a final path.
+        if len(path) > 1:
+            geometry.append({"geom": LineString(path),
+                             "kind": kind})
+
+        self.gcode_parsed = geometry
+        return geometry
+
+    # def plot(self, tooldia=None, dpi=75, margin=0.1,
+    #          color={"T": ["#F0E24D", "#B5AB3A"], "C": ["#5E6CFF", "#4650BD"]},
+    #          alpha={"T": 0.3, "C": 1.0}):
+    #     """
+    #     Creates a Matplotlib figure with a plot of the
+    #     G-code job.
+    #     """
+    #     if tooldia is None:
+    #         tooldia = self.tooldia
+    #
+    #     fig = Figure(dpi=dpi)
+    #     ax = fig.add_subplot(111)
+    #     ax.set_aspect(1)
+    #     xmin, ymin, xmax, ymax = self.input_geometry_bounds
+    #     ax.set_xlim(xmin-margin, xmax+margin)
+    #     ax.set_ylim(ymin-margin, ymax+margin)
+    #
+    #     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
+    #             ax.plot(x, y, linespec, color=linecolor)
+    #     else:
+    #         for geo in self.gcode_parsed:
+    #             poly = geo['geom'].buffer(tooldia/2.0)
+    #             patch = PolygonPatch(poly, facecolor=color[geo['kind'][0]][0],
+    #                                  edgecolor=color[geo['kind'][0]][1],
+    #                                  alpha=alpha[geo['kind'][0]], zorder=2)
+    #             ax.add_patch(patch)
+    #
+    #     return fig
+        
+    def plot2(self, tooldia=None, dpi=75, margin=0.1, gcode_parsed=None,
+              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 tooldia: Tool diameter.
+        :param dpi: Not used!
+        :param margin: Not used!
+        :param color: Color specification.
+        :param alpha: Transparency specification.
+        :param tool_tolerance: Tolerance when drawing the toolshape.
+        :return: None
+        """
+
+        gcode_parsed = gcode_parsed if gcode_parsed else self.gcode_parsed
+        path_num = 0
+
+        if tooldia is None:
+            tooldia = self.tooldia
+
+        if tooldia == 0:
+            for geo in gcode_parsed:
+                obj.add_shape(shape=geo['geom'], color=color[geo['kind'][0]][1], visible=visible)
+        else:
+            text = []
+            pos = []
+            for geo in gcode_parsed:
+                path_num += 1
+
+                text.append(str(path_num))
+                pos.append(geo['geom'].coords[0])
+
+                poly = geo['geom'].buffer(tooldia / 2.0).simplify(tool_tolerance)
+                obj.add_shape(shape=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.set(text=text, pos=pos, visible=obj.options['plot'])
+
+    def create_geometry(self):
+        # TODO: This takes forever. Too much data?
+        self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed])
+        return self.solid_geometry
+
+    def linear2gcode(self, linear, tolerance=0, down=True, up=True,
+                     z_cut=None, z_move=None, zdownrate=None,
+                     feedrate=None, feedrate_z=None, feedrate_rapid=None, cont=False):
+        """
+        Generates G-code to cut along the linear feature.
+
+        :param linear: The path to cut along.
+        :type: Shapely.LinearRing or Shapely.Linear String
+        :param tolerance: All points in the simplified object will be within the
+            tolerance distance of the original geometry.
+        :type tolerance: float
+        :param feedrate: speed for cut on X - Y plane
+        :param feedrate_z: speed for cut on Z plane
+        :param feedrate_rapid: speed to move between cuts; usually is G0 but some CNC require to specify it
+        :return: G-code to cut along the linear feature.
+        :rtype: str
+        """
+
+        if z_cut is None:
+            z_cut = self.z_cut
+
+        if z_move is None:
+            z_move = self.z_move
+        #
+        # if zdownrate is None:
+        #     zdownrate = self.zdownrate
+
+        if feedrate is None:
+            feedrate = self.feedrate
+
+        if feedrate_z is None:
+            feedrate_z = self.feedrate_z
+
+        if feedrate_rapid is None:
+            feedrate_rapid = self.feedrate_rapid
+
+        # Simplify paths?
+        if tolerance > 0:
+            target_linear = linear.simplify(tolerance)
+        else:
+            target_linear = linear
+
+        gcode = ""
+
+        path = list(target_linear.coords)
+        p = self.pp_geometry
+
+        # Move fast to 1st point
+        if not cont:
+            gcode += self.doformat(p.rapid_code, x=path[0][0], y=path[0][1])  # Move to first point
+
+        # Move down to cutting depth
+        if down:
+            # Different feedrate for vertical cut?
+            gcode += self.doformat(p.feedrate_z_code)
+            # gcode += self.doformat(p.feedrate_code)
+            gcode += self.doformat(p.down_code, x=path[0][0], y=path[0][1], z_cut=z_cut)
+            gcode += self.doformat(p.feedrate_code, feedrate=feedrate)
+
+        # Cutting...
+        for pt in path[1:]:
+            gcode += self.doformat(p.linear_code, x=pt[0], y=pt[1], z=z_cut)  # Linear motion to point
+
+        # Up to travelling height.
+        if up:
+            gcode += self.doformat(p.lift_code, x=pt[0], y=pt[1], z_move=z_move)  # Stop cutting
+        return gcode
+
+    def linear2gcode_extra(self, linear, tolerance=0, down=True, up=True,
+                     z_cut=None, z_move=None, zdownrate=None,
+                     feedrate=None, feedrate_z=None, feedrate_rapid=None, cont=False):
+        """
+        Generates G-code to cut along the linear feature.
+
+        :param linear: The path to cut along.
+        :type: Shapely.LinearRing or Shapely.Linear String
+        :param tolerance: All points in the simplified object will be within the
+            tolerance distance of the original geometry.
+        :type tolerance: float
+        :param feedrate: speed for cut on X - Y plane
+        :param feedrate_z: speed for cut on Z plane
+        :param feedrate_rapid: speed to move between cuts; usually is G0 but some CNC require to specify it
+        :return: G-code to cut along the linear feature.
+        :rtype: str
+        """
+
+        if z_cut is None:
+            z_cut = self.z_cut
+
+        if z_move is None:
+            z_move = self.z_move
+        #
+        # if zdownrate is None:
+        #     zdownrate = self.zdownrate
+
+        if feedrate is None:
+            feedrate = self.feedrate
+
+        if feedrate_z is None:
+            feedrate_z = self.feedrate_z
+
+        if feedrate_rapid is None:
+            feedrate_rapid = self.feedrate_rapid
+
+        # Simplify paths?
+        if tolerance > 0:
+            target_linear = linear.simplify(tolerance)
+        else:
+            target_linear = linear
+
+        gcode = ""
+
+        path = list(target_linear.coords)
+        p = self.pp_geometry
+
+        # Move fast to 1st point
+        if not cont:
+            gcode += self.doformat(p.rapid_code, x=path[0][0], y=path[0][1])  # Move to first point
+
+        # Move down to cutting depth
+        if down:
+            # Different feedrate for vertical cut?
+            if self.feedrate_z is not None:
+                gcode += self.doformat(p.feedrate_z_code)
+                # gcode += self.doformat(p.feedrate_code)
+                gcode += self.doformat(p.down_code, x=path[0][0], y=path[0][1], z_cut=z_cut)
+                gcode += self.doformat(p.feedrate_code, feedrate=feedrate)
+            else:
+                gcode += self.doformat(p.down_code, x=path[0][0], y=path[0][1], z_cut=z_cut)  # Start cutting
+
+        # Cutting...
+        for pt in path[1:]:
+            gcode += self.doformat(p.linear_code, x=pt[0], y=pt[1], z=z_cut)  # Linear motion to point
+
+        # this line is added to create an extra cut over the first point in patch
+        # to make sure that we remove the copper leftovers
+        gcode += self.doformat(p.linear_code, x=path[1][0], y=path[1][1])    # Linear motion to the 1st point in the cut path
+
+        # Up to travelling height.
+        if up:
+            gcode += self.doformat(p.lift_code, x=path[1][0], y=path[1][1], z_move=z_move)  # Stop cutting
+
+        return gcode
+
+    def point2gcode(self, point):
+        gcode = ""
+
+        path = list(point.coords)
+        p = self.pp_geometry
+        gcode += self.doformat(p.linear_code, x=path[0][0], y=path[0][1])  # Move to first point
+
+        if self.feedrate_z is not None:
+            gcode += self.doformat(p.feedrate_z_code)
+            gcode += self.doformat(p.down_code, x=path[0][0], y=path[0][1], z_cut = self.z_cut)
+            gcode += self.doformat(p.feedrate_code)
+        else:
+            gcode += self.doformat(p.down_code, x=path[0][0], y=path[0][1], z_cut = self.z_cut)  # Start cutting
+
+        gcode += self.doformat(p.lift_code, x=path[0][0], y=path[0][1])  # Stop cutting
+        return gcode
+
+    def export_svg(self, scale_factor=0.00):
+        """
+        Exports the CNC Job as a SVG Element
+
+        :scale_factor: float
+        :return: SVG Element string
+        """
+        # scale_factor is a multiplication factor for the SVG stroke-width used within shapely's svg export
+        # If not specified then try and use the tool diameter
+        # This way what is on screen will match what is outputed for the svg
+        # This is quite a useful feature for svg's used with visicut
+
+        if scale_factor <= 0:
+            scale_factor = self.options['tooldia'] / 2
+
+        # If still 0 then default to 0.05
+        # This value appears to work for zooming, and getting the output svg line width
+        # to match that viewed on screen with FlatCam
+        if scale_factor == 0:
+            scale_factor = 0.01
+
+        # Separate the list of cuts and travels into 2 distinct lists
+        # This way we can add different formatting / colors to both
+        cuts = []
+        travels = []
+        for g in self.gcode_parsed:
+            if g['kind'][0] == 'C': cuts.append(g)
+            if g['kind'][0] == 'T': travels.append(g)
+
+        # Used to determine the overall board size
+        self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed])
+
+        # Convert the cuts and travels into single geometry objects we can render as svg xml
+        if travels:
+            travelsgeom = cascaded_union([geo['geom'] for geo in travels])
+        if cuts:
+            cutsgeom = cascaded_union([geo['geom'] for geo in cuts])
+
+        # Render the SVG Xml
+        # The scale factor affects the size of the lines, and the stroke color adds different formatting for each set
+        # It's better to have the travels sitting underneath the cuts for visicut
+        svg_elem = ""
+        if travels:
+            svg_elem = travelsgeom.svg(scale_factor=scale_factor, stroke_color="#F0E24D")
+        if cuts:
+            svg_elem += cutsgeom.svg(scale_factor=scale_factor, stroke_color="#5E6CFF")
+
+        return svg_elem
+
+    def bounds(self):
+        """
+        Returns coordinates of rectangular bounds
+        of geometry: (xmin, ymin, xmax, ymax).
+        """
+        # fixed issue of getting bounds only for one level lists of objects
+        # now it can get bounds for nested lists of objects
+
+        def bounds_rec(obj):
+            if type(obj) is list:
+                minx = Inf
+                miny = Inf
+                maxx = -Inf
+                maxy = -Inf
+
+                for k in obj:
+                    if type(k) is dict:
+                        for key in k:
+                            minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
+                            minx = min(minx, minx_)
+                            miny = min(miny, miny_)
+                            maxx = max(maxx, maxx_)
+                            maxy = max(maxy, maxy_)
+                    else:
+                        minx_, miny_, maxx_, maxy_ = bounds_rec(k)
+                        minx = min(minx, minx_)
+                        miny = min(miny, miny_)
+                        maxx = max(maxx, maxx_)
+                        maxy = max(maxy, maxy_)
+                return minx, miny, maxx, maxy
+            else:
+                # it's a Shapely object, return it's bounds
+                return obj.bounds
+        if self.multitool is False:
+            log.debug("CNCJob->bounds()")
+            if self.solid_geometry is None:
+                log.debug("solid_geometry is None")
+                return 0, 0, 0, 0
+
+            bounds_coords = bounds_rec(self.solid_geometry)
+        else:
+            for k, v in self.cnc_tools.items():
+                minx = Inf
+                miny = Inf
+                maxx = -Inf
+                maxy = -Inf
+
+                for k in v['solid_geometry']:
+                    minx_, miny_, maxx_, maxy_ = bounds_rec(k)
+                    minx = min(minx, minx_)
+                    miny = min(miny, miny_)
+                    maxx = max(maxx, maxx_)
+                    maxy = max(maxy, maxy_)
+            bounds_coords = minx, miny, maxx, maxy
+        return bounds_coords
+
+    def scale(self, xfactor, yfactor=None, point=None):
+        """
+        Scales all the geometry on the XY plane in the object by the
+        given factor. Tool sizes, feedrates, or Z-axis dimensions are
+        not altered.
+
+        :param factor: Number by which to scale the object.
+        :type factor: float
+        :param point: the (x,y) coords for the point of origin of scale
+        :type tuple of floats
+        :return: None
+        :rtype: None
+        """
+
+        if yfactor is None:
+            yfactor = xfactor
+
+        if point is None:
+            px = 0
+            py = 0
+        else:
+            px, py = point
+
+        for g in self.gcode_parsed:
+            g['geom'] = affinity.scale(g['geom'], xfactor, yfactor, origin=(px, py))
+
+        self.create_geometry()
+
+    def offset(self, vect):
+        """
+        Offsets all the geometry on the XY plane in the object by the
+        given vector.
+        Offsets all the GCODE on the XY plane in the object by the
+        given vector.
+
+        g_offsetx_re, g_offsety_re, multitool, cnnc_tools are attributes of FlatCAMCNCJob class in camlib
+
+        :param vect: (x, y) offset vector.
+        :type vect: tuple
+        :return: None
+        """
+        dx, dy = vect
+
+        def offset_g(g):
+            """
+
+            :param g: 'g' parameter it's a gcode string
+            :return:  offseted gcode string
+            """
+
+            temp_gcode = ''
+            lines = StringIO(g)
+            for line in lines:
+                # find the X group
+                match_x = self.g_offsetx_re.search(line)
+                if match_x:
+                    if match_x.group(1) is not None:
+                        # get the coordinate and add X offset
+                        new_x = float(match_x.group(1)[1:]) + dx
+                        # replace the updated string
+                        line = line.replace(
+                            match_x.group(1),
+                            'X%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_x)
+                        )
+                match_y = self.g_offsety_re.search(line)
+                if match_y:
+                    if match_y.group(1) is not None:
+                        new_y = float(match_y.group(1)[1:]) + dy
+                        line = line.replace(
+                            match_y.group(1),
+                            'Y%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_y)
+                        )
+                temp_gcode += line
+            lines.close()
+            return temp_gcode
+
+        if self.multitool is False:
+            # offset Gcode
+            self.gcode = offset_g(self.gcode)
+            # offset geometry
+            for g in self.gcode_parsed:
+                g['geom'] = affinity.translate(g['geom'], xoff=dx, yoff=dy)
+            self.create_geometry()
+        else:
+            for k, v in self.cnc_tools.items():
+                # offset Gcode
+                v['gcode'] = offset_g(v['gcode'])
+                # offset gcode_parsed
+                for g in v['gcode_parsed']:
+                    g['geom'] = affinity.translate(g['geom'], xoff=dx, yoff=dy)
+                v['solid_geometry'] = cascaded_union([geo['geom'] for geo in v['gcode_parsed']])
+
+    def mirror(self, axis, point):
+        """
+        Mirror the geometrys of an object by an given axis around the coordinates of the 'point'
+        :param angle:
+        :param point: tupple of coordinates (x,y)
+        :return:
+        """
+        px, py = point
+        xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
+
+        for g in self.gcode_parsed:
+            g['geom'] = affinity.scale(g['geom'], xscale, yscale, origin=(px, py))
+
+        self.create_geometry()
+
+    def skew(self, angle_x, angle_y, point):
+        """
+        Shear/Skew the geometries of an object by angles along x and y dimensions.
+
+        Parameters
+        ----------
+        angle_x, angle_y : float, float
+            The shear angle(s) for the x and y axes respectively. These can be
+            specified in either degrees (default) or radians by setting
+            use_radians=True.
+        point: tupple of coordinates (x,y)
+
+        See shapely manual for more information:
+        http://toblerity.org/shapely/manual.html#affine-transformations
+        """
+        px, py = point
+
+        for g in self.gcode_parsed:
+            g['geom'] = affinity.skew(g['geom'], angle_x, angle_y,
+                                      origin=(px, py))
+
+        self.create_geometry()
+
+    def rotate(self, angle, point):
+        """
+        Rotate the geometrys of an object by an given angle around the coordinates of the 'point'
+        :param angle:
+        :param point: tupple of coordinates (x,y)
+        :return:
+        """
+
+        px, py = point
+
+        for g in self.gcode_parsed:
+            g['geom'] = affinity.rotate(g['geom'], angle, origin=(px, py))
+
+        self.create_geometry()
+
+def get_bounds(geometry_list):
+    xmin = Inf
+    ymin = Inf
+    xmax = -Inf
+    ymax = -Inf
+
+    #print "Getting bounds of:", str(geometry_set)
+    for gs in geometry_list:
+        try:
+            gxmin, gymin, gxmax, gymax = gs.bounds()
+            xmin = min([xmin, gxmin])
+            ymin = min([ymin, gymin])
+            xmax = max([xmax, gxmax])
+            ymax = max([ymax, gymax])
+        except:
+            log.warning("DEVELOPMENT: Tried to get bounds of empty geometry.")
+
+    return [xmin, ymin, xmax, ymax]
+
+
+def arc(center, radius, start, stop, direction, steps_per_circ):
+    """
+    Creates a list of point along the specified arc.
+
+    :param center: Coordinates of the center [x, y]
+    :type center: list
+    :param radius: Radius of the arc.
+    :type radius: float
+    :param start: Starting angle in radians
+    :type start: float
+    :param stop: End angle in radians
+    :type stop: float
+    :param direction: Orientation of the arc, "CW" or "CCW"
+    :type direction: string
+    :param steps_per_circ: Number of straight line segments to
+        represent a circle.
+    :type steps_per_circ: int
+    :return: The desired arc, as list of tuples
+    :rtype: list
+    """
+    # TODO: Resolution should be established by maximum error from the exact arc.
+
+    da_sign = {"cw": -1.0, "ccw": 1.0}
+    points = []
+    if direction == "ccw" and stop <= start:
+        stop += 2 * pi
+    if direction == "cw" and stop >= start:
+        stop -= 2 * pi
+    
+    angle = abs(stop - start)
+        
+    #angle = stop-start
+    steps = max([int(ceil(angle / (2 * pi) * steps_per_circ)), 2])
+    delta_angle = da_sign[direction] * angle * 1.0 / steps
+    for i in range(steps + 1):
+        theta = start + delta_angle * i
+        points.append((center[0] + radius * cos(theta), center[1] + radius * sin(theta)))
+    return points
+
+
+def arc2(p1, p2, center, direction, steps_per_circ):
+    r = sqrt((center[0] - p1[0]) ** 2 + (center[1] - p1[1]) ** 2)
+    start = arctan2(p1[1] - center[1], p1[0] - center[0])
+    stop = arctan2(p2[1] - center[1], p2[0] - center[0])
+    return arc(center, r, start, stop, direction, steps_per_circ)
+
+
+def arc_angle(start, stop, direction):
+    if direction == "ccw" and stop <= start:
+        stop += 2 * pi
+    if direction == "cw" and stop >= start:
+        stop -= 2 * pi
+
+    angle = abs(stop - start)
+    return angle
+
+
+# def find_polygon(poly, point):
+#     """
+#     Find an object that object.contains(Point(point)) in
+#     poly, which can can be iterable, contain iterable of, or
+#     be itself an implementer of .contains().
+#
+#     :param poly: See description
+#     :return: Polygon containing point or None.
+#     """
+#
+#     if poly is None:
+#         return None
+#
+#     try:
+#         for sub_poly in poly:
+#             p = find_polygon(sub_poly, point)
+#             if p is not None:
+#                 return p
+#     except TypeError:
+#         try:
+#             if poly.contains(Point(point)):
+#                 return poly
+#         except AttributeError:
+#             return None
+#
+#     return None
+
+
+def to_dict(obj):
+    """
+    Makes the following types into serializable form:
+
+    * ApertureMacro
+    * BaseGeometry
+
+    :param obj: Shapely geometry.
+    :type obj: BaseGeometry
+    :return: Dictionary with serializable form if ``obj`` was
+        BaseGeometry or ApertureMacro, otherwise returns ``obj``.
+    """
+    if isinstance(obj, ApertureMacro):
+        return {
+            "__class__": "ApertureMacro",
+            "__inst__": obj.to_dict()
+        }
+    if isinstance(obj, BaseGeometry):
+        return {
+            "__class__": "Shply",
+            "__inst__": sdumps(obj)
+        }
+    return obj
+
+
+def dict2obj(d):
+    """
+    Default deserializer.
+
+    :param d:  Serializable dictionary representation of an object
+        to be reconstructed.
+    :return: Reconstructed object.
+    """
+    if '__class__' in d and '__inst__' in d:
+        if d['__class__'] == "Shply":
+            return sloads(d['__inst__'])
+        if d['__class__'] == "ApertureMacro":
+            am = ApertureMacro()
+            am.from_dict(d['__inst__'])
+            return am
+        return d
+    else:
+        return d
+
+
+# def plotg(geo, solid_poly=False, color="black"):
+#     try:
+#         _ = iter(geo)
+#     except:
+#         geo = [geo]
+#
+#     for g in geo:
+#         if type(g) == Polygon:
+#             if solid_poly:
+#                 patch = PolygonPatch(g,
+#                                      facecolor="#BBF268",
+#                                      edgecolor="#006E20",
+#                                      alpha=0.75,
+#                                      zorder=2)
+#                 ax = subplot(111)
+#                 ax.add_patch(patch)
+#             else:
+#                 x, y = g.exterior.coords.xy
+#                 plot(x, y, color=color)
+#                 for ints in g.interiors:
+#                     x, y = ints.coords.xy
+#                     plot(x, y, color=color)
+#                 continue
+#
+#         if type(g) == LineString or type(g) == LinearRing:
+#             x, y = g.coords.xy
+#             plot(x, y, color=color)
+#             continue
+#
+#         if type(g) == Point:
+#             x, y = g.coords.xy
+#             plot(x, y, 'o')
+#             continue
+#
+#         try:
+#             _ = iter(g)
+#             plotg(g, color=color)
+#         except:
+#             log.error("Cannot plot: " + str(type(g)))
+#             continue
+
+
+def parse_gerber_number(strnumber, int_digits, frac_digits, zeros):
+    """
+    Parse a single number of Gerber coordinates.
+
+    :param strnumber: String containing a number in decimal digits
+    from a coordinate data block, possibly with a leading sign.
+    :type strnumber: str
+    :param int_digits: Number of digits used for the integer
+    part of the number
+    :type frac_digits: int
+    :param frac_digits: Number of digits used for the fractional
+    part of the number
+    :type frac_digits: int
+    :param zeros: If 'L', leading zeros are removed and trailing zeros are kept. If 'T', is in reverse.
+    :type zeros: str
+    :return: The number in floating point.
+    :rtype: float
+    """
+    if zeros == 'L':
+        ret_val = int(strnumber) * (10 ** (-frac_digits))
+
+    if zeros == 'T':
+        int_val = int(strnumber)
+        ret_val = (int_val * (10 ** ((int_digits + frac_digits) - len(strnumber)))) * (10 ** (-frac_digits))
+    return ret_val
+
+
+# def voronoi(P):
+#     """
+#     Returns a list of all edges of the voronoi diagram for the given input points.
+#     """
+#     delauny = Delaunay(P)
+#     triangles = delauny.points[delauny.vertices]
+#
+#     circum_centers = np.array([triangle_csc(tri) for tri in triangles])
+#     long_lines_endpoints = []
+#
+#     lineIndices = []
+#     for i, triangle in enumerate(triangles):
+#         circum_center = circum_centers[i]
+#         for j, neighbor in enumerate(delauny.neighbors[i]):
+#             if neighbor != -1:
+#                 lineIndices.append((i, neighbor))
+#             else:
+#                 ps = triangle[(j+1)%3] - triangle[(j-1)%3]
+#                 ps = np.array((ps[1], -ps[0]))
+#
+#                 middle = (triangle[(j+1)%3] + triangle[(j-1)%3]) * 0.5
+#                 di = middle - triangle[j]
+#
+#                 ps /= np.linalg.norm(ps)
+#                 di /= np.linalg.norm(di)
+#
+#                 if np.dot(di, ps) < 0.0:
+#                     ps *= -1000.0
+#                 else:
+#                     ps *= 1000.0
+#
+#                 long_lines_endpoints.append(circum_center + ps)
+#                 lineIndices.append((i, len(circum_centers) + len(long_lines_endpoints)-1))
+#
+#     vertices = np.vstack((circum_centers, long_lines_endpoints))
+#
+#     # filter out any duplicate lines
+#     lineIndicesSorted = np.sort(lineIndices) # make (1,2) and (2,1) both (1,2)
+#     lineIndicesTupled = [tuple(row) for row in lineIndicesSorted]
+#     lineIndicesUnique = np.unique(lineIndicesTupled)
+#
+#     return vertices, lineIndicesUnique
+#
+#
+# def triangle_csc(pts):
+#     rows, cols = pts.shape
+#
+#     A = np.bmat([[2 * np.dot(pts, pts.T), np.ones((rows, 1))],
+#                  [np.ones((1, rows)), np.zeros((1, 1))]])
+#
+#     b = np.hstack((np.sum(pts * pts, axis=1), np.ones((1))))
+#     x = np.linalg.solve(A,b)
+#     bary_coords = x[:-1]
+#     return np.sum(pts * np.tile(bary_coords.reshape((pts.shape[0], 1)), (1, pts.shape[1])), axis=0)
+#
+#
+# def voronoi_cell_lines(points, vertices, lineIndices):
+#     """
+#     Returns a mapping from a voronoi cell to its edges.
+#
+#     :param points: shape (m,2)
+#     :param vertices: shape (n,2)
+#     :param lineIndices: shape (o,2)
+#     :rtype: dict point index -> list of shape (n,2) with vertex indices
+#     """
+#     kd = KDTree(points)
+#
+#     cells = collections.defaultdict(list)
+#     for i1, i2 in lineIndices:
+#         v1, v2 = vertices[i1], vertices[i2]
+#         mid = (v1+v2)/2
+#         _, (p1Idx, p2Idx) = kd.query(mid, 2)
+#         cells[p1Idx].append((i1, i2))
+#         cells[p2Idx].append((i1, i2))
+#
+#     return cells
+#
+#
+# def voronoi_edges2polygons(cells):
+#     """
+#     Transforms cell edges into polygons.
+#
+#     :param cells: as returned from voronoi_cell_lines
+#     :rtype: dict point index -> list of vertex indices which form a polygon
+#     """
+#
+#     # first, close the outer cells
+#     for pIdx, lineIndices_ in cells.items():
+#         dangling_lines = []
+#         for i1, i2 in lineIndices_:
+#             connections = filter(lambda (i1_, i2_): (i1, i2) != (i1_, i2_) and (i1 == i1_ or i1 == i2_ or i2 == i1_ or i2 == i2_), lineIndices_)
+#             assert 1 <= len(connections) <= 2
+#             if len(connections) == 1:
+#                 dangling_lines.append((i1, i2))
+#         assert len(dangling_lines) in [0, 2]
+#         if len(dangling_lines) == 2:
+#             (i11, i12), (i21, i22) = dangling_lines
+#
+#             # determine which line ends are unconnected
+#             connected = filter(lambda (i1,i2): (i1,i2) != (i11,i12) and (i1 == i11 or i2 == i11), lineIndices_)
+#             i11Unconnected = len(connected) == 0
+#
+#             connected = filter(lambda (i1,i2): (i1,i2) != (i21,i22) and (i1 == i21 or i2 == i21), lineIndices_)
+#             i21Unconnected = len(connected) == 0
+#
+#             startIdx = i11 if i11Unconnected else i12
+#             endIdx = i21 if i21Unconnected else i22
+#
+#             cells[pIdx].append((startIdx, endIdx))
+#
+#     # then, form polygons by storing vertex indices in (counter-)clockwise order
+#     polys = dict()
+#     for pIdx, lineIndices_ in cells.items():
+#         # get a directed graph which contains both directions and arbitrarily follow one of both
+#         directedGraph = lineIndices_ + [(i2, i1) for (i1, i2) in lineIndices_]
+#         directedGraphMap = collections.defaultdict(list)
+#         for (i1, i2) in directedGraph:
+#             directedGraphMap[i1].append(i2)
+#         orderedEdges = []
+#         currentEdge = directedGraph[0]
+#         while len(orderedEdges) < len(lineIndices_):
+#             i1 = currentEdge[1]
+#             i2 = directedGraphMap[i1][0] if directedGraphMap[i1][0] != currentEdge[0] else directedGraphMap[i1][1]
+#             nextEdge = (i1, i2)
+#             orderedEdges.append(nextEdge)
+#             currentEdge = nextEdge
+#
+#         polys[pIdx] = [i1 for (i1, i2) in orderedEdges]
+#
+#     return polys
+#
+#
+# def voronoi_polygons(points):
+#     """
+#     Returns the voronoi polygon for each input point.
+#
+#     :param points: shape (n,2)
+#     :rtype: list of n polygons where each polygon is an array of vertices
+#     """
+#     vertices, lineIndices = voronoi(points)
+#     cells = voronoi_cell_lines(points, vertices, lineIndices)
+#     polys = voronoi_edges2polygons(cells)
+#     polylist = []
+#     for i in xrange(len(points)):
+#         poly = vertices[np.asarray(polys[i])]
+#         polylist.append(poly)
+#     return polylist
+#
+#
+# class Zprofile:
+#     def __init__(self):
+#
+#         # data contains lists of [x, y, z]
+#         self.data = []
+#
+#         # Computed voronoi polygons (shapely)
+#         self.polygons = []
+#         pass
+#
+#     def plot_polygons(self):
+#         axes = plt.subplot(1, 1, 1)
+#
+#         plt.axis([-0.05, 1.05, -0.05, 1.05])
+#
+#         for poly in self.polygons:
+#             p = PolygonPatch(poly, facecolor=np.random.rand(3, 1), alpha=0.3)
+#             axes.add_patch(p)
+#
+#     def init_from_csv(self, filename):
+#         pass
+#
+#     def init_from_string(self, zpstring):
+#         pass
+#
+#     def init_from_list(self, zplist):
+#         self.data = zplist
+#
+#     def generate_polygons(self):
+#         self.polygons = [Polygon(p) for p in voronoi_polygons(array([[x[0], x[1]] for x in self.data]))]
+#
+#     def normalize(self, origin):
+#         pass
+#
+#     def paste(self, path):
+#         """
+#         Return a list of dictionaries containing the parts of the original
+#         path and their z-axis offset.
+#         """
+#
+#         # At most one region/polygon will contain the path
+#         containing = [i for i in range(len(self.polygons)) if self.polygons[i].contains(path)]
+#
+#         if len(containing) > 0:
+#             return [{"path": path, "z": self.data[containing[0]][2]}]
+#
+#         # All region indexes that intersect with the path
+#         crossing = [i for i in range(len(self.polygons)) if self.polygons[i].intersects(path)]
+#
+#         return [{"path": path.intersection(self.polygons[i]),
+#                  "z": self.data[i][2]} for i in crossing]
+
+
+def autolist(obj):
+    try:
+        _ = iter(obj)
+        return obj
+    except TypeError:
+        return [obj]
+
+
+def three_point_circle(p1, p2, p3):
+    """
+    Computes the center and radius of a circle from
+    3 points on its circumference.
+
+    :param p1: Point 1
+    :param p2: Point 2
+    :param p3: Point 3
+    :return: center, radius
+    """
+    # Midpoints
+    a1 = (p1 + p2) / 2.0
+    a2 = (p2 + p3) / 2.0
+
+    # Normals
+    b1 = dot((p2 - p1), array([[0, -1], [1, 0]], dtype=float32))
+    b2 = dot((p3 - p2), array([[0, 1], [-1, 0]], dtype=float32))
+
+    # Params
+    T = solve(transpose(array([-b1, b2])), a1 - a2)
+
+    # Center
+    center = a1 + b1 * T[0]
+
+    # Radius
+    radius = norm(center - p1)
+
+    return center, radius, T[0]
+
+
+def distance(pt1, pt2):
+    return sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2)
+
+def distance_euclidian(x1, y1, x2, y2):
+    return sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)
+
+
+class FlatCAMRTree(object):
+    """
+    Indexes geometry (Any object with "cooords" property containing
+    a list of tuples with x, y values). Objects are indexed by
+    all their points by default. To index by arbitrary points,
+    override self.points2obj.
+    """
+
+    def __init__(self):
+        # Python RTree Index
+        self.rti = rtindex.Index()
+
+        ## Track object-point relationship
+        # Each is list of points in object.
+        self.obj2points = []
+
+        # Index is index in rtree, value is index of
+        # object in obj2points.
+        self.points2obj = []
+
+        self.get_points = lambda go: go.coords
+
+    def grow_obj2points(self, idx):
+        """
+        Increases the size of self.obj2points to fit
+        idx + 1 items.
+
+        :param idx: Index to fit into list.
+        :return: None
+        """
+        if len(self.obj2points) > idx:
+            # len == 2, idx == 1, ok.
+            return
+        else:
+            # len == 2, idx == 2, need 1 more.
+            # range(2, 3)
+            for i in range(len(self.obj2points), idx + 1):
+                self.obj2points.append([])
+
+    def insert(self, objid, obj):
+        self.grow_obj2points(objid)
+        self.obj2points[objid] = []
+
+        for pt in self.get_points(obj):
+            self.rti.insert(len(self.points2obj), (pt[0], pt[1], pt[0], pt[1]), obj=objid)
+            self.obj2points[objid].append(len(self.points2obj))
+            self.points2obj.append(objid)
+
+    def remove_obj(self, objid, obj):
+        # Use all ptids to delete from index
+        for i, pt in enumerate(self.get_points(obj)):
+            self.rti.delete(self.obj2points[objid][i], (pt[0], pt[1], pt[0], pt[1]))
+
+    def nearest(self, pt):
+        """
+        Will raise StopIteration if no items are found.
+
+        :param pt:
+        :return:
+        """
+        return next(self.rti.nearest(pt, objects=True))
+
+
+class FlatCAMRTreeStorage(FlatCAMRTree):
+    """
+    Just like FlatCAMRTree it indexes geometry, but also serves
+    as storage for the geometry.
+    """
+
+    def __init__(self):
+        # super(FlatCAMRTreeStorage, self).__init__()
+        super().__init__()
+
+        self.objects = []
+
+        # Optimization attempt!
+        self.indexes = {}
+
+    def insert(self, obj):
+        self.objects.append(obj)
+        idx = len(self.objects) - 1
+
+        # Note: Shapely objects are not hashable any more, althought
+        # there seem to be plans to re-introduce the feature in
+        # version 2.0. For now, we will index using the object's id,
+        # but it's important to remember that shapely geometry is
+        # mutable, ie. it can be modified to a totally different shape
+        # and continue to have the same id.
+        # self.indexes[obj] = idx
+        self.indexes[id(obj)] = idx
+
+        # super(FlatCAMRTreeStorage, self).insert(idx, obj)
+        super().insert(idx, obj)
+
+    #@profile
+    def remove(self, obj):
+        # See note about self.indexes in insert().
+        # objidx = self.indexes[obj]
+        objidx = self.indexes[id(obj)]
+
+        # Remove from list
+        self.objects[objidx] = None
+
+        # Remove from index
+        self.remove_obj(objidx, obj)
+
+    def get_objects(self):
+        return (o for o in self.objects if o is not None)
+
+    def nearest(self, pt):
+        """
+        Returns the nearest matching points and the object
+        it belongs to.
+
+        :param pt: Query point.
+        :return: (match_x, match_y), Object owner of
+          matching point.
+        :rtype: tuple
+        """
+        tidx = super(FlatCAMRTreeStorage, self).nearest(pt)
+        return (tidx.bbox[0], tidx.bbox[1]), self.objects[tidx.object]
+
+
+# class myO:
+#     def __init__(self, coords):
+#         self.coords = coords
+#
+#
+# def test_rti():
+#
+#     o1 = myO([(0, 0), (0, 1), (1, 1)])
+#     o2 = myO([(2, 0), (2, 1), (2, 1)])
+#     o3 = myO([(2, 0), (2, 1), (3, 1)])
+#
+#     os = [o1, o2]
+#
+#     idx = FlatCAMRTree()
+#
+#     for o in range(len(os)):
+#         idx.insert(o, os[o])
+#
+#     print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
+#
+#     idx.remove_obj(0, o1)
+#
+#     print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
+#
+#     idx.remove_obj(1, o2)
+#
+#     print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
+#
+#
+# def test_rtis():
+#
+#     o1 = myO([(0, 0), (0, 1), (1, 1)])
+#     o2 = myO([(2, 0), (2, 1), (2, 1)])
+#     o3 = myO([(2, 0), (2, 1), (3, 1)])
+#
+#     os = [o1, o2]
+#
+#     idx = FlatCAMRTreeStorage()
+#
+#     for o in range(len(os)):
+#         idx.insert(os[o])
+#
+#     #os = None
+#     #o1 = None
+#     #o2 = None
+#
+#     print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
+#
+#     idx.remove(idx.nearest((2,0))[1])
+#
+#     print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
+#
+#     idx.remove(idx.nearest((0,0))[1])
+#
+#     print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]

+ 169 - 0
flatcamTools/ToolCalculators.py

@@ -0,0 +1,169 @@
+from PyQt5 import QtGui
+from GUIElements import FCEntry
+from FlatCAMTool import FlatCAMTool
+from FlatCAMObj import *
+import math
+
+
+class ToolCalculator(FlatCAMTool):
+
+    toolName = "Calculators"
+    v_shapeName = "V-Shape Tool Calculator"
+    unitsName = "Units Calculator"
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+
+        self.app = app
+
+        ## Title
+        title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
+        self.layout.addWidget(title_label)
+
+        ## V-shape Tool Calculator
+
+        self.v_shape_spacer_label = QtWidgets.QLabel(" ")
+        self.layout.addWidget(self.v_shape_spacer_label)
+
+        ## Title of the V-shape Tools Calculator
+        v_shape_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.v_shapeName)
+        self.layout.addWidget(v_shape_title_label)
+
+        ## Form Layout
+        form_layout = QtWidgets.QFormLayout()
+        self.layout.addLayout(form_layout)
+
+        self.tipDia_label = QtWidgets.QLabel("Tip Diameter:")
+        self.tipDia_entry = FCEntry()
+        self.tipDia_entry.setFixedWidth(70)
+        self.tipDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.tipDia_entry.setToolTip('This is the diameter of the tool tip.\n'
+                                     'The manufacturer specifies it.')
+
+        self.tipAngle_label = QtWidgets.QLabel("Tip Angle:")
+        self.tipAngle_entry = FCEntry()
+        self.tipAngle_entry.setFixedWidth(70)
+        self.tipAngle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.tipAngle_entry.setToolTip("This is the angle of the tip of the tool.\n"
+                                       "It is specified by manufacturer.")
+
+        self.cutDepth_label = QtWidgets.QLabel("Cut Z:")
+        self.cutDepth_entry = FCEntry()
+        self.cutDepth_entry.setFixedWidth(70)
+        self.cutDepth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.cutDepth_entry.setToolTip("This is the depth to cut into the material.\n"
+                                       "In the CNCJob is the CutZ parameter.")
+
+        self.effectiveToolDia_label = QtWidgets.QLabel("Tool Diameter:")
+        self.effectiveToolDia_entry = FCEntry()
+        self.effectiveToolDia_entry.setFixedWidth(70)
+        self.effectiveToolDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.effectiveToolDia_entry.setToolTip("This is the tool diameter to be entered into\n"
+                                               "FlatCAM Gerber section.\n"
+                                               "In the CNCJob section it is called >Tool dia<.")
+        # self.effectiveToolDia_entry.setEnabled(False)
+
+
+        form_layout.addRow(self.tipDia_label, self.tipDia_entry)
+        form_layout.addRow(self.tipAngle_label, self.tipAngle_entry)
+        form_layout.addRow(self.cutDepth_label, self.cutDepth_entry)
+        form_layout.addRow(self.effectiveToolDia_label, self.effectiveToolDia_entry)
+
+
+        ## Buttons
+        self.calculate_button = QtWidgets.QPushButton("Calculate")
+        self.calculate_button.setFixedWidth(70)
+        self.calculate_button.setToolTip(
+            "Calculate either the Cut Z or the effective tool diameter,\n  "
+            "depending on which is desired and which is known. "
+        )
+        self.empty_label = QtWidgets.QLabel(" ")
+
+        form_layout.addRow(self.empty_label, self.calculate_button)
+
+        ## Units Calculator
+        self.unists_spacer_label = QtWidgets.QLabel(" ")
+        self.layout.addWidget(self.unists_spacer_label)
+
+        ## Title of the Units Calculator
+        units_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.unitsName)
+        self.layout.addWidget(units_label)
+
+        #Form Layout
+        form_units_layout = QtWidgets.QFormLayout()
+        self.layout.addLayout(form_units_layout)
+
+        inch_label = QtWidgets.QLabel("INCH")
+        mm_label = QtWidgets.QLabel("MM")
+
+        self.inch_entry = FCEntry()
+        self.inch_entry.setFixedWidth(70)
+        self.inch_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.inch_entry.setToolTip("Here you enter the value to be converted from INCH to MM")
+
+        self.mm_entry = FCEntry()
+        self.mm_entry.setFixedWidth(70)
+        self.mm_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.mm_entry.setToolTip("Here you enter the value to be converted from MM to INCH")
+
+        form_units_layout.addRow(mm_label, inch_label)
+        form_units_layout.addRow(self.mm_entry, self.inch_entry)
+
+        self.layout.addStretch()
+
+        ## Signals
+        self.cutDepth_entry.textChanged.connect(self.on_calculate_tool_dia)
+        self.cutDepth_entry.editingFinished.connect(self.on_calculate_tool_dia)
+        self.tipDia_entry.editingFinished.connect(self.on_calculate_tool_dia)
+        self.tipAngle_entry.editingFinished.connect(self.on_calculate_tool_dia)
+        self.calculate_button.clicked.connect(self.on_calculate_tool_dia)
+
+        self.mm_entry.editingFinished.connect(self.on_calculate_inch_units)
+        self.inch_entry.editingFinished.connect(self.on_calculate_mm_units)
+
+
+        ## Initialize form
+        if self.app.defaults["units"] == 'MM':
+            self.tipDia_entry.set_value('0.2')
+            self.tipAngle_entry.set_value('45')
+            self.cutDepth_entry.set_value('0.25')
+            self.effectiveToolDia_entry.set_value('0.39')
+        else:
+            self.tipDia_entry.set_value('7.87402')
+            self.tipAngle_entry.set_value('45')
+            self.cutDepth_entry.set_value('9.84252')
+            self.effectiveToolDia_entry.set_value('15.35433')
+
+        self.mm_entry.set_value('0')
+        self.inch_entry.set_value('0')
+
+    def run(self):
+        FlatCAMTool.run(self)
+        self.app.ui.notebook.setTabText(2, "Calc. Tool")
+
+    def on_calculate_tool_dia(self):
+        # Calculation:
+        # Manufacturer gives total angle of the the tip but we need only half of it
+        # tangent(half_tip_angle) = opposite side / adjacent = part_of _real_dia / depth_of_cut
+        # effective_diameter = tip_diameter + part_of_real_dia_left_side + part_of_real_dia_right_side
+        # tool is symmetrical therefore: part_of_real_dia_left_side = part_of_real_dia_right_side
+        # effective_diameter = tip_diameter + (2 * part_of_real_dia_left_side)
+        # effective diameter = tip_diameter + (2 * depth_of_cut * tangent(half_tip_angle))
+
+        try:
+            tip_diameter = float(self.tipDia_entry.get_value())
+            half_tip_angle = float(self.tipAngle_entry.get_value()) / 2
+            cut_depth = float(self.cutDepth_entry.get_value())
+        except TypeError:
+            return
+
+        tool_diameter = tip_diameter + (2 * cut_depth * math.tan(math.radians(half_tip_angle)))
+        self.effectiveToolDia_entry.set_value("%.4f" % tool_diameter)
+
+    def on_calculate_inch_units(self):
+        self.inch_entry.set_value('%.6f' % (float(self.mm_entry.get_value()) / 25.4))
+
+    def on_calculate_mm_units(self):
+        self.mm_entry.set_value('%.6f' % (float(self.inch_entry.get_value()) * 25.4))
+
+# end of file

+ 390 - 0
flatcamTools/ToolCutout.py

@@ -0,0 +1,390 @@
+from FlatCAMTool import FlatCAMTool
+from copy import copy,deepcopy
+from ObjectCollection import *
+from FlatCAMApp import *
+from PyQt5 import QtGui, QtCore, QtWidgets
+from GUIElements import IntEntry, RadioSet, LengthEntry
+
+from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber
+
+class ToolCutout(FlatCAMTool):
+
+    toolName = "Cutout PCB Tool"
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+
+        ## Title
+        title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
+        self.layout.addWidget(title_label)
+
+        ## Form Layout
+        form_layout = QtWidgets.QFormLayout()
+        self.layout.addLayout(form_layout)
+
+        ## Type of object to be cutout
+        self.type_obj_combo = QtWidgets.QComboBox()
+        self.type_obj_combo.addItem("Gerber")
+        self.type_obj_combo.addItem("Excellon")
+        self.type_obj_combo.addItem("Geometry")
+
+        # we get rid of item1 ("Excellon") as it is not suitable for creating film
+        self.type_obj_combo.view().setRowHidden(1, True)
+        self.type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
+        # self.type_obj_combo.setItemIcon(1, QtGui.QIcon("share/drill16.png"))
+        self.type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
+
+        self.type_obj_combo_label = QtWidgets.QLabel("Object Type:")
+        self.type_obj_combo_label.setToolTip(
+            "Specify the type of object to be cutout.\n"
+            "It can be of type: Gerber or Geometry.\n"
+            "What is selected here will dictate the kind\n"
+            "of objects that will populate the 'Object' combobox."
+        )
+        form_layout.addRow(self.type_obj_combo_label, self.type_obj_combo)
+
+        ## Object to be cutout
+        self.obj_combo = QtWidgets.QComboBox()
+        self.obj_combo.setModel(self.app.collection)
+        self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.obj_combo.setCurrentIndex(1)
+        self.object_label = QtWidgets.QLabel("Object:")
+        self.object_label.setToolTip(
+            "Object to be cutout.                        "
+        )
+        form_layout.addRow(self.object_label, self.obj_combo)
+
+        # Tool Diameter
+        self.dia = FCEntry()
+        self.dia_label = QtWidgets.QLabel("Tool Dia:")
+        self.dia_label.setToolTip(
+            "Diameter of the tool used to cutout\n"
+            "the PCB shape out of the surrounding material."
+        )
+        form_layout.addRow(self.dia_label, self.dia)
+
+        # Margin
+        self.margin = FCEntry()
+        self.margin_label = QtWidgets.QLabel("Margin:")
+        self.margin_label.setToolTip(
+            "Margin over bounds. A positive value here\n"
+            "will make the cutout of the PCB further from\n"
+            "the actual PCB border"
+        )
+        form_layout.addRow(self.margin_label, self.margin)
+
+        # Gapsize
+        self.gapsize = FCEntry()
+        self.gapsize_label = QtWidgets.QLabel("Gap size:")
+        self.gapsize_label.setToolTip(
+            "The size of the gaps in the cutout\n"
+            "used to keep the board connected to\n"
+            "the surrounding material (the one \n"
+            "from which the PCB is cutout)."
+        )
+        form_layout.addRow(self.gapsize_label, self.gapsize)
+
+        ## Title2
+        title_ff_label = QtWidgets.QLabel("<font size=4><b>FreeForm Cutout</b></font>")
+        self.layout.addWidget(title_ff_label)
+
+        ## Form Layout
+        form_layout_2 = QtWidgets.QFormLayout()
+        self.layout.addLayout(form_layout_2)
+
+        # How gaps wil be rendered:
+        # lr    - left + right
+        # tb    - top + bottom
+        # 4     - left + right +top + bottom
+        # 2lr   - 2*left + 2*right
+        # 2tb   - 2*top + 2*bottom
+        # 8     - 2*left + 2*right +2*top + 2*bottom
+
+        # Gaps
+        self.gaps = FCEntry()
+        self.gaps_label = QtWidgets.QLabel("Type of gaps:   ")
+        self.gaps_label.setToolTip(
+            "Number of gaps used for the cutout.\n"
+            "There can be maximum 8 bridges/gaps.\n"
+            "The choices are:\n"
+            "- lr    - left + right\n"
+            "- tb    - top + bottom\n"
+            "- 4     - left + right +top + bottom\n"
+            "- 2lr   - 2*left + 2*right\n"
+            "- 2tb  - 2*top + 2*bottom\n"
+            "- 8     - 2*left + 2*right +2*top + 2*bottom"
+        )
+        form_layout_2.addRow(self.gaps_label, self.gaps)
+
+        ## Buttons
+        hlay = QtWidgets.QHBoxLayout()
+        self.layout.addLayout(hlay)
+
+        hlay.addStretch()
+        self.ff_cutout_object_btn = QtWidgets.QPushButton("  FreeForm Cutout Object ")
+        self.ff_cutout_object_btn.setToolTip(
+            "Cutout the selected object.\n"
+            "The cutout shape can be any shape.\n"
+            "Useful when the PCB has a non-rectangular shape.\n"
+            "But if the object to be cutout is of Gerber Type,\n"
+            "it needs to be an outline of the actual board shape."
+        )
+        hlay.addWidget(self.ff_cutout_object_btn)
+
+        ## Title3
+        title_rct_label = QtWidgets.QLabel("<font size=4><b>Rectangular Cutout</b></font>")
+        self.layout.addWidget(title_rct_label)
+
+        ## Form Layout
+        form_layout_3 = QtWidgets.QFormLayout()
+        self.layout.addLayout(form_layout_3)
+
+        gapslabel_rect = QtWidgets.QLabel('Type of gaps:')
+        gapslabel_rect.setToolTip(
+            "Where to place the gaps:\n"
+            "- one gap Top / one gap Bottom\n"
+            "- one gap Left / one gap Right\n"
+            "- one gap on each of the 4 sides."
+        )
+        self.gaps_rect_radio = RadioSet([{'label': 'T/B', 'value': 'tb'},
+                                    {'label': 'L/R', 'value': 'lr'},
+                                    {'label': '4', 'value': '4'}])
+        form_layout_3.addRow(gapslabel_rect, self.gaps_rect_radio)
+
+        hlay2 = QtWidgets.QHBoxLayout()
+        self.layout.addLayout(hlay2)
+
+        hlay2.addStretch()
+        self.rect_cutout_object_btn = QtWidgets.QPushButton("Rectangular Cutout Object")
+        self.rect_cutout_object_btn.setToolTip(
+            "Cutout the selected object.\n"
+            "The resulting cutout shape is\n"
+            "always of a rectangle form and it will be\n"
+            "the bounding box of the Object."
+        )
+        hlay2.addWidget(self.rect_cutout_object_btn)
+
+        self.layout.addStretch()
+
+        ## Init GUI
+        self.dia.set_value(1)
+        self.margin.set_value(0)
+        self.gapsize.set_value(1)
+        self.gaps.set_value(4)
+        self.gaps_rect_radio.set_value("4")
+
+        ## Signals
+        self.ff_cutout_object_btn.clicked.connect(self.on_freeform_cutout)
+        self.rect_cutout_object_btn.clicked.connect(self.on_rectangular_cutout)
+
+        self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
+
+    def on_type_obj_index_changed(self, index):
+        obj_type = self.type_obj_combo.currentIndex()
+        self.obj_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.obj_combo.setCurrentIndex(0)
+
+    def run(self):
+        FlatCAMTool.run(self)
+        self.app.ui.notebook.setTabText(2, "Cutout Tool")
+
+    def on_freeform_cutout(self):
+
+        def subtract_rectangle(obj_, x0, y0, x1, y1):
+            pts = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
+            obj_.subtract_polygon(pts)
+
+        name = self.obj_combo.currentText()
+
+        # Get source object.
+        try:
+            cutout_obj = self.app.collection.get_by_name(str(name))
+        except:
+            self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % name)
+            return "Could not retrieve object: %s" % name
+
+        if cutout_obj is None:
+            self.app.inform.emit("[error_notcl]Object not found: %s" % cutout_obj)
+
+        try:
+            dia = float(self.dia.get_value())
+        except TypeError:
+            self.app.inform.emit("[warning_notcl] Tool diameter value is missing. Add it and retry.")
+            return
+        try:
+            margin = float(self.margin.get_value())
+        except TypeError:
+            self.app.inform.emit("[warning_notcl] Margin value is missing. Add it and retry.")
+            return
+        try:
+            gapsize = float(self.gapsize.get_value())
+        except TypeError:
+            self.app.inform.emit("[warning_notcl] Gap size value is missing. Add it and retry.")
+            return
+        try:
+            gaps = self.gaps.get_value()
+        except TypeError:
+            self.app.inform.emit("[warning_notcl] Number of gaps value is missing. Add it and retry.")
+            return
+
+        if 0 in {dia}:
+            self.app.inform.emit("[warning_notcl]Tool Diameter is zero value. Change it to a positive integer.")
+            return "Tool Diameter is zero value. Change it to a positive integer."
+
+        if gaps not in ['lr', 'tb', '2lr', '2tb', '4', '8']:
+            self.app.inform.emit("[warning_notcl] Gaps value can be only one of: 'lr', 'tb', '2lr', '2tb', 4 or 8. "
+                                 "Fill in a correct value and retry. ")
+            return
+
+        # Get min and max data for each object as we just cut rectangles across X or Y
+        xmin, ymin, xmax, ymax = cutout_obj.bounds()
+        px = 0.5 * (xmin + xmax) + margin
+        py = 0.5 * (ymin + ymax) + margin
+        lenghtx = (xmax - xmin) + (margin * 2)
+        lenghty = (ymax - ymin) + (margin * 2)
+
+        gapsize = gapsize + (dia / 2)
+
+        if isinstance(cutout_obj,FlatCAMGeometry):
+            # rename the obj name so it can be identified as cutout
+            cutout_obj.options["name"] += "_cutout"
+        else:
+            cutout_obj.isolate(dia=dia, passes=1, overlap=1, combine=False, outname="_temp")
+            ext_obj = self.app.collection.get_by_name("_temp")
+
+            def geo_init(geo_obj, app_obj):
+                geo_obj.solid_geometry = obj_exteriors
+
+            outname = cutout_obj.options["name"] + "_cutout"
+
+            obj_exteriors = ext_obj.get_exteriors()
+            self.app.new_object('geometry', outname, geo_init)
+
+            self.app.collection.set_all_inactive()
+            self.app.collection.set_active("_temp")
+            self.app.on_delete()
+
+            cutout_obj = self.app.collection.get_by_name(outname)
+
+        if int(gaps) == 8 or gaps == '2lr':
+            subtract_rectangle(cutout_obj,
+                               xmin - gapsize,  # botleft_x
+                               py - gapsize + lenghty / 4,  # botleft_y
+                               xmax + gapsize,  # topright_x
+                               py + gapsize + lenghty / 4)  # topright_y
+            subtract_rectangle(cutout_obj,
+                               xmin - gapsize,
+                               py - gapsize - lenghty / 4,
+                               xmax + gapsize,
+                               py + gapsize - lenghty / 4)
+
+        if int(gaps) == 8 or gaps == '2tb':
+            subtract_rectangle(cutout_obj,
+                               px - gapsize + lenghtx / 4,
+                               ymin - gapsize,
+                               px + gapsize + lenghtx / 4,
+                               ymax + gapsize)
+            subtract_rectangle(cutout_obj,
+                               px - gapsize - lenghtx / 4,
+                               ymin - gapsize,
+                               px + gapsize - lenghtx / 4,
+                               ymax + gapsize)
+
+        if int(gaps) == 4 or gaps == 'lr':
+            subtract_rectangle(cutout_obj,
+                               xmin - gapsize,
+                               py - gapsize,
+                               xmax + gapsize,
+                               py + gapsize)
+
+        if int(gaps) == 4 or gaps == 'tb':
+            subtract_rectangle(cutout_obj,
+                               px - gapsize,
+                               ymin - gapsize,
+                               px + gapsize,
+                               ymax + gapsize)
+
+        cutout_obj.plot()
+        self.app.inform.emit("[success] Any form CutOut operation finished.")
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
+
+    def on_rectangular_cutout(self):
+        name = self.obj_combo.currentText()
+
+        # Get source object.
+        try:
+            cutout_obj = self.app.collection.get_by_name(str(name))
+        except:
+            self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % name)
+            return "Could not retrieve object: %s" % name
+
+        if cutout_obj is None:
+            self.app.inform.emit("[error_notcl]Object not found: %s" % cutout_obj)
+
+        try:
+            dia = float(self.dia.get_value())
+        except TypeError:
+            self.app.inform.emit("[warning_notcl] Tool diameter value is missing. Add it and retry.")
+            return
+        try:
+            margin = float(self.margin.get_value())
+        except TypeError:
+            self.app.inform.emit("[warning_notcl] Margin value is missing. Add it and retry.")
+            return
+        try:
+            gapsize = float(self.gapsize.get_value())
+        except TypeError:
+            self.app.inform.emit("[warning_notcl] Gap size value is missing. Add it and retry.")
+            return
+        try:
+            gaps = self.gaps_rect_radio.get_value()
+        except TypeError:
+            self.app.inform.emit("[warning_notcl] Number of gaps value is missing. Add it and retry.")
+            return
+
+        if 0 in {dia}:
+            self.app.inform.emit("[error_notcl]Tool Diameter is zero value. Change it to a positive integer.")
+            return "Tool Diameter is zero value. Change it to a positive integer."
+
+        def geo_init(geo_obj, app_obj):
+            real_margin = margin + (dia / 2)
+            real_gap_size = gapsize + dia
+
+            minx, miny, maxx, maxy = cutout_obj.bounds()
+            minx -= real_margin
+            maxx += real_margin
+            miny -= real_margin
+            maxy += real_margin
+            midx = 0.5 * (minx + maxx)
+            midy = 0.5 * (miny + maxy)
+            hgap = 0.5 * real_gap_size
+            pts = [[midx - hgap, maxy],
+                   [minx, maxy],
+                   [minx, midy + hgap],
+                   [minx, midy - hgap],
+                   [minx, miny],
+                   [midx - hgap, miny],
+                   [midx + hgap, miny],
+                   [maxx, miny],
+                   [maxx, midy - hgap],
+                   [maxx, midy + hgap],
+                   [maxx, maxy],
+                   [midx + hgap, maxy]]
+            cases = {"tb": [[pts[0], pts[1], pts[4], pts[5]],
+                            [pts[6], pts[7], pts[10], pts[11]]],
+                     "lr": [[pts[9], pts[10], pts[1], pts[2]],
+                            [pts[3], pts[4], pts[7], pts[8]]],
+                     "4": [[pts[0], pts[1], pts[2]],
+                           [pts[3], pts[4], pts[5]],
+                           [pts[6], pts[7], pts[8]],
+                           [pts[9], pts[10], pts[11]]]}
+            cuts = cases[gaps]
+            geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
+
+        # TODO: Check for None
+        self.app.new_object("geometry", name + "_cutout", geo_init)
+        self.app.inform.emit("[success] Rectangular CutOut operation finished.")
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
+
+    def reset_fields(self):
+        self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))

+ 467 - 0
flatcamTools/ToolDblSided.py

@@ -0,0 +1,467 @@
+from PyQt5 import QtGui
+from GUIElements import RadioSet, EvalEntry, LengthEntry
+from FlatCAMTool import FlatCAMTool
+from FlatCAMObj import *
+from shapely.geometry import Point
+from shapely import affinity
+from PyQt5 import QtCore
+
+class DblSidedTool(FlatCAMTool):
+
+    toolName = "Double-Sided PCB Tool"
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+
+        ## Title
+        title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
+        self.layout.addWidget(title_label)
+
+        self.empty_lb = QtWidgets.QLabel("")
+        self.layout.addWidget(self.empty_lb)
+
+        ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay)
+
+        ## Gerber Object to mirror
+        self.gerber_object_combo = QtWidgets.QComboBox()
+        self.gerber_object_combo.setModel(self.app.collection)
+        self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.gerber_object_combo.setCurrentIndex(1)
+
+        self.botlay_label = QtWidgets.QLabel("<b>GERBER:</b>")
+        self.botlay_label.setToolTip(
+            "Gerber  to be mirrored."
+        )
+
+        self.mirror_gerber_button = QtWidgets.QPushButton("Mirror")
+        self.mirror_gerber_button.setToolTip(
+            "Mirrors (flips) the specified object around \n"
+            "the specified axis. Does not create a new \n"
+            "object, but modifies it."
+        )
+        self.mirror_gerber_button.setFixedWidth(40)
+
+        # grid_lay.addRow("Bottom Layer:", self.object_combo)
+        grid_lay.addWidget(self.botlay_label, 0, 0)
+        grid_lay.addWidget(self.gerber_object_combo, 1, 0, 1, 2)
+        grid_lay.addWidget(self.mirror_gerber_button, 1, 3)
+
+        ## Excellon Object to mirror
+        self.exc_object_combo = QtWidgets.QComboBox()
+        self.exc_object_combo.setModel(self.app.collection)
+        self.exc_object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
+        self.exc_object_combo.setCurrentIndex(1)
+
+        self.excobj_label = QtWidgets.QLabel("<b>EXCELLON:</b>")
+        self.excobj_label.setToolTip(
+            "Excellon Object to be mirrored."
+        )
+
+        self.mirror_exc_button = QtWidgets.QPushButton("Mirror")
+        self.mirror_exc_button.setToolTip(
+            "Mirrors (flips) the specified object around \n"
+            "the specified axis. Does not create a new \n"
+            "object, but modifies it."
+        )
+        self.mirror_exc_button.setFixedWidth(40)
+
+        # grid_lay.addRow("Bottom Layer:", self.object_combo)
+        grid_lay.addWidget(self.excobj_label, 2, 0)
+        grid_lay.addWidget(self.exc_object_combo, 3, 0, 1, 2)
+        grid_lay.addWidget(self.mirror_exc_button, 3, 3)
+
+        ## Geometry Object to mirror
+        self.geo_object_combo = QtWidgets.QComboBox()
+        self.geo_object_combo.setModel(self.app.collection)
+        self.geo_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
+        self.geo_object_combo.setCurrentIndex(1)
+
+        self.geoobj_label = QtWidgets.QLabel("<b>GEOMETRY</b>:")
+        self.geoobj_label.setToolTip(
+            "Geometry Obj to be mirrored."
+        )
+
+        self.mirror_geo_button = QtWidgets.QPushButton("Mirror")
+        self.mirror_geo_button.setToolTip(
+            "Mirrors (flips) the specified object around \n"
+            "the specified axis. Does not create a new \n"
+            "object, but modifies it."
+        )
+        self.mirror_geo_button.setFixedWidth(40)
+
+        # grid_lay.addRow("Bottom Layer:", self.object_combo)
+        grid_lay.addWidget(self.geoobj_label, 4, 0)
+        grid_lay.addWidget(self.geo_object_combo, 5, 0, 1, 2)
+        grid_lay.addWidget(self.mirror_geo_button, 5, 3)
+
+        ## Axis
+        self.mirror_axis = RadioSet([{'label': 'X', 'value': 'X'},
+                                     {'label': 'Y', 'value': 'Y'}])
+        self.mirax_label = QtWidgets.QLabel("Mirror Axis:")
+        self.mirax_label.setToolTip(
+            "Mirror vertically (X) or horizontally (Y)."
+        )
+        # grid_lay.addRow("Mirror Axis:", self.mirror_axis)
+        self.empty_lb1 = QtWidgets.QLabel("")
+        grid_lay.addWidget(self.empty_lb1, 6, 0)
+        grid_lay.addWidget(self.mirax_label, 7, 0)
+        grid_lay.addWidget(self.mirror_axis, 7, 1)
+
+        ## Axis Location
+        self.axis_location = RadioSet([{'label': 'Point', 'value': 'point'},
+                                       {'label': 'Box', 'value': 'box'}])
+        self.axloc_label = QtWidgets.QLabel("Axis Ref:")
+        self.axloc_label.setToolTip(
+            "The axis should pass through a <b>point</b> or cut\n "
+            "a specified <b>box</b> (in a Geometry object) in \n"
+            "the middle."
+        )
+        # grid_lay.addRow("Axis Location:", self.axis_location)
+        grid_lay.addWidget(self.axloc_label, 8, 0)
+        grid_lay.addWidget(self.axis_location, 8, 1)
+
+        self.empty_lb2 = QtWidgets.QLabel("")
+        grid_lay.addWidget(self.empty_lb2, 9, 0)
+
+        ## Point/Box
+        self.point_box_container = QtWidgets.QVBoxLayout()
+        self.pb_label = QtWidgets.QLabel("<b>Point/Box:</b>")
+        self.pb_label.setToolTip(
+            "Specify the point (x, y) through which the mirror axis \n "
+            "passes or the Geometry object containing a rectangle \n"
+            "that the mirror axis cuts in half."
+        )
+        # grid_lay.addRow("Point/Box:", self.point_box_container)
+
+        self.add_point_button = QtWidgets.QPushButton("Add")
+        self.add_point_button.setToolTip(
+            "Add the <b>point (x, y)</b> through which the mirror axis \n "
+            "passes or the Object containing a rectangle \n"
+            "that the mirror axis cuts in half.\n"
+            "The point is captured by pressing SHIFT key\n"
+            "and left mouse clicking on canvas or you can enter them manually."
+        )
+        self.add_point_button.setFixedWidth(40)
+
+        grid_lay.addWidget(self.pb_label, 10, 0)
+        grid_lay.addLayout(self.point_box_container, 11, 0, 1, 3)
+        grid_lay.addWidget(self.add_point_button, 11, 3)
+
+        self.point_entry = EvalEntry()
+
+        self.point_box_container.addWidget(self.point_entry)
+        self.box_combo = QtWidgets.QComboBox()
+        self.box_combo.setModel(self.app.collection)
+        self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.box_combo.setCurrentIndex(1)
+
+        self.box_combo_type = QtWidgets.QComboBox()
+        self.box_combo_type.addItem("Gerber   Reference Box Object")
+        self.box_combo_type.addItem("Excellon Reference Box Object")
+        self.box_combo_type.addItem("Geometry Reference Box Object")
+
+        self.point_box_container.addWidget(self.box_combo_type)
+        self.point_box_container.addWidget(self.box_combo)
+        self.box_combo.hide()
+        self.box_combo_type.hide()
+
+
+        ## Alignment holes
+        self.ah_label = QtWidgets.QLabel("<b>Alignment Drill Coordinates:</b>")
+        self.ah_label.setToolTip(
+            "Alignment holes (x1, y1), (x2, y2), ... "
+            "on one side of the mirror axis. For each set of (x, y) coordinates\n"
+            "entered here, a pair of drills will be created: one on the\n"
+            "coordinates entered and one in mirror position over the axis\n"
+            "selected above in the 'Mirror Axis'."
+        )
+        self.layout.addWidget(self.ah_label)
+
+        grid_lay1 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay1)
+
+        self.alignment_holes = EvalEntry()
+
+        self.add_drill_point_button = QtWidgets.QPushButton("Add")
+        self.add_drill_point_button.setToolTip(
+            "Add alignment drill holes coords (x1, y1), (x2, y2), ... \n"
+            "on one side of the mirror axis.\n"
+            "The point(s) can be captured by pressing SHIFT key\n"
+            "and left mouse clicking on canvas. Or you can enter them manually."
+        )
+        self.add_drill_point_button.setFixedWidth(40)
+
+        grid_lay1.addWidget(self.alignment_holes, 0, 0, 1, 2)
+        grid_lay1.addWidget(self.add_drill_point_button, 0, 3)
+
+        ## Drill diameter for alignment holes
+        self.dt_label = QtWidgets.QLabel("<b>Alignment Drill Creation</b>:")
+        self.dt_label.setToolTip(
+            "Create a set of alignment drill holes\n"
+            "with the specified diameter,\n"
+            "at the specified coordinates."
+        )
+        self.layout.addWidget(self.dt_label)
+
+        grid_lay2 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay2)
+
+        self.drill_dia = LengthEntry()
+        self.dd_label = QtWidgets.QLabel("Drill diam.:")
+        self.dd_label.setToolTip(
+            "Diameter of the drill for the "
+            "alignment holes."
+        )
+        grid_lay2.addWidget(self.dd_label, 0, 0)
+        grid_lay2.addWidget(self.drill_dia, 0, 1)
+
+        ## Buttons
+        self.create_alignment_hole_button = QtWidgets.QPushButton("Create Excellon Object")
+        self.create_alignment_hole_button.setToolTip(
+            "Creates an Excellon Object containing the\n"
+            "specified alignment holes and their mirror\n"
+            "images.")
+        # self.create_alignment_hole_button.setFixedWidth(40)
+        grid_lay2.addWidget(self.create_alignment_hole_button, 1,0, 1, 2)
+
+        self.reset_button = QtWidgets.QPushButton("Reset")
+        self.reset_button.setToolTip(
+            "Resets all the fields.")
+        self.reset_button.setFixedWidth(40)
+        grid_lay2.addWidget(self.reset_button, 1, 2)
+
+        self.layout.addStretch()
+
+        ## Signals
+        self.create_alignment_hole_button.clicked.connect(self.on_create_alignment_holes)
+        self.mirror_gerber_button.clicked.connect(self.on_mirror_gerber)
+        self.mirror_exc_button.clicked.connect(self.on_mirror_exc)
+        self.mirror_geo_button.clicked.connect(self.on_mirror_geo)
+        self.add_point_button.clicked.connect(self.on_point_add)
+        self.add_drill_point_button.clicked.connect(self.on_drill_add)
+        self.reset_button.clicked.connect(self.reset_fields)
+
+        self.box_combo_type.currentIndexChanged.connect(self.on_combo_box_type)
+
+        self.axis_location.group_toggle_fn = self.on_toggle_pointbox
+
+        self.drill_values = ""
+
+        ## Initialize form
+        self.mirror_axis.set_value('X')
+        self.axis_location.set_value('point')
+        self.drill_dia.set_value(1)
+
+    def on_combo_box_type(self):
+        obj_type = self.box_combo_type.currentIndex()
+        self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.box_combo.setCurrentIndex(0)
+
+    def on_create_alignment_holes(self):
+        axis = self.mirror_axis.get_value()
+        mode = self.axis_location.get_value()
+
+        if mode == "point":
+            try:
+                px, py = self.point_entry.get_value()
+            except TypeError:
+                self.app.inform.emit("[warning_notcl] 'Point' reference is selected and 'Point' coordinates "
+                                     "are missing. Add them and retry.")
+                return
+        else:
+            selection_index = self.box_combo.currentIndex()
+            model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
+            bb_obj = model_index.internalPointer().obj
+            xmin, ymin, xmax, ymax = bb_obj.bounds()
+            px = 0.5 * (xmin + xmax)
+            py = 0.5 * (ymin + ymax)
+
+        xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
+
+        dia = self.drill_dia.get_value()
+        tools = {"1": {"C": dia}}
+
+        # holes = self.alignment_holes.get_value()
+        holes = eval('[{}]'.format(self.alignment_holes.text()))
+        if not holes:
+            self.app.inform.emit("[warning_notcl] There are no Alignment Drill Coordinates to use. Add them and retry.")
+            return
+
+        drills = []
+
+        for hole in holes:
+            point = Point(hole)
+            point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
+            drills.append({"point": point, "tool": "1"})
+            drills.append({"point": point_mirror, "tool": "1"})
+
+        def obj_init(obj_inst, app_inst):
+            obj_inst.tools = tools
+            obj_inst.drills = drills
+            obj_inst.create_geometry()
+
+        self.app.new_object("excellon", "Alignment Drills", obj_init)
+        self.drill_values = ''
+
+    def on_mirror_gerber(self):
+        selection_index = self.gerber_object_combo.currentIndex()
+        # fcobj = self.app.collection.object_list[selection_index]
+        model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
+        try:
+            fcobj = model_index.internalPointer().obj
+        except Exception as e:
+            self.app.inform.emit("[warning_notcl] There is no Gerber object loaded ...")
+            return
+
+        if not isinstance(fcobj, FlatCAMGerber):
+            self.app.inform.emit("[error_notcl] Only Gerber, Excellon and Geometry objects can be mirrored.")
+            return
+
+        axis = self.mirror_axis.get_value()
+        mode = self.axis_location.get_value()
+
+        if mode == "point":
+            try:
+                px, py = self.point_entry.get_value()
+            except TypeError:
+                self.app.inform.emit("[warning_notcl] 'Point' coordinates missing. "
+                                     "Using Origin (0, 0) as mirroring reference.")
+                px, py = (0, 0)
+
+        else:
+            selection_index_box = self.box_combo.currentIndex()
+            model_index_box = self.app.collection.index(selection_index_box, 0, self.box_combo.rootModelIndex())
+            try:
+                bb_obj = model_index_box.internalPointer().obj
+            except Exception as e:
+                self.app.inform.emit("[warning_notcl] There is no Box object loaded ...")
+                return
+
+            xmin, ymin, xmax, ymax = bb_obj.bounds()
+            px = 0.5 * (xmin + xmax)
+            py = 0.5 * (ymin + ymax)
+
+        fcobj.mirror(axis, [px, py])
+        self.app.object_changed.emit(fcobj)
+        fcobj.plot()
+
+    def on_mirror_exc(self):
+        selection_index = self.exc_object_combo.currentIndex()
+        # fcobj = self.app.collection.object_list[selection_index]
+        model_index = self.app.collection.index(selection_index, 0, self.exc_object_combo.rootModelIndex())
+        try:
+            fcobj = model_index.internalPointer().obj
+        except Exception as e:
+            self.app.inform.emit("[warning_notcl] There is no Excellon object loaded ...")
+            return
+
+        if not isinstance(fcobj, FlatCAMExcellon):
+            self.app.inform.emit("[error_notcl] Only Gerber, Excellon and Geometry objects can be mirrored.")
+            return
+
+        axis = self.mirror_axis.get_value()
+        mode = self.axis_location.get_value()
+
+        if mode == "point":
+            px, py = self.point_entry.get_value()
+        else:
+            selection_index_box = self.box_combo.currentIndex()
+            model_index_box = self.app.collection.index(selection_index_box, 0, self.box_combo.rootModelIndex())
+            try:
+                bb_obj = model_index_box.internalPointer().obj
+            except Exception as e:
+                self.app.inform.emit("[warning_notcl] There is no Box object loaded ...")
+                return
+
+            xmin, ymin, xmax, ymax = bb_obj.bounds()
+            px = 0.5 * (xmin + xmax)
+            py = 0.5 * (ymin + ymax)
+
+        fcobj.mirror(axis, [px, py])
+        self.app.object_changed.emit(fcobj)
+        fcobj.plot()
+
+    def on_mirror_geo(self):
+        selection_index = self.geo_object_combo.currentIndex()
+        # fcobj = self.app.collection.object_list[selection_index]
+        model_index = self.app.collection.index(selection_index, 0, self.geo_object_combo.rootModelIndex())
+        try:
+            fcobj = model_index.internalPointer().obj
+        except Exception as e:
+            self.app.inform.emit("[warning_notcl] There is no Geometry object loaded ...")
+            return
+
+        if not isinstance(fcobj, FlatCAMGeometry):
+            self.app.inform.emit("[error_notcl] Only Gerber, Excellon and Geometry objects can be mirrored.")
+            return
+
+        axis = self.mirror_axis.get_value()
+        mode = self.axis_location.get_value()
+
+        if mode == "point":
+            px, py = self.point_entry.get_value()
+        else:
+            selection_index_box = self.box_combo.currentIndex()
+            model_index_box = self.app.collection.index(selection_index_box, 0, self.box_combo.rootModelIndex())
+            try:
+                bb_obj = model_index_box.internalPointer().obj
+            except Exception as e:
+                self.app.inform.emit("[warning_notcl] There is no Box object loaded ...")
+                return
+
+            xmin, ymin, xmax, ymax = bb_obj.bounds()
+            px = 0.5 * (xmin + xmax)
+            py = 0.5 * (ymin + ymax)
+
+        fcobj.mirror(axis, [px, py])
+        self.app.object_changed.emit(fcobj)
+        fcobj.plot()
+
+    def on_point_add(self):
+        val = self.app.defaults["global_point_clipboard_format"] % (self.app.pos[0], self.app.pos[1])
+        self.point_entry.set_value(val)
+
+    def on_drill_add(self):
+        self.drill_values += (self.app.defaults["global_point_clipboard_format"] %
+                              (self.app.pos[0], self.app.pos[1])) + ','
+        self.alignment_holes.set_value(self.drill_values)
+
+    def on_toggle_pointbox(self):
+        if self.axis_location.get_value() == "point":
+            self.point_entry.show()
+            self.box_combo.hide()
+            self.box_combo_type.hide()
+            self.add_point_button.setDisabled(False)
+        else:
+            self.point_entry.hide()
+            self.box_combo.show()
+            self.box_combo_type.show()
+            self.add_point_button.setDisabled(True)
+
+    def reset_fields(self):
+        self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.exc_object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
+        self.geo_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
+        self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+
+        self.gerber_object_combo.setCurrentIndex(0)
+        self.exc_object_combo.setCurrentIndex(0)
+        self.geo_object_combo.setCurrentIndex(0)
+        self.box_combo.setCurrentIndex(0)
+        self.box_combo_type.setCurrentIndex(0)
+
+
+        self.drill_values = ""
+        self.point_entry.set_value("")
+        self.alignment_holes.set_value("")
+        ## Initialize form
+        self.mirror_axis.set_value('X')
+        self.axis_location.set_value('point')
+        self.drill_dia.set_value(1)
+
+    def run(self):
+        FlatCAMTool.run(self)
+        self.app.ui.notebook.setTabText(2, "2-Sided Tool")
+        self.reset_fields()

+ 206 - 0
flatcamTools/ToolFilm.py

@@ -0,0 +1,206 @@
+from FlatCAMTool import FlatCAMTool
+
+from GUIElements import RadioSet, FloatEntry
+from PyQt5 import QtGui, QtCore, QtWidgets
+
+
+class Film(FlatCAMTool):
+
+    toolName = "Film PCB Tool"
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+
+        # Title
+        title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
+        self.layout.addWidget(title_label)
+
+        # Form Layout
+        tf_form_layout = QtWidgets.QFormLayout()
+        self.layout.addLayout(tf_form_layout)
+
+        # Type of object for which to create the film
+        self.tf_type_obj_combo = QtWidgets.QComboBox()
+        self.tf_type_obj_combo.addItem("Gerber")
+        self.tf_type_obj_combo.addItem("Excellon")
+        self.tf_type_obj_combo.addItem("Geometry")
+
+        # we get rid of item1 ("Excellon") as it is not suitable for creating film
+        self.tf_type_obj_combo.view().setRowHidden(1, True)
+        self.tf_type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
+        self.tf_type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
+
+        self.tf_type_obj_combo_label = QtWidgets.QLabel("Object Type:")
+        self.tf_type_obj_combo_label.setToolTip(
+            "Specify the type of object for which to create the film.\n"
+            "The object can be of type: Gerber or Geometry.\n"
+            "The selection here decide the type of objects that will be\n"
+            "in the Film Object combobox."
+        )
+        tf_form_layout.addRow(self.tf_type_obj_combo_label, self.tf_type_obj_combo)
+
+        # List of objects for which we can create the film
+        self.tf_object_combo = QtWidgets.QComboBox()
+        self.tf_object_combo.setModel(self.app.collection)
+        self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.tf_object_combo.setCurrentIndex(1)
+        self.tf_object_label = QtWidgets.QLabel("Film Object:")
+        self.tf_object_label.setToolTip(
+            "Object for which to create the film."
+        )
+        tf_form_layout.addRow(self.tf_object_label, self.tf_object_combo)
+
+        # Type of Box Object to be used as an envelope for film creation
+        # Within this we can create negative
+        self.tf_type_box_combo = QtWidgets.QComboBox()
+        self.tf_type_box_combo.addItem("Gerber")
+        self.tf_type_box_combo.addItem("Excellon")
+        self.tf_type_box_combo.addItem("Geometry")
+
+        # we get rid of item1 ("Excellon") as it is not suitable for box when creating film
+        self.tf_type_box_combo.view().setRowHidden(1, True)
+        self.tf_type_box_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
+        self.tf_type_box_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
+
+        self.tf_type_box_combo_label = QtWidgets.QLabel("Box Type:")
+        self.tf_type_box_combo_label.setToolTip(
+            "Specify the type of object to be used as an container for\n"
+            "film creation. It can be: Gerber or Geometry type."
+            "The selection here decide the type of objects that will be\n"
+            "in the Box Object combobox."
+        )
+        tf_form_layout.addRow(self.tf_type_box_combo_label, self.tf_type_box_combo)
+
+        # Box
+        self.tf_box_combo = QtWidgets.QComboBox()
+        self.tf_box_combo.setModel(self.app.collection)
+        self.tf_box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.tf_box_combo.setCurrentIndex(1)
+
+        self.tf_box_combo_label = QtWidgets.QLabel("Box Object:")
+        self.tf_box_combo_label.setToolTip(
+            "The actual object that is used a container for the\n "
+            "selected object for which we create the film.\n"
+            "Usually it is the PCB outline but it can be also the\n"
+            "same object for which the film is created.")
+        tf_form_layout.addRow(self.tf_box_combo_label, self.tf_box_combo)
+
+        # Film Type
+        self.film_type = RadioSet([{'label': 'Positive', 'value': 'pos'},
+                                     {'label': 'Negative', 'value': 'neg'}])
+        self.film_type_label = QtWidgets.QLabel("Film Type:")
+        self.film_type_label.setToolTip(
+            "Generate a Positive black film or a Negative film.\n"
+            "Positive means that it will print the features\n"
+            "with black on a white canvas.\n"
+            "Negative means that it will print the features\n"
+            "with white on a black canvas.\n"
+            "The Film format is SVG."
+        )
+        tf_form_layout.addRow(self.film_type_label, self.film_type)
+
+        # Boundary for negative film generation
+
+        self.boundary_entry = FloatEntry()
+        self.boundary_label = QtWidgets.QLabel("Border:")
+        self.boundary_label.setToolTip(
+            "Specify a border around the object.\n"
+            "Only for negative film.\n"
+            "It helps if we use as a Box Object the same \n"
+            "object as in Film Object. It will create a thick\n"
+            "black bar around the actual print allowing for a\n"
+            "better delimitation of the outline features which are of\n"
+            "white color like the rest and which may confound with the\n"
+            "surroundings if not for this border."
+        )
+        tf_form_layout.addRow(self.boundary_label, self.boundary_entry)
+
+        # Buttons
+        hlay = QtWidgets.QHBoxLayout()
+        self.layout.addLayout(hlay)
+        hlay.addStretch()
+
+        self.film_object_button = QtWidgets.QPushButton("Save Film")
+        self.film_object_button.setToolTip(
+            "Create a Film for the selected object, within\n"
+            "the specified box. Does not create a new \n "
+            "FlatCAM object, but directly save it in SVG format\n"
+            "which can be opened with Inkscape."
+        )
+        hlay.addWidget(self.film_object_button)
+
+        self.layout.addStretch()
+
+        ## Signals
+        self.film_object_button.clicked.connect(self.on_film_creation)
+        self.tf_type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
+        self.tf_type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed)
+
+        ## Initialize form
+        self.film_type.set_value('neg')
+        self.boundary_entry.set_value(0.0)
+
+    def on_type_obj_index_changed(self, index):
+        obj_type = self.tf_type_obj_combo.currentIndex()
+        self.tf_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.tf_object_combo.setCurrentIndex(0)
+
+    def on_type_box_index_changed(self, index):
+        obj_type = self.tf_type_box_combo.currentIndex()
+        self.tf_box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.tf_box_combo.setCurrentIndex(0)
+
+    def run(self):
+        FlatCAMTool.run(self)
+        self.app.ui.notebook.setTabText(2, "Film Tool")
+
+    def on_film_creation(self):
+        try:
+            name = self.tf_object_combo.currentText()
+        except:
+            self.app.inform.emit("[error_notcl] No Film object selected. Load a Film object and retry.")
+            return
+        try:
+            boxname = self.tf_box_combo.currentText()
+        except:
+            self.app.inform.emit("[error_notcl] No Box object selected. Load a Box object and retry.")
+            return
+
+        border = float(self.boundary_entry.get_value())
+        if border is None:
+            border = 0
+
+        self.app.inform.emit("Generating Film ...")
+
+        if self.film_type.get_value() == "pos":
+            try:
+                filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG positive",
+                                                             directory=self.app.get_last_save_folder(), filter="*.svg")
+            except TypeError:
+                filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG positive")
+
+            filename = str(filename)
+
+            if str(filename) == "":
+                self.app.inform.emit("Export SVG positive cancelled.")
+                return
+            else:
+                self.app.export_svg_black(name, boxname, filename)
+        else:
+            try:
+                filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG negative",
+                                                             directory=self.app.get_last_save_folder(), filter="*.svg")
+            except TypeError:
+                filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG negative")
+
+            filename = str(filename)
+
+            if str(filename) == "":
+                self.app.inform.emit("Export SVG negative cancelled.")
+                return
+            else:
+                self.app.export_svg_negative(name, boxname, filename, border)
+
+    def reset_fields(self):
+        self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.tf_box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))

+ 172 - 0
flatcamTools/ToolImage.py

@@ -0,0 +1,172 @@
+from FlatCAMTool import FlatCAMTool
+
+from GUIElements import RadioSet, FloatEntry, FCComboBox, IntEntry
+from PyQt5 import QtGui, QtCore, QtWidgets
+
+
+class ToolImage(FlatCAMTool):
+
+    toolName = "Image as Object"
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+
+        # Title
+        title_label = QtWidgets.QLabel("<font size=4><b>IMAGE to PCB</b></font>")
+        self.layout.addWidget(title_label)
+
+        # Form Layout
+        ti_form_layout = QtWidgets.QFormLayout()
+        self.layout.addLayout(ti_form_layout)
+
+        # Type of object to create for the image
+        self.tf_type_obj_combo = FCComboBox()
+        self.tf_type_obj_combo.addItem("Gerber")
+        self.tf_type_obj_combo.addItem("Geometry")
+
+        self.tf_type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
+        self.tf_type_obj_combo.setItemIcon(1, QtGui.QIcon("share/geometry16.png"))
+
+        self.tf_type_obj_combo_label = QtWidgets.QLabel("Object Type:")
+        self.tf_type_obj_combo_label.setToolTip(
+            "Specify the type of object to create from the image.\n"
+            "It can be of type: Gerber or Geometry."
+
+        )
+        ti_form_layout.addRow(self.tf_type_obj_combo_label, self.tf_type_obj_combo)
+
+        # DPI value of the imported image
+        self.dpi_entry = IntEntry()
+        self.dpi_label = QtWidgets.QLabel("DPI value:")
+        self.dpi_label.setToolTip(
+            "Specify a DPI value for the image."
+        )
+        ti_form_layout.addRow(self.dpi_label, self.dpi_entry)
+
+        self.emty_lbl = QtWidgets.QLabel("")
+        self.layout.addWidget(self.emty_lbl)
+
+        self.detail_label = QtWidgets.QLabel("<font size=4><b>Level of detail:</b>")
+        self.layout.addWidget(self.detail_label)
+
+        ti2_form_layout = QtWidgets.QFormLayout()
+        self.layout.addLayout(ti2_form_layout)
+
+        # Type of image interpretation
+        self.image_type = RadioSet([{'label': 'B/W', 'value': 'black'},
+                                     {'label': 'Color', 'value': 'color'}])
+        self.image_type_label = QtWidgets.QLabel("<b>Image type:</b>")
+        self.image_type_label.setToolTip(
+            "Choose a method for the image interpretation.\n"
+            "B/W means a black & white image. Color means a colored image."
+        )
+        ti2_form_layout.addRow(self.image_type_label, self.image_type)
+
+        # Mask value of the imported image when image monochrome
+        self.mask_bw_entry = IntEntry()
+        self.mask_bw_label = QtWidgets.QLabel("Mask value <b>B/W</b>:")
+        self.mask_bw_label.setToolTip(
+            "Mask for monochrome image.\n"
+            "Takes values between [0 ... 255].\n"
+            "Decides the level of details to include\n"
+            "in the resulting geometry.\n"
+            "0 means no detail and 255 means everything \n"
+            "(which is totally black)."
+        )
+        ti2_form_layout.addRow(self.mask_bw_label, self.mask_bw_entry)
+
+        # Mask value of the imported image for RED color when image color
+        self.mask_r_entry = IntEntry()
+        self.mask_r_label = QtWidgets.QLabel("Mask value <b>R:</b>")
+        self.mask_r_label.setToolTip(
+            "Mask for RED color.\n"
+            "Takes values between [0 ... 255].\n"
+            "Decides the level of details to include\n"
+            "in the resulting geometry."
+        )
+        ti2_form_layout.addRow(self.mask_r_label, self.mask_r_entry)
+
+        # Mask value of the imported image for GREEN color when image color
+        self.mask_g_entry = IntEntry()
+        self.mask_g_label = QtWidgets.QLabel("Mask value <b>G:</b>")
+        self.mask_g_label.setToolTip(
+            "Mask for GREEN color.\n"
+            "Takes values between [0 ... 255].\n"
+            "Decides the level of details to include\n"
+            "in the resulting geometry."
+        )
+        ti2_form_layout.addRow(self.mask_g_label, self.mask_g_entry)
+
+        # Mask value of the imported image for BLUE color when image color
+        self.mask_b_entry = IntEntry()
+        self.mask_b_label = QtWidgets.QLabel("Mask value <b>B:</b>")
+        self.mask_b_label.setToolTip(
+            "Mask for BLUE color.\n"
+            "Takes values between [0 ... 255].\n"
+            "Decides the level of details to include\n"
+            "in the resulting geometry."
+        )
+        ti2_form_layout.addRow(self.mask_b_label, self.mask_b_entry)
+
+        # Buttons
+        hlay = QtWidgets.QHBoxLayout()
+        self.layout.addLayout(hlay)
+        hlay.addStretch()
+
+        self.import_button = QtWidgets.QPushButton("Import image")
+        self.import_button.setToolTip(
+            "Open a image of raster type and then import it in FlatCAM."
+        )
+        hlay.addWidget(self.import_button)
+
+        self.layout.addStretch()
+
+        ## Signals
+        self.import_button.clicked.connect(self.on_file_importimage)
+
+        ## Initialize form
+        self.dpi_entry.set_value(96)
+        self.image_type.set_value('black')
+        self.mask_bw_entry.set_value(250)
+        self.mask_r_entry.set_value(250)
+        self.mask_g_entry.set_value(250)
+        self.mask_b_entry.set_value(250)
+
+    def run(self):
+        FlatCAMTool.run(self)
+        self.app.ui.notebook.setTabText(2, "Image Tool")
+
+    def on_file_importimage(self):
+        """
+        Callback for menu item File->Import IMAGE.
+        :param type_of_obj: to import the IMAGE as Geometry or as Gerber
+        :type type_of_obj: str
+        :return: None
+        """
+        mask = []
+        self.app.log.debug("on_file_importimage()")
+
+        filter = "Image Files(*.BMP *.PNG *.JPG *.JPEG);;" \
+                 "Bitmap File (*.BMP);;" \
+                 "PNG File (*.PNG);;" \
+                 "Jpeg File (*.JPG);;" \
+                 "All Files (*.*)"
+        try:
+            filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Import IMAGE",
+                                                         directory=self.app.get_last_folder(), filter=filter)
+        except TypeError:
+            filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Import IMAGE", filter=filter)
+
+        filename = str(filename)
+        type = self.tf_type_obj_combo.get_value().lower()
+        dpi = self.dpi_entry.get_value()
+        mode = self.image_type.get_value()
+        mask = [self.mask_bw_entry.get_value(), self.mask_r_entry.get_value(),self.mask_g_entry.get_value(),
+                self.mask_b_entry.get_value()]
+
+        if filename == "":
+            self.app.inform.emit("Open cancelled.")
+        else:
+            self.app.worker_task.emit({'fcn': self.app.import_image,
+                                       'params': [filename, type, dpi, mode, mask]})
+            #  self.import_svg(filename, "geometry")

+ 352 - 0
flatcamTools/ToolMeasurement.py

@@ -0,0 +1,352 @@
+from FlatCAMTool import FlatCAMTool
+from FlatCAMObj import *
+from VisPyVisuals import *
+
+from copy import copy
+from math import sqrt
+
+class Measurement(FlatCAMTool):
+
+    toolName = "Measurement Tool"
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+
+        self.units = self.app.general_options_form.general_group.units_radio.get_value().lower()
+
+        ## Title
+        title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
+        self.layout.addWidget(title_label)
+
+        ## Form Layout
+        form_layout = QtWidgets.QFormLayout()
+        self.layout.addLayout(form_layout)
+
+        form_layout_child_1 = QtWidgets.QFormLayout()
+        form_layout_child_1_1 = QtWidgets.QFormLayout()
+        form_layout_child_1_2 = QtWidgets.QFormLayout()
+        form_layout_child_2 = QtWidgets.QFormLayout()
+        form_layout_child_3 = QtWidgets.QFormLayout()
+
+        self.start_label = QtWidgets.QLabel("<b>Start</b> Coords:")
+        self.start_label.setToolTip("This is measuring Start point coordinates.")
+
+        self.stop_label = QtWidgets.QLabel("<b>Stop</b> Coords:")
+        self.stop_label.setToolTip("This is the measuring Stop point coordinates.")
+
+        self.distance_x_label = QtWidgets.QLabel("Dx:")
+        self.distance_x_label.setToolTip("This is the distance measured over the X axis.")
+
+        self.distance_y_label = QtWidgets.QLabel("Dy:")
+        self.distance_y_label.setToolTip("This is the distance measured over the Y axis.")
+
+        self.total_distance_label = QtWidgets.QLabel("<b>DISTANCE:</b>")
+        self.total_distance_label.setToolTip("This is the point to point Euclidian distance.")
+
+        self.units_entry_1 = FCEntry()
+        self.units_entry_1.setToolTip("Those are the units in which the distance is measured.")
+        self.units_entry_1.setDisabled(True)
+        self.units_entry_1.setFocusPolicy(QtCore.Qt.NoFocus)
+        self.units_entry_1.setFrame(False)
+        self.units_entry_1.setFixedWidth(30)
+
+        self.units_entry_2 = FCEntry()
+        self.units_entry_2.setToolTip("Those are the units in which the distance is measured.")
+        self.units_entry_2.setDisabled(True)
+        self.units_entry_2.setFocusPolicy(QtCore.Qt.NoFocus)
+        self.units_entry_2.setFrame(False)
+        self.units_entry_2.setFixedWidth(30)
+
+        self.units_entry_3 = FCEntry()
+        self.units_entry_3.setToolTip("Those are the units in which the distance is measured.")
+        self.units_entry_3.setDisabled(True)
+        self.units_entry_3.setFocusPolicy(QtCore.Qt.NoFocus)
+        self.units_entry_3.setFrame(False)
+        self.units_entry_3.setFixedWidth(30)
+
+        self.units_entry_4 = FCEntry()
+        self.units_entry_4.setToolTip("Those are the units in which the distance is measured.")
+        self.units_entry_4.setDisabled(True)
+        self.units_entry_4.setFocusPolicy(QtCore.Qt.NoFocus)
+        self.units_entry_4.setFrame(False)
+        self.units_entry_4.setFixedWidth(30)
+
+        self.units_entry_5 = FCEntry()
+        self.units_entry_5.setToolTip("Those are the units in which the distance is measured.")
+        self.units_entry_5.setDisabled(True)
+        self.units_entry_5.setFocusPolicy(QtCore.Qt.NoFocus)
+        self.units_entry_5.setFrame(False)
+        self.units_entry_5.setFixedWidth(30)
+
+        self.start_entry = FCEntry()
+        self.start_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.start_entry.setToolTip("This is measuring Start point coordinates.")
+        self.start_entry.setFixedWidth(100)
+
+        self.stop_entry = FCEntry()
+        self.stop_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.stop_entry.setToolTip("This is the measuring Stop point coordinates.")
+        self.stop_entry.setFixedWidth(100)
+
+        self.distance_x_entry = FCEntry()
+        self.distance_x_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.distance_x_entry.setToolTip("This is the distance measured over the X axis.")
+        self.distance_x_entry.setFixedWidth(100)
+
+
+        self.distance_y_entry = FCEntry()
+        self.distance_y_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.distance_y_entry.setToolTip("This is the distance measured over the Y axis.")
+        self.distance_y_entry.setFixedWidth(100)
+
+
+        self.total_distance_entry = FCEntry()
+        self.total_distance_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.total_distance_entry.setToolTip("This is the point to point Euclidian distance.")
+        self.total_distance_entry.setFixedWidth(100)
+
+        self.measure_btn = QtWidgets.QPushButton("Measure")
+        self.measure_btn.setFixedWidth(70)
+        self.layout.addWidget(self.measure_btn)
+
+
+        form_layout_child_1.addRow(self.start_entry, self.units_entry_1)
+        form_layout_child_1_1.addRow(self.stop_entry, self.units_entry_2)
+        form_layout_child_1_2.addRow(self.distance_x_entry, self.units_entry_3)
+        form_layout_child_2.addRow(self.distance_y_entry, self.units_entry_4)
+        form_layout_child_3.addRow(self.total_distance_entry, self.units_entry_5)
+
+        form_layout.addRow(self.start_label, form_layout_child_1)
+        form_layout.addRow(self.stop_label, form_layout_child_1_1)
+        form_layout.addRow(self.distance_x_label, form_layout_child_1_2)
+        form_layout.addRow(self.distance_y_label, form_layout_child_2)
+        form_layout.addRow(self.total_distance_label, form_layout_child_3)
+
+        # initial view of the layout
+        self.start_entry.set_value('(0, 0)')
+        self.stop_entry.set_value('(0, 0)')
+        self.distance_x_entry.set_value('0')
+        self.distance_y_entry.set_value('0')
+        self.total_distance_entry.set_value('0')
+        self.units_entry_1.set_value(str(self.units))
+        self.units_entry_2.set_value(str(self.units))
+        self.units_entry_3.set_value(str(self.units))
+        self.units_entry_4.set_value(str(self.units))
+        self.units_entry_5.set_value(str(self.units))
+
+
+        self.layout.addStretch()
+
+        self.clicked_meas = 0
+
+        self.point1 = None
+        self.point2 = None
+
+        # the default state is disabled for the Move command
+        # self.setVisible(False)
+        self.active = False
+
+        # VisPy visuals
+        self.sel_shapes = ShapeCollection(parent=self.app.plotcanvas.vispy_canvas.view.scene, layers=1)
+
+        self.measure_btn.clicked.connect(self.toggle)
+
+    def run(self):
+        if self.app.tool_tab_locked is True:
+            return
+
+        self.toggle()
+
+        # Remove anything else in the GUI
+        self.app.ui.tool_scroll_area.takeWidget()
+
+        # Put ourself in the GUI
+        self.app.ui.tool_scroll_area.setWidget(self)
+
+        # Switch notebook to tool page
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+        self.units = self.app.general_options_form.general_group.units_radio.get_value().lower()
+        self.show()
+        self.app.ui.notebook.setTabText(2, "Meas. Tool")
+
+    def on_key_release_meas(self, event):
+        if event.key == 'escape':
+            # abort the measurement action
+            self.toggle()
+            return
+
+        if event.key == 'G':
+            # toggle grid status
+            self.app.ui.grid_snap_btn.trigger()
+            return
+
+    def toggle(self):
+        # the self.active var is doing the 'toggle'
+        if self.active is True:
+            # DISABLE the Measuring TOOL
+            self.active = False
+            # disconnect the mouse/key events from functions of measurement tool
+            self.app.plotcanvas.vis_disconnect('mouse_move', self.on_mouse_move_meas)
+            self.app.plotcanvas.vis_disconnect('mouse_press', self.on_click_meas)
+            self.app.plotcanvas.vis_disconnect('key_release', self.on_key_release_meas)
+
+            # reconnect the mouse/key events to the functions from where the tool was called
+            if self.app.call_source == 'app':
+                self.app.plotcanvas.vis_connect('mouse_move', self.app.on_mouse_move_over_plot)
+                self.app.plotcanvas.vis_connect('mouse_press', self.app.on_mouse_click_over_plot)
+                self.app.plotcanvas.vis_connect('key_press', self.app.on_key_over_plot)
+                self.app.plotcanvas.vis_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
+            elif self.app.call_source == 'geo_editor':
+                self.app.geo_editor.canvas.vis_connect('mouse_move', self.app.geo_editor.on_canvas_move)
+                self.app.geo_editor.canvas.vis_connect('mouse_press', self.app.geo_editor.on_canvas_click)
+                self.app.geo_editor.canvas.vis_connect('key_press', self.app.geo_editor.on_canvas_key)
+                self.app.geo_editor.canvas.vis_connect('key_release', self.app.geo_editor.on_canvas_key_release)
+                self.app.geo_editor.canvas.vis_connect('mouse_release', self.app.geo_editor.on_canvas_click_release)
+            elif self.app.call_source == 'exc_editor':
+                self.app.exc_editor.canvas.vis_connect('mouse_move', self.app.exc_editor.on_canvas_move)
+                self.app.exc_editor.canvas.vis_connect('mouse_press', self.app.exc_editor.on_canvas_click)
+                self.app.exc_editor.canvas.vis_connect('key_press', self.app.exc_editor.on_canvas_key)
+                self.app.exc_editor.canvas.vis_connect('key_release', self.app.exc_editor.on_canvas_key_release)
+                self.app.exc_editor.canvas.vis_connect('mouse_release', self.app.exc_editor.on_canvas_click_release)
+
+            self.clicked_meas = 0
+            self.app.command_active = None
+            # delete the measuring line
+            self.delete_shape()
+            return
+        else:
+            # ENABLE the Measuring TOOL
+            self.active = True
+            self.units = self.app.general_options_form.general_group.units_radio.get_value().lower()
+
+            # we disconnect the mouse/key handlers from wherever the measurement tool was called
+            if self.app.call_source == 'app':
+                self.app.plotcanvas.vis_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
+                self.app.plotcanvas.vis_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
+                self.app.plotcanvas.vis_disconnect('key_press', self.app.on_key_over_plot)
+                self.app.plotcanvas.vis_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
+            elif self.app.call_source == 'geo_editor':
+                self.app.geo_editor.canvas.vis_disconnect('mouse_move', self.app.geo_editor.on_canvas_move)
+                self.app.geo_editor.canvas.vis_disconnect('mouse_press', self.app.geo_editor.on_canvas_click)
+                self.app.geo_editor.canvas.vis_disconnect('key_press', self.app.geo_editor.on_canvas_key)
+                self.app.geo_editor.canvas.vis_disconnect('key_release', self.app.geo_editor.on_canvas_key_release)
+                self.app.geo_editor.canvas.vis_disconnect('mouse_release', self.app.geo_editor.on_canvas_click_release)
+            elif self.app.call_source == 'exc_editor':
+                self.app.exc_editor.canvas.vis_disconnect('mouse_move', self.app.exc_editor.on_canvas_move)
+                self.app.exc_editor.canvas.vis_disconnect('mouse_press', self.app.exc_editor.on_canvas_click)
+                self.app.exc_editor.canvas.vis_disconnect('key_press', self.app.exc_editor.on_canvas_key)
+                self.app.exc_editor.canvas.vis_disconnect('key_release', self.app.exc_editor.on_canvas_key_release)
+                self.app.exc_editor.canvas.vis_disconnect('mouse_release', self.app.exc_editor.on_canvas_click_release)
+
+            # we can safely connect the app mouse events to the measurement tool
+            self.app.plotcanvas.vis_connect('mouse_move', self.on_mouse_move_meas)
+            self.app.plotcanvas.vis_connect('mouse_press', self.on_click_meas)
+            self.app.plotcanvas.vis_connect('key_release', self.on_key_release_meas)
+
+            self.app.command_active = "Measurement"
+
+            # initial view of the layout
+            self.start_entry.set_value('(0, 0)')
+            self.stop_entry.set_value('(0, 0)')
+
+            self.distance_x_entry.set_value('0')
+            self.distance_y_entry.set_value('0')
+            self.total_distance_entry.set_value('0')
+
+            self.units_entry_1.set_value(str(self.units))
+            self.units_entry_2.set_value(str(self.units))
+            self.units_entry_3.set_value(str(self.units))
+            self.units_entry_4.set_value(str(self.units))
+            self.units_entry_5.set_value(str(self.units))
+
+            self.app.inform.emit("MEASURING: Click on the Start point ...")
+
+    def on_click_meas(self, event):
+        # mouse click will be accepted only if the left button is clicked
+        # this is necessary because right mouse click and middle mouse click
+        # are used for panning on the canvas
+
+        if event.button == 1:
+            if self.clicked_meas == 0:
+                pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
+
+                # if GRID is active we need to get the snapped positions
+                if self.app.grid_status() == True:
+                    pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
+                else:
+                    pos = pos_canvas[0], pos_canvas[1]
+                self.point1 = pos
+                self.start_entry.set_value("(%.4f, %.4f)" % pos)
+                self.app.inform.emit("MEASURING: Click on the Destination point ...")
+
+            if self.clicked_meas == 1:
+                try:
+                    pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
+
+                    # delete the selection bounding box
+                    self.delete_shape()
+
+                    # if GRID is active we need to get the snapped positions
+                    if self.app.grid_status() == True:
+                        pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
+                    else:
+                        pos = pos_canvas[0], pos_canvas[1]
+
+                    dx = pos[0] - self.point1[0]
+                    dy = pos[1] - self.point1[1]
+                    d = sqrt(dx**2 + dy**2)
+
+                    self.stop_entry.set_value("(%.4f, %.4f)" % pos)
+
+                    self.app.inform.emit("MEASURING: Result D(x) = %.4f | D(y) = %.4f | Distance = %.4f" %
+                                         (abs(dx), abs(dy), abs(d)))
+
+                    self.distance_x_entry.set_value('%.4f' % abs(dx))
+                    self.distance_y_entry.set_value('%.4f' % abs(dy))
+                    self.total_distance_entry.set_value('%.4f' % abs(d))
+
+                    self.clicked_meas = 0
+                    self.toggle()
+
+                    # delete the measuring line
+                    self.delete_shape()
+                    return
+                except TypeError:
+                    pass
+
+            self.clicked_meas = 1
+
+    def on_mouse_move_meas(self, event):
+        pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
+
+        # if GRID is active we need to get the snapped positions
+        if self.app.grid_status() == True:
+            pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
+            self.app.app_cursor.enabled = True
+            # Update cursor
+            self.app.app_cursor.set_data(np.asarray([(pos[0], pos[1])]), symbol='++', edge_color='black', size=20)
+        else:
+            pos = pos_canvas
+            self.app.app_enabled = False
+
+        self.point2 = (pos[0], pos[1])
+
+        if self.clicked_meas == 1:
+            self.update_meas_shape([self.point2, self.point1])
+
+    def update_meas_shape(self, pos):
+        self.delete_shape()
+        self.draw_shape(pos)
+
+    def delete_shape(self):
+        self.sel_shapes.clear()
+        self.sel_shapes.redraw()
+
+    def draw_shape(self, coords):
+        self.meas_line = LineString(coords)
+        self.sel_shapes.add(self.meas_line, color='black', update=True, layer=0, tolerance=None)
+
+    def set_meas_units(self, units):
+        self.meas.units_label.setText("[" + self.app.options["units"].lower() + "]")
+
+# end of file

+ 238 - 0
flatcamTools/ToolMove.py

@@ -0,0 +1,238 @@
+from FlatCAMTool import FlatCAMTool
+from FlatCAMObj import *
+from VisPyVisuals import *
+
+from io import StringIO
+from copy import copy
+
+
+class ToolMove(FlatCAMTool):
+
+    toolName = "Move"
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+
+        self.layout.setContentsMargins(0, 0, 3, 0)
+        self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Maximum)
+
+        self.clicked_move = 0
+
+        self.point1 = None
+        self.point2 = None
+
+        # the default state is disabled for the Move command
+        self.setVisible(False)
+
+        self.sel_rect = None
+        self.old_coords = []
+
+        # VisPy visuals
+        self.sel_shapes = ShapeCollection(parent=self.app.plotcanvas.vispy_canvas.view.scene, layers=1)
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, **kwargs)
+
+    def run(self):
+        if self.app.tool_tab_locked is True:
+            return
+        self.toggle()
+
+    def on_left_click(self, event):
+        # mouse click will be accepted only if the left button is clicked
+        # this is necessary because right mouse click and middle mouse click
+        # are used for panning on the canvas
+
+        if event.button == 1:
+            if self.clicked_move == 0:
+                pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
+
+                # if GRID is active we need to get the snapped positions
+                if self.app.grid_status() == True:
+                    pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
+                else:
+                    pos = pos_canvas
+
+                if self.point1 is None:
+                    self.point1 = pos
+                else:
+                    self.point2 = copy(self.point1)
+                    self.point1 = pos
+                self.app.inform.emit("MOVE: Click on the Destination point ...")
+
+            if self.clicked_move == 1:
+                try:
+                    pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
+
+                    # delete the selection bounding box
+                    self.delete_shape()
+
+                    # if GRID is active we need to get the snapped positions
+                    if self.app.grid_status() == True:
+                        pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
+                    else:
+                        pos = pos_canvas
+
+                    dx = pos[0] - self.point1[0]
+                    dy = pos[1] - self.point1[1]
+
+                    proc = self.app.proc_container.new("Moving ...")
+
+                    def job_move(app_obj):
+                        obj_list = self.app.collection.get_selected()
+
+                        try:
+                            if not obj_list:
+                                self.app.inform.emit("[warning_notcl] No object(s) selected.")
+                                return "fail"
+                            else:
+                                for sel_obj in obj_list:
+
+                                    sel_obj.offset((dx, dy))
+                                    sel_obj.plot()
+                                    # Update the object bounding box options
+                                    a,b,c,d = sel_obj.bounds()
+                                    sel_obj.options['xmin'] = a
+                                    sel_obj.options['ymin'] = b
+                                    sel_obj.options['xmax'] = c
+                                    sel_obj.options['ymax'] = d
+                                    # self.app.collection.set_active(sel_obj.options['name'])
+                        except Exception as e:
+                            proc.done()
+                            self.app.inform.emit('[error_notcl] '
+                                                 'ToolMove.on_left_click() --> %s' % str(e))
+                            return "fail"
+                        proc.done()
+                        # delete the selection bounding box
+                        self.delete_shape()
+
+                    self.app.worker_task.emit({'fcn': job_move, 'params': [self]})
+
+                    self.clicked_move = 0
+                    self.toggle()
+                    self.app.inform.emit("[success]Object was moved ...")
+                    return
+
+                except TypeError:
+                    self.app.inform.emit('[error_notcl] '
+                                         'ToolMove.on_left_click() --> Error when mouse left click.')
+                    return
+
+            self.clicked_move = 1
+
+    def on_move(self, event):
+        pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
+
+        # if GRID is active we need to get the snapped positions
+        if self.app.grid_status() == True:
+            pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
+        else:
+            pos = pos_canvas
+
+        if self.point1 is None:
+            dx = pos[0]
+            dy = pos[1]
+        else:
+            dx = pos[0] - self.point1[0]
+            dy = pos[1] - self.point1[1]
+
+        if self.clicked_move == 1:
+            self.update_sel_bbox((dx, dy))
+
+    def on_key_press(self, event):
+        if event.key == 'escape':
+            # abort the move action
+            self.app.inform.emit("[warning_notcl]Move action cancelled.")
+            self.toggle()
+        return
+
+    def toggle(self):
+        if self.isVisible():
+            self.setVisible(False)
+
+            self.app.plotcanvas.vis_disconnect('mouse_move', self.on_move)
+            self.app.plotcanvas.vis_disconnect('mouse_press', self.on_left_click)
+            self.app.plotcanvas.vis_disconnect('key_release', self.on_key_press)
+            self.app.plotcanvas.vis_connect('key_press', self.app.on_key_over_plot)
+
+            self.clicked_move = 0
+
+            # signal that there is no command active
+            self.app.command_active = None
+
+            # delete the selection box
+            self.delete_shape()
+            return
+        else:
+            self.setVisible(True)
+            # signal that there is a command active and it is 'Move'
+            self.app.command_active = "Move"
+
+            if self.app.collection.get_selected():
+                self.app.inform.emit("MOVE: Click on the Start point ...")
+                # draw the selection box
+                self.draw_sel_bbox()
+            else:
+                self.setVisible(False)
+                # signal that there is no command active
+                self.app.command_active = None
+                self.app.inform.emit("[warning_notcl]MOVE action cancelled. No object(s) to move.")
+
+    def draw_sel_bbox(self):
+        xminlist = []
+        yminlist = []
+        xmaxlist = []
+        ymaxlist = []
+
+        obj_list = self.app.collection.get_selected()
+        if not obj_list:
+            self.app.inform.emit("[warning_notcl]Object(s) not selected")
+            self.toggle()
+        else:
+            # if we have an object selected then we can safely activate the mouse events
+            self.app.plotcanvas.vis_connect('mouse_move', self.on_move)
+            self.app.plotcanvas.vis_connect('mouse_press', self.on_left_click)
+            self.app.plotcanvas.vis_connect('key_release', self.on_key_press)
+            # first get a bounding box to fit all
+            for obj in obj_list:
+                xmin, ymin, xmax, ymax = obj.bounds()
+                xminlist.append(xmin)
+                yminlist.append(ymin)
+                xmaxlist.append(xmax)
+                ymaxlist.append(ymax)
+
+            # get the minimum x,y and maximum x,y for all objects selected
+            xminimal = min(xminlist)
+            yminimal = min(yminlist)
+            xmaximal = max(xmaxlist)
+            ymaximal = max(ymaxlist)
+
+            p1 = (xminimal, yminimal)
+            p2 = (xmaximal, yminimal)
+            p3 = (xmaximal, ymaximal)
+            p4 = (xminimal, ymaximal)
+            self.old_coords = [p1, p2, p3, p4]
+            self.draw_shape(self.old_coords)
+
+    def update_sel_bbox(self, pos):
+        self.delete_shape()
+
+        pt1 = (self.old_coords[0][0] + pos[0], self.old_coords[0][1] + pos[1])
+        pt2 = (self.old_coords[1][0] + pos[0], self.old_coords[1][1] + pos[1])
+        pt3 = (self.old_coords[2][0] + pos[0], self.old_coords[2][1] + pos[1])
+        pt4 = (self.old_coords[3][0] + pos[0], self.old_coords[3][1] + pos[1])
+
+        self.draw_shape([pt1, pt2, pt3, pt4])
+
+    def delete_shape(self):
+        self.sel_shapes.clear()
+        self.sel_shapes.redraw()
+
+    def draw_shape(self, coords):
+        self.sel_rect = Polygon(coords)
+
+        blue_t = Color('blue')
+        blue_t.alpha = 0.2
+        self.sel_shapes.add(self.sel_rect, color='blue', face_color=blue_t, update=True, layer=0, tolerance=None)
+
+# end of file

+ 882 - 0
flatcamTools/ToolNonCopperClear.py

@@ -0,0 +1,882 @@
+from FlatCAMTool import FlatCAMTool
+from copy import copy,deepcopy
+# from GUIElements import IntEntry, RadioSet, FCEntry
+# from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber
+from ObjectCollection import *
+import time
+
+class NonCopperClear(FlatCAMTool, Gerber):
+
+    toolName = "Non-Copper Clearing Tool"
+
+    def __init__(self, app):
+        self.app = app
+
+        FlatCAMTool.__init__(self, app)
+        Gerber.__init__(self, steps_per_circle=self.app.defaults["gerber_circle_steps"])
+
+        self.tools_frame = QtWidgets.QFrame()
+        self.tools_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.tools_frame)
+        self.tools_box = QtWidgets.QVBoxLayout()
+        self.tools_box.setContentsMargins(0, 0, 0, 0)
+        self.tools_frame.setLayout(self.tools_box)
+
+        ## Title
+        title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
+        self.tools_box.addWidget(title_label)
+
+        ## Form Layout
+        form_layout = QtWidgets.QFormLayout()
+        self.tools_box.addLayout(form_layout)
+
+        ## Object
+        self.object_combo = QtWidgets.QComboBox()
+        self.object_combo.setModel(self.app.collection)
+        self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.object_combo.setCurrentIndex(1)
+        self.object_label = QtWidgets.QLabel("Gerber:")
+        self.object_label.setToolTip(
+            "Gerber object to be cleared of excess copper.                        "
+        )
+        e_lab_0 = QtWidgets.QLabel('')
+
+        form_layout.addRow(self.object_label, self.object_combo)
+        form_layout.addRow(e_lab_0)
+
+        #### Tools ####
+        self.tools_table_label = QtWidgets.QLabel('<b>Tools Table</b>')
+        self.tools_table_label.setToolTip(
+            "Tools pool from which the algorithm\n"
+            "will pick the ones used for copper clearing."
+        )
+        self.tools_box.addWidget(self.tools_table_label)
+
+        self.tools_table = FCTable()
+        self.tools_box.addWidget(self.tools_table)
+
+        self.tools_table.setColumnCount(4)
+        self.tools_table.setHorizontalHeaderLabels(['#', 'Diameter', 'TT', ''])
+        self.tools_table.setColumnHidden(3, True)
+        self.tools_table.setSortingEnabled(False)
+        # self.tools_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+
+        self.tools_table.horizontalHeaderItem(0).setToolTip(
+            "This is the Tool Number.\n"
+            "Non copper clearing will start with the tool with the biggest \n"
+            "diameter, continuing until there are no more tools.\n"
+            "Only tools that create NCC clearing geometry will still be present\n"
+            "in the resulting geometry. This is because with some tools\n"
+            "this function will not be able to create painting geometry."
+            )
+        self.tools_table.horizontalHeaderItem(1).setToolTip(
+            "Tool Diameter. It's value (in current FlatCAM units) \n"
+            "is the cut width into the material.")
+
+        self.tools_table.horizontalHeaderItem(2).setToolTip(
+            "The Tool Type (TT) can be:<BR>"
+            "- <B>Circular</B> with 1 ... 4 teeth -> it is informative only. Being circular, <BR>"
+            "the cut width in material is exactly the tool diameter.<BR>"
+            "- <B>Ball</B> -> informative only and make reference to the Ball type endmill.<BR>"
+            "- <B>V-Shape</B> -> it will disable de Z-Cut parameter in the resulting geometry UI form "
+            "and enable two additional UI form fields in the resulting geometry: V-Tip Dia and "
+            "V-Tip Angle. Adjusting those two values will adjust the Z-Cut parameter such "
+            "as the cut width into material will be equal with the value in the Tool Diameter "
+            "column of this table.<BR>"
+            "Choosing the <B>V-Shape</B> Tool Type automatically will select the Operation Type "
+            "in the resulting geometry as Isolation.")
+
+        self.empty_label = QtWidgets.QLabel('')
+        self.tools_box.addWidget(self.empty_label)
+
+        #### Add a new Tool ####
+        hlay = QtWidgets.QHBoxLayout()
+        self.tools_box.addLayout(hlay)
+
+        self.addtool_entry_lbl = QtWidgets.QLabel('<b>Tool Dia:</b>')
+        self.addtool_entry_lbl.setToolTip(
+            "Diameter for the new tool to add in the Tool Table"
+        )
+        self.addtool_entry = FloatEntry()
+
+        # hlay.addWidget(self.addtool_label)
+        # hlay.addStretch()
+        hlay.addWidget(self.addtool_entry_lbl)
+        hlay.addWidget(self.addtool_entry)
+
+        grid2 = QtWidgets.QGridLayout()
+        self.tools_box.addLayout(grid2)
+
+        self.addtool_btn = QtWidgets.QPushButton('Add')
+        self.addtool_btn.setToolTip(
+            "Add a new tool to the Tool Table\n"
+            "with the diameter specified above."
+        )
+
+        # self.copytool_btn = QtWidgets.QPushButton('Copy')
+        # self.copytool_btn.setToolTip(
+        #     "Copy a selection of tools in the Tool Table\n"
+        #     "by first selecting a row in the Tool Table."
+        # )
+
+        self.deltool_btn = QtWidgets.QPushButton('Delete')
+        self.deltool_btn.setToolTip(
+            "Delete a selection of tools in the Tool Table\n"
+            "by first selecting a row(s) in the Tool Table."
+        )
+
+        grid2.addWidget(self.addtool_btn, 0, 0)
+        # grid2.addWidget(self.copytool_btn, 0, 1)
+        grid2.addWidget(self.deltool_btn, 0,2)
+
+        self.empty_label_0 = QtWidgets.QLabel('')
+        self.tools_box.addWidget(self.empty_label_0)
+
+        grid3 = QtWidgets.QGridLayout()
+        self.tools_box.addLayout(grid3)
+
+        e_lab_1 = QtWidgets.QLabel('')
+        grid3.addWidget(e_lab_1, 0, 0)
+
+        nccoverlabel = QtWidgets.QLabel('Overlap:')
+        nccoverlabel.setToolTip(
+            "How much (fraction) of the tool width to overlap each tool pass.\n"
+            "Example:\n"
+            "A value here of 0.25 means 25% from the tool diameter found above.\n\n"
+            "Adjust the value starting with lower values\n"
+            "and increasing it if areas that should be cleared are still \n"
+            "not cleared.\n"
+            "Lower values = faster processing, faster execution on PCB.\n"
+            "Higher values = slow processing and slow execution on CNC\n"
+            "due of too many paths."
+        )
+        grid3.addWidget(nccoverlabel, 1, 0)
+        self.ncc_overlap_entry = FloatEntry()
+        grid3.addWidget(self.ncc_overlap_entry, 1, 1)
+
+        nccmarginlabel = QtWidgets.QLabel('Margin:')
+        nccmarginlabel.setToolTip(
+            "Bounding box margin."
+        )
+        grid3.addWidget(nccmarginlabel, 2, 0)
+        self.ncc_margin_entry = FloatEntry()
+        grid3.addWidget(self.ncc_margin_entry, 2, 1)
+
+        # Method
+        methodlabel = QtWidgets.QLabel('Method:')
+        methodlabel.setToolTip(
+            "Algorithm for non-copper clearing:<BR>"
+            "<B>Standard</B>: Fixed step inwards.<BR>"
+            "<B>Seed-based</B>: Outwards from seed.<BR>"
+            "<B>Line-based</B>: Parallel lines."
+        )
+        grid3.addWidget(methodlabel, 3, 0)
+        self.ncc_method_radio = RadioSet([
+            {"label": "Standard", "value": "standard"},
+            {"label": "Seed-based", "value": "seed"},
+            {"label": "Straight lines", "value": "lines"}
+        ], orientation='vertical', stretch=False)
+        grid3.addWidget(self.ncc_method_radio, 3, 1)
+
+        # Connect lines
+        pathconnectlabel = QtWidgets.QLabel("Connect:")
+        pathconnectlabel.setToolTip(
+            "Draw lines between resulting\n"
+            "segments to minimize tool lifts."
+        )
+        grid3.addWidget(pathconnectlabel, 4, 0)
+        self.ncc_connect_cb = FCCheckBox()
+        grid3.addWidget(self.ncc_connect_cb, 4, 1)
+
+        contourlabel = QtWidgets.QLabel("Contour:")
+        contourlabel.setToolTip(
+            "Cut around the perimeter of the polygon\n"
+            "to trim rough edges."
+        )
+        grid3.addWidget(contourlabel, 5, 0)
+        self.ncc_contour_cb = FCCheckBox()
+        grid3.addWidget(self.ncc_contour_cb, 5, 1)
+
+        restlabel = QtWidgets.QLabel("Rest M.:")
+        restlabel.setToolTip(
+            "If checked, use 'rest machining'.\n"
+            "Basically it will clear copper outside PCB features,\n"
+            "using the biggest tool and continue with the next tools,\n"
+            "from bigger to smaller, to clear areas of copper that\n"
+            "could not be cleared by previous tool, until there is\n"
+            "no more copper to clear or there are no more tools.\n"
+            "If not checked, use the standard algorithm."
+        )
+        grid3.addWidget(restlabel, 6, 0)
+        self.ncc_rest_cb = FCCheckBox()
+        grid3.addWidget(self.ncc_rest_cb, 6, 1)
+
+        self.generate_ncc_button = QtWidgets.QPushButton('Generate Geometry')
+        self.generate_ncc_button.setToolTip(
+            "Create the Geometry Object\n"
+            "for non-copper routing."
+        )
+        self.tools_box.addWidget(self.generate_ncc_button)
+
+        self.units = ''
+        self.ncc_tools = {}
+        self.tooluid = 0
+        # store here the default data for Geometry Data
+        self.default_data = {}
+
+        self.obj_name = ""
+        self.ncc_obj = None
+
+        self.tools_box.addStretch()
+
+        self.addtool_btn.clicked.connect(self.on_tool_add)
+        self.deltool_btn.clicked.connect(self.on_tool_delete)
+        self.generate_ncc_button.clicked.connect(self.on_ncc)
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, **kwargs)
+
+    def run(self):
+        FlatCAMTool.run(self)
+        self.tools_frame.show()
+        self.set_ui()
+        self.build_ui()
+        self.app.ui.notebook.setTabText(2, "NCC Tool")
+
+    def set_ui(self):
+        self.ncc_overlap_entry.set_value(self.app.defaults["gerber_nccoverlap"])
+        self.ncc_margin_entry.set_value(self.app.defaults["gerber_nccmargin"])
+        self.ncc_method_radio.set_value(self.app.defaults["gerber_nccmethod"])
+        self.ncc_connect_cb.set_value(self.app.defaults["gerber_nccconnect"])
+        self.ncc_contour_cb.set_value(self.app.defaults["gerber_ncccontour"])
+        self.ncc_rest_cb.set_value(self.app.defaults["gerber_nccrest"])
+
+        self.tools_table.setupContextMenu()
+        self.tools_table.addContextMenu(
+            "Add", lambda: self.on_tool_add(dia=None, muted=None), icon=QtGui.QIcon("share/plus16.png"))
+        self.tools_table.addContextMenu(
+            "Delete", lambda:
+            self.on_tool_delete(rows_to_delete=None, all=None), icon=QtGui.QIcon("share/delete32.png"))
+
+        # init the working variables
+        self.default_data.clear()
+        self.default_data.update({
+            "name": '_ncc',
+            "plot": self.app.defaults["geometry_plot"],
+            "tooldia": self.app.defaults["geometry_painttooldia"],
+            "cutz": self.app.defaults["geometry_cutz"],
+            "vtipdia": 0.1,
+            "vtipangle": 30,
+            "travelz": self.app.defaults["geometry_travelz"],
+            "feedrate": self.app.defaults["geometry_feedrate"],
+            "feedrate_z": self.app.defaults["geometry_feedrate_z"],
+            "feedrate_rapid": self.app.defaults["geometry_feedrate_rapid"],
+            "dwell": self.app.defaults["geometry_dwell"],
+            "dwelltime": self.app.defaults["geometry_dwelltime"],
+            "multidepth": self.app.defaults["geometry_multidepth"],
+            "ppname_g": self.app.defaults["geometry_ppname_g"],
+            "depthperpass": self.app.defaults["geometry_depthperpass"],
+            "extracut": self.app.defaults["geometry_extracut"],
+            "toolchange": self.app.defaults["geometry_toolchange"],
+            "toolchangez": self.app.defaults["geometry_toolchangez"],
+            "endz": self.app.defaults["geometry_endz"],
+            "spindlespeed": self.app.defaults["geometry_spindlespeed"],
+            "toolchangexy": self.app.defaults["geometry_toolchangexy"],
+            "startz": self.app.defaults["geometry_startz"],
+            "paintmargin": self.app.defaults["geometry_paintmargin"],
+            "paintmethod": self.app.defaults["geometry_paintmethod"],
+            "selectmethod": self.app.defaults["geometry_selectmethod"],
+            "pathconnect": self.app.defaults["geometry_pathconnect"],
+            "paintcontour": self.app.defaults["geometry_paintcontour"],
+            "paintoverlap": self.app.defaults["geometry_paintoverlap"],
+            "nccoverlap": self.app.defaults["gerber_nccoverlap"],
+            "nccmargin": self.app.defaults["gerber_nccmargin"],
+            "nccmethod": self.app.defaults["gerber_nccmethod"],
+            "nccconnect": self.app.defaults["gerber_nccconnect"],
+            "ncccontour": self.app.defaults["gerber_ncccontour"],
+            "nccrest": self.app.defaults["gerber_nccrest"]
+        })
+
+        try:
+            dias = [float(eval(dia)) for dia in self.app.defaults["gerber_ncctools"].split(",")]
+        except:
+            log.error("At least one tool diameter needed. Verify in Edit -> Preferences -> Gerber Object -> NCC Tools.")
+            return
+
+        self.tooluid = 0
+
+        self.ncc_tools.clear()
+        for tool_dia in dias:
+            self.tooluid += 1
+            self.ncc_tools.update({
+                int(self.tooluid): {
+                    'tooldia': float('%.4f' % tool_dia),
+                    'offset': 'Path',
+                    'offset_value': 0.0,
+                    'type': 'Iso',
+                    'tool_type': 'V',
+                    'data': dict(self.default_data),
+                    'solid_geometry': []
+                }
+            })
+        self.obj_name = ""
+        self.ncc_obj = None
+        self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
+        self.units = self.app.general_options_form.general_group.units_radio.get_value().upper()
+
+    def build_ui(self):
+        self.ui_disconnect()
+
+        # updated units
+        self.units = self.app.general_options_form.general_group.units_radio.get_value().upper()
+
+        if self.units == "IN":
+            self.addtool_entry.set_value(0.039)
+        else:
+            self.addtool_entry.set_value(1)
+
+        sorted_tools = []
+        for k, v in self.ncc_tools.items():
+            sorted_tools.append(float('%.4f' % float(v['tooldia'])))
+        sorted_tools.sort()
+
+        n = len(sorted_tools)
+        self.tools_table.setRowCount(n)
+        tool_id = 0
+
+        for tool_sorted in sorted_tools:
+            for tooluid_key, tooluid_value in self.ncc_tools.items():
+                if float('%.4f' % tooluid_value['tooldia']) == tool_sorted:
+                    tool_id += 1
+                    id = QtWidgets.QTableWidgetItem('%d' % int(tool_id))
+                    id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+                    row_no = tool_id - 1
+                    self.tools_table.setItem(row_no, 0, id)  # Tool name/id
+
+                    # Make sure that the drill diameter when in MM is with no more than 2 decimals
+                    # There are no drill bits in MM with more than 3 decimals diameter
+                    # For INCH the decimals should be no more than 3. There are no drills under 10mils
+                    if self.units == 'MM':
+                        dia = QtWidgets.QTableWidgetItem('%.2f' % tooluid_value['tooldia'])
+                    else:
+                        dia = QtWidgets.QTableWidgetItem('%.3f' % tooluid_value['tooldia'])
+
+                    dia.setFlags(QtCore.Qt.ItemIsEnabled)
+
+                    tool_type_item = QtWidgets.QComboBox()
+                    for item in self.tool_type_item_options:
+                        tool_type_item.addItem(item)
+                        tool_type_item.setStyleSheet('background-color: rgb(255,255,255)')
+                    idx = tool_type_item.findText(tooluid_value['tool_type'])
+                    tool_type_item.setCurrentIndex(idx)
+
+                    tool_uid_item = QtWidgets.QTableWidgetItem(str(int(tooluid_key)))
+
+                    self.tools_table.setItem(row_no, 1, dia)  # Diameter
+                    self.tools_table.setCellWidget(row_no, 2, tool_type_item)
+
+                    ### REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY ###
+                    self.tools_table.setItem(row_no, 3, tool_uid_item)  # Tool unique ID
+
+        # make the diameter column editable
+        for row in range(tool_id):
+            self.tools_table.item(row, 1).setFlags(
+                QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+
+        # all the tools are selected by default
+        self.tools_table.selectColumn(0)
+        #
+        self.tools_table.resizeColumnsToContents()
+        self.tools_table.resizeRowsToContents()
+
+        vertical_header = self.tools_table.verticalHeader()
+        vertical_header.hide()
+        self.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header = self.tools_table.horizontalHeader()
+        horizontal_header.setMinimumSectionSize(10)
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(0, 20)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+
+        # self.tools_table.setSortingEnabled(True)
+        # sort by tool diameter
+        # self.tools_table.sortItems(1)
+
+        self.tools_table.setMinimumHeight(self.tools_table.getHeight())
+        self.tools_table.setMaximumHeight(self.tools_table.getHeight())
+
+        self.app.report_usage("gerber_on_ncc_button")
+        self.ui_connect()
+
+    def ui_connect(self):
+        self.tools_table.itemChanged.connect(self.on_tool_edit)
+
+    def ui_disconnect(self):
+        try:
+            # if connected, disconnect the signal from the slot on item_changed as it creates issues
+            self.tools_table.itemChanged.disconnect(self.on_tool_edit)
+        except:
+            pass
+
+    def on_tool_add(self, dia=None, muted=None):
+
+        self.ui_disconnect()
+
+        if dia:
+            tool_dia = dia
+        else:
+            tool_dia = self.addtool_entry.get_value()
+            if tool_dia is None:
+                self.build_ui()
+                self.app.inform.emit("[warning_notcl] Please enter a tool diameter to add, in Float format.")
+                return
+
+        # construct a list of all 'tooluid' in the self.tools
+        tool_uid_list = []
+        for tooluid_key in self.ncc_tools:
+            tool_uid_item = int(tooluid_key)
+            tool_uid_list.append(tool_uid_item)
+
+        # find maximum from the temp_uid, add 1 and this is the new 'tooluid'
+        if not tool_uid_list:
+            max_uid = 0
+        else:
+            max_uid = max(tool_uid_list)
+        self.tooluid = int(max_uid + 1)
+
+        tool_dias = []
+        for k, v in self.ncc_tools.items():
+            for tool_v in v.keys():
+                if tool_v == 'tooldia':
+                    tool_dias.append(float('%.4f' % v[tool_v]))
+
+        if float('%.4f' % tool_dia) in tool_dias:
+            if muted is None:
+                self.app.inform.emit("[warning_notcl]Adding tool cancelled. Tool already in Tool Table.")
+            self.tools_table.itemChanged.connect(self.on_tool_edit)
+            return
+        else:
+            if muted is None:
+                self.app.inform.emit("[success] New tool added to Tool Table.")
+            self.ncc_tools.update({
+                int(self.tooluid): {
+                    'tooldia': float('%.4f' % tool_dia),
+                    'offset': 'Path',
+                    'offset_value': 0.0,
+                    'type': 'Iso',
+                    'tool_type': 'V',
+                    'data': dict(self.default_data),
+                    'solid_geometry': []
+                }
+            })
+
+        self.build_ui()
+
+    def on_tool_edit(self):
+        self.ui_disconnect()
+
+        tool_dias = []
+        for k, v in self.ncc_tools.items():
+            for tool_v in v.keys():
+                if tool_v == 'tooldia':
+                    tool_dias.append(float('%.4f' % v[tool_v]))
+
+        for row in range(self.tools_table.rowCount()):
+            new_tool_dia = float(self.tools_table.item(row, 1).text())
+            tooluid = int(self.tools_table.item(row, 3).text())
+
+            # identify the tool that was edited and get it's tooluid
+            if new_tool_dia not in tool_dias:
+                self.ncc_tools[tooluid]['tooldia'] = new_tool_dia
+                self.app.inform.emit("[success] Tool from Tool Table was edited.")
+                self.build_ui()
+                return
+            else:
+                # identify the old tool_dia and restore the text in tool table
+                for k, v in self.ncc_tools.items():
+                    if k == tooluid:
+                        old_tool_dia = v['tooldia']
+                        break
+                restore_dia_item = self.tools_table.item(row, 1)
+                restore_dia_item.setText(str(old_tool_dia))
+                self.app.inform.emit("[warning_notcl] Edit cancelled. New diameter value is already in the Tool Table.")
+        self.build_ui()
+
+    def on_tool_delete(self, rows_to_delete=None, all=None):
+        self.ui_disconnect()
+
+        deleted_tools_list = []
+
+        if all:
+            self.paint_tools.clear()
+            self.build_ui()
+            return
+
+        if rows_to_delete:
+            try:
+                for row in rows_to_delete:
+                    tooluid_del = int(self.tools_table.item(row, 3).text())
+                    deleted_tools_list.append(tooluid_del)
+            except TypeError:
+                deleted_tools_list.append(rows_to_delete)
+
+            for t in deleted_tools_list:
+                self.ncc_tools.pop(t, None)
+            self.build_ui()
+            return
+
+        try:
+            if self.tools_table.selectedItems():
+                for row_sel in self.tools_table.selectedItems():
+                    row = row_sel.row()
+                    if row < 0:
+                        continue
+                    tooluid_del = int(self.tools_table.item(row, 3).text())
+                    deleted_tools_list.append(tooluid_del)
+
+                for t in deleted_tools_list:
+                    self.ncc_tools.pop(t, None)
+
+        except AttributeError:
+            self.app.inform.emit("[warning_notcl]Delete failed. Select a tool to delete.")
+            return
+        except Exception as e:
+            log.debug(str(e))
+
+        self.app.inform.emit("[success] Tool(s) deleted from Tool Table.")
+        self.build_ui()
+
+    def on_ncc(self):
+
+        over = self.ncc_overlap_entry.get_value()
+        over = over if over else self.app.defaults["gerber_nccoverlap"]
+
+        margin = self.ncc_margin_entry.get_value()
+        margin = margin if margin else self.app.defaults["gerber_nccmargin"]
+
+        connect = self.ncc_connect_cb.get_value()
+        connect = connect if connect else self.app.defaults["gerber_nccconnect"]
+
+        contour = self.ncc_contour_cb.get_value()
+        contour = contour if contour else self.app.defaults["gerber_ncccontour"]
+
+        clearing_method = self.ncc_rest_cb.get_value()
+        clearing_method = clearing_method if clearing_method else self.app.defaults["gerber_nccrest"]
+
+        pol_method = self.ncc_method_radio.get_value()
+        pol_method = pol_method if pol_method else self.app.defaults["gerber_nccmethod"]
+
+        self.obj_name = self.object_combo.currentText()
+        # Get source object.
+        try:
+            self.ncc_obj = self.app.collection.get_by_name(self.obj_name)
+        except:
+            self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % self.obj_name)
+            return "Could not retrieve object: %s" % self.obj_name
+
+
+        # Prepare non-copper polygons
+        try:
+            bounding_box = self.ncc_obj.solid_geometry.envelope.buffer(distance=margin, join_style=JOIN_STYLE.mitre)
+        except AttributeError:
+            self.app.inform.emit("[error_notcl]No Gerber file available.")
+            return
+
+        # calculate the empty area by substracting the solid_geometry from the object bounding box geometry
+        empty = self.ncc_obj.get_empty_area(bounding_box)
+        if type(empty) is Polygon:
+            empty = MultiPolygon([empty])
+
+        # clear non copper using standard algorithm
+        if clearing_method == False:
+            self.clear_non_copper(
+                empty=empty,
+                over=over,
+                pol_method=pol_method,
+                connect=connect,
+                contour=contour
+            )
+        # clear non copper using rest machining algorithm
+        else:
+            self.clear_non_copper_rest(
+                empty=empty,
+                over=over,
+                pol_method=pol_method,
+                connect=connect,
+                contour=contour
+            )
+
+    def clear_non_copper(self, empty, over, pol_method, outname=None, connect=True, contour=True):
+
+        name = outname if outname else self.obj_name + "_ncc"
+
+        # Sort tools in descending order
+        sorted_tools = []
+        for k, v in self.ncc_tools.items():
+            sorted_tools.append(float('%.4f' % float(v['tooldia'])))
+        sorted_tools.sort(reverse=True)
+
+        # Do job in background
+        proc = self.app.proc_container.new("Clearing Non-Copper areas.")
+
+        def initialize(geo_obj, app_obj):
+            assert isinstance(geo_obj, FlatCAMGeometry), \
+                "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
+
+            cleared_geo = []
+            # Already cleared area
+            cleared = MultiPolygon()
+
+            # flag for polygons not cleared
+            app_obj.poly_not_cleared = False
+
+            # Generate area for each tool
+            offset = sum(sorted_tools)
+            current_uid = int(1)
+
+            for tool in sorted_tools:
+                self.app.inform.emit('[success] Non-Copper Clearing with ToolDia = %s started.' % str(tool))
+                cleared_geo[:] = []
+
+                # Get remaining tools offset
+                offset -= (tool - 1e-12)
+
+                # Area to clear
+                area = empty.buffer(-offset)
+                try:
+                    area = area.difference(cleared)
+                except:
+                    continue
+
+                # Transform area to MultiPolygon
+                if type(area) is Polygon:
+                    area = MultiPolygon([area])
+
+                if area.geoms:
+                    if len(area.geoms) > 0:
+                        for p in area.geoms:
+                            try:
+                                if pol_method == 'standard':
+                                    cp = self.clear_polygon(p, tool, self.app.defaults["gerber_circle_steps"],
+                                                            overlap=over, contour=contour, connect=connect)
+                                elif pol_method == 'seed':
+                                    cp = self.clear_polygon2(p, tool, self.app.defaults["gerber_circle_steps"],
+                                                             overlap=over, contour=contour, connect=connect)
+                                else:
+                                    cp = self.clear_polygon3(p, tool, self.app.defaults["gerber_circle_steps"],
+                                                             overlap=over, contour=contour, connect=connect)
+                                if cp:
+                                    cleared_geo += list(cp.get_objects())
+                            except:
+                                log.warning("Polygon can not be cleared.")
+                                app_obj.poly_not_cleared = True
+                                continue
+
+                        # check if there is a geometry at all in the cleared geometry
+                        if cleared_geo:
+                            # Overall cleared area
+                            cleared = empty.buffer(-offset * (1 + over)).buffer(-tool / 1.999999).buffer(
+                                tool / 1.999999)
+
+                            # clean-up cleared geo
+                            cleared = cleared.buffer(0)
+
+                            # find the tooluid associated with the current tool_dia so we know where to add the tool
+                            # solid_geometry
+                            for k, v in self.ncc_tools.items():
+                                if float('%.4f' % v['tooldia']) == float('%.4f' % tool):
+                                    current_uid = int(k)
+
+                                    # add the solid_geometry to the current too in self.paint_tools dictionary
+                                    # and then reset the temporary list that stored that solid_geometry
+                                    v['solid_geometry'] = deepcopy(cleared_geo)
+                                    v['data']['name'] = name
+                                    break
+                            geo_obj.tools[current_uid] = dict(self.ncc_tools[current_uid])
+                        else:
+                            log.debug("There are no geometries in the cleared polygon.")
+
+            geo_obj.options["cnctooldia"] = tool
+            geo_obj.multigeo = True
+
+        def job_thread(app_obj):
+            try:
+                app_obj.new_object("geometry", name, initialize)
+            except Exception as e:
+                proc.done()
+                self.app.inform.emit('[error_notcl] NCCTool.clear_non_copper() --> %s' % str(e))
+                return
+            proc.done()
+
+            if app_obj.poly_not_cleared is False:
+                self.app.inform.emit('[success] NCC Tool finished.')
+            else:
+                self.app.inform.emit('[warning_notcl] NCC Tool finished but some PCB features could not be cleared. '
+                                     'Check the result.')
+            # reset the variable for next use
+            app_obj.poly_not_cleared = False
+
+            # focus on Selected Tab
+            self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
+            self.tools_frame.hide()
+            self.app.ui.notebook.setTabText(2, "Tools")
+
+        # Promise object with the new name
+        self.app.collection.promise(name)
+
+        # Background
+        self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
+
+    # clear copper with 'rest-machining' algorithm
+    def clear_non_copper_rest(self, empty, over, pol_method, outname=None, connect=True, contour=True):
+
+        name = outname if outname is not None else self.obj_name + "_ncc_rm"
+
+        # Sort tools in descending order
+        sorted_tools = []
+        for k, v in self.ncc_tools.items():
+            sorted_tools.append(float('%.4f' % float(v['tooldia'])))
+        sorted_tools.sort(reverse=True)
+
+        # Do job in background
+        proc = self.app.proc_container.new("Clearing Non-Copper areas.")
+
+        def initialize_rm(geo_obj, app_obj):
+            assert isinstance(geo_obj, FlatCAMGeometry), \
+                "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
+
+            cleared_geo = []
+            cleared_by_last_tool = []
+            rest_geo = []
+            current_uid = 1
+
+            # repurposed flag for final object, geo_obj. True if it has any solid_geometry, False if not.
+            app_obj.poly_not_cleared = True
+
+            area = empty.buffer(0)
+            # Generate area for each tool
+            while sorted_tools:
+                tool = sorted_tools.pop(0)
+                self.app.inform.emit('[success] Non-Copper Rest Clearing with ToolDia = %s started.' % str(tool))
+
+                tool_used = tool  - 1e-12
+                cleared_geo[:] = []
+
+                # Area to clear
+                for poly in cleared_by_last_tool:
+                    try:
+                        area = area.difference(poly)
+                    except:
+                        pass
+                cleared_by_last_tool[:] = []
+
+                # Transform area to MultiPolygon
+                if type(area) is Polygon:
+                    area = MultiPolygon([area])
+
+                # add the rest that was not able to be cleared previously; area is a MultyPolygon
+                # and rest_geo it's a list
+                allparts = [p.buffer(0) for p in area.geoms]
+                allparts += deepcopy(rest_geo)
+                rest_geo[:] = []
+                area = MultiPolygon(deepcopy(allparts))
+                allparts[:] = []
+
+                if area.geoms:
+                    if len(area.geoms) > 0:
+                        for p in area.geoms:
+                            try:
+                                if pol_method == 'standard':
+                                    cp = self.clear_polygon(p, tool_used, self.app.defaults["gerber_circle_steps"],
+                                                            overlap=over, contour=contour, connect=connect)
+                                elif pol_method == 'seed':
+                                    cp = self.clear_polygon2(p, tool_used,
+                                                             self.app.defaults["gerber_circle_steps"],
+                                                             overlap=over, contour=contour, connect=connect)
+                                else:
+                                    cp = self.clear_polygon3(p, tool_used,
+                                                             self.app.defaults["gerber_circle_steps"],
+                                                             overlap=over, contour=contour, connect=connect)
+                                cleared_geo.append(list(cp.get_objects()))
+                            except:
+                                log.warning("Polygon can't be cleared.")
+                                # this polygon should be added to a list and then try clear it with a smaller tool
+                                rest_geo.append(p)
+
+                        # check if there is a geometry at all in the cleared geometry
+                        if cleared_geo:
+                            # Overall cleared area
+                            cleared_area = list(self.flatten_list(cleared_geo))
+
+                            # cleared = MultiPolygon([p.buffer(tool_used / 2).buffer(-tool_used / 2)
+                            #                         for p in cleared_area])
+
+                            # here we store the poly's already processed in the original geometry by the current tool
+                            # into cleared_by_last_tool list
+                            # this will be sustracted from the original geometry_to_be_cleared and make data for
+                            # the next tool
+                            buffer_value = tool_used / 2
+                            for p in cleared_area:
+                                poly = p.buffer(buffer_value)
+                                cleared_by_last_tool.append(poly)
+
+                            # find the tooluid associated with the current tool_dia so we know
+                            # where to add the tool solid_geometry
+                            for k, v in self.ncc_tools.items():
+                                if float('%.4f' % v['tooldia']) == float('%.4f' % tool):
+                                    current_uid = int(k)
+
+                                    # add the solid_geometry to the current too in self.paint_tools dictionary
+                                    # and then reset the temporary list that stored that solid_geometry
+                                    v['solid_geometry'] = deepcopy(cleared_area)
+                                    v['data']['name'] = name
+                                    cleared_area[:] = []
+                                    break
+
+                            geo_obj.tools[current_uid] = dict(self.ncc_tools[current_uid])
+                        else:
+                            log.debug("There are no geometries in the cleared polygon.")
+
+            geo_obj.multigeo = True
+            geo_obj.options["cnctooldia"] = tool
+
+            # check to see if geo_obj.tools is empty
+            # it will be updated only if there is a solid_geometry for tools
+            if geo_obj.tools:
+                return
+            else:
+                # I will use this variable for this purpose although it was meant for something else
+                # signal that we have no geo in the object therefore don't create it
+                app_obj.poly_not_cleared = False
+                return "fail"
+
+        def job_thread(app_obj):
+            try:
+                app_obj.new_object("geometry", name, initialize_rm)
+            except Exception as e:
+                proc.done()
+                self.app.inform.emit('[error_notcl] NCCTool.clear_non_copper_rest() --> %s' % str(e))
+                return
+
+            if app_obj.poly_not_cleared is True:
+                self.app.inform.emit('[success] NCC Tool finished.')
+                # focus on Selected Tab
+                self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
+            else:
+                self.app.inform.emit('[error_notcl] NCC Tool finished but could not clear the object '
+                                     'with current settings.')
+                # focus on Project Tab
+                self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
+            proc.done()
+            # reset the variable for next use
+            app_obj.poly_not_cleared = False
+
+            self.tools_frame.hide()
+            self.app.ui.notebook.setTabText(2, "Tools")
+
+        # Promise object with the new name
+        self.app.collection.promise(name)
+
+        # Background
+        self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})

+ 1106 - 0
flatcamTools/ToolPaint.py

@@ -0,0 +1,1106 @@
+from FlatCAMTool import FlatCAMTool
+from copy import copy,deepcopy
+from ObjectCollection import *
+
+
+class ToolPaint(FlatCAMTool, Gerber):
+
+    toolName = "Paint Area Tool"
+
+    def __init__(self, app):
+        self.app = app
+
+        FlatCAMTool.__init__(self, app)
+        Geometry.__init__(self, geo_steps_per_circle=self.app.defaults["geometry_circle_steps"])
+
+        ## Title
+        title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
+        self.layout.addWidget(title_label)
+
+        self.tools_frame = QtWidgets.QFrame()
+        self.tools_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.tools_frame)
+        self.tools_box = QtWidgets.QVBoxLayout()
+        self.tools_box.setContentsMargins(0, 0, 0, 0)
+        self.tools_frame.setLayout(self.tools_box)
+
+        ## Form Layout
+        form_layout = QtWidgets.QFormLayout()
+        self.tools_box.addLayout(form_layout)
+
+        ## Object
+        self.object_combo = QtWidgets.QComboBox()
+        self.object_combo.setModel(self.app.collection)
+        self.object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
+        self.object_combo.setCurrentIndex(1)
+        self.object_label = QtWidgets.QLabel("Geometry:")
+        self.object_label.setToolTip(
+            "Geometry object to be painted.                        "
+        )
+        e_lab_0 = QtWidgets.QLabel('')
+        form_layout.addRow(self.object_label, self.object_combo)
+        form_layout.addRow(e_lab_0)
+
+        #### Tools ####
+        self.tools_table_label = QtWidgets.QLabel('<b>Tools Table</b>')
+        self.tools_table_label.setToolTip(
+            "Tools pool from which the algorithm\n"
+            "will pick the ones used for painting."
+        )
+        self.tools_box.addWidget(self.tools_table_label)
+
+        self.tools_table = FCTable()
+        self.tools_box.addWidget(self.tools_table)
+
+        self.tools_table.setColumnCount(4)
+        self.tools_table.setHorizontalHeaderLabels(['#', 'Diameter', 'TT', ''])
+        self.tools_table.setColumnHidden(3, True)
+        # self.tools_table.setSortingEnabled(False)
+        # self.tools_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+
+        self.tools_table.horizontalHeaderItem(0).setToolTip(
+            "This is the Tool Number.\n"
+            "Painting will start with the tool with the biggest diameter,\n"
+            "continuing until there are no more tools.\n"
+            "Only tools that create painting geometry will still be present\n"
+            "in the resulting geometry. This is because with some tools\n"
+            "this function will not be able to create painting geometry."
+            )
+        self.tools_table.horizontalHeaderItem(1).setToolTip(
+            "Tool Diameter. It's value (in current FlatCAM units) \n"
+            "is the cut width into the material.")
+
+        self.tools_table.horizontalHeaderItem(2).setToolTip(
+            "The Tool Type (TT) can be:<BR>"
+            "- <B>Circular</B> with 1 ... 4 teeth -> it is informative only. Being circular, <BR>"
+            "the cut width in material is exactly the tool diameter.<BR>"
+            "- <B>Ball</B> -> informative only and make reference to the Ball type endmill.<BR>"
+            "- <B>V-Shape</B> -> it will disable de Z-Cut parameter in the resulting geometry UI form "
+            "and enable two additional UI form fields in the resulting geometry: V-Tip Dia and "
+            "V-Tip Angle. Adjusting those two values will adjust the Z-Cut parameter such "
+            "as the cut width into material will be equal with the value in the Tool Diameter "
+            "column of this table.<BR>"
+            "Choosing the <B>V-Shape</B> Tool Type automatically will select the Operation Type "
+            "in the resulting geometry as Isolation.")
+
+        self.empty_label = QtWidgets.QLabel('')
+        self.tools_box.addWidget(self.empty_label)
+
+        #### Add a new Tool ####
+        hlay = QtWidgets.QHBoxLayout()
+        self.tools_box.addLayout(hlay)
+
+        self.addtool_entry_lbl = QtWidgets.QLabel('<b>Tool Dia:</b>')
+        self.addtool_entry_lbl.setToolTip(
+            "Diameter for the new tool."
+        )
+        self.addtool_entry = FloatEntry()
+
+        # hlay.addWidget(self.addtool_label)
+        # hlay.addStretch()
+        hlay.addWidget(self.addtool_entry_lbl)
+        hlay.addWidget(self.addtool_entry)
+
+        grid2 = QtWidgets.QGridLayout()
+        self.tools_box.addLayout(grid2)
+
+        self.addtool_btn = QtWidgets.QPushButton('Add')
+        self.addtool_btn.setToolTip(
+            "Add a new tool to the Tool Table\n"
+            "with the diameter specified above."
+        )
+
+        # self.copytool_btn = QtWidgets.QPushButton('Copy')
+        # self.copytool_btn.setToolTip(
+        #     "Copy a selection of tools in the Tool Table\n"
+        #     "by first selecting a row in the Tool Table."
+        # )
+
+        self.deltool_btn = QtWidgets.QPushButton('Delete')
+        self.deltool_btn.setToolTip(
+            "Delete a selection of tools in the Tool Table\n"
+            "by first selecting a row(s) in the Tool Table."
+        )
+
+        grid2.addWidget(self.addtool_btn, 0, 0)
+        # grid2.addWidget(self.copytool_btn, 0, 1)
+        grid2.addWidget(self.deltool_btn, 0,2)
+
+        self.empty_label_0 = QtWidgets.QLabel('')
+        self.tools_box.addWidget(self.empty_label_0)
+
+        grid3 = QtWidgets.QGridLayout()
+        self.tools_box.addLayout(grid3)
+
+        # Overlap
+        ovlabel = QtWidgets.QLabel('Overlap:')
+        ovlabel.setToolTip(
+            "How much (fraction) of the tool width to overlap each tool pass.\n"
+            "Example:\n"
+            "A value here of 0.25 means 25% from the tool diameter found above.\n\n"
+            "Adjust the value starting with lower values\n"
+            "and increasing it if areas that should be painted are still \n"
+            "not painted.\n"
+            "Lower values = faster processing, faster execution on PCB.\n"
+            "Higher values = slow processing and slow execution on CNC\n"
+            "due of too many paths."
+        )
+        grid3.addWidget(ovlabel, 1, 0)
+        self.paintoverlap_entry = LengthEntry()
+        grid3.addWidget(self.paintoverlap_entry, 1, 1)
+
+        # Margin
+        marginlabel = QtWidgets.QLabel('Margin:')
+        marginlabel.setToolTip(
+            "Distance by which to avoid\n"
+            "the edges of the polygon to\n"
+            "be painted."
+        )
+        grid3.addWidget(marginlabel, 2, 0)
+        self.paintmargin_entry = LengthEntry()
+        grid3.addWidget(self.paintmargin_entry, 2, 1)
+
+        # Method
+        methodlabel = QtWidgets.QLabel('Method:')
+        methodlabel.setToolTip(
+            "Algorithm for non-copper clearing:<BR>"
+            "<B>Standard</B>: Fixed step inwards.<BR>"
+            "<B>Seed-based</B>: Outwards from seed.<BR>"
+            "<B>Line-based</B>: Parallel lines."
+        )
+        grid3.addWidget(methodlabel, 3, 0)
+        self.paintmethod_combo = RadioSet([
+            {"label": "Standard", "value": "standard"},
+            {"label": "Seed-based", "value": "seed"},
+            {"label": "Straight lines", "value": "lines"}
+        ], orientation='vertical', stretch=False)
+        grid3.addWidget(self.paintmethod_combo, 3, 1)
+
+        # Connect lines
+        pathconnectlabel = QtWidgets.QLabel("Connect:")
+        pathconnectlabel.setToolTip(
+            "Draw lines between resulting\n"
+            "segments to minimize tool lifts."
+        )
+        grid3.addWidget(pathconnectlabel, 4, 0)
+        self.pathconnect_cb = FCCheckBox()
+        grid3.addWidget(self.pathconnect_cb, 4, 1)
+
+        contourlabel = QtWidgets.QLabel("Contour:")
+        contourlabel.setToolTip(
+            "Cut around the perimeter of the polygon\n"
+            "to trim rough edges."
+        )
+        grid3.addWidget(contourlabel, 5, 0)
+        self.paintcontour_cb = FCCheckBox()
+        grid3.addWidget(self.paintcontour_cb, 5, 1)
+
+        restlabel = QtWidgets.QLabel("Rest M.:")
+        restlabel.setToolTip(
+            "If checked, use 'rest machining'.\n"
+            "Basically it will clear copper outside PCB features,\n"
+            "using the biggest tool and continue with the next tools,\n"
+            "from bigger to smaller, to clear areas of copper that\n"
+            "could not be cleared by previous tool, until there is\n"
+            "no more copper to clear or there are no more tools.\n\n"
+            "If not checked, use the standard algorithm."
+        )
+        grid3.addWidget(restlabel, 6, 0)
+        self.rest_cb = FCCheckBox()
+        grid3.addWidget(self.rest_cb, 6, 1)
+
+        # Polygon selection
+        selectlabel = QtWidgets.QLabel('Selection:')
+        selectlabel.setToolTip(
+            "How to select the polygons to paint.<BR>"
+            "Options:<BR>"
+            "- <B>Single</B>: left mouse click on the polygon to be painted.<BR>"
+            "- <B>All</B>: paint all polygons."
+        )
+        grid3.addWidget(selectlabel, 7, 0)
+        # grid3 = QtWidgets.QGridLayout()
+        self.selectmethod_combo = RadioSet([
+            {"label": "Single", "value": "single"},
+            {"label": "All", "value": "all"},
+            # {"label": "Rectangle", "value": "rectangle"}
+        ])
+        grid3.addWidget(self.selectmethod_combo, 7, 1)
+
+        # GO Button
+        self.generate_paint_button = QtWidgets.QPushButton('Create Paint Geometry')
+        self.generate_paint_button.setToolTip(
+            "After clicking here, click inside<BR>"
+            "the polygon you wish to be painted if <B>Single</B> is selected.<BR>"
+            "If <B>All</B>  is selected then the Paint will start after click.<BR>"
+            "A new Geometry object with the tool<BR>"
+            "paths will be created."
+        )
+        self.tools_box.addWidget(self.generate_paint_button)
+
+        self.tools_box.addStretch()
+
+        self.obj_name = ""
+        self.paint_obj = None
+
+        self.units = ''
+        self.paint_tools = {}
+        self.tooluid = 0
+        # store here the default data for Geometry Data
+        self.default_data = {}
+        self.default_data.update({
+            "name": '_paint',
+            "plot": self.app.defaults["geometry_plot"],
+            "tooldia": self.app.defaults["geometry_painttooldia"],
+            "cutz": self.app.defaults["geometry_cutz"],
+            "vtipdia": 0.1,
+            "vtipangle": 30,
+            "travelz": self.app.defaults["geometry_travelz"],
+            "feedrate": self.app.defaults["geometry_feedrate"],
+            "feedrate_z": self.app.defaults["geometry_feedrate_z"],
+            "feedrate_rapid": self.app.defaults["geometry_feedrate_rapid"],
+            "dwell": self.app.defaults["geometry_dwell"],
+            "dwelltime": self.app.defaults["geometry_dwelltime"],
+            "multidepth": self.app.defaults["geometry_multidepth"],
+            "ppname_g": self.app.defaults["geometry_ppname_g"],
+            "depthperpass": self.app.defaults["geometry_depthperpass"],
+            "extracut": self.app.defaults["geometry_extracut"],
+            "toolchange": self.app.defaults["geometry_toolchange"],
+            "toolchangez": self.app.defaults["geometry_toolchangez"],
+            "endz": self.app.defaults["geometry_endz"],
+            "spindlespeed": self.app.defaults["geometry_spindlespeed"],
+            "toolchangexy": self.app.defaults["geometry_toolchangexy"],
+            "startz": self.app.defaults["geometry_startz"],
+            "paintmargin": self.app.defaults["geometry_paintmargin"],
+            "paintmethod": self.app.defaults["geometry_paintmethod"],
+            "selectmethod": self.app.defaults["geometry_selectmethod"],
+            "pathconnect": self.app.defaults["geometry_pathconnect"],
+            "paintcontour": self.app.defaults["geometry_paintcontour"],
+            "paintoverlap": self.app.defaults["geometry_paintoverlap"]
+        })
+
+        self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
+
+        ## Signals
+        self.addtool_btn.clicked.connect(self.on_tool_add)
+        # self.copytool_btn.clicked.connect(lambda: self.on_tool_copy())
+        self.tools_table.itemChanged.connect(self.on_tool_edit)
+        self.deltool_btn.clicked.connect(self.on_tool_delete)
+        self.generate_paint_button.clicked.connect(self.on_paint_button_click)
+        self.selectmethod_combo.activated_custom.connect(self.on_radio_selection)
+
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, **kwargs)
+
+    def run(self):
+        FlatCAMTool.run(self)
+        self.tools_frame.show()
+        self.set_ui()
+        self.app.ui.notebook.setTabText(2, "Paint Tool")
+
+    def on_radio_selection(self):
+        if self.selectmethod_combo.get_value() == 'single':
+            # disable rest-machining for single polygon painting
+            self.rest_cb.set_value(False)
+            self.rest_cb.setDisabled(True)
+            # delete all tools except first row / tool for single polygon painting
+            list_to_del = list(range(1, self.tools_table.rowCount()))
+            if list_to_del:
+                self.on_tool_delete(rows_to_delete=list_to_del)
+            # disable addTool and delTool
+            self.addtool_entry.setDisabled(True)
+            self.addtool_btn.setDisabled(True)
+            self.deltool_btn.setDisabled(True)
+        else:
+            self.rest_cb.setDisabled(False)
+            self.addtool_entry.setDisabled(False)
+            self.addtool_btn.setDisabled(False)
+            self.deltool_btn.setDisabled(False)
+
+    def set_ui(self):
+        ## Init the GUI interface
+        self.paintmargin_entry.set_value(self.default_data["paintmargin"])
+        self.paintmethod_combo.set_value(self.default_data["paintmethod"])
+        self.selectmethod_combo.set_value(self.default_data["selectmethod"])
+        self.pathconnect_cb.set_value(self.default_data["pathconnect"])
+        self.paintcontour_cb.set_value(self.default_data["paintcontour"])
+        self.paintoverlap_entry.set_value(self.default_data["paintoverlap"])
+
+        # updated units
+        self.units = self.app.general_options_form.general_group.units_radio.get_value().upper()
+
+        if self.units == "IN":
+            self.addtool_entry.set_value(0.039)
+        else:
+            self.addtool_entry.set_value(1)
+
+        self.tools_table.setupContextMenu()
+        self.tools_table.addContextMenu(
+            "Add", lambda: self.on_tool_add(dia=None, muted=None), icon=QtGui.QIcon("share/plus16.png"))
+        self.tools_table.addContextMenu(
+            "Delete", lambda:
+            self.on_tool_delete(rows_to_delete=None, all=None), icon=QtGui.QIcon("share/delete32.png"))
+
+        # set the working variables to a known state
+        self.paint_tools.clear()
+        self.tooluid = 0
+
+        self.default_data.clear()
+        self.default_data.update({
+            "name": '_paint',
+            "plot": self.app.defaults["geometry_plot"],
+            "tooldia": self.app.defaults["geometry_painttooldia"],
+            "cutz": self.app.defaults["geometry_cutz"],
+            "vtipdia": 0.1,
+            "vtipangle": 30,
+            "travelz": self.app.defaults["geometry_travelz"],
+            "feedrate": self.app.defaults["geometry_feedrate"],
+            "feedrate_z": self.app.defaults["geometry_feedrate_z"],
+            "feedrate_rapid": self.app.defaults["geometry_feedrate_rapid"],
+            "dwell": self.app.defaults["geometry_dwell"],
+            "dwelltime": self.app.defaults["geometry_dwelltime"],
+            "multidepth": self.app.defaults["geometry_multidepth"],
+            "ppname_g": self.app.defaults["geometry_ppname_g"],
+            "depthperpass": self.app.defaults["geometry_depthperpass"],
+            "extracut": self.app.defaults["geometry_extracut"],
+            "toolchange": self.app.defaults["geometry_toolchange"],
+            "toolchangez": self.app.defaults["geometry_toolchangez"],
+            "endz": self.app.defaults["geometry_endz"],
+            "spindlespeed": self.app.defaults["geometry_spindlespeed"],
+            "toolchangexy": self.app.defaults["geometry_toolchangexy"],
+            "startz": self.app.defaults["geometry_startz"],
+            "paintmargin": self.app.defaults["geometry_paintmargin"],
+            "paintmethod": self.app.defaults["geometry_paintmethod"],
+            "selectmethod": self.app.defaults["geometry_selectmethod"],
+            "pathconnect": self.app.defaults["geometry_pathconnect"],
+            "paintcontour": self.app.defaults["geometry_paintcontour"],
+            "paintoverlap": self.app.defaults["geometry_paintoverlap"]
+        })
+
+        # call on self.on_tool_add() counts as an call to self.build_ui()
+        # through this, we add a initial row / tool in the tool_table
+        self.on_tool_add(self.app.defaults["geometry_painttooldia"], muted=True)
+
+    def build_ui(self):
+
+        try:
+            # if connected, disconnect the signal from the slot on item_changed as it creates issues
+            self.tools_table.itemChanged.disconnect()
+        except:
+            pass
+
+        # updated units
+        self.units = self.app.general_options_form.general_group.units_radio.get_value().upper()
+
+        sorted_tools = []
+        for k, v in self.paint_tools.items():
+            sorted_tools.append(float('%.4f' % float(v['tooldia'])))
+        sorted_tools.sort()
+
+        n = len(sorted_tools)
+        self.tools_table.setRowCount(n)
+        tool_id = 0
+
+        for tool_sorted in sorted_tools:
+            for tooluid_key, tooluid_value in self.paint_tools.items():
+                if float('%.4f' % tooluid_value['tooldia']) == tool_sorted:
+                    tool_id += 1
+                    id = QtWidgets.QTableWidgetItem('%d' % int(tool_id))
+                    id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+                    row_no = tool_id - 1
+                    self.tools_table.setItem(row_no, 0, id)  # Tool name/id
+
+                    # Make sure that the drill diameter when in MM is with no more than 2 decimals
+                    # There are no drill bits in MM with more than 3 decimals diameter
+                    # For INCH the decimals should be no more than 3. There are no drills under 10mils
+                    if self.units == 'MM':
+                        dia = QtWidgets.QTableWidgetItem('%.2f' % tooluid_value['tooldia'])
+                    else:
+                        dia = QtWidgets.QTableWidgetItem('%.3f' % tooluid_value['tooldia'])
+
+                    dia.setFlags(QtCore.Qt.ItemIsEnabled)
+
+                    tool_type_item = QtWidgets.QComboBox()
+                    for item in self.tool_type_item_options:
+                        tool_type_item.addItem(item)
+                        tool_type_item.setStyleSheet('background-color: rgb(255,255,255)')
+                    idx = tool_type_item.findText(tooluid_value['tool_type'])
+                    tool_type_item.setCurrentIndex(idx)
+
+                    tool_uid_item = QtWidgets.QTableWidgetItem(str(int(tooluid_key)))
+
+                    self.tools_table.setItem(row_no, 1, dia)  # Diameter
+                    self.tools_table.setCellWidget(row_no, 2, tool_type_item)
+
+                    ### REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY ###
+                    self.tools_table.setItem(row_no, 3, tool_uid_item)  # Tool unique ID
+
+        # make the diameter column editable
+        for row in range(tool_id):
+            self.tools_table.item(row, 1).setFlags(
+                QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+
+        # all the tools are selected by default
+        self.tools_table.selectColumn(0)
+        #
+        self.tools_table.resizeColumnsToContents()
+        self.tools_table.resizeRowsToContents()
+
+        vertical_header = self.tools_table.verticalHeader()
+        vertical_header.hide()
+        self.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header = self.tools_table.horizontalHeader()
+        horizontal_header.setMinimumSectionSize(10)
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(0, 20)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+
+        # self.tools_table.setSortingEnabled(True)
+        # sort by tool diameter
+        # self.tools_table.sortItems(1)
+
+        self.tools_table.setMinimumHeight(self.tools_table.getHeight())
+        self.tools_table.setMaximumHeight(self.tools_table.getHeight())
+
+        # we reactivate the signals after the after the tool adding as we don't need to see the tool been populated
+        self.tools_table.itemChanged.connect(self.on_tool_edit)
+
+    def on_tool_add(self, dia=None, muted=None):
+
+        try:
+            self.tools_table.itemChanged.disconnect()
+        except:
+            pass
+
+        if dia:
+            tool_dia = dia
+        else:
+            tool_dia = self.addtool_entry.get_value()
+            if tool_dia is None:
+                self.build_ui()
+                self.app.inform.emit("[warning_notcl] Please enter a tool diameter to add, in Float format.")
+                return
+
+        # construct a list of all 'tooluid' in the self.tools
+        tool_uid_list = []
+        for tooluid_key in self.paint_tools:
+            tool_uid_item = int(tooluid_key)
+            tool_uid_list.append(tool_uid_item)
+
+        # find maximum from the temp_uid, add 1 and this is the new 'tooluid'
+        if not tool_uid_list:
+            max_uid = 0
+        else:
+            max_uid = max(tool_uid_list)
+        self.tooluid = int(max_uid + 1)
+
+        tool_dias = []
+        for k, v in self.paint_tools.items():
+            for tool_v in v.keys():
+                if tool_v == 'tooldia':
+                    tool_dias.append(float('%.4f' % v[tool_v]))
+
+        if float('%.4f' % tool_dia) in tool_dias:
+            if muted is None:
+                self.app.inform.emit("[warning_notcl]Adding tool cancelled. Tool already in Tool Table.")
+            self.tools_table.itemChanged.connect(self.on_tool_edit)
+            return
+        else:
+            if muted is None:
+                self.app.inform.emit("[success] New tool added to Tool Table.")
+            self.paint_tools.update({
+                int(self.tooluid): {
+                    'tooldia': float('%.4f' % tool_dia),
+                    'offset': 'Path',
+                    'offset_value': 0.0,
+                    'type': 'Iso',
+                    'tool_type': 'V',
+                    'data': dict(self.default_data),
+                    'solid_geometry': []
+                }
+            })
+
+        self.build_ui()
+
+    def on_tool_edit(self):
+        try:
+            self.tools_table.itemChanged.disconnect()
+        except:
+            pass
+
+        tool_dias = []
+        for k, v in self.paint_tools.items():
+            for tool_v in v.keys():
+                if tool_v == 'tooldia':
+                    tool_dias.append(float('%.4f' % v[tool_v]))
+
+        for row in range(self.tools_table.rowCount()):
+            new_tool_dia = float(self.tools_table.item(row, 1).text())
+            tooluid = int(self.tools_table.item(row, 3).text())
+
+            # identify the tool that was edited and get it's tooluid
+            if new_tool_dia not in tool_dias:
+                self.paint_tools[tooluid]['tooldia'] = new_tool_dia
+                self.app.inform.emit("[success] Tool from Tool Table was edited.")
+                self.build_ui()
+                return
+            else:
+                # identify the old tool_dia and restore the text in tool table
+                for k, v in self.paint_tools.items():
+                    if k == tooluid:
+                        old_tool_dia = v['tooldia']
+                        break
+                restore_dia_item = self.tools_table.item(row, 1)
+                restore_dia_item.setText(str(old_tool_dia))
+                self.app.inform.emit("[warning_notcl] Edit cancelled. New diameter value is already in the Tool Table.")
+        self.build_ui()
+
+    # def on_tool_copy(self, all=None):
+    #     try:
+    #         self.tools_table.itemChanged.disconnect()
+    #     except:
+    #         pass
+    #
+    #     # find the tool_uid maximum value in the self.tools
+    #     uid_list = []
+    #     for key in self.paint_tools:
+    #         uid_list.append(int(key))
+    #     try:
+    #         max_uid = max(uid_list, key=int)
+    #     except ValueError:
+    #         max_uid = 0
+    #
+    #     if all is None:
+    #         if self.tools_table.selectedItems():
+    #             for current_row in self.tools_table.selectedItems():
+    #                 # sometime the header get selected and it has row number -1
+    #                 # we don't want to do anything with the header :)
+    #                 if current_row.row() < 0:
+    #                     continue
+    #                 try:
+    #                     tooluid_copy = int(self.tools_table.item(current_row.row(), 3).text())
+    #                     max_uid += 1
+    #                     self.paint_tools[int(max_uid)] = dict(self.paint_tools[tooluid_copy])
+    #                     for td in self.paint_tools:
+    #                         print("COPIED", self.paint_tools[td])
+    #                     self.build_ui()
+    #                 except AttributeError:
+    #                     self.app.inform.emit("[warning_notcl]Failed. Select a tool to copy.")
+    #                     self.build_ui()
+    #                     return
+    #                 except Exception as e:
+    #                     log.debug("on_tool_copy() --> " + str(e))
+    #             # deselect the table
+    #             # self.ui.geo_tools_table.clearSelection()
+    #         else:
+    #             self.app.inform.emit("[warning_notcl]Failed. Select a tool to copy.")
+    #             self.build_ui()
+    #             return
+    #     else:
+    #         # we copy all tools in geo_tools_table
+    #         try:
+    #             temp_tools = dict(self.paint_tools)
+    #             max_uid += 1
+    #             for tooluid in temp_tools:
+    #                 self.paint_tools[int(max_uid)] = dict(temp_tools[tooluid])
+    #             temp_tools.clear()
+    #             self.build_ui()
+    #         except Exception as e:
+    #             log.debug("on_tool_copy() --> " + str(e))
+    #
+    #     self.app.inform.emit("[success] Tool was copied in the Tool Table.")
+
+    def on_tool_delete(self, rows_to_delete=None, all=None):
+        try:
+            self.tools_table.itemChanged.disconnect()
+        except:
+            pass
+
+        deleted_tools_list = []
+
+        if all:
+            self.paint_tools.clear()
+            self.build_ui()
+            return
+
+        if rows_to_delete:
+            try:
+                for row in rows_to_delete:
+                    tooluid_del = int(self.tools_table.item(row, 3).text())
+                    deleted_tools_list.append(tooluid_del)
+            except TypeError:
+                deleted_tools_list.append(rows_to_delete)
+
+            for t in deleted_tools_list:
+                self.paint_tools.pop(t, None)
+            self.build_ui()
+            return
+
+        try:
+            if self.tools_table.selectedItems():
+                for row_sel in self.tools_table.selectedItems():
+                    row = row_sel.row()
+                    if row < 0:
+                        continue
+                    tooluid_del = int(self.tools_table.item(row, 3).text())
+                    deleted_tools_list.append(tooluid_del)
+
+                for t in deleted_tools_list:
+                    self.paint_tools.pop(t, None)
+
+        except AttributeError:
+            self.app.inform.emit("[warning_notcl]Delete failed. Select a tool to delete.")
+            return
+        except Exception as e:
+            log.debug(str(e))
+
+        self.app.inform.emit("[success] Tool(s) deleted from Tool Table.")
+        self.build_ui()
+
+    def on_paint_button_click(self):
+        self.app.report_usage("geometry_on_paint_button")
+
+        self.app.inform.emit("[warning_notcl]Click inside the desired polygon.")
+
+        overlap = self.paintoverlap_entry.get_value()
+        connect = self.pathconnect_cb.get_value()
+        contour = self.paintcontour_cb.get_value()
+        select_method = self.selectmethod_combo.get_value()
+
+        self.obj_name = self.object_combo.currentText()
+
+        # Get source object.
+        try:
+            self.paint_obj = self.app.collection.get_by_name(str(self.obj_name))
+        except:
+            self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % self.obj_name)
+            return
+
+        if self.paint_obj is None:
+            self.app.inform.emit("[error_notcl]Object not found: %s" % self.paint_obj)
+            return
+
+        o_name = '%s_multitool_paint' % (self.obj_name)
+
+        if select_method == "all":
+            self.paint_poly_all(self.paint_obj,
+                                outname=o_name,
+                                overlap=overlap,
+                                connect=connect,
+                                contour=contour)
+
+        if select_method == "single":
+            self.app.inform.emit("[warning_notcl]Click inside the desired polygon.")
+
+            # use the first tool in the tool table; get the diameter
+            tooldia = float('%.4f' % float(self.tools_table.item(0, 1).text()))
+
+            # To be called after clicking on the plot.
+            def doit(event):
+                # do paint single only for left mouse clicks
+                if event.button == 1:
+                    self.app.inform.emit("Painting polygon...")
+                    self.app.plotcanvas.vis_disconnect('mouse_press', doit)
+                    pos = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
+                    self.paint_poly(self.paint_obj,
+                                    inside_pt=[pos[0], pos[1]],
+                                    tooldia=tooldia,
+                                    overlap=overlap,
+                                    connect=connect,
+                                    contour=contour)
+
+            self.app.plotcanvas.vis_connect('mouse_press', doit)
+
+    def paint_poly(self, obj, inside_pt, tooldia, overlap,
+                    outname=None, connect=True,
+                    contour=True):
+        """
+        Paints a polygon selected by clicking on its interior.
+
+        Note:
+            * The margin is taken directly from the form.
+
+        :param inside_pt: [x, y]
+        :param tooldia: Diameter of the painting tool
+        :param overlap: Overlap of the tool between passes.
+        :param outname: Name of the resulting Geometry Object.
+        :param connect: Connect lines to avoid tool lifts.
+        :param contour: Paint around the edges.
+        :return: None
+        """
+
+        # Which polygon.
+        # poly = find_polygon(self.solid_geometry, inside_pt)
+        poly = obj.find_polygon(inside_pt)
+        paint_method = self.paintmethod_combo.get_value()
+        paint_margin = self.paintmargin_entry.get_value()
+
+        # No polygon?
+        if poly is None:
+            self.app.log.warning('No polygon found.')
+            self.app.inform.emit('[warning] No polygon found.')
+            return
+
+        proc = self.app.proc_container.new("Painting polygon.")
+
+        name = outname if outname else self.obj_name + "_paint"
+
+        # Initializes the new geometry object
+        def gen_paintarea(geo_obj, app_obj):
+            assert isinstance(geo_obj, FlatCAMGeometry), \
+                "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
+            #assert isinstance(app_obj, App)
+
+            geo_obj.solid_geometry = []
+            try:
+                poly_buf = poly.buffer(-paint_margin)
+                if paint_method == "seed":
+                    # Type(cp) == FlatCAMRTreeStorage | None
+                    cp = self.clear_polygon2(poly_buf,
+                                             tooldia=tooldia,
+                                             steps_per_circle=self.app.defaults["geometry_circle_steps"],
+                                             overlap=overlap,
+                                             contour=contour,
+                                             connect=connect)
+
+                elif paint_method == "lines":
+                    # Type(cp) == FlatCAMRTreeStorage | None
+                    cp = self.clear_polygon3(poly_buf,
+                                             tooldia=tooldia,
+                                             steps_per_circle=self.app.defaults["geometry_circle_steps"],
+                                             overlap=overlap,
+                                             contour=contour,
+                                             connect=connect)
+
+                else:
+                    # Type(cp) == FlatCAMRTreeStorage | None
+                    cp = self.clear_polygon(poly_buf,
+                                             tooldia=tooldia,
+                                             steps_per_circle=self.app.defaults["geometry_circle_steps"],
+                                             overlap=overlap,
+                                             contour=contour,
+                                             connect=connect)
+
+                if cp is not None:
+                    geo_obj.solid_geometry += list(cp.get_objects())
+                else:
+                    self.app.inform.emit('[error_notcl] Geometry could not be painted completely')
+                    return
+            except Exception as e:
+                log.debug("Could not Paint the polygons. %s" % str(e))
+                self.app.inform.emit(
+                    "[error] Could not do Paint. Try a different combination of parameters. "
+                    "Or a different strategy of paint\n%s" % str(e))
+                return
+
+            if cp is not None:
+                geo_obj.solid_geometry = list(cp.get_objects())
+
+            geo_obj.options["cnctooldia"] = tooldia
+            # this turn on the FlatCAMCNCJob plot for multiple tools
+            geo_obj.multigeo = False
+            geo_obj.multitool = True
+
+            current_uid = int(self.tools_table.item(0, 3).text())
+            for k, v in self.paint_tools.items():
+                if k == current_uid:
+                    v['data']['name'] = name
+
+            geo_obj.tools = dict(self.paint_tools)
+
+            # Experimental...
+            # print("Indexing...", end=' ')
+            # geo_obj.make_index()
+            # if errors == 0:
+            #     print("[success] Paint single polygon Done")
+            #     self.app.inform.emit("[success] Paint single polygon Done")
+            # else:
+            #     print("[WARNING] Paint single polygon done with errors")
+            #     self.app.inform.emit("[warning] Paint single polygon done with errors. "
+            #                          "%d area(s) could not be painted.\n"
+            #                          "Use different paint parameters or edit the paint geometry and correct"
+            #                          "the issue."
+            #                          % errors)
+
+        def job_thread(app_obj):
+            try:
+                app_obj.new_object("geometry", name, gen_paintarea)
+            except Exception as e:
+                proc.done()
+                self.app.inform.emit('[error_notcl] PaintTool.paint_poly() --> %s' % str(e))
+                return
+            proc.done()
+            # focus on Selected Tab
+            self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
+
+        self.app.inform.emit("Polygon Paint started ...")
+
+        # Promise object with the new name
+        self.app.collection.promise(name)
+
+        # Background
+        self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
+
+    def paint_poly_all(self, obj, overlap, outname=None,
+                       connect=True, contour=True):
+        """
+        Paints all polygons in this object.
+
+        :param tooldia:
+        :param overlap:
+        :param outname:
+        :param connect: Connect lines to avoid tool lifts.
+        :param contour: Paint around the edges.
+        :return:
+        """
+        paint_method = self.paintmethod_combo.get_value()
+        paint_margin = self.paintmargin_entry.get_value()
+
+        proc = self.app.proc_container.new("Painting polygon.")
+
+        name = outname if outname else self.obj_name + "_paint"
+        over = overlap
+        conn = connect
+        cont = contour
+
+        # This is a recursive generator of individual Polygons.
+        # Note: Double check correct implementation. Might exit
+        #       early if it finds something that is not a Polygon?
+        # def recurse(geo):
+        #     try:
+        #         for subg in geo:
+        #             for subsubg in recurse(subg):
+        #                 yield subsubg
+        #     except TypeError:
+        #         if isinstance(geo, Polygon):
+        #             yield geo
+        #
+        #     raise StopIteration
+
+        def recurse(geometry, reset=True):
+            """
+            Creates a list of non-iterable linear geometry objects.
+            Results are placed in self.flat_geometry
+
+            :param geometry: Shapely type or list or list of list of such.
+            :param reset: Clears the contents of self.flat_geometry.
+            """
+
+            if geometry is None:
+                return
+
+            if reset:
+                self.flat_geometry = []
+
+            ## If iterable, expand recursively.
+            try:
+                for geo in geometry:
+                    if geo is not None:
+                        recurse(geometry=geo, reset=False)
+
+            ## Not iterable, do the actual indexing and add.
+            except TypeError:
+                self.flat_geometry.append(geometry)
+
+            return self.flat_geometry
+
+        # Initializes the new geometry object
+        def gen_paintarea(geo_obj, app_obj):
+            assert isinstance(geo_obj, FlatCAMGeometry), \
+                "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
+
+            sorted_tools = []
+            for row in range(self.tools_table.rowCount()):
+                sorted_tools.append(float(self.tools_table.item(row, 1).text()))
+            sorted_tools.sort(reverse=True)
+
+            total_geometry = []
+            current_uid = int(1)
+            geo_obj.solid_geometry = []
+            for tool_dia in sorted_tools:
+                # find the tooluid associated with the current tool_dia so we know where to add the tool solid_geometry
+                for k, v in self.paint_tools.items():
+                    if float('%.4f' % v['tooldia']) == float('%.4f' % tool_dia):
+                        current_uid = int(k)
+                        break
+
+                for geo in recurse(obj.solid_geometry):
+                    try:
+                        if not isinstance(geo, Polygon):
+                            geo = Polygon(geo)
+                        poly_buf = geo.buffer(-paint_margin)
+
+                        if paint_method == "seed":
+                            # Type(cp) == FlatCAMRTreeStorage | None
+                            cp = self.clear_polygon2(poly_buf,
+                                                     tooldia=tool_dia,
+                                                     steps_per_circle=self.app.defaults["geometry_circle_steps"],
+                                                     overlap=over,
+                                                     contour=cont,
+                                                     connect=conn)
+
+                        elif paint_method == "lines":
+                            # Type(cp) == FlatCAMRTreeStorage | None
+                            cp = self.clear_polygon3(poly_buf,
+                                                     tooldia=tool_dia,
+                                                     steps_per_circle=self.app.defaults["geometry_circle_steps"],
+                                                     overlap=over,
+                                                     contour=cont,
+                                                     connect=conn)
+
+                        else:
+                            # Type(cp) == FlatCAMRTreeStorage | None
+                            cp = self.clear_polygon(poly_buf,
+                                                     tooldia=tool_dia,
+                                                     steps_per_circle=self.app.defaults["geometry_circle_steps"],
+                                                     overlap=over,
+                                                     contour=cont,
+                                                     connect=conn)
+
+                        if cp is not None:
+                            total_geometry += list(cp.get_objects())
+                    except Exception as e:
+                        log.debug("Could not Paint the polygons. %s" % str(e))
+                        self.app.inform.emit(
+                            "[error] Could not do Paint All. Try a different combination of parameters. "
+                            "Or a different Method of paint\n%s" % str(e))
+                        return
+
+                # add the solid_geometry to the current too in self.paint_tools dictionary and then reset the
+                # temporary list that stored that solid_geometry
+                self.paint_tools[current_uid]['solid_geometry'] = deepcopy(total_geometry)
+
+                self.paint_tools[current_uid]['data']['name'] = name
+                total_geometry[:] = []
+
+            geo_obj.options["cnctooldia"] = tool_dia
+            # this turn on the FlatCAMCNCJob plot for multiple tools
+            geo_obj.multigeo = True
+            geo_obj.multitool = True
+            geo_obj.tools.clear()
+            geo_obj.tools = dict(self.paint_tools)
+
+            # test if at least one tool has solid_geometry. If no tool has solid_geometry we raise an Exception
+            has_solid_geo = 0
+            for tooluid in geo_obj.tools:
+                if geo_obj.tools[tooluid]['solid_geometry']:
+                    has_solid_geo += 1
+            if has_solid_geo == 0:
+                self.app.inform.emit("[error] There is no Painting Geometry in the file.\n"
+                                      "Usually it means that the tool diameter is too big for the painted geometry.\n"
+                                      "Change the painting parameters and try again.")
+                return
+
+            # Experimental...
+            # print("Indexing...", end=' ')
+            # geo_obj.make_index()
+
+            self.app.inform.emit("[success] Paint All Done.")
+
+        # Initializes the new geometry object
+        def gen_paintarea_rest_machining(geo_obj, app_obj):
+            assert isinstance(geo_obj, FlatCAMGeometry), \
+                "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
+
+            sorted_tools = []
+            for row in range(self.tools_table.rowCount()):
+                sorted_tools.append(float(self.tools_table.item(row, 1).text()))
+            sorted_tools.sort(reverse=True)
+
+            cleared_geo = []
+            current_uid = int(1)
+            geo_obj.solid_geometry = []
+
+            for tool_dia in sorted_tools:
+                for geo in recurse(obj.solid_geometry):
+                    try:
+                        geo = Polygon(geo) if not isinstance(geo, Polygon) else geo
+                        poly_buf = geo.buffer(-paint_margin)
+
+                        if paint_method == "standard":
+                            # Type(cp) == FlatCAMRTreeStorage | None
+                            cp = self.clear_polygon(poly_buf, tooldia=tool_dia,
+                                                     steps_per_circle=self.app.defaults["geometry_circle_steps"],
+                                                     overlap=over, contour=cont, connect=conn)
+
+                        elif paint_method == "seed":
+                            # Type(cp) == FlatCAMRTreeStorage | None
+                            cp = self.clear_polygon2(poly_buf, tooldia=tool_dia,
+                                                     steps_per_circle=self.app.defaults["geometry_circle_steps"],
+                                                     overlap=over, contour=cont, connect=conn)
+
+                        elif paint_method == "lines":
+                            # Type(cp) == FlatCAMRTreeStorage | None
+                            cp = self.clear_polygon3(poly_buf, tooldia=tool_dia,
+                                                    steps_per_circle=self.app.defaults["geometry_circle_steps"],
+                                                    overlap=over, contour=cont, connect=conn)
+
+                        if cp is not None:
+                            cleared_geo += list(cp.get_objects())
+
+                    except Exception as e:
+                        log.debug("Could not Paint the polygons. %s" % str(e))
+                        self.app.inform.emit(
+                            "[error] Could not do Paint All. Try a different combination of parameters. "
+                            "Or a different Method of paint\n%s" % str(e))
+                        return
+
+                # find the tooluid associated with the current tool_dia so we know where to add the tool solid_geometry
+                for k, v in self.paint_tools.items():
+                    if float('%.4f' % v['tooldia']) == float('%.4f' % tool_dia):
+                        current_uid = int(k)
+                        break
+
+                # add the solid_geometry to the current too in self.paint_tools dictionary and then reset the
+                # temporary list that stored that solid_geometry
+                self.paint_tools[current_uid]['solid_geometry'] = deepcopy(cleared_geo)
+
+                self.paint_tools[current_uid]['data']['name'] = name
+                cleared_geo[:] = []
+
+            geo_obj.options["cnctooldia"] = tool_dia
+            # this turn on the FlatCAMCNCJob plot for multiple tools
+            geo_obj.multigeo = True
+            geo_obj.multitool = True
+            geo_obj.tools.clear()
+            geo_obj.tools = dict(self.paint_tools)
+
+            # test if at least one tool has solid_geometry. If no tool has solid_geometry we raise an Exception
+            has_solid_geo = 0
+            for tooluid in geo_obj.tools:
+                if geo_obj.tools[tooluid]['solid_geometry']:
+                    has_solid_geo += 1
+            if has_solid_geo == 0:
+                self.app.inform.emit("[error_notcl] There is no Painting Geometry in the file.\n"
+                                      "Usually it means that the tool diameter is too big for the painted geometry.\n"
+                                      "Change the painting parameters and try again.")
+                return
+
+            # Experimental...
+            # print("Indexing...", end=' ')
+            # geo_obj.make_index()
+
+            self.app.inform.emit("[success] Paint All with Rest-Machining Done.")
+
+        def job_thread(app_obj):
+            try:
+                if self.rest_cb.isChecked():
+                    app_obj.new_object("geometry", name, gen_paintarea_rest_machining)
+                else:
+                    app_obj.new_object("geometry", name, gen_paintarea)
+            except Exception as e:
+                proc.done()
+                traceback.print_stack()
+                return
+            proc.done()
+            # focus on Selected Tab
+            self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
+
+        self.app.inform.emit("Polygon Paint started ...")
+
+        # Promise object with the new name
+        self.app.collection.promise(name)
+
+        # Background
+        self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})

+ 369 - 0
flatcamTools/ToolPanelize.py

@@ -0,0 +1,369 @@
+from FlatCAMTool import FlatCAMTool
+from copy import copy, deepcopy
+from ObjectCollection import *
+import time
+
+
+class Panelize(FlatCAMTool):
+
+    toolName = "Panelize PCB Tool"
+
+    def __init__(self, app):
+        super(Panelize, self).__init__(self)
+        self.app = app
+
+        ## Title
+        title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
+        self.layout.addWidget(title_label)
+
+        ## Form Layout
+        form_layout = QtWidgets.QFormLayout()
+        self.layout.addLayout(form_layout)
+
+        ## Type of object to be panelized
+        self.type_obj_combo = QtWidgets.QComboBox()
+        self.type_obj_combo.addItem("Gerber")
+        self.type_obj_combo.addItem("Excellon")
+        self.type_obj_combo.addItem("Geometry")
+
+        self.type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
+        self.type_obj_combo.setItemIcon(1, QtGui.QIcon("share/drill16.png"))
+        self.type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
+
+        self.type_obj_combo_label = QtWidgets.QLabel("Object Type:")
+        self.type_obj_combo_label.setToolTip(
+            "Specify the type of object to be panelized\n"
+            "It can be of type: Gerber, Excellon or Geometry.\n"
+            "The selection here decide the type of objects that will be\n"
+            "in the Object combobox."
+        )
+        form_layout.addRow(self.type_obj_combo_label, self.type_obj_combo)
+
+        ## Object to be panelized
+        self.object_combo = QtWidgets.QComboBox()
+        self.object_combo.setModel(self.app.collection)
+        self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.object_combo.setCurrentIndex(1)
+        self.object_label = QtWidgets.QLabel("Object:")
+        self.object_label.setToolTip(
+            "Object to be panelized. This means that it will\n"
+            "be duplicated in an array of rows and columns."
+        )
+        form_layout.addRow(self.object_label, self.object_combo)
+
+        ## Type of Box Object to be used as an envelope for panelization
+        self.type_box_combo = QtWidgets.QComboBox()
+        self.type_box_combo.addItem("Gerber")
+        self.type_box_combo.addItem("Excellon")
+        self.type_box_combo.addItem("Geometry")
+
+        # we get rid of item1 ("Excellon") as it is not suitable for use as a "box" for panelizing
+        self.type_box_combo.view().setRowHidden(1, True)
+        self.type_box_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
+        self.type_box_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
+
+        self.type_box_combo_label = QtWidgets.QLabel("Box Type:")
+        self.type_box_combo_label.setToolTip(
+            "Specify the type of object to be used as an container for\n"
+            "panelization. It can be: Gerber or Geometry type.\n"
+            "The selection here decide the type of objects that will be\n"
+            "in the Box Object combobox."
+        )
+        form_layout.addRow(self.type_box_combo_label, self.type_box_combo)
+
+        ## Box
+        self.box_combo = QtWidgets.QComboBox()
+        self.box_combo.setModel(self.app.collection)
+        self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.box_combo.setCurrentIndex(1)
+        self.box_combo_label = QtWidgets.QLabel("Box Object:")
+        self.box_combo_label.setToolTip(
+            "The actual object that is used a container for the\n "
+            "selected object that is to be panelized."
+        )
+        form_layout.addRow(self.box_combo_label, self.box_combo)
+
+        ## Spacing Columns
+        self.spacing_columns = FloatEntry()
+        self.spacing_columns.set_value(0.0)
+        self.spacing_columns_label = QtWidgets.QLabel("Spacing cols:")
+        self.spacing_columns_label.setToolTip(
+            "Spacing between columns of the desired panel.\n"
+            "In current units."
+        )
+        form_layout.addRow(self.spacing_columns_label, self.spacing_columns)
+
+        ## Spacing Rows
+        self.spacing_rows = FloatEntry()
+        self.spacing_rows.set_value(0.0)
+        self.spacing_rows_label = QtWidgets.QLabel("Spacing rows:")
+        self.spacing_rows_label.setToolTip(
+            "Spacing between rows of the desired panel.\n"
+            "In current units."
+        )
+        form_layout.addRow(self.spacing_rows_label, self.spacing_rows)
+
+        ## Columns
+        self.columns = IntEntry()
+        self.columns.set_value(1)
+        self.columns_label = QtWidgets.QLabel("Columns:")
+        self.columns_label.setToolTip(
+            "Number of columns of the desired panel"
+        )
+        form_layout.addRow(self.columns_label, self.columns)
+
+        ## Rows
+        self.rows = IntEntry()
+        self.rows.set_value(1)
+        self.rows_label = QtWidgets.QLabel("Rows:")
+        self.rows_label.setToolTip(
+            "Number of rows of the desired panel"
+        )
+        form_layout.addRow(self.rows_label, self.rows)
+
+        ## Constrains
+        self.constrain_cb = FCCheckBox("Constrain panel within:")
+        self.constrain_cb.setToolTip(
+            "Area define by DX and DY within to constrain the panel.\n"
+            "DX and DY values are in current units.\n"
+            "Regardless of how many columns and rows are desired,\n"
+            "the final panel will have as many columns and rows as\n"
+            "they fit completely within selected area."
+        )
+        form_layout.addRow(self.constrain_cb)
+
+        self.x_width_entry = FloatEntry()
+        self.x_width_entry.set_value(0.0)
+        self.x_width_lbl = QtWidgets.QLabel("Width (DX):")
+        self.x_width_lbl.setToolTip(
+            "The width (DX) within which the panel must fit.\n"
+            "In current units."
+        )
+        form_layout.addRow(self.x_width_lbl, self.x_width_entry)
+
+        self.y_height_entry = FloatEntry()
+        self.y_height_entry.set_value(0.0)
+        self.y_height_lbl = QtWidgets.QLabel("Height (DY):")
+        self.y_height_lbl.setToolTip(
+            "The height (DY)within which the panel must fit.\n"
+            "In current units."
+        )
+        form_layout.addRow(self.y_height_lbl, self.y_height_entry)
+
+        self.constrain_sel = OptionalInputSection(
+            self.constrain_cb, [self.x_width_lbl, self.x_width_entry, self.y_height_lbl, self.y_height_entry])
+
+
+        ## Buttons
+        hlay_2 = QtWidgets.QHBoxLayout()
+        self.layout.addLayout(hlay_2)
+
+        hlay_2.addStretch()
+        self.panelize_object_button = QtWidgets.QPushButton("Panelize Object")
+        self.panelize_object_button.setToolTip(
+            "Panelize the specified object around the specified box.\n"
+            "In other words it creates multiple copies of the source object,\n"
+            "arranged in a 2D array of rows and columns."
+        )
+        hlay_2.addWidget(self.panelize_object_button)
+
+        self.layout.addStretch()
+
+        ## Signals
+        self.panelize_object_button.clicked.connect(self.on_panelize)
+        self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
+        self.type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed)
+
+        # list to hold the temporary objects
+        self.objs = []
+
+        # final name for the panel object
+        self.outname = ""
+
+        # flag to signal the constrain was activated
+        self.constrain_flag = False
+
+    def on_type_obj_index_changed(self):
+        obj_type = self.type_obj_combo.currentIndex()
+        self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.object_combo.setCurrentIndex(0)
+
+    def on_type_box_index_changed(self):
+        obj_type = self.type_box_combo.currentIndex()
+        self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.box_combo.setCurrentIndex(0)
+
+    def run(self):
+        FlatCAMTool.run(self)
+        self.app.ui.notebook.setTabText(2, "Panel. Tool")
+
+    def on_panelize(self):
+        name = self.object_combo.currentText()
+
+        # Get source object.
+        try:
+            obj = self.app.collection.get_by_name(str(name))
+        except:
+            self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % name)
+            return "Could not retrieve object: %s" % name
+
+        panel_obj = obj
+
+        if panel_obj is None:
+            self.app.inform.emit("[error_notcl]Object not found: %s" % panel_obj)
+            return "Object not found: %s" % panel_obj
+
+        boxname = self.box_combo.currentText()
+
+        try:
+            box = self.app.collection.get_by_name(boxname)
+        except:
+            self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % boxname)
+            return "Could not retrieve object: %s" % boxname
+
+        if box is None:
+            self.app.inform.emit("[warning]No object Box. Using instead %s" % panel_obj)
+            box = panel_obj
+
+        self.outname = name + '_panelized'
+
+        spacing_columns = self.spacing_columns.get_value()
+        spacing_columns = spacing_columns if spacing_columns is not None else 0
+
+        spacing_rows = self.spacing_rows.get_value()
+        spacing_rows = spacing_rows if spacing_rows is not None else 0
+
+        rows = self.rows.get_value()
+        rows = rows if rows is not None else 1
+
+        columns = self.columns.get_value()
+        columns = columns if columns is not None else 1
+
+        constrain_dx = self.x_width_entry.get_value()
+        constrain_dy = self.y_height_entry.get_value()
+
+        if 0 in {columns, rows}:
+            self.app.inform.emit("[error_notcl]Columns or Rows are zero value. Change them to a positive integer.")
+            return "Columns or Rows are zero value. Change them to a positive integer."
+
+        xmin, ymin, xmax, ymax = box.bounds()
+        lenghtx = xmax - xmin + spacing_columns
+        lenghty = ymax - ymin + spacing_rows
+
+        # check if constrain within an area is desired
+        if self.constrain_cb.isChecked():
+            panel_lengthx = ((xmax - xmin) * columns) + (spacing_columns * (columns - 1))
+            panel_lengthy = ((ymax - ymin) * rows) + (spacing_rows * (rows - 1))
+
+            # adjust the number of columns and/or rows so the panel will fit within the panel constraint area
+            if (panel_lengthx > constrain_dx) or (panel_lengthy > constrain_dy):
+                self.constrain_flag = True
+
+                while panel_lengthx > constrain_dx:
+                    columns -= 1
+                    panel_lengthx = ((xmax - xmin) * columns) + (spacing_columns * (columns - 1))
+                while panel_lengthy > constrain_dy:
+                    rows -= 1
+                    panel_lengthy = ((ymax - ymin) * rows) + (spacing_rows * (rows - 1))
+
+        def clean_temp():
+            # deselect all  to avoid  delete selected object when run  delete  from  shell
+            self.app.collection.set_all_inactive()
+
+            for del_obj in self.objs:
+                self.app.collection.set_active(del_obj.options['name'])
+                self.app.on_delete()
+
+            self.objs[:] = []
+
+        def panelize():
+            if panel_obj is not None:
+                self.app.inform.emit("Generating panel ... Please wait.")
+
+                self.app.progress.emit(10)
+
+                if isinstance(panel_obj, FlatCAMExcellon):
+                    currenty = 0.0
+                    self.app.progress.emit(0)
+
+                    def initialize_local_excellon(obj_init, app):
+                        obj_init.tools = panel_obj.tools
+                        # drills are offset, so they need to be deep copied
+                        obj_init.drills = deepcopy(panel_obj.drills)
+                        obj_init.offset([float(currentx), float(currenty)])
+                        obj_init.create_geometry()
+                        self.objs.append(obj_init)
+
+                    self.app.progress.emit(0)
+                    for row in range(rows):
+                        currentx = 0.0
+                        for col in range(columns):
+                            local_outname = self.outname + ".tmp." + str(col) + "." + str(row)
+                            self.app.new_object("excellon", local_outname, initialize_local_excellon, plot=False,
+                                                autoselected=False)
+                            currentx += lenghtx
+                        currenty += lenghty
+                else:
+                    currenty = 0
+                    self.app.progress.emit(0)
+
+                    def initialize_local_geometry(obj_init, app):
+                        obj_init.solid_geometry = panel_obj.solid_geometry
+                        obj_init.offset([float(currentx), float(currenty)]),
+                        self.objs.append(obj_init)
+
+                    self.app.progress.emit(0)
+                    for row in range(rows):
+                        currentx = 0
+
+                        for col in range(columns):
+                            local_outname = self.outname + ".tmp." + str(col) + "." + str(row)
+                            self.app.new_object("geometry", local_outname, initialize_local_geometry, plot=False,
+                                                autoselected=False)
+                            currentx += lenghtx
+                        currenty += lenghty
+
+                def job_init_geometry(obj_fin, app_obj):
+                    FlatCAMGeometry.merge(self.objs, obj_fin)
+
+                def job_init_excellon(obj_fin, app_obj):
+                    # merge expects tools to exist in the target object
+                    obj_fin.tools = panel_obj.tools.copy()
+                    FlatCAMExcellon.merge(self.objs, obj_fin)
+
+                if isinstance(panel_obj, FlatCAMExcellon):
+                    self.app.progress.emit(50)
+                    self.app.new_object("excellon", self.outname, job_init_excellon, plot=True, autoselected=True)
+                else:
+                    self.app.progress.emit(50)
+                    self.app.new_object("geometry", self.outname, job_init_geometry, plot=True, autoselected=True)
+
+            else:
+                self.app.inform.emit("[error_notcl] Obj is None")
+                return "ERROR: Obj is None"
+
+        panelize()
+        clean_temp()
+        if self.constrain_flag is False:
+            self.app.inform.emit("[success]Panel done...")
+        else:
+            self.constrain_flag = False
+            self.app.inform.emit("[warning] Too big for the constrain area. Final panel has %s columns and %s rows" %
+                                 (columns, rows))
+
+        # proc = self.app.proc_container.new("Generating panel ... Please wait.")
+        #
+        # def job_thread(app_obj):
+        #     try:
+        #         panelize()
+        #     except Exception as e:
+        #         proc.done()
+        #         raise e
+        #     proc.done()
+        #
+        # self.app.collection.promise(self.outname)
+        # self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
+
+    def reset_fields(self):
+        self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))

+ 123 - 0
flatcamTools/ToolProperties.py

@@ -0,0 +1,123 @@
+from PyQt5 import QtGui, QtCore, QtWidgets
+from PyQt5.QtCore import Qt
+from FlatCAMTool import FlatCAMTool
+from FlatCAMObj import *
+
+
+class Properties(FlatCAMTool):
+
+    toolName = "Properties"
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+
+        self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
+
+        # this way I can hide/show the frame
+        self.properties_frame = QtWidgets.QFrame()
+        self.properties_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.properties_frame)
+        self.properties_box = QtWidgets.QVBoxLayout()
+        self.properties_box.setContentsMargins(0, 0, 0, 0)
+        self.properties_frame.setLayout(self.properties_box)
+
+        ## Title
+        title_label = QtWidgets.QLabel("<font size=4><b>&nbsp;%s</b></font>" % self.toolName)
+        self.properties_box.addWidget(title_label)
+
+        # self.layout.setMargin(0)  # PyQt4
+        self.properties_box.setContentsMargins(0, 0, 0, 0) # PyQt5
+
+        self.vlay = QtWidgets.QVBoxLayout()
+
+        self.properties_box.addLayout(self.vlay)
+
+        self.treeWidget = QtWidgets.QTreeWidget()
+        self.treeWidget.setColumnCount(2)
+        self.treeWidget.setHeaderHidden(True)
+        self.treeWidget.header().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
+        self.treeWidget.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Expanding)
+
+        self.vlay.addWidget(self.treeWidget)
+        self.vlay.setStretch(0,0)
+
+    def run(self):
+
+        if self.app.tool_tab_locked is True:
+            return
+
+        # this reset the TreeWidget
+        self.treeWidget.clear()
+        self.properties_frame.show()
+
+        FlatCAMTool.run(self)
+        self.properties()
+
+    def properties(self):
+        obj_list = self.app.collection.get_selected()
+        if not obj_list:
+            self.app.inform.emit("[error_notcl] Properties Tool was not displayed. No object selected.")
+            self.app.ui.notebook.setTabText(2, "Tools")
+            self.properties_frame.hide()
+            self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
+            return
+        for obj in obj_list:
+            self.addItems(obj)
+            self.app.inform.emit("[success] Object Properties are displayed.")
+        self.app.ui.notebook.setTabText(2, "Properties Tool")
+
+    def addItems(self, obj):
+        parent = self.treeWidget.invisibleRootItem()
+
+        font = QtGui.QFont()
+        font.setBold(True)
+        obj_type = self.addParent(parent, 'TYPE', expanded=True, color=QtGui.QColor("#000000"), font=font)
+        obj_name = self.addParent(parent, 'NAME', expanded=True, color=QtGui.QColor("#000000"), font=font)
+        dims = self.addParent(parent, 'Dimensions', expanded=True, color=QtGui.QColor("#000000"), font=font)
+        options = self.addParent(parent, 'Options', color=QtGui.QColor("#000000"), font=font)
+        separator = self.addParent(parent, '')
+
+        self.addChild(obj_type, [obj.kind.upper()])
+        self.addChild(obj_name, [obj.options['name']])
+
+        # calculate physical dimensions
+        xmin, ymin, xmax, ymax = obj.bounds()
+        length = abs(xmax - xmin)
+        width = abs(ymax - ymin)
+
+        self.addChild(dims, ['Length:', '%.4f %s' % (
+            length, self.app.general_options_form.general_group.units_radio.get_value().lower())], True)
+        self.addChild(dims, ['Width:', '%.4f %s' % (
+            width, self.app.general_options_form.general_group.units_radio.get_value().lower())], True)
+        if self.app.general_options_form.general_group.units_radio.get_value().lower() == 'mm':
+            area = (length * width) / 100
+            self.addChild(dims, ['Box Area:', '%.4f %s' % (area, 'cm2')], True)
+        else:
+            area = length * width
+            self.addChild(dims, ['Box Area:', '%.4f %s' % (area, 'in2')], True)
+
+        for option in obj.options:
+            if option is 'name':
+                continue
+            self.addChild(options, [str(option), str(obj.options[option])], True)
+
+        self.addChild(separator, [''])
+
+    def addParent(self, parent, title, expanded=False, color=None, font=None):
+        item = QtWidgets.QTreeWidgetItem(parent, [title])
+        item.setChildIndicatorPolicy(QtWidgets.QTreeWidgetItem.ShowIndicator)
+        item.setExpanded(expanded)
+        if color is not None:
+            # item.setTextColor(0, color) # PyQt4
+            item.setForeground(0, QtGui.QBrush(color))
+        if font is not None:
+            item.setFont(0, font)
+        return item
+
+    def addChild(self, parent, title, column1=None):
+        item = QtWidgets.QTreeWidgetItem(parent)
+        item.setText(0, str(title[0]))
+        if column1 is not None:
+            item.setText(1, str(title[1]))
+
+# end of file

+ 361 - 0
flatcamTools/ToolShell.py

@@ -0,0 +1,361 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+############################################################
+
+import html
+from PyQt5.QtCore import pyqtSignal
+from PyQt5.QtCore import Qt, QStringListModel
+from PyQt5.QtGui import QColor, QKeySequence, QPalette, QTextCursor
+from PyQt5.QtWidgets import QLineEdit, QSizePolicy, QTextEdit, QVBoxLayout, QWidget, QCompleter, QAction
+
+class _BrowserTextEdit(QTextEdit):
+
+    def __init__(self):
+        QTextEdit.__init__(self)
+        self.menu = None
+
+    def contextMenuEvent(self, event):
+        self.menu = self.createStandardContextMenu(event.pos())
+        clear_action = QAction("Clear", self)
+        clear_action.setShortcut(QKeySequence(Qt.Key_Delete))   # it's not working, the shortcut
+        self.menu.addAction(clear_action)
+        clear_action.triggered.connect(self.clear)
+        self.menu.exec_(event.globalPos())
+
+
+    def clear(self):
+        QTextEdit.clear(self)
+        text = "FlatCAM 3000\n(c) 2014-2019 Juan Pablo Caram\n\nType help to get started.\n\n"
+        text = html.escape(text)
+        text = text.replace('\n', '<br/>')
+        self.moveCursor(QTextCursor.End)
+        self.insertHtml(text)
+
+class _ExpandableTextEdit(QTextEdit):
+    """
+    Class implements edit line, which expands themselves automatically
+    """
+
+    historyNext = pyqtSignal()
+    historyPrev = pyqtSignal()
+
+    def __init__(self, termwidget, *args):
+        QTextEdit.__init__(self, *args)
+        self.setStyleSheet("font: 9pt \"Courier\";")
+        self._fittedHeight = 1
+        self.textChanged.connect(self._fit_to_document)
+        self._fit_to_document()
+        self._termWidget = termwidget
+
+        self.completer = MyCompleter()
+
+        self.model = QStringListModel()
+        self.completer.setModel(self.model)
+        self.set_model_data(keyword_list=[])
+        self.completer.insertText.connect(self.insertCompletion)
+
+    def set_model_data(self, keyword_list):
+        self.model.setStringList(keyword_list)
+
+    def insertCompletion(self, completion):
+        tc = self.textCursor()
+        extra = (len(completion) - len(self.completer.completionPrefix()))
+        tc.movePosition(QTextCursor.Left)
+        tc.movePosition(QTextCursor.EndOfWord)
+        tc.insertText(completion[-extra:])
+        self.setTextCursor(tc)
+        self.completer.popup().hide()
+
+    def focusInEvent(self, event):
+        if self.completer:
+            self.completer.setWidget(self)
+        QTextEdit.focusInEvent(self, event)
+
+    def keyPressEvent(self, event):
+        """
+        Catch keyboard events. Process Enter, Up, Down
+        """
+        if event.matches(QKeySequence.InsertParagraphSeparator):
+            text = self.toPlainText()
+            if self._termWidget.is_command_complete(text):
+                self._termWidget.exec_current_command()
+                return
+        elif event.matches(QKeySequence.MoveToNextLine):
+            text = self.toPlainText()
+            cursor_pos = self.textCursor().position()
+            textBeforeEnd = text[cursor_pos:]
+
+            if len(textBeforeEnd.split('\n')) <= 1:
+                self.historyNext.emit()
+                return
+        elif event.matches(QKeySequence.MoveToPreviousLine):
+            text = self.toPlainText()
+            cursor_pos = self.textCursor().position()
+            text_before_start = text[:cursor_pos]
+            # lineCount = len(textBeforeStart.splitlines())
+            line_count = len(text_before_start.split('\n'))
+            if len(text_before_start) > 0 and \
+                    (text_before_start[-1] == '\n' or text_before_start[-1] == '\r'):
+                line_count += 1
+            if line_count <= 1:
+                self.historyPrev.emit()
+                return
+        elif event.matches(QKeySequence.MoveToNextPage) or \
+                event.matches(QKeySequence.MoveToPreviousPage):
+            return self._termWidget.browser().keyPressEvent(event)
+
+        tc = self.textCursor()
+        if event.key() == Qt.Key_Tab and self.completer.popup().isVisible():
+            self.completer.insertText.emit(self.completer.getSelected())
+            self.completer.setCompletionMode(QCompleter.PopupCompletion)
+            return
+
+        QTextEdit.keyPressEvent(self, event)
+        tc.select(QTextCursor.WordUnderCursor)
+        cr = self.cursorRect()
+
+        if len(tc.selectedText()) > 0:
+            self.completer.setCompletionPrefix(tc.selectedText())
+            popup = self.completer.popup()
+            popup.setCurrentIndex(self.completer.completionModel().index(0, 0))
+
+            cr.setWidth(self.completer.popup().sizeHintForColumn(0)
+                        + self.completer.popup().verticalScrollBar().sizeHint().width())
+            self.completer.complete(cr)
+        else:
+            self.completer.popup().hide()
+
+    def sizeHint(self):
+        """
+        QWidget sizeHint impelemtation
+        """
+        hint = QTextEdit.sizeHint(self)
+        hint.setHeight(self._fittedHeight)
+        return hint
+
+    def _fit_to_document(self):
+        """
+        Update widget height to fit all text
+        """
+        documentsize = self.document().size().toSize()
+        self._fittedHeight = documentsize.height() + (self.height() - self.viewport().height())
+        self.setMaximumHeight(self._fittedHeight)
+        self.updateGeometry()
+
+    def insertFromMimeData(self, mime_data):
+        # Paste only plain text.
+        self.insertPlainText(mime_data.text())
+
+
+class MyCompleter(QCompleter):
+    insertText = pyqtSignal(str)
+
+    def __init__(self, parent=None):
+        QCompleter.__init__(self)
+        self.setCompletionMode(QCompleter.PopupCompletion)
+        self.highlighted.connect(self.setHighlighted)
+
+    def setHighlighted(self, text):
+        self.lastSelected = text
+
+    def getSelected(self):
+        return self.lastSelected
+
+
+class TermWidget(QWidget):
+    """
+    Widget wich represents terminal. It only displays text and allows to enter text.
+    All highlevel logic should be implemented by client classes
+
+    User pressed Enter. Client class should decide, if command must be executed or user may continue edit it
+    """
+
+    def __init__(self, *args):
+        QWidget.__init__(self, *args)
+
+        self._browser = _BrowserTextEdit()
+        self._browser.setStyleSheet("font: 9pt \"Courier\";")
+        self._browser.setReadOnly(True)
+        self._browser.document().setDefaultStyleSheet(
+            self._browser.document().defaultStyleSheet() +
+            "span {white-space:pre;}")
+
+        self._edit = _ExpandableTextEdit(self, self)
+        self._edit.historyNext.connect(self._on_history_next)
+        self._edit.historyPrev.connect(self._on_history_prev)
+        self._edit.setFocus()
+        self.setFocusProxy(self._edit)
+
+        layout = QVBoxLayout(self)
+        layout.setSpacing(0)
+        layout.setContentsMargins(0, 0, 0, 0)
+        layout.addWidget(self._browser)
+        layout.addWidget(self._edit)
+
+        self._history = ['']  # current empty line
+        self._historyIndex = 0
+
+    def open_proccessing(self, detail=None):
+        """
+        Open processing and disable using shell commands  again until all commands are finished
+
+        :param detail: text detail about what is currently called from TCL to python
+        :return: None
+        """
+
+        self._edit.setTextColor(Qt.white)
+        self._edit.setTextBackgroundColor(Qt.darkGreen)
+        if detail is None:
+            self._edit.setPlainText("...proccessing...")
+        else:
+            self._edit.setPlainText("...proccessing... [%s]" % detail)
+
+        self._edit.setDisabled(True)
+        self._edit.setFocus()
+
+    def close_proccessing(self):
+        """
+        Close processing and enable using shell commands  again
+        :return:
+        """
+
+        self._edit.setTextColor(Qt.black)
+        self._edit.setTextBackgroundColor(Qt.white)
+        self._edit.setPlainText('')
+        self._edit.setDisabled(False)
+        self._edit.setFocus()
+
+    def _append_to_browser(self, style, text):
+        """
+        Convert text to HTML for inserting it to browser
+        """
+        assert style in ('in', 'out', 'err')
+
+        text = html.escape(text)
+        text = text.replace('\n', '<br/>')
+
+        if style == 'in':
+            text = '<span style="font-weight: bold;">%s</span>' % text
+        elif style == 'err':
+            text = '<span style="font-weight: bold; color: red;">%s</span>' % text
+        else:
+            text = '<span>%s</span>' % text  # without span <br/> is ignored!!!
+
+        scrollbar = self._browser.verticalScrollBar()
+        old_value = scrollbar.value()
+        scrollattheend = old_value == scrollbar.maximum()
+
+        self._browser.moveCursor(QTextCursor.End)
+        self._browser.insertHtml(text)
+
+        """TODO When user enters second line to the input, and input is resized, scrollbar changes its positon
+        and stops moving. As quick fix of this problem, now we always scroll down when add new text.
+        To fix it correctly, srcoll to the bottom, if before intput has been resized,
+        scrollbar was in the bottom, and remove next lien
+        """
+        scrollattheend = True
+
+        if scrollattheend:
+            scrollbar.setValue(scrollbar.maximum())
+        else:
+            scrollbar.setValue(old_value)
+
+    def exec_current_command(self):
+        """
+        Save current command in the history. Append it to the log. Clear edit line
+        Reimplement in the child classes to actually execute command
+        """
+        text = str(self._edit.toPlainText())
+        self._append_to_browser('in', '> ' + text + '\n')
+
+        if len(self._history) < 2 or\
+           self._history[-2] != text:  # don't insert duplicating items
+            if text[-1] == '\n':
+                self._history.insert(-1, text[:-1])
+            else:
+                self._history.insert(-1, text)
+
+        self._historyIndex = len(self._history) - 1
+
+        self._history[-1] = ''
+        self._edit.clear()
+
+        if not text[-1] == '\n':
+            text += '\n'
+
+        self.child_exec_command(text)
+
+    def child_exec_command(self, text):
+        """
+        Reimplement in the child classes
+        """
+        pass
+
+    def add_line_break_to_input(self):
+        self._edit.textCursor().insertText('\n')
+
+    def append_output(self, text):
+        """Appent text to output widget
+        """
+        self._append_to_browser('out', text)
+
+    def append_error(self, text):
+        """Appent error text to output widget. Text is drawn with red background
+        """
+        self._append_to_browser('err', text)
+
+    def is_command_complete(self, text):
+        """
+        Executed by _ExpandableTextEdit. Reimplement this function in the child classes.
+        """
+        return True
+
+    def browser(self):
+        return self._browser
+
+    def _on_history_next(self):
+        """
+        Down pressed, show next item from the history
+        """
+        if (self._historyIndex + 1) < len(self._history):
+            self._historyIndex += 1
+            self._edit.setPlainText(self._history[self._historyIndex])
+            self._edit.moveCursor(QTextCursor.End)
+
+    def _on_history_prev(self):
+        """
+        Up pressed, show previous item from the history
+        """
+        if self._historyIndex > 0:
+            if self._historyIndex == (len(self._history) - 1):
+                self._history[-1] = self._edit.toPlainText()
+            self._historyIndex -= 1
+            self._edit.setPlainText(self._history[self._historyIndex])
+            self._edit.moveCursor(QTextCursor.End)
+
+class FCShell(TermWidget):
+    def __init__(self, sysShell, *args):
+        TermWidget.__init__(self, *args)
+        self._sysShell = sysShell
+
+    def is_command_complete(self, text):
+        def skipQuotes(text):
+            quote = text[0]
+            text = text[1:]
+            endIndex = str(text).index(quote)
+            return text[endIndex:]
+        while text:
+            if text[0] in ('"', "'"):
+                try:
+                    text = skipQuotes(text)
+                except ValueError:
+                    return False
+            text = text[1:]
+        return True
+
+    def child_exec_command(self, text):
+        self._sysShell.exec_command(text)

+ 756 - 0
flatcamTools/ToolTransform.py

@@ -0,0 +1,756 @@
+from PyQt5 import QtGui, QtCore, QtWidgets
+from PyQt5.QtCore import Qt
+from GUIElements import FCEntry, FCButton, OptionalInputSection
+from FlatCAMTool import FlatCAMTool
+from FlatCAMObj import *
+
+
+class ToolTransform(FlatCAMTool):
+
+    toolName = "Object Transform"
+    rotateName = "Rotate"
+    skewName = "Skew/Shear"
+    scaleName = "Scale"
+    flipName = "Mirror (Flip)"
+    offsetName = "Offset"
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+
+        self.transform_lay = QtWidgets.QVBoxLayout()
+        self.layout.addLayout(self.transform_lay)
+        ## Title
+        title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
+        self.transform_lay.addWidget(title_label)
+
+        self.empty_label = QtWidgets.QLabel("")
+        self.empty_label.setFixedWidth(50)
+
+        self.empty_label1 = QtWidgets.QLabel("")
+        self.empty_label1.setFixedWidth(70)
+        self.empty_label2 = QtWidgets.QLabel("")
+        self.empty_label2.setFixedWidth(70)
+        self.empty_label3 = QtWidgets.QLabel("")
+        self.empty_label3.setFixedWidth(70)
+        self.empty_label4 = QtWidgets.QLabel("")
+        self.empty_label4.setFixedWidth(70)
+        self.transform_lay.addWidget(self.empty_label)
+
+        ## Rotate Title
+        rotate_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.rotateName)
+        self.transform_lay.addWidget(rotate_title_label)
+
+        ## Layout
+        form_layout = QtWidgets.QFormLayout()
+        self.transform_lay.addLayout(form_layout)
+        form_child = QtWidgets.QFormLayout()
+
+        self.rotate_label = QtWidgets.QLabel("Angle:")
+        self.rotate_label.setToolTip(
+            "Angle for Rotation action, in degrees.\n"
+            "Float number between -360 and 359.\n"
+            "Positive numbers for CW motion.\n"
+            "Negative numbers for CCW motion."
+        )
+        self.rotate_label.setFixedWidth(50)
+
+        self.rotate_entry = FCEntry()
+        self.rotate_entry.setFixedWidth(60)
+        self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+
+        self.rotate_button = FCButton()
+        self.rotate_button.set_value("Rotate")
+        self.rotate_button.setToolTip(
+            "Rotate the selected object(s).\n"
+            "The point of reference is the middle of\n"
+            "the bounding box for all selected objects."
+        )
+        self.rotate_button.setFixedWidth(60)
+
+        form_child.addRow(self.rotate_entry, self.rotate_button)
+        form_layout.addRow(self.rotate_label, form_child)
+
+        self.transform_lay.addWidget(self.empty_label1)
+
+        ## Skew Title
+        skew_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.skewName)
+        self.transform_lay.addWidget(skew_title_label)
+
+        ## Form Layout
+        form1_layout = QtWidgets.QFormLayout()
+        self.transform_lay.addLayout(form1_layout)
+        form1_child_1 = QtWidgets.QFormLayout()
+        form1_child_2 = QtWidgets.QFormLayout()
+
+        self.skewx_label = QtWidgets.QLabel("Angle X:")
+        self.skewx_label.setToolTip(
+            "Angle for Skew action, in degrees.\n"
+            "Float number between -360 and 359."
+        )
+        self.skewx_label.setFixedWidth(50)
+        self.skewx_entry = FCEntry()
+        self.skewx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.skewx_entry.setFixedWidth(60)
+
+        self.skewx_button = FCButton()
+        self.skewx_button.set_value("Skew X")
+        self.skewx_button.setToolTip(
+            "Skew/shear the selected object(s).\n"
+            "The point of reference is the middle of\n"
+            "the bounding box for all selected objects.")
+        self.skewx_button.setFixedWidth(60)
+
+        self.skewy_label = QtWidgets.QLabel("Angle Y:")
+        self.skewy_label.setToolTip(
+            "Angle for Skew action, in degrees.\n"
+            "Float number between -360 and 359."
+        )
+        self.skewy_label.setFixedWidth(50)
+        self.skewy_entry = FCEntry()
+        self.skewy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.skewy_entry.setFixedWidth(60)
+
+        self.skewy_button = FCButton()
+        self.skewy_button.set_value("Skew Y")
+        self.skewy_button.setToolTip(
+            "Skew/shear the selected object(s).\n"
+            "The point of reference is the middle of\n"
+            "the bounding box for all selected objects.")
+        self.skewy_button.setFixedWidth(60)
+
+        form1_child_1.addRow(self.skewx_entry, self.skewx_button)
+        form1_child_2.addRow(self.skewy_entry, self.skewy_button)
+        form1_layout.addRow(self.skewx_label, form1_child_1)
+        form1_layout.addRow(self.skewy_label, form1_child_2)
+
+        self.transform_lay.addWidget(self.empty_label2)
+
+        ## Scale Title
+        scale_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.scaleName)
+        self.transform_lay.addWidget(scale_title_label)
+
+        ## Form Layout
+        form2_layout = QtWidgets.QFormLayout()
+        self.transform_lay.addLayout(form2_layout)
+        form2_child_1 = QtWidgets.QFormLayout()
+        form2_child_2 = QtWidgets.QFormLayout()
+
+        self.scalex_label = QtWidgets.QLabel("Factor X:")
+        self.scalex_label.setToolTip(
+            "Factor for Scale action over X axis."
+        )
+        self.scalex_label.setFixedWidth(50)
+        self.scalex_entry = FCEntry()
+        self.scalex_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.scalex_entry.setFixedWidth(60)
+
+        self.scalex_button = FCButton()
+        self.scalex_button.set_value("Scale X")
+        self.scalex_button.setToolTip(
+            "Scale the selected object(s).\n"
+            "The point of reference depends on \n"
+            "the Scale reference checkbox state.")
+        self.scalex_button.setFixedWidth(60)
+
+        self.scaley_label = QtWidgets.QLabel("Factor Y:")
+        self.scaley_label.setToolTip(
+            "Factor for Scale action over Y axis."
+        )
+        self.scaley_label.setFixedWidth(50)
+        self.scaley_entry = FCEntry()
+        self.scaley_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.scaley_entry.setFixedWidth(60)
+
+        self.scaley_button = FCButton()
+        self.scaley_button.set_value("Scale Y")
+        self.scaley_button.setToolTip(
+            "Scale the selected object(s).\n"
+            "The point of reference depends on \n"
+            "the Scale reference checkbox state.")
+        self.scaley_button.setFixedWidth(60)
+
+        self.scale_link_cb = FCCheckBox()
+        self.scale_link_cb.set_value(True)
+        self.scale_link_cb.setText("Link")
+        self.scale_link_cb.setToolTip(
+            "Scale the selected object(s)\n"
+            "using the Scale Factor X for both axis.")
+        self.scale_link_cb.setFixedWidth(50)
+
+        self.scale_zero_ref_cb = FCCheckBox()
+        self.scale_zero_ref_cb.set_value(True)
+        self.scale_zero_ref_cb.setText("Scale Reference")
+        self.scale_zero_ref_cb.setToolTip(
+            "Scale the selected object(s)\n"
+            "using the origin reference when checked,\n"
+            "and the center of the biggest bounding box\n"
+            "of the selected objects when unchecked.")
+
+        form2_child_1.addRow(self.scalex_entry, self.scalex_button)
+        form2_child_2.addRow(self.scaley_entry, self.scaley_button)
+        form2_layout.addRow(self.scalex_label, form2_child_1)
+        form2_layout.addRow(self.scaley_label, form2_child_2)
+        form2_layout.addRow(self.scale_link_cb, self.scale_zero_ref_cb)
+        self.ois_scale = OptionalInputSection(self.scale_link_cb, [self.scaley_entry, self.scaley_button], logic=False)
+
+        self.transform_lay.addWidget(self.empty_label3)
+
+        ## Offset Title
+        offset_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.offsetName)
+        self.transform_lay.addWidget(offset_title_label)
+
+        ## Form Layout
+        form3_layout = QtWidgets.QFormLayout()
+        self.transform_lay.addLayout(form3_layout)
+        form3_child_1 = QtWidgets.QFormLayout()
+        form3_child_2 = QtWidgets.QFormLayout()
+
+        self.offx_label = QtWidgets.QLabel("Value X:")
+        self.offx_label.setToolTip(
+            "Value for Offset action on X axis."
+        )
+        self.offx_label.setFixedWidth(50)
+        self.offx_entry = FCEntry()
+        self.offx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.offx_entry.setFixedWidth(60)
+
+        self.offx_button = FCButton()
+        self.offx_button.set_value("Offset X")
+        self.offx_button.setToolTip(
+            "Offset the selected object(s).\n"
+            "The point of reference is the middle of\n"
+            "the bounding box for all selected objects.\n")
+        self.offx_button.setFixedWidth(60)
+
+        self.offy_label = QtWidgets.QLabel("Value Y:")
+        self.offy_label.setToolTip(
+            "Value for Offset action on Y axis."
+        )
+        self.offy_label.setFixedWidth(50)
+        self.offy_entry = FCEntry()
+        self.offy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.offy_entry.setFixedWidth(60)
+
+        self.offy_button = FCButton()
+        self.offy_button.set_value("Offset Y")
+        self.offy_button.setToolTip(
+            "Offset the selected object(s).\n"
+            "The point of reference is the middle of\n"
+            "the bounding box for all selected objects.\n")
+        self.offy_button.setFixedWidth(60)
+
+        form3_child_1.addRow(self.offx_entry, self.offx_button)
+        form3_child_2.addRow(self.offy_entry, self.offy_button)
+        form3_layout.addRow(self.offx_label, form3_child_1)
+        form3_layout.addRow(self.offy_label, form3_child_2)
+
+        self.transform_lay.addWidget(self.empty_label4)
+
+        ## Flip Title
+        flip_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.flipName)
+        self.transform_lay.addWidget(flip_title_label)
+
+        ## Form Layout
+        form4_layout = QtWidgets.QFormLayout()
+        self.transform_lay.addLayout(form4_layout)
+        form4_child = QtWidgets.QFormLayout()
+        form4_child_1 = QtWidgets.QFormLayout()
+
+        self.flipx_button = FCButton()
+        self.flipx_button.set_value("Flip on X")
+        self.flipx_button.setToolTip(
+            "Flip the selected object(s) over the X axis.\n"
+            "Does not create a new object.\n "
+        )
+        self.flipx_button.setFixedWidth(60)
+
+        self.flipy_button = FCButton()
+        self.flipy_button.set_value("Flip on Y")
+        self.flipy_button.setToolTip(
+            "Flip the selected object(s) over the X axis.\n"
+            "Does not create a new object.\n "
+        )
+        self.flipy_button.setFixedWidth(60)
+
+        self.flip_ref_cb = FCCheckBox()
+        self.flip_ref_cb.set_value(True)
+        self.flip_ref_cb.setText("Ref Pt")
+        self.flip_ref_cb.setToolTip(
+            "Flip the selected object(s)\n"
+            "around the point in Point Entry Field.\n"
+            "\n"
+            "The point coordinates can be captured by\n"
+            "left click on canvas together with pressing\n"
+            "SHIFT key. \n"
+            "Then click Add button to insert coordinates.\n"
+            "Or enter the coords in format (x, y) in the\n"
+            "Point Entry field and click Flip on X(Y)")
+        self.flip_ref_cb.setFixedWidth(50)
+
+        self.flip_ref_label = QtWidgets.QLabel("Point:")
+        self.flip_ref_label.setToolTip(
+            "Coordinates in format (x, y) used as reference for mirroring.\n"
+            "The 'x' in (x, y) will be used when using Flip on X and\n"
+            "the 'y' in (x, y) will be used when using Flip on Y and"
+        )
+        self.flip_ref_label.setFixedWidth(50)
+        self.flip_ref_entry = EvalEntry2("(0, 0)")
+        self.flip_ref_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.flip_ref_entry.setFixedWidth(60)
+
+        self.flip_ref_button = FCButton()
+        self.flip_ref_button.set_value("Add")
+        self.flip_ref_button.setToolTip(
+            "The point coordinates can be captured by\n"
+            "left click on canvas together with pressing\n"
+            "SHIFT key. Then click Add button to insert.")
+        self.flip_ref_button.setFixedWidth(60)
+
+        form4_child.addRow(self.flipx_button, self.flipy_button)
+        form4_child_1.addRow(self.flip_ref_entry, self.flip_ref_button)
+
+        form4_layout.addRow(self.empty_label, form4_child)
+        form4_layout.addRow(self.flip_ref_cb)
+        form4_layout.addRow(self.flip_ref_label, form4_child_1)
+        self.ois_flip = OptionalInputSection(self.flip_ref_cb,
+                                              [self.flip_ref_entry, self.flip_ref_button], logic=True)
+
+        self.transform_lay.addStretch()
+
+        ## Signals
+        self.rotate_button.clicked.connect(self.on_rotate)
+        self.skewx_button.clicked.connect(self.on_skewx)
+        self.skewy_button.clicked.connect(self.on_skewy)
+        self.scalex_button.clicked.connect(self.on_scalex)
+        self.scaley_button.clicked.connect(self.on_scaley)
+        self.offx_button.clicked.connect(self.on_offx)
+        self.offy_button.clicked.connect(self.on_offy)
+        self.flipx_button.clicked.connect(self.on_flipx)
+        self.flipy_button.clicked.connect(self.on_flipy)
+        self.flip_ref_button.clicked.connect(self.on_flip_add_coords)
+
+        self.rotate_entry.returnPressed.connect(self.on_rotate)
+        self.skewx_entry.returnPressed.connect(self.on_skewx)
+        self.skewy_entry.returnPressed.connect(self.on_skewy)
+        self.scalex_entry.returnPressed.connect(self.on_scalex)
+        self.scaley_entry.returnPressed.connect(self.on_scaley)
+        self.offx_entry.returnPressed.connect(self.on_offx)
+        self.offy_entry.returnPressed.connect(self.on_offy)
+
+
+        ## Initialize form
+        self.rotate_entry.set_value('0')
+        self.skewx_entry.set_value('0')
+        self.skewy_entry.set_value('0')
+        self.scalex_entry.set_value('1')
+        self.scaley_entry.set_value('1')
+        self.offx_entry.set_value('0')
+        self.offy_entry.set_value('0')
+        self.flip_ref_cb.setChecked(False)
+
+    def run(self):
+        FlatCAMTool.run(self)
+        self.app.ui.notebook.setTabText(2, "Transform Tool")
+
+    def on_rotate(self):
+        try:
+            value = float(self.rotate_entry.get_value())
+        except Exception as e:
+            self.app.inform.emit("[error] Failed to rotate due of: %s" % str(e))
+            return
+        self.app.worker_task.emit({'fcn': self.on_rotate_action,
+                                       'params': [value]})
+        # self.on_rotate_action(value)
+        return
+
+    def on_flipx(self):
+        # self.on_flip("Y")
+        axis = 'Y'
+        self.app.worker_task.emit({'fcn': self.on_flip,
+                                   'params': [axis]})
+        return
+
+    def on_flipy(self):
+        # self.on_flip("X")
+        axis = 'X'
+        self.app.worker_task.emit({'fcn': self.on_flip,
+                                   'params': [axis]})
+        return
+
+    def on_flip_add_coords(self):
+        val = self.app.defaults["global_point_clipboard_format"] % (self.app.pos[0], self.app.pos[1])
+        self.flip_ref_entry.set_value(val)
+
+    def on_skewx(self):
+        try:
+            value = float(self.skewx_entry.get_value())
+        except:
+            self.app.inform.emit("[warning_notcl] No value for Skew!")
+            return
+        # self.on_skew("X", value)
+        axis = 'X'
+        self.app.worker_task.emit({'fcn': self.on_skew,
+                                   'params': [axis, value]})
+        return
+
+    def on_skewy(self):
+        try:
+            value = float(self.skewy_entry.get_value())
+        except:
+            self.app.inform.emit("[warning_notcl] No value for Skew!")
+            return
+        # self.on_skew("Y", value)
+        axis = 'Y'
+        self.app.worker_task.emit({'fcn': self.on_skew,
+                                   'params': [axis, value]})
+        return
+
+    def on_scalex(self):
+        try:
+            xvalue = float(self.scalex_entry.get_value())
+        except:
+            self.app.inform.emit("[warning_notcl] No value for Scale!")
+            return
+        # scaling to zero has no sense so we remove it, because scaling with 1 does nothing
+        if xvalue == 0:
+            xvalue = 1
+        if self.scale_link_cb.get_value():
+            yvalue = xvalue
+        else:
+            yvalue = 1
+
+        axis = 'X'
+        point = (0, 0)
+        if self.scale_zero_ref_cb.get_value():
+            self.app.worker_task.emit({'fcn': self.on_scale,
+                                       'params': [axis, xvalue, yvalue, point]})
+            # self.on_scale("X", xvalue, yvalue, point=(0,0))
+        else:
+            # self.on_scale("X", xvalue, yvalue)
+            self.app.worker_task.emit({'fcn': self.on_scale,
+                                       'params': [axis, xvalue, yvalue]})
+
+        return
+
+    def on_scaley(self):
+        xvalue = 1
+        try:
+            yvalue = float(self.scaley_entry.get_value())
+        except:
+            self.app.inform.emit("[warning_notcl] No value for Scale!")
+            return
+        # scaling to zero has no sense so we remove it, because scaling with 1 does nothing
+        if yvalue == 0:
+            yvalue = 1
+
+        axis = 'Y'
+        point = (0, 0)
+        if self.scale_zero_ref_cb.get_value():
+            self.app.worker_task.emit({'fcn': self.on_scale,
+                                       'params': [axis, xvalue, yvalue, point]})
+            # self.on_scale("Y", xvalue, yvalue, point=(0,0))
+        else:
+            # self.on_scale("Y", xvalue, yvalue)
+            self.app.worker_task.emit({'fcn': self.on_scale,
+                                       'params': [axis, xvalue, yvalue]})
+
+        return
+
+    def on_offx(self):
+        try:
+            value = float(self.offx_entry.get_value())
+        except:
+            self.app.inform.emit("[warning_notcl] No value for Offset!")
+            return
+        # self.on_offset("X", value)
+        axis = 'X'
+        self.app.worker_task.emit({'fcn': self.on_offset,
+                                   'params': [axis, value]})
+        return
+
+    def on_offy(self):
+        try:
+            value = float(self.offy_entry.get_value())
+        except:
+            self.app.inform.emit("[warning_notcl] No value for Offset!")
+            return
+        # self.on_offset("Y", value)
+        axis = 'Y'
+        self.app.worker_task.emit({'fcn': self.on_offset,
+                                   'params': [axis, value]})
+        return
+
+    def on_rotate_action(self, num):
+        obj_list = self.app.collection.get_selected()
+        xminlist = []
+        yminlist = []
+        xmaxlist = []
+        ymaxlist = []
+
+        if not obj_list:
+            self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to rotate!")
+            return
+        else:
+            with self.app.proc_container.new("Appying Rotate"):
+                try:
+                    # first get a bounding box to fit all
+                    for obj in obj_list:
+                        if isinstance(obj, FlatCAMCNCjob):
+                            pass
+                        else:
+                            xmin, ymin, xmax, ymax = obj.bounds()
+                            xminlist.append(xmin)
+                            yminlist.append(ymin)
+                            xmaxlist.append(xmax)
+                            ymaxlist.append(ymax)
+
+                    # get the minimum x,y and maximum x,y for all objects selected
+                    xminimal = min(xminlist)
+                    yminimal = min(yminlist)
+                    xmaximal = max(xmaxlist)
+                    ymaximal = max(ymaxlist)
+
+                    self.app.progress.emit(20)
+
+                    for sel_obj in obj_list:
+                        px = 0.5 * (xminimal + xmaximal)
+                        py = 0.5 * (yminimal + ymaximal)
+                        if isinstance(sel_obj, FlatCAMCNCjob):
+                            self.app.inform.emit("CNCJob objects can't be rotated.")
+                        else:
+                            sel_obj.rotate(-num, point=(px, py))
+                            sel_obj.plot()
+                            self.app.object_changed.emit(sel_obj)
+
+                        # add information to the object that it was changed and how much
+                        sel_obj.options['rotate'] = num
+
+                    self.app.inform.emit('Object(s) were rotated ...')
+                    self.app.progress.emit(100)
+
+                except Exception as e:
+                    self.app.inform.emit("[error_notcl] Due of %s, rotation movement was not executed." % str(e))
+                    return
+
+    def on_flip(self, axis):
+        obj_list = self.app.collection.get_selected()
+        xminlist = []
+        yminlist = []
+        xmaxlist = []
+        ymaxlist = []
+
+        if not obj_list:
+            self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to flip!")
+            return
+        else:
+            with self.app.proc_container.new("Applying Flip"):
+                try:
+                    # get mirroring coords from the point entry
+                    if self.flip_ref_cb.isChecked():
+                        px, py = eval('{}'.format(self.flip_ref_entry.text()))
+                    # get mirroing coords from the center of an all-enclosing bounding box
+                    else:
+                        # first get a bounding box to fit all
+                        for obj in obj_list:
+                            if isinstance(obj, FlatCAMCNCjob):
+                                pass
+                            else:
+                                xmin, ymin, xmax, ymax = obj.bounds()
+                                xminlist.append(xmin)
+                                yminlist.append(ymin)
+                                xmaxlist.append(xmax)
+                                ymaxlist.append(ymax)
+
+                        # get the minimum x,y and maximum x,y for all objects selected
+                        xminimal = min(xminlist)
+                        yminimal = min(yminlist)
+                        xmaximal = max(xmaxlist)
+                        ymaximal = max(ymaxlist)
+
+                        px = 0.5 * (xminimal + xmaximal)
+                        py = 0.5 * (yminimal + ymaximal)
+
+                    self.app.progress.emit(20)
+
+                    # execute mirroring
+                    for obj in obj_list:
+                        if isinstance(obj, FlatCAMCNCjob):
+                            self.app.inform.emit("CNCJob objects can't be mirrored/flipped.")
+                        else:
+                            if axis is 'X':
+                                obj.mirror('X', (px, py))
+                                # add information to the object that it was changed and how much
+                                # the axis is reversed because of the reference
+                                if 'mirror_y' in obj.options:
+                                    obj.options['mirror_y'] = not obj.options['mirror_y']
+                                else:
+                                    obj.options['mirror_y'] = True
+                                obj.plot()
+                                self.app.inform.emit('Flipped on the Y axis ...')
+                            elif axis is 'Y':
+                                obj.mirror('Y', (px, py))
+                                # add information to the object that it was changed and how much
+                                # the axis is reversed because of the reference
+                                if 'mirror_x' in obj.options:
+                                    obj.options['mirror_x'] = not obj.options['mirror_x']
+                                else:
+                                    obj.options['mirror_x'] = True
+                                obj.plot()
+                                self.app.inform.emit('Flipped on the X axis ...')
+                            self.app.object_changed.emit(obj)
+
+                    self.app.progress.emit(100)
+
+                except Exception as e:
+                    self.app.inform.emit("[error_notcl] Due of %s, Flip action was not executed." % str(e))
+                    return
+
+    def on_skew(self, axis, num):
+        obj_list = self.app.collection.get_selected()
+        xminlist = []
+        yminlist = []
+
+        if not obj_list:
+            self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to shear/skew!")
+            return
+        else:
+            with self.app.proc_container.new("Applying Skew"):
+                try:
+                    # first get a bounding box to fit all
+                    for obj in obj_list:
+                        if isinstance(obj, FlatCAMCNCjob):
+                            pass
+                        else:
+                            xmin, ymin, xmax, ymax = obj.bounds()
+                            xminlist.append(xmin)
+                            yminlist.append(ymin)
+
+                    # get the minimum x,y and maximum x,y for all objects selected
+                    xminimal = min(xminlist)
+                    yminimal = min(yminlist)
+
+                    self.app.progress.emit(20)
+
+                    for obj in obj_list:
+                        if isinstance(obj, FlatCAMCNCjob):
+                            self.app.inform.emit("CNCJob objects can't be skewed.")
+                        else:
+                            if axis is 'X':
+                                obj.skew(num, 0, point=(xminimal, yminimal))
+                                # add information to the object that it was changed and how much
+                                obj.options['skew_x'] = num
+                            elif axis is 'Y':
+                                obj.skew(0, num, point=(xminimal, yminimal))
+                                # add information to the object that it was changed and how much
+                                obj.options['skew_y'] = num
+                            obj.plot()
+                            self.app.object_changed.emit(obj)
+                    self.app.inform.emit('Object(s) were skewed on %s axis ...' % str(axis))
+                    self.app.progress.emit(100)
+
+                except Exception as e:
+                    self.app.inform.emit("[error_notcl] Due of %s, Skew action was not executed." % str(e))
+                    return
+
+    def on_scale(self, axis, xfactor, yfactor, point=None):
+        obj_list = self.app.collection.get_selected()
+        xminlist = []
+        yminlist = []
+        xmaxlist = []
+        ymaxlist = []
+
+        if not obj_list:
+            self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to scale!")
+            return
+        else:
+            with self.app.proc_container.new("Applying Scale"):
+                try:
+                    # first get a bounding box to fit all
+                    for obj in obj_list:
+                        if isinstance(obj, FlatCAMCNCjob):
+                            pass
+                        else:
+                            xmin, ymin, xmax, ymax = obj.bounds()
+                            xminlist.append(xmin)
+                            yminlist.append(ymin)
+                            xmaxlist.append(xmax)
+                            ymaxlist.append(ymax)
+
+                    # get the minimum x,y and maximum x,y for all objects selected
+                    xminimal = min(xminlist)
+                    yminimal = min(yminlist)
+                    xmaximal = max(xmaxlist)
+                    ymaximal = max(ymaxlist)
+
+                    self.app.progress.emit(20)
+
+                    if point is None:
+                        px = 0.5 * (xminimal + xmaximal)
+                        py = 0.5 * (yminimal + ymaximal)
+                    else:
+                        px = 0
+                        py = 0
+
+                    for obj in obj_list:
+                        if isinstance(obj, FlatCAMCNCjob):
+                            self.app.inform.emit("CNCJob objects can't be scaled.")
+                        else:
+                            obj.scale(xfactor, yfactor, point=(px, py))
+                            # add information to the object that it was changed and how much
+                            obj.options['scale_x'] = xfactor
+                            obj.options['scale_y'] = yfactor
+                            obj.plot()
+                            self.app.object_changed.emit(obj)
+                    self.app.inform.emit('Object(s) were scaled on %s axis ...' % str(axis))
+                    self.app.progress.emit(100)
+                except Exception as e:
+                    self.app.inform.emit("[error_notcl] Due of %s, Scale action was not executed." % str(e))
+                    return
+
+    def on_offset(self, axis, num):
+        obj_list = self.app.collection.get_selected()
+        xminlist = []
+        yminlist = []
+
+        if not obj_list:
+            self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to offset!")
+            return
+        else:
+            with self.app.proc_container.new("Applying Offset"):
+                try:
+                    # first get a bounding box to fit all
+                    for obj in obj_list:
+                        if isinstance(obj, FlatCAMCNCjob):
+                            pass
+                        else:
+                            xmin, ymin, xmax, ymax = obj.bounds()
+                            xminlist.append(xmin)
+                            yminlist.append(ymin)
+
+                    # get the minimum x,y and maximum x,y for all objects selected
+                    xminimal = min(xminlist)
+                    yminimal = min(yminlist)
+                    self.app.progress.emit(20)
+
+                    for obj in obj_list:
+                        if isinstance(obj, FlatCAMCNCjob):
+                            self.app.inform.emit("CNCJob objects can't be offseted.")
+                        else:
+                            if axis is 'X':
+                                obj.offset((num, 0))
+                                # add information to the object that it was changed and how much
+                                obj.options['offset_x'] = num
+                            elif axis is 'Y':
+                                obj.offset((0, num))
+                                # add information to the object that it was changed and how much
+                                obj.options['offset_y'] = num
+                            obj.plot()
+                            self.app.object_changed.emit(obj)
+                    self.app.inform.emit('Object(s) were offseted on %s axis ...' % str(axis))
+                    self.app.progress.emit(100)
+
+                except Exception as e:
+                    self.app.inform.emit("[error_notcl] Due of %s, Offset action was not executed." % str(e))
+                    return
+
+# end of file

+ 16 - 0
flatcamTools/__init__.py

@@ -0,0 +1,16 @@
+import sys
+
+from flatcamTools.ToolMeasurement import Measurement
+from flatcamTools.ToolPanelize import Panelize
+from flatcamTools.ToolFilm import Film
+from flatcamTools.ToolMove import ToolMove
+from flatcamTools.ToolDblSided import DblSidedTool
+from flatcamTools.ToolCutout import ToolCutout
+from flatcamTools.ToolCalculators import ToolCalculator
+from flatcamTools.ToolProperties import Properties
+from flatcamTools.ToolImage import ToolImage
+from flatcamTools.ToolPaint import ToolPaint
+from flatcamTools.ToolNonCopperClear import NonCopperClear
+from flatcamTools.ToolTransform import ToolTransform
+
+from flatcamTools.ToolShell import FCShell

+ 91 - 0
make_win.py

@@ -0,0 +1,91 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 12/20/2018                                           #
+# MIT Licence                                              #
+#                                                          #
+# Creates a portable copy of FlatCAM, including Python    #
+# itself and all dependencies.                             #
+#                                                          #
+# This is not an aid to install FlatCAM from source on     #
+# Windows platforms. It is only useful when FlatCAM is up  #
+# and running and ready to be packaged.                    #
+############################################################
+
+# Files not needed: Qt, tk.dll, tcl.dll, tk/, tcl/, vtk/,
+#   scipy.lib.lapack.flapack.pyd, scipy.lib.blas.fblas.pyd,
+#   numpy.core._dotblas.pyd, scipy.sparse.sparsetools._bsr.pyd,
+#   scipy.sparse.sparsetools._csr.pyd, scipy.sparse.sparsetools._csc.pyd,
+#   scipy.sparse.sparsetools._coo.pyd
+
+import os, site, sys, platform
+from cx_Freeze import setup, Executable
+
+# this is done to solve the tkinter not being found
+PYTHON_INSTALL_DIR = os.path.dirname(os.path.dirname(os.__file__))
+os.environ['TCL_LIBRARY'] = os.path.join(PYTHON_INSTALL_DIR, 'tcl', 'tcl8.6')
+os.environ['TK_LIBRARY'] = os.path.join(PYTHON_INSTALL_DIR, 'tcl', 'tk8.6')
+
+# Get the site-package folder, not everybody will install
+# Python into C:\PythonXX
+site_dir = site.getsitepackages()[1]
+
+include_files = []
+
+include_files.append((os.path.join(site_dir, "shapely"), "shapely"))
+include_files.append((os.path.join(site_dir, "svg"), "svg"))
+include_files.append((os.path.join(site_dir, "svg/path"), "svg"))
+include_files.append((os.path.join(site_dir, "vispy"), "vispy"))
+include_files.append((os.path.join(site_dir, "vispy/app"), "vispy/app"))
+include_files.append((os.path.join(site_dir, "vispy/app/backends"), "vispy/app/backends"))
+include_files.append((os.path.join(site_dir, "rtree"), "rtree"))
+
+if platform.architecture()[0] == '64bit':
+    include_files.append((os.path.join(site_dir, "google"), "google"))
+    include_files.append((os.path.join(site_dir, "google/protobuf"), "google/protobuf"))
+    include_files.append((os.path.join(site_dir, "ortools"), "ortools"))
+
+include_files.append(("share", "lib/share"))
+include_files.append(("postprocessors", "lib/postprocessors"))
+
+include_files.append(("README.md", "README.md"))
+include_files.append(("LICENSE", "LICENSE"))
+
+base = None
+
+# Lets not open the console while running the app
+if sys.platform == "win32":
+    base = "Win32GUI"
+
+if platform.architecture()[0] == '64bit':
+    buildOptions = dict(
+        include_files=include_files,
+        excludes=['scipy','pytz'],
+        # packages=['OpenGL','numpy','vispy','ortools','google']
+        packages=['numpy','google', 'rasterio'] # works for Python 3.7
+        # packages = ['opengl', 'numpy', 'google', 'rasterio'] # works for Python 3.6.5
+
+    )
+else:
+    buildOptions = dict(
+        include_files=include_files,
+        excludes=['scipy', 'pytz'],
+        # packages=['OpenGL','numpy','vispy','ortools','google']
+        packages=['numpy', 'rasterio']  # works for Python 3.7
+        # packages = ['opengl', 'numpy', 'google', 'rasterio'] # works for Python 3.6.5
+
+    )
+
+print("INCLUDE_FILES", include_files)
+
+#execfile('clean.py')
+
+setup(
+    name="FlatCAM",
+    author="Juan Pablo Caram",
+    version="3000",
+    description="FlatCAM: 2D Computer Aided PCB Manufacturing",
+    options=dict(build_exe=buildOptions),
+    executables=[Executable("FlatCAM.py", icon='share/flatcam_icon48.ico', base=base)]
+)

+ 119 - 0
postprocessors/Roland_MDX_20.py

@@ -0,0 +1,119 @@
+from FlatCAMPostProc import *
+
+
+# for Roland Postprocessors it is mandatory for the postprocessor name (python file and class name, both of them must be
+# the same) to contain the following keyword, case-sensitive: 'Roland' without the quotes.
+class Roland_MDX_20(FlatCAMPostProc):
+
+    coordinate_format = "%.1f"
+    feedrate_format = '%.1f'
+    feedrate_rapid_format = '%.1f'
+
+    def start_code(self, p):
+        gcode = ';;^IN;' + '\n'
+        gcode += '^PA;'
+        return gcode
+
+    def startz_code(self, p):
+        return ''
+
+    def lift_code(self, p):
+        if p.units.upper() == 'IN':
+            z = p.z_move / 25.4
+        else:
+            z = p.z_move
+        gcode = self.feedrate_rapid_code(p) + '\n'
+        gcode += self.position_code(p).format(**p) + ',' + str(float(z * 40.0)) + ';'
+        return gcode
+
+    def down_code(self, p):
+        if p.units.upper() == 'IN':
+            z = p.z_cut / 25.4
+        else:
+            z = p.z_cut
+        gcode = self.feedrate_code(p) + '\n'
+        gcode += self.position_code(p).format(**p) + ',' + str(float(z * 40.0)) + ';'
+        return gcode
+
+    def toolchange_code(self, p):
+        return ''
+
+    def up_to_zero_code(self, p):
+        gcode = self.feedrate_code(p) + '\n'
+        gcode += self.position_code(p).format(**p) + ',' + '0' + ';'
+        return gcode
+
+    def position_code(self, p):
+        if p.units.upper() == 'IN':
+            x = p.x / 25.4
+            y = p.y / 25.4
+        else:
+            x = p.x
+            y = p.y
+        return ('Z' + self.coordinate_format + ',' + self.coordinate_format) % (float(x * 40.0), float(y * 40.0))
+
+    def rapid_code(self, p):
+        if p.units.upper() == 'IN':
+            z = p.z_move / 25.4
+        else:
+            z = p.z_move
+        gcode = self.feedrate_rapid_code(p) + '\n'
+        gcode += self.position_code(p).format(**p) + ',' + str(float(z * 40.0)) + ';'
+        return gcode
+
+    def linear_code(self, p):
+        if p.units.upper() == 'IN':
+            z = p.z / 25.4
+        else:
+            z = p.z
+        gcode = self.feedrate_code(p) + '\n'
+        gcode += self.position_code(p).format(**p) + ',' + str(float(z * 40.0)) + ';'
+        return gcode
+
+    def end_code(self, p):
+        if p.units.upper() == 'IN':
+            z = p.endz / 25.4
+        else:
+            z = p.endz
+        gcode = self.feedrate_rapid_code(p) + '\n'
+        gcode += self.position_code(p).format(**p) + ',' + str(float(z * 40.0)) + ';'
+        return gcode
+
+    def feedrate_code(self, p):
+        fr_sec = p.feedrate / 60
+
+        # valid feedrate for MDX20 is between 0.1mm/sec and 15mm/sec (6mm/min to 900mm/min)
+        if p.feedrate >= 900:
+            fr_sec = 15
+        if p.feedrate < 6:
+            fr_sec = 6
+        return 'V' + str(self.feedrate_format % fr_sec) + ';'
+
+    def feedrate_z_code(self, p):
+        fr_sec = p.feedrate_z / 60
+
+        # valid feedrate for MDX20 is between 0.1mm/sec and 15mm/sec (6mm/min to 900mm/min)
+        if p.feedrate_z >= 900:
+            fr_sec = 15
+        if p.feedrate_z < 6:
+            fr_sec = 6
+        return 'V' + str(self.feedrate_format % fr_sec) + ';'
+
+    def feedrate_rapid_code(self, p):
+        fr_sec = p.feedrate_rapid / 60
+
+        # valid feedrate for MDX20 is between 0.1mm/sec and 15mm/sec (6mm/min to 900mm/min)
+        if p.feedrate_rapid >= 900:
+            fr_sec = 15
+        if p.feedrate_rapid < 6:
+            fr_sec = 6
+        return 'V' + str(self.feedrate_format % fr_sec) + ';'
+
+    def spindle_code(self, p):
+        return '!MC1'
+
+    def dwell_code(self, p):
+        return''
+
+    def spindle_stop_code(self,p):
+        return '!MC0'

+ 0 - 0
postprocessors/__init__.py


+ 137 - 0
postprocessors/default.py

@@ -0,0 +1,137 @@
+from FlatCAMPostProc import *
+
+
+class default(FlatCAMPostProc):
+
+    coordinate_format = "%.*f"
+    feedrate_format = '%.*f'
+
+    def start_code(self, p):
+        units = ' ' + str(p['units']).lower()
+        coords_xy = p['toolchange_xy']
+        gcode = ''
+
+        if str(p['options']['type']) == 'Geometry':
+            gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n'
+
+        gcode += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n'
+
+        if str(p['options']['type']) == 'Geometry':
+            gcode += '(Feedrate_Z: ' + str(p['feedrate_z']) + units + '/min' + ')\n'
+
+        gcode += '(Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n'
+        gcode += '(Z_Cut: ' + str(p['z_cut']) + units + ')\n'
+
+        if str(p['options']['type']) == 'Geometry':
+            if p['multidepth'] is True:
+                gcode += '(DepthPerCut: ' + str(p['depthpercut']) + units + ' <=>' + \
+                         str(math.ceil(abs(p['z_cut']) / p['depthpercut'])) + ' passes' + ')\n'
+
+        gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
+        gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n'
+        gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
+        gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
+        gcode += '(Z End: ' + str(p['endz']) + units + ')\n'
+        gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
+
+        if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
+            gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
+        else:
+            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n'
+
+        gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed'])
+
+        gcode += ('G20\n' if p.units.upper() == 'IN' else 'G21\n')
+        gcode += 'G90\n'
+        gcode += 'G94\n'
+
+        return gcode
+
+    def startz_code(self, p):
+        if p.startz is not None:
+            return 'G00 Z' + self.coordinate_format%(p.coords_decimals, p.startz)
+        else:
+            return ''
+
+    def lift_code(self, p):
+        return 'G00 Z' + self.coordinate_format%(p.coords_decimals, p.z_move)
+
+    def down_code(self, p):
+        return 'G01 Z' + self.coordinate_format%(p.coords_decimals, p.z_cut)
+
+    def toolchange_code(self, p):
+        toolchangez = p.toolchangez
+        toolchangexy = p.toolchange_xy
+        toolchangex = toolchangexy[0]
+        toolchangey = toolchangexy[1]
+
+        no_drills = 1
+
+        if int(p.tool) == 1 and p.startz is not None:
+            toolchangez = p.startz
+
+        if p.units.upper() == 'MM':
+            toolC_formatted = format(p.toolC, '.2f')
+        else:
+            toolC_formatted = format(p.toolC, '.4f')
+
+        if str(p['options']['type']) == 'Excellon':
+            for i in p['options']['Tools_in_use']:
+                if i[0] == p.tool:
+                    no_drills = i[2]
+            return """G00 Z{toolchangez}
+T{tool}
+M5
+M6
+(MSG, Change to Tool Dia = {toolC}, Total drills for current tool = {t_drills})
+M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
+             tool=int(p.tool),
+             t_drills=no_drills,
+             toolC=toolC_formatted)
+        else:
+            return """G00 Z{toolchangez}
+T{tool}
+M5
+M6    
+(MSG, Change to Tool Dia = {toolC})
+M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
+             tool=int(p.tool),
+             toolC=toolC_formatted)
+
+    def up_to_zero_code(self, p):
+        return 'G01 Z0'
+
+    def position_code(self, p):
+        return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \
+               (p.coords_decimals, p.x, p.coords_decimals, p.y)
+
+    def rapid_code(self, p):
+        return ('G00 ' + self.position_code(p)).format(**p)
+
+    def linear_code(self, p):
+        return ('G01 ' + self.position_code(p)).format(**p)
+
+    def end_code(self, p):
+        coords_xy = p['toolchange_xy']
+        gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n")
+        gcode += 'G00 X{x}Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n"
+        return gcode
+
+    def feedrate_code(self, p):
+        return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
+
+    def feedrate_z_code(self, p):
+        return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate_z))
+
+    def spindle_code(self, p):
+        if p.spindlespeed:
+            return 'M03 S' + str(p.spindlespeed)
+        else:
+            return 'M03'
+
+    def dwell_code(self, p):
+        if p.dwelltime:
+            return 'G4 P' + str(p.dwelltime)
+
+    def spindle_stop_code(self,p):
+        return 'M05'

+ 134 - 0
postprocessors/grbl_11.py

@@ -0,0 +1,134 @@
+from FlatCAMPostProc import *
+
+
+class grbl_11(FlatCAMPostProc):
+
+    coordinate_format = "%.*f"
+    feedrate_format = '%.*f'
+
+    def start_code(self, p):
+        units = ' ' + str(p['units']).lower()
+        coords_xy = p['toolchange_xy']
+        gcode = ''
+
+        if str(p['options']['type']) == 'Geometry':
+            gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n' + '\n'
+
+        gcode += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n'
+
+        if str(p['options']['type']) == 'Geometry':
+            gcode += '(Feedrate_Z: ' + str(p['feedrate_z']) + units + '/min' + ')\n'
+
+        gcode += '(Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n'
+        gcode += '(Z_Cut: ' + str(p['z_cut']) + units + ')\n'
+
+        if str(p['options']['type']) == 'Geometry':
+            if p['multidepth'] is True:
+                gcode += '(DepthPerCut: ' + str(p['depthpercut']) + units + ' <=>' + \
+                         str(math.ceil(abs(p['z_cut']) / p['depthpercut'])) + ' passes' + ')\n'
+
+        gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
+        gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n'
+        gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
+        gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
+        gcode += '(Z End: ' + str(p['endz']) + units + ')\n'
+        gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
+
+        if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
+            gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
+        else:
+            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n'
+
+        gcode += '(Spindle Speed: ' + str(p['spindlespeed']) + ' RPM' + ')\n' + '\n'
+
+        gcode += ('G20' if p.units.upper() == 'IN' else 'G21') + "\n"
+        gcode += 'G90\n'
+        gcode += 'G94\n'
+        gcode += 'G17\n'
+
+        return gcode
+
+    def startz_code(self, p):
+        if p.startz is not None:
+            return 'G00 Z' + self.coordinate_format%(p.coords_decimals, p.startz)
+        else:
+            return ''
+
+    def lift_code(self, p):
+        return 'G00 Z' + self.coordinate_format%(p.coords_decimals, p.z_move)
+
+    def down_code(self, p):
+        return 'G01 Z' + self.coordinate_format%(p.coords_decimals, p.z_cut)
+
+    def toolchange_code(self, p):
+        toolchangez = p.toolchangez
+
+        if int(p.tool) == 1 and p.startz is not None:
+            toolchangez = p.startz
+
+        if p.units.upper() == 'MM':
+            toolC_formatted = format(p.toolC, '.2f')
+        else:
+            toolC_formatted = format(p.toolC, '.4f')
+
+        for i in p['options']['Tools_in_use']:
+            if i[0] == p.tool:
+                no_drills = i[2]
+
+        if str(p['options']['type']) == 'Excellon':
+            return """G00 Z{toolchangez}
+T{tool}
+M5
+M6
+(MSG, Change to Tool Dia = {toolC}, Total drills for current tool = {t_drills})
+M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
+             tool=int(p.tool),
+             t_drills=no_drills,
+             toolC=toolC_formatted)
+        else:
+            return """G00 Z{toolchangez}
+T{tool}
+M5
+M6    
+(MSG, Change to Tool Dia = {toolC})
+M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
+             tool=int(p.tool),
+             toolC=toolC_formatted)
+
+    def up_to_zero_code(self, p):
+        return 'G01 Z0'
+
+    def position_code(self, p):
+        return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \
+               (p.coords_decimals, p.x, p.coords_decimals, p.y)
+
+    def rapid_code(self, p):
+        return ('G00 ' + self.position_code(p)).format(**p)
+
+    def linear_code(self, p):
+        return ('G01 ' + self.position_code(p)).format(**p) + " " + self.feedrate_code(p)
+
+    def end_code(self, p):
+        coords_xy = p['toolchange_xy']
+        gcode = ('G00 Z' + self.feedrate_format % (p.fr_decimals, p.endz) + "\n")
+        gcode += 'G00 X{x}Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n"
+        return gcode
+
+    def feedrate_code(self, p):
+        return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
+
+    def feedrate_z_code(self, p):
+        return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate_z))
+
+    def spindle_code(self,p):
+        if p.spindlespeed:
+            return 'M03 S%d' % p.spindlespeed
+        else:
+            return 'M03'
+
+    def dwell_code(self, p):
+        if p.dwelltime:
+            return 'G4 P' + str(p.dwelltime)
+
+    def spindle_stop_code(self,p):
+        return 'M05'

+ 74 - 0
postprocessors/grbl_laser.py

@@ -0,0 +1,74 @@
+from FlatCAMPostProc import *
+
+# This post processor is configured to output code that
+# is compatible with almost any version of Grbl.
+
+
+class grbl_laser(FlatCAMPostProc):
+
+    coordinate_format = "%.*f"
+    feedrate_format = '%.*f'
+
+    def start_code(self, p):
+        units = ' ' + str(p['units']).lower()
+        gcode = ''
+
+        gcode += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n'
+        gcode += '(Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n'
+
+        gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
+
+        if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
+            gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
+        else:
+            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n'
+
+        gcode += ('G20' if p.units.upper() == 'IN' else 'G21') + "\n"
+        gcode += 'G90\n'
+        gcode += 'G94\n'
+        gcode += 'G17\n'
+
+        return gcode
+
+    def startz_code(self, p):
+        return ''
+
+    def lift_code(self, p):
+        return 'M05'
+
+    def down_code(self, p):
+        return 'M03'
+
+    def toolchange_code(self, p):
+        return ''
+
+    def up_to_zero_code(self, p):
+        return 'M05'
+
+    def position_code(self, p):
+        return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \
+               (p.coords_decimals, p.x, p.coords_decimals, p.y)
+
+    def rapid_code(self, p):
+        return ('G00 ' + self.position_code(p)).format(**p)
+
+    def linear_code(self, p):
+        return ('G01 ' + self.position_code(p)).format(**p) + " " + self.feedrate_code(p)
+
+    def end_code(self, p):
+        gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n")
+        gcode += 'G00 X0Y0'
+        return gcode
+
+    def feedrate_code(self, p):
+        return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
+
+    def spindle_code(self,p):
+        if p.spindlespeed:
+            return 'S%d' % p.spindlespeed
+
+    def dwell_code(self, p):
+        return ''
+
+    def spindle_stop_code(self,p):
+        return 'M05'

+ 152 - 0
postprocessors/manual_toolchange.py

@@ -0,0 +1,152 @@
+from FlatCAMPostProc import *
+
+
+class manual_toolchange(FlatCAMPostProc):
+
+    coordinate_format = "%.*f"
+    feedrate_format = '%.*f'
+
+    def start_code(self, p):
+        units = ' ' + str(p['units']).lower()
+        coords_xy = p['toolchange_xy']
+        gcode = ''
+
+        if str(p['options']['type']) == 'Geometry':
+            gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n'
+
+        gcode += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n'
+
+        if str(p['options']['type']) == 'Geometry':
+            gcode += '(Feedrate_Z: ' + str(p['feedrate_z']) + units + '/min' + ')\n'
+
+        gcode += '(Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n'
+        gcode += '(Z_Cut: ' + str(p['z_cut']) + units + ')\n'
+
+        if str(p['options']['type']) == 'Geometry':
+            if p['multidepth'] is True:
+                gcode += '(DepthPerCut: ' + str(p['depthpercut']) + units + ' <=>' + \
+                         str(math.ceil(abs(p['z_cut']) / p['depthpercut'])) + ' passes' + ')\n'
+
+        gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
+        gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n'
+        gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
+        gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
+        gcode += '(Z End: ' + str(p['endz']) + units + ')\n'
+        gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
+
+        if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
+            gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
+        else:
+            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n'
+
+        gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed'])
+
+        gcode += ('G20\n' if p.units.upper() == 'IN' else 'G21\n')
+        gcode += 'G90\n'
+        gcode += 'G94\n'
+
+        return gcode
+
+    def startz_code(self, p):
+        if p.startz is not None:
+            return 'G00 Z' + self.coordinate_format % (p.coords_decimals, p.startz)
+        else:
+            return ''
+
+    def lift_code(self, p):
+        return 'G00 Z' + self.coordinate_format % (p.coords_decimals, p.z_move)
+
+    def down_code(self, p):
+        return 'G01 Z' + self.coordinate_format % (p.coords_decimals, p.z_cut)
+
+    def toolchange_code(self, p):
+        toolchangez = p.toolchangez
+        toolchangexy = p.toolchange_xy
+        toolchangex = toolchangexy[0]
+        toolchangey = toolchangexy[1]
+
+        no_drills = 1
+
+        if int(p.tool) == 1 and p.startz is not None:
+            toolchangez = p.startz
+
+        if p.units.upper() == 'MM':
+            toolC_formatted = format(p.toolC, '.2f')
+        else:
+            toolC_formatted = format(p.toolC, '.4f')
+
+        for i in p['options']['Tools_in_use']:
+            if i[0] == p.tool:
+                no_drills = i[2]
+
+        if str(p['options']['type']) == 'Excellon':
+            return """G00 Z{toolchangez}
+T{tool}
+M5
+G00 X{toolchangex}Y{toolchangey}    
+(MSG, Change to Tool Dia = {toolC}, Total drills for current tool = {t_drills})
+M0
+G01 Z0
+M0
+G00 Z{toolchangez}
+M0
+""".format(toolchangex=self.coordinate_format%(p.coords_decimals, toolchangex),
+           toolchangey=self.coordinate_format%(p.coords_decimals, toolchangey),
+           toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
+           tool=int(p.tool),
+           t_drills=no_drills,
+           toolC=toolC_formatted)
+        else:
+            return """G00 Z{toolchangez}
+T{tool}
+M5
+G00 X{toolchangex}Y{toolchangey}    
+(MSG, Change to Tool Dia = {toolC})
+M0
+G01 Z0
+M0
+G00 Z{toolchangez}
+M0
+""".format(toolchangex=self.coordinate_format%(p.coords_decimals, toolchangex),
+           toolchangey=self.coordinate_format%(p.coords_decimals, toolchangey),
+           toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
+           tool=int(p.tool),
+           toolC=toolC_formatted)
+
+    def up_to_zero_code(self, p):
+        return 'G01 Z0'
+
+    def position_code(self, p):
+        return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \
+               (p.coords_decimals, p.x, p.coords_decimals, p.y)
+
+    def rapid_code(self, p):
+        return ('G00 ' + self.position_code(p)).format(**p)
+
+    def linear_code(self, p):
+        return ('G01 ' + self.position_code(p)).format(**p)
+
+    def end_code(self, p):
+        coords_xy = p['toolchange_xy']
+        gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n")
+        gcode += 'G00 X{x}Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n"
+        return gcode
+
+    def feedrate_code(self, p):
+        return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
+
+    def feedrate_z_code(self, p):
+        return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate_z))
+
+    def spindle_code(self,p):
+        if p.spindlespeed:
+            return 'M03 S' + str(p.spindlespeed)
+        else:
+            return 'M03'
+
+    def dwell_code(self, p):
+        if p.dwelltime:
+            return 'G4 P' + str(p.dwelltime)
+
+    def spindle_stop_code(self,p):
+        return 'M05'

+ 135 - 0
postprocessors/marlin.py

@@ -0,0 +1,135 @@
+from FlatCAMPostProc import *
+
+
+class marlin(FlatCAMPostProc):
+
+    coordinate_format = "%.*f"
+    feedrate_format = '%.*f'
+    feedrate_rapid_format = feedrate_format
+
+    def start_code(self, p):
+        units = ' ' + str(p['units']).lower()
+        gcode = ''
+
+        if str(p['options']['type']) == 'Geometry':
+            gcode += ';TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + '\n' + '\n'
+
+        gcode += ';Feedrate: ' + str(p['feedrate']) + units + '/min' + '\n'
+
+        if str(p['options']['type']) == 'Geometry':
+            gcode += ';Feedrate_Z: ' + str(p['feedrate_z']) + units + '/min' + '\n'
+
+        gcode += ';Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + '\n' + '\n'
+        gcode += ';Z_Cut: ' + str(p['z_cut']) + units + '\n'
+
+        if str(p['options']['type']) == 'Geometry':
+            if p['multidepth'] is True:
+                gcode += ';DepthPerCut: ' + str(p['depthpercut']) + units + ' <=>' + \
+                         str(math.ceil(abs(p['z_cut']) / p['depthpercut'])) + ' passes' + '\n'
+
+        gcode += ';Z_Move: ' + str(p['z_move']) + units + '\n'
+        gcode += ';Z Toolchange: ' + str(p['toolchangez']) + units + '\n'
+        gcode += ';Z Start: ' + str(p['startz']) + units + '\n'
+        gcode += ';Z End: ' + str(p['endz']) + units + '\n'
+        gcode += ';Steps per circle: ' + str(p['steps_per_circle']) + '\n'
+
+        if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
+            gcode += ';Postprocessor Excellon: ' + str(p['pp_excellon_name']) + '\n'
+        else:
+            gcode += ';Postprocessor Geometry: ' + str(p['pp_geometry_name']) + '\n'
+
+        gcode += ';Spindle Speed: ' + str(p['spindlespeed']) + ' RPM' + '\n' + '\n'
+
+        gcode += ('G20' if p.units.upper() == 'IN' else 'G21') + "\n"
+        gcode += 'G90\n'
+
+        return gcode
+
+    def startz_code(self, p):
+        if p.startz is not None:
+            return 'G0 Z' + self.coordinate_format % (p.coords_decimals, p.startz)
+        else:
+            return ''
+
+    def lift_code(self, p):
+        return 'G0 Z' + self.coordinate_format%(p.coords_decimals, p.z_move) + " " + self.feedrate_rapid_code(p)
+
+    def down_code(self, p):
+        return 'G1 Z' + self.coordinate_format%(p.coords_decimals, p.z_cut) + " " + self.end_feedrate_code(p)
+
+    def toolchange_code(self, p):
+        toolchangez = p.toolchangez
+        no_drills = 1
+
+        if int(p.tool) == 1 and p.startz is not None:
+            toolchangez = p.startz
+
+        if p.units.upper() == 'MM':
+            toolC_formatted = format(p.toolC, '.2f')
+        else:
+            toolC_formatted = format(p.toolC, '.4f')
+
+        for i in p['options']['Tools_in_use']:
+            if i[0] == p.tool:
+                no_drills = i[2]
+
+        if str(p['options']['type']) == 'Excellon':
+            return """G0 Z{toolchangez}
+M5
+M0 Change to Tool Dia = {toolC}, Total drills for current tool = {t_drills}
+""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
+           tool=int(p.tool),
+           t_drills=no_drills,
+           toolC=toolC_formatted)
+        else:
+            return """G0 Z{toolchangez}
+M5
+M0 Change to Tool Dia = {toolC}
+""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
+           tool=int(p.tool),
+           toolC=toolC_formatted)
+
+    def up_to_zero_code(self, p):
+        return 'G1 Z0' + " " + self.feedrate_code(p)
+
+    def position_code(self, p):
+        return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \
+               (p.coords_decimals, p.x, p.coords_decimals, p.y)
+
+    def rapid_code(self, p):
+        return ('G0 ' + self.position_code(p)).format(**p) + " " + self.feedrate_rapid_code(p)
+
+    def linear_code(self, p):
+        return ('G1 ' + self.position_code(p)).format(**p) + " " + self.end_feedrate_code(p)
+
+    def end_code(self, p):
+        coords_xy = p['toolchange_xy']
+        gcode = ('G0 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + " " + self.feedrate_rapid_code(p) + "\n")
+        gcode += 'G0 X{x}Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + " " + self.feedrate_rapid_code(p) + "\n"
+
+        return gcode
+
+    def feedrate_code(self, p):
+        return 'G1 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
+
+    def end_feedrate_code(self, p):
+        return 'F' + self.feedrate_format %(p.fr_decimals, p.feedrate)
+
+    def feedrate_z_code(self, p):
+        return 'G1 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate_z))
+
+    def feedrate_rapid_code(self, p):
+        return 'F' + self.feedrate_rapid_format % (p.fr_decimals, p.feedrate_rapid)
+
+    def spindle_code(self,p):
+        if p.spindlespeed:
+            return 'M3 S%d' % p.spindlespeed
+        else:
+            return 'M3'
+
+    def dwell_code(self, p):
+        if p.dwelltime:
+            return 'G4 P' + str(p.dwelltime)
+
+    def spindle_stop_code(self,p):
+        return 'M5'

+ 18 - 0
requirements.txt

@@ -0,0 +1,18 @@
+# This file contains python only requirements to be installed with pip
+# Python pacakges that cannot be installed with pip (e.g. PyQt5, GDAL) are not included.
+# Usage: pip install -r requirements.txt
+numpy>=1.8
+simplejson
+rtree
+pyopengl
+pyopengl-accelerate
+vispy
+ortools
+svg.path
+simplejson
+shapely>=1.3
+freetype-py
+fontTools
+rasterio
+lxml
+ezdxf

+ 31 - 0
setup_ubuntu.sh

@@ -0,0 +1,31 @@
+#!/bin/sh
+apt-get install python3-pip
+apt-get install python3-pyqt5
+apt-get install python3-pyqt5.qtopengl
+apt-get install libpng-dev
+apt-get install libfreetype6 libfreetype6-dev
+apt-get install python3-dev
+apt-get install python3-simplejson
+apt-get install python3-numpy python3-scipy
+apt-get install libgeos-dev
+apt-get install python3-shapely
+apt-get install python3-rtree
+apt-get install python3-tk
+apt-get install libspatialindex-dev
+apt-get install python3-gdal
+apt-get install python3-lxml
+apt-get install python3-ezdxf
+easy_install3 -U distribute
+pip3 install --upgrade Shapely
+pip3 install --upgrade vispy
+pip3 install --upgrade rtree
+pip3 install --upgrade pyopengl
+pip3 install --upgrade pyopengl-accelerate
+pip3 install --upgrade setuptools
+pip3 install --upgrade svg.path
+pip3 install --upgrade ortools
+pip3 install --upgrade freetype-py
+pip3 install --upgrade fontTools
+pip3 install --upgrade rasterio
+pip3 install --upgrade lxml
+pip3 install --upgrade ezdxf


BIN
share/activity2.gif


BIN
share/addarray16.png


BIN
share/addarray20.png


BIN
share/addarray32.png






BIN
share/blocked16.png



BIN
share/buffer16-2.png


BIN
share/buffer16.png


BIN
share/buffer20.png


BIN
share/buffer24.png



BIN
share/calculator24.png


BIN
share/cancel_edit16.png


BIN
share/cancel_edit32.png


BIN
share/circle32.png


BIN
share/clear_plot16.png


BIN
share/clear_plot32.png





BIN
share/convert24.png





BIN
share/copy_geo.png


BIN
share/corner32.png




BIN
share/cutpath16.png


BIN
share/cutpath24.png


BIN
share/cutpath32.png


BIN
share/defaults.png


BIN
share/delete32.png


BIN
share/deleteshape16.png


BIN
share/deleteshape24.png


BIN
share/deleteshape32.png


BIN
share/doubleside16.png


BIN
share/doubleside32.png



BIN
share/drill16.png


BIN
share/drill32.png





Неке датотеке нису приказане због велике количине промена