소스 검색

Merged in marius_stanciu/flatcam_beta/Beta (pull request #237)

Beta
Marius Stanciu 6 년 전
부모
커밋
8ce5050cdd
100개의 변경된 파일13250개의 추가작업 그리고 5790개의 파일을 삭제
  1. 2 0
      FlatCAM.py
  2. 361 107
      FlatCAMApp.py
  3. 778 104
      FlatCAMObj.py
  4. 2 2
      FlatCAMPool.py
  5. 2 2
      FlatCAMTranslation.py
  6. 58 16
      ObjectCollection.py
  7. 194 1
      README.md
  8. 210 1829
      camlib.py
  9. 27 28
      flatcamEditors/FlatCAMExcEditor.py
  10. 110 24
      flatcamEditors/FlatCAMGeoEditor.py
  11. 111 56
      flatcamEditors/FlatCAMGrbEditor.py
  12. 274 0
      flatcamEditors/FlatCAMTextEditor.py
  13. 288 209
      flatcamGUI/FlatCAMGUI.py
  14. 328 98
      flatcamGUI/GUIElements.py
  15. 345 150
      flatcamGUI/ObjectUI.py
  16. 85 14
      flatcamGUI/PlotCanvas.py
  17. 62 12
      flatcamGUI/PlotCanvasLegacy.py
  18. 379 149
      flatcamGUI/PreferencesUI.py
  19. 54 22
      flatcamGUI/VisPyCanvas.py
  20. 2 2
      flatcamGUI/VisPyPatches.py
  21. 2 2
      flatcamGUI/VisPyTesselators.py
  22. 5 3
      flatcamGUI/VisPyVisuals.py
  23. 0 1
      flatcamParsers/ParseDXF.py
  24. 46 34
      flatcamParsers/ParseDXF_Spline.py
  25. 1438 0
      flatcamParsers/ParseExcellon.py
  26. 6 7
      flatcamParsers/ParseFont.py
  27. 2071 0
      flatcamParsers/ParseGerber.py
  28. 5 3
      flatcamParsers/ParseSVG.py
  29. 70 146
      flatcamTools/ToolCalculators.py
  30. 27 111
      flatcamTools/ToolCutOut.py
  31. 12 6
      flatcamTools/ToolDblSided.py
  32. 93 36
      flatcamTools/ToolDistance.py
  33. 296 0
      flatcamTools/ToolDistanceMin.py
  34. 463 65
      flatcamTools/ToolFilm.py
  35. 18 17
      flatcamTools/ToolImage.py
  36. 39 33
      flatcamTools/ToolMove.py
  37. 165 108
      flatcamTools/ToolNonCopperClear.py
  38. 550 0
      flatcamTools/ToolOptimal.py
  39. 2 3
      flatcamTools/ToolPDF.py
  40. 73 77
      flatcamTools/ToolPaint.py
  41. 30 72
      flatcamTools/ToolPanelize.py
  42. 2 3
      flatcamTools/ToolPcbWizard.py
  43. 2 3
      flatcamTools/ToolProperties.py
  44. 1599 0
      flatcamTools/ToolRulesCheck.py
  45. 2 2
      flatcamTools/ToolShell.py
  46. 48 23
      flatcamTools/ToolSolderPaste.py
  47. 2 3
      flatcamTools/ToolSub.py
  48. 156 254
      flatcamTools/ToolTransform.py
  49. 23 11
      flatcamTools/__init__.py
  50. BIN
      locale/de/LC_MESSAGES/strings.mo
  51. 277 225
      locale/de/LC_MESSAGES/strings.po
  52. BIN
      locale/en/LC_MESSAGES/strings.mo
  53. 279 228
      locale/en/LC_MESSAGES/strings.po
  54. BIN
      locale/es/LC_MESSAGES/strings.mo
  55. 276 225
      locale/es/LC_MESSAGES/strings.po
  56. BIN
      locale/fr/LC_MESSAGES/strings.mo
  57. 276 225
      locale/fr/LC_MESSAGES/strings.po
  58. BIN
      locale/pt_BR/LC_MESSAGES/strings.mo
  59. 277 225
      locale/pt_BR/LC_MESSAGES/strings.po
  60. BIN
      locale/ro/LC_MESSAGES/strings.mo
  61. 276 227
      locale/ro/LC_MESSAGES/strings.po
  62. BIN
      locale/ru/LC_MESSAGES/strings.mo
  63. 276 224
      locale/ru/LC_MESSAGES/strings.po
  64. 391 358
      locale_template/strings.pot
  65. 5 5
      make_win.py
  66. BIN
      share/align_center32.png
  67. BIN
      share/align_justify32.png
  68. BIN
      share/align_left32.png
  69. BIN
      share/align_right32.png
  70. BIN
      share/bookmarks16.png
  71. BIN
      share/bookmarks32.png
  72. BIN
      share/calculator16.png
  73. BIN
      share/calculator24.png
  74. BIN
      share/dark/about32.png
  75. BIN
      share/dark/active_static.png
  76. BIN
      share/dark/addarray16.png
  77. BIN
      share/dark/addarray20.png
  78. BIN
      share/dark/addarray32.png
  79. BIN
      share/dark/align_center32.png
  80. BIN
      share/dark/align_justify32.png
  81. BIN
      share/dark/align_left32.png
  82. BIN
      share/dark/align_right32.png
  83. BIN
      share/dark/aperture16.png
  84. BIN
      share/dark/aperture32.png
  85. BIN
      share/dark/arc16.png
  86. BIN
      share/dark/arc24.png
  87. BIN
      share/dark/arc32.png
  88. BIN
      share/dark/axis32.png
  89. BIN
      share/dark/backup24.png
  90. BIN
      share/dark/backup_export24.png
  91. BIN
      share/dark/backup_import24.png
  92. BIN
      share/dark/blocked16.png
  93. BIN
      share/dark/bluelight12.png
  94. BIN
      share/dark/bold32.png
  95. BIN
      share/dark/buffer16-2.png
  96. BIN
      share/dark/buffer16.png
  97. BIN
      share/dark/buffer20.png
  98. BIN
      share/dark/buffer24.png
  99. BIN
      share/dark/bug16.png
  100. BIN
      share/dark/bug32.png

+ 2 - 0
FlatCAM.py

@@ -7,6 +7,8 @@ from FlatCAMApp import App
 from flatcamGUI import VisPyPatches
 
 from multiprocessing import freeze_support
+# import copyreg
+# import types
 
 if sys.platform == "win32":
     # cx_freeze 'module win32' workaround

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 361 - 107
FlatCAMApp.py


+ 778 - 104
FlatCAMObj.py

@@ -1,20 +1,33 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # Author: Juan Pablo Caram (c)                             #
 # Date: 2/5/2014                                           #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
+# ##########################################################
+# File modified by: Marius Stanciu                         #
+# ##########################################################
+
+from PyQt5.QtCore import Qt
+from PyQt5.QtGui import QTextDocument
 import copy
 import inspect  # TODO: For debugging only.
 from datetime import datetime
 
+from flatcamEditors.FlatCAMTextEditor import TextEditor
+
 from flatcamGUI.ObjectUI import *
 from FlatCAMCommon import LoudDict
 from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
 from camlib import *
+from flatcamParsers.ParseExcellon import Excellon
+from flatcamParsers.ParseGerber import Gerber
+
 import itertools
+import tkinter as tk
+import sys
 
 import gettext
 import FlatCAMTranslation as fcTranslate
@@ -109,11 +122,6 @@ class FlatCAMObj(QtCore.QObject):
 
         self.plot_single_object.connect(self.single_object_plot)
 
-        # 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
 
@@ -162,11 +170,23 @@ class FlatCAMObj(QtCore.QObject):
 
         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)
+        try:
+            # it will raise an exception for those FlatCAM objects that do not build UI with the common elements
+            self.ui.offset_button.clicked.connect(self.on_offset_button_click)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.ui.scale_button.clicked.connect(self.on_scale_button_click)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.ui.offsetvector_entry.returnPressed.connect(self.on_offset_button_click)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.ui.scale_entry.returnPressed.connect(self.on_scale_button_click)
+        except (TypeError, AttributeError):
+            pass
         # self.ui.skew_button.clicked.connect(self.on_skew_button_click)
 
     def build_ui(self):
@@ -520,6 +540,10 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             "plot": True,
             "multicolored": False,
             "solid": False,
+            "tool_type": 'circular',
+            "vtipdia": 0.1,
+            "vtipangle": 30,
+            "vcutz": -0.05,
             "isotooldia": 0.016,
             "isopasses": 1,
             "isooverlap": 0.15,
@@ -548,6 +572,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         # list of rows with apertures plotted
         self.marked_rows = []
 
+        # Number of decimals to be used by tools in this class
+        self.decimals = 4
+
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # from predecessors.
@@ -565,12 +592,23 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         FlatCAMObj.set_ui(self, ui)
         FlatCAMApp.App.log.debug("FlatCAMGerber.set_ui()")
 
+        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+
+        if self.units == 'MM':
+            self.decimals = 2
+        else:
+            self.decimals = 4
+
         self.replotApertures.connect(self.on_mark_cb_click_table)
 
         self.form_fields.update({
             "plot": self.ui.plot_cb,
             "multicolored": self.ui.multicolored_cb,
             "solid": self.ui.solid_cb,
+            "tool_type": self.ui.tool_type_radio,
+            "vtipdia": self.ui.tipdia_spinner,
+            "vtipangle": self.ui.tipangle_spinner,
+            "vcutz": self.ui.cutz_spinner,
             "isotooldia": self.ui.iso_tool_dia_entry,
             "isopasses": self.ui.iso_width_entry,
             "isooverlap": self.ui.iso_overlap_entry,
@@ -606,9 +644,24 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         self.ui.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.ui.obj_combo.setCurrentIndex(1)
         self.ui.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
+
+        self.ui.tool_type_radio.activated_custom.connect(self.on_tool_type_change)
+        # establish visibility for the GUI elements found in the slot function
+        self.ui.tool_type_radio.activated_custom.emit(self.options['tool_type'])
+
         # Show/Hide Advanced Options
         if self.app.defaults["global_app_level"] == 'b':
             self.ui.level.setText('<span style="color:green;"><b>%s</b></span>' % _('Basic'))
+            self.options['tool_type'] = 'circular'
+
+            self.ui.tool_type_label.hide()
+            self.ui.tool_type_radio.hide()
+            self.ui.tipdialabel.hide()
+            self.ui.tipdia_spinner.hide()
+            self.ui.tipanglelabel.hide()
+            self.ui.tipangle_spinner.hide()
+            self.ui.cutzlabel.hide()
+            self.ui.cutz_spinner.hide()
 
             self.ui.apertures_table_label.hide()
             self.ui.aperture_table_visibility_cb.hide()
@@ -621,6 +674,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             self.ui.except_cb.hide()
         else:
             self.ui.level.setText('<span style="color:red;"><b>%s</b></span>' % _('Advanced'))
+            self.ui.tipdia_spinner.valueChanged.connect(self.on_calculate_tooldia)
+            self.ui.tipangle_spinner.valueChanged.connect(self.on_calculate_tooldia)
+            self.ui.cutz_spinner.valueChanged.connect(self.on_calculate_tooldia)
 
         if self.app.defaults["gerber_buffering"] == 'no':
             self.ui.create_buffer_button.show()
@@ -637,11 +693,56 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
         self.build_ui()
 
+    def on_calculate_tooldia(self):
+        try:
+            tdia = float(self.ui.tipdia_spinner.get_value())
+        except Exception as e:
+            return
+        try:
+            dang = float(self.ui.tipangle_spinner.get_value())
+        except Exception as e:
+            return
+        try:
+            cutz = float(self.ui.cutz_spinner.get_value())
+        except Exception as e:
+            return
+
+        cutz *= -1
+        if cutz < 0:
+            cutz *= -1
+
+        half_tip_angle = dang / 2
+
+        tool_diameter = tdia + (2 * cutz * math.tan(math.radians(half_tip_angle)))
+        self.ui.iso_tool_dia_entry.set_value(tool_diameter)
+
     def on_type_obj_index_changed(self, index):
         obj_type = self.ui.type_obj_combo.currentIndex()
         self.ui.obj_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
         self.ui.obj_combo.setCurrentIndex(0)
 
+    def on_tool_type_change(self, state):
+        if state == 'circular':
+            self.ui.tipdialabel.hide()
+            self.ui.tipdia_spinner.hide()
+            self.ui.tipanglelabel.hide()
+            self.ui.tipangle_spinner.hide()
+            self.ui.cutzlabel.hide()
+            self.ui.cutz_spinner.hide()
+            self.ui.iso_tool_dia_entry.setDisabled(False)
+            # update the value in the self.iso_tool_dia_entry once this is selected
+            self.ui.iso_tool_dia_entry.set_value(self.options['isotooldia'])
+        else:
+            self.ui.tipdialabel.show()
+            self.ui.tipdia_spinner.show()
+            self.ui.tipanglelabel.show()
+            self.ui.tipangle_spinner.show()
+            self.ui.cutzlabel.show()
+            self.ui.cutz_spinner.show()
+            self.ui.iso_tool_dia_entry.setDisabled(True)
+            # update the value in the self.iso_tool_dia_entry once this is selected
+            self.on_calculate_tooldia()
+
     def build_ui(self):
         FlatCAMObj.build_ui(self)
 
@@ -682,15 +783,15 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
             if str(self.apertures[ap_code]['type']) == 'R' or str(self.apertures[ap_code]['type']) == 'O':
                 ap_dim_item = QtWidgets.QTableWidgetItem(
-                    '%.4f, %.4f' % (self.apertures[ap_code]['width'] * self.file_units_factor,
-                                    self.apertures[ap_code]['height'] * self.file_units_factor
+                    '%.*f, %.*f' % (self.decimals, self.apertures[ap_code]['width'],
+                                    self.decimals, self.apertures[ap_code]['height']
                                     )
                 )
                 ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled)
             elif str(self.apertures[ap_code]['type']) == 'P':
                 ap_dim_item = QtWidgets.QTableWidgetItem(
-                    '%.4f, %.4f' % (self.apertures[ap_code]['diam'] * self.file_units_factor,
-                                    self.apertures[ap_code]['nVertices'] * self.file_units_factor)
+                    '%.*f, %.*f' % (self.decimals, self.apertures[ap_code]['diam'],
+                                    self.decimals, self.apertures[ap_code]['nVertices'])
                 )
                 ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled)
             else:
@@ -699,9 +800,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
             try:
                 if self.apertures[ap_code]['size'] is not None:
-                    ap_size_item = QtWidgets.QTableWidgetItem('%.4f' %
-                                                              float(self.apertures[ap_code]['size'] *
-                                                                    self.file_units_factor))
+                    ap_size_item = QtWidgets.QTableWidgetItem(
+                        '%.*f' % (self.decimals, float(self.apertures[ap_code]['size'])))
                 else:
                     ap_size_item = QtWidgets.QTableWidgetItem('')
             except KeyError:
@@ -1080,14 +1180,26 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                         return 'fail'
                     geo_obj.solid_geometry.append(geom)
 
+                    # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI
+                    if self.ui.tool_type_radio.get_value() == 'circular':
+                        new_cutz = self.app.defaults['geometry_cutz']
+                        new_vtipdia = self.app.defaults['geometry_vtipdia']
+                        new_vtipangle = self.app.defaults['geometry_vtipangle']
+                        tool_type = 'C1'
+                    else:
+                        new_cutz = self.ui.cutz_spinner.get_value()
+                        new_vtipdia = self.ui.tipdia_spinner.get_value()
+                        new_vtipangle = self.ui.tipangle_spinner.get_value()
+                        tool_type = 'V'
+
                     # store here the default data for Geometry Data
                     default_data = {}
                     default_data.update({
                         "name": iso_name,
                         "plot": self.app.defaults['geometry_plot'],
-                        "cutz": self.app.defaults['geometry_cutz'],
-                        "vtipdia": self.app.defaults['geometry_vtipdia'],
-                        "vtipangle": self.app.defaults['geometry_vtipangle'],
+                        "cutz": new_cutz,
+                        "vtipdia": new_vtipdia,
+                        "vtipangle": new_vtipangle,
                         "travelz": self.app.defaults['geometry_travelz'],
                         "feedrate": self.app.defaults['geometry_feedrate'],
                         "feedrate_z": self.app.defaults['geometry_feedrate_z'],
@@ -1114,7 +1226,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                             'offset': 'Path',
                             'offset_value': 0.0,
                             'type': _('Rough'),
-                            'tool_type': 'C1',
+                            'tool_type': tool_type,
                             'data': default_data,
                             'solid_geometry': geo_obj.solid_geometry
                         }
@@ -1295,6 +1407,13 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         :return: None
         :rtype: None
         """
+
+        # units conversion to get a conversion should be done only once even if we found multiple
+        # units declaration inside a Gerber file (it can happen to find also the obsolete declaration)
+        if self.conversion_done is True:
+            log.debug("Gerber units conversion cancelled. Already done.")
+            return
+
         log.debug("FlatCAMObj.FlatCAMGerber.convert_units()")
 
         factor = Gerber.convert_units(self, units)
@@ -1441,14 +1560,14 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                     if aperture_to_plot_mark in self.apertures:
                         for elem in self.apertures[aperture_to_plot_mark]['geometry']:
                             if 'solid' in elem:
-                                    geo = elem['solid']
-                                    if type(geo) == Polygon or type(geo) == LineString:
-                                        self.add_mark_shape(apid=aperture_to_plot_mark, shape=geo, color=color,
+                                geo = elem['solid']
+                                if type(geo) == Polygon or type(geo) == LineString:
+                                    self.add_mark_shape(apid=aperture_to_plot_mark, shape=geo, color=color,
+                                                        face_color=color, visible=visibility)
+                                else:
+                                    for el in geo:
+                                        self.add_mark_shape(apid=aperture_to_plot_mark, shape=el, color=color,
                                                             face_color=color, visible=visibility)
-                                    else:
-                                        for el in geo:
-                                            self.add_mark_shape(apid=aperture_to_plot_mark, shape=el, color=color,
-                                                                face_color=color, visible=visibility)
 
                     self.mark_shapes[aperture_to_plot_mark].redraw()
 
@@ -1897,6 +2016,9 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
 
         self.multigeo = False
 
+        # Number fo decimals to be used for tools in this class
+        self.decimals = 4
+
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # from predecessors.
@@ -1918,6 +2040,11 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         :return: None
         """
 
+        try:
+            decimals_exc = self.decimals
+        except AttributeError:
+            decimals_exc = 4
+
         # flag to signal that we need to reorder the tools dictionary and drills and slots lists
         flag_order = False
 
@@ -1944,7 +2071,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
                         exc.app.log.warning("Failed to copy option.", option)
 
             for drill in exc.drills:
-                exc_tool_dia = float('%.4f' % exc.tools[drill['tool']]['C'])
+                exc_tool_dia = float('%.*f' % (decimals_exc, exc.tools[drill['tool']]['C']))
 
                 if exc_tool_dia not in custom_dict_drills:
                     custom_dict_drills[exc_tool_dia] = [drill['point']]
@@ -1952,7 +2079,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
                     custom_dict_drills[exc_tool_dia].append(drill['point'])
 
             for slot in exc.slots:
-                exc_tool_dia = float('%.4f' % exc.tools[slot['tool']]['C'])
+                exc_tool_dia = float('%.*f' % (decimals_exc, exc.tools[slot['tool']]['C']))
 
                 if exc_tool_dia not in custom_dict_slots:
                     custom_dict_slots[exc_tool_dia] = [[slot['start'], slot['stop']]]
@@ -2045,7 +2172,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
                 temp_tools[tool_name_temp] = spec_temp
 
                 for drill in exc_final.drills:
-                    exc_tool_dia = float('%.4f' % exc_final.tools[drill['tool']]['C'])
+                    exc_tool_dia = float('%.*f' % (decimals_exc, exc_final.tools[drill['tool']]['C']))
                     if exc_tool_dia == ordered_dia:
                         temp_drills.append(
                             {
@@ -2055,7 +2182,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
                         )
 
                 for slot in exc_final.slots:
-                    slot_tool_dia = float('%.4f' % exc_final.tools[slot['tool']]['C'])
+                    slot_tool_dia = float('%.*f' % (decimals_exc, exc_final.tools[slot['tool']]['C']))
                     if slot_tool_dia == ordered_dia:
                         temp_slots.append(
                             {
@@ -2130,10 +2257,8 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
             # 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('%.4f' % (self.tools[tool_no]['C']))
+
+            dia = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, self.tools[tool_no]['C']))
 
             dia.setFlags(QtCore.Qt.ItemIsEnabled)
 
@@ -2148,10 +2273,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
             slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
 
             try:
-                if self.units == 'MM':
-                    t_offset = self.tool_offset[float('%.2f' % float(self.tools[tool_no]['C']))]
-                else:
-                    t_offset = self.tool_offset[float('%.4f' % float(self.tools[tool_no]['C']))]
+                t_offset = self.tool_offset[float('%.*f' % (self.decimals, float(self.tools[tool_no]['C'])))]
             except KeyError:
                 t_offset = self.app.defaults['excellon_offset']
 
@@ -2307,6 +2429,11 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
 
         self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
 
+        if self.units == 'MM':
+            self.decimals = 2
+        else:
+            self.decimals = 4
+
         self.form_fields.update({
             "plot": self.ui.plot_cb,
             "solid": self.ui.solid_cb,
@@ -2347,10 +2474,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         t_default_offset = self.app.defaults["excellon_offset"]
         if not self.tool_offset:
             for value in self.tools.values():
-                if self.units == 'MM':
-                    dia = float('%.2f' % float(value['C']))
-                else:
-                    dia = float('%.4f' % float(value['C']))
+                dia = float('%.*f' % (self.decimals, float(value['C'])))
                 self.tool_offset[dia] = t_default_offset
 
         # Show/Hide Advanced Options
@@ -2407,10 +2531,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         self.is_modified = True
 
         row_of_item_changed = self.ui.tools_table.currentRow()
-        if self.units == 'MM':
-            dia = float('%.2f' % float(self.ui.tools_table.item(row_of_item_changed, 1).text()))
-        else:
-            dia = float('%.4f' % float(self.ui.tools_table.item(row_of_item_changed, 1).text()))
+        dia = float('%.*f' % (self.decimals, float(self.ui.tools_table.item(row_of_item_changed, 1).text())))
 
         current_table_offset_edited = None
         if self.ui.tools_table.currentItem() is not None:
@@ -2455,8 +2576,21 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         for x in self.ui.tools_table.selectedItems():
             # from the columnCount we subtract a value of 1 which represent the last column (plot column)
             # which does not have text
-            table_tools_items.append([self.ui.tools_table.item(x.row(), column).text()
-                                      for column in range(0, self.ui.tools_table.columnCount() - 1)])
+            txt = ''
+            elem = list()
+
+            for column in range(0, self.ui.tools_table.columnCount() - 1):
+                try:
+                    txt = self.ui.tools_table.item(x.row(), column).text()
+                except AttributeError:
+                    try:
+                        txt = self.ui.tools_table.cellWidget(x.row(), column).currentText()
+                    except AttributeError:
+                        pass
+                elem.append(txt)
+            table_tools_items.append(deepcopy(elem))
+            # table_tools_items.append([self.ui.tools_table.item(x.row(), column).text()
+            #                           for column in range(0, self.ui.tools_table.columnCount() - 1)])
         for item in table_tools_items:
             item[0] = str(item[0])
         return table_tools_items
@@ -2762,8 +2896,8 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
 
         for tool in tools:
             # I add the 0.0001 value to account for the rounding error in converting from IN to MM and reverse
-            adj_toolstable_tooldia = float('%.4f' % float(tooldia))
-            adj_file_tooldia = float('%.4f' % float(self.tools[tool]["C"]))
+            adj_toolstable_tooldia = float('%.*f' % (self.decimals, float(tooldia)))
+            adj_file_tooldia = float('%.*f' % (self.decimals, float(self.tools[tool]["C"])))
             if adj_toolstable_tooldia > adj_file_tooldia + 0.0001:
                 self.app.inform.emit('[ERROR_NOTCL] %s' %
                                      _("Milling tool for SLOTS is larger than hole size. Cancelled."))
@@ -2792,8 +2926,8 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
             # 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:
-                    toolstable_tool = float('%.4f' % float(tooldia))
-                    file_tool = float('%.4f' % float(self.tools[tool]["C"]))
+                    toolstable_tool = float('%.*f' % (self.decimals, float(tooldia)))
+                    file_tool = float('%.*f' % (self.decimals, float(self.tools[tool]["C"])))
 
                     # I add the 0.0001 value to account for the rounding error in converting from IN to MM and reverse
                     # for the file_tool (tooldia actually)
@@ -3337,6 +3471,9 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         self.old_pp_state = self.app.defaults["geometry_multidepth"]
         self.old_toolchangeg_state = self.app.defaults["geometry_toolchange"]
 
+        # Number of decimals to be used for tools in this class
+        self.decimals = 4
+
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # from predecessors.
@@ -3365,10 +3502,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             # 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 = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(tooluid_value['tooldia'])))
 
             dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
 
@@ -3495,6 +3630,13 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         assert isinstance(self.ui, GeometryObjectUI), \
             "Expected a GeometryObjectUI, got %s" % type(self.ui)
 
+        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+
+        if self.units == 'MM':
+            self.decimals = 2
+        else:
+            self.decimals = 4
+
         # populate postprocessor names in the combobox
         for name in list(self.app.postprocessors.keys()):
             self.ui.pp_geometry_name_cb.addItem(name)
@@ -3584,7 +3726,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             for toold in tools_list:
                 self.tools.update({
                     self.tooluid: {
-                        'tooldia': float(toold),
+                        'tooldia': float('%.*f' % (self.decimals, float(toold))),
                         'offset': 'Path',
                         'offset_value': 0.0,
                         'type': _('Rough'),
@@ -3714,6 +3856,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             elif isinstance(current_widget, FloatEntry) or isinstance(current_widget, LengthEntry) or \
                     isinstance(current_widget, FCEntry) or isinstance(current_widget, IntEntry):
                 current_widget.editingFinished.connect(self.gui_form_to_storage)
+            elif isinstance(current_widget, FCSpinner) or isinstance(current_widget, FCDoubleSpinner):
+                current_widget.returnPressed.connect(self.gui_form_to_storage)
 
         for row in range(self.ui.geo_tools_table.rowCount()):
             for col in [2, 3, 4]:
@@ -3728,7 +3872,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
 
         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)
+        self.ui.tool_offset_entry.returnPressed.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)
@@ -3756,6 +3900,11 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                     self.ui.grid3.itemAt(i).widget().editingFinished.disconnect(self.gui_form_to_storage)
                 except (TypeError, AttributeError):
                     pass
+            elif isinstance(current_widget, FCSpinner) or isinstance(current_widget, FCDoubleSpinner):
+                try:
+                    self.ui.grid3.itemAt(i).widget().returnPressed.disconnect(self.gui_form_to_storage)
+                except TypeError:
+                    pass
 
         for row in range(self.ui.geo_tools_table.rowCount()):
             for col in [2, 3, 4]:
@@ -3790,7 +3939,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             pass
 
         try:
-            self.ui.tool_offset_entry.editingFinished.disconnect()
+            self.ui.tool_offset_entry.returnPressed.disconnect()
         except (TypeError, AttributeError):
             pass
 
@@ -3846,10 +3995,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             max_uid = max(tool_uid_list)
         self.tooluid = max_uid + 1
 
-        if self.units == 'IN':
-            tooldia = float('%.4f' % tooldia)
-        else:
-            tooldia = float('%.2f' % tooldia)
+        tooldia = float('%.*f' % (self.decimals, 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
@@ -3996,7 +4142,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                                      _("Wrong value format entered, use a number."))
                 return
 
-        tool_dia = float('%.4f' % d)
+        tool_dia = float('%.*f' % (self.decimals, d))
         tooluid = int(self.ui.geo_tools_table.item(current_row, 5).text())
 
         self.tools[tooluid]['tooldia'] = tool_dia
@@ -4203,7 +4349,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
 
         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) # this value has to be negative
+        new_cutz = float('%.*f' % (self.decimals, -new_cutz))   # this value has to be negative
         self.ui.cutz_entry.set_value(new_cutz)
 
         # store the new CutZ value into storage (self.tools)
@@ -4426,8 +4572,21 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         table_tools_items = []
         if self.multigeo:
             for x in self.ui.geo_tools_table.selectedItems():
-                table_tools_items.append([self.ui.geo_tools_table.item(x.row(), column).text()
-                                          for column in range(0, self.ui.geo_tools_table.columnCount())])
+                elem = list()
+                txt = ''
+
+                for column in range(0, self.ui.geo_tools_table.columnCount()):
+                    try:
+                        txt = self.ui.geo_tools_table.item(x.row(), column).text()
+                    except AttributeError:
+                        try:
+                            txt = self.ui.geo_tools_table.cellWidget(x.row(), column).currentText()
+                        except AttributeError:
+                            pass
+                    elem.append(txt)
+                table_tools_items.append(deepcopy(elem))
+                # table_tools_items.append([self.ui.geo_tools_table.item(x.row(), column).text()
+                #                           for column in range(0, self.ui.geo_tools_table.columnCount())])
         else:
             for x in self.ui.geo_tools_table.selectedItems():
                 r = []
@@ -4441,9 +4600,10 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                     try:
                         txt = self.ui.geo_tools_table.item(x.row(), column).text()
                     except AttributeError:
-                        txt = self.ui.geo_tools_table.cellWidget(x.row(), column).currentText()
-                    except Exception as e:
-                        pass
+                        try:
+                            txt = self.ui.geo_tools_table.cellWidget(x.row(), column).currentText()
+                        except AttributeError:
+                            pass
                     r.append(txt)
                 table_tools_items.append(r)
 
@@ -4563,6 +4723,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
 
         :param segx: number of segments on the X axis, for auto-levelling
         :param segy: number of segments on the Y axis, for auto-levelling
+        :param plot: if True the generated object will be plotted; if False will not be plotted
         :param use_thread: if True use threading
         :return: None
         """
@@ -4625,7 +4786,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 tool_cnt += 1
 
                 dia_cnc_dict = deepcopy(tools_dict[tooluid_key])
-                tooldia_val = float('%.4f' % float(tools_dict[tooluid_key]['tooldia']))
+                tooldia_val = float('%.*f' % (self.decimals, float(tools_dict[tooluid_key]['tooldia'])))
                 dia_cnc_dict.update({
                     'tooldia': tooldia_val
                 })
@@ -4779,7 +4940,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             for tooluid_key in list(tools_dict.keys()):
                 tool_cnt += 1
                 dia_cnc_dict = deepcopy(tools_dict[tooluid_key])
-                tooldia_val = float('%.4f' % float(tools_dict[tooluid_key]['tooldia']))
+                tooldia_val = float('%.*f' % (self.decimals, float(tools_dict[tooluid_key]['tooldia'])))
 
                 dia_cnc_dict.update({
                     'tooldia': tooldia_val
@@ -4789,7 +4950,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 # 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'])) == tooldia_val:
+                    if float('%.*f' % (self.decimals, float(v['tooldia']))) == tooldia_val:
                         current_uid = int(k)
                         break
 
@@ -5202,8 +5363,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         except TypeError:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
                                  _("An (x,y) pair of values are needed. "
-                                   "Probable you entered only one value in the Offset field."
-            ))
+                                   "Probable you entered only one value in the Offset field.")
+                                 )
             return
 
         self.geo_len = 0
@@ -5286,8 +5447,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 self.app.inform.emit('[ERROR] %s' %
                                      _("The Toolchange X,Y field in Edit -> Preferences "
                                        "has to be in the format (x, y)\n"
-                                       "but now there is only one value, not two."
-                ))
+                                       "but now there is only one value, not two.")
+                                     )
                 return 'fail'
             coords_xy[0] *= factor
             coords_xy[1] *= factor
@@ -5307,7 +5468,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 for dia_key, dia_value in tooluid_value.items():
                     if dia_key == 'tooldia':
                         dia_value *= factor
-                        dia_value = float('%.4f' % dia_value)
+                        dia_value = float('%.*f' % (self.decimals, dia_value))
                         tool_dia_copy[dia_key] = dia_value
                     if dia_key == 'offset':
                         tool_dia_copy[dia_key] = dia_value
@@ -5384,7 +5545,6 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             # if self.app.is_legacy is False:
             self.add_shape(shape=element, color=color, visible=visible, layer=0)
 
-
     def plot(self, visible=None, kind=None):
         """
         Plot the object.
@@ -5578,6 +5738,8 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         # from predecessors.
         self.ser_attrs += ['options', 'kind', 'cnc_tools', 'multitool']
 
+        self.decimals = 4
+
         if self.app.is_legacy is False:
             self.text_col = self.app.plotcanvas.new_text_collection()
             self.text_col.enabled = True
@@ -5614,10 +5776,8 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
             # 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']))
+
+            dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(dia_value['tooldia'])))
 
             offset_txt = list(str(dia_value['offset']))
             offset_txt[0] = offset_txt[0].upper()
@@ -5706,13 +5866,20 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         assert isinstance(self.ui, CNCObjectUI), \
             "Expected a CNCObjectUI, got %s" % type(self.ui)
 
+        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+
+        if self.units == "IN":
+            self.decimals = 4
+        else:
+            self.decimals = 2
+
         # this signal has to be connected to it's slot before the defaults are populated
         # the decision done in the slot has to override the default value set bellow
         self.ui.toolchange_cb.toggled.connect(self.on_toolchange_custom_clicked)
 
         self.form_fields.update({
             "plot": self.ui.plot_cb,
-            # "tooldia": self.ui.tooldia_entry,
+            "tooldia": self.ui.tooldia_entry,
             "append": self.ui.append_text,
             "prepend": self.ui.prepend_text,
             "toolchange_macro": self.ui.toolchange_text,
@@ -5728,7 +5895,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
                 self.ui.t_distance_label.show()
                 self.ui.t_distance_entry.setVisible(True)
                 self.ui.t_distance_entry.setDisabled(True)
-                self.ui.t_distance_entry.set_value('%.4f' % float(self.travel_distance))
+                self.ui.t_distance_entry.set_value('%.*f' % (self.decimals, float(self.travel_distance)))
                 self.ui.units_label.setText(str(self.units).lower())
                 self.ui.units_label.setDisabled(True)
 
@@ -5737,16 +5904,23 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
                 self.ui.t_time_entry.setDisabled(True)
                 # if time is more than 1 then we have minutes, else we have seconds
                 if self.routing_time > 1:
-                    self.ui.t_time_entry.set_value('%.4f' % math.ceil(float(self.routing_time)))
+                    self.ui.t_time_entry.set_value('%.*f' % (self.decimals, math.ceil(float(self.routing_time))))
                     self.ui.units_time_label.setText('min')
                 else:
                     time_r = self.routing_time * 60
-                    self.ui.t_time_entry.set_value('%.4f' % math.ceil(float(time_r)))
+                    self.ui.t_time_entry.set_value('%.*f' % (self.decimals, math.ceil(float(time_r))))
                     self.ui.units_time_label.setText('sec')
                 self.ui.units_time_label.setDisabled(True)
         except AttributeError:
             pass
 
+        if self.multitool is False:
+            self.ui.tooldia_entry.show()
+            self.ui.updateplot_button.show()
+        else:
+            self.ui.tooldia_entry.hide()
+            self.ui.updateplot_button.hide()
+
         # set the kind of geometries are plotted by default with plot2() from camlib.CNCJob
         self.ui.cncplot_method_combo.set_value(self.app.defaults["cncjob_plot_kind"])
 
@@ -5805,7 +5979,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         and plots the object.
         """
         self.read_form()
-        self.plot()
+        self.on_plot_kind_change()
 
     def on_plot_kind_change(self):
         kind = self.ui.cncplot_method_combo.get_value()
@@ -5867,6 +6041,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
                              (_("Machine Code file saved to"), filename))
 
     def on_edit_code_click(self, *args):
+        self.app.proc_container.view.set_busy(_("Loading..."))
 
         preamble = str(self.ui.prepend_text.get_value())
         postamble = str(self.ui.append_text.get_value())
@@ -5877,25 +6052,46 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         else:
             self.app.gcode_edited = gco
 
-        self.app.init_code_editor(name=_("Code Editor"))
-        self.app.ui.buttonOpen.clicked.connect(self.app.handleOpen)
-        self.app.ui.buttonSave.clicked.connect(self.app.handleSaveGCode)
+        self.gcode_editor_tab = TextEditor(app=self.app)
+
+        # add the tab if it was closed
+        self.app.ui.plot_tab_area.addTab(self.gcode_editor_tab, '%s' % _("Code Editor"))
+        self.gcode_editor_tab.setObjectName('code_editor_tab')
 
+        # delete the absolute and relative position and messages in the infobar
+        self.app.ui.position_label.setText("")
+        self.app.ui.rel_position_label.setText("")
+
+        # first clear previous text in text editor (if any)
+        self.gcode_editor_tab.code_editor.clear()
+        self.gcode_editor_tab.code_editor.setReadOnly(False)
+
+        self.gcode_editor_tab.code_editor.completer_enable = False
+        self.gcode_editor_tab.buttonRun.hide()
+
+        # Switch plot_area to CNCJob tab
+        self.app.ui.plot_tab_area.setCurrentWidget(self.gcode_editor_tab)
+
+        self.gcode_editor_tab.t_frame.hide()
         # then append the text from GCode to the text editor
         try:
             for line in self.app.gcode_edited:
+                QtWidgets.QApplication.processEvents()
+
                 proc_line = str(line).strip('\n')
-                self.app.ui.code_editor.append(proc_line)
+                self.gcode_editor_tab.code_editor.append(proc_line)
         except Exception as e:
             log.debug('FlatCAMCNNJob.on_edit_code_click() -->%s' % str(e))
             self.app.inform.emit('[ERROR] %s %s' %
                                  ('FlatCAMCNNJob.on_edit_code_click() -->', str(e)))
             return
 
-        self.app.ui.code_editor.moveCursor(QtGui.QTextCursor.Start)
+        self.gcode_editor_tab.code_editor.moveCursor(QtGui.QTextCursor.Start)
+
+        self.gcode_editor_tab.handleTextChanged()
+        self.gcode_editor_tab.t_frame.show()
+        self.app.proc_container.view.set_idle()
 
-        self.app.handleTextChanged()
-        self.app.ui.show()
         self.app.inform.emit('[success] %s...' %
                              _('Loaded Machine Code into Code Editor'))
 
@@ -6078,8 +6274,8 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
                 m6_code = self.parse_custom_toolchange_code(self.ui.toolchange_text.get_value())
                 if m6_code is None or m6_code == '':
                     self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                         _("Cancelled. The Toolchange Custom code is enabled but it's empty."
-                    ))
+                                         _("Cancelled. The Toolchange Custom code is enabled but it's empty.")
+                                         )
                     return 'fail'
 
                 g = g.replace('M6', m6_code)
@@ -6174,7 +6370,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         self.shapes.clear(update=True)
 
         for tooluid_key in self.cnc_tools:
-            tooldia = float('%.4f' % float(self.cnc_tools[tooluid_key]['tooldia']))
+            tooldia = float('%.*f' % (self.decimals, 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())
 
@@ -6227,7 +6423,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
             else:
                 # multiple tools usage
                 for tooluid_key in self.cnc_tools:
-                    tooldia = float('%.4f' % float(self.cnc_tools[tooluid_key]['tooldia']))
+                    tooldia = float('%.*f' % (self.decimals, 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, kind=kind)
             self.shapes.redraw()
@@ -6266,7 +6462,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
             for dia_key, dia_value in tooluid_value.items():
                 if dia_key == 'tooldia':
                     dia_value *= factor
-                    dia_value = float('%.4f' % dia_value)
+                    dia_value = float('%.*f' % (self.decimals, dia_value))
                     tool_dia_copy[dia_key] = dia_value
                 if dia_key == 'offset':
                     tool_dia_copy[dia_key] = dia_value
@@ -6314,4 +6510,482 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         self.cnc_tools.clear()
         self.cnc_tools = deepcopy(temp_tools_dict)
 
+
+class FlatCAMScript(FlatCAMObj):
+    """
+    Represents a TCL script object.
+    """
+    optionChanged = QtCore.pyqtSignal(str)
+    ui_type = ScriptObjectUI
+
+    def __init__(self, name):
+        FlatCAMApp.App.log.debug("Creating a FlatCAMScript object...")
+        FlatCAMObj.__init__(self, name)
+
+        self.kind = "script"
+
+        self.options.update({
+            "plot": True,
+            "type": 'Script',
+            "source_file": '',
+        })
+
+        self.units = ''
+        self.decimals = 4
+
+        self.ser_attrs = ['options', 'kind', 'source_file']
+        self.source_file = ''
+        self.script_code = ''
+
+        self.script_editor_tab = None
+
+    def set_ui(self, ui):
+        FlatCAMObj.set_ui(self, ui)
+        FlatCAMApp.App.log.debug("FlatCAMScript.set_ui()")
+
+        assert isinstance(self.ui, ScriptObjectUI), \
+            "Expected a ScriptObjectUI, got %s" % type(self.ui)
+
+        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+
+        if self.units == "IN":
+            self.decimals = 4
+        else:
+            self.decimals = 2
+
+        # Fill form fields only on object create
+        self.to_form()
+
+        # Show/Hide Advanced Options
+        if self.app.defaults["global_app_level"] == 'b':
+            self.ui.level.setText(_(
+                '<span style="color:green;"><b>Basic</b></span>'
+            ))
+        else:
+            self.ui.level.setText(_(
+                '<span style="color:red;"><b>Advanced</b></span>'
+            ))
+
+        self.script_editor_tab = TextEditor(app=self.app)
+
+        # first clear previous text in text editor (if any)
+        self.script_editor_tab.code_editor.clear()
+        self.script_editor_tab.code_editor.setReadOnly(False)
+
+        self.script_editor_tab.buttonRun.show()
+
+        self.ui.autocomplete_cb.set_value(self.app.defaults['script_autocompleter'])
+        self.on_autocomplete_changed(state=self.app.defaults['script_autocompleter'])
+
+        flt = "FlatCAM Scripts (*.FlatScript);;All Files (*.*)"
+        self.script_editor_tab.buttonOpen.clicked.disconnect()
+        self.script_editor_tab.buttonOpen.clicked.connect(lambda: self.script_editor_tab.handleOpen(filt=flt))
+        self.script_editor_tab.buttonSave.clicked.disconnect()
+        self.script_editor_tab.buttonSave.clicked.connect(lambda: self.script_editor_tab.handleSaveGCode(filt=flt))
+
+        self.script_editor_tab.buttonRun.clicked.connect(self.handle_run_code)
+
+        self.script_editor_tab.handleTextChanged()
+
+        self.ui.autocomplete_cb.stateChanged.connect(self.on_autocomplete_changed)
+
+        self.ser_attrs = ['options', 'kind', 'source_file']
+
+        for line in self.source_file.splitlines():
+            self.script_editor_tab.code_editor.append(line)
+
+        self.build_ui()
+
+    def build_ui(self):
+        FlatCAMObj.build_ui(self)
+        tab_here = False
+
+        # try to not add too many times a tab that it is already installed
+        for idx in range(self.app.ui.plot_tab_area.count()):
+            if self.app.ui.plot_tab_area.widget(idx).objectName() == self.options['name']:
+                tab_here = True
+                break
+
+        # add the tab if it is not already added
+        if tab_here is False:
+            self.app.ui.plot_tab_area.addTab(self.script_editor_tab, '%s' % _("Script Editor"))
+            self.script_editor_tab.setObjectName(self.options['name'])
+
+        # Switch plot_area to CNCJob tab
+        self.app.ui.plot_tab_area.setCurrentWidget(self.script_editor_tab)
+
+    def handle_run_code(self):
+        # trying to run a Tcl command without having the Shell open will create some warnings because the Tcl Shell
+        # tries to print on a hidden widget, therefore show the dock if hidden
+        if self.app.ui.shell_dock.isHidden():
+            self.app.ui.shell_dock.show()
+
+        self.script_code = deepcopy(self.script_editor_tab.code_editor.toPlainText())
+
+        old_line = ''
+        for tcl_command_line in self.script_code.splitlines():
+            # do not process lines starting with '#' = comment and empty lines
+            if not tcl_command_line.startswith('#') and tcl_command_line != '':
+                # id FlatCAM is run in Windows then replace all the slashes with
+                # the UNIX style slash that TCL understands
+                if sys.platform == 'win32':
+                    if "open" in tcl_command_line:
+                        tcl_command_line = tcl_command_line.replace('\\', '/')
+
+                if old_line != '':
+                    new_command = old_line + tcl_command_line + '\n'
+                else:
+                    new_command = tcl_command_line
+
+                # execute the actual Tcl command
+                try:
+                    self.app.shell.open_proccessing()  # Disables input box.
+
+                    result = self.app.tcl.eval(str(new_command))
+                    if result != 'None':
+                        self.app.shell.append_output(result + '\n')
+
+                    old_line = ''
+                except tk.TclError:
+                    old_line = old_line + tcl_command_line + '\n'
+                except Exception as e:
+                    log.debug("App.handleRunCode() --> %s" % str(e))
+
+        if old_line != '':
+            # it means that the script finished with an error
+            result = self.app.tcl.eval("set errorInfo")
+            log.error("Exec command Exception: %s" % (result + '\n'))
+            self.app.shell.append_error('ERROR: ' + result + '\n')
+
+        self.app.shell.close_proccessing()
+
+    def on_autocomplete_changed(self, state):
+        if state:
+            self.script_editor_tab.code_editor.completer_enable = True
+        else:
+            self.script_editor_tab.code_editor.completer_enable = False
+
+    def to_dict(self):
+        """
+        Returns a representation 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])
+
+
+class FlatCAMDocument(FlatCAMObj):
+    """
+    Represents a Document object.
+    """
+    optionChanged = QtCore.pyqtSignal(str)
+    ui_type = DocumentObjectUI
+
+    def __init__(self, name):
+        FlatCAMApp.App.log.debug("Creating a Document object...")
+        FlatCAMObj.__init__(self, name)
+
+        self.kind = "document"
+
+        self.units = ''
+        self.decimals = 4
+
+        self.ser_attrs = ['options', 'kind', 'source_file']
+        self.source_file = ''
+        self.doc_code = ''
+
+        self.font_italic = None
+        self.font_bold = None
+        self.font_underline =None
+
+        self.document_editor_tab = None
+
+        self._read_only = False
+
+    def set_ui(self, ui):
+        FlatCAMObj.set_ui(self, ui)
+        FlatCAMApp.App.log.debug("FlatCAMDocument.set_ui()")
+
+        assert isinstance(self.ui, DocumentObjectUI), \
+            "Expected a DocumentObjectUI, got %s" % type(self.ui)
+
+        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+
+        if self.units == "IN":
+            self.decimals = 4
+        else:
+            self.decimals = 2
+
+        # Fill form fields only on object create
+        self.to_form()
+
+        # Show/Hide Advanced Options
+        if self.app.defaults["global_app_level"] == 'b':
+            self.ui.level.setText(_(
+                '<span style="color:green;"><b>Basic</b></span>'
+            ))
+        else:
+            self.ui.level.setText(_(
+                '<span style="color:red;"><b>Advanced</b></span>'
+            ))
+
+        self.document_editor_tab = TextEditor(app=self.app)
+        stylesheet = """
+                        QTextEdit {selection-background-color:%s;
+                                   selection-color:white;
+                        }
+                     """ % self.app.defaults["document_sel_color"]
+
+        self.document_editor_tab.code_editor.setStyleSheet(stylesheet)
+
+        # first clear previous text in text editor (if any)
+        self.document_editor_tab.code_editor.clear()
+        self.document_editor_tab.code_editor.setReadOnly(self._read_only)
+
+        self.document_editor_tab.buttonRun.hide()
+
+        self.ui.autocomplete_cb.set_value(self.app.defaults['document_autocompleter'])
+        self.on_autocomplete_changed(state=self.app.defaults['document_autocompleter'])
+        self.on_tab_size_change(val=self.app.defaults['document_tab_size'])
+
+        flt = "FlatCAM Docs (*.FlatDoc);;All Files (*.*)"
+
+        # ######################################################################
+        # ######################## SIGNALS #####################################
+        # ######################################################################
+        self.document_editor_tab.buttonOpen.clicked.disconnect()
+        self.document_editor_tab.buttonOpen.clicked.connect(lambda: self.document_editor_tab.handleOpen(filt=flt))
+        self.document_editor_tab.buttonSave.clicked.disconnect()
+        self.document_editor_tab.buttonSave.clicked.connect(lambda: self.document_editor_tab.handleSaveGCode(filt=flt))
+
+        self.document_editor_tab.code_editor.textChanged.connect(self.on_text_changed)
+
+        self.ui.font_type_cb.currentFontChanged.connect(self.font_family)
+        self.ui.font_size_cb.activated.connect(self.font_size)
+        self.ui.font_bold_tb.clicked.connect(self.on_bold_button)
+        self.ui.font_italic_tb.clicked.connect(self.on_italic_button)
+        self.ui.font_under_tb.clicked.connect(self.on_underline_button)
+
+        self.ui.font_color_entry.editingFinished.connect(self.on_font_color_entry)
+        self.ui.font_color_button.clicked.connect(self.on_font_color_button)
+        self.ui.sel_color_entry.editingFinished.connect(self.on_selection_color_entry)
+        self.ui.sel_color_button.clicked.connect(self.on_selection_color_button)
+
+        self.ui.al_left_tb.clicked.connect(lambda: self.document_editor_tab.code_editor.setAlignment(Qt.AlignLeft))
+        self.ui.al_center_tb.clicked.connect(lambda: self.document_editor_tab.code_editor.setAlignment(Qt.AlignCenter))
+        self.ui.al_right_tb.clicked.connect(lambda: self.document_editor_tab.code_editor.setAlignment(Qt.AlignRight))
+        self.ui.al_justify_tb.clicked.connect(
+            lambda: self.document_editor_tab.code_editor.setAlignment(Qt.AlignJustify)
+        )
+
+        self.ui.autocomplete_cb.stateChanged.connect(self.on_autocomplete_changed)
+        self.ui.tab_size_spinner.returnPressed.connect(self.on_tab_size_change)
+        # #######################################################################
+
+        self.ui.font_color_entry.set_value(self.app.defaults['document_font_color'])
+        self.ui.font_color_button.setStyleSheet(
+            "background-color:%s" % str(self.app.defaults['document_font_color']))
+
+        self.ui.sel_color_entry.set_value(self.app.defaults['document_sel_color'])
+        self.ui.sel_color_button.setStyleSheet(
+            "background-color:%s" % self.app.defaults['document_sel_color'])
+
+        self.ui.font_size_cb.setCurrentIndex(int(self.app.defaults['document_font_size']))
+
+        self.document_editor_tab.handleTextChanged()
+        self.ser_attrs = ['options', 'kind', 'source_file']
+
+        if Qt.mightBeRichText(self.source_file):
+            self.document_editor_tab.code_editor.setHtml(self.source_file)
+        else:
+            for line in self.source_file.splitlines():
+                self.document_editor_tab.code_editor.append(line)
+
+        self.build_ui()
+
+    @property
+    def read_only(self):
+        return self._read_only
+
+    @read_only.setter
+    def read_only(self, val):
+        if val:
+            self._read_only = True
+        else:
+            self._read_only = False
+
+    def build_ui(self):
+        FlatCAMObj.build_ui(self)
+        tab_here = False
+
+        # try to not add too many times a tab that it is already installed
+        for idx in range(self.app.ui.plot_tab_area.count()):
+            if self.app.ui.plot_tab_area.widget(idx).objectName() == self.options['name']:
+                tab_here = True
+                break
+
+        # add the tab if it is not already added
+        if tab_here is False:
+            self.app.ui.plot_tab_area.addTab(self.document_editor_tab, '%s' % _("Document Editor"))
+            self.document_editor_tab.setObjectName(self.options['name'])
+
+        # Switch plot_area to CNCJob tab
+        self.app.ui.plot_tab_area.setCurrentWidget(self.document_editor_tab)
+
+    def on_autocomplete_changed(self, state):
+        if state:
+            self.document_editor_tab.code_editor.completer_enable = True
+        else:
+            self.document_editor_tab.code_editor.completer_enable = False
+
+    def on_tab_size_change(self, val=None):
+        try:
+            self.ui.tab_size_spinner.returnPressed.disconnect(self.on_tab_size_change)
+        except TypeError:
+            pass
+
+        if val:
+            self.ui.tab_size_spinner.set_value(val)
+
+        tab_balue = int(self.ui.tab_size_spinner.get_value())
+        self.document_editor_tab.code_editor.setTabStopWidth(tab_balue)
+        self.app.defaults['document_tab_size'] = tab_balue
+
+        self.ui.tab_size_spinner.returnPressed.connect(self.on_tab_size_change)
+
+    def on_text_changed(self):
+        self.source_file = self.document_editor_tab.code_editor.toHtml()
+        # print(self.source_file)
+
+    def font_family(self, font):
+        # self.document_editor_tab.code_editor.selectAll()
+        font.setPointSize(float(self.ui.font_size_cb.get_value()))
+        self.document_editor_tab.code_editor.setCurrentFont(font)
+        self.font_name = self.ui.font_type_cb.currentFont().family()
+
+    def font_size(self):
+        # self.document_editor_tab.code_editor.selectAll()
+        self.document_editor_tab.code_editor.setFontPointSize(float(self.ui.font_size_cb.get_value()))
+
+    def on_bold_button(self):
+        if self.ui.font_bold_tb.isChecked():
+            self.document_editor_tab.code_editor.setFontWeight(QtGui.QFont.Bold)
+            self.font_bold = True
+        else:
+            self.document_editor_tab.code_editor.setFontWeight(QtGui.QFont.Normal)
+            self.font_bold = False
+
+    def on_italic_button(self):
+        if self.ui.font_italic_tb.isChecked():
+            self.document_editor_tab.code_editor.setFontItalic(True)
+            self.font_italic = True
+        else:
+            self.document_editor_tab.code_editor.setFontItalic(False)
+            self.font_italic = False
+
+    def on_underline_button(self):
+        if self.ui.font_under_tb.isChecked():
+            self.document_editor_tab.code_editor.setFontUnderline(True)
+            self.font_underline = True
+        else:
+            self.document_editor_tab.code_editor.setFontUnderline(False)
+            self.font_underline = False
+
+    # Setting font colors handlers
+    def on_font_color_entry(self):
+        self.app.defaults['document_font_color'] = self.ui.font_color_entry.get_value()
+        self.ui.font_color_button.setStyleSheet("background-color:%s" % str(self.app.defaults['document_font_color']))
+
+    def on_font_color_button(self):
+        current_color = QtGui.QColor(self.app.defaults['document_font_color'])
+
+        c_dialog = QtWidgets.QColorDialog()
+        font_color = c_dialog.getColor(initial=current_color)
+
+        if font_color.isValid() is False:
+            return
+
+        self.document_editor_tab.code_editor.setTextColor(font_color)
+        self.ui.font_color_button.setStyleSheet("background-color:%s" % str(font_color.name()))
+
+        new_val = str(font_color.name())
+        self.ui.font_color_entry.set_value(new_val)
+        self.app.defaults['document_font_color'] = new_val
+
+    # Setting selection colors handlers
+    def on_selection_color_entry(self):
+        self.app.defaults['document_sel_color'] = self.ui.sel_color_entry.get_value()
+        self.ui.sel_color_button.setStyleSheet("background-color:%s" % str(self.app.defaults['document_sel_color']))
+
+    def on_selection_color_button(self):
+        current_color = QtGui.QColor(self.app.defaults['document_sel_color'])
+
+        c_dialog = QtWidgets.QColorDialog()
+        sel_color = c_dialog.getColor(initial=current_color)
+
+        if sel_color.isValid() is False:
+            return
+
+        p = QtGui.QPalette()
+        p.setColor(QtGui.QPalette.Highlight, sel_color)
+        p.setColor(QtGui.QPalette.HighlightedText, QtGui.QColor('white'))
+
+        self.document_editor_tab.code_editor.setPalette(p)
+
+        self.ui.sel_color_button.setStyleSheet("background-color:%s" % str(sel_color.name()))
+
+        new_val = str(sel_color.name())
+        self.ui.sel_color_entry.set_value(new_val)
+        self.app.defaults['document_sel_color'] = new_val
+
+    def to_dict(self):
+        """
+        Returns a representation 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])
+
 # end of file

+ 2 - 2
FlatCAMPool.py

@@ -1,5 +1,5 @@
 from PyQt5 import QtCore
-from multiprocessing import Pool
+from multiprocessing import Pool, cpu_count
 import dill
 
 
@@ -23,7 +23,7 @@ class WorkerPool(QtCore.QObject):
 
     def __init__(self):
         super(WorkerPool, self).__init__()
-        self.pool = Pool(2)
+        self.pool = Pool(cpu_count())
 
     def add_task(self, task):
         print("adding task", task)

+ 2 - 2
FlatCAMTranslation.py

@@ -1,6 +1,5 @@
 # ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
@@ -8,14 +7,15 @@
 
 import os
 import sys
+import logging
 from pathlib import Path
 
 from PyQt5 import QtWidgets, QtGui
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import log
 import gettext
 
+log = logging.getLogger('base')
 
 # import builtins
 #

+ 58 - 16
ObjectCollection.py

@@ -1,14 +1,15 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # Author: Juan Pablo Caram (c)                             #
 # Date: 2/5/2014                                           #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
-# ########################################################## ##
+# ##########################################################
 # File modified by: Dennis Hayrullin                       #
-# ########################################################## ##
+# File modified by: Marius Stanciu                         #
+# ##########################################################
 
 # from PyQt5.QtCore import QModelIndex
 from FlatCAMObj import *
@@ -186,21 +187,27 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         ("gerber", "Gerber"),
         ("excellon", "Excellon"),
         ("geometry", "Geometry"),
-        ("cncjob", "CNC Job")
+        ("cncjob", "CNC Job"),
+        ("script", "Scripts"),
+        ("document", "Document"),
     ]
 
     classdict = {
         "gerber": FlatCAMGerber,
         "excellon": FlatCAMExcellon,
         "cncjob": FlatCAMCNCjob,
-        "geometry": FlatCAMGeometry
+        "geometry": FlatCAMGeometry,
+        "script": FlatCAMScript,
+        "document": FlatCAMDocument
     }
 
     icon_files = {
         "gerber": "share/flatcam_icon16.png",
         "excellon": "share/drill16.png",
         "cncjob": "share/cnc16.png",
-        "geometry": "share/geometry16.png"
+        "geometry": "share/geometry16.png",
+        "script": "share/script_new16.png",
+        "document": "share/notes16_1.png"
     }
 
     root_item = None
@@ -322,6 +329,14 @@ class ObjectCollection(QtCore.QAbstractItemModel):
                     self.app.ui.menuprojectedit.setVisible(False)
                 if type(obj) != FlatCAMGerber and type(obj) != FlatCAMExcellon and type(obj) != FlatCAMCNCjob:
                     self.app.ui.menuprojectviewsource.setVisible(False)
+                if type(obj) != FlatCAMGerber and type(obj) != FlatCAMGeometry and type(obj) != FlatCAMExcellon and \
+                        type(obj) != FlatCAMCNCjob:
+                    # meaning for Scripts and for Document type of FlatCAM object
+                    self.app.ui.menuprojectenable.setVisible(False)
+                    self.app.ui.menuprojectdisable.setVisible(False)
+                    self.app.ui.menuprojectedit.setVisible(False)
+                    self.app.ui.menuprojectproperties.setVisible(False)
+                    self.app.ui.menuprojectgeneratecnc.setVisible(False)
         else:
             self.app.ui.menuprojectgeneratecnc.setVisible(False)
 
@@ -411,17 +426,17 @@ class ObjectCollection(QtCore.QAbstractItemModel):
                     # rename the object
                     obj.options["name"] = deepcopy(data)
 
+                    self.app.object_status_changed.emit(obj, 'rename', old_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)
-                        self.app.ui.code_editor.set_model_data(self.app.myKeywords)
                     except Exception as e:
                         log.debug(
                             "setData() --> Could not remove the old object name from auto-completer model list. %s" %
                             str(e))
-
                     # obj.build_ui()
                     self.app.inform.emit(_("Object renamed from <b>{old}</b> to <b>{new}</b>").format(old=old_name,
                                                                                                       new=new_name))
@@ -490,7 +505,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
 
         self.app.should_we_save = True
 
-        self.app.object_status_changed.emit(obj, 'append')
+        self.app.object_status_changed.emit(obj, 'append', name)
 
         # decide if to show or hide the Notebook side of the screen
         if self.app.defaults["global_project_autohide"] is True:
@@ -547,7 +562,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         :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()")
+        # 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():
@@ -570,24 +585,33 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         # send signal with the object that is deleted
         # self.app.object_status_changed.emit(active.obj, 'delete')
 
+        # some objects add a Tab on creation, close it here
+        for idx in range(self.app.ui.plot_tab_area.count()):
+            if self.app.ui.plot_tab_area.widget(idx).objectName() == active.obj.options['name']:
+                self.app.ui.plot_tab_area.removeTab(idx)
+                break
+
         # 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)
-            self.app.ui.code_editor.set_model_data(self.app.myKeywords)
+            # this is not needed any more because now the code editor is created on demand
+            # self.app.ui.code_editor.set_model_data(self.app.myKeywords)
         except Exception as e:
             log.debug(
                 "delete_active() --> Could not remove the old object name from auto-completer model list. %s" % str(e))
 
-        self.beginRemoveRows(self.index(group.row(), 0, QtCore.QModelIndex()), active.row(), active.row())
+        self.app.object_status_changed.emit(active.obj, 'delete', name)
 
+        # ############ OBJECT DELETION FROM MODEL STARTS HERE ####################
+        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()
+        # ############ OBJECT DELETION FROM MODEL STOPS HERE ####################
+
         if self.app.is_legacy is False:
             self.app.plotcanvas.redraw()
 
@@ -605,6 +629,9 @@ class ObjectCollection(QtCore.QAbstractItemModel):
 
     def delete_all(self):
         FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.delete_all()")
+
+        self.app.object_status_changed.emit(None, 'delete_all', '')
+
         try:
             self.app.all_objects_list.clear()
 
@@ -688,6 +715,16 @@ class ObjectCollection(QtCore.QAbstractItemModel):
             log.error("[ERROR] Cause: %s" % str(e))
             raise
 
+    def set_exclusive_active(self, name):
+        """
+        Make the object with the name in parameters the only selected object
+
+        :param name: name of object to be selected and made the only active object
+        :return: None
+        """
+        self.set_all_inactive()
+        self.set_active(name)
+
     def set_inactive(self, name):
         """
         Unselect object by name from the project list. This triggers the
@@ -736,7 +773,12 @@ class ObjectCollection(QtCore.QAbstractItemModel):
             elif obj.kind == 'geometry':
                 self.app.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
                     color='red', name=str(obj.options['name'])))
-
+            elif obj.kind == 'script':
+                self.app.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
+                    color='orange', name=str(obj.options['name'])))
+            elif obj.kind == 'document':
+                self.app.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
+                    color='darkCyan', name=str(obj.options['name'])))
         except IndexError:
             # FlatCAMApp.App.log.debug("on_list_selection_change(): Index Error (Nothing selected?)")
             self.app.inform.emit('')

+ 194 - 1
README.md

@@ -9,12 +9,205 @@ CAD program, and create G-Code for Isolation routing.
 
 =================================================
 
+14.10.2019
+
+- modified the result highlight color in Check Rules Tool
+- added the Check Rules Tool parameters to the unit conversion list
+- converted more of the Preferences entries to FCDoubleSpinner and FCSpinner
+- converted all ObjectUI entries to FCDoubleSpinner and FCSpinner
+- updated the translation files (~ 89% translation level)
+- changed the splash screen as it seems that FlatCAM beta will never be more than beta
+- changed some of the signals from returnPressed to editingFinished due of now using the SpinBoxes
+- fixed an issue that caused the impossibility to load a GCode file that contained the % symbol even when was loaded in a regular way from the File menu
+- re-added the CNC tool diameter entry for the CNCjob object in Selected tab.FCSpinner
+- since the CNCjob geometry creation is only useful for graphical purposes and have no impact on the GCode creation I have removed the cascaded union on the GCode geometry therefore speeding up the Gcode display by many factors (perhaps hundreds of times faster)
+- added a secondary link in the bookmark manager
+- fixed the bookmark manager order of bookmark links; first two links are always protected from deletion or drag-and-drop to other positions
+- fixed a whole load of PyQT signal problems generated by recent changes to the usage of SpinBoxes; added a signal returnPressed for the FCSpinner and for FCDoubleSpinner
+- fixed issue in Paint Tool where the first added tool was expected to have a float diameter but it was a string
+- updated the translation files to the latest state in the app
+
+13.10.2019
+
+- fixed a bug in the Merge functions
+- fixed the Export PNG function when using the 2D legacy graphic engine
+- added a new capability to toggle the grid lines for both graphic engines: menu link in View and key shortcut combo ALT+G
+- changed the grid colors for 3D graphic engine when in Dark mode
+- enhanced the Tool Film adding the Film adjustments and added the GUI in Preferences
+- set the GUI layout in Preferences for a new category named Tools 2
+- added the Preferences for Check Rules Tool and for Optimal Tool and also updated the Film Tool to use the default settings in Preferences
+
+12.10.2019
+
+- fixed the Gerber Parser convert units unnecessary usage. The only units conversion should be done when creating the new object, after the parsing
+- more fixes in Rules Check Tool
+- optimized the Move Tool
+- added support for key-based panning in 3D graphic engine. Moving the mouse wheel while pressing the CTRL key will pan up-down and while pressing SHIFT key will pan left-right
+- fixed a bug in NCC Tool and start trying to make the App responsive while the NCC tool is run in a non-threaded way
+- fixed a GUI bug with the QMenuBar recently introduced
+
+11.10.2019
+
+- added a Bookmark Manager and a Bookmark menu in the Help Menu
+- added an initial support for rows drag and drop in FCTable in GUIElements; it crashes for CellWidgets for now, if CellWidgetsare in the table rows
+- fixed some issues in the Bookmark Manager
+- modified the Bookmark manager to be installed as a widget tab in Plot Area; fixed the drag & drop function for the table rows that have CellWidgets inside
+- marked in gray color the rows in the Bookmark Manager table that will populate the BookMark menu
+- made sure that only one instance of the BookmarkManager class is active at one time
+
+10.10.2019
+
+- fixed Tool Move to work only for objects that are selected but also plotted, therefore disabled objects will not be moved even if selected
+
+9.10.2019
+
+- updated the Rules Check Tool - solved some issues
+- made FCDoubleSpinner to use either comma or dot as a decimal separator
+- fixed the FCDoubleSpinner to only allow the amount of decimals already set with set_precision()
+- fixed ToolPanelize to use FCDoubleSpinner in some places
+
+8.10.2019
+
+- modified the FCSpinner and FCDoubleSpinner GUI elements such that the wheel event will not change the values inside unless there is a focus in the lineedit of the SpinBox
+- in Preferences General, Gerber, Geometry, Excellon, CNCJob sections made all the input fields of type SpinBox (where possible)
+- updated the Distance Tool utility geometry color to adapt to the dark theme canvas
+- Toggle Code Editor now works as expected even when the user is closing the Editor tab and not using the command Toggle Code Editor
+- more changes in Preferences GUI, replacing the FCEntries with Spinners
+- some small fixes in toggle units conversion
+- small GUI changes
+
+7.10.2019
+
+- fixed an conflict in a signal usage that was triggered by Tool SolderPaste when a new project was created
+- updated Optimal Tool to display both points coordinates that made a distance (and the minimum) not only the middle point (which is still the place where the jump happen)
+- added a dark theme to FlatCAM (only for canvas). The selection is done in Edit -> Preferences -> General -> GUI Settings
+- updated the .POT file and worked a bit in the romanian translation
+- small changes: reduced the thickness of the axis in 3D mode from 3 pixels to 1 pixel
+- made sure that is the text in the source file of a FlatCAMDocument is HTML is loaded as such
+- added inverted icons
+
+6.10.2019
+
+- remade the Mark area Tool in Gerber Editor to be able to clear the markings and also to delete the marked polygons (Gerber apertures)
+- working in adding to the Optimal Tool the rest of the distances found in the Gerber and the locations associated; added GUI
+- added display of the results for the Rules Check Tool in a formatted way
+- made the Rules Check Tool document window Read Only
+- made Excellon and Gerber classes from camlib into their own files in the flatcamParser folder
+- moved the ApertureMacro class from camlib to ParseGerber file
+- moved back the ApertureMacro class to camlib for now and made some import changes in the new ParseGerber and ParseExcellon classes
+- some changes to the tests - perhaps I will try adding a few tests in the future
+- changed the Jump To icon and reverted some changes to the parseGerber and ParseExcellon classes
+- updated Tool Optimal with display of all distances (and locations of the middle point between where they happen) found in the Gerber Object
+
+5.10.2019
+
+- remade the Tool Calculators to use the QSpinBox in order to simplify the user interaction and remove possible errors
+- remade: Tool Cutout, Tool 2Sided, Tool Image, Panelize Tool, NCC Tool, Paint Tool  to use the QSpinBox GUI elements
+- optimized the Transformation Tool both in GUI and in functionality and replaced the entries with QSpinBox
+- fixed an issue with the tool table context menu in Paint Tool
+- made some changes in the GUI in Paint Tool, NCC Tool and SolderPaste Tool
+- changed some of the icons; added attributions for icons source in the About FlatCAM window
+- added a new tool in the Geometry Editor named Explode which is the opposite of Union Tool: it will explode the polygons into lines
+
+4.10.2019
+
+- updated the Film Tool and added the ability to generate Punched Positive films (holes in the pads) when a Gerber file is the film's source. The punch holes source can be either an Excellon file or the pads center
+- optimized Rules Check Tool so it runs faster when doing Copper 2 Copper rule
+- small GUI changes in Optimal Tool and in Film Tool
+- some PEP8 corrections
+- some code annotations to make it easier to navigate in the FlatCAMGUI.py
+- fixed exit FullScreen with Escape key
+- added a new menu category in the MenuBar named 'Objects'. It will hold the objects found in the Project tab. Useful when working in FullScreen
+- disabled a log.debug in ObjectColection.get_by_name()
+- added a Toggle Notebook button named 'NB' in the QMenBar which toggle the notebook
+- in Gerber isolation section, the tool dia value is updated when changing from Circular to V-shape and reverse
+- in Tool Film, when punching holes in a positive film, if the resulting object geometry is the same as the source object geometry, the film will not ge generated
+- fixed a bug that when a Gerber object is edited and it has as solid_geometry a single Polygon, saving the result was failing due of len() function not working on a single Polygon
+- added the Distance Tool, Distance Min Tool, Jump To and Set Origin functions to the Edit Toolbar
+
+3.10.2019
+
+- previously I've added the initial layout for the FlatCAMDocument object
+- added more editing features in the Selected Tab for the FlatCAMDocument object
+
+2.10.2019
+
+- fixed bug in Geometry Editor that did not allow the copy of geometric elements
+- created a new class that holds all the Code Editor functionality and integrated as a Editor in FlatCAM, the location is in flatcamEditors folder
+- remade all the functions for view_source, scripts and view_code to use the new TextEditor class; now all the Code Editor tabs are being kept alive, before only one could be in an open state
+- changed the name of the new object FlatCAMNotes to a more general one FlatCAMDocument
+- changed the way a new FlatCAMScript object is made, the method that is processing the Tcl commands when the Run button is clicked is moved to the FlatCAMObj.FlatCAMScript() class
+- reused the Multiprocessing Pool declared in the App for the ToolRulesCheck() class
+- adapted the Project context menu for the new types of FLatCAM objects
+- modified the setup_recent_files to accommodate the new FlatCAM objects
+- made sure that when an FlatCAMScript object is deleted, it's associated Tab is closed
+- fixed the FlatCMAScript object saving when project is saved (loading a project with this script object is not working yet)
+- fixed the FlatCMAScript object when loading it from a project
+
+1.10.2019
+
+- fixed the FCSpinner and FCDoubleSpinner GUI elements to select all on first click and deselect on second click in the Spinbox LineEdit
+- for Gerber object in Selected Tab added ability to chose a V-Shape tool and therefore control the isolation better by adjusting the cut width of the isolation in function of the cut depth, tip width of the tool and the tip angle of the tool
+- when in Gerber UI is selected the V-Shape tool, all those parameters (tip dia, tip angle, tool_type = 'V' and cut Z) are transferred to the generated Geometry and prefilled in the Geoemtry UI
+- added a fix in the Gerber parser to work even when there is no information about zero suppression in the Gerber file
+- added new settings in Edit -> Preferences -> Gerber for Gerber Units and Gerber Zeros to be used as defaults in case that those informations are missing from the Gerber file
+- added new settings for the Gerber newly introduced feature to isolate with the V-Shape tools (tip dia, tip angle, tool_type and cut Z) in Edit -> Preferences -> Gerber Advanced
+- made those settings just added for Gerber, to be updated on object creation
+- added the Geo Tolerance parameter to those that are converted from MM to INCH
+- added two new FlatCAM objects: FlatCAMScript and FlatCAMNotes
+
+30.09.2019
+
+- modified the Distance Tool such that the number of decimals all over the tool is set in one place by the self.decimals
+- added a new tool named Minimum Distance Tool who will calculate the minimum distance between two objects; key shortcut: SHIFT + M
+- finished the Minimum Distance Tool in case of using it at the object level (not in Editors)
+- completed the Minimum Distance Tool by adding the usage in Editors
+- made the Minimum Distance Tool more precise for the Excellon Editor since in the Excellon Editor the holes shape are represented as a cross line but in reality they should be evaluated as circles
+- small change in the UI layout for Check Rules Tool by adding a new rule (Check trace size)
+- changed a tooltip in Optimal Tool
+- in Optimal Tool added display of how frequent that minimum distance is found
+- in Tool Distance and Tool Minimal Distance made the entry fields read-only
+- in Optimal Tool added the display of the locations where the minimum distance was detected
+- added support to use Multi Processing (multi core usage, not simple threading) in Rules Check Tool
+- in Rules Check Tool added the functionality for the following rules: Hole Size, Trace Size, Hole to Hole Clearance
+- in Rules Check Tool added the functionality for Copper to Copper Clearance
+- in Rules Check Tool added the functionality for Copper to Outline Clearance, Silk to Silk Clearance, Silk to Solder Mask Clearance, Silk to Outline Clearance, Minimum Solder Mask Sliver, Minimum Annular Ring
+- fixes to cover all possible situations for the Minimum Annular Ring Rule in Rules Check Tool
+- some fixes in Rules Check Tool and added a QSignal that is fired at the end of the job
+
+29.09.2019
+
+- work done for the GUI layout of the Rule Check Tool
+- setup signals in the Rules Check Tool GUI
+- changed the name of the Measurement Tool to Distance Tool. Moved it's location to the Edit Menu
+- added Angle parameter which is continuously updated to the Distance Tool
+
+28.09.2019
+
+- changed the icon for Open Script and reused it for the Check Rules Tool
+- added a new tool named "Optimal Tool" which will determine the minimum distance between the copper features for a Gerber object, in fact determining the maximum diameter for a isolation tool that can be used for a complete isolation
+- fixed the ToolMeasurement geometry not being displayed
+- fixed a bug in Excellon Editor that crashed the app when editing the first tool added automatically into a new black Excellon file
+- made sure that if the big mouse cursor is selected, the utility geometry in Excellon Editor has a thicker line width (2 pixels now) so it is visible over the geometry of the mouse cursor
+- fixed issue #319 where generating a CNCJob from a geometry made with NCC Tool made the app crash
+- replaced in FlatCAM Tools and in FLatCAMObj.py  and in Editors all references to hardcoded decimals in string formats for tools with a variable declared in the __init__()
+- fixed a small bug that made app crash when the splash screen is disabled: it was trying to close it without being open
+
 27.09.2019
 
 - optimized the toggle axis command
-- added posibility of using a big mouse cursor or a small mouse cursor. The big mouse cursor is made from 2 infinite lines. This was implemented for both graphic engines
+- added possibility of using a big mouse cursor or a small mouse cursor. The big mouse cursor is made from 2 infinite lines. This was implemented for both graphic engines
 - added ability to change the cursor size when the small mouse cursor is selected in Preferences -> General
 - removed the line that remove the spaces from the path parameter in the Tcl commands that open something (Gerber, Gcode, Excellon)
+- fixed issue with the old SysTray icon not hidden when the application is restarted programmatically
+- if an object is edited but the result is not saved, the app will reload the edited object UI and set the Selected tab as active
+- made the mouse cursor (big, small) change in real time for both graphic engines
+- started to work on a new FlatCAM tool: Rules Check
+- created the GUI for the Rule Check Tool
+- if there are (x, y) coordinates in the clipboard, when launching the "Jump to" function, those coordinates will be preloaded in the Dialog box.
+- when the combo SHIFT + LMB is executed there is no longer a deselection of objects
+- when the "Jump to" function is called, the mouse cursor (if active) will be moved to the new position and the screen position labels will be updated accordingly
+
 
 27.09.2019
 

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 210 - 1829
camlib.py


+ 27 - 28
flatcamEditors/FlatCAMExcEditor.py

@@ -1,6 +1,5 @@
 # ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 8/17/2019                                          #
 # MIT Licence                                              #
@@ -19,6 +18,7 @@ from rtree import index as rtindex
 from camlib import *
 from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, LengthEntry, RadioSet, SpinBoxDelegate
 from flatcamEditors.FlatCAMGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, FlatCAMGeoEditor
+from flatcamParsers.ParseExcellon import Excellon
 
 from copy import copy, deepcopy
 
@@ -1984,7 +1984,7 @@ class FlatCAMExcEditor(QtCore.QObject):
         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.returnPressed.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.tools_table_exc.cellPressed.connect(self.on_row_selected)
@@ -2014,7 +2014,10 @@ class FlatCAMExcEditor(QtCore.QObject):
         # VisPy Visuals
         if self.app.is_legacy is False:
             self.shapes = self.app.plotcanvas.new_shape_collection(layers=1)
-            self.tool_shape = self.app.plotcanvas.new_shape_collection(layers=1)
+            if self.app.plotcanvas.big_cursor is True:
+                self.tool_shape = self.app.plotcanvas.new_shape_collection(layers=1, line_width=2)
+            else:
+                self.tool_shape = self.app.plotcanvas.new_shape_collection(layers=1)
         else:
             from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
             self.shapes = ShapeCollectionLegacy(obj=self, app=self.app, name='shapes_exc_editor')
@@ -2043,6 +2046,9 @@ class FlatCAMExcEditor(QtCore.QObject):
 
         self.complete = False
 
+        # Number of decimals used by tools in this class
+        self.decimals = 4
+
         def make_callback(thetool):
             def f():
                 self.on_tool_select(thetool)
@@ -2113,16 +2119,18 @@ class FlatCAMExcEditor(QtCore.QObject):
         # updated units
         self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
 
+        if self.units == "IN":
+            self.decimals = 4
+        else:
+            self.decimals = 2
+
         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('%.4f' % self.exc_obj.tools[drill['tool']]['C'])
-                else:
-                    tool_dia = float('%.2f' % self.exc_obj.tools[drill['tool']]['C'])
+                tool_dia = float('%.*f' % (self.decimals, self.exc_obj.tools[drill['tool']]['C']))
 
                 try:
                     self.points_edit[tool_dia].append(drill['point'])
@@ -2132,10 +2140,7 @@ class FlatCAMExcEditor(QtCore.QObject):
         # build the self.slot_points_edit dict {dimaters: {"start": Point, "stop": Point}}
         for slot in self.exc_obj.slots:
             if slot['tool'] in self.exc_obj.tools:
-                if self.units == 'IN':
-                    tool_dia = float('%.4f' % self.exc_obj.tools[slot['tool']]['C'])
-                else:
-                    tool_dia = float('%.2f' % self.exc_obj.tools[slot['tool']]['C'])
+                tool_dia = float('%.*f' % (self.decimals, self.exc_obj.tools[slot['tool']]['C']))
 
                 try:
                     self.slot_points_edit[tool_dia].append({
@@ -2171,10 +2176,7 @@ class FlatCAMExcEditor(QtCore.QObject):
             # Excellon file has no tool diameter information. In this case do not order the diameter in the table
             # but use the real order found in the exc_obj.tools
             for k, v in self.exc_obj.tools.items():
-                if self.units == 'IN':
-                    tool_dia = float('%.4f' % v['C'])
-                else:
-                    tool_dia = float('%.2f' % v['C'])
+                tool_dia = float('%.*f' % (self.decimals, v['C']))
                 self.tool2tooldia[int(k)] = tool_dia
 
         # Init GUI
@@ -2271,12 +2273,9 @@ class FlatCAMExcEditor(QtCore.QObject):
             self.tools_table_exc.setItem(self.tool_row, 0, idd)  # 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('%.4f' % self.olddia_newdia[tool_no])
+            # There are no drill bits in MM with more than 2 decimals diameter
+            # For INCH the decimals should be no more than 4. There are no drills under 10mils
+            dia = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, self.olddia_newdia[tool_no]))
 
             dia.setFlags(QtCore.Qt.ItemIsEnabled)
 
@@ -2474,9 +2473,9 @@ class FlatCAMExcEditor(QtCore.QObject):
             else:
                 if isinstance(dia, list):
                     for dd in dia:
-                        deleted_tool_dia_list.append(float('%.4f' % dd))
+                        deleted_tool_dia_list.append(float('%.*f' % (self.decimals, dd)))
                 else:
-                    deleted_tool_dia_list.append(float('%.4f' % dia))
+                    deleted_tool_dia_list.append(float('%.*f' % (self.decimals, dia)))
         except Exception as e:
             self.app.inform.emit('[WARNING_NOTCL] %s' %
                                  _("Select a tool in Tool Table"))
@@ -2814,7 +2813,7 @@ class FlatCAMExcEditor(QtCore.QObject):
             self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
             self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
             self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
-            self.app.plotcanvas.graph_event_disconnect('mouse_double_click', self.app.on_double_click_over_plot)
+            self.app.plotcanvas.graph_event_disconnect('mouse_double_click', self.app.on_mouse_double_click_over_plot)
         else:
             self.app.plotcanvas.graph_event_disconnect(self.app.mp)
             self.app.plotcanvas.graph_event_disconnect(self.app.mm)
@@ -2844,7 +2843,7 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
                                                               self.app.on_mouse_click_release_over_plot)
         self.app.mdc = self.app.plotcanvas.graph_event_connect('mouse_double_click',
-                                                               self.app.on_double_click_over_plot)
+                                                               self.app.on_mouse_double_click_over_plot)
         self.app.collection.view.clicked.connect(self.app.collection.on_mouse_down)
 
         if self.app.is_legacy is False:
@@ -2977,7 +2976,7 @@ class FlatCAMExcEditor(QtCore.QObject):
 
         # add a first tool in the Tool Table but only if the Excellon Object is empty
         if not self.tool2tooldia:
-            self.on_tool_add(tooldia=float(self.app.defaults['excellon_editor_newdia']))
+            self.on_tool_add(tooldia=float('%.2f' % float(self.app.defaults['excellon_editor_newdia'])))
 
     def update_fcexcellon(self, exc_obj):
         """
@@ -3657,7 +3656,7 @@ class FlatCAMExcEditor(QtCore.QObject):
             x, y = self.app.geo_editor.snap(x, y)
 
             # Update cursor
-            self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color='black',
+            self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color=self.app.cursor_color_3D,
                                          size=self.app.defaults["global_cursor_size"])
 
         self.snap_x = x
@@ -3706,7 +3705,7 @@ class FlatCAMExcEditor(QtCore.QObject):
             self.app.selection_type = None
 
         # Update cursor
-        self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color='black',
+        self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color=self.app.cursor_color_3D,
                                      size=self.app.defaults["global_cursor_size"])
 
     def on_canvas_key_release(self, event):

+ 110 - 24
flatcamEditors/FlatCAMGeoEditor.py

@@ -236,7 +236,7 @@ class TextInputTool(FlatCAMTool):
 
         self.font_type_cb = QtWidgets.QFontComboBox(self)
         self.font_type_cb.setCurrentFont(f_current)
-        self.form_layout.addRow("Font:", self.font_type_cb)
+        self.form_layout.addRow(QtWidgets.QLabel('%s:' % _("Font")), self.font_type_cb)
 
         # Flag variables to show if font is bold, italic, both or none (regular)
         self.font_bold = False
@@ -308,7 +308,7 @@ class TextInputTool(FlatCAMTool):
         self.font_italic_tb.setIcon(QtGui.QIcon('share/italic32.png'))
         hlay.addWidget(self.font_italic_tb)
 
-        self.form_layout.addRow("Size:", hlay)
+        self.form_layout.addRow(QtWidgets.QLabel('%s:' % "Size"), hlay)
 
         # Text input
         self.text_input_entry = FCTextAreaRich()
@@ -317,7 +317,7 @@ class TextInputTool(FlatCAMTool):
         # 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)
+        self.form_layout.addRow(QtWidgets.QLabel('%s:' % _("Text")), self.text_input_entry)
 
         # Buttons
         hlay1 = QtWidgets.QHBoxLayout()
@@ -973,13 +973,13 @@ class TransformEditorTool(FlatCAMTool):
         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)
+        self.rotate_entry.editingFinished.connect(self.on_rotate)
+        self.skewx_entry.editingFinished.connect(self.on_skewx)
+        self.skewy_entry.editingFinished.connect(self.on_skewy)
+        self.scalex_entry.editingFinished.connect(self.on_scalex)
+        self.scaley_entry.editingFinished.connect(self.on_scaley)
+        self.offx_entry.editingFinished.connect(self.on_offx)
+        self.offy_entry.editingFinished.connect(self.on_offy)
 
         self.set_tool_ui()
 
@@ -1324,7 +1324,7 @@ class TransformEditorTool(FlatCAMTool):
                     # 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
+                    # get mirroring coords from the center of an all-enclosing bounding box
                     else:
                         # first get a bounding box to fit all
                         for sha in shape_list:
@@ -2455,6 +2455,61 @@ class FCSelect(DrawTool):
         return ""
 
 
+class FCExplode(FCShapeTool):
+    def __init__(self, draw_app):
+        FCShapeTool.__init__(self, draw_app)
+        self.name = 'explode'
+
+        self.draw_app = draw_app
+
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception as e:
+            pass
+
+        self.storage = self.draw_app.storage
+        self.origin = (0, 0)
+        self.destination = None
+
+        self.draw_app.active_tool = self
+        if len(self.draw_app.get_selected()) == 0:
+            self.draw_app.app.inform.emit('[WARNING_NOTCL] %s...' %
+                                          _("No shape selected. Select a shape to explode"))
+        else:
+            self.make()
+
+    def make(self):
+        to_be_deleted_list = list()
+        lines = list()
+
+        for shape in self.draw_app.get_selected():
+            to_be_deleted_list.append(shape)
+            geo = shape.geo
+            ext_coords = list(geo.exterior.coords)
+
+            for c in range(len(ext_coords)):
+                if c < len(ext_coords) - 1:
+                    lines.append(LineString([ext_coords[c], ext_coords[c + 1]]))
+
+            for int_geo in geo.interiors:
+                int_coords = list(int_geo.coords)
+                for c in range(len(int_coords)):
+                    if c < len(int_coords):
+                        lines.append(LineString([int_coords[c], int_coords[c + 1]]))
+
+        for shape in to_be_deleted_list:
+            self.draw_app.storage.remove(shape)
+            if shape in self.draw_app.selected:
+                self.draw_app.selected.remove(shape)
+
+        geo_list = list()
+        for line in lines:
+            geo_list.append(DrawToolShape(line))
+        self.geometry = geo_list
+        self.draw_app.on_shape_complete()
+        self.draw_app.app.inform.emit('[success] %s...' % _("Done. Polygons exploded into lines."))
+
+
 class FCMove(FCShapeTool):
     def __init__(self, draw_app):
         FCShapeTool.__init__(self, draw_app)
@@ -3015,7 +3070,9 @@ class FlatCAMGeoEditor(QtCore.QObject):
             "transform": {"button": self.app.ui.geo_transform_btn,
                       "constructor": FCTransform},
             "copy": {"button": self.app.ui.geo_copy_btn,
-                     "constructor": FCCopy}
+                     "constructor": FCCopy},
+            "explode": {"button": self.app.ui.geo_explode_btn,
+                     "constructor": FCExplode}
         }
 
         # # ## Data
@@ -3104,6 +3161,9 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         self.rtree_index = rtindex.Index()
 
+        # Number of decimals used by tools in this class
+        self.decimals = 4
+
         def entry2option(option, entry):
             try:
                 self.options[option] = float(entry.text())
@@ -3330,7 +3390,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
             self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
             self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
             self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
-            self.app.plotcanvas.graph_event_disconnect('mouse_double_click', self.app.on_double_click_over_plot)
+            self.app.plotcanvas.graph_event_disconnect('mouse_double_click', self.app.on_mouse_double_click_over_plot)
         else:
 
             self.app.plotcanvas.graph_event_disconnect(self.app.mp)
@@ -3377,7 +3437,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
                                                               self.app.on_mouse_click_release_over_plot)
         self.app.mdc = self.app.plotcanvas.graph_event_connect('mouse_double_click',
-                                                               self.app.on_double_click_over_plot)
+                                                               self.app.on_mouse_double_click_over_plot)
         # self.app.collection.view.clicked.connect(self.app.collection.on_mouse_down)
 
         if self.app.is_legacy is False:
@@ -3602,6 +3662,14 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         self.replot()
 
+        # updated units
+        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+
+        if self.units == "IN":
+            self.decimals = 4
+        else:
+            self.decimals = 2
+
         # start with GRID toolbar activated
         if self.app.ui.grid_snap_btn.isChecked() is False:
             self.app.ui.grid_snap_btn.trigger()
@@ -3755,7 +3823,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
             x, y = self.snap(x, y)
 
             # Update cursor
-            self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color='black',
+            self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color=self.app.cursor_color_3D,
                                          size=self.app.defaults["global_cursor_size"])
 
         self.snap_x = x
@@ -4030,7 +4098,8 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
             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))
+                                                     tolerance=self.fcgeometry.drawing_tolerance,
+                                                     linewidth=linewidth))
 
             if type(geometry) == Point:
                 pass
@@ -4070,7 +4139,12 @@ class FlatCAMGeoEditor(QtCore.QObject):
     def on_shape_complete(self):
         self.app.log.debug("on_shape_complete()")
 
-        geom = self.active_tool.geometry.geo
+        geom = []
+        try:
+            for shape in self.active_tool.geometry:
+                geom.append(shape.geo)
+        except TypeError:
+            geom = self.active_tool.geometry.geo
 
         if self.app.defaults['geometry_editor_milling_type'] == 'cl':
             # reverse the geometry coordinates direction to allow creation of Gcode for  climb milling
@@ -4082,9 +4156,14 @@ class FlatCAMGeoEditor(QtCore.QObject):
                             pl.append(Polygon(p.exterior.coords[::-1], p.interiors))
                         elif isinstance(p, LinearRing):
                             pl.append(Polygon(p.coords[::-1]))
-                        # elif isinstance(p, LineString):
-                        #     pl.append(LineString(p.coords[::-1]))
-                geom = MultiPolygon(pl)
+                        elif isinstance(p, LineString):
+                            pl.append(LineString(p.coords[::-1]))
+                try:
+                    geom = MultiPolygon(pl)
+                except TypeError:
+                    # this may happen if the geom elements are made out of LineStrings because you can't create a
+                    # MultiPolygon out of LineStrings
+                    pass
             except TypeError:
                 if isinstance(geom, Polygon) and geom is not None:
                     geom = Polygon(geom.exterior.coords[::-1], geom.interiors)
@@ -4099,8 +4178,15 @@ class FlatCAMGeoEditor(QtCore.QObject):
                 log.debug("FlatCAMGeoEditor.on_shape_complete() Error --> %s" % str(e))
                 return 'fail'
 
+        shape_list = list()
+        try:
+            for geo in geom:
+                shape_list.append(DrawToolShape(geo))
+        except TypeError:
+            shape_list.append(DrawToolShape(geom))
+
         # Add shape
-        self.add_shape(DrawToolShape(geom))
+        self.add_shape(shape_list)
 
         # Remove any utility shapes
         self.delete_utility_geometry()
@@ -4170,7 +4256,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # # ## 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']
+                snap_x_ = round(x / float(self.options["global_gridx"])) * float(self.options['global_gridx'])
             else:
                 snap_x_ = x
 
@@ -4178,12 +4264,12 @@ class FlatCAMGeoEditor(QtCore.QObject):
             # 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']
+                    snap_y_ = round(y / float(self.options["global_gridx"])) * float(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']
+                    snap_y_ = round(y / float(self.options["global_gridy"])) * float(self.options['global_gridy'])
                 else:
                     snap_y_ = y
             nearest_grid_distance = distance((x, y), (snap_x_, snap_y_))

+ 111 - 56
flatcamEditors/FlatCAMGrbEditor.py

@@ -1,6 +1,5 @@
 # ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 8/17/2019                                          #
 # MIT Licence                                              #
@@ -24,6 +23,7 @@ from camlib import *
 from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, LengthEntry, RadioSet, \
     SpinBoxDelegate, EvalEntry, EvalEntry2, FCInputDialog, FCButton, OptionalInputSection, FCCheckBox
 from FlatCAMObj import FlatCAMGerber
+from flatcamParsers.ParseGerber import Gerber
 from FlatCAMTool import FlatCAMTool
 
 from numpy.linalg import norm as numpy_norm
@@ -1809,28 +1809,46 @@ class FCMarkArea(FCShapeTool):
         self.activate_markarea()
 
     def activate_markarea(self):
-        self.draw_app.hide_tool('all')
         self.draw_app.ma_tool_frame.show()
 
         # clear previous marking
         self.draw_app.ma_annotation.clear(update=True)
 
         try:
-            self.draw_app.ma_threshold__button.clicked.disconnect()
+            self.draw_app.ma_threshold_button.clicked.disconnect()
         except (TypeError, AttributeError):
             pass
-        self.draw_app.ma_threshold__button.clicked.connect(self.on_markarea_click)
+        self.draw_app.ma_threshold_button.clicked.connect(self.on_markarea_click)
+
+        try:
+            self.draw_app.ma_delete_button.clicked.disconnect()
+        except TypeError:
+            pass
+        self.draw_app.ma_delete_button.clicked.connect(self.on_markarea_delete)
+
+        try:
+            self.draw_app.ma_clear_button.clicked.disconnect()
+        except TypeError:
+            pass
+        self.draw_app.ma_clear_button.clicked.connect(self.on_markarea_clear)
 
     def deactivate_markarea(self):
-        self.draw_app.ma_threshold__button.clicked.disconnect()
+        self.draw_app.ma_threshold_button.clicked.disconnect()
         self.complete = True
         self.draw_app.select_tool("select")
         self.draw_app.hide_tool(self.name)
 
     def on_markarea_click(self):
         self.draw_app.on_markarea()
+
+    def on_markarea_clear(self):
+        self.draw_app.ma_annotation.clear(update=True)
         self.deactivate_markarea()
 
+    def on_markarea_delete(self):
+        self.draw_app.delete_marked_polygons()
+        self.on_markarea_clear()
+
     def clean_up(self):
         self.draw_app.selected = []
         self.draw_app.apertures_table.clearSelection()
@@ -2332,6 +2350,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
         self.app = app
         self.canvas = self.app.plotcanvas
+        self.decimals = 4
 
         # Current application units in Upper Case
         self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
@@ -2581,7 +2600,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.ma_tool_frame.hide()
 
         # Title
-        ma_title_lbl = QtWidgets.QLabel('<b>%s:</b>' % _('Mark polygon areas'))
+        ma_title_lbl = QtWidgets.QLabel('<b>%s:</b>' % _('Mark polygons'))
         ma_title_lbl.setToolTip(
             _("Mark the polygon areas.")
         )
@@ -2596,16 +2615,18 @@ class FlatCAMGrbEditor(QtCore.QObject):
             _("The threshold value, all areas less than this are marked.\n"
               "Can have a value between 0.0000 and 9999.9999")
         )
-        self.ma_upper_threshold_entry = FCEntry()
-        self.ma_upper_threshold_entry.setValidator(QtGui.QDoubleValidator(0.0000, 9999.9999, 4))
+        self.ma_upper_threshold_entry = FCDoubleSpinner()
+        self.ma_upper_threshold_entry.set_precision(self.decimals)
+        self.ma_upper_threshold_entry.set_range(0, 10000)
 
         self.ma_lower_threshold_lbl = QtWidgets.QLabel('%s:' % _("Area LOWER threshold"))
         self.ma_lower_threshold_lbl.setToolTip(
             _("The threshold value, all areas more than this are marked.\n"
               "Can have a value between 0.0000 and 9999.9999")
         )
-        self.ma_lower_threshold_entry = FCEntry()
-        self.ma_lower_threshold_entry.setValidator(QtGui.QDoubleValidator(0.0000, 9999.9999, 4))
+        self.ma_lower_threshold_entry = FCDoubleSpinner()
+        self.ma_lower_threshold_entry.set_precision(self.decimals)
+        self.ma_lower_threshold_entry.set_range(0, 10000)
 
         ma_form_layout.addRow(self.ma_lower_threshold_lbl, self.ma_lower_threshold_entry)
         ma_form_layout.addRow(self.ma_upper_threshold_lbl, self.ma_upper_threshold_entry)
@@ -2614,8 +2635,23 @@ class FlatCAMGrbEditor(QtCore.QObject):
         hlay_ma = QtWidgets.QHBoxLayout()
         self.ma_tools_box.addLayout(hlay_ma)
 
-        self.ma_threshold__button = QtWidgets.QPushButton(_("Go"))
-        hlay_ma.addWidget(self.ma_threshold__button)
+        self.ma_threshold_button = QtWidgets.QPushButton(_("Mark"))
+        self.ma_threshold_button.setToolTip(
+            _("Mark the polygons that fit within limits.")
+        )
+        hlay_ma.addWidget(self.ma_threshold_button)
+
+        self.ma_delete_button = QtWidgets.QPushButton(_("Delete"))
+        self.ma_delete_button.setToolTip(
+            _("Delete all the marked polygons.")
+        )
+        hlay_ma.addWidget(self.ma_delete_button)
+
+        self.ma_clear_button = QtWidgets.QPushButton(_("Clear"))
+        self.ma_clear_button.setToolTip(
+            _("Clear all the markings.")
+        )
+        hlay_ma.addWidget(self.ma_clear_button)
 
         # ######################
         # ### Add Pad Array ####
@@ -2786,27 +2822,30 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # # ## Data
         self.active_tool = None
 
-        self.storage_dict = {}
-        self.current_storage = []
+        self.storage_dict = dict()
+        self.current_storage = list()
 
-        self.sorted_apid = []
+        self.sorted_apid = list()
 
-        self.new_apertures = {}
-        self.new_aperture_macros = {}
+        self.new_apertures = dict()
+        self.new_aperture_macros = dict()
 
         # store here the plot promises, if empty the delayed plot will be activated
-        self.grb_plot_promises = []
+        self.grb_plot_promises = list()
 
         # dictionary to store the tool_row and aperture codes in Tool_table
         # it will be updated everytime self.build_ui() is called
-        self.olddia_newdia = {}
+        self.olddia_newdia = dict()
 
-        self.tool2tooldia = {}
+        self.tool2tooldia = dict()
 
         # 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_aperture_selected = None
-        self.utility = []
+        self.utility = list()
+
+        # this will store the polygons marked by mark are to be perhaps deleted
+        self.geo_to_delete = list()
 
         # this will flag if the Editor "tools" are launched from key shortcuts (True) or from menu toolbar (False)
         self.launched_from_shortcuts = False
@@ -2920,8 +2959,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.aptype_cb.currentIndexChanged[str].connect(self.on_aptype_changed)
 
         self.addaperture_btn.clicked.connect(self.on_aperture_add)
-        self.apsize_entry.returnPressed.connect(self.on_aperture_add)
-        self.apdim_entry.returnPressed.connect(self.on_aperture_add)
+        self.apsize_entry.editingFinished.connect(self.on_aperture_add)
+        self.apdim_entry.editingFinished.connect(self.on_aperture_add)
 
         self.delaperture_btn.clicked.connect(self.on_aperture_delete)
         self.apertures_table.cellPressed.connect(self.on_row_selected)
@@ -2955,6 +2994,9 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
         self.conversion_factor = 1
 
+        # number of decimals for the tool diameters to be used in this editor
+        self.decimals = 4
+
         self.set_ui()
         log.debug("Initialization of the FlatCAM Gerber Editor is finished ...")
 
@@ -2966,6 +3008,11 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # updated units
         self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
 
+        if self.units == "IN":
+            self.decimals = 4
+        else:
+            self.decimals = 2
+
         self.olddia_newdia.clear()
         self.tool2tooldia.clear()
 
@@ -2994,14 +3041,14 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.aptype_cb.set_value(self.app.defaults["gerber_editor_newtype"])
         self.apdim_entry.set_value(self.app.defaults["gerber_editor_newdim"])
 
-        self.pad_array_size_entry.set_value(self.app.defaults["gerber_editor_array_size"])
+        self.pad_array_size_entry.set_value(int(self.app.defaults["gerber_editor_array_size"]))
         # linear array
         self.pad_axis_radio.set_value(self.app.defaults["gerber_editor_lin_axis"])
-        self.pad_pitch_entry.set_value(self.app.defaults["gerber_editor_lin_pitch"])
+        self.pad_pitch_entry.set_value(float(self.app.defaults["gerber_editor_lin_pitch"]))
         self.linear_angle_spinner.set_value(self.app.defaults["gerber_editor_lin_angle"])
         # circular array
         self.pad_direction_radio.set_value(self.app.defaults["gerber_editor_circ_dir"])
-        self.pad_angle_entry.set_value(self.app.defaults["gerber_editor_circ_angle"])
+        self.pad_angle_entry.set_value(float(self.app.defaults["gerber_editor_circ_angle"]))
 
     def build_ui(self, first_run=None):
 
@@ -3056,15 +3103,15 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
             if str(self.storage_dict[ap_code]['type']) == 'R' or str(self.storage_dict[ap_code]['type']) == 'O':
                 ap_dim_item = QtWidgets.QTableWidgetItem(
-                    '%.4f, %.4f' % (self.storage_dict[ap_code]['width'],
-                                    self.storage_dict[ap_code]['height']
+                    '%.*f, %.*f' % (self.decimals, self.storage_dict[ap_code]['width'],
+                                    self.decimals, self.storage_dict[ap_code]['height']
                                     )
                 )
                 ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled)
             elif str(self.storage_dict[ap_code]['type']) == 'P':
                 ap_dim_item = QtWidgets.QTableWidgetItem(
-                    '%.4f, %.4f' % (self.storage_dict[ap_code]['diam'],
-                                    self.storage_dict[ap_code]['nVertices'])
+                    '%.*f, %.*f' % (self.decimals, self.storage_dict[ap_code]['diam'],
+                                    self.decimals, self.storage_dict[ap_code]['nVertices'])
                 )
                 ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled)
             else:
@@ -3073,8 +3120,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
             try:
                 if self.storage_dict[ap_code]['size'] is not None:
-                    ap_size_item = QtWidgets.QTableWidgetItem('%.4f' % float(
-                        self.storage_dict[ap_code]['size']))
+                    ap_size_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals,
+                                                                        float(self.storage_dict[ap_code]['size'])))
                 else:
                     ap_size_item = QtWidgets.QTableWidgetItem('')
             except KeyError:
@@ -3534,7 +3581,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             self.canvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
             self.canvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
             self.canvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
-            self.canvas.graph_event_disconnect('mouse_double_click', self.app.on_double_click_over_plot)
+            self.canvas.graph_event_disconnect('mouse_double_click', self.app.on_mouse_double_click_over_plot)
         else:
             self.canvas.graph_event_disconnect(self.app.mp)
             self.canvas.graph_event_disconnect(self.app.mm)
@@ -3575,7 +3622,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.app.mp = self.canvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot)
         self.app.mm = self.canvas.graph_event_connect('mouse_move', self.app.on_mouse_move_over_plot)
         self.app.mr = self.canvas.graph_event_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
-        self.app.mdc = self.canvas.graph_event_connect('mouse_double_click', self.app.on_double_click_over_plot)
+        self.app.mdc = self.canvas.graph_event_connect('mouse_double_click', self.app.on_mouse_double_click_over_plot)
         self.app.collection.view.clicked.connect(self.app.collection.on_mouse_down)
 
         if self.app.is_legacy is False:
@@ -3691,7 +3738,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.gerber_obj = orig_grb_obj
         self.gerber_obj_options = orig_grb_obj.options
 
-        file_units = self.gerber_obj.gerber_units if self.gerber_obj.gerber_units else 'IN'
+        file_units = self.gerber_obj.units if self.gerber_obj.units else 'IN'
         app_units = self.app.defaults['units']
         self.conversion_factor = 25.4 if file_units == 'IN' else (1 / 25.4) if file_units != app_units else 1
 
@@ -3725,7 +3772,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
                     conv_apertures[apid][key] = self.gerber_obj.apertures[apid][key]
 
         self.gerber_obj.apertures = conv_apertures
-        self.gerber_obj.gerber_units = app_units
+        self.gerber_obj.units = app_units
 
         # ############################################################# ##
         # APPLY CLEAR_GEOMETRY on the SOLID_GEOMETRY
@@ -3987,7 +4034,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
             grb_obj.multigeo = False
             grb_obj.follow = False
-            grb_obj.gerber_units = app_obj.defaults['units']
+            grb_obj.units = app_obj.defaults['units']
 
             try:
                 grb_obj.create_geometry()
@@ -4096,7 +4143,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         if specific_shape:
             geo = specific_shape
         else:
-            geo = self.active_tool.geometry
+            geo = deepcopy(self.active_tool.geometry)
             if geo is None:
                 return
 
@@ -4398,7 +4445,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             x, y = self.app.geo_editor.snap(x, y)
 
             # Update cursor
-            self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color='black',
+            self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color=self.app.cursor_color_3D,
                                          size=self.app.defaults["global_cursor_size"])
 
         self.snap_x = x
@@ -4820,16 +4867,15 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.ma_annotation.clear(update=True)
 
         self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-        upper_threshold_val = None
-        lower_threshold_val = None
+
         text = []
         position = []
 
-        for apid in self.gerber_obj.apertures:
-            if 'geometry' in self.gerber_obj.apertures[apid]:
-                for geo_el in self.gerber_obj.apertures[apid]['geometry']:
-                    if 'solid' in geo_el:
-                        area = geo_el['solid'].area
+        for apid in self.storage_dict:
+            if 'geometry' in self.storage_dict[apid]:
+                for geo_el in self.storage_dict[apid]['geometry']:
+                    if 'solid' in geo_el.geo:
+                        area = geo_el.geo['solid'].area
                         try:
                             upper_threshold_val = self.ma_upper_threshold_entry.get_value()
                         except Exception as e:
@@ -4841,20 +4887,29 @@ class FlatCAMGrbEditor(QtCore.QObject):
                             lower_threshold_val = 0.0
 
                         if float(upper_threshold_val) > area > float(lower_threshold_val):
-                            current_pos = geo_el['solid'].exterior.coords[-1]
+                            current_pos = geo_el.geo['solid'].exterior.coords[-1]
                             text_elem = '%.4f' % area
                             text.append(text_elem)
                             position.append(current_pos)
+                            self.geo_to_delete.append(geo_el)
 
         if text:
             self.ma_annotation.set(text=text, pos=position, visible=True,
                                    font_size=self.app.defaults["cncjob_annotation_fontsize"],
                                    color='#000000FF')
             self.app.inform.emit('[success] %s' %
-                                 _("Polygon areas marked."))
+                                 _("Polygons marked."))
         else:
             self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                 _("There are no polygons to mark area."))
+                                 _("No polygons were marked. None fit within the limits."))
+
+    def delete_marked_polygons(self):
+        for shape_sel in self.geo_to_delete:
+            self.delete_shape(shape_sel)
+
+        self.build_ui()
+        self.plot_all()
+        self.app.inform.emit('[success] %s' % _("Done. Apertures geometry deleted."))
 
     def on_eraser(self):
         self.select_tool('eraser')
@@ -5244,13 +5299,13 @@ class TransformEditorTool(FlatCAMTool):
         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)
+        self.rotate_entry.editingFinished.connect(self.on_rotate)
+        self.skewx_entry.editingFinished.connect(self.on_skewx)
+        self.skewy_entry.editingFinished.connect(self.on_skewy)
+        self.scalex_entry.editingFinished.connect(self.on_scalex)
+        self.scaley_entry.editingFinished.connect(self.on_scaley)
+        self.offx_entry.editingFinished.connect(self.on_offx)
+        self.offy_entry.editingFinished.connect(self.on_offy)
 
         self.set_tool_ui()
 

+ 274 - 0
flatcamEditors/FlatCAMTextEditor.py

@@ -0,0 +1,274 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Date: 10/10/2019                                         #
+# MIT Licence                                              #
+# ##########################################################
+
+from flatcamGUI.GUIElements import *
+from PyQt5 import QtPrintSupport
+
+import gettext
+import FlatCAMTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class TextEditor(QtWidgets.QWidget):
+
+    def __init__(self, app, text=None):
+        super().__init__()
+
+        self.app = app
+        self.setSizePolicy(
+            QtWidgets.QSizePolicy.MinimumExpanding,
+            QtWidgets.QSizePolicy.MinimumExpanding
+        )
+
+        self.main_editor_layout = QtWidgets.QVBoxLayout(self)
+        self.main_editor_layout.setContentsMargins(0, 0, 0, 0)
+
+        self.t_frame = QtWidgets.QFrame()
+        self.t_frame.setContentsMargins(0, 0, 0, 0)
+        self.main_editor_layout.addWidget(self.t_frame)
+
+        self.work_editor_layout = QtWidgets.QGridLayout(self.t_frame)
+        self.work_editor_layout.setContentsMargins(2, 2, 2, 2)
+        self.t_frame.setLayout(self.work_editor_layout)
+
+        self.code_editor = FCTextAreaExtended()
+        stylesheet = """
+                        QTextEdit { selection-background-color:yellow;
+                                    selection-color:black;
+                        }
+                     """
+
+        self.code_editor.setStyleSheet(stylesheet)
+
+        if text:
+            self.code_editor.setPlainText(text)
+
+        self.buttonPreview = QtWidgets.QPushButton(_('Print Preview'))
+        self.buttonPreview.setToolTip(_("Open a OS standard Preview Print window."))
+        self.buttonPreview.setMinimumWidth(100)
+
+        self.buttonPrint = QtWidgets.QPushButton(_('Print Code'))
+        self.buttonPrint.setToolTip(_("Open a OS standard Print window."))
+
+        self.buttonFind = QtWidgets.QPushButton(_('Find in Code'))
+        self.buttonFind.setToolTip(_("Will search and highlight in yellow the string in the Find box."))
+        self.buttonFind.setMinimumWidth(100)
+
+        self.entryFind = FCEntry()
+        self.entryFind.setToolTip(_("Find box. Enter here the strings to be searched in the text."))
+
+        self.buttonReplace = QtWidgets.QPushButton(_('Replace With'))
+        self.buttonReplace.setToolTip(_("Will replace the string from the Find box with the one in the Replace box."))
+        self.buttonReplace.setMinimumWidth(100)
+
+        self.entryReplace = FCEntry()
+        self.entryReplace.setToolTip(_("String to replace the one in the Find box throughout the text."))
+
+        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.button_copy_all = QtWidgets.QPushButton(_('Copy All'))
+        self.button_copy_all.setToolTip(_("Will copy all the text in the Code Editor to the clipboard."))
+        self.button_copy_all.setMinimumWidth(100)
+
+        self.buttonOpen = QtWidgets.QPushButton(_('Open Code'))
+        self.buttonOpen.setToolTip(_("Will open a text file in the editor."))
+
+        self.buttonSave = QtWidgets.QPushButton(_('Save Code'))
+        self.buttonSave.setToolTip(_("Will save the text in the editor into a file."))
+
+        self.buttonRun = QtWidgets.QPushButton(_('Run Code'))
+        self.buttonRun.setToolTip(_("Will run the TCL commands found in the text file, one by one."))
+
+        self.buttonRun.hide()
+        self.work_editor_layout.addWidget(self.code_editor, 0, 0, 1, 5)
+
+        editor_hlay_1 = QtWidgets.QHBoxLayout()
+        # cnc_tab_lay_1.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        editor_hlay_1.addWidget(self.buttonFind)
+        editor_hlay_1.addWidget(self.entryFind)
+        editor_hlay_1.addWidget(self.buttonReplace)
+        editor_hlay_1.addWidget(self.entryReplace)
+        editor_hlay_1.addWidget(self.sel_all_cb)
+        editor_hlay_1.addWidget(self.button_copy_all)
+        self.work_editor_layout.addLayout(editor_hlay_1, 1, 0, 1, 5)
+
+        editor_hlay_2 = QtWidgets.QHBoxLayout()
+        editor_hlay_2.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        editor_hlay_2.addWidget(self.buttonPreview)
+        editor_hlay_2.addWidget(self.buttonPrint)
+        self.work_editor_layout.addLayout(editor_hlay_2, 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)
+        cnc_tab_lay_4.addWidget(self.buttonRun)
+        self.work_editor_layout.addLayout(cnc_tab_lay_4, 2, 4, 1, 1)
+
+        # #################################################################################
+        # ################### SIGNALS #####################################################
+        # #################################################################################
+
+        self.code_editor.textChanged.connect(self.handleTextChanged)
+        self.buttonOpen.clicked.connect(self.handleOpen)
+        self.buttonSave.clicked.connect(self.handleSaveGCode)
+        self.buttonPrint.clicked.connect(self.handlePrint)
+        self.buttonPreview.clicked.connect(self.handlePreview)
+        self.buttonFind.clicked.connect(self.handleFindGCode)
+        self.buttonReplace.clicked.connect(self.handleReplaceGCode)
+        self.button_copy_all.clicked.connect(self.handleCopyAll)
+
+        self.code_editor.set_model_data(self.app.myKeywords)
+
+        self.gcode_edited = ''
+
+    def handlePrint(self):
+        self.app.report_usage("handlePrint()")
+
+        dialog = QtPrintSupport.QPrintDialog()
+        if dialog.exec_() == QtWidgets.QDialog.Accepted:
+            self.code_editor.document().print_(dialog.printer())
+
+    def handlePreview(self):
+        self.app.report_usage("handlePreview()")
+
+        dialog = QtPrintSupport.QPrintPreviewDialog()
+        dialog.paintRequested.connect(self.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 handleOpen(self, filt=None):
+        self.app.report_usage("handleOpen()")
+
+        if filt:
+            _filter_ = filt
+        else:
+            _filter_ = "G-Code Files (*.nc);; G-Code Files (*.txt);; G-Code Files (*.tap);; G-Code Files (*.cnc);; " \
+                       "All Files (*.*)"
+
+        path, _f = QtWidgets.QFileDialog.getOpenFileName(
+            caption=_('Open file'), directory=self.app.get_last_folder(), filter=_filter_)
+
+        if path:
+            file = QtCore.QFile(path)
+            if file.open(QtCore.QIODevice.ReadOnly):
+                stream = QtCore.QTextStream(file)
+                self.gcode_edited = stream.readAll()
+                self.code_editor.setPlainText(self.gcode_edited)
+                file.close()
+
+    def handleSaveGCode(self, name=None, filt=None):
+        self.app.report_usage("handleSaveGCode()")
+
+        if filt:
+            _filter_ = filt
+        else:
+            _filter_ = "G-Code Files (*.nc);; G-Code Files (*.txt);; G-Code Files (*.tap);; G-Code Files (*.cnc);; " \
+                       "All Files (*.*)"
+
+        if name:
+            obj_name = name
+        else:
+            try:
+                obj_name = self.app.collection.get_active().options['name']
+            except AttributeError:
+                obj_name = 'file'
+                if filt is None:
+                    _filter_ = "FlatConfig Files (*.FlatConfig);;All Files (*.*)"
+
+        try:
+            filename = str(QtWidgets.QFileDialog.getSaveFileName(
+                caption=_("Export G-Code ..."),
+                directory=self.app.defaults["global_last_folder"] + '/' + str(obj_name),
+                filter=_filter_
+            )[0])
+        except TypeError:
+            filename = str(QtWidgets.QFileDialog.getSaveFileName(caption=_("Export G-Code ..."), filter=_filter_)[0])
+
+        if filename == "":
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export Code cancelled."))
+            return
+        else:
+            try:
+                my_gcode = self.code_editor.toPlainText()
+                with open(filename, 'w') as f:
+                    for line in my_gcode:
+                        f.write(line)
+            except FileNotFoundError:
+                self.app.inform.emit('[WARNING] %s' % _("No such file or directory"))
+                return
+            except PermissionError:
+                self.app.inform.emit('[WARNING] %s' %
+                                     _("Permission denied, saving not possible.\n"
+                                       "Most likely another app is holding the file open and not accessible."))
+                return
+
+        # Just for adding it to the recent files list.
+        if self.app.defaults["global_open_style"] is False:
+            self.app.file_opened.emit("cncjob", filename)
+        self.app.file_saved.emit("cncjob", filename)
+        self.app.inform.emit('%s: %s' % (_("Saved to"), str(filename)))
+
+    def handleFindGCode(self):
+        self.app.report_usage("handleFindGCode()")
+
+        flags = QtGui.QTextDocument.FindCaseSensitively
+        text_to_be_found = self.entryFind.get_value()
+
+        r = self.code_editor.find(str(text_to_be_found), flags)
+        if r is False:
+            self.code_editor.moveCursor(QtGui.QTextCursor.Start)
+
+    def handleReplaceGCode(self):
+        self.app.report_usage("handleReplaceGCode()")
+
+        old = self.entryFind.get_value()
+        new = self.entryReplace.get_value()
+
+        if self.sel_all_cb.isChecked():
+            while True:
+                cursor = self.code_editor.textCursor()
+                cursor.beginEditBlock()
+                flags = QtGui.QTextDocument.FindCaseSensitively
+                # self.ui.editor is the QPlainTextEdit
+                r = self.code_editor.find(str(old), flags)
+                if r:
+                    qc = self.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.code_editor.textCursor()
+            cursor.beginEditBlock()
+            qc = self.code_editor.textCursor()
+            if qc.hasSelection():
+                qc.insertText(new)
+            # Mark end of undo block
+            cursor.endEditBlock()
+
+    def handleCopyAll(self):
+        text = self.code_editor.toPlainText()
+        self.app.clipboard.setText(text)
+        self.app.inform.emit(_("Code Editor content copied to clipboard ..."))
+
+    # def closeEvent(self, QCloseEvent):
+    #     super().closeEvent(QCloseEvent)

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 288 - 209
flatcamGUI/FlatCAMGUI.py


+ 328 - 98
flatcamGUI/GUIElements.py

@@ -1,15 +1,15 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # Author: Juan Pablo Caram (c)                             #
 # Date: 2/5/2014                                           #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
-# ########################################################## ##
+# ##########################################################
 # File Modified (major mod): Marius Adrian Stanciu         #
 # Date: 3/10/2019                                          #
-# ########################################################## ##
+# ##########################################################
 
 from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
@@ -20,6 +20,10 @@ from copy import copy
 import re
 import logging
 import html
+import webbrowser
+from copy import deepcopy
+import sys
+from datetime import datetime
 
 log = logging.getLogger('base')
 
@@ -507,6 +511,168 @@ class EvalEntry2(QtWidgets.QLineEdit):
         return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
 
 
+class FCSpinner(QtWidgets.QSpinBox):
+
+    returnPressed = pyqtSignal()
+
+    def __init__(self, parent=None):
+        super(FCSpinner, self).__init__(parent)
+        self.readyToEdit = True
+        self.editingFinished.connect(self.on_edit_finished)
+        self.lineEdit().installEventFilter(self)
+
+    def eventFilter(self, object, event):
+        if event.type() == QtCore.QEvent.MouseButtonPress:
+            if self.readyToEdit:
+                self.lineEdit().selectAll()
+                self.readyToEdit = False
+            else:
+                self.lineEdit().deselect()
+            return True
+        return False
+
+    def keyPressEvent(self, event):
+        if event.key() == Qt.Key_Enter:
+            self.returnPressed.emit()
+            self.clearFocus()
+        else:
+            super().keyPressEvent(event)
+
+    def wheelEvent(self, *args, **kwargs):
+        # should work only there is a focus in the lineedit of the SpinBox
+        if self.readyToEdit is False:
+            super().wheelEvent(*args, **kwargs)
+
+    def on_edit_finished(self):
+        self.clearFocus()
+
+    # def mousePressEvent(self, e, parent=None):
+    #     super(FCSpinner, self).mousePressEvent(e)  # required to deselect on 2e click
+    #     if self.readyToEdit:
+    #         self.lineEdit().selectAll()
+    #         self.readyToEdit = False
+
+    def focusOutEvent(self, e):
+        # don't focus out if the user requests an popup menu
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(FCSpinner, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.lineEdit().deselect()
+            self.readyToEdit = True
+
+    def get_value(self):
+        return int(self.value())
+
+    def set_value(self, val):
+        try:
+            k = int(val)
+        except Exception as e:
+            log.debug(str(e))
+            return
+        self.setValue(k)
+
+    def set_range(self, min_val, max_val):
+        self.setRange(min_val, max_val)
+
+    # def sizeHint(self):
+    #     default_hint_size = super(FCSpinner, self).sizeHint()
+    #     return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+
+class FCDoubleSpinner(QtWidgets.QDoubleSpinBox):
+
+    returnPressed = pyqtSignal()
+
+    def __init__(self, parent=None):
+        super(FCDoubleSpinner, self).__init__(parent)
+        self.readyToEdit = True
+
+        self.editingFinished.connect(self.on_edit_finished)
+        self.lineEdit().installEventFilter(self)
+
+        # by default don't allow the minus sign to be entered as the default for QDoubleSpinBox is the positive range
+        # between 0.00 and 99.00 (2 decimals)
+        self.lineEdit().setValidator(
+            QtGui.QRegExpValidator(QtCore.QRegExp("[0-9]*[.,]?[0-9]{%d}" % self.decimals()), self))
+
+    def on_edit_finished(self):
+        self.clearFocus()
+
+    def eventFilter(self, object, event):
+        if event.type() == QtCore.QEvent.MouseButtonPress:
+            if self.readyToEdit:
+                self.lineEdit().selectAll()
+                self.readyToEdit = False
+            else:
+                self.lineEdit().deselect()
+            return True
+        return False
+
+    def keyPressEvent(self, event):
+        if event.key() == Qt.Key_Enter:
+            self.returnPressed.emit()
+            self.clearFocus()
+        else:
+            super().keyPressEvent(event)
+
+    def wheelEvent(self, *args, **kwargs):
+        # should work only there is a focus in the lineedit of the SpinBox
+        if self.readyToEdit is False:
+            super().wheelEvent(*args, **kwargs)
+
+    def focusOutEvent(self, e):
+        # don't focus out if the user requests an popup menu
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(FCDoubleSpinner, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.lineEdit().deselect()
+            self.readyToEdit = True
+
+    def valueFromText(self, p_str):
+        text = p_str.replace(',', '.')
+        try:
+            ret_val = float(text)
+        except ValueError:
+            ret_val = 0.0
+
+        return ret_val
+
+    def validate(self, p_str, p_int):
+        try:
+            if float(p_str) < self.minimum() or float(p_str) > self.maximum():
+                return QtGui.QValidator.Intermediate, p_str, p_int
+        except ValueError:
+            pass
+        return QtGui.QValidator.Acceptable, p_str, p_int
+
+    def get_value(self):
+        return float(self.value())
+
+    def set_value(self, val):
+        try:
+            k = float(val)
+        except Exception as e:
+            log.debug(str(e))
+            return
+        self.setValue(k)
+
+    def set_precision(self, val):
+        self.setDecimals(val)
+
+        # make sure that the user can't type more decimals than the set precision
+        if self.minimum() < 0 or self.maximum() < 0:
+            self.lineEdit().setValidator(
+                QtGui.QRegExpValidator(QtCore.QRegExp("-?[0-9]*[.,]?[0-9]{%d}" % self.decimals()), self))
+        else:
+            self.lineEdit().setValidator(
+                QtGui.QRegExpValidator(QtCore.QRegExp("[0-9]*[.,]?[0-9]{%d}" % self.decimals()), self))
+
+    def set_range(self, min_val, max_val):
+        if min_val < 0 or max_val < 0:
+            self.lineEdit().setValidator(
+                QtGui.QRegExpValidator(QtCore.QRegExp("-?[0-9]*[.,]?[0-9]{%d}" % self.decimals()), self))
+
+        self.setRange(min_val, max_val)
+
+
 class FCCheckBox(QtWidgets.QCheckBox):
     def __init__(self, label='', parent=None):
         super(FCCheckBox, self).__init__(str(label), parent)
@@ -557,7 +723,7 @@ class FCTextAreaRich(QtWidgets.QTextEdit):
 
 class FCTextAreaExtended(QtWidgets.QTextEdit):
     def __init__(self, parent=None):
-        super(FCTextAreaExtended, self).__init__(parent)
+        super().__init__(parent)
 
         self.completer = MyCompleter()
 
@@ -1425,7 +1591,7 @@ class FCDetachableTab(QtWidgets.QTabWidget):
 
 
 class FCDetachableTab2(FCDetachableTab):
-    tab_closed_signal = pyqtSignal()
+    tab_closed_signal = pyqtSignal(object)
 
     def __init__(self, protect=None, protect_by_name=None, parent=None):
         super(FCDetachableTab2, self).__init__(protect=protect, protect_by_name=protect_by_name, parent=parent)
@@ -1439,9 +1605,7 @@ class FCDetachableTab2(FCDetachableTab):
         """
         idx = self.currentIndex()
 
-        # emit the signal only if the name is the one we want; the name should be a parameter somehow
-        if self.tabText(idx) == _("Preferences"):
-            self.tab_closed_signal.emit()
+        self.tab_closed_signal.emit(self.tabText(idx))
 
         self.removeTab(currentIndex)
 
@@ -1564,9 +1728,33 @@ class OptionalHideInputSection:
 
 
 class FCTable(QtWidgets.QTableWidget):
-    def __init__(self, parent=None):
+
+    drag_drop_sig = pyqtSignal()
+
+    def __init__(self, drag_drop=False, protected_rows=None, parent=None):
         super(FCTable, self).__init__(parent)
 
+        if drag_drop:
+            self.setDragEnabled(True)
+            self.setAcceptDrops(True)
+            self.viewport().setAcceptDrops(True)
+            self.setDragDropOverwriteMode(False)
+            self.setDropIndicatorShown(True)
+
+            self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+            self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+            self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
+
+        self.rows_not_for_drag_and_drop = list()
+        if protected_rows:
+            try:
+                for r in protected_rows:
+                    self.rows_not_for_drag_and_drop.append(r)
+            except TypeError:
+                self.rows_not_for_drag_and_drop = [protected_rows]
+
+        self.rows_to_move = list()
+
     def sizeHint(self):
         default_hint_size = super(FCTable, self).sizeHint()
         return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
@@ -1583,7 +1771,7 @@ class FCTable(QtWidgets.QTableWidget):
             width += self.columnWidth(i)
         return width
 
-    # color is in format QtGui.Qcolor(r, g, b, alpha) with or without alpfa
+    # color is in format QtGui.Qcolor(r, g, b, alpha) with or without alpha
     def setColortoRow(self, rowIndex, color):
         for j in range(self.columnCount()):
             self.item(rowIndex, j).setBackground(color)
@@ -1609,6 +1797,124 @@ class FCTable(QtWidgets.QTableWidget):
         self.addAction(action)
         action.triggered.connect(call_function)
 
+    # def dropEvent(self, event: QtGui.QDropEvent):
+    #     if not event.isAccepted() and event.source() == self:
+    #         drop_row = self.drop_on(event)
+    #
+    #         rows = sorted(set(item.row() for item in self.selectedItems()))
+    #         # rows_to_move = [
+    #         #     [QtWidgets.QTableWidgetItem(self.item(row_index, column_index))
+    #         #      for column_index in range(self.columnCount())] for row_index in rows
+    #         # ]
+    #         self.rows_to_move[:] = []
+    #         for row_index in rows:
+    #             row_items = list()
+    #             for column_index in range(self.columnCount()):
+    #                 r_item = self.item(row_index, column_index)
+    #                 w_item = self.cellWidget(row_index, column_index)
+    #
+    #                 if r_item is not None:
+    #                     row_items.append(QtWidgets.QTableWidgetItem(r_item))
+    #                 elif w_item is not None:
+    #                     row_items.append(w_item)
+    #
+    #             self.rows_to_move.append(row_items)
+    #
+    #         for row_index in reversed(rows):
+    #             self.removeRow(row_index)
+    #             if row_index < drop_row:
+    #                 drop_row -= 1
+    #
+    #         for row_index, data in enumerate(self.rows_to_move):
+    #             row_index += drop_row
+    #             self.insertRow(row_index)
+    #
+    #             for column_index, column_data in enumerate(data):
+    #                 if isinstance(column_data, QtWidgets.QTableWidgetItem):
+    #                     self.setItem(row_index, column_index, column_data)
+    #                 else:
+    #                     self.setCellWidget(row_index, column_index, column_data)
+    #
+    #         event.accept()
+    #         for row_index in range(len(self.rows_to_move)):
+    #             self.item(drop_row + row_index, 0).setSelected(True)
+    #             self.item(drop_row + row_index, 1).setSelected(True)
+    #
+    #     super().dropEvent(event)
+    #
+    # def drop_on(self, event):
+    #     ret_val = False
+    #     index = self.indexAt(event.pos())
+    #     if not index.isValid():
+    #         return self.rowCount()
+    #
+    #     ret_val = index.row() + 1 if self.is_below(event.pos(), index) else index.row()
+    #
+    #     return ret_val
+    #
+    # def is_below(self, pos, index):
+    #     rect = self.visualRect(index)
+    #     margin = 2
+    #     if pos.y() - rect.top() < margin:
+    #         return False
+    #     elif rect.bottom() - pos.y() < margin:
+    #         return True
+    #     # noinspection PyTypeChecker
+    #     return rect.contains(pos, True) and not (
+    #                 int(self.model().flags(index)) & Qt.ItemIsDropEnabled) and pos.y() >= rect.center().y()
+
+    def dropEvent(self, event):
+        """
+        From here: https://stackoverflow.com/questions/26227885/drag-and-drop-rows-within-qtablewidget
+        :param event:
+        :return:
+        """
+        if event.source() == self:
+            rows = set([mi.row() for mi in self.selectedIndexes()])
+
+            # if one of the selected rows for drag and drop is within the protected list, return
+            for r in rows:
+                if r in self.rows_not_for_drag_and_drop:
+                    return
+
+            targetRow = self.indexAt(event.pos()).row()
+            rows.discard(targetRow)
+            rows = sorted(rows)
+
+            if not rows:
+                return
+            if targetRow == -1:
+                targetRow = self.rowCount()
+
+            for _ in range(len(rows)):
+                self.insertRow(targetRow)
+
+            rowMapping = dict()  # Src row to target row.
+            for idx, row in enumerate(rows):
+                if row < targetRow:
+                    rowMapping[row] = targetRow + idx
+                else:
+                    rowMapping[row + len(rows)] = targetRow + idx
+
+            colCount = self.columnCount()
+            for srcRow, tgtRow in sorted(rowMapping.items()):
+                for col in range(0, colCount):
+                    new_item = self.item(srcRow, col)
+                    if new_item is None:
+                        new_item = self.cellWidget(srcRow, col)
+                    if isinstance(new_item, QtWidgets.QTableWidgetItem):
+                        new_item = self.takeItem(srcRow, col)
+                        self.setItem(tgtRow, col, new_item)
+                    else:
+                        self.setCellWidget(tgtRow, col, new_item)
+
+            for row in reversed(sorted(rowMapping.keys())):
+                self.removeRow(row)
+            event.accept()
+            self.drag_drop_sig.emit()
+
+            return
+
 
 class SpinBoxDelegate(QtWidgets.QItemDelegate):
 
@@ -1652,103 +1958,27 @@ class SpinBoxDelegate(QtWidgets.QItemDelegate):
         spinbox.setDecimals(digits)
 
 
-class FCSpinner(QtWidgets.QSpinBox):
-    def __init__(self, parent=None):
-        super(FCSpinner, self).__init__(parent)
-        self.readyToEdit = True
-        self.editingFinished.connect(self.on_edit_finished)
-
-    def on_edit_finished(self):
-        self.clearFocus()
-
-    def mousePressEvent(self, e, parent=None):
-        super(FCSpinner, self).mousePressEvent(e)  # required to deselect on 2e click
-        if self.readyToEdit:
-            self.lineEdit().selectAll()
-            self.readyToEdit = False
-
-    def focusOutEvent(self, e):
-        # don't focus out if the user requests an popup menu
-        if e.reason() != QtCore.Qt.PopupFocusReason:
-            super(FCSpinner, self).focusOutEvent(e)  # required to remove cursor on focusOut
-            self.lineEdit().deselect()
-            self.readyToEdit = True
-
-    def get_value(self):
-        return str(self.value())
-
-    def set_value(self, val):
-        try:
-            k = int(val)
-        except Exception as e:
-            log.debug(str(e))
-            return
-        self.setValue(k)
-
-    def set_range(self, min_val, max_val):
-        self.setRange(min_val, max_val)
-
-    # def sizeHint(self):
-    #     default_hint_size = super(FCSpinner, self).sizeHint()
-    #     return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
-
-
-class FCDoubleSpinner(QtWidgets.QDoubleSpinBox):
-    def __init__(self, parent=None):
-        super(FCDoubleSpinner, self).__init__(parent)
-        self.readyToEdit = True
-        self.editingFinished.connect(self.on_edit_finished)
-
-    def on_edit_finished(self):
-        self.clearFocus()
-
-    def mousePressEvent(self, e, parent=None):
-        super(FCDoubleSpinner, self).mousePressEvent(e)  # required to deselect on 2e click
-        if self.readyToEdit:
-            self.lineEdit().selectAll()
-            self.readyToEdit = False
-
-    def focusOutEvent(self, e):
-        # don't focus out if the user requests an popup menu
-        if e.reason() != QtCore.Qt.PopupFocusReason:
-            super(FCDoubleSpinner, self).focusOutEvent(e)  # required to remove cursor on focusOut
-            self.lineEdit().deselect()
-            self.readyToEdit = True
-
-    def get_value(self):
-        return str(self.value())
-
-    def set_value(self, val):
-        try:
-            k = float(val)
-        except Exception as e:
-            log.debug(str(e))
-            return
-        self.setValue(k)
-
-    def set_precision(self, val):
-        self.setDecimals(val)
-
-    def set_range(self, min_val, max_val):
-        self.setRange(min_val, max_val)
-
-
 class Dialog_box(QtWidgets.QWidget):
-    def __init__(self, title=None, label=None, icon=None):
+    def __init__(self, title=None, label=None, icon=None, initial_text=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)
+        if initial_text is None:
+            self.location = str((0, 0))
+        else:
+            self.location = initial_text
+
         self.ok = False
 
-        dialog_box = QtWidgets.QInputDialog()
-        dialog_box.setMinimumWidth(290)
+        self.dialog_box = QtWidgets.QInputDialog()
+        self.dialog_box.setMinimumWidth(290)
         self.setWindowIcon(icon)
 
-        self.location, self.ok = dialog_box.getText(self, title, label, text="0, 0")
+        self.location, self.ok = self.dialog_box.getText(self, title, label,
+                                                         text=str(self.location).replace('(', '').replace(')', ''))
         self.readyToEdit = True
 
     def mousePressEvent(self, e, parent=None):
@@ -1782,7 +2012,7 @@ class _BrowserTextEdit(QTextEdit):
 
     def clear(self):
         QTextEdit.clear(self)
-        text = "FlatCAM %s - Open Source Software - Type help to get started\n\n" % self.version
+        text = "FlatCAM %s - Type >help< to get started\n\n" % self.version
         text = html.escape(text)
         text = text.replace('\n', '<br/>')
         self.moveCursor(QTextCursor.End)

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 345 - 150
flatcamGUI/ObjectUI.py


+ 85 - 14
flatcamGUI/PlotCanvas.py

@@ -1,17 +1,17 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://caram.cl/software/flatcam                         #
-# Author: Juan Pablo Caram (c)                             #
-# Date: 2/5/2014                                           #
+# Author: Dennis Hayrullin (c)                             #
+# Date: 2016                                               #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 from PyQt5 import QtCore
 
 import logging
-from flatcamGUI.VisPyCanvas import VisPyCanvas, time
+from flatcamGUI.VisPyCanvas import VisPyCanvas, time, Color
 from flatcamGUI.VisPyVisuals import ShapeGroup, ShapeCollection, TextCollection, TextGroup, Cursor
 from vispy.scene.visuals import InfiniteLine, Line
+
 import numpy as np
 from vispy.geometry import Rect
 
@@ -44,6 +44,17 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         # Parent container
         self.container = container
 
+        settings = QtCore.QSettings("Open Source", "FlatCAM")
+        if settings.contains("theme"):
+            theme = settings.value('theme', type=str)
+        else:
+            theme = 'white'
+
+        if theme == 'white':
+            self.line_color = (0.3, 0.0, 0.0, 1.0)
+        else:
+            self.line_color = (0.4, 0.4, 0.4, 1.0)
+
         # 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
@@ -68,7 +79,6 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         self.draw_workspace()
 
         self.line_parent = None
-        self.line_color = (0.3, 0.0, 0.0, 1.0)
         self.cursor_v_line = InfiniteLine(pos=None, color=self.line_color, vertical=True,
                                           parent=self.line_parent)
 
@@ -89,10 +99,12 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         self.text_collection.enabled = True
 
         self.c = None
-
+        self.big_cursor = None
         # Keep VisPy canvas happy by letting it be "frozen" again.
         self.freeze()
 
+        self.graph_event_connect('mouse_wheel', self.on_mouse_scroll)
+
     # 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):
@@ -104,7 +116,7 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         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)])
+        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)])
 
@@ -130,14 +142,14 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         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.view.scene)
+                           antialias=True, method='agg', parent=self.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.view.scene)
+                           antialias=True, method='agg', parent=self.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.view.scene)
+                           antialias=True, method='agg', parent=self.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.view.scene)
+                           antialias=True, method='agg', parent=self.view.scene)
 
         if self.fcapp.defaults['global_workspace'] is False:
             self.delete_workspace()
@@ -196,13 +208,33 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         return ShapeCollection(parent=self.view.scene, pool=self.fcapp.pool, **kwargs)
 
     def new_cursor(self, big=None):
+        """
+        Will create a mouse cursor pointer on canvas
+
+        :param big: if True will create a mouse cursor made out of infinite lines
+        :return: the mouse cursor object
+        """
         if big is True:
+            self.big_cursor = True
             self.c = CursorBig()
+
+            # in case there are multiple new_cursor calls, best to disconnect first the signals
+            try:
+                self.c.mouse_state_updated.disconnect(self.on_mouse_state)
+            except (TypeError, AttributeError):
+                pass
+            try:
+                self.c.mouse_position_updated.disconnect(self.on_mouse_position)
+            except (TypeError, AttributeError):
+                pass
+
             self.c.mouse_state_updated.connect(self.on_mouse_state)
             self.c.mouse_position_updated.connect(self.on_mouse_position)
         else:
+            self.big_cursor = False
             self.c = Cursor(pos=np.empty((0, 2)), parent=self.view.scene)
             self.c.antialias = 0
+
         return self.c
 
     def on_mouse_state(self, state):
@@ -220,6 +252,42 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         self.cursor_v_line.set_data(pos=pos[0], color=self.line_color)
         self.view.scene.update()
 
+    def on_mouse_scroll(self, event):
+        # key modifiers
+        modifiers = event.modifiers
+        pan_delta_x = self.fcapp.defaults["global_gridx"]
+        pan_delta_y = self.fcapp.defaults["global_gridy"]
+        curr_pos = event.pos
+
+        # Controlled pan by mouse wheel
+        if 'Shift' in modifiers:
+            p1 = np.array(curr_pos)[:2]
+
+            if event.delta[1] > 0:
+                curr_pos[0] -= pan_delta_x
+            else:
+                curr_pos[0] += pan_delta_x
+            p2 = np.array(curr_pos)[:2]
+            self.view.camera.pan(p2 - p1)
+        elif 'Control' in modifiers:
+            p1 = np.array(curr_pos)[:2]
+
+            if event.delta[1] > 0:
+                curr_pos[1] += pan_delta_y
+            else:
+                curr_pos[1] -= pan_delta_y
+            p2 = np.array(curr_pos)[:2]
+            self.view.camera.pan(p2 - p1)
+
+        if self.fcapp.grid_status() == True:
+            pos_canvas = self.translate_coords(curr_pos)
+            pos = self.fcapp.geo_editor.snap(pos_canvas[0], pos_canvas[1])
+
+            # Update cursor
+            self.fcapp.app_cursor.set_data(np.asarray([(pos[0], pos[1])]),
+                                           symbol='++', edge_color=self.fcapp.cursor_color_3D,
+                                           size=self.fcapp.defaults["global_cursor_size"])
+
     def new_text_group(self, collection=None):
         if collection:
             return TextGroup(collection)
@@ -308,7 +376,10 @@ class CursorBig(QtCore.QObject):
         if 'edge_color' in kwargs:
             color = kwargs['edge_color']
         else:
-            color = (0.0, 0.0, 0.0, 1.0)
+            if self.app.defaults['global_theme'] == 'white':
+                color = '#000000FF'
+            else:
+                color = '#FFFFFFFF'
 
         position = [pos[0][0], pos[0][1]]
         self.mouse_position_updated.emit(position)

+ 62 - 12
flatcamGUI/PlotCanvasLegacy.py

@@ -77,6 +77,11 @@ class CanvasCache(QtCore.QObject):
         self.axes.set_xticks([])
         self.axes.set_yticks([])
 
+        if self.app.defaults['global_theme'] == 'white':
+            self.axes.set_facecolor('#FFFFFF')
+        else:
+            self.axes.set_facecolor('#000000')
+
         self.canvas = FigureCanvas(self.figure)
 
         self.cache = None
@@ -140,6 +145,13 @@ class PlotCanvasLegacy(QtCore.QObject):
 
         self.app = app
 
+        if self.app.defaults['global_theme'] == 'white':
+            theme_color = '#FFFFFF'
+            tick_color = '#000000'
+        else:
+            theme_color = '#000000'
+            tick_color = '#FFFFFF'
+
         # Options
         self.x_margin = 15  # pixels
         self.y_margin = 25  # Pixels
@@ -149,16 +161,26 @@ class PlotCanvasLegacy(QtCore.QObject):
 
         # Plots go onto a single matplotlib.figure
         self.figure = Figure(dpi=50)  # TODO: dpi needed?
-        self.figure.patch.set_visible(False)
+        self.figure.patch.set_visible(True)
+        self.figure.set_facecolor(theme_color)
 
         # These axes show the ticks and grid. No plotting done here.
         # New axes must have a label, otherwise mpl returns an existing one.
         self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
         self.axes.set_aspect(1)
-        self.axes.grid(True)
+        self.axes.grid(True, color='gray')
         self.axes.axhline(color=(0.70, 0.3, 0.3), linewidth=2)
         self.axes.axvline(color=(0.70, 0.3, 0.3), linewidth=2)
 
+        self.axes.tick_params(axis='x', color=tick_color, labelcolor=tick_color)
+        self.axes.tick_params(axis='y', color=tick_color, labelcolor=tick_color)
+        self.axes.spines['bottom'].set_color(tick_color)
+        self.axes.spines['top'].set_color(tick_color)
+        self.axes.spines['right'].set_color(tick_color)
+        self.axes.spines['left'].set_color(tick_color)
+
+        self.axes.set_facecolor(theme_color)
+
         self.ch_line = None
         self.cv_line = None
 
@@ -264,10 +286,18 @@ class PlotCanvasLegacy(QtCore.QObject):
         # else:
         #     c = MplCursor(axes=axes, color='black', linewidth=1)
 
+        if self.app.defaults['global_theme'] == 'white':
+            color = '#000000'
+        else:
+            color = '#FFFFFF'
+
         if big is True:
             self.big_cursor = True
-            self.ch_line = self.axes.axhline(color=(0.0, 0.0, 0.0), linewidth=1)
-            self.cv_line = self.axes.axvline(color=(0.0, 0.0, 0.0), linewidth=1)
+            self.ch_line = self.axes.axhline(color=color, linewidth=1)
+            self.cv_line = self.axes.axvline(color=color, linewidth=1)
+        else:
+            self.big_cursor = False
+
         c = FakeCursor()
         c.mouse_state_updated.connect(self.clear_cursor)
 
@@ -283,6 +313,11 @@ class PlotCanvasLegacy(QtCore.QObject):
         """
         # there is no point in drawing mouse cursor when panning as it jumps in a confusing way
         if self.app.app_cursor.enabled is True and self.panning is False:
+            if self.app.defaults['global_theme'] == 'white':
+                color = '#000000'
+            else:
+                color = '#FFFFFF'
+
             if self.big_cursor is False:
                 try:
                     x, y = self.app.geo_editor.snap(x_pos, y_pos)
@@ -291,13 +326,13 @@ class PlotCanvasLegacy(QtCore.QObject):
                     # The size of the cursor is multiplied by 1.65 because that value made the cursor similar with the
                     # one in the OpenGL(3D) graphic engine
                     pointer_size = int(float(self.app.defaults["global_cursor_size"] ) * 1.65)
-                    elements = self.axes.plot(x, y, 'k+', ms=pointer_size, mew=1, animated=True)
+                    elements = self.axes.plot(x, y, '+', color=color, ms=pointer_size, mew=1, animated=True)
                     for el in elements:
                         self.axes.draw_artist(el)
                 except Exception as e:
                     # this happen at app initialization since self.app.geo_editor does not exist yet
-                    # I could reshuffle the object instantiating order but what's the point? I could crash something else
-                    # and that's pythonic, too
+                    # I could reshuffle the object instantiating order but what's the point?
+                    # I could crash something else and that's pythonic, too
                     pass
             else:
                 self.ch_line.set_ydata(y_pos)
@@ -311,6 +346,11 @@ class PlotCanvasLegacy(QtCore.QObject):
         if state is True:
             self.draw_cursor(x_pos=self.mouse[0], y_pos=self.mouse[1])
         else:
+            if self.big_cursor is True:
+                self.ch_line.remove()
+                self.cv_line.remove()
+                self.canvas.draw_idle()
+
             self.canvas.restore_region(self.background)
             self.canvas.blit(self.axes.bbox)
 
@@ -468,7 +508,7 @@ class PlotCanvasLegacy(QtCore.QObject):
 
         # Adjust axes
         for ax in self.figure.get_axes():
-            ax.set_xlim((x - half_width , x + half_width))
+            ax.set_xlim((x - half_width, x + half_width))
             ax.set_ylim((y - half_height, y + half_height))
 
         # Re-draw
@@ -802,7 +842,8 @@ class ShapeCollectionLegacy:
             self.axes = self.app.plotcanvas.new_axes(axes_name)
 
     def add(self, shape=None, color=None, face_color=None, alpha=None, visible=True,
-            update=False, layer=1, tolerance=0.01, obj=None, gcode_parsed=None, tool_tolerance=None, tooldia=None):
+            update=False, layer=1, tolerance=0.01, obj=None, gcode_parsed=None, tool_tolerance=None, tooldia=None,
+            linewidth=None):
         """
         This function will add shapes to the shape collection
 
@@ -818,6 +859,7 @@ class ShapeCollectionLegacy:
         :param gcode_parsed: not used; just for compatibility with VIsPy canvas
         :param tool_tolerance: just for compatibility with VIsPy canvas
         :param tooldia:
+        :param linewidth: the width of the line
         :return:
         """
         self._color = color[:-2] if color is not None else None
@@ -845,6 +887,7 @@ class ShapeCollectionLegacy:
                 self.shape_dict.update({
                     'color': self._color,
                     'face_color': self._face_color,
+                    'linewidth': linewidth,
                     'alpha': self._alpha,
                     'shape': sh
                 })
@@ -857,6 +900,7 @@ class ShapeCollectionLegacy:
             self.shape_dict.update({
                 'color': self._color,
                 'face_color': self._face_color,
+                'linewidth': linewidth,
                 'alpha': self._alpha,
                 'shape': shape
             })
@@ -920,15 +964,21 @@ class ShapeCollectionLegacy:
                 elif obj_type == 'geometry':
                     if type(local_shapes[element]['shape']) == Polygon:
                         x, y = local_shapes[element]['shape'].exterior.coords.xy
-                        self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-')
+                        self.axes.plot(x, y, local_shapes[element]['color'],
+                                       linestyle='-',
+                                       linewidth=local_shapes[element]['linewidth'])
                         for ints in local_shapes[element]['shape'].interiors:
                             x, y = ints.coords.xy
-                            self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-')
+                            self.axes.plot(x, y, local_shapes[element]['color'],
+                                           linestyle='-',
+                                           linewidth=local_shapes[element]['linewidth'])
                     elif type(local_shapes[element]['shape']) == LineString or \
                             type(local_shapes[element]['shape']) == LinearRing:
 
                         x, y = local_shapes[element]['shape'].coords.xy
-                        self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-')
+                        self.axes.plot(x, y, local_shapes[element]['color'],
+                                       linestyle='-',
+                                       linewidth=local_shapes[element]['linewidth'])
 
                 elif obj_type == 'gerber':
                     if self.obj.options["multicolored"]:

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 379 - 149
flatcamGUI/PreferencesUI.py


+ 54 - 22
flatcamGUI/VisPyCanvas.py

@@ -1,10 +1,10 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # File Author: Dennis Hayrullin                            #
 # Date: 2/5/2016                                           #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 import numpy as np
 from PyQt5.QtGui import QPalette
@@ -25,7 +25,27 @@ class VisPyCanvas(scene.SceneCanvas):
 
         self.unfreeze()
 
-        back_color = str(QPalette().color(QPalette.Window).name())
+        settings = QSettings("Open Source", "FlatCAM")
+        if settings.contains("axis_font_size"):
+            a_fsize = settings.value('axis_font_size', type=int)
+        else:
+            a_fsize = 8
+
+        if settings.contains("theme"):
+            theme = settings.value('theme', type=str)
+        else:
+            theme = 'white'
+
+        if theme == 'white':
+            theme_color = Color('#FFFFFF')
+            tick_color = Color('#000000')
+            back_color = str(QPalette().color(QPalette.Window).name())
+        else:
+            theme_color = Color('#000000')
+            tick_color = Color('gray')
+            back_color = Color('#000000')
+            # back_color = Color('#272822') # darker
+            # back_color = Color('#3c3f41') # lighter
 
         self.central_widget.bgcolor = back_color
         self.central_widget.border_color = back_color
@@ -36,18 +56,16 @@ class VisPyCanvas(scene.SceneCanvas):
         top_padding = self.grid_widget.add_widget(row=0, col=0, col_span=2)
         top_padding.height_max = 0
 
-        settings = QSettings("Open Source", "FlatCAM")
-        if settings.contains("axis_font_size"):
-            a_fsize = settings.value('axis_font_size', type=int)
-        else:
-            a_fsize = 8
-
-        self.yaxis = scene.AxisWidget(orientation='left', axis_color='black', text_color='black', font_size=a_fsize)
+        self.yaxis = scene.AxisWidget(
+            orientation='left', axis_color=tick_color, text_color=tick_color, font_size=a_fsize, axis_width=1
+        )
         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=a_fsize,
-                                      anchors=['center', 'bottom'])
+        self.xaxis = scene.AxisWidget(
+            orientation='bottom', axis_color=tick_color, text_color=tick_color, font_size=a_fsize, axis_width=1,
+            anchors=['center', 'bottom']
+        )
         self.xaxis.height_max = 30
         self.grid_widget.add_widget(self.xaxis, row=2, col=1)
 
@@ -55,7 +73,7 @@ class VisPyCanvas(scene.SceneCanvas):
         # 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 = self.grid_widget.add_view(row=1, col=1, border_color=tick_color, bgcolor=theme_color)
         view.camera = Camera(aspect=1, rect=(-25, -25, 150, 150))
 
         # Following function was removed from 'prepare_draw()' of 'Grid' class by patch,
@@ -65,11 +83,22 @@ class VisPyCanvas(scene.SceneCanvas):
         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)
+        # grid1 = scene.GridLines(parent=view.scene, color='dimgray')
+        # grid1.set_gl_state(depth_test=False)
+
+        settings = QSettings("Open Source", "FlatCAM")
+        if settings.contains("theme"):
+            theme = settings.value('theme', type=str)
+        else:
+            theme = 'white'
 
         self.view = view
-        self.grid = grid1
+        if theme == 'white':
+            self.grid = scene.GridLines(parent=self.view.scene, color='dimgray')
+        else:
+            self.grid = scene.GridLines(parent=self.view.scene, color='#dededeff')
+
+        self.grid.set_gl_state(depth_test=False)
 
         self.freeze()
 
@@ -115,6 +144,9 @@ class Camera(scene.PanZoomCamera):
         if event.handled or not self.interactive:
             return
 
+        # key modifiers
+        modifiers = event.mouse_event.modifiers
+
         # Limit mouse move events
         last_event = event.last_event
         t = time.time()
@@ -129,21 +161,21 @@ class Camera(scene.PanZoomCamera):
             event.handled = True
             return
 
-        # Scrolling
+        # ################### 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)
+            if not modifiers:
+                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
-
+            # ################ Panning ############################
             # 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

+ 2 - 2
flatcamGUI/VisPyPatches.py

@@ -1,10 +1,10 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # File Author: Dennis Hayrullin                            #
 # Date: 2/5/2016                                           #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 from vispy.visuals import markers, LineVisual, InfiniteLineVisual
 from vispy.visuals.axis import Ticker, _get_ticks_talbot

+ 2 - 2
flatcamGUI/VisPyTesselators.py

@@ -1,10 +1,10 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # File Author: Dennis Hayrullin                            #
 # Date: 2/5/2016                                           #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 from OpenGL import GLU
 

+ 5 - 3
flatcamGUI/VisPyVisuals.py

@@ -1,10 +1,10 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # File Author: Dennis Hayrullin                            #
 # Date: 2/5/2016                                           #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 from vispy.visuals import CompoundVisual, LineVisual, MeshVisual, TextVisual, MarkersVisual
 from vispy.scene.visuals import VisualNode, generate_docstring, visuals
@@ -235,7 +235,7 @@ class ShapeCollectionVisual(CompoundVisual):
         self.freeze()
 
     def add(self, shape=None, color=None, face_color=None, alpha=None, visible=True,
-            update=False, layer=1, tolerance=0.01):
+            update=False, layer=1, tolerance=0.01, linewidth=None):
         """
         Adds shape to collection
         :return:
@@ -253,6 +253,8 @@ class ShapeCollectionVisual(CompoundVisual):
             Layer number. 0 - lowest.
         :param tolerance: float
             Geometry simplifying tolerance
+        :param linewidth: int
+            Not used, for compatibility
         :return: int
             Index of shape
         """

+ 0 - 1
flatcamParsers/ParseDXF.py

@@ -1,6 +1,5 @@
 # ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #

+ 46 - 34
flatcamParsers/ParseDXF_Spline.py

@@ -2,30 +2,32 @@
 # Vasilis Vlachoudis
 # Date: 20-Oct-2015
 
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File modified: Marius Adrian Stanciu                     #
 # Date: 3/10/2019                                          #
-# ########################################################## ##
+# ##########################################################
 
 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
@@ -33,7 +35,7 @@ def spline2Polyline(xyz, degree, closed, segments, knots):
     :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:
@@ -51,16 +53,16 @@ def spline2Polyline(xyz, degree, closed, segments, knots):
 
     npts = len(xyz)
 
-    if degree<1 or degree>3:
-        #print "invalid degree"
-        return None,None,None
+    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
+        # print "not enough control points"
+        return None, None, None
 
     # resolution:
     nseg = segments * npts
@@ -72,12 +74,12 @@ def spline2Polyline(xyz, degree, closed, segments, knots):
 
     i = 1
     for pt in xyz:
-        b[i]   = pt[0]
+        b[i] = pt[0]
         b[i+1] = pt[1]
         b[i+2] = pt[2]
-        i +=3
+        i += 3
 
-    #if periodic:
+    # if periodic:
     if closed:
         _rbsplinu(npts, k, nseg, b, h, p, knots)
     else:
@@ -86,7 +88,7 @@ def spline2Polyline(xyz, degree, closed, segments, knots):
     x = []
     y = []
     z = []
-    for i in range(1,3*nseg+1,3):
+    for i in range(1, 3*nseg+1, 3):
         x.append(p[i])
         y.append(p[i+1])
         z.append(p[i+2])
@@ -94,7 +96,8 @@ def spline2Polyline(xyz, degree, closed, segments, knots):
 #    for i,xyz in enumerate(zip(x,y,z)):
 #        print i,xyz
 
-    return x,y,z
+    return x, y, z
+
 
 # ------------------------------------------------------------------------------
 # Subroutine to generate a B-spline open knot vector with multiplicity
@@ -108,12 +111,13 @@ def spline2Polyline(xyz, degree, closed, segments, knots):
 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:
+        if order < 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.
 #
@@ -128,6 +132,7 @@ def _knotu(n, order):
         x[i] = float(i-1)
     return x
 
+
 # ------------------------------------------------------------------------------
 # Subroutine to generate rational B-spline basis functions--open knot vector
 
@@ -163,8 +168,8 @@ def _rbasis(c, t, npts, x, h, r):
             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):
+    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])
@@ -184,7 +189,7 @@ def _rbasis(c, t, npts, x, h, r):
 
     # calculate sum for denominator of rational basis functions
     s = 0.0
-    for i in range(1,npts+1):
+    for i in range(1, npts+1):
         s += temp[i]*h[i]
 
     # form rational basis functions and put in r vector
@@ -194,6 +199,7 @@ def _rbasis(c, t, npts, x, h, r):
         else:
             r[i] = 0
 
+
 # ------------------------------------------------------------------------------
 # Generates a rational B-spline curve using a uniform open knot vector.
 #
@@ -245,11 +251,12 @@ def _rbspline(npts, k, p1, b, h, p, x):
             p[icount+j] = 0.0
             # Do local matrix multiplication
             for i in range(1, npts+1):
-                p[icount+j] +=  nbasis[i]*b[jcount]
+                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
 #
@@ -296,23 +303,24 @@ def _rbsplinu(npts, k, p1, b, h, p, x=None):
         nbasis = [0.0]*(npts+1)
         _rbasis(k, t, npts, x, h, nbasis)
         # generate a point on the curve
-        for j in range(1,4):
+        for j in range(1, 4):
             jcount = j
             p[icount+j] = 0.0
             #  Do local matrix multiplication
-            for i in range(1,npts+1):
+            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
+    return abs(x) < _accuracy
 
 
 def gauss(A, B):
@@ -337,7 +345,8 @@ def gauss(A, B):
                 j = i
                 ap = api
 
-        if j != k: p[k], p[j] = p[j], p[k]  # Swap values
+        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]
@@ -384,20 +393,22 @@ class Vector(list):
         """Set vector"""
         self[0] = x
         self[1] = y
-        if z: self[2] = z
+        if z:
+            self[2] = z
 
     # ----------------------------------------------------------------------
     def __repr__(self):
-        return "[%s]" % (", ".join([repr(x) for x in self]))
+        return "[%s]" % ", ".join([repr(x) for x in self])
 
     # ----------------------------------------------------------------------
     def __str__(self):
-        return "[%s]" % (", ".join([("%15g" % (x)).strip() for x in 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
+        if len(self) != len(v):
+            return False
         s2 = 0.0
         for a, b in zip(self, v):
             s2 += (a - b) ** 2
@@ -523,12 +534,12 @@ class Vector(list):
     # ----------------------------------------------------------------------
     def norm(self):
         """Normalize vector and return length"""
-        l = self.length()
-        if l > 0.0:
-            invlen = 1.0 / l
+        length = self.length()
+        if length > 0.0:
+            invlen = 1.0 / length
             for i in range(len(self)):
                 self[i] *= invlen
-        return l
+        return length
 
     normalize = norm
 
@@ -580,8 +591,9 @@ class Vector(list):
         """return containing the direction if normalized with any of the axis"""
 
         v = self.clone()
-        l = v.norm()
-        if abs(l) <= zero: return "O"
+        length = v.norm()
+        if abs(length) <= zero:
+            return "O"
 
         if abs(v[0] - 1.0) < zero:
             return "X"

+ 1438 - 0
flatcamParsers/ParseExcellon.py

@@ -0,0 +1,1438 @@
+from camlib import *
+
+import FlatCAMTranslation as fcTranslate
+
+import gettext
+import builtins
+
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class Excellon(Geometry):
+    """
+    Here it is done all the Excellon parsing.
+
+    *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
+    solid_geometry    Geometry list for each 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
+    stop              (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 = int(Excellon.defaults['geo_steps_per_circle'])
+        self.geo_steps_per_circle = int(geo_steps_per_circle)
+
+        Geometry.__init__(self, geo_steps_per_circle=int(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 = []
+
+        self.source_file = ''
+
+        # 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
+
+        # this will serve as a default if the Excellon file has no info regarding of tool diameters (this info may be
+        # in another file like for PCB WIzard ECAD software
+        self.toolless_diam = 1.0
+        # signal that the Excellon file has no tool diameter informations and the tools have bogus (random) diameter
+        self.diameterless = False
+
+        # 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"]
+        # detected Excellon format is stored here:
+        self.excellon_format = None
+
+        # 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',
+                           'source_file']
+
+        # ### 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])$')
+
+        # Uunits and possible Excellon zeros and possible Excellon format
+        # INCH uses 6 digits
+        # METRIC uses 5/6
+        self.units_re = re.compile(r'^(INCH|METRIC)(?:,([TL])Z)?,?(\d*\.\d+)?.*$')
+
+        # 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*))')
+        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))')
+
+        # Altium Excellon format support
+        # it's a comment like this: ";FILE_FORMAT=2:5"
+        self.altium_format = re.compile(r'^;\s*(?:FILE_FORMAT)?(?:Format)?[=|:]\s*(\d+)[:|.](\d+).*$')
+
+        # 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=None, file_obj=None):
+        """
+        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
+        """
+        if file_obj:
+            estr = file_obj
+        else:
+            if filename is None:
+                return "fail"
+            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:
+                if self.app.abort_flag:
+                    # graceful abort requested by the user
+                    raise FlatCAMApp.GracefulException
+
+                line_num += 1
+                # log.debug("%3d %s" % (line_num, str(eline)))
+
+                self.source_file += 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] %s: %s' %
+                                         (_('This is GCODE mark'), eline))
+                    return
+
+                # Header Begin (M48) #
+                if self.hbegin_re.search(eline):
+                    in_header = True
+                    headerless = False
+                    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
+
+                # Search for Header End #
+                # Since there might be comments in the header that include header end char (% or M95)
+                # we ignore the lines starting with ';' that contains such header end chars because it is not a
+                # real header end.
+                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 in comments: %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))
+                            spec['solid_geometry'] = []
+                            continue
+                    # search for Altium Excellon Format / Sprint Layout who is included as a comment
+                    match = self.altium_format.search(eline)
+                    if match:
+                        self.excellon_format_upper_mm = match.group(1)
+                        self.excellon_format_lower_mm = match.group(2)
+
+                        self.excellon_format_upper_in = match.group(1)
+                        self.excellon_format_lower_in = match.group(2)
+                        log.warning("Altium Excellon format preset found in comments: %s:%s" %
+                                    (match.group(1), match.group(2)))
+                        continue
+                    else:
+                        log.warning("Line ignored, it's a comment: %s" % eline)
+                else:
+                    if self.hend_re.search(eline):
+                        if in_header is False or bool(self.tools) 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])
+                            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 bool(headerless):
+                            match = self.toolset_hl_re.search(eline)
+                            if match:
+                                name = str(int(match.group(1)))
+                                try:
+                                    diam = float(match.group(2))
+                                except:
+                                    # it's possible that tool definition has only tool number and no diameter info
+                                    # (those could be in another file like PCB Wizard do)
+                                    # then match.group(2) = None and float(None) will create the exception
+                                    # the bellow construction is so each tool will have a slightly different diameter
+                                    # starting with a default value, to allow Excellon editing after that
+                                    self.diameterless = True
+                                    self.app.inform.emit('[WARNING] %s%s %s' %
+                                                         (_("No tool diameter info's. See shell.\n"
+                                                            "A tool change event: T"),
+                                                          str(current_tool),
+                                                          _("was found but the Excellon file "
+                                                            "have no informations regarding the tool "
+                                                            "diameters therefore the application will try to load it "
+                                                            "by using some 'fake' diameters.\n"
+                                                            "The user needs to edit the resulting Excellon object and "
+                                                            "change the diameters to reflect the real diameters.")
+                                                          )
+                                                         )
+
+                                    if self.excellon_units == 'MM':
+                                        diam = self.toolless_diam + (int(current_tool) - 1) / 100
+                                    else:
+                                        diam = (self.toolless_diam + (int(current_tool) - 1) / 100) / 25.4
+
+                                spec = {"C": diam, 'solid_geometry': []}
+                                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 Exception as e:
+                                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 Exception as e:
+                                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)), 'solid_geometry': []}
+                        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.excellon_format = match.group(3)
+                        if self.excellon_format:
+                            upper = len(self.excellon_format.partition('.')[0])
+                            lower = len(self.excellon_format.partition('.')[2])
+                            if self.units == 'MM':
+                                self.excellon_format_upper_mm = upper
+                                self.excellon_format_lower_mm = lower
+                            else:
+                                self.excellon_format_upper_in = upper
+                                self.excellon_format_lower_in = lower
+
+                        # 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.excellon_format = match.group(3)
+                    if self.excellon_format:
+                        upper = len(self.excellon_format.partition('.')[0])
+                        lower = len(self.excellon_format.partition('.')[2])
+                        if self.units == 'MM':
+                            self.excellon_format_upper_mm = upper
+                            self.excellon_format_lower_mm = lower
+                        else:
+                            self.excellon_format_upper_in = upper
+                            self.excellon_format_lower_in = lower
+
+                    # 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("Excellon PARSING FAILED. Line %d: %s" % (line_num, eline))
+            msg = '[ERROR_NOTCL] %s' % \
+                  _("An internal error has ocurred. See shell.\n")
+            msg += _('{e_code} Excellon Parser error.\nParsing Failed. Line {l_nr}: {line}\n').format(
+                e_code='[ERROR]',
+                l_nr=line_num,
+                line=eline)
+            msg += traceback.format_exc()
+            self.app.inform.emit(msg)
+
+            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":  # Leading
+                # 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:
+            # clear the solid_geometry in self.tools
+            for tool in self.tools:
+                try:
+                    self.tools[tool]['solid_geometry'][:] = []
+                except KeyError:
+                    self.tools[tool]['solid_geometry'] = []
+
+            for drill in self.drills:
+                # poly = drill['point'].buffer(self.tools[drill['tool']]["C"]/2.0)
+                if drill['tool'] is '':
+                    self.app.inform.emit('[WARNING] %s' %
+                                         _("Excellon.create_geometry() -> a drill location was skipped "
+                                           "due of not having a tool associated.\n"
+                                           "Check the resulting GCode."))
+                    log.debug("Excellon.create_geometry() -> a drill location was skipped "
+                              "due of not having a tool associated")
+                    continue
+                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)
+                self.tools[drill['tool']]['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)
+                self.tools[slot['tool']]['solid_geometry'].append(poly)
+
+        except Exception as e:
+            log.debug("Excellon geometry creation failed due of ERROR: %s" % str(e))
+            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 Excellon 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("camlib.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
+
+        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))
+
+    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:
+        """
+        log.debug("camlib.Excellon.convert_units()")
+
+        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
+        """
+        log.debug("camlib.Excellon.scale()")
+
+        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:
+                try:
+                    return affinity.scale(obj, xfactor, yfactor, origin=(px, py))
+                except AttributeError:
+                    return obj
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for g in self.drills:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        # Drills
+        for drill in self.drills:
+            drill['point'] = affinity.scale(drill['point'], xfactor, yfactor, origin=(px, py))
+
+            self.el_count += 1
+            disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+            if self.old_disp_number < disp_number <= 100:
+                self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                self.old_disp_number = disp_number
+
+        # scale solid_geometry
+        for tool in self.tools:
+            self.tools[tool]['solid_geometry'] = scale_geom(self.tools[tool]['solid_geometry'])
+
+        # 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()
+        self.app.proc_container.new_text = ''
+
+    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
+        """
+        log.debug("camlib.Excellon.offset()")
+
+        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:
+                try:
+                    return affinity.translate(obj, xoff=dx, yoff=dy)
+                except AttributeError:
+                    return obj
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for g in self.drills:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        # Drills
+        for drill in self.drills:
+            drill['point'] = affinity.translate(drill['point'], xoff=dx, yoff=dy)
+
+            self.el_count += 1
+            disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+            if self.old_disp_number < disp_number <= 100:
+                self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                self.old_disp_number = disp_number
+
+        # offset solid_geometry
+        for tool in self.tools:
+            self.tools[tool]['solid_geometry'] = offset_geom(self.tools[tool]['solid_geometry'])
+
+        # 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()
+        self.app.proc_container.new_text = ''
+
+    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
+        """
+        log.debug("camlib.Excellon.mirror()")
+
+        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:
+                try:
+                    return affinity.scale(obj, xscale, yscale, origin=(px, py))
+                except AttributeError:
+                    return obj
+
+        # Modify data
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for g in self.drills:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        # Drills
+        for drill in self.drills:
+            drill['point'] = affinity.scale(drill['point'], xscale, yscale, origin=(px, py))
+
+            self.el_count += 1
+            disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+            if self.old_disp_number < disp_number <= 100:
+                self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                self.old_disp_number = disp_number
+
+        # mirror solid_geometry
+        for tool in self.tools:
+            self.tools[tool]['solid_geometry'] = mirror_geom(self.tools[tool]['solid_geometry'])
+
+        # 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()
+        self.app.proc_container.new_text = ''
+
+    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
+        """
+        log.debug("camlib.Excellon.skew()")
+
+        if angle_x is None:
+            angle_x = 0.0
+
+        if angle_y is None:
+            angle_y = 0.0
+
+        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:
+                try:
+                    return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
+                except AttributeError:
+                    return obj
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for g in self.drills:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        if point is None:
+            px, py = 0, 0
+
+            # Drills
+            for drill in self.drills:
+                drill['point'] = affinity.skew(drill['point'], angle_x, angle_y,
+                                               origin=(px, py))
+
+                self.el_count += 1
+                disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+                if self.old_disp_number < disp_number <= 100:
+                    self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                    self.old_disp_number = disp_number
+
+            # skew solid_geometry
+            for tool in self.tools:
+                self.tools[tool]['solid_geometry'] = skew_geom(self.tools[tool]['solid_geometry'])
+
+            # 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))
+        else:
+            px, py = point
+            # Drills
+            for drill in self.drills:
+                drill['point'] = affinity.skew(drill['point'], angle_x, angle_y,
+                                               origin=(px, py))
+
+                self.el_count += 1
+                disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+                if self.old_disp_number < disp_number <= 100:
+                    self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                    self.old_disp_number = disp_number
+
+            # skew solid_geometry
+            for tool in self.tools:
+                self.tools[tool]['solid_geometry'] = skew_geom(self.tools[tool]['solid_geometry'])
+
+            # 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()
+        self.app.proc_container.new_text = ''
+
+    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:
+        """
+        log.debug("camlib.Excellon.rotate()")
+
+        def rotate_geom(obj, origin=None):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(rotate_geom(g))
+                return new_obj
+            else:
+                if origin:
+                    try:
+                        return affinity.rotate(obj, angle, origin=origin)
+                    except AttributeError:
+                        return obj
+                else:
+                    try:
+                        return affinity.rotate(obj, angle, origin=(px, py))
+                    except AttributeError:
+                        return obj
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for g in self.drills:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        if point is None:
+            # Drills
+            for drill in self.drills:
+                drill['point'] = affinity.rotate(drill['point'], angle, origin='center')
+
+            # rotate solid_geometry
+            for tool in self.tools:
+                self.tools[tool]['solid_geometry'] = rotate_geom(self.tools[tool]['solid_geometry'], origin='center')
+
+                self.el_count += 1
+                disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+                if self.old_disp_number < disp_number <= 100:
+                    self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                    self.old_disp_number = disp_number
+
+            # 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))
+
+                self.el_count += 1
+                disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+                if self.old_disp_number < disp_number <= 100:
+                    self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                    self.old_disp_number = disp_number
+
+            # rotate solid_geometry
+            for tool in self.tools:
+                self.tools[tool]['solid_geometry'] = rotate_geom(self.tools[tool]['solid_geometry'])
+
+            # 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()
+        self.app.proc_container.new_text = ''

+ 6 - 7
flatcamParsers/ParseFont.py

@@ -1,15 +1,14 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
-# ####################################################################### ##
-# ## Borrowed code from 'https://github.com/gddc/ttfquery/blob/master/ # ##
-# ## and made it work with Python 3                          ########### ##
-# ####################################################################### ##
+# ######################################################################
+# ## Borrowed code from 'https://github.com/gddc/ttfquery/blob/master/ #
+# ## and made it work with Python 3                                    #
+# ######################################################################
 
 import re, os, sys, glob
 import itertools

+ 2071 - 0
flatcamParsers/ParseGerber.py

@@ -0,0 +1,2071 @@
+from camlib import *
+import FlatCAMTranslation as fcTranslate
+
+import gettext
+import builtins
+
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class Gerber(Geometry):
+    """
+    Here it is done all the Gerber parsing.
+
+    **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``                |
+    +-----------+-----------------------------------+
+    | solid_geometry      | (list)                  |
+    +-----------+-----------------------------------+
+    * ``aperture_macros`` (dictionary): Are predefined geometrical structures
+      that can be instantiated 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": 128,
+    #     "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 approximate a circle with lines.
+        self.steps_per_circle = int(self.app.defaults["gerber_circle_steps"])
+
+        # Initialize parent
+        Geometry.__init__(self, geo_steps_per_circle=self.steps_per_circle)
+
+        # 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 = self.app.defaults['gerber_def_zeros']
+        """Zeros in Gerber numbers. If 'L' then remove leading zeros, if 'T' remove trailing zeros. Used during parsing.
+        """
+
+        # ## Gerber elements # ##
+        '''
+        apertures = {
+            'id':{
+                'type':string, 
+                'size':float, 
+                'width':float,
+                'height':float,
+                'geometry': [],
+            }
+        }
+        apertures['geometry'] list elements are dicts
+        dict = {
+            'solid': [],
+            'follow': [],
+            'clear': []
+        }
+        '''
+
+        # store the file units here:
+        self.units = self.app.defaults['gerber_def_units']
+
+        # aperture storage
+        self.apertures = {}
+
+        # Aperture Macros
+        self.aperture_macros = {}
+
+        # will store the Gerber geometry's as solids
+        self.solid_geometry = Polygon()
+
+        # will store the Gerber geometry's as paths
+        self.follow_geometry = []
+
+        # made True when the LPC command is encountered in Gerber parsing
+        # it allows adding data into the clear_geometry key of the self.apertures[aperture] dict
+        self.is_lpc = False
+
+        self.source_file = ''
+
+        # 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', 'source_file']
+
+        # ### Parser patterns ## ##
+        # FS - Format Specification
+        # The format of X and Y must be the same!
+        # L-omit leading zeros, T-omit trailing zeros, D-no zero supression
+        # A-absolute notation, I-incremental notation
+        self.fmt_re = re.compile(r'%?FS([LTD])?([AI])X(\d)(\d)Y\d\d\*%?$')
+        self.fmt_re_alt = re.compile(r'%FS([LTD])?([AI])X(\d)(\d)Y\d\d\*MO(IN|MM)\*%$')
+        self.fmt_re_orcad = re.compile(r'(G\d+)*\**%FS([LTD])?([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'(.*)%$')
+
+        # flag to store if a conversion was done. It is needed because multiple units declarations can be found
+        # in a Gerber file (normal or obsolete ones)
+        self.conversion_done = False
+
+        self.use_buffer_for_union = self.app.defaults["gerber_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
+        """
+        if self.app.abort_flag:
+            # graceful abort requested by the user
+            raise FlatCAMApp.GracefulException
+
+        # 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 clean line
+                            yield line
+                            break
+
+            processed_lines = list(line_generator())
+            self.parse_lines(processed_lines)
+
+    # @profile
+    def parse_lines(self, glines):
+        """
+        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
+        :return: None
+        :rtype: None
+        """
+
+        # Coordinates of the current path, each is [x, y]
+        path = []
+
+        # this is for temporary storage of solid geometry until it is added to poly_buffer
+        geo_s = None
+
+        # this is for temporary storage of follow geometry until it is added to follow_buffer
+        geo_f = 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 = []
+
+        # store here the follow geometry
+        follow_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 = ""
+
+        s_tol = float(self.app.defaults["gerber_simp_tolerance"])
+
+        self.app.inform.emit('%s %d %s.' % (_("Gerber processing. Parsing"), len(glines), _("lines")))
+        try:
+            for gline in glines:
+                if self.app.abort_flag:
+                    # graceful abort requested by the user
+                    raise FlatCAMApp.GracefulException
+
+                line_num += 1
+                self.source_file += gline + '\n'
+
+                # 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:
+                    new_polarity = match.group(1)
+                    # log.info("Polarity CHANGE, LPC = %s, poly_buff = %s" % (self.is_lpc, poly_buffer))
+                    self.is_lpc = True if new_polarity == 'C' else False
+                    if len(path) > 1 and current_polarity != new_polarity:
+
+                        # finish the current path and add it to the storage
+                        # --- Buffered ----
+                        width = self.apertures[last_path_aperture]["size"]
+
+                        geo_dict = dict()
+                        geo_f = LineString(path)
+                        if not geo_f.is_empty:
+                            follow_buffer.append(geo_f)
+                            geo_dict['follow'] = geo_f
+
+                        geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                        if not geo_s.is_empty:
+                            if self.app.defaults['gerber_simplification']:
+                                poly_buffer.append(geo_s.simplify(s_tol))
+                            else:
+                                poly_buffer.append(geo_s)
+                            if self.is_lpc is True:
+                                geo_dict['clear'] = geo_s
+                            else:
+                                geo_dict['solid'] = geo_s
+
+                        if last_path_aperture not in self.apertures:
+                            self.apertures[last_path_aperture] = dict()
+                        if 'geometry' not in self.apertures[last_path_aperture]:
+                            self.apertures[last_path_aperture]['geometry'] = []
+                        self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
+
+                        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.follow_geometry = self.follow_geometry.union(cascaded_union(follow_buffer))
+                            self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer))
+
+                        else:
+                            # self.follow_geometry = self.follow_geometry.difference(cascaded_union(follow_buffer))
+                            self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer))
+
+                        # follow_buffer = []
+                        poly_buffer = []
+
+                    current_polarity = new_polarity
+                    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)]
+                    if match.group(1) is not None:
+                        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, "
+                        "D-no zero supression)" % 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:
+                    self.units = match.group(1)
+                    log.debug("Gerber units found = %s" % self.units)
+                    # Changed for issue #80
+                    # self.convert_units(match.group(1))
+                    self.conversion_done = True
+                    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)]
+                    if match.group(1) is not None:
+                        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, "
+                        "D-no zero suppression)" % self.gerber_zeros)
+                    log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute)
+
+                    self.units = match.group(5)
+                    log.debug("Gerber units found = %s" % self.units)
+                    # Changed for issue #80
+                    # self.convert_units(match.group(5))
+                    self.conversion_done = True
+                    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)]
+                        if match.group(2) is not None:
+                            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, "
+                            "D-no zerosuppressionn)" % self.gerber_zeros)
+                        log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute)
+
+                        self.units = match.group(1)
+                        log.debug("Gerber units found = %s" % self.units)
+                        # Changed for issue #80
+                        # self.convert_units(match.group(5))
+                        self.conversion_done = True
+                        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)])
+                    self.conversion_done = True
+                    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)
+                            geo_dict = dict()
+                            flash = self.create_flash_geometry(
+                                Point(current_x, current_y), self.apertures[current_aperture],
+                                self.steps_per_circle)
+
+                            geo_dict['follow'] = Point([current_x, current_y])
+
+                            if not flash.is_empty:
+                                if self.app.defaults['gerber_simplification']:
+                                    poly_buffer.append(flash.simplify(s_tol))
+                                else:
+                                    poly_buffer.append(flash)
+                                if self.is_lpc is True:
+                                    geo_dict['clear'] = flash
+                                else:
+                                    geo_dict['solid'] = flash
+
+                                if current_aperture not in self.apertures:
+                                    self.apertures[current_aperture] = dict()
+                                if 'geometry' not in self.apertures[current_aperture]:
+                                    self.apertures[current_aperture]['geometry'] = []
+                                self.apertures[current_aperture]['geometry'].append(deepcopy(geo_dict))
+
+                        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, current_aperture))
+
+                    # 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:
+                            geo_dict = dict()
+                            geo_f = LineString(path)
+                            if not geo_f.is_empty:
+                                follow_buffer.append(geo_f)
+                                geo_dict['follow'] = geo_f
+
+                            # --- Buffered ----
+                            width = self.apertures[last_path_aperture]["size"]
+                            geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                            if not geo_s.is_empty:
+                                if self.app.defaults['gerber_simplification']:
+                                    poly_buffer.append(geo_s.simplify(s_tol))
+                                else:
+                                    poly_buffer.append(geo_s)
+                                if self.is_lpc is True:
+                                    geo_dict['clear'] = geo_s
+                                else:
+                                    geo_dict['solid'] = geo_s
+
+                            if last_path_aperture not in self.apertures:
+                                self.apertures[last_path_aperture] = dict()
+                            if 'geometry' not in self.apertures[last_path_aperture]:
+                                self.apertures[last_path_aperture]['geometry'] = []
+                            self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
+
+                            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
+
+                        geo_dict = dict()
+                        geo_f = LineString(path)
+                        if not geo_f.is_empty:
+                            follow_buffer.append(geo_f)
+                            geo_dict['follow'] = geo_f
+
+                        # --- Buffered ----
+                        width = self.apertures[last_path_aperture]["size"]
+                        geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                        if not geo_s.is_empty:
+                            if self.app.defaults['gerber_simplification']:
+                                poly_buffer.append(geo_s.simplify(s_tol))
+                            else:
+                                poly_buffer.append(geo_s)
+                            if self.is_lpc is True:
+                                geo_dict['clear'] = geo_s
+                            else:
+                                geo_dict['solid'] = geo_s
+
+                        if last_path_aperture not in self.apertures:
+                            self.apertures[last_path_aperture] = dict()
+                        if 'geometry' not in self.apertures[last_path_aperture]:
+                            self.apertures[last_path_aperture]['geometry'] = []
+                        self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
+
+                        path = [path[-1]]
+
+                    making_region = True
+                    continue
+
+                # ############################################################# ##
+                # G37* - End region
+                # ############################################################# ##
+                if self.regionoff_re.search(gline):
+                    making_region = False
+
+                    if '0' not in self.apertures:
+                        self.apertures['0'] = {}
+                        self.apertures['0']['type'] = 'REG'
+                        self.apertures['0']['size'] = 0.0
+                        self.apertures['0']['geometry'] = []
+
+                    # if D02 happened before G37 we now have a path with 1 element only; we have to add the current
+                    # geo to the poly_buffer otherwise we loose it
+                    if current_operation_code == 2:
+                        if len(path) == 1:
+                            # this means that the geometry was prepared previously and we just need to add it
+                            geo_dict = dict()
+                            if geo_f:
+                                if not geo_f.is_empty:
+                                    follow_buffer.append(geo_f)
+                                    geo_dict['follow'] = geo_f
+                            if geo_s:
+                                if not geo_s.is_empty:
+                                    if self.app.defaults['gerber_simplification']:
+                                        poly_buffer.append(geo_s.simplify(s_tol))
+                                    else:
+                                        poly_buffer.append(geo_s)
+                                    if self.is_lpc is True:
+                                        geo_dict['clear'] = geo_s
+                                    else:
+                                        geo_dict['solid'] = geo_s
+
+                            if geo_s or geo_f:
+                                self.apertures['0']['geometry'].append(deepcopy(geo_dict))
+
+                            path = [[current_x, current_y]]  # Start new path
+
+                    # 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:"
+                        # path = [[current_x, current_y]]
+                        continue
+
+                    # For regions we may ignore an aperture that is None
+
+                    # --- Buffered ---
+                    geo_dict = dict()
+                    region_f = Polygon(path).exterior
+                    if not region_f.is_empty:
+                        follow_buffer.append(region_f)
+                        geo_dict['follow'] = region_f
+
+                    region_s = Polygon(path)
+                    if not region_s.is_valid:
+                        region_s = region_s.buffer(0, int(self.steps_per_circle / 4))
+
+                    if not region_s.is_empty:
+                        if self.app.defaults['gerber_simplification']:
+                            poly_buffer.append(region_s.simplify(s_tol))
+                        else:
+                            poly_buffer.append(region_s)
+                        if self.is_lpc is True:
+                            geo_dict['clear'] = region_s
+                        else:
+                            geo_dict['solid'] = region_s
+
+                    if not region_s.is_empty or not region_f.is_empty:
+                        self.apertures['0']['geometry'].append(deepcopy(geo_dict))
+
+                    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 current_x is not None and current_y is not None:
+                            # only add the point if it's a new one otherwise skip it (harder to process)
+                            if path[-1] != [current_x, current_y]:
+                                path.append([current_x, current_y])
+
+                            if 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))
+
+                                        geo_dict = dict()
+                                        geo_f = Point([current_x, current_y])
+                                        follow_buffer.append(geo_f)
+                                        geo_dict['follow'] = geo_f
+
+                                        geo_s = shply_box(minx, miny, maxx, maxy)
+                                        if self.app.defaults['gerber_simplification']:
+                                            poly_buffer.append(geo_s.simplify(s_tol))
+                                        else:
+                                            poly_buffer.append(geo_s)
+
+                                        if self.is_lpc is True:
+                                            geo_dict['clear'] = geo_s
+                                        else:
+                                            geo_dict['solid'] = geo_s
+
+                                        if current_aperture not in self.apertures:
+                                            self.apertures[current_aperture] = dict()
+                                        if 'geometry' not in self.apertures[current_aperture]:
+                                            self.apertures[current_aperture]['geometry'] = []
+                                        self.apertures[current_aperture]['geometry'].append(deepcopy(geo_dict))
+                                except Exception as e:
+                                    pass
+                            last_path_aperture = current_aperture
+                            # we do this for the case that a region is done without having defined any aperture
+                            if last_path_aperture is None:
+                                if '0' not in self.apertures:
+                                    self.apertures['0'] = {}
+                                    self.apertures['0']['type'] = 'REG'
+                                    self.apertures['0']['size'] = 0.0
+                                    self.apertures['0']['geometry'] = []
+                                last_path_aperture = '0'
+                        else:
+                            self.app.inform.emit('[WARNING] %s: %s' %
+                                                 (_("Coordinates missing, line ignored"), str(gline)))
+                            self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                                 _("GERBER file might be CORRUPT. Check the file !!!"))
+
+                    elif current_operation_code == 2:
+                        if len(path) > 1:
+                            geo_s = None
+
+                            geo_dict = dict()
+                            # --- BUFFERED ---
+                            # this treats the case when we are storing geometry as paths only
+                            if making_region:
+                                # we do this for the case that a region is done without having defined any aperture
+                                if last_path_aperture is None:
+                                    if '0' not in self.apertures:
+                                        self.apertures['0'] = {}
+                                        self.apertures['0']['type'] = 'REG'
+                                        self.apertures['0']['size'] = 0.0
+                                        self.apertures['0']['geometry'] = []
+                                    last_path_aperture = '0'
+                                geo_f = Polygon()
+                            else:
+                                geo_f = LineString(path)
+
+                            try:
+                                if self.apertures[last_path_aperture]["type"] != 'R':
+                                    if not geo_f.is_empty:
+                                        follow_buffer.append(geo_f)
+                                        geo_dict['follow'] = geo_f
+                            except Exception as e:
+                                log.debug("camlib.Gerber.parse_lines() --> %s" % str(e))
+                                if not geo_f.is_empty:
+                                    follow_buffer.append(geo_f)
+                                    geo_dict['follow'] = geo_f
+
+                            # this treats the case when we are storing geometry as solids
+                            if making_region:
+                                # we do this for the case that a region is done without having defined any aperture
+                                if last_path_aperture is None:
+                                    if '0' not in self.apertures:
+                                        self.apertures['0'] = {}
+                                        self.apertures['0']['type'] = 'REG'
+                                        self.apertures['0']['size'] = 0.0
+                                        self.apertures['0']['geometry'] = []
+                                    last_path_aperture = '0'
+
+                                try:
+                                    geo_s = Polygon(path)
+                                except ValueError:
+                                    log.warning("Problem %s %s" % (gline, line_num))
+                                    self.app.inform.emit('[ERROR] %s: %s' %
+                                                         (_("Region does not have enough points. "
+                                                            "File will be processed but there are parser errors. "
+                                                            "Line number"), 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!
+                                geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+
+                            try:
+                                if self.apertures[last_path_aperture]["type"] != 'R':
+                                    if not geo_s.is_empty:
+                                        if self.app.defaults['gerber_simplification']:
+                                            poly_buffer.append(geo_s.simplify(s_tol))
+                                        else:
+                                            poly_buffer.append(geo_s)
+
+                                        if self.is_lpc is True:
+                                            geo_dict['clear'] = geo_s
+                                        else:
+                                            geo_dict['solid'] = geo_s
+                            except Exception as e:
+                                log.debug("camlib.Gerber.parse_lines() --> %s" % str(e))
+                                if self.app.defaults['gerber_simplification']:
+                                    poly_buffer.append(geo_s.simplify(s_tol))
+                                else:
+                                    poly_buffer.append(geo_s)
+
+                                if self.is_lpc is True:
+                                    geo_dict['clear'] = geo_s
+                                else:
+                                    geo_dict['solid'] = geo_s
+
+                            if last_path_aperture not in self.apertures:
+                                self.apertures[last_path_aperture] = dict()
+                            if 'geometry' not in self.apertures[last_path_aperture]:
+                                self.apertures[last_path_aperture]['geometry'] = []
+                            self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
+
+                        # 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] %s: %s' %
+                                                 (_("Coordinates missing, line ignored"), str(gline)))
+                            self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                                 _("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 ----
+                            geo_dict = dict()
+
+                            # this treats the case when we are storing geometry as paths
+                            geo_f = LineString(path)
+                            if not geo_f.is_empty:
+                                try:
+                                    if self.apertures[last_path_aperture]["type"] != 'R':
+                                        follow_buffer.append(geo_f)
+                                        geo_dict['follow'] = geo_f
+                                except Exception as e:
+                                    log.debug("camlib.Gerber.parse_lines() --> G01 match D03 --> %s" % str(e))
+                                    follow_buffer.append(geo_f)
+                                    geo_dict['follow'] = geo_f
+
+                            # this treats the case when we are storing geometry as solids
+                            width = self.apertures[last_path_aperture]["size"]
+                            geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                            if not geo_s.is_empty:
+                                try:
+                                    if self.apertures[last_path_aperture]["type"] != 'R':
+                                        if self.app.defaults['gerber_simplification']:
+                                            poly_buffer.append(geo_s.simplify(s_tol))
+                                        else:
+                                            poly_buffer.append(geo_s)
+
+                                        if self.is_lpc is True:
+                                            geo_dict['clear'] = geo_s
+                                        else:
+                                            geo_dict['solid'] = geo_s
+                                except:
+                                    if self.app.defaults['gerber_simplification']:
+                                        poly_buffer.append(geo_s.simplify(s_tol))
+                                    else:
+                                        poly_buffer.append(geo_s)
+
+                                    if self.is_lpc is True:
+                                        geo_dict['clear'] = geo_s
+                                    else:
+                                        geo_dict['solid'] = geo_s
+
+                            if last_path_aperture not in self.apertures:
+                                self.apertures[last_path_aperture] = dict()
+                            if 'geometry' not in self.apertures[last_path_aperture]:
+                                self.apertures[last_path_aperture]['geometry'] = []
+                            self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
+
+                        # Reset path starting point
+                        path = [[linear_x, linear_y]]
+
+                        # --- BUFFERED ---
+                        # Draw the flash
+                        # this treats the case when we are storing geometry as paths
+                        geo_dict = dict()
+                        geo_flash = Point([linear_x, linear_y])
+                        follow_buffer.append(geo_flash)
+                        geo_dict['follow'] = geo_flash
+
+                        # this treats the case when we are storing geometry as solids
+                        flash = self.create_flash_geometry(
+                            Point([linear_x, linear_y]),
+                            self.apertures[current_aperture],
+                            self.steps_per_circle
+                        )
+                        if not flash.is_empty:
+                            if self.app.defaults['gerber_simplification']:
+                                poly_buffer.append(flash.simplify(s_tol))
+                            else:
+                                poly_buffer.append(flash)
+
+                            if self.is_lpc is True:
+                                geo_dict['clear'] = flash
+                            else:
+                                geo_dict['solid'] = flash
+
+                        if current_aperture not in self.apertures:
+                            self.apertures[current_aperture] = dict()
+                        if 'geometry' not in self.apertures[current_aperture]:
+                            self.apertures[current_aperture]['geometry'] = []
+                        self.apertures[current_aperture]['geometry'].append(deepcopy(geo_dict))
+
+                    # 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 Exception as e:
+                        circular_x = current_x
+
+                    try:
+                        circular_y = parse_gerber_number(circular_y,
+                                                         self.int_digits, self.frac_digits, self.gerber_zeros)
+                    except Exception as e:
+                        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 Exception as e:
+                        i = 0
+
+                    try:
+                        j = parse_gerber_number(j, self.int_digits, self.frac_digits, self.gerber_zeros)
+                    except Exception as e:
+                        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
+                    if d is not None:
+                        current_operation_code = int(d)
+
+                    # Nothing created! Pen Up.
+                    if current_operation_code == 2:
+                        log.warning("Arc with D2. (%d)" % line_num)
+                        if len(path) > 1:
+                            geo_dict = dict()
+
+                            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"]
+
+                            # this treats the case when we are storing geometry as paths
+                            geo_f = LineString(path)
+                            if not geo_f.is_empty:
+                                follow_buffer.append(geo_f)
+                                geo_dict['follow'] = geo_f
+
+                            # this treats the case when we are storing geometry as solids
+                            buffered = LineString(path).buffer(width / 1.999, int(self.steps_per_circle))
+                            if not buffered.is_empty:
+                                if self.app.defaults['gerber_simplification']:
+                                    poly_buffer.append(buffered.simplify(s_tol))
+                                else:
+                                    poly_buffer.append(buffered)
+
+                                if self.is_lpc is True:
+                                    geo_dict['clear'] = buffered
+                                else:
+                                    geo_dict['solid'] = buffered
+
+                            if last_path_aperture not in self.apertures:
+                                self.apertures[last_path_aperture] = dict()
+                            if 'geometry' not in self.apertures[last_path_aperture]:
+                                self.apertures[last_path_aperture]['geometry'] = []
+                            self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
+
+                        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],
+                                       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],
+                                               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 ---
+
+                    geo_dict = dict()
+                    # this treats the case when we are storing geometry as paths
+                    geo_f = LineString(path)
+                    if not geo_f.is_empty:
+                        follow_buffer.append(geo_f)
+                        geo_dict['follow'] = geo_f
+
+                    # this treats the case when we are storing geometry as solids
+                    width = self.apertures[last_path_aperture]["size"]
+                    geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                    if not geo_s.is_empty:
+                        if self.app.defaults['gerber_simplification']:
+                            poly_buffer.append(geo_s.simplify(s_tol))
+                        else:
+                            poly_buffer.append(geo_s)
+
+                        if self.is_lpc is True:
+                            geo_dict['clear'] = geo_s
+                        else:
+                            geo_dict['solid'] = geo_s
+
+                    if last_path_aperture not in self.apertures:
+                        self.apertures[last_path_aperture] = dict()
+                    if 'geometry' not in self.apertures[last_path_aperture]:
+                        self.apertures[last_path_aperture]['geometry'] = []
+                    self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
+
+            # --- Apply buffer ---
+            # this treats the case when we are storing geometry as paths
+            self.follow_geometry = follow_buffer
+
+            # this treats the case when we are storing geometry as solids
+
+            if len(poly_buffer) == 0:
+                log.error("Object is not Gerber file or empty. Aborting Object creation.")
+                return 'fail'
+
+            log.warning("Joining %d polygons." % len(poly_buffer))
+            self.app.inform.emit('%s: %d.' % (_("Gerber processing. Joining polygons"), len(poly_buffer)))
+
+            if self.use_buffer_for_union:
+                log.debug("Union by buffer...")
+
+                new_poly = MultiPolygon(poly_buffer)
+                if self.app.defaults["gerber_buffering"] == 'full':
+                    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.app.inform.emit('%s' % _("Gerber processing. Applying Gerber polarity."))
+                if new_poly.is_valid:
+                    self.solid_geometry = self.solid_geometry.union(new_poly)
+                else:
+                    # I do this so whenever the parsed geometry of the file is not valid (intersections) it is still
+                    # loaded. Instead of applying a union I add to a list of polygons.
+                    final_poly = []
+                    try:
+                        for poly in new_poly:
+                            final_poly.append(poly)
+                    except TypeError:
+                        final_poly.append(new_poly)
+
+                    try:
+                        for poly in self.solid_geometry:
+                            final_poly.append(poly)
+                    except TypeError:
+                        final_poly.append(self.solid_geometry)
+
+                    self.solid_geometry = final_poly
+
+                # try:
+                #     self.solid_geometry = self.solid_geometry.union(new_poly)
+                # except Exception as e:
+                #     # in case in the new_poly are some self intersections try to avoid making union with them
+                #     for poly in new_poly:
+                #         try:
+                #             self.solid_geometry = self.solid_geometry.union(poly)
+                #         except:
+                #             pass
+            else:
+                self.solid_geometry = self.solid_geometry.difference(new_poly)
+
+            # init this for the following operations
+            self.conversion_done = False
+        except Exception as err:
+            ex_type, ex, tb = sys.exc_info()
+            traceback.print_tb(tb)
+            # print traceback.format_exc()
+
+            log.error("Gerber PARSING FAILED. Line %d: %s" % (line_num, gline))
+
+            loc = '%s #%d %s: %s\n' % (_("Gerber Line"), line_num, _("Gerber Line Content"), gline) + repr(err)
+            self.app.inform.emit('[ERROR] %s\n%s:' %
+                                 (_("Gerber Parser ERROR"), loc))
+
+    @staticmethod
+    def create_flash_geometry(location, aperture, steps_per_circle=None):
+
+        # log.debug('Flashing @%s, Aperture: %s' % (location, aperture))
+
+        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
+        """
+        pass
+        # 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("parseGerber.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 and type(obj) is not MultiPolygon:
+                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:
+                        if not k.is_empty:
+                            try:
+                                minx_, miny_, maxx_, maxy_ = bounds_rec(k)
+                            except Exception as e:
+                                log.debug("camlib.Gerber.bounds() --> %s" % str(e))
+                                return
+
+                            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, obj_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
+        """
+
+        if obj_units.upper() == self.units.upper():
+            log.debug("parseGerber.Gerber.convert_units() --> Factor: 1")
+            return 1.0
+
+        if obj_units.upper() == "MM":
+            factor = 25.4
+            log.debug("parseGerber.Gerber.convert_units() --> Factor: 25.4")
+        elif obj_units.upper() == "IN":
+            factor = 1 / 25.4
+            log.debug("parseGerber.Gerber.convert_units() --> Factor: %s" % str(1 / 25.4))
+        else:
+            log.error("Unsupported units: %s" % str(obj_units))
+            log.debug("parseGerber.Gerber.convert_units() --> Factor: 1")
+            return 1.0
+
+        self.units = obj_units
+        self.file_units_factor = factor
+        self.scale(factor, factor)
+        return factor
+
+    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 xfactor: Number by which to scale on X axis.
+        :type xfactor: float
+        :param yfactor: Number by which to scale on Y axis.
+        :type yfactor: float
+        :param point: reference point for scaling operation
+        :rtype : None
+        """
+        log.debug("parseGerber.Gerber.scale()")
+
+        try:
+            xfactor = float(xfactor)
+        except:
+            self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                 _("Scale factor has to be a number: integer or float."))
+            return
+
+        if yfactor is None:
+            yfactor = xfactor
+        else:
+            try:
+                yfactor = float(yfactor)
+            except:
+                self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                     _("Scale factor has to be a number: integer or float."))
+                return
+
+        if point is None:
+            px = 0
+            py = 0
+        else:
+            px, py = point
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for __ in self.solid_geometry:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        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:
+                try:
+                    self.el_count += 1
+                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+                    if self.old_disp_number < disp_number <= 100:
+                        self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                        self.old_disp_number = disp_number
+
+                    return affinity.scale(obj, xfactor, yfactor, origin=(px, py))
+                except AttributeError:
+                    return obj
+
+        self.solid_geometry = scale_geom(self.solid_geometry)
+        self.follow_geometry = scale_geom(self.follow_geometry)
+
+        # we need to scale the geometry stored in the Gerber apertures, too
+        try:
+            for apid in self.apertures:
+                new_geometry = list()
+                if 'geometry' in self.apertures[apid]:
+                    for geo_el in self.apertures[apid]['geometry']:
+                        new_geo_el = dict()
+                        if 'solid' in geo_el:
+                            new_geo_el['solid'] = scale_geom(geo_el['solid'])
+                        if 'follow' in geo_el:
+                            new_geo_el['follow'] = scale_geom(geo_el['follow'])
+                        if 'clear' in geo_el:
+                            new_geo_el['clear'] = scale_geom(geo_el['clear'])
+                        new_geometry.append(new_geo_el)
+
+                self.apertures[apid]['geometry'] = deepcopy(new_geometry)
+
+                try:
+                    if str(self.apertures[apid]['type']) == 'R' or str(self.apertures[apid]['type']) == 'O':
+                        self.apertures[apid]['width'] *= xfactor
+                        self.apertures[apid]['height'] *= xfactor
+                    elif str(self.apertures[apid]['type']) == 'P':
+                        self.apertures[apid]['diam'] *= xfactor
+                        self.apertures[apid]['nVertices'] *= xfactor
+                except KeyError:
+                    pass
+
+                try:
+                    if self.apertures[apid]['size'] is not None:
+                        self.apertures[apid]['size'] = float(self.apertures[apid]['size'] * xfactor)
+                except KeyError:
+                    pass
+
+        except Exception as e:
+            log.debug('camlib.Gerber.scale() Exception --> %s' % str(e))
+            return 'fail'
+
+        self.app.inform.emit('[success] %s' %
+                             _("Gerber Scale done."))
+        self.app.proc_container.new_text = ''
+
+        # ## 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
+        """
+        log.debug("parseGerber.Gerber.offset()")
+
+        try:
+            dx, dy = vect
+        except TypeError:
+            self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                 _("An (x,y) pair of values are needed. "
+                                   "Probable you entered only one value in the Offset field."))
+            return
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for __ in self.solid_geometry:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        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:
+                try:
+                    self.el_count += 1
+                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+                    if self.old_disp_number < disp_number <= 100:
+                        self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                        self.old_disp_number = disp_number
+
+                    return affinity.translate(obj, xoff=dx, yoff=dy)
+                except AttributeError:
+                    return obj
+
+        # ## Solid geometry
+        self.solid_geometry = offset_geom(self.solid_geometry)
+        self.follow_geometry = offset_geom(self.follow_geometry)
+
+        # we need to offset the geometry stored in the Gerber apertures, too
+        try:
+            for apid in self.apertures:
+                if 'geometry' in self.apertures[apid]:
+                    for geo_el in self.apertures[apid]['geometry']:
+                        if 'solid' in geo_el:
+                            geo_el['solid'] = offset_geom(geo_el['solid'])
+                        if 'follow' in geo_el:
+                            geo_el['follow'] = offset_geom(geo_el['follow'])
+                        if 'clear' in geo_el:
+                            geo_el['clear'] = offset_geom(geo_el['clear'])
+
+        except Exception as e:
+            log.debug('camlib.Gerber.offset() Exception --> %s' % str(e))
+            return 'fail'
+
+        self.app.inform.emit('[success] %s' %
+                             _("Gerber Offset done."))
+        self.app.proc_container.new_text = ''
+
+    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
+        """
+        log.debug("parseGerber.Gerber.mirror()")
+
+        px, py = point
+        xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for __ in self.solid_geometry:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        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:
+                try:
+                    self.el_count += 1
+                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+                    if self.old_disp_number < disp_number <= 100:
+                        self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                        self.old_disp_number = disp_number
+
+                    return affinity.scale(obj, xscale, yscale, origin=(px, py))
+                except AttributeError:
+                    return obj
+
+        self.solid_geometry = mirror_geom(self.solid_geometry)
+        self.follow_geometry = mirror_geom(self.follow_geometry)
+
+        # we need to mirror the geometry stored in the Gerber apertures, too
+        try:
+            for apid in self.apertures:
+                if 'geometry' in self.apertures[apid]:
+                    for geo_el in self.apertures[apid]['geometry']:
+                        if 'solid' in geo_el:
+                            geo_el['solid'] = mirror_geom(geo_el['solid'])
+                        if 'follow' in geo_el:
+                            geo_el['follow'] = mirror_geom(geo_el['follow'])
+                        if 'clear' in geo_el:
+                            geo_el['clear'] = mirror_geom(geo_el['clear'])
+        except Exception as e:
+            log.debug('camlib.Gerber.mirror() Exception --> %s' % str(e))
+            return 'fail'
+
+        self.app.inform.emit('[success] %s' %
+                             _("Gerber Mirror done."))
+        self.app.proc_container.new_text = ''
+
+    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.
+
+        See shapely manual for more information:
+        http://toblerity.org/shapely/manual.html#affine-transformations
+        :param angle_x: the angle on X axis for skewing
+        :param angle_y: the angle on Y axis for skewing
+        :param point: reference point for skewing operation
+        :return None
+        """
+        log.debug("parseGerber.Gerber.skew()")
+
+        px, py = point
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for __ in self.solid_geometry:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        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:
+                try:
+                    self.el_count += 1
+                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+                    if self.old_disp_number < disp_number <= 100:
+                        self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                        self.old_disp_number = disp_number
+
+                    return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
+                except AttributeError:
+                    return obj
+
+        self.solid_geometry = skew_geom(self.solid_geometry)
+        self.follow_geometry = skew_geom(self.follow_geometry)
+
+        # we need to skew the geometry stored in the Gerber apertures, too
+        try:
+            for apid in self.apertures:
+                if 'geometry' in self.apertures[apid]:
+                    for geo_el in self.apertures[apid]['geometry']:
+                        if 'solid' in geo_el:
+                            geo_el['solid'] = skew_geom(geo_el['solid'])
+                        if 'follow' in geo_el:
+                            geo_el['follow'] = skew_geom(geo_el['follow'])
+                        if 'clear' in geo_el:
+                            geo_el['clear'] = skew_geom(geo_el['clear'])
+        except Exception as e:
+            log.debug('camlib.Gerber.skew() Exception --> %s' % str(e))
+            return 'fail'
+
+        self.app.inform.emit('[success] %s' % _("Gerber Skew done."))
+        self.app.proc_container.new_text = ''
+
+    def rotate(self, angle, point):
+        """
+        Rotate an object by a given angle around given coords (point)
+        :param angle:
+        :param point:
+        :return:
+        """
+        log.debug("parseGerber.Gerber.rotate()")
+
+        px, py = point
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for __ in self.solid_geometry:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        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:
+                try:
+                    self.el_count += 1
+                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+                    if self.old_disp_number < disp_number <= 100:
+                        self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                        self.old_disp_number = disp_number
+
+                    return affinity.rotate(obj, angle, origin=(px, py))
+                except AttributeError:
+                    return obj
+
+        self.solid_geometry = rotate_geom(self.solid_geometry)
+        self.follow_geometry = rotate_geom(self.follow_geometry)
+
+        # we need to rotate the geometry stored in the Gerber apertures, too
+        try:
+            for apid in self.apertures:
+                if 'geometry' in self.apertures[apid]:
+                    for geo_el in self.apertures[apid]['geometry']:
+                        if 'solid' in geo_el:
+                            geo_el['solid'] = rotate_geom(geo_el['solid'])
+                        if 'follow' in geo_el:
+                            geo_el['follow'] = rotate_geom(geo_el['follow'])
+                        if 'clear' in geo_el:
+                            geo_el['clear'] = rotate_geom(geo_el['clear'])
+        except Exception as e:
+            log.debug('camlib.Gerber.rotate() Exception --> %s' % str(e))
+            return 'fail'
+        self.app.inform.emit('[success] %s' %
+                             _("Gerber Rotate done."))
+        self.app.proc_container.new_text = ''
+
+
+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. Same situation for 'D' when
+    no zero suppression is done. If 'T', is in reverse.
+    :type zeros: str
+    :return: The number in floating point.
+    :rtype: float
+    """
+
+    ret_val = None
+
+    if zeros == 'L' or zeros == 'D':
+        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

+ 5 - 3
flatcamParsers/ParseSVG.py

@@ -1,4 +1,4 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # Author: Juan Pablo Caram (c)                             #
@@ -17,7 +17,7 @@
 #  * All transformations                                   #
 #                                                          #
 #  Reference: www.w3.org/TR/SVG/Overview.html              #
-# ########################################################## ##
+# ##########################################################
 
 # import xml.etree.ElementTree as ET
 from svg.path import Line, Arc, CubicBezier, QuadraticBezier, parse_path
@@ -136,6 +136,7 @@ def path2shapely(path, object_type, res=1.0):
 
     return geometry
 
+
 def svgrect2shapely(rect, n_points=32):
     """
     Converts an SVG rect into Shapely geometry.
@@ -284,7 +285,7 @@ def svgpolygon2shapely(polygon):
     # return LinearRing(points)
 
 
-def getsvggeo(node, object_type, root = None):
+def getsvggeo(node, object_type, root=None):
     """
     Extracts and flattens all geometry from an SVG node
     into a list of Shapely geometry.
@@ -482,6 +483,7 @@ def getsvgtext(node, object_type, units='MM'):
 
     return geo
 
+
 def parse_svg_point_list(ptliststr):
     """
     Returns a list of coordinate pairs extracted from the "points"

+ 70 - 146
flatcamTools/ToolCalculators.py

@@ -1,10 +1,9 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 from FlatCAMTool import FlatCAMTool
 from FlatCAMObj import *
@@ -30,6 +29,7 @@ class ToolCalculator(FlatCAMTool):
         FlatCAMTool.__init__(self, app)
 
         self.app = app
+        self.decimals = 6
 
         # ## Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
@@ -63,13 +63,14 @@ class ToolCalculator(FlatCAMTool):
         grid_units_layout.addWidget(inch_label, 0, 1)
 
         self.inch_entry = FCEntry()
+
         # self.inch_entry.setFixedWidth(70)
-        self.inch_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        # 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(130)
-        self.mm_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        # 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"))
 
         grid_units_layout.addWidget(self.mm_entry, 1, 0)
@@ -90,31 +91,35 @@ class ToolCalculator(FlatCAMTool):
         self.layout.addLayout(form_layout)
 
         self.tipDia_label = QtWidgets.QLabel('%s:' % _("Tip Diameter"))
-        self.tipDia_entry = FCEntry()
-        # self.tipDia_entry.setFixedWidth(70)
-        self.tipDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.tipDia_entry = FCDoubleSpinner()
+        self.tipDia_entry.set_precision(self.decimals)
+
+        # self.tipDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.tipDia_label.setToolTip(
             _("This is the tool tip diameter.\n"
               "It is specified by manufacturer.")
         )
         self.tipAngle_label = QtWidgets.QLabel('%s:' % _("Tip Angle"))
-        self.tipAngle_entry = FCEntry()
-        # self.tipAngle_entry.setFixedWidth(70)
-        self.tipAngle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.tipAngle_entry = FCSpinner()
+
+        # self.tipAngle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.tipAngle_label.setToolTip(_("This is the angle of the tip of the tool.\n"
                                          "It is specified by manufacturer."))
 
         self.cutDepth_label = QtWidgets.QLabel('%s:' % _("Cut Z"))
-        self.cutDepth_entry = FCEntry()
-        # self.cutDepth_entry.setFixedWidth(70)
-        self.cutDepth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.cutDepth_entry = FCDoubleSpinner()
+        self.cutDepth_entry.setMinimum(-1e10)    # to allow negative numbers without actually adding a real limit
+        self.cutDepth_entry.set_precision(self.decimals)
+
+        # self.cutDepth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.cutDepth_label.setToolTip(_("This is the depth to cut into the material.\n"
                                          "In the CNCJob is the CutZ parameter."))
 
         self.effectiveToolDia_label = QtWidgets.QLabel('%s:' % _("Tool Diameter"))
-        self.effectiveToolDia_entry = FCEntry()
-        # self.effectiveToolDia_entry.setFixedWidth(70)
-        self.effectiveToolDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.effectiveToolDia_entry = FCDoubleSpinner()
+        self.effectiveToolDia_entry.set_precision(self.decimals)
+
+        # self.effectiveToolDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.effectiveToolDia_label.setToolTip(_("This is the tool diameter to be entered into\n"
                                                  "FlatCAM Gerber section.\n"
                                                  "In the CNCJob section it is called >Tool dia<."))
@@ -132,9 +137,8 @@ class ToolCalculator(FlatCAMTool):
             _("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_vshape_button)
+        self.layout.addWidget(self.calculate_vshape_button)
 
         # ####################################
         # ## ElectroPlating Tool Calculator ##
@@ -156,48 +160,54 @@ class ToolCalculator(FlatCAMTool):
         self.layout.addLayout(plate_form_layout)
 
         self.pcblengthlabel = QtWidgets.QLabel('%s:' % _("Board Length"))
-        self.pcblength_entry = FCEntry()
-        # self.pcblengthlabel.setFixedWidth(70)
-        self.pcblength_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.pcblength_entry = FCDoubleSpinner()
+        self.pcblength_entry.set_precision(self.decimals)
+
+        # self.pcblength_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.pcblengthlabel.setToolTip(_('This is the board length. In centimeters.'))
 
         self.pcbwidthlabel = QtWidgets.QLabel('%s:' % _("Board Width"))
-        self.pcbwidth_entry = FCEntry()
-        # self.pcbwidthlabel.setFixedWidth(70)
-        self.pcbwidth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.pcbwidth_entry = FCDoubleSpinner()
+        self.pcbwidth_entry.set_precision(self.decimals)
+
+        # self.pcbwidth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.pcbwidthlabel.setToolTip(_('This is the board width.In centimeters.'))
 
         self.cdensity_label = QtWidgets.QLabel('%s:' % _("Current Density"))
-        self.cdensity_entry = FCEntry()
-        # self.cdensity_entry.setFixedWidth(70)
-        self.cdensity_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.cdensity_entry = FCDoubleSpinner()
+        self.cdensity_entry.set_precision(self.decimals)
+
+        # self.cdensity_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.cdensity_label.setToolTip(_("Current density to pass through the board. \n"
                                          "In Amps per Square Feet ASF."))
 
         self.growth_label = QtWidgets.QLabel('%s:' % _("Copper Growth"))
-        self.growth_entry = FCEntry()
-        # self.growth_entry.setFixedWidth(70)
-        self.growth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.growth_entry = FCDoubleSpinner()
+        self.growth_entry.set_precision(self.decimals)
+
+        # self.growth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.growth_label.setToolTip(_("How thick the copper growth is intended to be.\n"
                                        "In microns."))
 
         # self.growth_entry.setEnabled(False)
 
         self.cvaluelabel = QtWidgets.QLabel('%s:' % _("Current Value"))
-        self.cvalue_entry = FCEntry()
-        # self.cvaluelabel.setFixedWidth(70)
-        self.cvalue_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.cvalue_entry = FCDoubleSpinner()
+        self.cvalue_entry.set_precision(self.decimals)
+
+        # self.cvalue_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.cvaluelabel.setToolTip(_('This is the current intensity value\n'
                                       'to be set on the Power Supply. In Amps.'))
-        self.cvalue_entry.setDisabled(True)
+        self.cvalue_entry.setReadOnly(True)
 
         self.timelabel = QtWidgets.QLabel('%s:' % _("Time"))
-        self.time_entry = FCEntry()
-        # self.timelabel.setFixedWidth(70)
-        self.time_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.time_entry = FCDoubleSpinner()
+        self.time_entry.set_precision(self.decimals)
+
+        # self.time_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.timelabel.setToolTip(_('This is the calculated time required for the procedure.\n'
                                     'In minutes.'))
-        self.time_entry.setDisabled(True)
+        self.time_entry.setReadOnly(True)
 
         plate_form_layout.addRow(self.pcblengthlabel, self.pcblength_entry)
         plate_form_layout.addRow(self.pcbwidthlabel, self.pcbwidth_entry)
@@ -213,19 +223,17 @@ class ToolCalculator(FlatCAMTool):
             _("Calculate the current intensity value and the procedure time,\n"
               "depending on the parameters above")
         )
-        self.empty_label_2 = QtWidgets.QLabel(" ")
-
-        plate_form_layout.addRow(self.empty_label_2, self.calculate_plate_button)
+        self.layout.addWidget(self.calculate_plate_button)
 
         self.layout.addStretch()
 
         self.units = ''
 
         # ## 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.cutDepth_entry.valueChanged.connect(self.on_calculate_tool_dia)
+        self.cutDepth_entry.returnPressed.connect(self.on_calculate_tool_dia)
+        self.tipDia_entry.returnPressed.connect(self.on_calculate_tool_dia)
+        self.tipAngle_entry.returnPressed.connect(self.on_calculate_tool_dia)
         self.calculate_vshape_button.clicked.connect(self.on_calculate_tool_dia)
 
         self.mm_entry.editingFinished.connect(self.on_calculate_inch_units)
@@ -268,8 +276,8 @@ class ToolCalculator(FlatCAMTool):
         self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
 
         # ## Initialize form
-        self.mm_entry.set_value('0')
-        self.inch_entry.set_value('0')
+        self.mm_entry.set_value('%.*f' % (self.decimals, 0))
+        self.inch_entry.set_value('%.*f' % (self.decimals, 0))
 
         length = self.app.defaults["tools_calc_electro_length"]
         width = self.app.defaults["tools_calc_electro_width"]
@@ -300,114 +308,30 @@ class ToolCalculator(FlatCAMTool):
         # 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())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                tip_diameter = float(self.tipDia_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
-
-        try:
-            half_tip_angle = float(self.tipAngle_entry.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                half_tip_angle = float(self.tipAngle_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
+        tip_diameter = float(self.tipDia_entry.get_value())
+
+        half_tip_angle = float(self.tipAngle_entry.get_value())
         half_tip_angle /= 2
 
-        try:
-            cut_depth = float(self.cutDepth_entry.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                cut_depth = float(self.cutDepth_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
+        cut_depth = float(self.cutDepth_entry.get_value())
+        cut_depth = -cut_depth if cut_depth < 0 else cut_depth
 
         tool_diameter = tip_diameter + (2 * cut_depth * math.tan(math.radians(half_tip_angle)))
-        self.effectiveToolDia_entry.set_value("%.4f" % tool_diameter)
+        self.effectiveToolDia_entry.set_value("%.*f" % (self.decimals, tool_diameter))
 
     def on_calculate_inch_units(self):
-        try:
-            mm_val = float(self.mm_entry.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                mm_val = float(self.mm_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
-        self.inch_entry.set_value('%.6f' % (mm_val / 25.4))
+        mm_val = float(self.mm_entry.get_value())
+        self.inch_entry.set_value('%.*f' % (self.decimals,(mm_val / 25.4)))
 
     def on_calculate_mm_units(self):
-        try:
-            inch_val = float(self.inch_entry.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                inch_val = float(self.inch_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
-        self.mm_entry.set_value('%.6f' % (inch_val * 25.4))
+        inch_val = float(self.inch_entry.get_value())
+        self.mm_entry.set_value('%.*f' % (self.decimals,(inch_val * 25.4)))
 
     def on_calculate_eplate(self):
-
-        try:
-            length = float(self.pcblength_entry.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                length = float(self.pcblength_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
-
-        try:
-            width = float(self.pcbwidth_entry.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                width = float(self.pcbwidth_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
-
-        try:
-            density = float(self.cdensity_entry.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                density = float(self.cdensity_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
-
-        try:
-            copper = float(self.growth_entry.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                copper = float(self.growth_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
+        length = float(self.pcblength_entry.get_value())
+        width = float(self.pcbwidth_entry.get_value())
+        density = float(self.cdensity_entry.get_value())
+        copper = float(self.growth_entry.get_value())
 
         calculated_current = (length * width * density) * 0.0021527820833419
         calculated_time = copper * 2.142857142857143 * float(20 / density)

+ 27 - 111
flatcamTools/ToolCutOut.py

@@ -1,3 +1,10 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Date: 3/10/2019                                          #
+# MIT Licence                                              #
+# ##########################################################
+
 from FlatCAMTool import FlatCAMTool
 from ObjectCollection import *
 from FlatCAMApp import *
@@ -22,6 +29,7 @@ class CutOut(FlatCAMTool):
 
         self.app = app
         self.canvas = app.plotcanvas
+        self.decimals = 4
 
         # Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
@@ -87,7 +95,9 @@ class CutOut(FlatCAMTool):
         form_layout.addRow(self.kindlabel, self.obj_kind_combo)
 
         # Tool Diameter
-        self.dia = FCEntry()
+        self.dia = FCDoubleSpinner()
+        self.dia.set_precision(self.decimals)
+
         self.dia_label = QtWidgets.QLabel('%s:' % _("Tool dia"))
         self.dia_label.setToolTip(
            _("Diameter of the tool used to cutout\n"
@@ -96,7 +106,9 @@ class CutOut(FlatCAMTool):
         form_layout.addRow(self.dia_label, self.dia)
 
         # Margin
-        self.margin = FCEntry()
+        self.margin = FCDoubleSpinner()
+        self.margin.set_precision(self.decimals)
+
         self.margin_label = QtWidgets.QLabel('%s:' % _("Margin:"))
         self.margin_label.setToolTip(
            _("Margin over bounds. A positive value here\n"
@@ -106,7 +118,9 @@ class CutOut(FlatCAMTool):
         form_layout.addRow(self.margin_label, self.margin)
 
         # Gapsize
-        self.gapsize = FCEntry()
+        self.gapsize = FCDoubleSpinner()
+        self.gapsize.set_precision(self.decimals)
+
         self.gapsize_label = QtWidgets.QLabel('%s:' % _("Gap size:"))
         self.gapsize_label.setToolTip(
            _("The size of the bridge gaps in the cutout\n"
@@ -381,17 +395,7 @@ class CutOut(FlatCAMTool):
                                  _("There is no object selected for Cutout.\nSelect one and try again."))
             return
 
-        try:
-            dia = float(self.dia.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                dia = float(self.dia.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                     _("Tool diameter value is missing or wrong format. Add it and retry."))
-                return
-
+        dia = float(self.dia.get_value())
         if 0 in {dia}:
             self.app.inform.emit('[WARNING_NOTCL] %s' %
                                  _("Tool Diameter is zero value. Change it to a positive real number."))
@@ -402,27 +406,8 @@ class CutOut(FlatCAMTool):
         except ValueError:
             return
 
-        try:
-            margin = float(self.margin.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                margin = float(self.margin.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                     _("Margin value is missing or wrong format. Add it and retry."))
-                return
-
-        try:
-            gapsize = float(self.gapsize.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                gapsize = float(self.gapsize.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                     _("Gap size value is missing or wrong format. Add it and retry."))
-                return
+        margin = float(self.margin.get_value())
+        gapsize = float(self.gapsize.get_value())
 
         try:
             gaps = self.gaps.get_value()
@@ -579,17 +564,7 @@ class CutOut(FlatCAMTool):
         if cutout_obj is None:
             self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(name)))
 
-        try:
-            dia = float(self.dia.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                dia = float(self.dia.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                     _("Tool diameter value is missing or wrong format. Add it and retry."))
-                return
-
+        dia = float(self.dia.get_value())
         if 0 in {dia}:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
                                  _("Tool Diameter is zero value. Change it to a positive real number."))
@@ -600,27 +575,8 @@ class CutOut(FlatCAMTool):
         except ValueError:
             return
 
-        try:
-            margin = float(self.margin.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                margin = float(self.margin.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                     _("Margin value is missing or wrong format. Add it and retry."))
-                return
-
-        try:
-            gapsize = float(self.gapsize.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                gapsize = float(self.gapsize.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                     _("Gap size value is missing or wrong format. Add it and retry."))
-                return
+        margin = float(self.margin.get_value())
+        gapsize = float(self.gapsize.get_value())
 
         try:
             gaps = self.gaps.get_value()
@@ -749,32 +705,13 @@ class CutOut(FlatCAMTool):
         self.app.inform.emit(_("Click on the selected geometry object perimeter to create a bridge gap ..."))
         self.app.geo_editor.tool_shape.enabled = True
 
-        try:
-            self.cutting_dia = float(self.dia.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                self.cutting_dia = float(self.dia.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                     _("Tool diameter value is missing or wrong format. Add it and retry."))
-                return
-
+        self.cutting_dia = float(self.dia.get_value())
         if 0 in {self.cutting_dia}:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
                                  _("Tool Diameter is zero value. Change it to a positive real number."))
             return "Tool Diameter is zero value. Change it to a positive real number."
 
-        try:
-            self.cutting_gapsize = float(self.gapsize.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                self.cutting_gapsize = float(self.gapsize.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                     _("Gap size value is missing or wrong format. Add it and retry."))
-                return
+        self.cutting_gapsize = float(self.gapsize.get_value())
 
         name = self.man_object_combo.currentText()
         # Get Geometry source object to be used as target for Manual adding Gaps
@@ -800,7 +737,6 @@ class CutOut(FlatCAMTool):
         self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
         self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
 
-
     def on_manual_cutout(self, click_pos):
         name = self.man_object_combo.currentText()
 
@@ -851,17 +787,7 @@ class CutOut(FlatCAMTool):
                                    "Select a Gerber file and try again."))
             return
 
-        try:
-            dia = float(self.dia.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                dia = float(self.dia.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                     _("Tool diameter value is missing or wrong format. Add it and retry."))
-                return
-
+        dia = float(self.dia.get_value())
         if 0 in {dia}:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
                                  _("Tool Diameter is zero value. Change it to a positive real number."))
@@ -872,17 +798,7 @@ class CutOut(FlatCAMTool):
         except ValueError:
             return
 
-        try:
-            margin = float(self.margin.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                margin = float(self.margin.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                     _("Margin value is missing or wrong format. Add it and retry."))
-                return
-
+        margin = float(self.margin.get_value())
         convex_box = self.convex_box.get_value()
 
         def geo_init(geo_obj, app_obj):

+ 12 - 6
flatcamTools/ToolDblSided.py

@@ -19,6 +19,7 @@ class DblSidedTool(FlatCAMTool):
 
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
+        self.decimals = 4
 
         # ## Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
@@ -219,25 +220,30 @@ class DblSidedTool(FlatCAMTool):
         grid_lay3.addWidget(self.alignment_holes, 0, 0)
         grid_lay3.addWidget(self.add_drill_point_button, 0, 1)
 
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+
         # ## Drill diameter for alignment holes
         self.dt_label = QtWidgets.QLabel("<b>%s:</b>" % _('Alignment Drill Diameter'))
         self.dt_label.setToolTip(
             _("Diameter of the drill for the "
               "alignment holes.")
         )
-        self.layout.addWidget(self.dt_label)
+        grid0.addWidget(self.dt_label, 0, 0, 1, 2)
 
-        hlay = QtWidgets.QHBoxLayout()
-        self.layout.addLayout(hlay)
+        # Drill diameter value
+        self.drill_dia = FCDoubleSpinner()
+        self.drill_dia.set_precision(self.decimals)
 
-        self.drill_dia = FCEntry()
         self.dd_label = QtWidgets.QLabel('%s:' % _("Drill dia"))
         self.dd_label.setToolTip(
             _("Diameter of the drill for the "
               "alignment holes.")
         )
-        hlay.addWidget(self.dd_label)
-        hlay.addWidget(self.drill_dia)
+        grid0.addWidget(self.dd_label, 1, 0)
+        grid0.addWidget(self.drill_dia, 1, 1)
 
         hlay2 = QtWidgets.QHBoxLayout()
         self.layout.addLayout(hlay2)

+ 93 - 36
flatcamTools/ToolMeasurement.py → flatcamTools/ToolDistance.py

@@ -1,10 +1,9 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 from FlatCAMTool import FlatCAMTool
 from FlatCAMObj import *
@@ -21,9 +20,9 @@ if '_' not in builtins.__dict__:
     _ = gettext.gettext
 
 
-class Measurement(FlatCAMTool):
+class Distance(FlatCAMTool):
 
-    toolName = _("Measurement")
+    toolName = _("Distance Tool")
 
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
@@ -57,26 +56,39 @@ class Measurement(FlatCAMTool):
         self.distance_y_label = QtWidgets.QLabel('%s:' % _("Dy"))
         self.distance_y_label.setToolTip(_("This is the distance measured over the Y axis."))
 
+        self.angle_label = QtWidgets.QLabel('%s:' % _("Angle"))
+        self.angle_label.setToolTip(_("This is orientation angle of the measuring line."))
+
         self.total_distance_label = QtWidgets.QLabel("<b>%s:</b>" % _('DISTANCE'))
         self.total_distance_label.setToolTip(_("This is the point to point Euclidian distance."))
 
         self.start_entry = FCEntry()
+        self.start_entry.setReadOnly(True)
         self.start_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.start_entry.setToolTip(_("This is measuring Start point coordinates."))
 
         self.stop_entry = FCEntry()
+        self.stop_entry.setReadOnly(True)
         self.stop_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.stop_entry.setToolTip(_("This is the measuring Stop point coordinates."))
 
         self.distance_x_entry = FCEntry()
+        self.distance_x_entry.setReadOnly(True)
         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_y_entry = FCEntry()
+        self.distance_y_entry.setReadOnly(True)
         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.angle_entry = FCEntry()
+        self.angle_entry.setReadOnly(True)
+        self.angle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.angle_entry.setToolTip(_("This is orientation angle of the measuring line."))
+
         self.total_distance_entry = FCEntry()
+        self.total_distance_entry.setReadOnly(True)
         self.total_distance_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.total_distance_entry.setToolTip(_("This is the point to point Euclidian distance."))
 
@@ -89,14 +101,16 @@ class Measurement(FlatCAMTool):
         form_layout.addRow(self.stop_label, self.stop_entry)
         form_layout.addRow(self.distance_x_label, self.distance_x_entry)
         form_layout.addRow(self.distance_y_label, self.distance_y_entry)
+        form_layout.addRow(self.angle_label, self.angle_entry)
         form_layout.addRow(self.total_distance_label, self.total_distance_entry)
 
         # 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.distance_x_entry.set_value('0.0')
+        self.distance_y_entry.set_value('0.0')
+        self.angle_entry.set_value('0.0')
+        self.total_distance_entry.set_value('0.0')
 
         self.layout.addStretch()
 
@@ -112,6 +126,12 @@ class Measurement(FlatCAMTool):
 
         self.original_call_source = 'app'
 
+        # store here the event connection ID's
+        self.mm = None
+        self.mr = None
+
+        self.decimals = 4
+
         # VisPy visuals
         if self.app.is_legacy is False:
             self.sel_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1)
@@ -122,7 +142,7 @@ class Measurement(FlatCAMTool):
         self.measure_btn.clicked.connect(self.activate_measure_tool)
 
     def run(self, toggle=False):
-        self.app.report_usage("ToolMeasurement()")
+        self.app.report_usage("ToolDistance()")
 
         self.points[:] = []
 
@@ -132,7 +152,7 @@ class Measurement(FlatCAMTool):
         if self.app.tool_tab_locked is True:
             return
 
-        self.app.ui.notebook.setTabText(2, _("Meas. Tool"))
+        self.app.ui.notebook.setTabText(2, _("Distance Tool"))
 
         # if the splitter is hidden, display it
         if self.app.ui.splitter.sizes()[0] == 0:
@@ -159,16 +179,17 @@ class Measurement(FlatCAMTool):
         self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
         self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
 
-        self.app.command_active = "Measurement"
+        self.app.command_active = "Distance"
 
         # 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')
-        log.debug("Measurement Tool --> tool initialized")
+        self.distance_x_entry.set_value('0.0')
+        self.distance_y_entry.set_value('0.0')
+        self.angle_entry.set_value('0.0')
+        self.total_distance_entry.set_value('0.0')
+        log.debug("Distance Tool --> tool initialized")
 
     def activate_measure_tool(self):
         # ENABLE the Measuring TOOL
@@ -275,13 +296,13 @@ class Measurement(FlatCAMTool):
         # delete the measuring line
         self.delete_shape()
 
-        log.debug("Measurement Tool --> exit tool")
+        log.debug("Distance Tool --> exit tool")
 
     def on_mouse_click_release(self, event):
         # mouse click releases will be accepted only if the left button is clicked
         # this is necessary because right mouse click or middle mouse click
         # are used for panning on the canvas
-        log.debug("Measuring Tool --> mouse click release")
+        log.debug("Distance Tool --> mouse click release")
 
         if event.button == 1:
             if self.app.is_legacy is False:
@@ -300,30 +321,44 @@ class Measurement(FlatCAMTool):
 
             # Reset here the relative coordinates so there is a new reference on the click position
             if self.rel_point1 is None:
-                self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
-                                                       "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0.0, 0.0))
+                self.app.ui.rel_position_label.setText("<b>Dx</b>: %.*f&nbsp;&nbsp;  <b>Dy</b>: "
+                                                       "%.*f&nbsp;&nbsp;&nbsp;&nbsp;" %
+                                                       (self.decimals, 0.0, self.decimals, 0.0))
                 self.rel_point1 = pos
             else:
                 self.rel_point2 = copy(self.rel_point1)
                 self.rel_point1 = pos
 
             if len(self.points) == 1:
-                self.start_entry.set_value("(%.4f, %.4f)" % pos)
+                self.start_entry.set_value("(%.*f, %.*f)" % (self.decimals, pos[0], self.decimals, pos[1]))
                 self.app.inform.emit(_("MEASURING: Click on the Destination point ..."))
             elif len(self.points) == 2:
                 dx = self.points[1][0] - self.points[0][0]
                 dy = self.points[1][1] - self.points[0][1]
                 d = sqrt(dx ** 2 + dy ** 2)
-                self.stop_entry.set_value("(%.4f, %.4f)" % pos)
+                self.stop_entry.set_value("(%.*f, %.*f)" % (self.decimals, pos[0], self.decimals, pos[1]))
 
                 self.app.inform.emit(_("MEASURING: Result D(x) = {d_x} | D(y) = {d_y} | Distance = {d_z}").format(
-                    d_x='%4f' % abs(dx), d_y='%4f' % abs(dy), d_z='%4f' % 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.app.ui.rel_position_label.setText("<b>Dx</b>: {0:.4f}&nbsp;&nbsp;  <b>Dy</b>: "
-                                                       "{0:.4f}&nbsp;&nbsp;&nbsp;&nbsp;".format(pos[0], pos[1]))
+                    d_x='%*f' % (self.decimals, abs(dx)),
+                    d_y='%*f' % (self.decimals, abs(dy)),
+                    d_z='%*f' % (self.decimals, abs(d)))
+                )
+
+                self.distance_x_entry.set_value('%.*f' % (self.decimals, abs(dx)))
+                self.distance_y_entry.set_value('%.*f' % (self.decimals, abs(dy)))
+
+                try:
+                    angle = math.degrees(math.atan(dy / dx))
+                    self.angle_entry.set_value('%.*f' % (self.decimals, angle))
+                except Exception as e:
+                    pass
+
+                self.total_distance_entry.set_value('%.*f' % (self.decimals, abs(d)))
+                self.app.ui.rel_position_label.setText(
+                    "<b>Dx</b>: {}&nbsp;&nbsp;  <b>Dy</b>: {}&nbsp;&nbsp;&nbsp;&nbsp;".format(
+                        '%.*f' % (self.decimals, pos[0]), '%.*f' % (self.decimals, pos[1])
+                    )
+                )
                 self.deactivate_measure_tool()
 
     def on_mouse_move_meas(self, event):
@@ -346,13 +381,16 @@ class Measurement(FlatCAMTool):
 
                 # Update cursor
                 self.app.app_cursor.set_data(np.asarray([(pos[0], pos[1])]),
-                                             symbol='++', edge_color='black',
+                                             symbol='++', edge_color=self.app.cursor_color_3D,
                                              size=self.app.defaults["global_cursor_size"])
             else:
                 pos = (pos_canvas[0], pos_canvas[1])
 
-            self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: {0:.4f}&nbsp;&nbsp;   "
-                                               "<b>Y</b>: {0:.4f}".format(pos[0], pos[1]))
+            self.app.ui.position_label.setText(
+                "&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: {}&nbsp;&nbsp;   <b>Y</b>: {}".format(
+                    '%.*f' % (self.decimals, pos[0]), '%.*f' % (self.decimals, pos[1])
+                )
+            )
 
             if self.rel_point1 is not None:
                 dx = pos[0] - float(self.rel_point1[0])
@@ -361,15 +399,24 @@ class Measurement(FlatCAMTool):
                 dx = pos[0]
                 dy = pos[1]
 
-            self.app.ui.rel_position_label.setText("<b>Dx</b>: {0:.4f}&nbsp;&nbsp;  <b>Dy</b>: "
-                                                   "{0:.4f}&nbsp;&nbsp;&nbsp;&nbsp;".format(dx, dy))
+            self.app.ui.rel_position_label.setText(
+                "<b>Dx</b>: {}&nbsp;&nbsp;  <b>Dy</b>: {}&nbsp;&nbsp;&nbsp;&nbsp;".format(
+                    '%.*f' % (self.decimals, dx), '%.*f' % (self.decimals, dy)
+                )
+            )
 
             # update utility geometry
-
             if len(self.points) == 1:
                 self.utility_geometry(pos=pos)
+                # and display the temporary angle
+                try:
+                    angle = math.degrees(math.atan(dy / dx))
+                    self.angle_entry.set_value('%.*f' % (self.decimals, angle))
+                except Exception as e:
+                    pass
+
         except Exception as e:
-            log.debug("Measurement.on_mouse_move_meas() --> %s" % str(e))
+            log.debug("Distance.on_mouse_move_meas() --> %s" % str(e))
             self.app.ui.position_label.setText("")
             self.app.ui.rel_position_label.setText("")
 
@@ -380,7 +427,17 @@ class Measurement(FlatCAMTool):
         # second draw the new shape of the utility geometry
         meas_line = LineString([pos, self.points[0]])
 
-        color = '#00000000'
+        settings = QtCore.QSettings("Open Source", "FlatCAM")
+        if settings.contains("theme"):
+            theme = settings.value('theme', type=str)
+        else:
+            theme = 'white'
+
+        if theme == 'white':
+            color = '#000000FF'
+        else:
+            color = '#FFFFFFFF'
+
         self.sel_shapes.add(meas_line, color=color, update=True, layer=0, tolerance=None)
 
         if self.app.is_legacy is True:

+ 296 - 0
flatcamTools/ToolDistanceMin.py

@@ -0,0 +1,296 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Date: 09/29/2019                                         #
+# MIT Licence                                              #
+# ##########################################################
+
+from FlatCAMTool import FlatCAMTool
+from FlatCAMObj import *
+from flatcamGUI.VisPyVisuals import *
+
+from shapely.ops import nearest_points
+
+from math import sqrt
+
+import gettext
+import FlatCAMTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class DistanceMin(FlatCAMTool):
+
+    toolName = _("Minimum Distance Tool")
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+
+        self.app = app
+        self.canvas = self.app.plotcanvas
+        self.units = self.app.ui.general_defaults_form.general_app_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)
+
+        self.units_label = QtWidgets.QLabel('%s:' % _("Units"))
+        self.units_label.setToolTip(_("Those are the units in which the distance is measured."))
+        self.units_value = QtWidgets.QLabel("%s" % str({'mm': _("METRIC (mm)"), 'in': _("INCH (in)")}[self.units]))
+        self.units_value.setDisabled(True)
+
+        self.start_label = QtWidgets.QLabel("%s:" % _('First object point'))
+        self.start_label.setToolTip(_("This is first object point coordinates.\n"
+                                      "This is the start point for measuring distance."))
+
+        self.stop_label = QtWidgets.QLabel("%s:" % _('Second object point'))
+        self.stop_label.setToolTip(_("This is second object point coordinates.\n"
+                                      "This is the end point for measuring distance."))
+
+        self.distance_x_label = QtWidgets.QLabel('%s:' % _("Dx"))
+        self.distance_x_label.setToolTip(_("This is the distance measured over the X axis."))
+
+        self.distance_y_label = QtWidgets.QLabel('%s:' % _("Dy"))
+        self.distance_y_label.setToolTip(_("This is the distance measured over the Y axis."))
+
+        self.angle_label = QtWidgets.QLabel('%s:' % _("Angle"))
+        self.angle_label.setToolTip(_("This is orientation angle of the measuring line."))
+
+        self.total_distance_label = QtWidgets.QLabel("<b>%s:</b>" % _('DISTANCE'))
+        self.total_distance_label.setToolTip(_("This is the point to point Euclidean distance."))
+
+        self.half_point_label = QtWidgets.QLabel("<b>%s:</b>" % _('Half Point'))
+        self.half_point_label.setToolTip(_("This is the middle point of the point to point Euclidean distance."))
+
+        self.start_entry = FCEntry()
+        self.start_entry.setReadOnly(True)
+        self.start_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.start_entry.setToolTip(_("This is first object point coordinates.\n"
+                                      "This is the start point for measuring distance."))
+
+        self.stop_entry = FCEntry()
+        self.stop_entry.setReadOnly(True)
+        self.stop_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.stop_entry.setToolTip(_("This is second object point coordinates.\n"
+                                      "This is the end point for measuring distance."))
+
+        self.distance_x_entry = FCEntry()
+        self.distance_x_entry.setReadOnly(True)
+        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_y_entry = FCEntry()
+        self.distance_y_entry.setReadOnly(True)
+        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.angle_entry = FCEntry()
+        self.angle_entry.setReadOnly(True)
+        self.angle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.angle_entry.setToolTip(_("This is orientation angle of the measuring line."))
+
+        self.total_distance_entry = FCEntry()
+        self.total_distance_entry.setReadOnly(True)
+        self.total_distance_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.total_distance_entry.setToolTip(_("This is the point to point Euclidean distance."))
+
+        self.half_point_entry = FCEntry()
+        self.half_point_entry.setReadOnly(True)
+        self.half_point_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.half_point_entry.setToolTip(_("This is the middle point of the point to point Euclidean distance."))
+
+        self.measure_btn = QtWidgets.QPushButton(_("Measure"))
+        self.layout.addWidget(self.measure_btn)
+
+        self.jump_hp_btn = QtWidgets.QPushButton(_("Jump to Half Point"))
+        self.layout.addWidget(self.jump_hp_btn)
+        self.jump_hp_btn.setDisabled(True)
+
+        form_layout.addRow(self.units_label, self.units_value)
+        form_layout.addRow(self.start_label, self.start_entry)
+        form_layout.addRow(self.stop_label, self.stop_entry)
+        form_layout.addRow(self.distance_x_label, self.distance_x_entry)
+        form_layout.addRow(self.distance_y_label, self.distance_y_entry)
+        form_layout.addRow(self.angle_label, self.angle_entry)
+        form_layout.addRow(self.total_distance_label, self.total_distance_entry)
+        form_layout.addRow(self.half_point_label, self.half_point_entry)
+
+        # 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.0')
+        self.distance_y_entry.set_value('0.0')
+        self.angle_entry.set_value('0.0')
+        self.total_distance_entry.set_value('0.0')
+        self.half_point_entry.set_value('(0, 0)')
+
+        self.layout.addStretch()
+
+        self.decimals = 4
+        self.h_point = (0, 0)
+
+        self.measure_btn.clicked.connect(self.activate_measure_tool)
+        self.jump_hp_btn.clicked.connect(self.on_jump_to_half_point)
+
+    def run(self, toggle=False):
+        self.app.report_usage("ToolDistanceMin()")
+
+        if self.app.tool_tab_locked is True:
+            return
+
+        self.app.ui.notebook.setTabText(2, _("Minimum Distance Tool"))
+
+        # if the splitter is hidden, display it
+        if self.app.ui.splitter.sizes()[0] == 0:
+            self.app.ui.splitter.setSizes([1, 1])
+
+        if toggle:
+            pass
+
+        self.set_tool_ui()
+        self.app.inform.emit('MEASURING: %s' %
+                             _("Select two objects and no more, to measure the distance between them ..."))
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='SHIFT+M', **kwargs)
+
+    def set_tool_ui(self):
+        # Remove anything else in the GUI
+        self.app.ui.tool_scroll_area.takeWidget()
+
+        # Put oneself 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.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
+
+        # 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.0')
+        self.distance_y_entry.set_value('0.0')
+        self.angle_entry.set_value('0.0')
+        self.total_distance_entry.set_value('0.0')
+        self.half_point_entry.set_value('(0, 0)')
+
+        self.jump_hp_btn.setDisabled(True)
+
+        log.debug("Minimum Distance Tool --> tool initialized")
+
+    def activate_measure_tool(self):
+        # ENABLE the Measuring TOOL
+        self.jump_hp_btn.setDisabled(False)
+
+        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
+
+        if self.app.call_source == 'app':
+            selected_objs = self.app.collection.get_selected()
+            if len(selected_objs) != 2:
+                self.app.inform.emit('[WARNING_NOTCL] %s %s' %
+                                     (_("Select two objects and no more. Currently the selection has objects: "),
+                                     str(len(selected_objs))))
+                return
+            else:
+                first_pos, last_pos = nearest_points(selected_objs[0].solid_geometry, selected_objs[1].solid_geometry)
+
+        elif self.app.call_source == 'geo_editor':
+            selected_objs = self.app.geo_editor.selected
+            if len(selected_objs) != 2:
+                self.app.inform.emit('[WARNING_NOTCL] %s %s' %
+                                     (_("Select two objects and no more. Currently the selection has objects: "),
+                                     str(len(selected_objs))))
+                return
+            else:
+                first_pos, last_pos = nearest_points(selected_objs[0].geo, selected_objs[1].geo)
+        elif self.app.call_source == 'exc_editor':
+            selected_objs = self.app.exc_editor.selected
+            if len(selected_objs) != 2:
+                self.app.inform.emit('[WARNING_NOTCL] %s %s' %
+                                     (_("Select two objects and no more. Currently the selection has objects: "),
+                                      str(len(selected_objs))))
+                return
+            else:
+                # the objects are really MultiLinesStrings made out of 2 lines in cross shape
+                xmin, ymin, xmax, ymax = selected_objs[0].geo.bounds
+                first_geo_radius = (xmax - xmin) / 2
+                first_geo_center = Point(xmin + first_geo_radius, ymin + first_geo_radius)
+                first_geo = first_geo_center.buffer(first_geo_radius)
+
+                # the objects are really MultiLinesStrings made out of 2 lines in cross shape
+                xmin, ymin, xmax, ymax = selected_objs[1].geo.bounds
+                last_geo_radius = (xmax - xmin) / 2
+                last_geo_center = Point(xmin + last_geo_radius, ymin + last_geo_radius)
+                last_geo = last_geo_center.buffer(last_geo_radius)
+
+                first_pos, last_pos = nearest_points(first_geo, last_geo)
+        elif self.app.call_source == 'grb_editor':
+            selected_objs = self.app.grb_editor.selected
+            if len(selected_objs) != 2:
+                self.app.inform.emit('[WARNING_NOTCL] %s %s' %
+                                     (_("Select two objects and no more. Currently the selection has objects: "),
+                                      str(len(selected_objs))))
+                return
+            else:
+                first_pos, last_pos = nearest_points(selected_objs[0].geo['solid'], selected_objs[1].geo['solid'])
+        else:
+            first_pos, last_pos = 0, 0
+
+        self.start_entry.set_value("(%.*f, %.*f)" % (self.decimals, first_pos.x, self.decimals, first_pos.y))
+        self.stop_entry.set_value("(%.*f, %.*f)" % (self.decimals, last_pos.x, self.decimals, last_pos.y))
+
+        dx = first_pos.x - last_pos.x
+        dy = first_pos.y - last_pos.y
+
+        self.distance_x_entry.set_value('%.*f' % (self.decimals, abs(dx)))
+        self.distance_y_entry.set_value('%.*f' % (self.decimals, abs(dy)))
+
+        try:
+            angle = math.degrees(math.atan(dy / dx))
+            self.angle_entry.set_value('%.*f' % (self.decimals, angle))
+        except Exception as e:
+            pass
+
+        d = sqrt(dx ** 2 + dy ** 2)
+        self.total_distance_entry.set_value('%.*f' % (self.decimals, abs(d)))
+
+        self.h_point = (min(first_pos.x, last_pos.x) + (abs(dx) / 2), min(first_pos.y, last_pos.y) + (abs(dy) / 2))
+        if d != 0:
+            self.half_point_entry.set_value(
+                "(%.*f, %.*f)" % (self.decimals, self.h_point[0], self.decimals, self.h_point[1])
+            )
+        else:
+            self.half_point_entry.set_value(
+                "(%.*f, %.*f)" % (self.decimals, 0.0, self.decimals, 0.0)
+            )
+
+        if d != 0:
+            self.app.inform.emit(_("MEASURING: Result D(x) = {d_x} | D(y) = {d_y} | Distance = {d_z}").format(
+                d_x='%*f' % (self.decimals, abs(dx)),
+                d_y='%*f' % (self.decimals, abs(dy)),
+                d_z='%*f' % (self.decimals, abs(d)))
+            )
+        else:
+            self.app.inform.emit('[WARNING_NOTCL] %s: %s' %
+                                 (_("Objects intersects or touch at"),
+                                  "(%.*f, %.*f)" % (self.decimals, self.h_point[0], self.decimals, self.h_point[1])))
+
+    def on_jump_to_half_point(self):
+        self.app.on_jump_to(custom_location=self.h_point)
+        self.app.inform.emit('[success] %s: %s' %
+                             (_("Jumped to the half point between the two selected objects"),
+                              "(%.*f, %.*f)" % (self.decimals, self.h_point[0], self.decimals, self.h_point[1])))
+
+    def set_meas_units(self, units):
+        self.meas.units_label.setText("[" + self.app.options["units"].lower() + "]")
+
+# end of file

+ 463 - 65
flatcamTools/ToolFilm.py

@@ -1,16 +1,19 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 from FlatCAMTool import FlatCAMTool
+from FlatCAMObj import *
 
-from flatcamGUI.GUIElements import RadioSet, FCEntry
+from flatcamGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, \
+    OptionalHideInputSection, OptionalInputSection
 from PyQt5 import QtGui, QtCore, QtWidgets
 
+from copy import deepcopy
+
 import gettext
 import FlatCAMTranslation as fcTranslate
 import builtins
@@ -27,6 +30,8 @@ class Film(FlatCAMTool):
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
 
+        self.decimals = 4
+
         # Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label.setStyleSheet("""
@@ -39,8 +44,11 @@ class Film(FlatCAMTool):
         self.layout.addWidget(title_label)
 
         # Form Layout
-        tf_form_layout = QtWidgets.QFormLayout()
-        self.layout.addLayout(tf_form_layout)
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
 
         # Type of object for which to create the film
         self.tf_type_obj_combo = QtWidgets.QComboBox()
@@ -60,7 +68,8 @@ class Film(FlatCAMTool):
               "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)
+        grid0.addWidget(self.tf_type_obj_combo_label, 0, 0)
+        grid0.addWidget(self.tf_type_obj_combo, 0, 1)
 
         # List of objects for which we can create the film
         self.tf_object_combo = QtWidgets.QComboBox()
@@ -72,7 +81,8 @@ class Film(FlatCAMTool):
         self.tf_object_label.setToolTip(
             _("Object for which to create the film.")
         )
-        tf_form_layout.addRow(self.tf_object_label, self.tf_object_combo)
+        grid0.addWidget(self.tf_object_label, 1, 0)
+        grid0.addWidget(self.tf_object_combo, 1, 1)
 
         # Type of Box Object to be used as an envelope for film creation
         # Within this we can create negative
@@ -93,7 +103,8 @@ class Film(FlatCAMTool):
               "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)
+        grid0.addWidget(self.tf_type_box_combo_label, 2, 0)
+        grid0.addWidget(self.tf_type_box_combo, 2, 1)
 
         # Box
         self.tf_box_combo = QtWidgets.QComboBox()
@@ -108,11 +119,149 @@ class Film(FlatCAMTool):
               "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)
+        grid0.addWidget(self.tf_box_combo_label, 3, 0)
+        grid0.addWidget(self.tf_box_combo, 3, 1)
+
+        grid0.addWidget(QtWidgets.QLabel(''), 4, 0)
+
+        self.film_adj_label = QtWidgets.QLabel('<b>%s</b>' % _("Film Adjustments"))
+        self.film_adj_label.setToolTip(
+            _("Sometime the printers will distort the print shape, especially the Laser types.\n"
+              "This section provide the tools to compensate for the print distortions.")
+        )
+
+        grid0.addWidget(self.film_adj_label, 5, 0, 1, 2)
+
+        # Scale Geometry
+        self.film_scale_cb = FCCheckBox('%s' % _("Scale Film geometry"))
+        self.film_scale_cb.setToolTip(
+            _("A value greater than 1 will stretch the film\n"
+              "while a value less than 1 will jolt it.")
+        )
+        self.film_scale_cb.setStyleSheet(
+            """
+            QCheckBox {font-weight: bold; color: black}
+            """
+        )
+        grid0.addWidget(self.film_scale_cb, 6, 0, 1, 2)
+
+        self.film_scalex_label = QtWidgets.QLabel('%s:' % _("X factor"))
+        self.film_scalex_entry = FCDoubleSpinner()
+        self.film_scalex_entry.set_range(-999.9999, 999.9999)
+        self.film_scalex_entry.set_precision(self.decimals)
+        self.film_scalex_entry.setSingleStep(0.01)
+
+        grid0.addWidget(self.film_scalex_label, 7, 0)
+        grid0.addWidget(self.film_scalex_entry, 7, 1)
+
+        self.film_scaley_label = QtWidgets.QLabel('%s:' % _("Y factor"))
+        self.film_scaley_entry = FCDoubleSpinner()
+        self.film_scaley_entry.set_range(-999.9999, 999.9999)
+        self.film_scaley_entry.set_precision(self.decimals)
+        self.film_scaley_entry.setSingleStep(0.01)
+
+        grid0.addWidget(self.film_scaley_label, 8, 0)
+        grid0.addWidget(self.film_scaley_entry, 8, 1)
+
+        self.ois_scale = OptionalInputSection(self.film_scale_cb, [self.film_scalex_label, self.film_scalex_entry,
+                                                                   self.film_scaley_label,  self.film_scaley_entry])
+        # Skew Geometry
+        self.film_skew_cb =FCCheckBox('%s' % _("Skew Film geometry"))
+        self.film_skew_cb.setToolTip(
+            _("Positive values will skew to the right\n"
+              "while negative values will skew to the left.")
+        )
+        self.film_skew_cb.setStyleSheet(
+            """
+            QCheckBox {font-weight: bold; color: black}
+            """
+        )
+        grid0.addWidget(self.film_skew_cb, 9, 0, 1, 2)
+
+        self.film_skewx_label = QtWidgets.QLabel('%s:' % _("X angle"))
+        self.film_skewx_entry = FCDoubleSpinner()
+        self.film_skewx_entry.set_range(-999.9999, 999.9999)
+        self.film_skewx_entry.set_precision(self.decimals)
+        self.film_skewx_entry.setSingleStep(0.01)
+
+        grid0.addWidget(self.film_skewx_label, 10, 0)
+        grid0.addWidget(self.film_skewx_entry, 10, 1)
+
+        self.film_skewy_label = QtWidgets.QLabel('%s:' % _("Y angle"))
+        self.film_skewy_entry = FCDoubleSpinner()
+        self.film_skewy_entry.set_range(-999.9999, 999.9999)
+        self.film_skewy_entry.set_precision(self.decimals)
+        self.film_skewy_entry.setSingleStep(0.01)
+
+        grid0.addWidget(self.film_skewy_label, 11, 0)
+        grid0.addWidget(self.film_skewy_entry, 11, 1)
+
+        self.film_skew_ref_label = QtWidgets.QLabel('%s:' % _("Reference"))
+        self.film_skew_ref_label.setToolTip(
+            _("The reference point to be used as origin for the skew.\n"
+              "It can be one of the four points of the geometry bounding box.")
+        )
+        self.film_skew_reference = RadioSet([{'label': _('Bottom Left'), 'value': 'bottomleft'},
+                                          {'label': _('Top Left'), 'value': 'topleft'},
+                                          {'label': _('Bottom Right'), 'value': 'bottomright'},
+                                          {'label': _('Top right'), 'value': 'topright'}],
+                                            orientation='vertical',
+                                            stretch=False)
+
+        grid0.addWidget(self.film_skew_ref_label, 12, 0)
+        grid0.addWidget(self.film_skew_reference, 12, 1)
+
+        self.ois_skew = OptionalInputSection(self.film_skew_cb, [self.film_skewx_label, self.film_skewx_entry,
+                                                                 self.film_skewy_label,  self.film_skewy_entry,
+                                                                 self.film_skew_reference])
+        # Mirror Geometry
+        self.film_mirror_cb = FCCheckBox('%s' % _("Mirror Film geometry"))
+        self.film_mirror_cb.setToolTip(
+            _("Mirror the film geometry on the selected axis or on both.")
+        )
+        self.film_mirror_cb.setStyleSheet(
+            """
+            QCheckBox {font-weight: bold; color: black}
+            """
+        )
+        grid0.addWidget(self.film_mirror_cb, 13, 0, 1, 2)
+
+        self.film_mirror_axis = RadioSet([{'label': _('None'), 'value': 'none'},
+                                          {'label': _('X'), 'value': 'x'},
+                                          {'label': _('Y'), 'value': 'y'},
+                                          {'label': _('Both'), 'value': 'both'}],
+                                         stretch=False)
+        self.film_mirror_axis_label = QtWidgets.QLabel('%s:' % _("Mirror axis"))
+
+        grid0.addWidget(self.film_mirror_axis_label, 14, 0)
+        grid0.addWidget(self.film_mirror_axis, 14, 1)
+
+        self.ois_mirror = OptionalInputSection(self.film_mirror_cb,
+                                               [self.film_mirror_axis_label, self.film_mirror_axis])
+
+        grid0.addWidget(QtWidgets.QLabel(''), 15, 0)
+
+        # Scale Stroke size
+        self.film_scale_stroke_entry = FCDoubleSpinner()
+        self.film_scale_stroke_entry.set_range(-999.9999, 999.9999)
+        self.film_scale_stroke_entry.setSingleStep(0.01)
+        self.film_scale_stroke_entry.set_precision(self.decimals)
+
+        self.film_scale_stroke_label = QtWidgets.QLabel('%s:' % _("Scale Stroke"))
+        self.film_scale_stroke_label.setToolTip(
+            _("Scale the line stroke thickness of each feature in the SVG file.\n"
+              "It means that the line that envelope each SVG feature will be thicker or thinner,\n"
+              "therefore the fine features may be more affected by this parameter.")
+        )
+        grid0.addWidget(self.film_scale_stroke_label, 16, 0)
+        grid0.addWidget(self.film_scale_stroke_entry, 16, 1)
+
+        grid0.addWidget(QtWidgets.QLabel(''), 17, 0)
 
         # Film Type
         self.film_type = RadioSet([{'label': _('Positive'), 'value': 'pos'},
-                                   {'label': _('Negative'), 'value': 'neg'}])
+                                   {'label': _('Negative'), 'value': 'neg'}],
+                                  stretch=False)
         self.film_type_label = QtWidgets.QLabel(_("Film Type:"))
         self.film_type_label.setToolTip(
             _("Generate a Positive black film or a Negative film.\n"
@@ -122,11 +271,15 @@ class Film(FlatCAMTool):
               "with white on a black canvas.\n"
               "The Film format is SVG.")
         )
-        tf_form_layout.addRow(self.film_type_label, self.film_type)
+        grid0.addWidget(self.film_type_label, 18, 0)
+        grid0.addWidget(self.film_type, 18, 1)
 
         # Boundary for negative film generation
+        self.boundary_entry = FCDoubleSpinner()
+        self.boundary_entry.set_range(-999.9999, 999.9999)
+        self.boundary_entry.setSingleStep(0.01)
+        self.boundary_entry.set_precision(self.decimals)
 
-        self.boundary_entry = FCEntry()
         self.boundary_label = QtWidgets.QLabel('%s:' % _("Border"))
         self.boundary_label.setToolTip(
             _("Specify a border around the object.\n"
@@ -138,21 +291,74 @@ class Film(FlatCAMTool):
               "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)
-
-        self.film_scale_entry = FCEntry()
-        self.film_scale_label = QtWidgets.QLabel('%s:' % _("Scale Stroke"))
-        self.film_scale_label.setToolTip(
-            _("Scale the line stroke thickness of each feature in the SVG file.\n"
-              "It means that the line that envelope each SVG feature will be thicker or thinner,\n"
-              "therefore the fine features may be more affected by this parameter.")
+        grid0.addWidget(self.boundary_label, 19, 0)
+        grid0.addWidget(self.boundary_entry, 19, 1)
+
+        self.boundary_label.hide()
+        self.boundary_entry.hide()
+
+        # Punch Drill holes
+        self.punch_cb = FCCheckBox(_("Punch drill holes"))
+        self.punch_cb.setToolTip(_("When checked the generated film will have holes in pads when\n"
+                                   "the generated film is positive. This is done to help drilling,\n"
+                                   "when done manually."))
+        grid0.addWidget(self.punch_cb, 20, 0, 1, 2)
+
+        # this way I can hide/show the frame
+        self.punch_frame = QtWidgets.QFrame()
+        self.punch_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.punch_frame)
+        punch_grid = QtWidgets.QGridLayout()
+        punch_grid.setContentsMargins(0, 0, 0, 0)
+        self.punch_frame.setLayout(punch_grid)
+
+        punch_grid.setColumnStretch(0, 0)
+        punch_grid.setColumnStretch(1, 1)
+
+        self.ois_p = OptionalHideInputSection(self.punch_cb, [self.punch_frame])
+
+        self.source_label = QtWidgets.QLabel('%s:' % _("Source"))
+        self.source_label.setToolTip(
+            _("The punch hole source can be:\n"
+              "- Excellon -> an Excellon holes center will serve as reference.\n"
+              "- Pad Center -> will try to use the pads center as reference.")
+        )
+        self.source_punch = RadioSet([{'label': _('Excellon'), 'value': 'exc'},
+                                      {'label': _('Pad center'), 'value': 'pad'}],
+                                     stretch=False)
+        punch_grid.addWidget(self.source_label, 0, 0)
+        punch_grid.addWidget(self.source_punch, 0, 1)
+
+        self.exc_label = QtWidgets.QLabel('%s:' % _("Excellon Obj"))
+        self.exc_label.setToolTip(
+            _("Remove the geometry of Excellon from the Film to create tge holes in pads.")
         )
-        tf_form_layout.addRow(self.film_scale_label, self.film_scale_entry)
+        self.exc_combo = QtWidgets.QComboBox()
+        self.exc_combo.setModel(self.app.collection)
+        self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
+        self.exc_combo.setCurrentIndex(1)
+        punch_grid.addWidget(self.exc_label, 1, 0)
+        punch_grid.addWidget(self.exc_combo, 1, 1)
+
+        self.exc_label.hide()
+        self.exc_combo.hide()
+
+        self.punch_size_label = QtWidgets.QLabel('%s:' % _("Punch Size"))
+        self.punch_size_label.setToolTip(_("The value here will control how big is the punch hole in the pads."))
+        self.punch_size_spinner = FCDoubleSpinner()
+        self.punch_size_spinner.set_range(0, 999.9999)
+        self.punch_size_spinner.setSingleStep(0.1)
+        self.punch_size_spinner.set_precision(self.decimals)
+
+        punch_grid.addWidget(self.punch_size_label, 2, 0)
+        punch_grid.addWidget(self.punch_size_spinner, 2, 1)
+
+        self.punch_size_label.hide()
+        self.punch_size_spinner.hide()
 
         # Buttons
         hlay = QtWidgets.QHBoxLayout()
         self.layout.addLayout(hlay)
-        hlay.addStretch()
 
         self.film_object_button = QtWidgets.QPushButton(_("Save Film"))
         self.film_object_button.setToolTip(
@@ -170,6 +376,9 @@ class Film(FlatCAMTool):
         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)
 
+        self.film_type.activated_custom.connect(self.on_film_type)
+        self.source_punch.activated_custom.connect(self.on_punch_source)
+
     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()))
@@ -216,14 +425,61 @@ class Film(FlatCAMTool):
 
         f_type = self.app.defaults["tools_film_type"] if self.app.defaults["tools_film_type"] else 'neg'
         self.film_type.set_value(str(f_type))
+        self.on_film_type(val=f_type)
 
         b_entry = self.app.defaults["tools_film_boundary"] if self.app.defaults["tools_film_boundary"] else 0.0
         self.boundary_entry.set_value(float(b_entry))
 
-        scale_stroke_width = self.app.defaults["tools_film_scale"] if self.app.defaults["tools_film_scale"] else 0.0
-        self.film_scale_entry.set_value(int(scale_stroke_width))
+        scale_stroke_width = self.app.defaults["tools_film_scale_stroke"] if \
+            self.app.defaults["tools_film_scale_stroke"] else 0.0
+        self.film_scale_stroke_entry.set_value(int(scale_stroke_width))
+
+        self.punch_cb.set_value(False)
+        self.source_punch.set_value('exc')
+
+        self.film_scale_cb.set_value(self.app.defaults["tools_film_scale_cb"])
+        self.film_scalex_entry.set_value(float(self.app.defaults["tools_film_scale_x_entry"]))
+        self.film_scaley_entry.set_value(float(self.app.defaults["tools_film_scale_y_entry"]))
+        self.film_skew_cb.set_value(self.app.defaults["tools_film_skew_cb"])
+        self.film_skewx_entry.set_value(float(self.app.defaults["tools_film_skew_x_entry"]))
+        self.film_skewy_entry.set_value(float(self.app.defaults["tools_film_skew_y_entry"]))
+        self.film_skew_reference.set_value(self.app.defaults["tools_film_skew_ref_radio"])
+        self.film_mirror_cb.set_value(self.app.defaults["tools_film_mirror_cb"])
+        self.film_mirror_axis.set_value(self.app.defaults["tools_film_mirror_axis_radio"])
+
+    def on_film_type(self, val):
+        type_of_film = val
+
+        if type_of_film == 'neg':
+            self.boundary_label.show()
+            self.boundary_entry.show()
+            self.punch_cb.set_value(False)  # required so the self.punch_frame it's hidden also by the signal emitted
+            self.punch_cb.hide()
+        else:
+            self.boundary_label.hide()
+            self.boundary_entry.hide()
+            self.punch_cb.show()
+
+    def on_punch_source(self, val):
+        if val == 'pad' and self.punch_cb.get_value():
+            self.punch_size_label.show()
+            self.punch_size_spinner.show()
+            self.exc_label.hide()
+            self.exc_combo.hide()
+        else:
+            self.punch_size_label.hide()
+            self.punch_size_spinner.hide()
+            self.exc_label.show()
+            self.exc_combo.show()
+
+        if val == 'pad' and self.tf_type_obj_combo.currentText() == 'Geometry':
+            self.source_punch.set_value('exc')
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Using the Pad center does not work on Geometry objects. "
+                                                          "Only a Gerber object has pads."))
 
     def on_film_creation(self):
+        log.debug("ToolFilm.Film.on_film_creation() started ...")
+
         try:
             name = self.tf_object_combo.currentText()
         except Exception as e:
@@ -238,59 +494,201 @@ class Film(FlatCAMTool):
                                  _("No FlatCAM object selected. Load an object for Box and retry."))
             return
 
-        try:
-            border = float(self.boundary_entry.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                border = float(self.boundary_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number."))
-                return
+        scale_stroke_width = float(self.film_scale_stroke_entry.get_value())
 
-        try:
-            scale_stroke_width = int(self.film_scale_entry.get_value())
-        except ValueError:
-            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number."))
-            return
+        source = self.source_punch.get_value()
 
-        if border is None:
-            border = 0
+        # #################################################################
+        # ################ STARTING THE JOB ###############################
+        # #################################################################
 
         self.app.inform.emit(_("Generating Film ..."))
 
         if self.film_type.get_value() == "pos":
-            try:
-                filename, _f = QtWidgets.QFileDialog.getSaveFileName(
-                    caption=_("Export SVG positive"),
-                    directory=self.app.get_last_save_folder() + '/' + name,
-                    filter="*.svg")
-            except TypeError:
-                filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG positive"))
-
-            filename = str(filename)
 
-            if str(filename) == "":
-                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export SVG positive cancelled."))
-                return
+            if self.punch_cb.get_value() is False:
+                self.generate_positive_normal_film(name, boxname, factor=scale_stroke_width)
             else:
-                self.app.export_svg_positive(name, boxname, filename, scale_factor=scale_stroke_width)
+                self.generate_positive_punched_film(name, boxname, source, factor=scale_stroke_width)
         else:
-            try:
-                filename, _f = QtWidgets.QFileDialog.getSaveFileName(
-                    caption=_("Export SVG negative"),
-                    directory=self.app.get_last_save_folder() + '/' + name,
-                    filter="*.svg")
-            except TypeError:
-                filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG negative"))
+            self.generate_negative_film(name, boxname, factor=scale_stroke_width)
+
+    def generate_positive_normal_film(self, name, boxname, factor):
+        log.debug("ToolFilm.Film.generate_positive_normal_film() started ...")
+
+        scale_factor_x = None
+        scale_factor_y = None
+        skew_factor_x = None
+        skew_factor_y = None
+        mirror = None
+        skew_reference = 'center'
+
+        if self.film_scale_cb.get_value():
+            if self.film_scalex_entry.get_value() != 1.0:
+                scale_factor_x = self.film_scalex_entry.get_value()
+            if self.film_scaley_entry.get_value() != 1.0:
+                scale_factor_y = self.film_scaley_entry.get_value()
+        if self.film_skew_cb.get_value():
+            if self.film_skewx_entry.get_value() != 0.0:
+                skew_factor_x = self.film_skewx_entry.get_value()
+            if self.film_skewy_entry.get_value() != 0.0:
+                skew_factor_y = self.film_skewy_entry.get_value()
+
+            skew_reference = self.film_skew_reference.get_value()
+        if self.film_mirror_cb.get_value():
+            if self.film_mirror_axis.get_value() != 'none':
+                mirror = self.film_mirror_axis.get_value()
+        try:
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(
+                caption=_("Export SVG positive"),
+                directory=self.app.get_last_save_folder() + '/' + name,
+                filter="*.svg")
+        except TypeError:
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG positive"))
+
+        filename = str(filename)
+
+        if str(filename) == "":
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export SVG positive cancelled."))
+            return
+        else:
+            self.app.export_svg_positive(name, boxname, filename,
+                                         scale_stroke_factor=factor,
+                                         scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
+                                         skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
+                                         skew_reference=skew_reference,
+                                         mirror=mirror
+                                         )
+
+    def generate_positive_punched_film(self, name, boxname, source, factor):
+
+        film_obj = self.app.collection.get_by_name(name)
 
-            filename = str(filename)
+        if source == 'exc':
+            log.debug("ToolFilm.Film.generate_positive_punched_film() with Excellon source started ...")
 
-            if str(filename) == "":
-                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export SVG negative cancelled."))
+            try:
+                exc_name = self.exc_combo.currentText()
+            except Exception as e:
+                self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                     _("No Excellon object selected. Load an object for punching reference and retry."))
                 return
+
+            exc_obj = self.app.collection.get_by_name(exc_name)
+            exc_solid_geometry = MultiPolygon(exc_obj.solid_geometry)
+            punched_solid_geometry = MultiPolygon(film_obj.solid_geometry).difference(exc_solid_geometry)
+
+            def init_func(new_obj, app_obj):
+                new_obj.solid_geometry = deepcopy(punched_solid_geometry)
+
+            outname = name + "_punched"
+            self.app.new_object('gerber', outname, init_func)
+
+            self.generate_positive_normal_film(outname, boxname, factor=factor)
+        else:
+            log.debug("ToolFilm.Film.generate_positive_punched_film() with Pad center source started ...")
+
+            punch_size = float(self.punch_size_spinner.get_value())
+
+            punching_geo = list()
+            for apid in film_obj.apertures:
+                if film_obj.apertures[apid]['type'] == 'C':
+                    if punch_size >= float(film_obj.apertures[apid]['size']):
+                        self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                             _(" Could not generate punched hole film because the punch hole size"
+                                               "is bigger than some of the apertures in the Gerber object."))
+                        return 'fail'
+                    else:
+                        for elem in film_obj.apertures[apid]['geometry']:
+                            if 'follow' in elem:
+                                if isinstance(elem['follow'], Point):
+                                    punching_geo.append(elem['follow'].buffer(punch_size / 2))
+                else:
+                    if punch_size >= float(film_obj.apertures[apid]['width']) or \
+                            punch_size >= float(film_obj.apertures[apid]['height']):
+                        self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                             _("Could not generate punched hole film because the punch hole size"
+                                               "is bigger than some of the apertures in the Gerber object."))
+                        return 'fail'
+                    else:
+                        for elem in film_obj.apertures[apid]['geometry']:
+                            if 'follow' in elem:
+                                if isinstance(elem['follow'], Point):
+                                    punching_geo.append(elem['follow'].buffer(punch_size / 2))
+
+            punching_geo = MultiPolygon(punching_geo)
+            if not isinstance(film_obj.solid_geometry, Polygon):
+                temp_solid_geometry = MultiPolygon(film_obj.solid_geometry)
             else:
-                self.app.export_svg_negative(name, boxname, filename, border, scale_factor=scale_stroke_width)
+                temp_solid_geometry = film_obj.solid_geometry
+            punched_solid_geometry = temp_solid_geometry.difference(punching_geo)
+
+            if punched_solid_geometry == temp_solid_geometry:
+                self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                     _("Could not generate punched hole film because the newly created object geometry "
+                                       "is the same as the one in the source object geometry..."))
+                return 'fail'
+
+            def init_func(new_obj, app_obj):
+                new_obj.solid_geometry = deepcopy(punched_solid_geometry)
+
+            outname = name + "_punched"
+            self.app.new_object('gerber', outname, init_func)
+
+            self.generate_positive_normal_film(outname, boxname, factor=factor)
+
+    def generate_negative_film(self, name, boxname, factor):
+        log.debug("ToolFilm.Film.generate_negative_film() started ...")
+
+        scale_factor_x = None
+        scale_factor_y = None
+        skew_factor_x = None
+        skew_factor_y = None
+        mirror = None
+        skew_reference = 'center'
+
+        if self.film_scale_cb.get_value():
+            if self.film_scalex_entry.get_value() != 1.0:
+                scale_factor_x = self.film_scalex_entry.get_value()
+            if self.film_scaley_entry.get_value() != 1.0:
+                scale_factor_y = self.film_scaley_entry.get_value()
+        if self.film_skew_cb.get_value():
+            if self.film_skewx_entry.get_value() != 0.0:
+                skew_factor_x = self.film_skewx_entry.get_value()
+            if self.film_skewy_entry.get_value() != 0.0:
+                skew_factor_y = self.film_skewy_entry.get_value()
+
+            skew_reference = self.film_skew_reference.get_value()
+        if self.film_mirror_cb.get_value():
+            if self.film_mirror_axis.get_value() != 'none':
+                mirror = self.film_mirror_axis.get_value()
+
+        border = float(self.boundary_entry.get_value())
+
+        if border is None:
+            border = 0
+
+        try:
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(
+                caption=_("Export SVG negative"),
+                directory=self.app.get_last_save_folder() + '/' + name,
+                filter="*.svg")
+        except TypeError:
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG negative"))
+
+        filename = str(filename)
+
+        if str(filename) == "":
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export SVG negative cancelled."))
+            return
+        else:
+            self.app.export_svg_negative(name, boxname, filename, border,
+                                         scale_stroke_factor=factor,
+                                         scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
+                                         skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
+                                         skew_reference=skew_reference,
+                                         mirror=mirror
+                                         )
 
     def reset_fields(self):
         self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))

+ 18 - 17
flatcamTools/ToolImage.py

@@ -1,14 +1,13 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 from FlatCAMTool import FlatCAMTool
 
-from flatcamGUI.GUIElements import RadioSet, FCComboBox, IntEntry
+from flatcamGUI.GUIElements import RadioSet, FCComboBox, FCSpinner
 from PyQt5 import QtGui, QtWidgets
 
 import gettext
@@ -59,11 +58,9 @@ class ToolImage(FlatCAMTool):
         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_entry = FCSpinner()
         self.dpi_label = QtWidgets.QLabel('%s:' % _("DPI value"))
-        self.dpi_label.setToolTip(
-           _("Specify a DPI value for the image.")
-        )
+        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("")
@@ -86,7 +83,9 @@ class ToolImage(FlatCAMTool):
         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_entry = FCSpinner()
+        self.mask_bw_entry.set_range(0, 255)
+
         self.mask_bw_label = QtWidgets.QLabel("%s <b>B/W</b>:" % _('Mask value'))
         self.mask_bw_label.setToolTip(
             _("Mask for monochrome image.\n"
@@ -99,7 +98,9 @@ class ToolImage(FlatCAMTool):
         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_entry = FCSpinner()
+        self.mask_r_entry.set_range(0, 255)
+
         self.mask_r_label = QtWidgets.QLabel("%s <b>R:</b>" % _('Mask value'))
         self.mask_r_label.setToolTip(
             _("Mask for RED color.\n"
@@ -110,7 +111,9 @@ class ToolImage(FlatCAMTool):
         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_entry = FCSpinner()
+        self.mask_g_entry.set_range(0, 255)
+
         self.mask_g_label = QtWidgets.QLabel("%s <b>G:</b>" % _('Mask value'))
         self.mask_g_label.setToolTip(
             _("Mask for GREEN color.\n"
@@ -121,7 +124,9 @@ class ToolImage(FlatCAMTool):
         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_entry = FCSpinner()
+        self.mask_b_entry.set_range(0, 255)
+
         self.mask_b_label = QtWidgets.QLabel("%s <b>B:</b>" % _('Mask value'))
         self.mask_b_label.setToolTip(
             _("Mask for BLUE color.\n"
@@ -132,15 +137,11 @@ class ToolImage(FlatCAMTool):
         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.addWidget(self.import_button)
 
         self.layout.addStretch()
 

+ 39 - 33
flatcamTools/ToolMove.py

@@ -1,6 +1,5 @@
 # ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
@@ -50,6 +49,10 @@ class ToolMove(FlatCAMTool):
             from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
             self.sel_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name="move")
 
+        self.mm = None
+        self.mp = None
+        self.kr = None
+
         self.replot_signal[list].connect(self.replot)
 
     def install(self, icon=None, separator=None, **kwargs):
@@ -90,14 +93,19 @@ class ToolMove(FlatCAMTool):
             # signal that there is a command active and it is 'Move'
             self.app.command_active = "Move"
 
-            if self.app.collection.get_selected():
+            sel_obj_list = self.app.collection.get_selected()
+            if sel_obj_list:
                 self.app.inform.emit(_("MOVE: Click on the Start point ..."))
+
+                # if we have an object selected then we can safely activate the mouse events
+                self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_move)
+                self.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.on_left_click)
+                self.kr = self.app.plotcanvas.graph_event_connect('key_release', self.on_key_press)
+
                 # 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.toggle()
                 self.app.inform.emit('[WARNING_NOTCL] %s' % _("MOVE action cancelled. No object(s) to move."))
 
     def on_left_click(self, event):
@@ -143,7 +151,9 @@ class ToolMove(FlatCAMTool):
                     dx = pos[0] - self.point1[0]
                     dy = pos[1] - self.point1[1]
 
-                    obj_list = self.app.collection.get_selected()
+                    # move only the objects selected and plotted and visible
+                    obj_list = [obj for obj in self.app.collection.get_selected()
+                                if obj.options['plot'] and obj.visible is True]
 
                     def job_move(app_obj):
                         with self.app.proc_container.new(_("Moving...")) as proc:
@@ -152,20 +162,21 @@ class ToolMove(FlatCAMTool):
                                     self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object(s) selected."))
                                     return "fail"
 
+                                # remove any mark aperture shape that may be displayed
                                 for sel_obj in obj_list:
                                     # if the Gerber mark shapes are enabled they need to be disabled before move
                                     if isinstance(sel_obj, FlatCAMGerber):
                                         sel_obj.ui.aperture_table_visibility_cb.setChecked(False)
 
-                                    # offset solid_geometry
-                                    sel_obj.offset((dx, dy))
-                                    # sel_obj.plot()
-
                                     try:
                                         sel_obj.replotApertures.emit()
                                     except Exception as e:
                                         pass
 
+                                for sel_obj in obj_list:
+                                    # offset solid_geometry
+                                    sel_obj.offset((dx, dy))
+
                                     # Update the object bounding box options
                                     a, b, c, d = sel_obj.bounds()
                                     sel_obj.options['xmin'] = a
@@ -254,38 +265,33 @@ class ToolMove(FlatCAMTool):
         ymaxlist = []
 
         obj_list = self.app.collection.get_selected()
-        if not obj_list:
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Object(s) not selected"))
-            self.toggle()
-        else:
-            # if we have an object selected then we can safely activate the mouse events
-            self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_move)
-            self.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.on_left_click)
-            self.kr = self.app.plotcanvas.graph_event_connect('key_release', self.on_key_press)
-            # first get a bounding box to fit all
-            for obj in obj_list:
+
+        # first get a bounding box to fit all
+        for obj in obj_list:
+            # don't move disabled objects, move only plotted objects
+            if obj.options['plot']:
                 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)
+        # 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)
+        p1 = (xminimal, yminimal)
+        p2 = (xmaximal, yminimal)
+        p3 = (xmaximal, ymaximal)
+        p4 = (xminimal, ymaximal)
 
-            self.old_coords = [p1, p2, p3, p4]
-            self.draw_shape(Polygon(self.old_coords))
+        self.old_coords = [p1, p2, p3, p4]
+        self.draw_shape(Polygon(self.old_coords))
 
-            if self.app.is_legacy is True:
-                self.sel_shapes.redraw()
+        if self.app.is_legacy is True:
+            self.sel_shapes.redraw()
 
     def update_sel_bbox(self, pos):
         self.delete_shape()

+ 165 - 108
flatcamTools/ToolNonCopperClear.py

@@ -1,10 +1,9 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Modified by: Marius Adrian Stanciu (c)              #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 from FlatCAMTool import FlatCAMTool
 from copy import copy, deepcopy
@@ -27,6 +26,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
     def __init__(self, app):
         self.app = app
+        self.decimals = 4
 
         FlatCAMTool.__init__(self, app)
         Gerber.__init__(self, steps_per_circle=self.app.defaults["gerber_circle_steps"])
@@ -213,14 +213,19 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.addtool_entry_lbl.setToolTip(
             _("Diameter for the new tool to add in the Tool Table")
         )
-        self.addtool_entry = FCEntry2()
+        self.addtool_entry = FCDoubleSpinner()
+        self.addtool_entry.set_precision(self.decimals)
+
         form.addRow(self.addtool_entry_lbl, self.addtool_entry)
 
         # Tip Dia
         self.tipdialabel = QtWidgets.QLabel('%s:' % _('V-Tip Dia'))
         self.tipdialabel.setToolTip(
             _("The tip diameter for V-Shape Tool"))
-        self.tipdia_entry = LengthEntry()
+        self.tipdia_entry = FCDoubleSpinner()
+        self.tipdia_entry.set_precision(self.decimals)
+        self.tipdia_entry.setSingleStep(0.1)
+
         form.addRow(self.tipdialabel, self.tipdia_entry)
 
         # Tip Angle
@@ -228,7 +233,10 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.tipanglelabel.setToolTip(
             _("The tip angle for V-Shape Tool.\n"
               "In degree."))
-        self.tipangle_entry = LengthEntry()
+        self.tipangle_entry = FCDoubleSpinner()
+        self.tipangle_entry.set_precision(self.decimals)
+        self.tipangle_entry.setSingleStep(5)
+
         form.addRow(self.tipanglelabel, self.tipangle_entry)
 
         grid2 = QtWidgets.QGridLayout()
@@ -271,7 +279,10 @@ class NonCopperClear(FlatCAMTool, Gerber):
            _("Depth of cut into material. Negative value.\n"
              "In FlatCAM units.")
         )
-        self.cutz_entry = FloatEntry()
+        self.cutz_entry = FCDoubleSpinner()
+        self.cutz_entry.set_precision(self.decimals)
+        self.cutz_entry.set_range(-99999, -0.00000000000001)
+
         self.cutz_entry.setToolTip(
            _("Depth of cut into material. Negative value.\n"
              "In FlatCAM units.")
@@ -305,7 +316,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
             _("Bounding box margin.")
         )
         grid3.addWidget(nccmarginlabel, 3, 0)
-        self.ncc_margin_entry = FCEntry()
+        self.ncc_margin_entry = FCDoubleSpinner()
+        self.ncc_margin_entry.set_precision(self.decimals)
+
         grid3.addWidget(self.ncc_margin_entry, 3, 1)
 
         # Method
@@ -448,30 +461,39 @@ class NonCopperClear(FlatCAMTool, Gerber):
         )
         self.tools_box.addWidget(self.generate_ncc_button)
         self.tools_box.addStretch()
+        # ############################ FINSIHED GUI ###################################
+        # #############################################################################
 
+        # #############################################################################
+        # ###################### Setup CONTEXT MENU ###################################
+        # #############################################################################
         self.tools_table.setupContextMenu()
         self.tools_table.addContextMenu(
-            "Add", lambda: self.on_tool_add(dia=None, muted=None), icon=QtGui.QIcon("share/plus16.png"))
+            "Add", self.on_add_tool_by_key, 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"))
 
+        # #############################################################################
+        # ########################## VARIABLES ########################################
+        # #############################################################################
         self.units = ''
-        self.ncc_tools = {}
+        self.ncc_tools = dict()
         self.tooluid = 0
+
         # store here the default data for Geometry Data
-        self.default_data = {}
+        self.default_data = dict()
 
         self.obj_name = ""
         self.ncc_obj = None
 
-        self.sel_rect = []
+        self.sel_rect = list()
 
         self.bound_obj_name = ""
         self.bound_obj = None
 
-        self.ncc_dia_list = []
-        self.iso_dia_list = []
+        self.ncc_dia_list = list()
+        self.iso_dia_list = list()
         self.has_offset = None
         self.o_name = None
         self.overlap = None
@@ -485,11 +507,18 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
         self.mm = None
         self.mr = None
+
         # store here solid_geometry when there are tool with isolation job
-        self.solid_geometry = []
+        self.solid_geometry = list()
 
-        self.tool_type_item_options = []
+        self.select_method = None
+        self.tool_type_item_options = list()
 
+        self.grb_circle_steps = int(self.app.defaults["gerber_circle_steps"])
+
+        # #############################################################################
+        # ############################ SGINALS ########################################
+        # #############################################################################
         self.addtool_btn.clicked.connect(self.on_tool_add)
         self.addtool_entry.returnPressed.connect(self.on_tool_add)
         self.deltool_btn.clicked.connect(self.on_tool_delete)
@@ -508,6 +537,22 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
         self.object_combo.setCurrentIndex(0)
 
+    def on_add_tool_by_key(self):
+        tool_add_popup = FCInputDialog(title='%s...' % _("New Tool"),
+                                       text='%s:' % _('Enter a Tool Diameter'),
+                                       min=0.0000, max=99.9999, decimals=4)
+        tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png'))
+
+        val, ok = tool_add_popup.get_value()
+        if ok:
+            if float(val) == 0:
+                self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                     _("Please enter a tool diameter with non-zero value, in Float format."))
+                return
+            self.on_tool_add(dia=float(val))
+        else:
+            self.app.inform.emit('[WARNING_NOTCL] %s...' % _("Adding Tool cancelled"))
+
     def install(self, icon=None, separator=None, **kwargs):
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+N', **kwargs)
 
@@ -546,6 +591,13 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.app.ui.notebook.setTabText(2, _("NCC Tool"))
 
     def set_tool_ui(self):
+        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+
+        if self.units == "IN":
+            self.decimals = 4
+        else:
+            self.decimals = 2
+
         self.tools_frame.show()
 
         self.ncc_order_radio.set_value(self.app.defaults["tools_nccorder"])
@@ -619,7 +671,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
             self.tooluid += 1
             self.ncc_tools.update({
                 int(self.tooluid): {
-                    'tooldia': float('%.4f' % tool_dia),
+                    'tooldia': float('%.*f' % (self.decimals, tool_dia)),
                     'offset': 'Path',
                     'offset_value': 0.0,
                     'type': 'Iso',
@@ -651,7 +703,10 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
         sorted_tools = []
         for k, v in self.ncc_tools.items():
-            sorted_tools.append(float('%.4f' % float(v['tooldia'])))
+            if self.units == "IN":
+                sorted_tools.append(float('%.*f' % (self.decimals, float(v['tooldia']))))
+            else:
+                sorted_tools.append(float('%.*f' % (self.decimals, float(v['tooldia']))))
 
         order = self.ncc_order_radio.get_value()
         if order == 'fwd':
@@ -667,7 +722,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
         for tool_sorted in sorted_tools:
             for tooluid_key, tooluid_value in self.ncc_tools.items():
-                if float('%.4f' % tooluid_value['tooldia']) == tool_sorted:
+                if float('%.*f' % (self.decimals, tooluid_value['tooldia'])) == tool_sorted:
                     tool_id += 1
                     id_ = QtWidgets.QTableWidgetItem('%d' % int(tool_id))
                     id_.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
@@ -675,12 +730,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
                     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('%.4f' % tooluid_value['tooldia'])
+                    # There are no drill bits in MM with more than 2 decimals diameter
+                    # For INCH the decimals should be no more than 4. There are no drills under 10mils
+                    dia = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, tooluid_value['tooldia']))
 
                     dia.setFlags(QtCore.Qt.ItemIsEnabled)
 
@@ -867,36 +919,10 @@ class NonCopperClear(FlatCAMTool, Gerber):
         else:
             if self.tool_type_radio.get_value() == 'V':
 
-                try:
-                    tip_dia = float(self.tipdia_entry.get_value())
-                except ValueError:
-                    # try to convert comma to decimal point. if it's still not working error message and return
-                    try:
-                        tip_dia = float(self.tipdia_entry.get_value().replace(',', '.'))
-                    except ValueError:
-                        self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, "
-                                                                    "use a number."))
-                        return
-
-                try:
-                    tip_angle = float(self.tipangle_entry.get_value()) / 2
-                except ValueError:
-                    # try to convert comma to decimal point. if it's still not working error message and return
-                    try:
-                        tip_angle = float(self.tipangle_entry.get_value().replace(',', '.')) / 2
-                    except ValueError:
-                        self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number."))
-                        return
+                tip_dia = float(self.tipdia_entry.get_value())
+                tip_angle = float(self.tipangle_entry.get_value()) / 2
+                cut_z = float(self.cutz_entry.get_value())
 
-                try:
-                    cut_z = float(self.cutz_entry.get_value())
-                except ValueError:
-                    # try to convert comma to decimal point. if it's still not working error message and return
-                    try:
-                        cut_z = float(self.cutz_entry.get_value().replace(',', '.'))
-                    except ValueError:
-                        self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number."))
-                        return
                 # calculated tool diameter so the cut_z parameter is obeyed
                 tool_dia = tip_dia + 2 * cut_z * math.tan(math.radians(tip_angle))
 
@@ -921,10 +947,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
                 self.app.inform.emit('[WARNING_NOTCL] %s' % _("Please enter a tool diameter to add, in Float format."))
                 return
 
-        if self.units == 'MM':
-            tool_dia = float('%.2f' % tool_dia)
-        else:
-            tool_dia = float('%.4f' % tool_dia)
+        tool_dia = float('%.*f' % (self.decimals, tool_dia))
 
         if tool_dia == 0:
             self.app.inform.emit('[WARNING_NOTCL] %s' % _("Please enter a tool diameter with non-zero value, "
@@ -948,9 +971,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
         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]))
+                    tool_dias.append(float('%.*f' % (self.decimals, (v[tool_v]))))
 
-        if float('%.4f' % tool_dia) in tool_dias:
+        if float('%.*f' % (self.decimals, tool_dia)) in tool_dias:
             if muted is None:
                 self.app.inform.emit('[WARNING_NOTCL] %s' % _("Adding tool cancelled. Tool already in Tool Table."))
             self.tools_table.itemChanged.connect(self.on_tool_edit)
@@ -960,7 +983,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
                 self.app.inform.emit('[success] %s' % _("New tool added to Tool Table."))
             self.ncc_tools.update({
                 int(self.tooluid): {
-                    'tooldia': float('%.4f' % tool_dia),
+                    'tooldia': float('%.*f' % (self.decimals, tool_dia)),
                     'offset': 'Path',
                     'offset_value': 0.0,
                     'type': 'Iso',
@@ -981,7 +1004,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         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]))
+                    tool_dias.append(float('%.*f' % (self.decimals, v[tool_v])))
 
         for row in range(self.tools_table.rowCount()):
 
@@ -1061,22 +1084,18 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.build_ui()
 
     def on_ncc_click(self):
+        """
+        Slot for clicking signal of the self.generate.ncc_button
+        :return: None
+        """
 
         # init values for the next usage
         self.reset_usage()
-
         self.app.report_usage("on_paint_button_click")
 
-        try:
-            self.overlap = float(self.ncc_overlap_entry.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                self.overlap = float(self.ncc_overlap_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL]  %s' % _("Wrong value format entered, "
-                                                             "use a number."))
-                return
+        self.overlap = float(self.ncc_overlap_entry.get_value())
+
+        self.grb_circle_steps = int(self.app.defaults["gerber_circle_steps"])
 
         if self.overlap >= 1 or self.overlap < 0:
             self.app.inform.emit('[ERROR_NOTCL] %s' % _("Overlap value must be between "
@@ -1085,9 +1104,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
         self.connect = self.ncc_connect_cb.get_value()
         self.contour = self.ncc_contour_cb.get_value()
-
         self.has_offset = self.ncc_choice_offset_cb.isChecked()
-
         self.rest = self.ncc_rest_cb.get_value()
 
         self.obj_name = self.object_combo.currentText()
@@ -1282,7 +1299,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
             curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
 
             self.app.app_cursor.set_data(np.asarray([(curr_pos[0], curr_pos[1])]),
-                                         symbol='++', edge_color='black', size=self.app.defaults["global_cursor_size"])
+                                         symbol='++', edge_color=self.app.cursor_color_3D,
+                                         size=self.app.defaults["global_cursor_size"])
 
         # update the positions on status bar
         self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
@@ -1339,12 +1357,16 @@ class NonCopperClear(FlatCAMTool, Gerber):
         :param rest: True if to use rest-machining
         :param tools_storage: whether to use the current tools_storage self.ncc_tools or a different one.
         Usage of the different one is related to when this function is called from a TcL command.
+        :param plot: if True after the job is finished the result will be plotted, else it will not.
         :param run_threaded: If True the method will be run in a threaded way suitable for GUI usage; if False it will
         run non-threaded for TclShell usage
         :return:
         """
-
-        proc = self.app.proc_container.new(_("Non-Copper clearing ..."))
+        if run_threaded:
+            proc = self.app.proc_container.new(_("Non-Copper clearing ..."))
+        else:
+            self.app.proc_container.view.set_busy(_("Non-Copper clearing ..."))
+            QtWidgets.QApplication.processEvents()
 
         # #####################################################################
         # ####### Read the parameters #########################################
@@ -1360,22 +1382,15 @@ class NonCopperClear(FlatCAMTool, Gerber):
         if margin is not None:
             ncc_margin = margin
         else:
-            try:
-                ncc_margin = float(self.ncc_margin_entry.get_value())
-            except ValueError:
-                # try to convert comma to decimal point. if it's still not working error message and return
-                try:
-                    ncc_margin = float(self.ncc_margin_entry.get_value().replace(',', '.'))
-                except ValueError:
-                    self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number."))
-                    return
+            ncc_margin = float(self.ncc_margin_entry.get_value())
 
         if select_method is not None:
             ncc_select = select_method
         else:
             ncc_select = self.reference_radio.get_value()
 
-        overlap = overlap if overlap else self.app.defaults["tools_nccoverlap"]
+        overlap = overlap if overlap else float(self.app.defaults["tools_nccoverlap"])
+
         connect = connect if connect else self.app.defaults["tools_nccconnect"]
         contour = contour if contour else self.app.defaults["tools_ncccontour"]
         order = order if order else self.ncc_order_radio.get_value()
@@ -1514,6 +1529,10 @@ class NonCopperClear(FlatCAMTool, Gerber):
             assert isinstance(geo_obj, FlatCAMGeometry), \
                 "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
 
+            # provide the app with a way to process the GUI events when in a blocking loop
+            if not run_threaded:
+                QtWidgets.QApplication.processEvents()
+
             log.debug("NCC Tool. Normal copper clearing task started.")
             self.app.inform.emit(_("NCC Tool. Finished non-copper polygons. Normal copper clearing task started."))
 
@@ -1592,6 +1611,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
                     else:
                         try:
                             for geo_elem in isolated_geo:
+                                # provide the app with a way to process the GUI events when in a blocking loop
+                                QtWidgets.QApplication.processEvents()
+
                                 if self.app.abort_flag:
                                     # graceful abort requested by the user
                                     raise FlatCAMApp.GracefulException
@@ -1641,7 +1663,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
                                 break
 
                         for k, v in tools_storage.items():
-                            if float('%.4f' % v['tooldia']) == float('%.4f' % tool_iso):
+                            if float('%.*f' % (self.decimals, v['tooldia'])) == float('%.*f' % (self.decimals,
+                                                                                                tool_iso)):
                                 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
@@ -1696,6 +1719,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
                     # graceful abort requested by the user
                     raise FlatCAMApp.GracefulException
 
+                # provide the app with a way to process the GUI events when in a blocking loop
+                QtWidgets.QApplication.processEvents()
+
                 app_obj.inform.emit(
                     '[success] %s %s%s %s' % (_('NCC Tool clearing with tool diameter = '),
                                               str(tool),
@@ -1730,6 +1756,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
                     if len(area.geoms) > 0:
                         pol_nr = 0
                         for p in area.geoms:
+                            # provide the app with a way to process the GUI events when in a blocking loop
+                            QtWidgets.QApplication.processEvents()
+
                             if self.app.abort_flag:
                                 # graceful abort requested by the user
                                 raise FlatCAMApp.GracefulException
@@ -1737,15 +1766,15 @@ class NonCopperClear(FlatCAMTool, Gerber):
                                 try:
                                     if isinstance(p, Polygon):
                                         if ncc_method == 'standard':
-                                            cp = self.clear_polygon(p, tool, self.app.defaults["gerber_circle_steps"],
+                                            cp = self.clear_polygon(p, tool, self.grb_circle_steps,
                                                                     overlap=overlap, contour=contour, connect=connect,
                                                                     prog_plot=prog_plot)
                                         elif ncc_method == 'seed':
-                                            cp = self.clear_polygon2(p, tool, self.app.defaults["gerber_circle_steps"],
+                                            cp = self.clear_polygon2(p, tool, self.grb_circle_steps,
                                                                      overlap=overlap, contour=contour, connect=connect,
                                                                      prog_plot=prog_plot)
                                         else:
-                                            cp = self.clear_polygon3(p, tool, self.app.defaults["gerber_circle_steps"],
+                                            cp = self.clear_polygon3(p, tool, self.grb_circle_steps,
                                                                      overlap=overlap, contour=contour, connect=connect,
                                                                      prog_plot=prog_plot)
                                         if cp:
@@ -1755,19 +1784,19 @@ class NonCopperClear(FlatCAMTool, Gerber):
                                             if pol is not None:
                                                 if ncc_method == 'standard':
                                                     cp = self.clear_polygon(pol, tool,
-                                                                            self.app.defaults["gerber_circle_steps"],
+                                                                            self.grb_circle_steps,
                                                                             overlap=overlap, contour=contour,
                                                                             connect=connect,
                                                                             prog_plot=prog_plot)
                                                 elif ncc_method == 'seed':
                                                     cp = self.clear_polygon2(pol, tool,
-                                                                             self.app.defaults["gerber_circle_steps"],
+                                                                             self.grb_circle_steps,
                                                                              overlap=overlap, contour=contour,
                                                                              connect=connect,
                                                                              prog_plot=prog_plot)
                                                 else:
                                                     cp = self.clear_polygon3(pol, tool,
-                                                                             self.app.defaults["gerber_circle_steps"],
+                                                                             self.grb_circle_steps,
                                                                              overlap=overlap, contour=contour,
                                                                              connect=connect,
                                                                              prog_plot=prog_plot)
@@ -1799,7 +1828,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
                             # find the tooluid associated with the current tool_dia so we know where to add the tool
                             # solid_geometry
                             for k, v in tools_storage.items():
-                                if float('%.4f' % v['tooldia']) == float('%.4f' % tool):
+                                if float('%.*f' % (self.decimals, v['tooldia'])) == float('%.*f' % (self.decimals,
+                                                                                                    tool)):
                                     current_uid = int(k)
 
                                     # add the solid_geometry to the current too in self.paint_tools dictionary
@@ -1838,7 +1868,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
                 if geo_obj.tools[tooluid]['solid_geometry']:
                     has_solid_geo += 1
             if has_solid_geo == 0:
-                app_obj.inform.emit('[ERROR] %s' % _("There is no Painting Geometry in the file.\n"
+                app_obj.inform.emit('[ERROR] %s' % _("There is no NCC 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."))
@@ -1865,6 +1895,10 @@ class NonCopperClear(FlatCAMTool, Gerber):
             log.debug("NCC Tool. Rest machining copper clearing task started.")
             app_obj.inform.emit('_(NCC Tool. Rest machining copper clearing task started.')
 
+            # provide the app with a way to process the GUI events when in a blocking loop
+            if not run_threaded:
+                QtWidgets.QApplication.processEvents()
+
             # a flag to signal that the isolation is broken by the bounding box in 'area' and 'box' cases
             # will store the number of tools for which the isolation is broken
             warning_flag = 0
@@ -1920,6 +1954,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
                     else:
                         try:
                             for geo_elem in isolated_geo:
+                                # provide the app with a way to process the GUI events when in a blocking loop
+                                QtWidgets.QApplication.processEvents()
+
                                 if self.app.abort_flag:
                                     # graceful abort requested by the user
                                     raise FlatCAMApp.GracefulException
@@ -1972,7 +2009,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
                                 break
 
                         for k, v in tools_storage.items():
-                            if float('%.4f' % v['tooldia']) == float('%.4f' % tool_iso):
+                            if float('%.*f' % (self.decimals, v['tooldia'])) == float('%.*f' % (self.decimals,
+                                                                                                tool_iso)):
                                 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
@@ -2047,6 +2085,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
                 # Area to clear
                 for poly in cleared_by_last_tool:
+                    # provide the app with a way to process the GUI events when in a blocking loop
+                    QtWidgets.QApplication.processEvents()
+
                     if self.app.abort_flag:
                         # graceful abort requested by the user
                         raise FlatCAMApp.GracefulException
@@ -2083,21 +2124,24 @@ class NonCopperClear(FlatCAMTool, Gerber):
                                 raise FlatCAMApp.GracefulException
 
                             if p is not None:
+                                # provide the app with a way to process the GUI events when in a blocking loop
+                                QtWidgets.QApplication.processEvents()
+
                                 if isinstance(p, Polygon):
                                     try:
                                         if ncc_method == 'standard':
                                             cp = self.clear_polygon(p, tool_used,
-                                                                    self.app.defaults["gerber_circle_steps"],
+                                                                    self.grb_circle_steps,
                                                                     overlap=overlap, contour=contour, connect=connect,
                                                                     prog_plot=prog_plot)
                                         elif ncc_method == 'seed':
                                             cp = self.clear_polygon2(p, tool_used,
-                                                                     self.app.defaults["gerber_circle_steps"],
+                                                                     self.grb_circle_steps,
                                                                      overlap=overlap, contour=contour, connect=connect,
                                                                      prog_plot=prog_plot)
                                         else:
                                             cp = self.clear_polygon3(p, tool_used,
-                                                                     self.app.defaults["gerber_circle_steps"],
+                                                                     self.grb_circle_steps,
                                                                      overlap=overlap, contour=contour, connect=connect,
                                                                      prog_plot=prog_plot)
                                         cleared_geo.append(list(cp.get_objects()))
@@ -2109,22 +2153,25 @@ class NonCopperClear(FlatCAMTool, Gerber):
                                 elif isinstance(p, MultiPolygon):
                                     for poly in p:
                                         if poly is not None:
+                                            # provide the app with a way to process the GUI events when in a blocking loop
+                                            QtWidgets.QApplication.processEvents()
+
                                             try:
                                                 if ncc_method == 'standard':
                                                     cp = self.clear_polygon(poly, tool_used,
-                                                                            self.app.defaults["gerber_circle_steps"],
+                                                                            self.grb_circle_steps,
                                                                             overlap=overlap, contour=contour,
                                                                             connect=connect,
                                                                             prog_plot=prog_plot)
                                                 elif ncc_method == 'seed':
                                                     cp = self.clear_polygon2(poly, tool_used,
-                                                                             self.app.defaults["gerber_circle_steps"],
+                                                                             self.grb_circle_steps,
                                                                              overlap=overlap, contour=contour,
                                                                              connect=connect,
                                                                              prog_plot=prog_plot)
                                                 else:
                                                     cp = self.clear_polygon3(poly, tool_used,
-                                                                             self.app.defaults["gerber_circle_steps"],
+                                                                             self.grb_circle_steps,
                                                                              overlap=overlap, contour=contour,
                                                                              connect=connect,
                                                                              prog_plot=prog_plot)
@@ -2172,7 +2219,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
                             # find the tooluid associated with the current tool_dia so we know
                             # where to add the tool solid_geometry
                             for k, v in tools_storage.items():
-                                if float('%.4f' % v['tooldia']) == float('%.4f' % tool):
+                                if float('%.*f' % (self.decimals, v['tooldia'])) == float('%.*f' % (self.decimals,
+                                                                                                    tool)):
                                     current_uid = int(k)
 
                                     # add the solid_geometry to the current too in self.paint_tools dictionary
@@ -2219,13 +2267,19 @@ class NonCopperClear(FlatCAMTool, Gerber):
                 else:
                     app_obj.new_object("geometry", name, gen_clear_area, plot=plot)
             except FlatCAMApp.GracefulException:
-                proc.done()
+                if run_threaded:
+                    proc.done()
                 return
             except Exception as e:
-                proc.done()
+                if run_threaded:
+                    proc.done()
                 traceback.print_stack()
                 return
-            proc.done()
+            if run_threaded:
+                proc.done()
+            else:
+                app_obj.proc_container.view.set_idle()
+
             # focus on Selected Tab
             self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
 
@@ -2614,6 +2668,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
         except Exception as e:
             try:
                 for el in target:
+                    # provide the app with a way to process the GUI events when in a blocking loop
+                    QtWidgets.QApplication.processEvents()
+
                     if self.app.abort_flag:
                         # graceful abort requested by the user
                         raise FlatCAMApp.GracefulException

+ 550 - 0
flatcamTools/ToolOptimal.py

@@ -0,0 +1,550 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Date: 09/27/2019                                         #
+# MIT Licence                                              #
+# ##########################################################
+
+from FlatCAMTool import FlatCAMTool
+from FlatCAMObj import *
+from shapely.geometry import Point
+from shapely import affinity
+from shapely.ops import nearest_points
+from PyQt5 import QtCore
+
+import gettext
+import FlatCAMTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class ToolOptimal(FlatCAMTool):
+
+    toolName = _("Optimal Tool")
+
+    update_text = pyqtSignal(list)
+    update_sec_distances = pyqtSignal(dict)
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+
+        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        self.decimals = 4
+
+        # ############################################################################
+        # ############################ GUI creation ##################################
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet(
+            """
+            QLabel
+            {
+                font-size: 16px;
+                font-weight: bold;
+            }
+            """)
+        self.layout.addWidget(title_label)
+
+        # ## Form Layout
+        form_lay = QtWidgets.QFormLayout()
+        self.layout.addLayout(form_lay)
+
+        form_lay.addRow(QtWidgets.QLabel(""))
+
+        # ## 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.gerber_object_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
+        self.gerber_object_label.setToolTip(
+            "Gerber object for which to find the minimum distance between copper features."
+        )
+        form_lay.addRow(self.gerber_object_label, self.gerber_object_combo)
+
+        # Precision = nr of decimals
+        self.precision_label = QtWidgets.QLabel('%s:' % _("Precision"))
+        self.precision_label.setToolTip(_("Number of decimals kept for found distances."))
+
+        self.precision_spinner = FCSpinner()
+        self.precision_spinner.set_range(2, 10)
+        self.precision_spinner.setWrapping(True)
+        form_lay.addRow(self.precision_label, self.precision_spinner)
+
+        # Results Title
+        self.title_res_label = QtWidgets.QLabel('<b>%s:</b>' % _("Minimum distance"))
+        self.title_res_label.setToolTip(_("Display minimum distance between copper features."))
+        form_lay.addRow(self.title_res_label)
+
+        # Result value
+        self.result_label = QtWidgets.QLabel('%s:' % _("Determined"))
+        self.result_entry = FCEntry()
+        self.result_entry.setReadOnly(True)
+
+        self.units_lbl = QtWidgets.QLabel(self.units.lower())
+        self.units_lbl.setDisabled(True)
+
+        hlay = QtWidgets.QHBoxLayout()
+        hlay.addWidget(self.result_entry)
+        hlay.addWidget(self.units_lbl)
+
+        form_lay.addRow(self.result_label, hlay)
+
+        # Frequency of minimum encounter
+        self.freq_label = QtWidgets.QLabel('%s:' % _("Occurring"))
+        self.freq_label.setToolTip(_("How many times this minimum is found."))
+        self.freq_entry = FCEntry()
+        self.freq_entry.setReadOnly(True)
+        form_lay.addRow(self.freq_label, self.freq_entry)
+
+        # Control if to display the locations of where the minimum was found
+        self.locations_cb = FCCheckBox(_("Minimum points coordinates"))
+        self.locations_cb.setToolTip(_("Coordinates for points where minimum distance was found."))
+        form_lay.addRow(self.locations_cb)
+
+        # Locations where minimum was found
+        self.locations_textb = FCTextArea(parent=self)
+        self.locations_textb.setReadOnly(True)
+        stylesheet = """
+                        QTextEdit { selection-background-color:blue;
+                                    selection-color:white;
+                        }
+                     """
+
+        self.locations_textb.setStyleSheet(stylesheet)
+        form_lay.addRow(self.locations_textb)
+
+        # Jump button
+        self.locate_button = QtWidgets.QPushButton(_("Jump to selected position"))
+        self.locate_button.setToolTip(
+            _("Select a position in the Locations text box and then\n"
+              "click this button.")
+        )
+        self.locate_button.setMinimumWidth(60)
+        self.locate_button.setDisabled(True)
+        form_lay.addRow(self.locate_button)
+
+        # Other distances in Gerber
+        self.title_second_res_label = QtWidgets.QLabel('<b>%s:</b>' % _("Other distances"))
+        self.title_second_res_label.setToolTip(_("Will display other distances in the Gerber file ordered from\n"
+                                                 "the minimum to the maximum, not including the absolute minimum."))
+        form_lay.addRow(self.title_second_res_label)
+
+        # Control if to display the locations of where the minimum was found
+        self.sec_locations_cb = FCCheckBox(_("Other distances points coordinates"))
+        self.sec_locations_cb.setToolTip(_("Other distances and the coordinates for points\n"
+                                           "where the distance was found."))
+        form_lay.addRow(self.sec_locations_cb)
+
+        # this way I can hide/show the frame
+        self.sec_locations_frame = QtWidgets.QFrame()
+        self.sec_locations_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.sec_locations_frame)
+        self.distances_box = QtWidgets.QVBoxLayout()
+        self.distances_box.setContentsMargins(0, 0, 0, 0)
+        self.sec_locations_frame.setLayout(self.distances_box)
+
+        # Other Distances label
+        self.distances_label = QtWidgets.QLabel('%s' % _("Gerber distances"))
+        self.distances_label.setToolTip(_("Other distances and the coordinates for points\n"
+                                          "where the distance was found."))
+        self.distances_box.addWidget(self.distances_label)
+
+        # Other distances
+        self.distances_textb = FCTextArea(parent=self)
+        self.distances_textb.setReadOnly(True)
+        stylesheet = """
+                        QTextEdit { selection-background-color:blue;
+                                    selection-color:white;
+                        }
+                     """
+
+        self.distances_textb.setStyleSheet(stylesheet)
+        self.distances_box.addWidget(self.distances_textb)
+
+        self.distances_box.addWidget(QtWidgets.QLabel(''))
+
+        # Other Locations label
+        self.locations_label = QtWidgets.QLabel('%s' % _("Points coordinates"))
+        self.locations_label.setToolTip(_("Other distances and the coordinates for points\n"
+                                          "where the distance was found."))
+        self.distances_box.addWidget(self.locations_label)
+
+        # Locations where minimum was found
+        self.locations_sec_textb = FCTextArea(parent=self)
+        self.locations_sec_textb.setReadOnly(True)
+        stylesheet = """
+                        QTextEdit { selection-background-color:blue;
+                                    selection-color:white;
+                        }
+                     """
+
+        self.locations_sec_textb.setStyleSheet(stylesheet)
+        self.distances_box.addWidget(self.locations_sec_textb)
+
+        # Jump button
+        self.locate_sec_button = QtWidgets.QPushButton(_("Jump to selected position"))
+        self.locate_sec_button.setToolTip(
+            _("Select a position in the Locations text box and then\n"
+              "click this button.")
+        )
+        self.locate_sec_button.setMinimumWidth(60)
+        self.locate_sec_button.setDisabled(True)
+        self.distances_box.addWidget(self.locate_sec_button)
+
+        # GO button
+        self.calculate_button = QtWidgets.QPushButton(_("Find Minimum"))
+        self.calculate_button.setToolTip(
+            _("Calculate the minimum distance between copper features,\n"
+              "this will allow the determination of the right tool to\n"
+              "use for isolation or copper clearing.")
+        )
+        self.calculate_button.setMinimumWidth(60)
+        self.layout.addWidget(self.calculate_button)
+
+        self.loc_ois = OptionalHideInputSection(self.locations_cb, [self.locations_textb, self.locate_button])
+        self.sec_loc_ois = OptionalHideInputSection(self.sec_locations_cb, [self.sec_locations_frame])
+        # ################## Finished GUI creation ###################################
+        # ############################################################################
+
+        # this is the line selected in the textbox with the locations of the minimum
+        self.selected_text = ''
+
+        # this is the line selected in the textbox with the locations of the other distances found in the Gerber object
+        self.selected_locations_text = ''
+
+        # dict to hold the distances between every two elements in Gerber as keys and the actual locations where that
+        # distances happen as values
+        self.min_dict = dict()
+
+        # ############################################################################
+        # ############################ Signals #######################################
+        # ############################################################################
+        self.calculate_button.clicked.connect(self.find_minimum_distance)
+        self.locate_button.clicked.connect(self.on_locate_position)
+        self.update_text.connect(self.on_update_text)
+        self.locations_textb.cursorPositionChanged.connect(self.on_textbox_clicked)
+
+        self.locate_sec_button.clicked.connect(self.on_locate_sec_position)
+        self.update_sec_distances.connect(self.on_update_sec_distances_txt)
+        self.distances_textb.cursorPositionChanged.connect(self.on_distances_textb_clicked)
+        self.locations_sec_textb.cursorPositionChanged.connect(self.on_locations_sec_clicked)
+
+        self.layout.addStretch()
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+O', **kwargs)
+
+    def run(self, toggle=True):
+        self.app.report_usage("ToolOptimal()")
+
+        self.result_entry.set_value(0.0)
+        self.freq_entry.set_value('0')
+
+        if toggle:
+            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+            else:
+                try:
+                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
+                except AttributeError:
+                    pass
+        else:
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+
+        FlatCAMTool.run(self)
+        self.set_tool_ui()
+
+        self.app.ui.notebook.setTabText(2, _("Optimal Tool"))
+
+    def set_tool_ui(self):
+        self.precision_spinner.set_value(int(self.app.defaults["tools_opt_precision"]))
+        self.locations_textb.clear()
+        # new cursor - select all document
+        cursor = self.locations_textb.textCursor()
+        cursor.select(QtGui.QTextCursor.Document)
+
+        # clear previous selection highlight
+        tmp = cursor.blockFormat()
+        tmp.clearBackground()
+        cursor.setBlockFormat(tmp)
+
+        self.locations_textb.setVisible(False)
+        self.locate_button.setVisible(False)
+
+        self.result_entry.set_value(0.0)
+        self.freq_entry.set_value('0')
+        self.reset_fields()
+
+    def find_minimum_distance(self):
+        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        self.decimals = int(self.precision_spinner.get_value())
+
+        selection_index = self.gerber_object_combo.currentIndex()
+
+        model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
+        try:
+            fcobj = model_index.internalPointer().obj
+        except Exception as e:
+            log.debug("ToolOptimal.find_minimum_distance() --> %s" % str(e))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
+            return
+
+        if not isinstance(fcobj, FlatCAMGerber):
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Gerber objects can be evaluated."))
+            return
+
+        proc = self.app.proc_container.new(_("Working..."))
+
+        def job_thread(app_obj):
+            app_obj.inform.emit(_("Optimal Tool. Started to search for the minimum distance between copper features."))
+            try:
+                old_disp_number = 0
+                pol_nr = 0
+                app_obj.proc_container.update_view_text(' %d%%' % 0)
+                total_geo = list()
+
+                for ap in list(fcobj.apertures.keys()):
+                    if 'geometry' in fcobj.apertures[ap]:
+                        app_obj.inform.emit(
+                            '%s: %s' % (_("Optimal Tool. Parsing geometry for aperture"), str(ap)))
+
+                        for geo_el in fcobj.apertures[ap]['geometry']:
+                            if self.app.abort_flag:
+                                # graceful abort requested by the user
+                                raise FlatCAMApp.GracefulException
+
+                            if 'solid' in geo_el and geo_el['solid'] is not None and geo_el['solid'].is_valid:
+                                total_geo.append(geo_el['solid'])
+
+                app_obj.inform.emit(
+                    _("Optimal Tool. Creating a buffer for the object geometry."))
+                total_geo = MultiPolygon(total_geo)
+                total_geo = total_geo.buffer(0)
+
+                try:
+                    __ = iter(total_geo)
+                    geo_len = len(total_geo)
+                    geo_len = (geo_len * (geo_len - 1)) / 2
+                except TypeError:
+                    app_obj.inform.emit('[ERROR_NOTCL] %s' %
+                                        _("The Gerber object has one Polygon as geometry.\n"
+                                          "There are no distances between geometry elements to be found."))
+                    return 'fail'
+
+                app_obj.inform.emit(
+                    '%s: %s' % (_("Optimal Tool. Finding the distances between each two elements. Iterations"),
+                                str(geo_len)))
+
+                self.min_dict = dict()
+                idx = 1
+                for geo in total_geo:
+                    for s_geo in total_geo[idx:]:
+                        if self.app.abort_flag:
+                            # graceful abort requested by the user
+                            raise FlatCAMApp.GracefulException
+
+                        # minimize the number of distances by not taking into considerations those that are too small
+                        dist = geo.distance(s_geo)
+                        dist = float('%.*f' % (self.decimals, dist))
+                        loc_1, loc_2 = nearest_points(geo, s_geo)
+
+                        proc_loc = (
+                            (float('%.*f' % (self.decimals, loc_1.x)), float('%.*f' % (self.decimals, loc_1.y))),
+                            (float('%.*f' % (self.decimals, loc_2.x)), float('%.*f' % (self.decimals, loc_2.y)))
+                        )
+
+                        if dist in self.min_dict:
+                            self.min_dict[dist].append(proc_loc)
+                        else:
+                            self.min_dict[dist] = [proc_loc]
+
+                        pol_nr += 1
+                        disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
+
+                        if old_disp_number < disp_number <= 100:
+                            app_obj.proc_container.update_view_text(' %d%%' % disp_number)
+                            old_disp_number = disp_number
+                    idx += 1
+
+                app_obj.inform.emit(
+                    _("Optimal Tool. Finding the minimum distance."))
+
+                min_list = list(self.min_dict.keys())
+                min_dist = min(min_list)
+                min_dist_string = '%.*f' % (self.decimals, float(min_dist))
+                self.result_entry.set_value(min_dist_string)
+
+                freq = len(self.min_dict[min_dist])
+                freq = '%d' % int(freq)
+                self.freq_entry.set_value(freq)
+
+                min_locations = self.min_dict.pop(min_dist)
+
+                self.update_text.emit(min_locations)
+                self.update_sec_distances.emit(self.min_dict)
+
+                app_obj.inform.emit('[success] %s' % _("Optimal Tool. Finished successfully."))
+            except Exception as ee:
+                proc.done()
+                log.debug(str(ee))
+                return
+            proc.done()
+
+        self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
+
+    def on_locate_position(self):
+        # cursor = self.locations_textb.textCursor()
+        # self.selected_text = cursor.selectedText()
+
+        try:
+            if self.selected_text != '':
+                loc = eval(self.selected_text)
+            else:
+                return 'fail'
+        except Exception as e:
+            log.debug("ToolOptimal.on_locate_position() --> first try %s" % str(e))
+            self.app.inform.emit("[ERROR_NOTCL] The selected text is no valid location in the format "
+                                 "((x0, y0), (x1, y1)).")
+            return 'fail'
+
+        try:
+            loc_1 = loc[0]
+            loc_2 = loc[1]
+            dx = loc_1[0] - loc_2[0]
+            dy = loc_1[1] - loc_2[1]
+            loc = (float('%.*f' % (self.decimals, (min(loc_1[0], loc_2[0]) + (abs(dx) / 2)))),
+                   float('%.*f' % (self.decimals, (min(loc_1[1], loc_2[1]) + (abs(dy) / 2)))))
+            self.app.on_jump_to(custom_location=loc)
+        except Exception as e:
+            log.debug("ToolOptimal.on_locate_position() --> sec try %s" % str(e))
+            return 'fail'
+
+    def on_update_text(self, data):
+        txt = ''
+        for loc in data:
+            if loc:
+                txt += '%s, %s\n' % (str(loc[0]), str(loc[1]))
+        self.locations_textb.setPlainText(txt)
+        self.locate_button.setDisabled(False)
+
+    def on_textbox_clicked(self):
+        # new cursor - select all document
+        cursor = self.locations_textb.textCursor()
+        cursor.select(QtGui.QTextCursor.Document)
+
+        # clear previous selection highlight
+        tmp = cursor.blockFormat()
+        tmp.clearBackground()
+        cursor.setBlockFormat(tmp)
+
+        # new cursor - select the current line
+        cursor = self.locations_textb.textCursor()
+        cursor.select(QtGui.QTextCursor.LineUnderCursor)
+
+        # highlight the current selected line
+        tmp = cursor.blockFormat()
+        tmp.setBackground(QtGui.QBrush(QtCore.Qt.yellow))
+        cursor.setBlockFormat(tmp)
+
+        self.selected_text = cursor.selectedText()
+
+    def on_update_sec_distances_txt(self, data):
+        distance_list = sorted(list(data.keys()))
+        txt = ''
+        for loc in distance_list:
+            txt += '%s\n' % str(loc)
+        self.distances_textb.setPlainText(txt)
+        self.locate_sec_button.setDisabled(False)
+
+    def on_distances_textb_clicked(self):
+        # new cursor - select all document
+        cursor = self.distances_textb.textCursor()
+        cursor.select(QtGui.QTextCursor.Document)
+
+        # clear previous selection highlight
+        tmp = cursor.blockFormat()
+        tmp.clearBackground()
+        cursor.setBlockFormat(tmp)
+
+        # new cursor - select the current line
+        cursor = self.distances_textb.textCursor()
+        cursor.select(QtGui.QTextCursor.LineUnderCursor)
+
+        # highlight the current selected line
+        tmp = cursor.blockFormat()
+        tmp.setBackground(QtGui.QBrush(QtCore.Qt.yellow))
+        cursor.setBlockFormat(tmp)
+
+        distance_text = cursor.selectedText()
+        key_in_min_dict = eval(distance_text)
+        self.on_update_locations_text(dist=key_in_min_dict)
+
+    def on_update_locations_text(self, dist):
+        distance_list = self.min_dict[dist]
+        txt = ''
+        for loc in distance_list:
+            if loc:
+                txt += '%s, %s\n' % (str(loc[0]), str(loc[1]))
+        self.locations_sec_textb.setPlainText(txt)
+
+    def on_locations_sec_clicked(self):
+        # new cursor - select all document
+        cursor = self.locations_sec_textb.textCursor()
+        cursor.select(QtGui.QTextCursor.Document)
+
+        # clear previous selection highlight
+        tmp = cursor.blockFormat()
+        tmp.clearBackground()
+        cursor.setBlockFormat(tmp)
+
+        # new cursor - select the current line
+        cursor = self.locations_sec_textb.textCursor()
+        cursor.select(QtGui.QTextCursor.LineUnderCursor)
+
+        # highlight the current selected line
+        tmp = cursor.blockFormat()
+        tmp.setBackground(QtGui.QBrush(QtCore.Qt.yellow))
+        cursor.setBlockFormat(tmp)
+
+        self.selected_locations_text = cursor.selectedText()
+
+    def on_locate_sec_position(self):
+        try:
+            if self.selected_locations_text != '':
+                loc = eval(self.selected_locations_text)
+            else:
+                return 'fail'
+        except Exception as e:
+            log.debug("ToolOptimal.on_locate_sec_position() --> first try %s" % str(e))
+            self.app.inform.emit("[ERROR_NOTCL] The selected text is no valid location in the format "
+                                 "((x0, y0), (x1, y1)).")
+            return 'fail'
+
+        try:
+            loc_1 = loc[0]
+            loc_2 = loc[1]
+            dx = loc_1[0] - loc_2[0]
+            dy = loc_1[1] - loc_2[1]
+            loc = (float('%.*f' % (self.decimals, (min(loc_1[0], loc_2[0]) + (abs(dx) / 2)))),
+                   float('%.*f' % (self.decimals, (min(loc_1[1], loc_2[1]) + (abs(dy) / 2)))))
+            self.app.on_jump_to(custom_location=loc)
+        except Exception as e:
+            log.debug("ToolOptimal.on_locate_sec_position() --> sec try %s" % str(e))
+            return 'fail'
+
+    def reset_fields(self):
+        self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.gerber_object_combo.setCurrentIndex(0)

+ 2 - 3
flatcamTools/ToolPDF.py

@@ -1,10 +1,9 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 4/23/2019                                          #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 from FlatCAMTool import FlatCAMTool
 from shapely.geometry import Point, Polygon, LineString

+ 73 - 77
flatcamTools/ToolPaint.py

@@ -1,6 +1,5 @@
 # ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Modified: Marius Adrian Stanciu (c)                 #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
@@ -26,6 +25,7 @@ class ToolPaint(FlatCAMTool, Gerber):
 
     def __init__(self, app):
         self.app = app
+        self.decimals = 4
 
         FlatCAMTool.__init__(self, app)
         Geometry.__init__(self, geo_steps_per_circle=self.app.defaults["geometry_circle_steps"])
@@ -156,19 +156,14 @@ class ToolPaint(FlatCAMTool, Gerber):
         form.addRow(self.order_label, self.order_radio)
 
         # ### Add a new Tool ## ##
-        hlay = QtWidgets.QHBoxLayout()
-        self.tools_box.addLayout(hlay)
-
         self.addtool_entry_lbl = QtWidgets.QLabel('<b>%s:</b>' % _('Tool Dia'))
         self.addtool_entry_lbl.setToolTip(
             _("Diameter for the new tool.")
         )
-        self.addtool_entry = FCEntry2()
+        self.addtool_entry = FCDoubleSpinner()
+        self.addtool_entry.set_precision(self.decimals)
 
-        # hlay.addWidget(self.addtool_label)
-        # hlay.addStretch()
-        hlay.addWidget(self.addtool_entry_lbl)
-        hlay.addWidget(self.addtool_entry)
+        form.addRow(self.addtool_entry_lbl, self.addtool_entry)
 
         grid2 = QtWidgets.QGridLayout()
         self.tools_box.addLayout(grid2)
@@ -200,6 +195,8 @@ class ToolPaint(FlatCAMTool, Gerber):
 
         grid3 = QtWidgets.QGridLayout()
         self.tools_box.addLayout(grid3)
+        grid3.setColumnStretch(0, 0)
+        grid3.setColumnStretch(1, 1)
 
         # Overlap
         ovlabel = QtWidgets.QLabel('%s:' % _('Overlap Rate'))
@@ -230,7 +227,9 @@ class ToolPaint(FlatCAMTool, Gerber):
               "be painted.")
         )
         grid3.addWidget(marginlabel, 2, 0)
-        self.paintmargin_entry = FCEntry()
+        self.paintmargin_entry = FCDoubleSpinner()
+        self.paintmargin_entry.set_precision(self.decimals)
+
         grid3.addWidget(self.paintmargin_entry, 2, 1)
 
         # Method
@@ -351,6 +350,8 @@ class ToolPaint(FlatCAMTool, Gerber):
         self.tools_box.addWidget(self.generate_paint_button)
 
         self.tools_box.addStretch()
+        # #################################### FINSIHED GUI #####################################
+        # #######################################################################################
 
         self.obj_name = ""
         self.paint_obj = None
@@ -412,7 +413,9 @@ class ToolPaint(FlatCAMTool, Gerber):
 
         self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
 
-        # ## Signals
+        # #############################################################################
+        # ################################# Signals ###################################
+        # #############################################################################
         self.addtool_btn.clicked.connect(self.on_tool_add)
         self.addtool_entry.returnPressed.connect(self.on_tool_add)
         # self.copytool_btn.clicked.connect(lambda: self.on_tool_copy())
@@ -426,6 +429,16 @@ class ToolPaint(FlatCAMTool, Gerber):
         self.box_combo_type.currentIndexChanged.connect(self.on_combo_box_type)
         self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
 
+        # #############################################################################
+        # ###################### Setup CONTEXT MENU ###################################
+        # #############################################################################
+        self.tools_table.setupContextMenu()
+        self.tools_table.addContextMenu(
+            "Add", self.on_add_tool_by_key, 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"))
+
     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()))
@@ -434,6 +447,22 @@ class ToolPaint(FlatCAMTool, Gerber):
     def install(self, icon=None, separator=None, **kwargs):
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+P', **kwargs)
 
+    def on_add_tool_by_key(self):
+        tool_add_popup = FCInputDialog(title='%s...' % _("New Tool"),
+                                       text='%s:' % _('Enter a Tool Diameter'),
+                                       min=0.0000, max=99.9999, decimals=4)
+        tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png'))
+
+        val, ok = tool_add_popup.get_value()
+        if ok:
+            if float(val) == 0:
+                self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                     _("Please enter a tool diameter with non-zero value, in Float format."))
+                return
+            self.on_tool_add(dia=float(val))
+        else:
+            self.app.inform.emit('[WARNING_NOTCL] %s...' % _("Adding Tool cancelled"))
+
     def run(self, toggle=True):
         self.app.report_usage("ToolPaint()")
 
@@ -488,15 +517,6 @@ class ToolPaint(FlatCAMTool, Gerber):
             # 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)
-            # self.tools_table.setContextMenuPolicy(Qt.NoContextMenu)
         if self.selectmethod_combo.get_value() == 'area':
             # disable rest-machining for single polygon painting
             self.rest_cb.set_value(False)
@@ -540,17 +560,12 @@ class ToolPaint(FlatCAMTool, Gerber):
         self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
 
         if self.units == "IN":
+            self.decimals = 4
             self.addtool_entry.set_value(0.039)
         else:
+            self.decimals = 2
             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
@@ -559,28 +574,28 @@ class ToolPaint(FlatCAMTool, Gerber):
         self.default_data.update({
             "name": '_paint',
             "plot": self.app.defaults["geometry_plot"],
-            "cutz": self.app.defaults["geometry_cutz"],
+            "cutz": float(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"],
+            "travelz": float(self.app.defaults["geometry_travelz"]),
+            "feedrate": float(self.app.defaults["geometry_feedrate"]),
+            "feedrate_z": float(self.app.defaults["geometry_feedrate_z"]),
+            "feedrate_rapid": float(self.app.defaults["geometry_feedrate_rapid"]),
             "dwell": self.app.defaults["geometry_dwell"],
-            "dwelltime": self.app.defaults["geometry_dwelltime"],
+            "dwelltime": float(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"],
+            "depthperpass": float(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"],
+            "toolchangez": float(self.app.defaults["geometry_toolchangez"]),
+            "endz": float(self.app.defaults["geometry_endz"]),
             "spindlespeed": self.app.defaults["geometry_spindlespeed"],
             "toolchangexy": self.app.defaults["geometry_toolchangexy"],
             "startz": self.app.defaults["geometry_startz"],
 
-            "tooldia": self.app.defaults["tools_painttooldia"],
-            "paintmargin": self.app.defaults["tools_paintmargin"],
+            "tooldia": float(self.app.defaults["tools_painttooldia"]),
+            "paintmargin": float(self.app.defaults["tools_paintmargin"]),
             "paintmethod": self.app.defaults["tools_paintmethod"],
             "selectmethod": self.app.defaults["tools_selectmethod"],
             "pathconnect": self.app.defaults["tools_pathconnect"],
@@ -590,7 +605,7 @@ class ToolPaint(FlatCAMTool, Gerber):
 
         # 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["tools_painttooldia"], muted=True)
+        self.on_tool_add(float(self.app.defaults["tools_painttooldia"]), muted=True)
 
         # if the Paint Method is "Single" disable the tool table context menu
         if self.default_data["selectmethod"] == "single":
@@ -608,7 +623,7 @@ class ToolPaint(FlatCAMTool, Gerber):
 
         sorted_tools = []
         for k, v in self.paint_tools.items():
-            sorted_tools.append(float('%.4f' % float(v['tooldia'])))
+            sorted_tools.append(float('%.*f' % (self.decimals, float(v['tooldia']))))
 
         order = self.order_radio.get_value()
         if order == 'fwd':
@@ -624,7 +639,7 @@ class ToolPaint(FlatCAMTool, Gerber):
 
         for tool_sorted in sorted_tools:
             for tooluid_key, tooluid_value in self.paint_tools.items():
-                if float('%.4f' % tooluid_value['tooldia']) == tool_sorted:
+                if float('%.*f' % (self.decimals, tooluid_value['tooldia'])) == tool_sorted:
                     tool_id += 1
                     id = QtWidgets.QTableWidgetItem('%d' % int(tool_id))
                     id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
@@ -632,12 +647,10 @@ class ToolPaint(FlatCAMTool, Gerber):
                     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('%.4f' % tooluid_value['tooldia'])
+                    # There are no drill bits in MM with more than 2 decimals diameter
+                    # For INCH the decimals should be no more than 4. There are no drills under 10mils
+
+                    dia = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, tooluid_value['tooldia']))
 
                     dia.setFlags(QtCore.Qt.ItemIsEnabled)
 
@@ -702,16 +715,7 @@ class ToolPaint(FlatCAMTool, Gerber):
         if dia:
             tool_dia = dia
         else:
-            try:
-                tool_dia = float(self.addtool_entry.get_value())
-            except ValueError:
-                # try to convert comma to decimal point. if it's still not working error message and return
-                try:
-                    tool_dia = float(self.addtool_entry.get_value().replace(',', '.'))
-                except ValueError:
-                    self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                         _("Wrong value format entered, use a number."))
-                    return
+            tool_dia = float(self.addtool_entry.get_value())
 
             if tool_dia is None:
                 self.build_ui()
@@ -736,9 +740,9 @@ class ToolPaint(FlatCAMTool, Gerber):
         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]))
+                    tool_dias.append(float('%.*f' % (self.decimals, v[tool_v])))
 
-        if float('%.4f' % tool_dia) in tool_dias:
+        if float('%.*f' % (self.decimals, tool_dia)) in tool_dias:
             if muted is None:
                 self.app.inform.emit('[WARNING_NOTCL] %s' %
                                      _("Adding tool cancelled. Tool already in Tool Table."))
@@ -750,7 +754,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                                      _("New tool added to Tool Table."))
             self.paint_tools.update({
                 int(self.tooluid): {
-                    'tooldia': float('%.4f' % tool_dia),
+                    'tooldia': float('%.*f' % (self.decimals, tool_dia)),
                     'offset': 'Path',
                     'offset_value': 0.0,
                     'type': 'Iso',
@@ -774,7 +778,7 @@ class ToolPaint(FlatCAMTool, Gerber):
         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]))
+                    tool_dias.append(float('%.*f' % (self.decimals, v[tool_v])))
 
         for row in range(self.tools_table.rowCount()):
             try:
@@ -925,16 +929,7 @@ class ToolPaint(FlatCAMTool, Gerber):
         # #####################################################
         self.app.inform.emit(_("Paint Tool. Reading parameters."))
 
-        try:
-            self.overlap = float(self.paintoverlap_entry.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                self.overlap = float(self.paintoverlap_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
+        self.overlap = float(self.paintoverlap_entry.get_value())
 
         if self.overlap >= 1 or self.overlap < 0:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
@@ -1185,7 +1180,8 @@ class ToolPaint(FlatCAMTool, Gerber):
             curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
 
             self.app.app_cursor.set_data(np.asarray([(curr_pos[0], curr_pos[1])]),
-                                         symbol='++', edge_color='black', size=self.app.defaults["global_cursor_size"])
+                                         symbol='++', edge_color=self.app.cursor_color_3D,
+                                         size=self.app.defaults["global_cursor_size"])
 
         # update the positions on status bar
         self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
@@ -1392,7 +1388,7 @@ class ToolPaint(FlatCAMTool, Gerber):
             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 tools_storage.items():
-                    if float('%.4f' % v['tooldia']) == float('%.4f' % tool_dia):
+                    if float('%.*f' % (self.decimals, v['tooldia'])) == float('%.*f' % (self.decimals, tool_dia)):
                         current_uid = int(k)
                         break
 
@@ -1688,7 +1684,7 @@ class ToolPaint(FlatCAMTool, Gerber):
 
                 # find the tooluid associated with the current tool_dia so we know where to add the tool solid_geometry
                 for k, v in tools_storage.items():
-                    if float('%.4f' % v['tooldia']) == float('%.4f' % tool_dia):
+                    if float('%.*f' % (self.decimals, v['tooldia'])) == float('%.*f' % (self.decimals, tool_dia)):
                         current_uid = int(k)
                         break
 
@@ -1916,7 +1912,7 @@ class ToolPaint(FlatCAMTool, Gerber):
 
                 # find the tooluid associated with the current tool_dia so we know where to add the tool solid_geometry
                 for k, v in tools_storage.items():
-                    if float('%.4f' % v['tooldia']) == float('%.4f' % tool_dia):
+                    if float('%.*f' % (self.decimals, v['tooldia'])) == float('%.*f' % (self.decimals, tool_dia)):
                         current_uid = int(k)
                         break
 
@@ -2162,7 +2158,7 @@ class ToolPaint(FlatCAMTool, Gerber):
 
                 # find the tooluid associated with the current tool_dia so we know where to add the tool solid_geometry
                 for k, v in tools_storage.items():
-                    if float('%.4f' % v['tooldia']) == float('%.4f' % tool_dia):
+                    if float('%.*f' % (self.decimals, v['tooldia'])) == float('%.*f' % (self.decimals, tool_dia)):
                         current_uid = int(k)
                         break
 
@@ -2391,7 +2387,7 @@ class ToolPaint(FlatCAMTool, Gerber):
 
                 # find the tooluid associated with the current tool_dia so we know where to add the tool solid_geometry
                 for k, v in tools_storage.items():
-                    if float('%.4f' % v['tooldia']) == float('%.4f' % tool_dia):
+                    if float('%.*f' % (self.decimals, v['tooldia'])) == float('%.*f' % (self.decimals, tool_dia)):
                         current_uid = int(k)
                         break
 

+ 30 - 72
flatcamTools/ToolPanelize.py

@@ -1,10 +1,9 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 from FlatCAMTool import FlatCAMTool
 from copy import copy, deepcopy
@@ -143,7 +142,10 @@ class Panelize(FlatCAMTool):
         form_layout.addRow(panel_data_label)
 
         # Spacing Columns
-        self.spacing_columns = FCEntry()
+        self.spacing_columns = FCDoubleSpinner()
+        self.spacing_columns.set_range(0, 9999)
+        self.spacing_columns.set_precision(4)
+
         self.spacing_columns_label = QtWidgets.QLabel('%s:' % _("Spacing cols"))
         self.spacing_columns_label.setToolTip(
             _("Spacing between columns of the desired panel.\n"
@@ -152,7 +154,10 @@ class Panelize(FlatCAMTool):
         form_layout.addRow(self.spacing_columns_label, self.spacing_columns)
 
         # Spacing Rows
-        self.spacing_rows = FCEntry()
+        self.spacing_rows = FCDoubleSpinner()
+        self.spacing_rows.set_range(0, 9999)
+        self.spacing_rows.set_precision(4)
+
         self.spacing_rows_label = QtWidgets.QLabel('%s:' % _("Spacing rows"))
         self.spacing_rows_label.setToolTip(
             _("Spacing between rows of the desired panel.\n"
@@ -161,7 +166,9 @@ class Panelize(FlatCAMTool):
         form_layout.addRow(self.spacing_rows_label, self.spacing_rows)
 
         # Columns
-        self.columns = FCEntry()
+        self.columns = FCSpinner()
+        self.columns.set_range(0, 9999)
+
         self.columns_label = QtWidgets.QLabel('%s:' % _("Columns"))
         self.columns_label.setToolTip(
             _("Number of columns of the desired panel")
@@ -169,7 +176,9 @@ class Panelize(FlatCAMTool):
         form_layout.addRow(self.columns_label, self.columns)
 
         # Rows
-        self.rows = FCEntry()
+        self.rows = FCSpinner()
+        self.rows.set_range(0, 9999)
+
         self.rows_label = QtWidgets.QLabel('%s:' % _("Rows"))
         self.rows_label.setToolTip(
             _("Number of rows of the desired panel")
@@ -200,7 +209,10 @@ class Panelize(FlatCAMTool):
         )
         form_layout.addRow(self.constrain_cb)
 
-        self.x_width_entry = FCEntry()
+        self.x_width_entry = FCDoubleSpinner()
+        self.x_width_entry.set_precision(4)
+        self.x_width_entry.set_range(0, 9999)
+
         self.x_width_lbl = QtWidgets.QLabel('%s:' % _("Width (DX)"))
         self.x_width_lbl.setToolTip(
             _("The width (DX) within which the panel must fit.\n"
@@ -208,7 +220,10 @@ class Panelize(FlatCAMTool):
         )
         form_layout.addRow(self.x_width_lbl, self.x_width_entry)
 
-        self.y_height_entry = FCEntry()
+        self.y_height_entry = FCDoubleSpinner()
+        self.y_height_entry.set_range(0, 9999)
+        self.y_height_entry.set_precision(4)
+
         self.y_height_lbl = QtWidgets.QLabel('%s:' % _("Height (DY)"))
         self.y_height_lbl.setToolTip(
             _("The height (DY)within which the panel must fit.\n"
@@ -386,77 +401,20 @@ class Panelize(FlatCAMTool):
 
         self.outname = name + '_panelized'
 
-        try:
-            spacing_columns = float(self.spacing_columns.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                spacing_columns = float(self.spacing_columns.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
+        spacing_columns = float(self.spacing_columns.get_value())
         spacing_columns = spacing_columns if spacing_columns is not None else 0
 
-        try:
-            spacing_rows = float(self.spacing_rows.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                spacing_rows = float(self.spacing_rows.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
+        spacing_rows = float(self.spacing_rows.get_value())
         spacing_rows = spacing_rows if spacing_rows is not None else 0
 
-        try:
-            rows = int(self.rows.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                rows = float(self.rows.get_value().replace(',', '.'))
-                rows = int(rows)
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
+        rows = int(self.rows.get_value())
         rows = rows if rows is not None else 1
 
-        try:
-            columns = int(self.columns.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                columns = float(self.columns.get_value().replace(',', '.'))
-                columns = int(columns)
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
+        columns = int(self.columns.get_value())
         columns = columns if columns is not None else 1
 
-        try:
-            constrain_dx = float(self.x_width_entry.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                constrain_dx = float(self.x_width_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
-
-        try:
-            constrain_dy = float(self.y_height_entry.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                constrain_dy = float(self.y_height_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
+        constrain_dx = float(self.x_width_entry.get_value())
+        constrain_dy = float(self.y_height_entry.get_value())
 
         panel_type = str(self.panel_type_radio.get_value())
 

+ 2 - 3
flatcamTools/ToolPcbWizard.py

@@ -1,10 +1,9 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 4/15/2019                                          #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 from FlatCAMTool import FlatCAMTool
 

+ 2 - 3
flatcamTools/ToolProperties.py

@@ -1,10 +1,9 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5.QtCore import Qt

+ 1599 - 0
flatcamTools/ToolRulesCheck.py

@@ -0,0 +1,1599 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Date: 09/27/2019                                         #
+# MIT Licence                                              #
+# ##########################################################
+
+from FlatCAMTool import FlatCAMTool
+from copy import copy, deepcopy
+from ObjectCollection import *
+import time
+from FlatCAMPool import *
+from os import getpid
+from shapely.ops import nearest_points
+from shapely.geometry.base import BaseGeometry
+
+import gettext
+import FlatCAMTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class RulesCheck(FlatCAMTool):
+
+    toolName = _("Check Rules")
+
+    tool_finished = pyqtSignal(list)
+
+    def __init__(self, app):
+        super(RulesCheck, self).__init__(self)
+        self.app = app
+        self.decimals = 4
+
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                        QLabel
+                        {
+                            font-size: 16px;
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(title_label)
+
+        # Form Layout
+        self.grid_layout = QtWidgets.QGridLayout()
+        self.layout.addLayout(self.grid_layout)
+
+        self.grid_layout.setColumnStretch(0, 0)
+        self.grid_layout.setColumnStretch(1, 3)
+        self.grid_layout.setColumnStretch(2, 0)
+
+        self.gerber_title_lbl = QtWidgets.QLabel('<b>%s</b>:' % _("Gerber Files"))
+        self.gerber_title_lbl.setToolTip(
+            _("Gerber objects for which to check rules.")
+        )
+
+        self.all_obj_cb = FCCheckBox()
+
+        self.grid_layout.addWidget(self.gerber_title_lbl, 0, 0, 1, 2)
+        self.grid_layout.addWidget(self.all_obj_cb, 0, 2)
+
+        # Copper Top object
+        self.copper_t_object = QtWidgets.QComboBox()
+        self.copper_t_object.setModel(self.app.collection)
+        self.copper_t_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.copper_t_object.setCurrentIndex(1)
+
+        self.copper_t_object_lbl = QtWidgets.QLabel('%s:' % _("Top"))
+        self.copper_t_object_lbl.setToolTip(
+            _("The Top Gerber Copper object for which rules are checked.")
+        )
+
+        self.copper_t_cb = FCCheckBox()
+
+        self.grid_layout.addWidget(self.copper_t_object_lbl, 1, 0)
+        self.grid_layout.addWidget(self.copper_t_object, 1, 1)
+        self.grid_layout.addWidget(self.copper_t_cb, 1, 2)
+
+        # Copper Bottom object
+        self.copper_b_object = QtWidgets.QComboBox()
+        self.copper_b_object.setModel(self.app.collection)
+        self.copper_b_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.copper_b_object.setCurrentIndex(1)
+
+        self.copper_b_object_lbl = QtWidgets.QLabel('%s:' % _("Bottom"))
+        self.copper_b_object_lbl.setToolTip(
+            _("The Bottom Gerber Copper object for which rules are checked.")
+        )
+
+        self.copper_b_cb = FCCheckBox()
+
+        self.grid_layout.addWidget(self.copper_b_object_lbl, 2, 0)
+        self.grid_layout.addWidget(self.copper_b_object, 2, 1)
+        self.grid_layout.addWidget(self.copper_b_cb, 2, 2)
+
+        # SolderMask Top object
+        self.sm_t_object = QtWidgets.QComboBox()
+        self.sm_t_object.setModel(self.app.collection)
+        self.sm_t_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.sm_t_object.setCurrentIndex(1)
+
+        self.sm_t_object_lbl = QtWidgets.QLabel('%s:' % _("SM Top"))
+        self.sm_t_object_lbl.setToolTip(
+            _("The Top Gerber Solder Mask object for which rules are checked.")
+        )
+
+        self.sm_t_cb = FCCheckBox()
+
+        self.grid_layout.addWidget(self.sm_t_object_lbl, 3, 0)
+        self.grid_layout.addWidget(self.sm_t_object, 3, 1)
+        self.grid_layout.addWidget(self.sm_t_cb, 3, 2)
+
+        # SolderMask Bottom object
+        self.sm_b_object = QtWidgets.QComboBox()
+        self.sm_b_object.setModel(self.app.collection)
+        self.sm_b_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.sm_b_object.setCurrentIndex(1)
+
+        self.sm_b_object_lbl = QtWidgets.QLabel('%s:' % _("SM Bottom"))
+        self.sm_b_object_lbl.setToolTip(
+            _("The Bottom Gerber Solder Mask object for which rules are checked.")
+        )
+
+        self.sm_b_cb = FCCheckBox()
+
+        self.grid_layout.addWidget(self.sm_b_object_lbl, 4, 0)
+        self.grid_layout.addWidget(self.sm_b_object, 4, 1)
+        self.grid_layout.addWidget(self.sm_b_cb, 4, 2)
+
+        # SilkScreen Top object
+        self.ss_t_object = QtWidgets.QComboBox()
+        self.ss_t_object.setModel(self.app.collection)
+        self.ss_t_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.ss_t_object.setCurrentIndex(1)
+
+        self.ss_t_object_lbl = QtWidgets.QLabel('%s:' % _("Silk Top"))
+        self.ss_t_object_lbl.setToolTip(
+            _("The Top Gerber Silkscreen object for which rules are checked.")
+        )
+
+        self.ss_t_cb = FCCheckBox()
+
+        self.grid_layout.addWidget(self.ss_t_object_lbl, 5, 0)
+        self.grid_layout.addWidget(self.ss_t_object, 5, 1)
+        self.grid_layout.addWidget(self.ss_t_cb, 5, 2)
+
+        # SilkScreen Bottom object
+        self.ss_b_object = QtWidgets.QComboBox()
+        self.ss_b_object.setModel(self.app.collection)
+        self.ss_b_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.ss_b_object.setCurrentIndex(1)
+
+        self.ss_b_object_lbl = QtWidgets.QLabel('%s:' % _("Silk Bottom"))
+        self.ss_b_object_lbl.setToolTip(
+            _("The Bottom Gerber Silkscreen object for which rules are checked.")
+        )
+
+        self.ss_b_cb = FCCheckBox()
+
+        self.grid_layout.addWidget(self.ss_b_object_lbl, 6, 0)
+        self.grid_layout.addWidget(self.ss_b_object, 6, 1)
+        self.grid_layout.addWidget(self.ss_b_cb, 6, 2)
+
+        # Outline object
+        self.outline_object = QtWidgets.QComboBox()
+        self.outline_object.setModel(self.app.collection)
+        self.outline_object.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.outline_object.setCurrentIndex(1)
+
+        self.outline_object_lbl = QtWidgets.QLabel('%s:' % _("Outline"))
+        self.outline_object_lbl.setToolTip(
+            _("The Gerber Outline (Cutout) object for which rules are checked.")
+        )
+
+        self.out_cb = FCCheckBox()
+
+        self.grid_layout.addWidget(self.outline_object_lbl, 7, 0)
+        self.grid_layout.addWidget(self.outline_object, 7, 1)
+        self.grid_layout.addWidget(self.out_cb, 7, 2)
+
+        self.grid_layout.addWidget(QtWidgets.QLabel(""), 8, 0, 1, 3)
+
+        self.excellon_title_lbl = QtWidgets.QLabel('<b>%s</b>:' % _("Excellon Objects"))
+        self.excellon_title_lbl.setToolTip(
+            _("Excellon objects for which to check rules.")
+        )
+
+        self.grid_layout.addWidget(self.excellon_title_lbl, 9, 0, 1, 3)
+
+        # Excellon 1 object
+        self.e1_object = QtWidgets.QComboBox()
+        self.e1_object.setModel(self.app.collection)
+        self.e1_object.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
+        self.e1_object.setCurrentIndex(1)
+
+        self.e1_object_lbl = QtWidgets.QLabel('%s:' % _("Excellon 1"))
+        self.e1_object_lbl.setToolTip(
+            _("Excellon object for which to check rules.\n"
+              "Holds the plated holes or a general Excellon file content.")
+        )
+
+        self.e1_cb = FCCheckBox()
+
+        self.grid_layout.addWidget(self.e1_object_lbl, 10, 0)
+        self.grid_layout.addWidget(self.e1_object, 10, 1)
+        self.grid_layout.addWidget(self.e1_cb, 10, 2)
+
+        # Excellon 2 object
+        self.e2_object = QtWidgets.QComboBox()
+        self.e2_object.setModel(self.app.collection)
+        self.e2_object.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
+        self.e2_object.setCurrentIndex(1)
+
+        self.e2_object_lbl = QtWidgets.QLabel('%s:' % _("Excellon 2"))
+        self.e2_object_lbl.setToolTip(
+            _("Excellon object for which to check rules.\n"
+              "Holds the non-plated holes.")
+        )
+
+        self.e2_cb = FCCheckBox()
+
+        self.grid_layout.addWidget(self.e2_object_lbl, 11, 0)
+        self.grid_layout.addWidget(self.e2_object, 11, 1)
+        self.grid_layout.addWidget(self.e2_cb, 11, 2)
+
+        self.grid_layout.addWidget(QtWidgets.QLabel(""), 12, 0, 1, 3)
+
+        # Control All
+        self.all_cb = FCCheckBox('%s' % _("All Rules"))
+        self.all_cb.setToolTip(
+            _("This check/uncheck all the rules below.")
+        )
+        self.all_cb.setStyleSheet(
+            """
+            QCheckBox {font-weight: bold; color: green}
+            """
+        )
+        self.layout.addWidget(self.all_cb)
+
+        # Form Layout
+        self.form_layout_1 = QtWidgets.QFormLayout()
+        self.layout.addLayout(self.form_layout_1)
+
+        self.form_layout_1.addRow(QtWidgets.QLabel(""))
+
+        # Trace size
+        self.trace_size_cb = FCCheckBox('%s:' % _("Trace Size"))
+        self.trace_size_cb.setToolTip(
+            _("This checks if the minimum size for traces is met.")
+        )
+        self.form_layout_1.addRow(self.trace_size_cb)
+
+        # Trace size value
+        self.trace_size_entry = FCDoubleSpinner()
+        self.trace_size_entry.set_range(0.00001, 999.99999)
+        self.trace_size_entry.set_precision(self.decimals)
+        self.trace_size_entry.setSingleStep(0.1)
+
+        self.trace_size_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
+        self.trace_size_lbl.setToolTip(
+            _("Minimum acceptable trace size.")
+        )
+        self.form_layout_1.addRow(self.trace_size_lbl, self.trace_size_entry)
+
+        self.ts = OptionalInputSection(self.trace_size_cb, [self.trace_size_lbl, self.trace_size_entry])
+
+        # Copper2copper clearance
+        self.clearance_copper2copper_cb = FCCheckBox('%s:' % _("Copper to Copper clearance"))
+        self.clearance_copper2copper_cb.setToolTip(
+            _("This checks if the minimum clearance between copper\n"
+              "features is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_copper2copper_cb)
+
+        # Copper2copper clearance value
+        self.clearance_copper2copper_entry = FCDoubleSpinner()
+        self.clearance_copper2copper_entry.set_range(0.00001, 999.99999)
+        self.clearance_copper2copper_entry.set_precision(self.decimals)
+        self.clearance_copper2copper_entry.setSingleStep(0.1)
+
+        self.clearance_copper2copper_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
+        self.clearance_copper2copper_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.clearance_copper2copper_lbl, self.clearance_copper2copper_entry)
+
+        self.c2c = OptionalInputSection(
+            self.clearance_copper2copper_cb, [self.clearance_copper2copper_lbl, self.clearance_copper2copper_entry])
+
+        # Copper2outline clearance
+        self.clearance_copper2ol_cb = FCCheckBox('%s:' % _("Copper to Outline clearance"))
+        self.clearance_copper2ol_cb.setToolTip(
+            _("This checks if the minimum clearance between copper\n"
+              "features and the outline is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_copper2ol_cb)
+
+        # Copper2outline clearance value
+        self.clearance_copper2ol_entry = FCDoubleSpinner()
+        self.clearance_copper2ol_entry.set_range(0.00001, 999.99999)
+        self.clearance_copper2ol_entry.set_precision(self.decimals)
+        self.clearance_copper2ol_entry.setSingleStep(0.1)
+
+        self.clearance_copper2ol_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
+        self.clearance_copper2ol_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.clearance_copper2ol_lbl, self.clearance_copper2ol_entry)
+
+        self.c2ol = OptionalInputSection(
+            self.clearance_copper2ol_cb, [self.clearance_copper2ol_lbl, self.clearance_copper2ol_entry])
+
+        # Silkscreen2silkscreen clearance
+        self.clearance_silk2silk_cb = FCCheckBox('%s:' % _("Silk to Silk Clearance"))
+        self.clearance_silk2silk_cb.setToolTip(
+            _("This checks if the minimum clearance between silkscreen\n"
+              "features and silkscreen features is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_silk2silk_cb)
+
+        # Copper2silkscreen clearance value
+        self.clearance_silk2silk_entry = FCDoubleSpinner()
+        self.clearance_silk2silk_entry.set_range(0.00001, 999.99999)
+        self.clearance_silk2silk_entry.set_precision(self.decimals)
+        self.clearance_silk2silk_entry.setSingleStep(0.1)
+
+        self.clearance_silk2silk_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
+        self.clearance_silk2silk_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.clearance_silk2silk_lbl, self.clearance_silk2silk_entry)
+
+        self.s2s = OptionalInputSection(
+            self.clearance_silk2silk_cb, [self.clearance_silk2silk_lbl, self.clearance_silk2silk_entry])
+
+        # Silkscreen2soldermask clearance
+        self.clearance_silk2sm_cb = FCCheckBox('%s:' % _("Silk to Solder Mask Clearance"))
+        self.clearance_silk2sm_cb.setToolTip(
+            _("This checks if the minimum clearance between silkscreen\n"
+              "features and soldermask features is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_silk2sm_cb)
+
+        # Silkscreen2soldermask clearance value
+        self.clearance_silk2sm_entry = FCDoubleSpinner()
+        self.clearance_silk2sm_entry.set_range(0.00001, 999.99999)
+        self.clearance_silk2sm_entry.set_precision(self.decimals)
+        self.clearance_silk2sm_entry.setSingleStep(0.1)
+
+        self.clearance_silk2sm_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
+        self.clearance_silk2sm_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.clearance_silk2sm_lbl, self.clearance_silk2sm_entry)
+
+        self.s2sm = OptionalInputSection(
+            self.clearance_silk2sm_cb, [self.clearance_silk2sm_lbl, self.clearance_silk2sm_entry])
+
+        # Silk2outline clearance
+        self.clearance_silk2ol_cb = FCCheckBox('%s:' % _("Silk to Outline Clearance"))
+        self.clearance_silk2ol_cb.setToolTip(
+            _("This checks if the minimum clearance between silk\n"
+              "features and the outline is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_silk2ol_cb)
+
+        # Silk2outline clearance value
+        self.clearance_silk2ol_entry = FCDoubleSpinner()
+        self.clearance_silk2ol_entry.set_range(0.00001, 999.99999)
+        self.clearance_silk2ol_entry.set_precision(self.decimals)
+        self.clearance_silk2ol_entry.setSingleStep(0.1)
+
+        self.clearance_silk2ol_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
+        self.clearance_silk2ol_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.clearance_silk2ol_lbl, self.clearance_silk2ol_entry)
+
+        self.s2ol = OptionalInputSection(
+            self.clearance_silk2ol_cb, [self.clearance_silk2ol_lbl, self.clearance_silk2ol_entry])
+
+        # Soldermask2soldermask clearance
+        self.clearance_sm2sm_cb = FCCheckBox('%s:' % _("Minimum Solder Mask Sliver"))
+        self.clearance_sm2sm_cb.setToolTip(
+            _("This checks if the minimum clearance between soldermask\n"
+              "features and soldermask features is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_sm2sm_cb)
+
+        # Soldermask2soldermask clearance value
+        self.clearance_sm2sm_entry = FCDoubleSpinner()
+        self.clearance_sm2sm_entry.set_range(0.00001, 999.99999)
+        self.clearance_sm2sm_entry.set_precision(self.decimals)
+        self.clearance_sm2sm_entry.setSingleStep(0.1)
+
+        self.clearance_sm2sm_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
+        self.clearance_sm2sm_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.clearance_sm2sm_lbl, self.clearance_sm2sm_entry)
+
+        self.sm2sm = OptionalInputSection(
+            self.clearance_sm2sm_cb, [self.clearance_sm2sm_lbl, self.clearance_sm2sm_entry])
+
+        # Ring integrity check
+        self.ring_integrity_cb = FCCheckBox('%s:' % _("Minimum Annular Ring"))
+        self.ring_integrity_cb.setToolTip(
+            _("This checks if the minimum copper ring left by drilling\n"
+              "a hole into a pad is met.")
+        )
+        self.form_layout_1.addRow(self.ring_integrity_cb)
+
+        # Ring integrity value
+        self.ring_integrity_entry = FCDoubleSpinner()
+        self.ring_integrity_entry.set_range(0.00001, 999.99999)
+        self.ring_integrity_entry.set_precision(self.decimals)
+        self.ring_integrity_entry.setSingleStep(0.1)
+
+        self.ring_integrity_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
+        self.ring_integrity_lbl.setToolTip(
+            _("Minimum acceptable ring value.")
+        )
+        self.form_layout_1.addRow(self.ring_integrity_lbl, self.ring_integrity_entry)
+
+        self.anr = OptionalInputSection(
+            self.ring_integrity_cb, [self.ring_integrity_lbl, self.ring_integrity_entry])
+
+        self.form_layout_1.addRow(QtWidgets.QLabel(""))
+
+        # Hole2Hole clearance
+        self.clearance_d2d_cb = FCCheckBox('%s:' % _("Hole to Hole Clearance"))
+        self.clearance_d2d_cb.setToolTip(
+            _("This checks if the minimum clearance between a drill hole\n"
+              "and another drill hole is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_d2d_cb)
+
+        # Hole2Hole clearance value
+        self.clearance_d2d_entry = FCDoubleSpinner()
+        self.clearance_d2d_entry.set_range(0.00001, 999.99999)
+        self.clearance_d2d_entry.set_precision(self.decimals)
+        self.clearance_d2d_entry.setSingleStep(0.1)
+
+        self.clearance_d2d_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
+        self.clearance_d2d_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.clearance_d2d_lbl, self.clearance_d2d_entry)
+
+        self.d2d = OptionalInputSection(
+            self.clearance_d2d_cb, [self.clearance_d2d_lbl, self.clearance_d2d_entry])
+
+        # Drill holes size check
+        self.drill_size_cb = FCCheckBox('%s:' % _("Hole Size"))
+        self.drill_size_cb.setToolTip(
+            _("This checks if the drill holes\n"
+              "sizes are above the threshold.")
+        )
+        self.form_layout_1.addRow(self.drill_size_cb)
+
+        # Drile holes value
+        self.drill_size_entry = FCDoubleSpinner()
+        self.drill_size_entry.set_range(0.00001, 999.99999)
+        self.drill_size_entry.set_precision(self.decimals)
+        self.drill_size_entry.setSingleStep(0.1)
+
+        self.drill_size_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
+        self.drill_size_lbl.setToolTip(
+            _("Minimum acceptable drill size.")
+        )
+        self.form_layout_1.addRow(self.drill_size_lbl, self.drill_size_entry)
+
+        self.ds = OptionalInputSection(
+            self.drill_size_cb, [self.drill_size_lbl, self.drill_size_entry])
+
+        # Buttons
+        hlay_2 = QtWidgets.QHBoxLayout()
+        self.layout.addLayout(hlay_2)
+
+        # hlay_2.addStretch()
+        self.run_button = QtWidgets.QPushButton(_("Run Rules Check"))
+        self.run_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.run_button)
+
+        self.layout.addStretch()
+
+        # #######################################################
+        # ################ SIGNALS ##############################
+        # #######################################################
+        self.copper_t_cb.stateChanged.connect(lambda st: self.copper_t_object.setDisabled(not st))
+        self.copper_b_cb.stateChanged.connect(lambda st: self.copper_b_object.setDisabled(not st))
+
+        self.sm_t_cb.stateChanged.connect(lambda st: self.sm_t_object.setDisabled(not st))
+        self.sm_b_cb.stateChanged.connect(lambda st: self.sm_b_object.setDisabled(not st))
+
+        self.ss_t_cb.stateChanged.connect(lambda st: self.ss_t_object.setDisabled(not st))
+        self.ss_b_cb.stateChanged.connect(lambda st: self.ss_b_object.setDisabled(not st))
+
+        self.out_cb.stateChanged.connect(lambda st: self.outline_object.setDisabled(not st))
+
+        self.e1_cb.stateChanged.connect(lambda st: self.e1_object.setDisabled(not st))
+        self.e2_cb.stateChanged.connect(lambda st: self.e2_object.setDisabled(not st))
+
+        self.all_obj_cb.stateChanged.connect(self.on_all_objects_cb_changed)
+        self.all_cb.stateChanged.connect(self.on_all_cb_changed)
+        self.run_button.clicked.connect(self.execute)
+        # self.app.collection.rowsInserted.connect(self.on_object_loaded)
+
+        self.tool_finished.connect(self.on_tool_finished)
+
+        # 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
+
+        # Multiprocessing Process Pool
+        self.pool = self.app.pool
+        self.results = None
+
+        self.decimals = 4
+
+    # def on_object_loaded(self, index, row):
+    #     print(index.internalPointer().child_items[row].obj.options['name'], index.data())
+
+    def on_all_cb_changed(self, state):
+        cb_items = [self.form_layout_1.itemAt(i).widget() for i in range(self.form_layout_1.count())
+                    if isinstance(self.form_layout_1.itemAt(i).widget(), FCCheckBox)]
+
+        for cb in cb_items:
+            if state:
+                cb.setChecked(True)
+            else:
+                cb.setChecked(False)
+
+    def on_all_objects_cb_changed(self, state):
+        cb_items = [self.grid_layout.itemAt(i).widget() for i in range(self.grid_layout.count())
+                    if isinstance(self.grid_layout.itemAt(i).widget(), FCCheckBox)]
+
+        for cb in cb_items:
+            if state:
+                cb.setChecked(True)
+            else:
+                cb.setChecked(False)
+
+    def run(self, toggle=True):
+        self.app.report_usage("ToolRulesCheck()")
+
+        if toggle:
+            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+            else:
+                try:
+                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
+                except AttributeError:
+                    pass
+        else:
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+
+        FlatCAMTool.run(self)
+        self.set_tool_ui()
+
+        self.app.ui.notebook.setTabText(2, _("Rules Tool"))
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+R', **kwargs)
+
+    def set_tool_ui(self):
+
+        # all object combobox default as disabled
+        self.copper_t_object.setDisabled(True)
+        self.copper_b_object.setDisabled(True)
+
+        self.sm_t_object.setDisabled(True)
+        self.sm_b_object.setDisabled(True)
+
+        self.ss_t_object.setDisabled(True)
+        self.ss_b_object.setDisabled(True)
+
+        self.outline_object.setDisabled(True)
+
+        self.e1_object.setDisabled(True)
+        self.e2_object.setDisabled(True)
+
+        self.trace_size_cb.set_value(self.app.defaults["tools_cr_trace_size"])
+        self.trace_size_entry.set_value(float(self.app.defaults["tools_cr_trace_size_val"]))
+        self.clearance_copper2copper_cb.set_value(self.app.defaults["tools_cr_c2c"])
+        self.clearance_copper2copper_entry.set_value(float(self.app.defaults["tools_cr_c2c_val"]))
+        self.clearance_copper2ol_cb.set_value(self.app.defaults["tools_cr_c2o"])
+        self.clearance_copper2ol_entry.set_value(float(self.app.defaults["tools_cr_c2o_val"]))
+        self.clearance_silk2silk_cb.set_value(self.app.defaults["tools_cr_s2s"])
+        self.clearance_silk2silk_entry.set_value(float(self.app.defaults["tools_cr_s2s_val"]))
+        self.clearance_silk2sm_cb.set_value(self.app.defaults["tools_cr_s2sm"])
+        self.clearance_silk2sm_entry.set_value(float(self.app.defaults["tools_cr_s2sm_val"]))
+        self.clearance_silk2ol_cb.set_value(self.app.defaults["tools_cr_s2o"])
+        self.clearance_silk2ol_entry.set_value(float(self.app.defaults["tools_cr_s2o_val"]))
+        self.clearance_sm2sm_cb.set_value(self.app.defaults["tools_cr_sm2sm"])
+        self.clearance_sm2sm_entry.set_value(float(self.app.defaults["tools_cr_sm2sm_val"]))
+        self.ring_integrity_cb.set_value(self.app.defaults["tools_cr_ri"])
+        self.ring_integrity_entry.set_value(float(self.app.defaults["tools_cr_ri_val"]))
+        self.clearance_d2d_cb.set_value(self.app.defaults["tools_cr_h2h"])
+        self.clearance_d2d_entry.set_value(float(self.app.defaults["tools_cr_h2h_val"]))
+        self.drill_size_cb.set_value(self.app.defaults["tools_cr_dh"])
+        self.drill_size_entry.set_value(float(self.app.defaults["tools_cr_dh_val"]))
+
+        self.reset_fields()
+
+    @staticmethod
+    def check_inside_gerber_clearance(gerber_obj, size, rule):
+        log.debug("RulesCheck.check_inside_gerber_clearance()")
+
+        rule_title = rule
+
+        violations = list()
+        obj_violations = dict()
+        obj_violations.update({
+            'name': '',
+            'points': list()
+        })
+
+        if not gerber_obj:
+            return 'Fail. Not enough Gerber objects to check Gerber 2 Gerber clearance'
+
+        obj_violations['name'] = gerber_obj['name']
+
+        solid_geo = list()
+        clear_geo = list()
+        for apid in gerber_obj['apertures']:
+            if 'geometry' in gerber_obj['apertures'][apid]:
+                geometry = gerber_obj['apertures'][apid]['geometry']
+                for geo_el in geometry:
+                    if 'solid' in geo_el and geo_el['solid'] is not None:
+                        solid_geo.append(geo_el['solid'])
+                    if 'clear' in geo_el and geo_el['clear'] is not None:
+                        clear_geo.append(geo_el['clear'])
+
+        if clear_geo:
+            total_geo = list()
+            for geo_c in clear_geo:
+                for geo_s in solid_geo:
+                    if geo_c.within(geo_s):
+                        total_geo.append(geo_s.difference(geo_c))
+        else:
+            total_geo = MultiPolygon(solid_geo)
+            total_geo = total_geo.buffer(0.000001)
+
+        if isinstance(total_geo, Polygon):
+            obj_violations['points'] = ['Failed. Only one polygon.']
+            return rule_title, [obj_violations]
+        else:
+            iterations = len(total_geo)
+            iterations = (iterations * (iterations - 1)) / 2
+        log.debug("RulesCheck.check_gerber_clearance(). Iterations: %s" % str(iterations))
+
+        min_dict = dict()
+        idx = 1
+        for geo in total_geo:
+            for s_geo in total_geo[idx:]:
+                # minimize the number of distances by not taking into considerations those that are too small
+                dist = geo.distance(s_geo)
+                if float(dist) < float(size):
+                    loc_1, loc_2 = nearest_points(geo, s_geo)
+
+                    dx = loc_1.x - loc_2.x
+                    dy = loc_1.y - loc_2.y
+                    loc = min(loc_1.x, loc_2.x) + (abs(dx) / 2), min(loc_1.y, loc_2.y) + (abs(dy) / 2)
+
+                    if dist in min_dict:
+                        min_dict[dist].append(loc)
+                    else:
+                        min_dict[dist] = [loc]
+            idx += 1
+        points_list = set()
+        for dist in min_dict.keys():
+            for location in min_dict[dist]:
+                points_list.add(location)
+
+        obj_violations['points'] = list(points_list)
+        violations.append(deepcopy(obj_violations))
+
+        return rule_title, violations
+
+    @staticmethod
+    def check_gerber_clearance(gerber_list, size, rule):
+        log.debug("RulesCheck.check_gerber_clearance()")
+        rule_title = rule
+
+        violations = list()
+        obj_violations = dict()
+        obj_violations.update({
+            'name': '',
+            'points': list()
+        })
+
+        if len(gerber_list) == 2:
+            gerber_1 = gerber_list[0]
+            # added it so I won't have errors of using before declaring
+            gerber_2 = dict()
+
+            gerber_3 = gerber_list[1]
+        elif len(gerber_list) == 3:
+            gerber_1 = gerber_list[0]
+            gerber_2 = gerber_list[1]
+            gerber_3 = gerber_list[2]
+        else:
+            return 'Fail. Not enough Gerber objects to check Gerber 2 Gerber clearance'
+
+        total_geo_grb_1 = list()
+        for apid in gerber_1['apertures']:
+            if 'geometry' in gerber_1['apertures'][apid]:
+                geometry = gerber_1['apertures'][apid]['geometry']
+                for geo_el in geometry:
+                    if 'solid' in geo_el and geo_el['solid'] is not None:
+                        total_geo_grb_1.append(geo_el['solid'])
+
+        if len(gerber_list) == 3:
+            # add the second Gerber geometry to the first one if it exists
+            for apid in gerber_2['apertures']:
+                if 'geometry' in gerber_2['apertures'][apid]:
+                    geometry = gerber_2['apertures'][apid]['geometry']
+                    for geo_el in geometry:
+                        if 'solid' in geo_el and geo_el['solid'] is not None:
+                            total_geo_grb_1.append(geo_el['solid'])
+
+        total_geo_grb_3 = list()
+        for apid in gerber_3['apertures']:
+            if 'geometry' in gerber_3['apertures'][apid]:
+                geometry = gerber_3['apertures'][apid]['geometry']
+                for geo_el in geometry:
+                    if 'solid' in geo_el and geo_el['solid'] is not None:
+                        total_geo_grb_3.append(geo_el['solid'])
+
+        total_geo_grb_1 = MultiPolygon(total_geo_grb_1)
+        total_geo_grb_1 = total_geo_grb_1.buffer(0)
+
+        total_geo_grb_3 = MultiPolygon(total_geo_grb_3)
+        total_geo_grb_3 = total_geo_grb_3.buffer(0)
+
+        if isinstance(total_geo_grb_1, Polygon):
+            len_1 = 1
+            total_geo_grb_1 = [total_geo_grb_1]
+        else:
+            len_1 = len(total_geo_grb_1)
+
+        if isinstance(total_geo_grb_3, Polygon):
+            len_3 = 1
+            total_geo_grb_3 = [total_geo_grb_3]
+        else:
+            len_3 = len(total_geo_grb_3)
+
+        iterations = len_1 * len_3
+        log.debug("RulesCheck.check_gerber_clearance(). Iterations: %s" % str(iterations))
+
+        min_dict = dict()
+        for geo in total_geo_grb_1:
+            for s_geo in total_geo_grb_3:
+                # minimize the number of distances by not taking into considerations those that are too small
+                dist = geo.distance(s_geo)
+                if float(dist) < float(size):
+                    loc_1, loc_2 = nearest_points(geo, s_geo)
+
+                    dx = loc_1.x - loc_2.x
+                    dy = loc_1.y - loc_2.y
+                    loc = min(loc_1.x, loc_2.x) + (abs(dx) / 2), min(loc_1.y, loc_2.y) + (abs(dy) / 2)
+
+                    if dist in min_dict:
+                        min_dict[dist].append(loc)
+                    else:
+                        min_dict[dist] = [loc]
+
+        points_list = set()
+        for dist in min_dict.keys():
+            for location in min_dict[dist]:
+                points_list.add(location)
+
+        name_list = list()
+        if gerber_1:
+            name_list.append(gerber_1['name'])
+        if gerber_2:
+            name_list.append(gerber_2['name'])
+        if gerber_3:
+            name_list.append(gerber_3['name'])
+
+        obj_violations['name'] = name_list
+        obj_violations['points'] = list(points_list)
+        violations.append(deepcopy(obj_violations))
+
+        return rule_title, violations
+
+    @staticmethod
+    def check_holes_size(elements, size):
+        log.debug("RulesCheck.check_holes_size()")
+
+        rule = _("Hole Size")
+
+        violations = list()
+        obj_violations = dict()
+        obj_violations.update({
+            'name': '',
+            'dia': list()
+        })
+
+        for elem in elements:
+            dia_list = []
+
+            name = elem['name']
+            for tool in elem['tools']:
+                tool_dia = float('%.*f' % (4, float(elem['tools'][tool]['C'])))
+                if tool_dia < float(size):
+                    dia_list.append(tool_dia)
+            obj_violations['name'] = name
+            obj_violations['dia'] = dia_list
+            violations.append(deepcopy(obj_violations))
+
+        return rule, violations
+
+    @staticmethod
+    def check_holes_clearance(elements, size):
+        log.debug("RulesCheck.check_holes_clearance()")
+        rule = _("Hole to Hole Clearance")
+
+        violations = list()
+        obj_violations = dict()
+        obj_violations.update({
+            'name': '',
+            'points': list()
+        })
+
+        total_geo = list()
+        for elem in elements:
+            for tool in elem['tools']:
+                if 'solid_geometry' in elem['tools'][tool]:
+                    geometry = elem['tools'][tool]['solid_geometry']
+                    for geo in geometry:
+                        total_geo.append(geo)
+
+        min_dict = dict()
+        idx = 1
+        for geo in total_geo:
+            for s_geo in total_geo[idx:]:
+
+                # minimize the number of distances by not taking into considerations those that are too small
+                dist = geo.distance(s_geo)
+                loc_1, loc_2 = nearest_points(geo, s_geo)
+
+                dx = loc_1.x - loc_2.x
+                dy = loc_1.y - loc_2.y
+                loc = min(loc_1.x, loc_2.x) + (abs(dx) / 2), min(loc_1.y, loc_2.y) + (abs(dy) / 2)
+
+                if dist in min_dict:
+                    min_dict[dist].append(loc)
+                else:
+                    min_dict[dist] = [loc]
+            idx += 1
+
+        points_list = set()
+        for dist in min_dict.keys():
+            if float(dist) < size:
+                for location in min_dict[dist]:
+                    points_list.add(location)
+
+        name_list = list()
+        for elem in elements:
+            name_list.append(elem['name'])
+
+        obj_violations['name'] = name_list
+        obj_violations['points'] = list(points_list)
+        violations.append(deepcopy(obj_violations))
+
+        return rule, violations
+
+    @staticmethod
+    def check_traces_size(elements, size):
+        log.debug("RulesCheck.check_traces_size()")
+
+        rule = _("Trace Size")
+
+        violations = list()
+        obj_violations = dict()
+        obj_violations.update({
+            'name': '',
+            'size': list(),
+            'points': list()
+        })
+
+        for elem in elements:
+            dia_list = []
+            points_list = []
+            name = elem['name']
+
+            for apid in elem['apertures']:
+                try:
+                    tool_dia = float(elem['apertures'][apid]['size'])
+                    if tool_dia < float(size) and tool_dia != 0.0:
+                        dia_list.append(tool_dia)
+                        for geo_el in elem['apertures'][apid]['geometry']:
+                            if 'solid' in geo_el.keys():
+                                geo = geo_el['solid']
+                                pt = geo.representative_point()
+                                points_list.append((pt.x, pt.y))
+                except Exception as e:
+                    # An exception  will be raised for the 'size' key in case of apertures of type AM (macro) which does
+                    # not have the size key
+                    pass
+
+            obj_violations['name'] = name
+            obj_violations['size'] = dia_list
+            obj_violations['points'] = points_list
+            violations.append(deepcopy(obj_violations))
+        return rule, violations
+
+    @staticmethod
+    def check_gerber_annular_ring(obj_list, size, rule):
+        rule_title = rule
+
+        violations = list()
+        obj_violations = dict()
+        obj_violations.update({
+            'name': '',
+            'points': list()
+        })
+
+        # added it so I won't have errors of using before declaring
+        gerber_obj = dict()
+        gerber_extra_obj = dict()
+        exc_obj = dict()
+        exc_extra_obj = dict()
+
+        if len(obj_list) == 2:
+            gerber_obj = obj_list[0]
+            exc_obj = obj_list[1]
+            if 'apertures' in gerber_obj and 'tools' in exc_obj:
+                pass
+            else:
+                return 'Fail. At least one Gerber and one Excellon object is required to check Minimum Annular Ring'
+        elif len(obj_list) == 3:
+            o1 = obj_list[0]
+            o2 = obj_list[1]
+            o3 = obj_list[2]
+            if 'apertures' in o1 and 'apertures' in o2:
+                gerber_obj = o1
+                gerber_extra_obj = o2
+                exc_obj = o3
+            elif 'tools' in o2 and 'tools' in o3:
+                gerber_obj = o1
+                exc_obj = o2
+                exc_extra_obj = o3
+        elif len(obj_list) == 4:
+            gerber_obj = obj_list[0]
+            gerber_extra_obj = obj_list[1]
+            exc_obj = obj_list[2]
+            exc_extra_obj = obj_list[3]
+        else:
+            return 'Fail. Not enough objects to check Minimum Annular Ring'
+
+        total_geo_grb = list()
+        for apid in gerber_obj['apertures']:
+            if 'geometry' in gerber_obj['apertures'][apid]:
+                geometry = gerber_obj['apertures'][apid]['geometry']
+                for geo_el in geometry:
+                    if 'solid' in geo_el and geo_el['solid'] is not None:
+                        total_geo_grb.append(geo_el['solid'])
+
+        if len(obj_list) == 3 and gerber_extra_obj:
+            # add the second Gerber geometry to the first one if it exists
+            for apid in gerber_extra_obj['apertures']:
+                if 'geometry' in gerber_extra_obj['apertures'][apid]:
+                    geometry = gerber_extra_obj['apertures'][apid]['geometry']
+                    for geo_el in geometry:
+                        if 'solid' in geo_el and geo_el['solid'] is not None:
+                            total_geo_grb.append(geo_el['solid'])
+
+        total_geo_grb = MultiPolygon(total_geo_grb)
+        total_geo_grb = total_geo_grb.buffer(0)
+
+        total_geo_exc = list()
+        for tool in exc_obj['tools']:
+            if 'solid_geometry' in exc_obj['tools'][tool]:
+                geometry = exc_obj['tools'][tool]['solid_geometry']
+                for geo in geometry:
+                    total_geo_exc.append(geo)
+
+        if len(obj_list) == 3 and exc_extra_obj:
+            # add the second Excellon geometry to the first one if it exists
+            for tool in exc_extra_obj['tools']:
+                if 'solid_geometry' in exc_extra_obj['tools'][tool]:
+                    geometry = exc_extra_obj['tools'][tool]['solid_geometry']
+                    for geo in geometry:
+                        total_geo_exc.append(geo)
+
+        if isinstance(total_geo_grb, Polygon):
+            len_1 = 1
+            total_geo_grb = [total_geo_grb]
+        else:
+            len_1 = len(total_geo_grb)
+
+        if isinstance(total_geo_exc, Polygon):
+            len_2 = 1
+            total_geo_exc = [total_geo_exc]
+        else:
+            len_2 = len(total_geo_exc)
+
+        iterations = len_1 * len_2
+        log.debug("RulesCheck.check_gerber_annular_ring(). Iterations: %s" % str(iterations))
+
+        min_dict = dict()
+        dist = None
+        for geo in total_geo_grb:
+            for s_geo in total_geo_exc:
+                try:
+                    # minimize the number of distances by not taking into considerations those that are too small
+                    dist = abs(geo.exterior.distance(s_geo))
+                except Exception as e:
+                    log.debug("RulesCheck.check_gerber_annular_ring() --> %s" % str(e))
+
+                if dist > 0:
+                    if float(dist) < float(size):
+                        loc_1, loc_2 = nearest_points(geo.exterior, s_geo)
+
+                        dx = loc_1.x - loc_2.x
+                        dy = loc_1.y - loc_2.y
+                        loc = min(loc_1.x, loc_2.x) + (abs(dx) / 2), min(loc_1.y, loc_2.y) + (abs(dy) / 2)
+
+                        if dist in min_dict:
+                            min_dict[dist].append(loc)
+                        else:
+                            min_dict[dist] = [loc]
+                else:
+                    if dist in min_dict:
+                        min_dict[dist].append(s_geo.representative_point())
+                    else:
+                        min_dict[dist] = [s_geo.representative_point()]
+
+        points_list = list()
+        for dist in min_dict.keys():
+            for location in min_dict[dist]:
+                points_list.append(location)
+
+        name_list = list()
+        try:
+            if gerber_obj:
+                name_list.append(gerber_obj['name'])
+        except KeyError:
+            pass
+        try:
+            if gerber_extra_obj:
+                name_list.append(gerber_extra_obj['name'])
+        except KeyError:
+            pass
+
+        try:
+            if exc_obj:
+                name_list.append(exc_obj['name'])
+        except KeyError:
+            pass
+
+        try:
+            if exc_extra_obj:
+                name_list.append(exc_extra_obj['name'])
+        except KeyError:
+            pass
+
+        obj_violations['name'] = name_list
+        obj_violations['points'] = points_list
+        violations.append(deepcopy(obj_violations))
+        return rule_title, violations
+
+    def execute(self):
+        self.results = list()
+
+        log.debug("RuleCheck() executing")
+
+        def worker_job(app_obj):
+            self.app.proc_container.new(_("Working..."))
+
+            # RULE: Check Trace Size
+            if self.trace_size_cb.get_value():
+                copper_list = list()
+                copper_name_1 = self.copper_t_object.currentText()
+                if copper_name_1 is not '' and self.copper_t_cb.get_value():
+                    elem_dict = dict()
+                    elem_dict['name'] = deepcopy(copper_name_1)
+                    elem_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_name_1).apertures)
+                    copper_list.append(elem_dict)
+
+                copper_name_2 = self.copper_b_object.currentText()
+                if copper_name_2 is not '' and self.copper_b_cb.get_value():
+                    elem_dict = dict()
+                    elem_dict['name'] = deepcopy(copper_name_2)
+                    elem_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_name_2).apertures)
+                    copper_list.append(elem_dict)
+
+                trace_size = float(self.trace_size_entry.get_value())
+                self.results.append(self.pool.apply_async(self.check_traces_size, args=(copper_list, trace_size)))
+
+            # RULE: Check Copper to Copper Clearance
+            if self.clearance_copper2copper_cb.get_value():
+
+                try:
+                    copper_copper_clearance = float(self.clearance_copper2copper_entry.get_value())
+                except Exception as e:
+                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Copper to Copper clearance"),
+                        _("Value is not valid.")))
+                    return
+
+                if self.copper_t_cb.get_value():
+                    copper_t_obj = self.copper_t_object.currentText()
+                    copper_t_dict = dict()
+
+                    if copper_t_obj is not '':
+                        copper_t_dict['name'] = deepcopy(copper_t_obj)
+                        copper_t_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_t_obj).apertures)
+
+                        self.results.append(self.pool.apply_async(self.check_inside_gerber_clearance,
+                                                                  args=(copper_t_dict,
+                                                                        copper_copper_clearance,
+                                                                        _("TOP -> Copper to Copper clearance"))))
+                if self.copper_b_cb.get_value():
+                    copper_b_obj = self.copper_b_object.currentText()
+                    copper_b_dict = dict()
+                    if copper_b_obj is not '':
+                        copper_b_dict['name'] = deepcopy(copper_b_obj)
+                        copper_b_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_b_obj).apertures)
+
+                        self.results.append(self.pool.apply_async(self.check_inside_gerber_clearance,
+                                                                  args=(copper_b_dict,
+                                                                        copper_copper_clearance,
+                                                                        _("BOTTOM -> Copper to Copper clearance"))))
+
+                if self.copper_t_cb.get_value() is False and self.copper_b_cb.get_value() is False:
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Copper to Copper clearance"),
+                        _("At least one Gerber object has to be selected for this rule but none is selected.")))
+                    return
+
+            # RULE: Check Copper to Outline Clearance
+            if self.clearance_copper2ol_cb.get_value():
+                top_dict = dict()
+                bottom_dict = dict()
+                outline_dict = dict()
+
+                copper_top = self.copper_t_object.currentText()
+                if copper_top is not '' and self.copper_t_cb.get_value():
+                    top_dict['name'] = deepcopy(copper_top)
+                    top_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_top).apertures)
+
+                copper_bottom = self.copper_b_object.currentText()
+                if copper_bottom is not '' and self.copper_b_cb.get_value():
+                    bottom_dict['name'] = deepcopy(copper_bottom)
+                    bottom_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_bottom).apertures)
+
+                copper_outline = self.outline_object.currentText()
+                if copper_outline is not '' and self.out_cb.get_value():
+                    outline_dict['name'] = deepcopy(copper_outline)
+                    outline_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_outline).apertures)
+
+                try:
+                    copper_outline_clearance = float(self.clearance_copper2ol_entry.get_value())
+                except Exception as e:
+                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Copper to Outline clearance"),
+                        _("Value is not valid.")))
+                    return
+
+                if not top_dict and not bottom_dict or not outline_dict:
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Copper to Outline clearance"),
+                        _("One of the copper Gerber objects or the Outline Gerber object is not valid.")))
+                    return
+                objs = []
+                if top_dict:
+                    objs.append(top_dict)
+                if bottom_dict:
+                    objs.append(bottom_dict)
+
+                if outline_dict:
+                    objs.append(outline_dict)
+                else:
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Copper to Outline clearance"),
+                        _("Outline Gerber object presence is mandatory for this rule but it is not selected.")))
+                    return
+
+                self.results.append(self.pool.apply_async(self.check_gerber_clearance,
+                                                          args=(objs,
+                                                                copper_outline_clearance,
+                                                                _("Copper to Outline clearance"))))
+
+            # RULE: Check Silk to Silk Clearance
+            if self.clearance_silk2silk_cb.get_value():
+                silk_dict = dict()
+
+                try:
+                    silk_silk_clearance = float(self.clearance_silk2silk_entry.get_value())
+                except Exception as e:
+                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Silk to Silk clearance"),
+                        _("Value is not valid.")))
+                    return
+
+                if self.ss_t_cb.get_value():
+                    silk_obj = self.ss_t_object.currentText()
+                    if silk_obj is not '':
+                        silk_dict['name'] = deepcopy(silk_obj)
+                        silk_dict['apertures'] = deepcopy(self.app.collection.get_by_name(silk_obj).apertures)
+
+                        self.results.append(self.pool.apply_async(self.check_inside_gerber_clearance,
+                                                                  args=(silk_dict,
+                                                                        silk_silk_clearance,
+                                                                        _("TOP -> Silk to Silk clearance"))))
+                if self.ss_b_cb.get_value():
+                    silk_obj = self.ss_b_object.currentText()
+                    if silk_obj is not '':
+                        silk_dict['name'] = deepcopy(silk_obj)
+                        silk_dict['apertures'] = deepcopy(self.app.collection.get_by_name(silk_obj).apertures)
+
+                        self.results.append(self.pool.apply_async(self.check_inside_gerber_clearance,
+                                                                  args=(silk_dict,
+                                                                        silk_silk_clearance,
+                                                                        _("BOTTOM -> Silk to Silk clearance"))))
+
+                if self.ss_t_cb.get_value() is False and self.ss_b_cb.get_value() is False:
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Silk to Silk clearance"),
+                        _("At least one Gerber object has to be selected for this rule but none is selected.")))
+                    return
+
+            # RULE: Check Silk to Solder Mask Clearance
+            if self.clearance_silk2sm_cb.get_value():
+                silk_t_dict = dict()
+                sm_t_dict = dict()
+                silk_b_dict = dict()
+                sm_b_dict = dict()
+
+                top_ss = False
+                bottom_ss = False
+                top_sm = False
+                bottom_sm = False
+
+                silk_top = self.ss_t_object.currentText()
+                if silk_top is not '' and self.ss_t_cb.get_value():
+                    silk_t_dict['name'] = deepcopy(silk_top)
+                    silk_t_dict['apertures'] = deepcopy(self.app.collection.get_by_name(silk_top).apertures)
+                    top_ss = True
+
+                silk_bottom = self.ss_b_object.currentText()
+                if silk_bottom is not '' and self.ss_b_cb.get_value():
+                    silk_b_dict['name'] = deepcopy(silk_bottom)
+                    silk_b_dict['apertures'] = deepcopy(self.app.collection.get_by_name(silk_bottom).apertures)
+                    bottom_ss = True
+
+                sm_top = self.sm_t_object.currentText()
+                if sm_top is not '' and self.sm_t_cb.get_value():
+                    sm_t_dict['name'] = deepcopy(sm_top)
+                    sm_t_dict['apertures'] = deepcopy(self.app.collection.get_by_name(sm_top).apertures)
+                    top_sm = True
+
+                sm_bottom = self.sm_b_object.currentText()
+                if sm_bottom is not '' and self.sm_b_cb.get_value():
+                    sm_b_dict['name'] = deepcopy(sm_bottom)
+                    sm_b_dict['apertures'] = deepcopy(self.app.collection.get_by_name(sm_bottom).apertures)
+                    bottom_sm = True
+
+                try:
+                    silk_sm_clearance = float(self.clearance_silk2sm_entry.get_value())
+                except Exception as e:
+                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Silk to Solder Mask Clearance"),
+                        _("Value is not valid.")))
+                    return
+
+                if (not silk_t_dict and not silk_b_dict) or (not sm_t_dict and not sm_b_dict):
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Silk to Solder Mask Clearance"),
+                        _("One or more of the Gerber objects is not valid.")))
+                    return
+
+                if top_ss is True and top_sm is True:
+                    objs = [silk_t_dict, sm_t_dict]
+                    self.results.append(self.pool.apply_async(self.check_gerber_clearance,
+                                                              args=(objs,
+                                                                    silk_sm_clearance,
+                                                                    _("TOP -> Silk to Solder Mask Clearance"))))
+                elif bottom_ss is True and bottom_sm is True:
+                    objs = [silk_b_dict, sm_b_dict]
+                    self.results.append(self.pool.apply_async(self.check_gerber_clearance,
+                                                              args=(objs,
+                                                                    silk_sm_clearance,
+                                                                    _("BOTTOM -> Silk to Solder Mask Clearance"))))
+                else:
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Silk to Solder Mask Clearance"),
+                        _("Both Silk and Solder Mask Gerber objects has to be either both Top or both Bottom.")))
+                    return
+
+            # RULE: Check Silk to Outline Clearance
+            if self.clearance_silk2ol_cb.get_value():
+                top_dict = dict()
+                bottom_dict = dict()
+                outline_dict = dict()
+
+                silk_top = self.ss_t_object.currentText()
+                if silk_top is not '' and self.ss_t_cb.get_value():
+                    top_dict['name'] = deepcopy(silk_top)
+                    top_dict['apertures'] = deepcopy(self.app.collection.get_by_name(silk_top).apertures)
+
+                silk_bottom = self.ss_b_object.currentText()
+                if silk_bottom is not '' and self.ss_b_cb.get_value():
+                    bottom_dict['name'] = deepcopy(silk_bottom)
+                    bottom_dict['apertures'] = deepcopy(self.app.collection.get_by_name(silk_bottom).apertures)
+
+                copper_outline = self.outline_object.currentText()
+                if copper_outline is not '' and self.out_cb.get_value():
+                    outline_dict['name'] = deepcopy(copper_outline)
+                    outline_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_outline).apertures)
+
+                try:
+                    copper_outline_clearance = float(self.clearance_copper2ol_entry.get_value())
+                except Exception as e:
+                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Silk to Outline Clearance"),
+                        _("Value is not valid.")))
+                    return
+
+                if not top_dict and not bottom_dict or not outline_dict:
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Silk to Outline Clearance"),
+                        _("One of the Silk Gerber objects or the Outline Gerber object is not valid.")))
+                    return
+
+                objs = []
+                if top_dict:
+                    objs.append(top_dict)
+                if bottom_dict:
+                    objs.append(bottom_dict)
+
+                if outline_dict:
+                    objs.append(outline_dict)
+                else:
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Silk to Outline Clearance"),
+                        _("Outline Gerber object presence is mandatory for this rule but it is not selected.")))
+                    return
+
+                self.results.append(self.pool.apply_async(self.check_gerber_clearance,
+                                                          args=(objs,
+                                                                copper_outline_clearance,
+                                                                _("Silk to Outline Clearance"))))
+
+            # RULE: Check Minimum Solder Mask Sliver
+            if self.clearance_silk2silk_cb.get_value():
+                sm_dict = dict()
+
+                try:
+                    sm_sm_clearance = float(self.clearance_sm2sm_entry.get_value())
+                except Exception as e:
+                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Minimum Solder Mask Sliver"),
+                        _("Value is not valid.")))
+                    return
+
+                if self.sm_t_cb.get_value():
+                    solder_obj = self.sm_t_object.currentText()
+                    if solder_obj is not '':
+                        sm_dict['name'] = deepcopy(solder_obj)
+                        sm_dict['apertures'] = deepcopy(self.app.collection.get_by_name(solder_obj).apertures)
+
+                        self.results.append(self.pool.apply_async(self.check_inside_gerber_clearance,
+                                                                  args=(sm_dict,
+                                                                        sm_sm_clearance,
+                                                                        _("TOP -> Minimum Solder Mask Sliver"))))
+                if self.sm_b_cb.get_value():
+                    solder_obj = self.sm_b_object.currentText()
+                    if solder_obj is not '':
+                        sm_dict['name'] = deepcopy(solder_obj)
+                        sm_dict['apertures'] = deepcopy(self.app.collection.get_by_name(solder_obj).apertures)
+
+                        self.results.append(self.pool.apply_async(self.check_inside_gerber_clearance,
+                                                                  args=(sm_dict,
+                                                                        sm_sm_clearance,
+                                                                        _("BOTTOM -> Minimum Solder Mask Sliver"))))
+
+                if self.sm_t_cb.get_value() is False and self.sm_b_cb.get_value() is False:
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Minimum Solder Mask Sliver"),
+                        _("At least one Gerber object has to be selected for this rule but none is selected.")))
+                    return
+
+            # RULE: Check Minimum Annular Ring
+            if self.ring_integrity_cb.get_value():
+                top_dict = dict()
+                bottom_dict = dict()
+                exc_1_dict = dict()
+                exc_2_dict = dict()
+
+                copper_top = self.copper_t_object.currentText()
+                if copper_top is not '' and self.copper_t_cb.get_value():
+                    top_dict['name'] = deepcopy(copper_top)
+                    top_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_top).apertures)
+
+                copper_bottom = self.copper_b_object.currentText()
+                if copper_bottom is not '' and self.copper_b_cb.get_value():
+                    bottom_dict['name'] = deepcopy(copper_bottom)
+                    bottom_dict['apertures'] = deepcopy(self.app.collection.get_by_name(copper_bottom).apertures)
+
+                excellon_1 = self.e1_object.currentText()
+                if excellon_1 is not '' and self.e1_cb.get_value():
+                    exc_1_dict['name'] = deepcopy(excellon_1)
+                    exc_1_dict['tools'] = deepcopy(
+                        self.app.collection.get_by_name(excellon_1).tools)
+
+                excellon_2 = self.e2_object.currentText()
+                if excellon_2 is not '' and self.e2_cb.get_value():
+                    exc_2_dict['name'] = deepcopy(excellon_2)
+                    exc_2_dict['tools'] = deepcopy(
+                        self.app.collection.get_by_name(excellon_2).tools)
+
+                try:
+                    ring_val = float(self.ring_integrity_entry.get_value())
+                except Exception as e:
+                    log.debug("RulesCheck.execute.worker_job() --> %s" % str(e))
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Minimum Annular Ring"),
+                        _("Value is not valid.")))
+                    return
+
+                if (not top_dict and not bottom_dict) or (not exc_1_dict and not exc_2_dict):
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Minimum Annular Ring"),
+                        _("One of the Copper Gerber objects or the Excellon objects is not valid.")))
+                    return
+
+                objs = []
+                if top_dict:
+                    objs.append(top_dict)
+                elif bottom_dict:
+                    objs.append(bottom_dict)
+
+                if exc_1_dict:
+                    objs.append(exc_1_dict)
+                elif exc_2_dict:
+                    objs.append(exc_2_dict)
+                else:
+                    self.app.inform.emit('[ERROR_NOTCL] %s. %s' % (
+                        _("Minimum Annular Ring"),
+                        _("Excellon object presence is mandatory for this rule but none is selected.")))
+                    return
+
+                self.results.append(self.pool.apply_async(self.check_gerber_annular_ring,
+                                                          args=(objs,
+                                                                ring_val,
+                                                                _("Minimum Annular Ring"))))
+
+            # RULE: Check Hole to Hole Clearance
+            if self.clearance_d2d_cb.get_value():
+                exc_list = list()
+                exc_name_1 = self.e1_object.currentText()
+                if exc_name_1 is not '' and self.e1_cb.get_value():
+                    elem_dict = dict()
+                    elem_dict['name'] = deepcopy(exc_name_1)
+                    elem_dict['tools'] = deepcopy(self.app.collection.get_by_name(exc_name_1).tools)
+                    exc_list.append(elem_dict)
+
+                exc_name_2 = self.e2_object.currentText()
+                if exc_name_2 is not '' and self.e2_cb.get_value():
+                    elem_dict = dict()
+                    elem_dict['name'] = deepcopy(exc_name_2)
+                    elem_dict['tools'] = deepcopy(self.app.collection.get_by_name(exc_name_2).tools)
+                    exc_list.append(elem_dict)
+
+                hole_clearance = float(self.clearance_d2d_entry.get_value())
+                self.results.append(self.pool.apply_async(self.check_holes_clearance, args=(exc_list, hole_clearance)))
+
+            # RULE: Check Holes Size
+            if self.drill_size_cb.get_value():
+                exc_list = list()
+                exc_name_1 = self.e1_object.currentText()
+                if exc_name_1 is not '' and self.e1_cb.get_value():
+                    elem_dict = dict()
+                    elem_dict['name'] = deepcopy(exc_name_1)
+                    elem_dict['tools'] = deepcopy(self.app.collection.get_by_name(exc_name_1).tools)
+                    exc_list.append(elem_dict)
+
+                exc_name_2 = self.e2_object.currentText()
+                if exc_name_2 is not '' and self.e2_cb.get_value():
+                    elem_dict = dict()
+                    elem_dict['name'] = deepcopy(exc_name_2)
+                    elem_dict['tools'] = deepcopy(self.app.collection.get_by_name(exc_name_2).tools)
+                    exc_list.append(elem_dict)
+
+                drill_size = float(self.drill_size_entry.get_value())
+                self.results.append(self.pool.apply_async(self.check_holes_size, args=(exc_list, drill_size)))
+
+            output = list()
+            for p in self.results:
+                output.append(p.get())
+
+            self.tool_finished.emit(output)
+
+            log.debug("RuleCheck() finished")
+
+        self.app.worker_task.emit({'fcn': worker_job, 'params': [self.app]})
+
+    def on_tool_finished(self, res):
+        def init(new_obj, app_obj):
+            txt = ''
+            for el in res:
+                txt += '<b>RULE NAME:</b>&nbsp;&nbsp;&nbsp;&nbsp;%s<BR>' % str(el[0]).upper()
+                if isinstance(el[1][0]['name'], list):
+                    for name in el[1][0]['name']:
+                        txt += 'File name: %s<BR>' % str(name)
+                else:
+                    txt += 'File name: %s<BR>' % str(el[1][0]['name'])
+
+                point_txt = ''
+                try:
+                    if el[1][0]['points']:
+                        txt += '{title}: <span style="color:{color};background-color:{h_color}"' \
+                               '>&nbsp;{status} </span>.<BR>'.format(title=_("STATUS"),
+                                                                     h_color='red',
+                                                                     color='white',
+                                                                     status=_("FAILED"))
+                        if 'Failed' in el[1][0]['points'][0]:
+                            point_txt = el[1][0]['points'][0]
+                        else:
+                            for pt in el[1][0]['points']:
+                                point_txt += '(%.*f, %.*f)' % (self.decimals, float(pt[0]), self.decimals, float(pt[1]))
+                                point_txt += ', '
+                        txt += 'Violations: %s<BR>' % str(point_txt)
+                    else:
+                        txt += '{title}: <span style="color:{color};background-color:{h_color}"' \
+                               '>&nbsp;{status} </span>.<BR>'.format(title=_("STATUS"),
+                                                                     h_color='green',
+                                                                     color='white',
+                                                                     status=_("PASSED"))
+                        txt += '%s<BR>' % _("Violations: There are no violations for the current rule.")
+                except KeyError:
+                    pass
+
+                try:
+                    if el[1][0]['dia']:
+                        txt += '{title}: <span style="color:{color};background-color:{h_color}"' \
+                               '>&nbsp;{status} </span>.<BR>'.format(title=_("STATUS"),
+                                                                     h_color='red',
+                                                                     color='white',
+                                                                     status=_("FAILED"))
+                        if 'Failed' in el[1][0]['dia']:
+                            point_txt = el[1][0]['dia']
+                        else:
+                            for pt in el[1][0]['dia']:
+                                point_txt += '%.*f' % (self.decimals, float(pt))
+                                point_txt += ', '
+                        txt += 'Violations: %s<BR>' % str(point_txt)
+                    else:
+                        txt += '{title}: <span style="color:{color};background-color:{h_color}"' \
+                               '>&nbsp;{status} </span>.<BR>'.format(title=_("STATUS"),
+                                                                     h_color='green',
+                                                                     color='white',
+                                                                     status=_("PASSED"))
+                        txt += '%s<BR>' % _("Violations: There are no violations for the current rule.")
+                except KeyError:
+                    pass
+
+                txt += '<BR><BR>'
+            new_obj.source_file = txt
+            new_obj.read_only = True
+
+        self.app.new_object('document', name='Rules Check results', initialize=init, plot=False)
+
+    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()))
+        pass

+ 2 - 2
flatcamTools/ToolShell.py

@@ -1,10 +1,10 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # Author: Juan Pablo Caram (c)                             #
 # Date: 2/5/2014                                           #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 # from PyQt5.QtCore import pyqtSignal
 from PyQt5.QtCore import Qt

+ 48 - 23
flatcamTools/ToolSolderPaste.py

@@ -1,14 +1,13 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 from FlatCAMTool import FlatCAMTool
 from FlatCAMCommon import LoudDict
-from flatcamGUI.GUIElements import FCComboBox, FCEntry, FCEntry2, FCTable
+from flatcamGUI.GUIElements import FCComboBox, FCEntry, FCEntry2, FCTable, FCInputDialog
 from FlatCAMApp import log
 from camlib import distance
 from FlatCAMObj import FlatCAMCNCjob
@@ -402,6 +401,9 @@ class SolderPaste(FlatCAMTool):
         self.units = ''
         self.name = ""
 
+        # Number of decimals to be used for tools/nozzles in this FlatCAM Tool
+        self.decimals = 4
+
         # this will be used in the combobox context menu, for delete entry
         self.obj_to_be_deleted_name = ''
 
@@ -414,7 +416,7 @@ class SolderPaste(FlatCAMTool):
         # ## Signals
         self.combo_context_del_action.triggered.connect(self.on_delete_object)
         self.addtool_btn.clicked.connect(self.on_tool_add)
-        self.addtool_entry.returnPressed.connect(self.on_tool_add)
+        self.addtool_entry.editingFinished.connect(self.on_tool_add)
         self.deltool_btn.clicked.connect(self.on_tool_delete)
         self.soldergeo_btn.clicked.connect(self.on_create_geo_click)
         self.solder_gcode_btn.clicked.connect(self.on_create_gcode_click)
@@ -457,6 +459,22 @@ class SolderPaste(FlatCAMTool):
     def install(self, icon=None, separator=None, **kwargs):
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+K', **kwargs)
 
+    def on_add_tool_by_key(self):
+        tool_add_popup = FCInputDialog(title='%s...' % _("New Tool"),
+                                       text='%s:' % _('Enter a Tool Diameter'),
+                                       min=0.0000, max=99.9999, decimals=4)
+        tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png'))
+
+        val, ok = tool_add_popup.get_value()
+        if ok:
+            if float(val) == 0:
+                self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                     _("Please enter a tool diameter with non-zero value, in Float format."))
+                return
+            self.on_tool_add(dia=float(val))
+        else:
+            self.app.inform.emit('[WARNING_NOTCL] %s...' % _("Adding Tool cancelled"))
+
     def set_tool_ui(self):
         self.form_fields.update({
             "tools_solderpaste_new": self.addtool_entry,
@@ -499,7 +517,7 @@ class SolderPaste(FlatCAMTool):
             self.tooluid += 1
             self.tooltable_tools.update({
                 int(self.tooluid): {
-                    'tooldia': float('%.4f' % tool_dia),
+                    'tooldia': float('%.*f' % (self.decimals, tool_dia)),
                     'data': deepcopy(self.options),
                     'solid_geometry': []
                 }
@@ -510,6 +528,11 @@ class SolderPaste(FlatCAMTool):
 
         self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
 
+        if self.units == "IN":
+            self.decimals = 4
+        else:
+            self.decimals = 2
+
         for name in list(self.app.postprocessors.keys()):
             # populate only with postprocessor files that start with 'Paste_'
             if name.partition('_')[0] != 'Paste':
@@ -530,7 +553,7 @@ class SolderPaste(FlatCAMTool):
 
         sorted_tools = []
         for k, v in self.tooltable_tools.items():
-            sorted_tools.append(float('%.4f' % float(v['tooldia'])))
+            sorted_tools.append(float('%.*f' % (self.decimals, float(v['tooldia']))))
         sorted_tools.sort(reverse=True)
 
         n = len(sorted_tools)
@@ -539,7 +562,7 @@ class SolderPaste(FlatCAMTool):
 
         for tool_sorted in sorted_tools:
             for tooluid_key, tooluid_value in self.tooltable_tools.items():
-                if float('%.4f' % tooluid_value['tooldia']) == tool_sorted:
+                if float('%.*f' % (self.decimals, tooluid_value['tooldia'])) == tool_sorted:
                     tool_id += 1
                     id = QtWidgets.QTableWidgetItem('%d' % int(tool_id))
                     id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
@@ -547,12 +570,9 @@ class SolderPaste(FlatCAMTool):
                     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('%.4f' % tooluid_value['tooldia'])
+                    # There are no drill bits in MM with more than 2 decimals diameter
+                    # For INCH the decimals should be no more than 4. There are no drills under 10mils
+                    dia = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, tooluid_value['tooldia']))
 
                     dia.setFlags(QtCore.Qt.ItemIsEnabled)
 
@@ -678,7 +698,12 @@ class SolderPaste(FlatCAMTool):
         :param status: what kind of change happened: 'append' or 'delete'
         :return:
         """
-        obj_name = obj.options['name']
+        try:
+            obj_name = obj.options['name']
+        except AttributeError:
+            # this happen when the 'delete all' is emitted since in that case the obj is set to None and None has no
+            # attribute named 'options'
+            return
 
         if status == 'append':
             idx = self.obj_combo.findText(obj_name)
@@ -791,9 +816,9 @@ class SolderPaste(FlatCAMTool):
         for k, v in self.tooltable_tools.items():
             for tool_v in v.keys():
                 if tool_v == 'tooldia':
-                    tool_dias.append(float('%.4f' % v[tool_v]))
+                    tool_dias.append(float('%.*f' % (self.decimals, v[tool_v])))
 
-        if float('%.4f' % tool_dia) in tool_dias:
+        if float('%.*f' % (self.decimals, tool_dia)) in tool_dias:
             if muted is None:
                 self.app.inform.emit('[WARNING_NOTCL] %s' %
                                      _("Adding Nozzle tool cancelled. Tool already in Tool Table."))
@@ -805,7 +830,7 @@ class SolderPaste(FlatCAMTool):
                                      _("New Nozzle tool added to Tool Table."))
             self.tooltable_tools.update({
                 int(self.tooluid): {
-                    'tooldia': float('%.4f' % tool_dia),
+                    'tooldia': float('%.*f' % (self.decimals, tool_dia)),
                     'data': deepcopy(self.options),
                     'solid_geometry': []
                 }
@@ -824,7 +849,7 @@ class SolderPaste(FlatCAMTool):
         for k, v in self.tooltable_tools.items():
             for tool_v in v.keys():
                 if tool_v == 'tooldia':
-                    tool_dias.append(float('%.4f' % v[tool_v]))
+                    tool_dias.append(float('%.*f' % (self.decimals, v[tool_v])))
 
         for row in range(self.tools_table.rowCount()):
 
@@ -991,7 +1016,7 @@ class SolderPaste(FlatCAMTool):
         for k, v in self.tooltable_tools.items():
             # make sure that the tools diameter is more than zero and not zero
             if float(v['tooldia']) > 0:
-                sorted_tools.append(float('%.4f' % float(v['tooldia'])))
+                sorted_tools.append(float('%.*f' % (self.decimals, float(v['tooldia']))))
         sorted_tools.sort(reverse=True)
 
         if not sorted_tools:
@@ -1049,7 +1074,7 @@ class SolderPaste(FlatCAMTool):
             for tool in sorted_tools:
                 offset = tool / 2
                 for uid, vl in self.tooltable_tools.items():
-                    if float('%.4f' % float(vl['tooldia'])) == tool:
+                    if float('%.*f' % (self.decimals, float(vl['tooldia']))) == tool:
                         tooluid = int(uid)
                         break
 
@@ -1301,13 +1326,13 @@ class SolderPaste(FlatCAMTool):
         time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
 
         # add the tab if it was closed
-        self.app.ui.plot_tab_area.addTab(self.app.ui.cncjob_tab, _("Code Editor"))
+        self.app.ui.plot_tab_area.addTab(self.app.ui.text_editor_tab, _("Code Editor"))
 
         # first clear previous text in text editor (if any)
         self.app.ui.code_editor.clear()
 
         # Switch plot_area to CNCJob tab
-        self.app.ui.plot_tab_area.setCurrentWidget(self.app.ui.cncjob_tab)
+        self.app.ui.plot_tab_area.setCurrentWidget(self.app.ui.text_editor_tab)
 
         name = self.cnc_obj_combo.currentText()
         obj = self.app.collection.get_by_name(name)

+ 2 - 3
flatcamTools/ToolSub.py

@@ -1,10 +1,9 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 4/24/2019                                          #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 
 from FlatCAMTool import FlatCAMTool

+ 156 - 254
flatcamTools/ToolTransform.py

@@ -1,10 +1,9 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 from FlatCAMTool import FlatCAMTool
 from FlatCAMObj import *
@@ -29,6 +28,7 @@ class ToolTransform(FlatCAMTool):
 
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
+        self.decimals = 4
 
         self.transform_lay = QtWidgets.QVBoxLayout()
         self.layout.addLayout(self.transform_lay)
@@ -42,28 +42,20 @@ class ToolTransform(FlatCAMTool):
                         }
                         """)
         self.transform_lay.addWidget(title_label)
+        self.transform_lay.addWidget(QtWidgets.QLabel(''))
 
-        self.empty_label = QtWidgets.QLabel("")
-        self.empty_label.setMinimumWidth(70)
+        # ## Layout
+        grid0 = QtWidgets.QGridLayout()
+        self.transform_lay.addLayout(grid0)
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+        grid0.setColumnStretch(2, 0)
 
-        self.empty_label1 = QtWidgets.QLabel("")
-        self.empty_label1.setMinimumWidth(70)
-        self.empty_label2 = QtWidgets.QLabel("")
-        self.empty_label2.setMinimumWidth(70)
-        self.empty_label3 = QtWidgets.QLabel("")
-        self.empty_label3.setMinimumWidth(70)
-        self.empty_label4 = QtWidgets.QLabel("")
-        self.empty_label4.setMinimumWidth(70)
-        self.transform_lay.addWidget(self.empty_label)
+        grid0.addWidget(QtWidgets.QLabel(''))
 
         # ## 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.QHBoxLayout()
+        grid0.addWidget(rotate_title_label, 0, 0, 1, 3)
 
         self.rotate_label = QtWidgets.QLabel('%s:' % _("Angle"))
         self.rotate_label.setToolTip(
@@ -72,11 +64,14 @@ class ToolTransform(FlatCAMTool):
               "Positive numbers for CW motion.\n"
               "Negative numbers for CCW motion.")
         )
-        self.rotate_label.setMinimumWidth(70)
 
-        self.rotate_entry = FCEntry()
-        # self.rotate_entry.setFixedWidth(70)
-        self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.rotate_entry = FCDoubleSpinner()
+        self.rotate_entry.set_precision(self.decimals)
+        self.rotate_entry.setSingleStep(45)
+        self.rotate_entry.setWrapping(True)
+        self.rotate_entry.set_range(-360, 360)
+
+        # self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
 
         self.rotate_button = FCButton()
         self.rotate_button.set_value(_("Rotate"))
@@ -87,32 +82,25 @@ class ToolTransform(FlatCAMTool):
         )
         self.rotate_button.setMinimumWidth(90)
 
-        form_child.addWidget(self.rotate_entry)
-        form_child.addWidget(self.rotate_button)
-
-        form_layout.addRow(self.rotate_label, form_child)
+        grid0.addWidget(self.rotate_label, 1, 0)
+        grid0.addWidget(self.rotate_entry, 1, 1)
+        grid0.addWidget(self.rotate_button, 1, 2)
 
-        self.transform_lay.addWidget(self.empty_label1)
+        grid0.addWidget(QtWidgets.QLabel(''), 2, 0)
 
         # ## 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.QHBoxLayout()
-        form1_child_2 = QtWidgets.QHBoxLayout()
+        grid0.addWidget(skew_title_label, 3, 0, 1, 3)
 
-        self.skewx_label = QtWidgets.QLabel('%s:' % _("Skew_X angle"))
+        self.skewx_label = QtWidgets.QLabel('%s:' % _("X angle"))
         self.skewx_label.setToolTip(
             _("Angle for Skew action, in degrees.\n"
-              "Float number between -360 and 359.")
+              "Float number between -360 and 360.")
         )
-        self.skewx_label.setMinimumWidth(70)
-        self.skewx_entry = FCEntry()
-        self.skewx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        # self.skewx_entry.setFixedWidth(70)
+        self.skewx_entry = FCDoubleSpinner()
+        # self.skewx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.skewx_entry.set_precision(self.decimals)
+        self.skewx_entry.set_range(-360, 360)
 
         self.skewx_button = FCButton()
         self.skewx_button.set_value(_("Skew X"))
@@ -122,15 +110,19 @@ class ToolTransform(FlatCAMTool):
               "the bounding box for all selected objects."))
         self.skewx_button.setMinimumWidth(90)
 
-        self.skewy_label = QtWidgets.QLabel('%s:' % _("Skew_Y angle"))
+        grid0.addWidget(self.skewx_label, 4, 0)
+        grid0.addWidget(self.skewx_entry, 4, 1)
+        grid0.addWidget(self.skewx_button, 4, 2)
+
+        self.skewy_label = QtWidgets.QLabel('%s:' % _("Y angle"))
         self.skewy_label.setToolTip(
             _("Angle for Skew action, in degrees.\n"
-              "Float number between -360 and 359.")
+              "Float number between -360 and 360.")
         )
-        self.skewy_label.setMinimumWidth(70)
-        self.skewy_entry = FCEntry()
-        self.skewy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        # self.skewy_entry.setFixedWidth(70)
+        self.skewy_entry = FCDoubleSpinner()
+        # self.skewy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.skewy_entry.set_precision(self.decimals)
+        self.skewy_entry.set_range(-360, 360)
 
         self.skewy_button = FCButton()
         self.skewy_button.set_value(_("Skew Y"))
@@ -140,35 +132,24 @@ class ToolTransform(FlatCAMTool):
               "the bounding box for all selected objects."))
         self.skewy_button.setMinimumWidth(90)
 
-        form1_child_1.addWidget(self.skewx_entry)
-        form1_child_1.addWidget(self.skewx_button)
-
-        form1_child_2.addWidget(self.skewy_entry)
-        form1_child_2.addWidget(self.skewy_button)
-
-        form1_layout.addRow(self.skewx_label, form1_child_1)
-        form1_layout.addRow(self.skewy_label, form1_child_2)
+        grid0.addWidget(self.skewy_label, 5, 0)
+        grid0.addWidget(self.skewy_entry, 5, 1)
+        grid0.addWidget(self.skewy_button, 5, 2)
 
-        self.transform_lay.addWidget(self.empty_label2)
+        grid0.addWidget(QtWidgets.QLabel(''), 6, 0)
 
         # ## Scale Title
         scale_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.scaleName)
-        self.transform_lay.addWidget(scale_title_label)
+        grid0.addWidget(scale_title_label, 7, 0, 1, 3)
 
-        # ## Form Layout
-        form2_layout = QtWidgets.QFormLayout()
-        self.transform_lay.addLayout(form2_layout)
-        form2_child_1 = QtWidgets.QHBoxLayout()
-        form2_child_2 = QtWidgets.QHBoxLayout()
-
-        self.scalex_label = QtWidgets.QLabel('%s:' % _("Scale_X factor"))
+        self.scalex_label = QtWidgets.QLabel('%s:' % _("X factor"))
         self.scalex_label.setToolTip(
             _("Factor for scaling on X axis.")
         )
-        self.scalex_label.setMinimumWidth(70)
-        self.scalex_entry = FCEntry()
-        self.scalex_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        # self.scalex_entry.setFixedWidth(70)
+        self.scalex_entry = FCDoubleSpinner()
+        # self.scalex_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.scalex_entry.set_precision(self.decimals)
+        self.scalex_entry.setMinimum(-1e6)
 
         self.scalex_button = FCButton()
         self.scalex_button.set_value(_("Scale X"))
@@ -178,14 +159,18 @@ class ToolTransform(FlatCAMTool):
               "the Scale reference checkbox state."))
         self.scalex_button.setMinimumWidth(90)
 
-        self.scaley_label = QtWidgets.QLabel('%s:' % _("Scale_Y factor"))
+        grid0.addWidget(self.scalex_label, 8, 0)
+        grid0.addWidget(self.scalex_entry, 8, 1)
+        grid0.addWidget(self.scalex_button, 8, 2)
+
+        self.scaley_label = QtWidgets.QLabel('%s:' % _("Y factor"))
         self.scaley_label.setToolTip(
             _("Factor for scaling on Y axis.")
         )
-        self.scaley_label.setMinimumWidth(70)
-        self.scaley_entry = FCEntry()
-        self.scaley_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        # self.scaley_entry.setFixedWidth(70)
+        self.scaley_entry = FCDoubleSpinner()
+        # self.scaley_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.scaley_entry.set_precision(self.decimals)
+        self.scaley_entry.setMinimum(-1e6)
 
         self.scaley_button = FCButton()
         self.scaley_button.set_value(_("Scale Y"))
@@ -195,6 +180,10 @@ class ToolTransform(FlatCAMTool):
               "the Scale reference checkbox state."))
         self.scaley_button.setMinimumWidth(90)
 
+        grid0.addWidget(self.scaley_label, 9, 0)
+        grid0.addWidget(self.scaley_entry, 9, 1)
+        grid0.addWidget(self.scaley_button, 9, 2)
+
         self.scale_link_cb = FCCheckBox()
         self.scale_link_cb.set_value(True)
         self.scale_link_cb.setText(_("Link"))
@@ -202,7 +191,6 @@ class ToolTransform(FlatCAMTool):
             _("Scale the selected object(s)\n"
               "using the Scale_X factor for both axis.")
         )
-        self.scale_link_cb.setMinimumWidth(70)
 
         self.scale_zero_ref_cb = FCCheckBox()
         self.scale_zero_ref_cb.set_value(True)
@@ -213,37 +201,24 @@ class ToolTransform(FlatCAMTool):
               "and the center of the biggest bounding box\n"
               "of the selected objects when unchecked."))
 
-        form2_child_1.addWidget(self.scalex_entry)
-        form2_child_1.addWidget(self.scalex_button)
-
-        form2_child_2.addWidget(self.scaley_entry)
-        form2_child_2.addWidget(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)
+        grid0.addWidget(self.scale_link_cb, 10, 0)
+        grid0.addWidget(self.scale_zero_ref_cb, 10, 1)
+        grid0.addWidget(QtWidgets.QLabel(''), 11, 0)
 
         # ## 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.QHBoxLayout()
-        form3_child_2 = QtWidgets.QHBoxLayout()
+        grid0.addWidget(offset_title_label, 12, 0, 1, 3)
 
-        self.offx_label = QtWidgets.QLabel('%s:' % _("Offset_X val"))
+        self.offx_label = QtWidgets.QLabel('%s:' % _("X val"))
         self.offx_label.setToolTip(
             _("Distance to offset on X axis. In current units.")
         )
-        self.offx_label.setMinimumWidth(70)
-        self.offx_entry = FCEntry()
-        self.offx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        # self.offx_entry.setFixedWidth(70)
+        self.offx_entry = FCDoubleSpinner()
+        # self.offx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.offx_entry.set_precision(self.decimals)
+        self.offx_entry.setMinimum(-1e6)
 
         self.offx_button = FCButton()
         self.offx_button.set_value(_("Offset X"))
@@ -253,14 +228,18 @@ class ToolTransform(FlatCAMTool):
               "the bounding box for all selected objects.\n"))
         self.offx_button.setMinimumWidth(90)
 
-        self.offy_label = QtWidgets.QLabel('%s:' % _("Offset_Y val"))
+        grid0.addWidget(self.offx_label, 13, 0)
+        grid0.addWidget(self.offx_entry, 13, 1)
+        grid0.addWidget(self.offx_button, 13, 2)
+
+        self.offy_label = QtWidgets.QLabel('%s:' % _("Y val"))
         self.offy_label.setToolTip(
             _("Distance to offset on Y axis. In current units.")
         )
-        self.offy_label.setMinimumWidth(70)
-        self.offy_entry = FCEntry()
-        self.offy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        # self.offy_entry.setFixedWidth(70)
+        self.offy_entry = FCDoubleSpinner()
+        # self.offy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.offy_entry.set_precision(self.decimals)
+        self.offy_entry.setMinimum(-1e6)
 
         self.offy_button = FCButton()
         self.offy_button.set_value(_("Offset Y"))
@@ -270,43 +249,33 @@ class ToolTransform(FlatCAMTool):
               "the bounding box for all selected objects.\n"))
         self.offy_button.setMinimumWidth(90)
 
-        form3_child_1.addWidget(self.offx_entry)
-        form3_child_1.addWidget(self.offx_button)
-
-        form3_child_2.addWidget(self.offy_entry)
-        form3_child_2.addWidget(self.offy_button)
-
-        form3_layout.addRow(self.offx_label, form3_child_1)
-        form3_layout.addRow(self.offy_label, form3_child_2)
+        grid0.addWidget(self.offy_label, 14, 0)
+        grid0.addWidget(self.offy_entry, 14, 1)
+        grid0.addWidget(self.offy_button, 14, 2)
 
-        self.transform_lay.addWidget(self.empty_label4)
+        grid0.addWidget(QtWidgets.QLabel(''))
 
         # ## 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()
-        form4_child_hlay = QtWidgets.QHBoxLayout()
-        self.transform_lay.addLayout(form4_child_hlay)
-        self.transform_lay.addLayout(form4_layout)
-        form4_child_1 = QtWidgets.QHBoxLayout()
-
         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 ")
+            _("Flip the selected object(s) over the X axis.")
         )
-        self.flipx_button.setMinimumWidth(100)
 
         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 ")
+            _("Flip the selected object(s) over the X axis.")
         )
-        self.flipy_button.setMinimumWidth(90)
+
+        hlay0= QtWidgets.QHBoxLayout()
+        self.transform_lay.addLayout(hlay0)
+
+        hlay0.addWidget(self.flipx_button)
+        hlay0.addWidget(self.flipy_button)
 
         self.flip_ref_cb = FCCheckBox()
         self.flip_ref_cb.set_value(True)
@@ -321,17 +290,17 @@ class ToolTransform(FlatCAMTool):
               "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.setMinimumWidth(70)
 
-        self.flip_ref_label = QtWidgets.QLabel('%s:' % _(" Mirror Ref. Point"))
+        self.transform_lay.addWidget(self.flip_ref_cb)
+
+        self.flip_ref_label = QtWidgets.QLabel('%s:' % _("Ref. 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")
+              "the 'y' in (x, y) will be used when using Flip on Y.")
         )
-        self.flip_ref_label.setMinimumWidth(70)
         self.flip_ref_entry = EvalEntry2("(0, 0)")
-        self.flip_ref_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        # self.flip_ref_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.flip_ref_entry.setFixedWidth(70)
 
         self.flip_ref_button = FCButton()
@@ -340,19 +309,16 @@ class ToolTransform(FlatCAMTool):
             _("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.setMinimumWidth(90)
 
-        form4_child_hlay.addStretch()
-        form4_child_hlay.addWidget(self.flipx_button)
-        form4_child_hlay.addWidget(self.flipy_button)
+        self.ois_flip = OptionalInputSection(self.flip_ref_cb, [self.flip_ref_entry, self.flip_ref_button], logic=True)
 
-        form4_child_1.addWidget(self.flip_ref_entry)
-        form4_child_1.addWidget(self.flip_ref_button)
+        hlay1= QtWidgets.QHBoxLayout()
+        self.transform_lay.addLayout(hlay1)
 
-        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)
+        hlay1.addWidget(self.flip_ref_label)
+        hlay1.addWidget(self.flip_ref_entry)
 
+        self.transform_lay.addWidget(self.flip_ref_button)
         self.transform_lay.addStretch()
 
         # ## Signals
@@ -403,7 +369,7 @@ class ToolTransform(FlatCAMTool):
         self.app.ui.notebook.setTabText(2, _("Transform Tool"))
 
     def install(self, icon=None, separator=None, **kwargs):
-        FlatCAMTool.install(self, icon, separator, shortcut='ALT+R', **kwargs)
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+E', **kwargs)
 
     def set_tool_ui(self):
         # ## Initialize form
@@ -463,33 +429,23 @@ class ToolTransform(FlatCAMTool):
             self.flip_ref_entry.set_value((0, 0))
 
     def on_rotate(self):
-        try:
-            value = float(self.rotate_entry.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                value = float(self.rotate_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
-        self.app.worker_task.emit({'fcn': self.on_rotate_action,
-                                   'params': [value]})
-        # self.on_rotate_action(value)
+        value = float(self.rotate_entry.get_value())
+        if value == 0:
+            self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                 _("Rotate transformation can not be done for a value of 0."))
+        self.app.worker_task.emit({'fcn': self.on_rotate_action, 'params': [value]})
         return
 
     def on_flipx(self):
-        # self.on_flip("Y")
         axis = 'Y'
-        self.app.worker_task.emit({'fcn': self.on_flip,
-                                   'params': [axis]})
+
+        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]})
+
+        self.app.worker_task.emit({'fcn': self.on_flip, 'params': [axis]})
         return
 
     def on_flip_add_coords(self):
@@ -497,56 +453,27 @@ class ToolTransform(FlatCAMTool):
         self.flip_ref_entry.set_value(val)
 
     def on_skewx(self):
-        try:
-            value = float(self.skewx_entry.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                value = float(self.skewx_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
-
-        # self.on_skew("X", value)
+        value = float(self.skewx_entry.get_value())
         axis = 'X'
-        self.app.worker_task.emit({'fcn': self.on_skew,
-                                   'params': [axis, value]})
+
+        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 ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                value = float(self.skewy_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
-
-        # self.on_skew("Y", value)
+        value = float(self.skewy_entry.get_value())
         axis = 'Y'
-        self.app.worker_task.emit({'fcn': self.on_skew,
-                                   'params': [axis, value]})
+
+        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 ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                xvalue = float(self.scalex_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
-
-        # scaling to zero has no sense so we remove it, because scaling with 1 does nothing
-        if xvalue == 0:
-            xvalue = 1
+        xvalue = float(self.scalex_entry.get_value())
+
+        if xvalue == 0 or xvalue == 1:
+            self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                 _("Scale transformation can not be done for a factor of 0 or 1."))
+            return
+
         if self.scale_link_cb.get_value():
             yvalue = xvalue
         else:
@@ -555,80 +482,50 @@ class ToolTransform(FlatCAMTool):
         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))
+            self.app.worker_task.emit({'fcn': self.on_scale, 'params': [axis, xvalue, yvalue, point]})
         else:
-            # self.on_scale("X", xvalue, yvalue)
-            self.app.worker_task.emit({'fcn': self.on_scale,
-                                       'params': [axis, 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 ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                yvalue = float(self.scaley_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
-
-        # scaling to zero has no sense so we remove it, because scaling with 1 does nothing
-        if yvalue == 0:
-            yvalue = 1
+        yvalue = float(self.scaley_entry.get_value())
+
+        if yvalue == 0 or yvalue == 1:
+            self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                 _("Scale transformation can not be done for a factor of 0 or 1."))
+            return
 
         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))
+            self.app.worker_task.emit({'fcn': self.on_scale, 'params': [axis, xvalue, yvalue, point]})
         else:
-            # self.on_scale("Y", xvalue, yvalue)
-            self.app.worker_task.emit({'fcn': self.on_scale,
-                                       'params': [axis, 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 ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                value = float(self.offx_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
-
-        # self.on_offset("X", value)
+        value = float(self.offx_entry.get_value())
+        if value == 0:
+            self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                 _("Offset transformation can not be done for a value of 0."))
+            return
         axis = 'X'
-        self.app.worker_task.emit({'fcn': self.on_offset,
-                                   'params': [axis, value]})
+
+        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 ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                value = float(self.offy_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Wrong value format entered, use a number."))
-                return
-
-        # self.on_offset("Y", value)
+        value = float(self.offy_entry.get_value())
+        if value == 0:
+            self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                 _("Offset transformation can not be done for a value of 0."))
+            return
         axis = 'Y'
-        self.app.worker_task.emit({'fcn': self.on_offset,
-                                   'params': [axis, value]})
+
+        self.app.worker_task.emit({'fcn': self.on_offset, 'params': [axis, value]})
         return
 
     def on_rotate_action(self, num):
@@ -764,6 +661,11 @@ class ToolTransform(FlatCAMTool):
         xminlist = []
         yminlist = []
 
+        if num == 0 or num == 90 or num == 180:
+            self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                 _("Skew transformation can not be done for 0, 90 and 180 degrees."))
+            return
+
         if not obj_list:
             self.app.inform.emit('[WARNING_NOTCL] %s' %
                                  _("No object selected. Please Select an object to shear/skew!"))

+ 23 - 11
flatcamTools/__init__.py

@@ -1,22 +1,34 @@
 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 CutOut
 from flatcamTools.ToolCalculators import ToolCalculator
-from flatcamTools.ToolProperties import Properties
+from flatcamTools.ToolCutOut import CutOut
+
+from flatcamTools.ToolDblSided import DblSidedTool
+
+from flatcamTools.ToolFilm import Film
+
 from flatcamTools.ToolImage import ToolImage
-from flatcamTools.ToolPaint import ToolPaint
+
+from flatcamTools.ToolDistance import Distance
+from flatcamTools.ToolDistanceMin import DistanceMin
+
+from flatcamTools.ToolMove import ToolMove
+
 from flatcamTools.ToolNonCopperClear import NonCopperClear
-from flatcamTools.ToolTransform import ToolTransform
-from flatcamTools.ToolSolderPaste import SolderPaste
+
+from flatcamTools.ToolOptimal import ToolOptimal
+
+from flatcamTools.ToolPaint import ToolPaint
+from flatcamTools.ToolPanelize import Panelize
 from flatcamTools.ToolPcbWizard import PcbWizard
 from flatcamTools.ToolPDF import ToolPDF
-from flatcamTools.ToolSub import ToolSub
+from flatcamTools.ToolProperties import Properties
 
+from flatcamTools.ToolRulesCheck import RulesCheck
 
 from flatcamTools.ToolShell import FCShell
+from flatcamTools.ToolSolderPaste import SolderPaste
+from flatcamTools.ToolSub import ToolSub
+
+from flatcamTools.ToolTransform import ToolTransform

BIN
locale/de/LC_MESSAGES/strings.mo


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 277 - 225
locale/de/LC_MESSAGES/strings.po


BIN
locale/en/LC_MESSAGES/strings.mo


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 279 - 228
locale/en/LC_MESSAGES/strings.po


BIN
locale/es/LC_MESSAGES/strings.mo


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 276 - 225
locale/es/LC_MESSAGES/strings.po


BIN
locale/fr/LC_MESSAGES/strings.mo


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 276 - 225
locale/fr/LC_MESSAGES/strings.po


BIN
locale/pt_BR/LC_MESSAGES/strings.mo


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 277 - 225
locale/pt_BR/LC_MESSAGES/strings.po


BIN
locale/ro/LC_MESSAGES/strings.mo


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 276 - 227
locale/ro/LC_MESSAGES/strings.po


BIN
locale/ru/LC_MESSAGES/strings.mo


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 276 - 224
locale/ru/LC_MESSAGES/strings.po


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 391 - 358
locale_template/strings.pot


+ 5 - 5
make_win.py

@@ -1,4 +1,4 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # Author: Juan Pablo Caram (c)                             #
@@ -11,12 +11,12 @@
 # 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.                    #
-# ########################################################## ##
+# ##########################################################
 
-# ########################################################## ##
-# File Modified (major mod): Marius Adrian Stanciu         #
+# ##########################################################
+# File Modified: Marius Adrian Stanciu                     #
 # Date: 3/10/2019                                          #
-# ########################################################## ##
+# ##########################################################
 
 
 # Files not needed: Qt, tk.dll, tcl.dll, tk/, tcl/, vtk/,

BIN
share/align_center32.png


BIN
share/align_justify32.png


BIN
share/align_left32.png


BIN
share/align_right32.png


BIN
share/bookmarks16.png


BIN
share/bookmarks32.png


BIN
share/calculator16.png


BIN
share/calculator24.png


BIN
share/dark/about32.png


BIN
share/dark/active_static.png


BIN
share/dark/addarray16.png


BIN
share/dark/addarray20.png


BIN
share/dark/addarray32.png


BIN
share/dark/align_center32.png


BIN
share/dark/align_justify32.png


BIN
share/dark/align_left32.png


BIN
share/dark/align_right32.png


BIN
share/dark/aperture16.png


BIN
share/dark/aperture32.png


BIN
share/dark/arc16.png


BIN
share/dark/arc24.png


BIN
share/dark/arc32.png


BIN
share/dark/axis32.png


BIN
share/dark/backup24.png


BIN
share/dark/backup_export24.png


BIN
share/dark/backup_import24.png


BIN
share/dark/blocked16.png


BIN
share/dark/bluelight12.png


BIN
share/dark/bold32.png


BIN
share/dark/buffer16-2.png


BIN
share/dark/buffer16.png


BIN
share/dark/buffer20.png


BIN
share/dark/buffer24.png


BIN
share/dark/bug16.png


BIN
share/dark/bug32.png


이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.