Преглед изворни кода

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 flatcamGUI import VisPyPatches
 
 
 from multiprocessing import freeze_support
 from multiprocessing import freeze_support
+# import copyreg
+# import types
 
 
 if sys.platform == "win32":
 if sys.platform == "win32":
     # cx_freeze 'module win32' workaround
     # cx_freeze 'module win32' workaround

Разлика између датотеке није приказан због своје велике величине
+ 361 - 107
FlatCAMApp.py


+ 778 - 104
FlatCAMObj.py

@@ -1,20 +1,33 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # http://flatcam.org                                       #
 # Author: Juan Pablo Caram (c)                             #
 # Author: Juan Pablo Caram (c)                             #
 # Date: 2/5/2014                                           #
 # Date: 2/5/2014                                           #
 # MIT Licence                                              #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 
+# ##########################################################
+# File modified by: Marius Stanciu                         #
+# ##########################################################
+
+from PyQt5.QtCore import Qt
+from PyQt5.QtGui import QTextDocument
 import copy
 import copy
 import inspect  # TODO: For debugging only.
 import inspect  # TODO: For debugging only.
 from datetime import datetime
 from datetime import datetime
 
 
+from flatcamEditors.FlatCAMTextEditor import TextEditor
+
 from flatcamGUI.ObjectUI import *
 from flatcamGUI.ObjectUI import *
 from FlatCAMCommon import LoudDict
 from FlatCAMCommon import LoudDict
 from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
 from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
 from camlib import *
 from camlib import *
+from flatcamParsers.ParseExcellon import Excellon
+from flatcamParsers.ParseGerber import Gerber
+
 import itertools
 import itertools
+import tkinter as tk
+import sys
 
 
 import gettext
 import gettext
 import FlatCAMTranslation as fcTranslate
 import FlatCAMTranslation as fcTranslate
@@ -109,11 +122,6 @@ class FlatCAMObj(QtCore.QObject):
 
 
         self.plot_single_object.connect(self.single_object_plot)
         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):
     def __del__(self):
         pass
         pass
 
 
@@ -162,11 +170,23 @@ class FlatCAMObj(QtCore.QObject):
 
 
         assert isinstance(self.ui, ObjectUI)
         assert isinstance(self.ui, ObjectUI)
         self.ui.name_entry.returnPressed.connect(self.on_name_activate)
         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)
         # self.ui.skew_button.clicked.connect(self.on_skew_button_click)
 
 
     def build_ui(self):
     def build_ui(self):
@@ -520,6 +540,10 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             "plot": True,
             "plot": True,
             "multicolored": False,
             "multicolored": False,
             "solid": False,
             "solid": False,
+            "tool_type": 'circular',
+            "vtipdia": 0.1,
+            "vtipangle": 30,
+            "vcutz": -0.05,
             "isotooldia": 0.016,
             "isotooldia": 0.016,
             "isopasses": 1,
             "isopasses": 1,
             "isooverlap": 0.15,
             "isooverlap": 0.15,
@@ -548,6 +572,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         # list of rows with apertures plotted
         # list of rows with apertures plotted
         self.marked_rows = []
         self.marked_rows = []
 
 
+        # Number of decimals to be used by tools in this class
+        self.decimals = 4
+
         # Attributes to be included in serialization
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # Always append to it because it carries contents
         # from predecessors.
         # from predecessors.
@@ -565,12 +592,23 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         FlatCAMObj.set_ui(self, ui)
         FlatCAMObj.set_ui(self, ui)
         FlatCAMApp.App.log.debug("FlatCAMGerber.set_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.replotApertures.connect(self.on_mark_cb_click_table)
 
 
         self.form_fields.update({
         self.form_fields.update({
             "plot": self.ui.plot_cb,
             "plot": self.ui.plot_cb,
             "multicolored": self.ui.multicolored_cb,
             "multicolored": self.ui.multicolored_cb,
             "solid": self.ui.solid_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,
             "isotooldia": self.ui.iso_tool_dia_entry,
             "isopasses": self.ui.iso_width_entry,
             "isopasses": self.ui.iso_width_entry,
             "isooverlap": self.ui.iso_overlap_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.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.ui.obj_combo.setCurrentIndex(1)
         self.ui.obj_combo.setCurrentIndex(1)
         self.ui.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
         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
         # Show/Hide Advanced Options
         if self.app.defaults["global_app_level"] == 'b':
         if self.app.defaults["global_app_level"] == 'b':
             self.ui.level.setText('<span style="color:green;"><b>%s</b></span>' % _('Basic'))
             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.apertures_table_label.hide()
             self.ui.aperture_table_visibility_cb.hide()
             self.ui.aperture_table_visibility_cb.hide()
@@ -621,6 +674,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             self.ui.except_cb.hide()
             self.ui.except_cb.hide()
         else:
         else:
             self.ui.level.setText('<span style="color:red;"><b>%s</b></span>' % _('Advanced'))
             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':
         if self.app.defaults["gerber_buffering"] == 'no':
             self.ui.create_buffer_button.show()
             self.ui.create_buffer_button.show()
@@ -637,11 +693,56 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
 
         self.build_ui()
         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):
     def on_type_obj_index_changed(self, index):
         obj_type = self.ui.type_obj_combo.currentIndex()
         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.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
         self.ui.obj_combo.setCurrentIndex(0)
         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):
     def build_ui(self):
         FlatCAMObj.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':
             if str(self.apertures[ap_code]['type']) == 'R' or str(self.apertures[ap_code]['type']) == 'O':
                 ap_dim_item = QtWidgets.QTableWidgetItem(
                 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)
                 ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled)
             elif str(self.apertures[ap_code]['type']) == 'P':
             elif str(self.apertures[ap_code]['type']) == 'P':
                 ap_dim_item = QtWidgets.QTableWidgetItem(
                 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)
                 ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled)
             else:
             else:
@@ -699,9 +800,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
 
             try:
             try:
                 if self.apertures[ap_code]['size'] is not None:
                 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:
                 else:
                     ap_size_item = QtWidgets.QTableWidgetItem('')
                     ap_size_item = QtWidgets.QTableWidgetItem('')
             except KeyError:
             except KeyError:
@@ -1080,14 +1180,26 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                         return 'fail'
                         return 'fail'
                     geo_obj.solid_geometry.append(geom)
                     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
                     # store here the default data for Geometry Data
                     default_data = {}
                     default_data = {}
                     default_data.update({
                     default_data.update({
                         "name": iso_name,
                         "name": iso_name,
                         "plot": self.app.defaults['geometry_plot'],
                         "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'],
                         "travelz": self.app.defaults['geometry_travelz'],
                         "feedrate": self.app.defaults['geometry_feedrate'],
                         "feedrate": self.app.defaults['geometry_feedrate'],
                         "feedrate_z": self.app.defaults['geometry_feedrate_z'],
                         "feedrate_z": self.app.defaults['geometry_feedrate_z'],
@@ -1114,7 +1226,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                             'offset': 'Path',
                             'offset': 'Path',
                             'offset_value': 0.0,
                             'offset_value': 0.0,
                             'type': _('Rough'),
                             'type': _('Rough'),
-                            'tool_type': 'C1',
+                            'tool_type': tool_type,
                             'data': default_data,
                             'data': default_data,
                             'solid_geometry': geo_obj.solid_geometry
                             'solid_geometry': geo_obj.solid_geometry
                         }
                         }
@@ -1295,6 +1407,13 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         :return: None
         :return: None
         :rtype: 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()")
         log.debug("FlatCAMObj.FlatCAMGerber.convert_units()")
 
 
         factor = Gerber.convert_units(self, units)
         factor = Gerber.convert_units(self, units)
@@ -1441,14 +1560,14 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                     if aperture_to_plot_mark in self.apertures:
                     if aperture_to_plot_mark in self.apertures:
                         for elem in self.apertures[aperture_to_plot_mark]['geometry']:
                         for elem in self.apertures[aperture_to_plot_mark]['geometry']:
                             if 'solid' in elem:
                             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)
                                                             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()
                     self.mark_shapes[aperture_to_plot_mark].redraw()
 
 
@@ -1897,6 +2016,9 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
 
 
         self.multigeo = False
         self.multigeo = False
 
 
+        # Number fo decimals to be used for tools in this class
+        self.decimals = 4
+
         # Attributes to be included in serialization
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # Always append to it because it carries contents
         # from predecessors.
         # from predecessors.
@@ -1918,6 +2040,11 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         :return: None
         :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 to signal that we need to reorder the tools dictionary and drills and slots lists
         flag_order = False
         flag_order = False
 
 
@@ -1944,7 +2071,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
                         exc.app.log.warning("Failed to copy option.", option)
                         exc.app.log.warning("Failed to copy option.", option)
 
 
             for drill in exc.drills:
             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:
                 if exc_tool_dia not in custom_dict_drills:
                     custom_dict_drills[exc_tool_dia] = [drill['point']]
                     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'])
                     custom_dict_drills[exc_tool_dia].append(drill['point'])
 
 
             for slot in exc.slots:
             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:
                 if exc_tool_dia not in custom_dict_slots:
                     custom_dict_slots[exc_tool_dia] = [[slot['start'], slot['stop']]]
                     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
                 temp_tools[tool_name_temp] = spec_temp
 
 
                 for drill in exc_final.drills:
                 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:
                     if exc_tool_dia == ordered_dia:
                         temp_drills.append(
                         temp_drills.append(
                             {
                             {
@@ -2055,7 +2182,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
                         )
                         )
 
 
                 for slot in exc_final.slots:
                 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:
                     if slot_tool_dia == ordered_dia:
                         temp_slots.append(
                         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
             # 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
             # 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
             # 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)
             dia.setFlags(QtCore.Qt.ItemIsEnabled)
 
 
@@ -2148,10 +2273,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
             slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
             slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
 
 
             try:
             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:
             except KeyError:
                 t_offset = self.app.defaults['excellon_offset']
                 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()
         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({
         self.form_fields.update({
             "plot": self.ui.plot_cb,
             "plot": self.ui.plot_cb,
             "solid": self.ui.solid_cb,
             "solid": self.ui.solid_cb,
@@ -2347,10 +2474,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         t_default_offset = self.app.defaults["excellon_offset"]
         t_default_offset = self.app.defaults["excellon_offset"]
         if not self.tool_offset:
         if not self.tool_offset:
             for value in self.tools.values():
             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
                 self.tool_offset[dia] = t_default_offset
 
 
         # Show/Hide Advanced Options
         # Show/Hide Advanced Options
@@ -2407,10 +2531,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         self.is_modified = True
         self.is_modified = True
 
 
         row_of_item_changed = self.ui.tools_table.currentRow()
         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
         current_table_offset_edited = None
         if self.ui.tools_table.currentItem() is not 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():
         for x in self.ui.tools_table.selectedItems():
             # from the columnCount we subtract a value of 1 which represent the last column (plot column)
             # from the columnCount we subtract a value of 1 which represent the last column (plot column)
             # which does not have text
             # 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:
         for item in table_tools_items:
             item[0] = str(item[0])
             item[0] = str(item[0])
         return table_tools_items
         return table_tools_items
@@ -2762,8 +2896,8 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
 
 
         for tool in tools:
         for tool in tools:
             # I add the 0.0001 value to account for the rounding error in converting from IN to MM and reverse
             # 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:
             if adj_toolstable_tooldia > adj_file_tooldia + 0.0001:
                 self.app.inform.emit('[ERROR_NOTCL] %s' %
                 self.app.inform.emit('[ERROR_NOTCL] %s' %
                                      _("Milling tool for SLOTS is larger than hole size. Cancelled."))
                                      _("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"
             # 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:
             for slot in self.slots:
                 if slot['tool'] in tools:
                 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
                     # 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)
                     # 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_pp_state = self.app.defaults["geometry_multidepth"]
         self.old_toolchangeg_state = self.app.defaults["geometry_toolchange"]
         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
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # Always append to it because it carries contents
         # from predecessors.
         # 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.
             # 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.
             # 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.
             # 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)
             dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
 
 
@@ -3495,6 +3630,13 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         assert isinstance(self.ui, GeometryObjectUI), \
         assert isinstance(self.ui, GeometryObjectUI), \
             "Expected a GeometryObjectUI, got %s" % type(self.ui)
             "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
         # populate postprocessor names in the combobox
         for name in list(self.app.postprocessors.keys()):
         for name in list(self.app.postprocessors.keys()):
             self.ui.pp_geometry_name_cb.addItem(name)
             self.ui.pp_geometry_name_cb.addItem(name)
@@ -3584,7 +3726,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             for toold in tools_list:
             for toold in tools_list:
                 self.tools.update({
                 self.tools.update({
                     self.tooluid: {
                     self.tooluid: {
-                        'tooldia': float(toold),
+                        'tooldia': float('%.*f' % (self.decimals, float(toold))),
                         'offset': 'Path',
                         'offset': 'Path',
                         'offset_value': 0.0,
                         'offset_value': 0.0,
                         'type': _('Rough'),
                         'type': _('Rough'),
@@ -3714,6 +3856,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             elif isinstance(current_widget, FloatEntry) or isinstance(current_widget, LengthEntry) or \
             elif isinstance(current_widget, FloatEntry) or isinstance(current_widget, LengthEntry) or \
                     isinstance(current_widget, FCEntry) or isinstance(current_widget, IntEntry):
                     isinstance(current_widget, FCEntry) or isinstance(current_widget, IntEntry):
                 current_widget.editingFinished.connect(self.gui_form_to_storage)
                 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 row in range(self.ui.geo_tools_table.rowCount()):
             for col in [2, 3, 4]:
             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.currentItemChanged.connect(self.on_row_selection_change)
         self.ui.geo_tools_table.itemChanged.connect(self.on_tool_edit)
         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()):
         for row in range(self.ui.geo_tools_table.rowCount()):
             self.ui.geo_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table)
             self.ui.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)
                     self.ui.grid3.itemAt(i).widget().editingFinished.disconnect(self.gui_form_to_storage)
                 except (TypeError, AttributeError):
                 except (TypeError, AttributeError):
                     pass
                     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 row in range(self.ui.geo_tools_table.rowCount()):
             for col in [2, 3, 4]:
             for col in [2, 3, 4]:
@@ -3790,7 +3939,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             pass
             pass
 
 
         try:
         try:
-            self.ui.tool_offset_entry.editingFinished.disconnect()
+            self.ui.tool_offset_entry.returnPressed.disconnect()
         except (TypeError, AttributeError):
         except (TypeError, AttributeError):
             pass
             pass
 
 
@@ -3846,10 +3995,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             max_uid = max(tool_uid_list)
             max_uid = max(tool_uid_list)
         self.tooluid = max_uid + 1
         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
         # 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
         # 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."))
                                      _("Wrong value format entered, use a number."))
                 return
                 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())
         tooluid = int(self.ui.geo_tools_table.item(current_row, 5).text())
 
 
         self.tools[tooluid]['tooldia'] = tool_dia
         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())
         tooldia = float(self.ui.geo_tools_table.item(row, 1).text())
         new_cutz = (tooldia - vdia) / (2 * math.tan(math.radians(half_vangle)))
         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)
         self.ui.cutz_entry.set_value(new_cutz)
 
 
         # store the new CutZ value into storage (self.tools)
         # store the new CutZ value into storage (self.tools)
@@ -4426,8 +4572,21 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         table_tools_items = []
         table_tools_items = []
         if self.multigeo:
         if self.multigeo:
             for x in self.ui.geo_tools_table.selectedItems():
             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:
         else:
             for x in self.ui.geo_tools_table.selectedItems():
             for x in self.ui.geo_tools_table.selectedItems():
                 r = []
                 r = []
@@ -4441,9 +4600,10 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                     try:
                     try:
                         txt = self.ui.geo_tools_table.item(x.row(), column).text()
                         txt = self.ui.geo_tools_table.item(x.row(), column).text()
                     except AttributeError:
                     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)
                     r.append(txt)
                 table_tools_items.append(r)
                 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 segx: number of segments on the X axis, for auto-levelling
         :param segy: number of segments on the Y 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
         :param use_thread: if True use threading
         :return: None
         :return: None
         """
         """
@@ -4625,7 +4786,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 tool_cnt += 1
                 tool_cnt += 1
 
 
                 dia_cnc_dict = deepcopy(tools_dict[tooluid_key])
                 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({
                 dia_cnc_dict.update({
                     'tooldia': tooldia_val
                     'tooldia': tooldia_val
                 })
                 })
@@ -4779,7 +4940,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             for tooluid_key in list(tools_dict.keys()):
             for tooluid_key in list(tools_dict.keys()):
                 tool_cnt += 1
                 tool_cnt += 1
                 dia_cnc_dict = deepcopy(tools_dict[tooluid_key])
                 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({
                 dia_cnc_dict.update({
                     'tooldia': tooldia_val
                     '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
                 # 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
                 # on the found tooluid in self.tools we also have the solid_geometry that interest us
                 for k, v in self.tools.items():
                 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)
                         current_uid = int(k)
                         break
                         break
 
 
@@ -5202,8 +5363,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         except TypeError:
         except TypeError:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
             self.app.inform.emit('[ERROR_NOTCL] %s' %
                                  _("An (x,y) pair of values are needed. "
                                  _("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
             return
 
 
         self.geo_len = 0
         self.geo_len = 0
@@ -5286,8 +5447,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 self.app.inform.emit('[ERROR] %s' %
                 self.app.inform.emit('[ERROR] %s' %
                                      _("The Toolchange X,Y field in Edit -> Preferences "
                                      _("The Toolchange X,Y field in Edit -> Preferences "
                                        "has to be in the format (x, y)\n"
                                        "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'
                 return 'fail'
             coords_xy[0] *= factor
             coords_xy[0] *= factor
             coords_xy[1] *= factor
             coords_xy[1] *= factor
@@ -5307,7 +5468,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 for dia_key, dia_value in tooluid_value.items():
                 for dia_key, dia_value in tooluid_value.items():
                     if dia_key == 'tooldia':
                     if dia_key == 'tooldia':
                         dia_value *= factor
                         dia_value *= factor
-                        dia_value = float('%.4f' % dia_value)
+                        dia_value = float('%.*f' % (self.decimals, dia_value))
                         tool_dia_copy[dia_key] = dia_value
                         tool_dia_copy[dia_key] = dia_value
                     if dia_key == 'offset':
                     if dia_key == 'offset':
                         tool_dia_copy[dia_key] = dia_value
                         tool_dia_copy[dia_key] = dia_value
@@ -5384,7 +5545,6 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             # if self.app.is_legacy is False:
             # if self.app.is_legacy is False:
             self.add_shape(shape=element, color=color, visible=visible, layer=0)
             self.add_shape(shape=element, color=color, visible=visible, layer=0)
 
 
-
     def plot(self, visible=None, kind=None):
     def plot(self, visible=None, kind=None):
         """
         """
         Plot the object.
         Plot the object.
@@ -5578,6 +5738,8 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         # from predecessors.
         # from predecessors.
         self.ser_attrs += ['options', 'kind', 'cnc_tools', 'multitool']
         self.ser_attrs += ['options', 'kind', 'cnc_tools', 'multitool']
 
 
+        self.decimals = 4
+
         if self.app.is_legacy is False:
         if self.app.is_legacy is False:
             self.text_col = self.app.plotcanvas.new_text_collection()
             self.text_col = self.app.plotcanvas.new_text_collection()
             self.text_col.enabled = True
             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.
             # 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.
             # 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.
             # 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 = list(str(dia_value['offset']))
             offset_txt[0] = offset_txt[0].upper()
             offset_txt[0] = offset_txt[0].upper()
@@ -5706,13 +5866,20 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         assert isinstance(self.ui, CNCObjectUI), \
         assert isinstance(self.ui, CNCObjectUI), \
             "Expected a CNCObjectUI, got %s" % type(self.ui)
             "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
         # 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
         # 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.ui.toolchange_cb.toggled.connect(self.on_toolchange_custom_clicked)
 
 
         self.form_fields.update({
         self.form_fields.update({
             "plot": self.ui.plot_cb,
             "plot": self.ui.plot_cb,
-            # "tooldia": self.ui.tooldia_entry,
+            "tooldia": self.ui.tooldia_entry,
             "append": self.ui.append_text,
             "append": self.ui.append_text,
             "prepend": self.ui.prepend_text,
             "prepend": self.ui.prepend_text,
             "toolchange_macro": self.ui.toolchange_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_label.show()
                 self.ui.t_distance_entry.setVisible(True)
                 self.ui.t_distance_entry.setVisible(True)
                 self.ui.t_distance_entry.setDisabled(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.setText(str(self.units).lower())
                 self.ui.units_label.setDisabled(True)
                 self.ui.units_label.setDisabled(True)
 
 
@@ -5737,16 +5904,23 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
                 self.ui.t_time_entry.setDisabled(True)
                 self.ui.t_time_entry.setDisabled(True)
                 # if time is more than 1 then we have minutes, else we have seconds
                 # if time is more than 1 then we have minutes, else we have seconds
                 if self.routing_time > 1:
                 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')
                     self.ui.units_time_label.setText('min')
                 else:
                 else:
                     time_r = self.routing_time * 60
                     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.setText('sec')
                 self.ui.units_time_label.setDisabled(True)
                 self.ui.units_time_label.setDisabled(True)
         except AttributeError:
         except AttributeError:
             pass
             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
         # 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"])
         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.
         and plots the object.
         """
         """
         self.read_form()
         self.read_form()
-        self.plot()
+        self.on_plot_kind_change()
 
 
     def on_plot_kind_change(self):
     def on_plot_kind_change(self):
         kind = self.ui.cncplot_method_combo.get_value()
         kind = self.ui.cncplot_method_combo.get_value()
@@ -5867,6 +6041,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
                              (_("Machine Code file saved to"), filename))
                              (_("Machine Code file saved to"), filename))
 
 
     def on_edit_code_click(self, *args):
     def on_edit_code_click(self, *args):
+        self.app.proc_container.view.set_busy(_("Loading..."))
 
 
         preamble = str(self.ui.prepend_text.get_value())
         preamble = str(self.ui.prepend_text.get_value())
         postamble = str(self.ui.append_text.get_value())
         postamble = str(self.ui.append_text.get_value())
@@ -5877,25 +6052,46 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         else:
         else:
             self.app.gcode_edited = gco
             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
         # then append the text from GCode to the text editor
         try:
         try:
             for line in self.app.gcode_edited:
             for line in self.app.gcode_edited:
+                QtWidgets.QApplication.processEvents()
+
                 proc_line = str(line).strip('\n')
                 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:
         except Exception as e:
             log.debug('FlatCAMCNNJob.on_edit_code_click() -->%s' % str(e))
             log.debug('FlatCAMCNNJob.on_edit_code_click() -->%s' % str(e))
             self.app.inform.emit('[ERROR] %s %s' %
             self.app.inform.emit('[ERROR] %s %s' %
                                  ('FlatCAMCNNJob.on_edit_code_click() -->', str(e)))
                                  ('FlatCAMCNNJob.on_edit_code_click() -->', str(e)))
             return
             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...' %
         self.app.inform.emit('[success] %s...' %
                              _('Loaded Machine Code into Code Editor'))
                              _('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())
                 m6_code = self.parse_custom_toolchange_code(self.ui.toolchange_text.get_value())
                 if m6_code is None or m6_code == '':
                 if m6_code is None or m6_code == '':
                     self.app.inform.emit('[ERROR_NOTCL] %s' %
                     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'
                     return 'fail'
 
 
                 g = g.replace('M6', m6_code)
                 g = g.replace('M6', m6_code)
@@ -6174,7 +6370,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         self.shapes.clear(update=True)
         self.shapes.clear(update=True)
 
 
         for tooluid_key in self.cnc_tools:
         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']
             gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
             # tool_uid = int(self.ui.cnc_tools_table.item(cw_row, 3).text())
             # tool_uid = int(self.ui.cnc_tools_table.item(cw_row, 3).text())
 
 
@@ -6227,7 +6423,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
             else:
             else:
                 # multiple tools usage
                 # multiple tools usage
                 for tooluid_key in self.cnc_tools:
                 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']
                     gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
                     self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
                     self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
             self.shapes.redraw()
             self.shapes.redraw()
@@ -6266,7 +6462,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
             for dia_key, dia_value in tooluid_value.items():
             for dia_key, dia_value in tooluid_value.items():
                 if dia_key == 'tooldia':
                 if dia_key == 'tooldia':
                     dia_value *= factor
                     dia_value *= factor
-                    dia_value = float('%.4f' % dia_value)
+                    dia_value = float('%.*f' % (self.decimals, dia_value))
                     tool_dia_copy[dia_key] = dia_value
                     tool_dia_copy[dia_key] = dia_value
                 if dia_key == 'offset':
                 if dia_key == 'offset':
                     tool_dia_copy[dia_key] = dia_value
                     tool_dia_copy[dia_key] = dia_value
@@ -6314,4 +6510,482 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         self.cnc_tools.clear()
         self.cnc_tools.clear()
         self.cnc_tools = deepcopy(temp_tools_dict)
         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
 # end of file

+ 2 - 2
FlatCAMPool.py

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

+ 2 - 2
FlatCAMTranslation.py

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

+ 58 - 16
ObjectCollection.py

@@ -1,14 +1,15 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # http://flatcam.org                                       #
 # Author: Juan Pablo Caram (c)                             #
 # Author: Juan Pablo Caram (c)                             #
 # Date: 2/5/2014                                           #
 # Date: 2/5/2014                                           #
 # MIT Licence                                              #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 
-# ########################################################## ##
+# ##########################################################
 # File modified by: Dennis Hayrullin                       #
 # File modified by: Dennis Hayrullin                       #
-# ########################################################## ##
+# File modified by: Marius Stanciu                         #
+# ##########################################################
 
 
 # from PyQt5.QtCore import QModelIndex
 # from PyQt5.QtCore import QModelIndex
 from FlatCAMObj import *
 from FlatCAMObj import *
@@ -186,21 +187,27 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         ("gerber", "Gerber"),
         ("gerber", "Gerber"),
         ("excellon", "Excellon"),
         ("excellon", "Excellon"),
         ("geometry", "Geometry"),
         ("geometry", "Geometry"),
-        ("cncjob", "CNC Job")
+        ("cncjob", "CNC Job"),
+        ("script", "Scripts"),
+        ("document", "Document"),
     ]
     ]
 
 
     classdict = {
     classdict = {
         "gerber": FlatCAMGerber,
         "gerber": FlatCAMGerber,
         "excellon": FlatCAMExcellon,
         "excellon": FlatCAMExcellon,
         "cncjob": FlatCAMCNCjob,
         "cncjob": FlatCAMCNCjob,
-        "geometry": FlatCAMGeometry
+        "geometry": FlatCAMGeometry,
+        "script": FlatCAMScript,
+        "document": FlatCAMDocument
     }
     }
 
 
     icon_files = {
     icon_files = {
         "gerber": "share/flatcam_icon16.png",
         "gerber": "share/flatcam_icon16.png",
         "excellon": "share/drill16.png",
         "excellon": "share/drill16.png",
         "cncjob": "share/cnc16.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
     root_item = None
@@ -322,6 +329,14 @@ class ObjectCollection(QtCore.QAbstractItemModel):
                     self.app.ui.menuprojectedit.setVisible(False)
                     self.app.ui.menuprojectedit.setVisible(False)
                 if type(obj) != FlatCAMGerber and type(obj) != FlatCAMExcellon and type(obj) != FlatCAMCNCjob:
                 if type(obj) != FlatCAMGerber and type(obj) != FlatCAMExcellon and type(obj) != FlatCAMCNCjob:
                     self.app.ui.menuprojectviewsource.setVisible(False)
                     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:
         else:
             self.app.ui.menuprojectgeneratecnc.setVisible(False)
             self.app.ui.menuprojectgeneratecnc.setVisible(False)
 
 
@@ -411,17 +426,17 @@ class ObjectCollection(QtCore.QAbstractItemModel):
                     # rename the object
                     # rename the object
                     obj.options["name"] = deepcopy(data)
                     obj.options["name"] = deepcopy(data)
 
 
+                    self.app.object_status_changed.emit(obj, 'rename', old_name)
+
                     # update the SHELL auto-completer model data
                     # update the SHELL auto-completer model data
                     try:
                     try:
                         self.app.myKeywords.remove(old_name)
                         self.app.myKeywords.remove(old_name)
                         self.app.myKeywords.append(new_name)
                         self.app.myKeywords.append(new_name)
                         self.app.shell._edit.set_model_data(self.app.myKeywords)
                         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:
                     except Exception as e:
                         log.debug(
                         log.debug(
                             "setData() --> Could not remove the old object name from auto-completer model list. %s" %
                             "setData() --> Could not remove the old object name from auto-completer model list. %s" %
                             str(e))
                             str(e))
-
                     # obj.build_ui()
                     # obj.build_ui()
                     self.app.inform.emit(_("Object renamed from <b>{old}</b> to <b>{new}</b>").format(old=old_name,
                     self.app.inform.emit(_("Object renamed from <b>{old}</b> to <b>{new}</b>").format(old=old_name,
                                                                                                       new=new_name))
                                                                                                       new=new_name))
@@ -490,7 +505,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
 
 
         self.app.should_we_save = True
         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
         # decide if to show or hide the Notebook side of the screen
         if self.app.defaults["global_project_autohide"] is True:
         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.
         :return: The requested object or None if no such object.
         :rtype: FlatCAMObj or None
         :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:
         if isCaseSensitive is None or isCaseSensitive is True:
             for obj in self.get_list():
             for obj in self.get_list():
@@ -570,24 +585,33 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         # send signal with the object that is deleted
         # send signal with the object that is deleted
         # self.app.object_status_changed.emit(active.obj, 'delete')
         # 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
         # update the SHELL auto-completer model data
         name = active.obj.options['name']
         name = active.obj.options['name']
         try:
         try:
             self.app.myKeywords.remove(name)
             self.app.myKeywords.remove(name)
             self.app.shell._edit.set_model_data(self.app.myKeywords)
             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:
         except Exception as e:
             log.debug(
             log.debug(
                 "delete_active() --> Could not remove the old object name from auto-completer model list. %s" % str(e))
                 "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)
         group.remove_child(active)
-
         # after deletion of object store the current list of objects into the self.app.all_objects_list
         # 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.app.all_objects_list = self.get_list()
-
         self.endRemoveRows()
         self.endRemoveRows()
+        # ############ OBJECT DELETION FROM MODEL STOPS HERE ####################
+
         if self.app.is_legacy is False:
         if self.app.is_legacy is False:
             self.app.plotcanvas.redraw()
             self.app.plotcanvas.redraw()
 
 
@@ -605,6 +629,9 @@ class ObjectCollection(QtCore.QAbstractItemModel):
 
 
     def delete_all(self):
     def delete_all(self):
         FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.delete_all()")
         FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.delete_all()")
+
+        self.app.object_status_changed.emit(None, 'delete_all', '')
+
         try:
         try:
             self.app.all_objects_list.clear()
             self.app.all_objects_list.clear()
 
 
@@ -688,6 +715,16 @@ class ObjectCollection(QtCore.QAbstractItemModel):
             log.error("[ERROR] Cause: %s" % str(e))
             log.error("[ERROR] Cause: %s" % str(e))
             raise
             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):
     def set_inactive(self, name):
         """
         """
         Unselect object by name from the project list. This triggers the
         Unselect object by name from the project list. This triggers the
@@ -736,7 +773,12 @@ class ObjectCollection(QtCore.QAbstractItemModel):
             elif obj.kind == 'geometry':
             elif obj.kind == 'geometry':
                 self.app.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
                 self.app.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
                     color='red', name=str(obj.options['name'])))
                     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:
         except IndexError:
             # FlatCAMApp.App.log.debug("on_list_selection_change(): Index Error (Nothing selected?)")
             # FlatCAMApp.App.log.debug("on_list_selection_change(): Index Error (Nothing selected?)")
             self.app.inform.emit('')
             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
 27.09.2019
 
 
 - optimized the toggle axis command
 - 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
 - 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)
 - 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
 27.09.2019
 
 

Разлика између датотеке није приказан због своје велике величине
+ 210 - 1829
camlib.py


+ 27 - 28
flatcamEditors/FlatCAMExcEditor.py

@@ -1,6 +1,5 @@
 # ##########################################################
 # ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 8/17/2019                                          #
 # Date: 8/17/2019                                          #
 # MIT Licence                                              #
 # MIT Licence                                              #
@@ -19,6 +18,7 @@ from rtree import index as rtindex
 from camlib import *
 from camlib import *
 from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, LengthEntry, RadioSet, SpinBoxDelegate
 from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, LengthEntry, RadioSet, SpinBoxDelegate
 from flatcamEditors.FlatCAMGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, FlatCAMGeoEditor
 from flatcamEditors.FlatCAMGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, FlatCAMGeoEditor
+from flatcamParsers.ParseExcellon import Excellon
 
 
 from copy import copy, deepcopy
 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.app.ui.delete_drill_btn.triggered.connect(self.on_delete_btn)
         self.name_entry.returnPressed.connect(self.on_name_activate)
         self.name_entry.returnPressed.connect(self.on_name_activate)
         self.addtool_btn.clicked.connect(self.on_tool_add)
         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.deltool_btn.clicked.connect(self.on_tool_delete)
         # self.tools_table_exc.selectionModel().currentChanged.connect(self.on_row_selected)
         # self.tools_table_exc.selectionModel().currentChanged.connect(self.on_row_selected)
         self.tools_table_exc.cellPressed.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
         # VisPy Visuals
         if self.app.is_legacy is False:
         if self.app.is_legacy is False:
             self.shapes = self.app.plotcanvas.new_shape_collection(layers=1)
             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:
         else:
             from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
             from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
             self.shapes = ShapeCollectionLegacy(obj=self, app=self.app, name='shapes_exc_editor')
             self.shapes = ShapeCollectionLegacy(obj=self, app=self.app, name='shapes_exc_editor')
@@ -2043,6 +2046,9 @@ class FlatCAMExcEditor(QtCore.QObject):
 
 
         self.complete = False
         self.complete = False
 
 
+        # Number of decimals used by tools in this class
+        self.decimals = 4
+
         def make_callback(thetool):
         def make_callback(thetool):
             def f():
             def f():
                 self.on_tool_select(thetool)
                 self.on_tool_select(thetool)
@@ -2113,16 +2119,18 @@ class FlatCAMExcEditor(QtCore.QObject):
         # updated units
         # updated units
         self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
         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.olddia_newdia.clear()
         self.tool2tooldia.clear()
         self.tool2tooldia.clear()
 
 
         # build the self.points_edit dict {dimaters: [point_list]}
         # build the self.points_edit dict {dimaters: [point_list]}
         for drill in self.exc_obj.drills:
         for drill in self.exc_obj.drills:
             if drill['tool'] in self.exc_obj.tools:
             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:
                 try:
                     self.points_edit[tool_dia].append(drill['point'])
                     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}}
         # build the self.slot_points_edit dict {dimaters: {"start": Point, "stop": Point}}
         for slot in self.exc_obj.slots:
         for slot in self.exc_obj.slots:
             if slot['tool'] in self.exc_obj.tools:
             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:
                 try:
                     self.slot_points_edit[tool_dia].append({
                     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
             # 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
             # but use the real order found in the exc_obj.tools
             for k, v in self.exc_obj.tools.items():
             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
                 self.tool2tooldia[int(k)] = tool_dia
 
 
         # Init GUI
         # Init GUI
@@ -2271,12 +2273,9 @@ class FlatCAMExcEditor(QtCore.QObject):
             self.tools_table_exc.setItem(self.tool_row, 0, idd)  # Tool name/id
             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
             # 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)
             dia.setFlags(QtCore.Qt.ItemIsEnabled)
 
 
@@ -2474,9 +2473,9 @@ class FlatCAMExcEditor(QtCore.QObject):
             else:
             else:
                 if isinstance(dia, list):
                 if isinstance(dia, list):
                     for dd in dia:
                     for dd in dia:
-                        deleted_tool_dia_list.append(float('%.4f' % dd))
+                        deleted_tool_dia_list.append(float('%.*f' % (self.decimals, dd)))
                 else:
                 else:
-                    deleted_tool_dia_list.append(float('%.4f' % dia))
+                    deleted_tool_dia_list.append(float('%.*f' % (self.decimals, dia)))
         except Exception as e:
         except Exception as e:
             self.app.inform.emit('[WARNING_NOTCL] %s' %
             self.app.inform.emit('[WARNING_NOTCL] %s' %
                                  _("Select a tool in Tool Table"))
                                  _("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_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_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_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:
         else:
             self.app.plotcanvas.graph_event_disconnect(self.app.mp)
             self.app.plotcanvas.graph_event_disconnect(self.app.mp)
             self.app.plotcanvas.graph_event_disconnect(self.app.mm)
             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.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
                                                               self.app.on_mouse_click_release_over_plot)
                                                               self.app.on_mouse_click_release_over_plot)
         self.app.mdc = self.app.plotcanvas.graph_event_connect('mouse_double_click',
         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)
         self.app.collection.view.clicked.connect(self.app.collection.on_mouse_down)
 
 
         if self.app.is_legacy is False:
         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
         # add a first tool in the Tool Table but only if the Excellon Object is empty
         if not self.tool2tooldia:
         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):
     def update_fcexcellon(self, exc_obj):
         """
         """
@@ -3657,7 +3656,7 @@ class FlatCAMExcEditor(QtCore.QObject):
             x, y = self.app.geo_editor.snap(x, y)
             x, y = self.app.geo_editor.snap(x, y)
 
 
             # Update cursor
             # 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"])
                                          size=self.app.defaults["global_cursor_size"])
 
 
         self.snap_x = x
         self.snap_x = x
@@ -3706,7 +3705,7 @@ class FlatCAMExcEditor(QtCore.QObject):
             self.app.selection_type = None
             self.app.selection_type = None
 
 
         # Update cursor
         # 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"])
                                      size=self.app.defaults["global_cursor_size"])
 
 
     def on_canvas_key_release(self, event):
     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 = QtWidgets.QFontComboBox(self)
         self.font_type_cb.setCurrentFont(f_current)
         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)
         # Flag variables to show if font is bold, italic, both or none (regular)
         self.font_bold = False
         self.font_bold = False
@@ -308,7 +308,7 @@ class TextInputTool(FlatCAMTool):
         self.font_italic_tb.setIcon(QtGui.QIcon('share/italic32.png'))
         self.font_italic_tb.setIcon(QtGui.QIcon('share/italic32.png'))
         hlay.addWidget(self.font_italic_tb)
         hlay.addWidget(self.font_italic_tb)
 
 
-        self.form_layout.addRow("Size:", hlay)
+        self.form_layout.addRow(QtWidgets.QLabel('%s:' % "Size"), hlay)
 
 
         # Text input
         # Text input
         self.text_input_entry = FCTextAreaRich()
         self.text_input_entry = FCTextAreaRich()
@@ -317,7 +317,7 @@ class TextInputTool(FlatCAMTool):
         # self.text_input_entry.setMaximumHeight(150)
         # self.text_input_entry.setMaximumHeight(150)
         self.text_input_entry.setCurrentFont(f_current)
         self.text_input_entry.setCurrentFont(f_current)
         self.text_input_entry.setFontPointSize(10)
         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
         # Buttons
         hlay1 = QtWidgets.QHBoxLayout()
         hlay1 = QtWidgets.QHBoxLayout()
@@ -973,13 +973,13 @@ class TransformEditorTool(FlatCAMTool):
         self.flipy_button.clicked.connect(self.on_flipy)
         self.flipy_button.clicked.connect(self.on_flipy)
         self.flip_ref_button.clicked.connect(self.on_flip_add_coords)
         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()
         self.set_tool_ui()
 
 
@@ -1324,7 +1324,7 @@ class TransformEditorTool(FlatCAMTool):
                     # get mirroring coords from the point entry
                     # get mirroring coords from the point entry
                     if self.flip_ref_cb.isChecked():
                     if self.flip_ref_cb.isChecked():
                         px, py = eval('{}'.format(self.flip_ref_entry.text()))
                         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:
                     else:
                         # first get a bounding box to fit all
                         # first get a bounding box to fit all
                         for sha in shape_list:
                         for sha in shape_list:
@@ -2455,6 +2455,61 @@ class FCSelect(DrawTool):
         return ""
         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):
 class FCMove(FCShapeTool):
     def __init__(self, draw_app):
     def __init__(self, draw_app):
         FCShapeTool.__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,
             "transform": {"button": self.app.ui.geo_transform_btn,
                       "constructor": FCTransform},
                       "constructor": FCTransform},
             "copy": {"button": self.app.ui.geo_copy_btn,
             "copy": {"button": self.app.ui.geo_copy_btn,
-                     "constructor": FCCopy}
+                     "constructor": FCCopy},
+            "explode": {"button": self.app.ui.geo_explode_btn,
+                     "constructor": FCExplode}
         }
         }
 
 
         # # ## Data
         # # ## Data
@@ -3104,6 +3161,9 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
 
         self.rtree_index = rtindex.Index()
         self.rtree_index = rtindex.Index()
 
 
+        # Number of decimals used by tools in this class
+        self.decimals = 4
+
         def entry2option(option, entry):
         def entry2option(option, entry):
             try:
             try:
                 self.options[option] = float(entry.text())
                 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_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_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_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:
         else:
 
 
             self.app.plotcanvas.graph_event_disconnect(self.app.mp)
             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.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
                                                               self.app.on_mouse_click_release_over_plot)
                                                               self.app.on_mouse_click_release_over_plot)
         self.app.mdc = self.app.plotcanvas.graph_event_connect('mouse_double_click',
         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)
         # self.app.collection.view.clicked.connect(self.app.collection.on_mouse_down)
 
 
         if self.app.is_legacy is False:
         if self.app.is_legacy is False:
@@ -3602,6 +3662,14 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
 
         self.replot()
         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
         # start with GRID toolbar activated
         if self.app.ui.grid_snap_btn.isChecked() is False:
         if self.app.ui.grid_snap_btn.isChecked() is False:
             self.app.ui.grid_snap_btn.trigger()
             self.app.ui.grid_snap_btn.trigger()
@@ -3755,7 +3823,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
             x, y = self.snap(x, y)
             x, y = self.snap(x, y)
 
 
             # Update cursor
             # 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"])
                                          size=self.app.defaults["global_cursor_size"])
 
 
         self.snap_x = x
         self.snap_x = x
@@ -4030,7 +4098,8 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
 
             if type(geometry) == LineString or type(geometry) == LinearRing:
             if type(geometry) == LineString or type(geometry) == LinearRing:
                 plot_elements.append(self.shapes.add(shape=geometry, color=color, layer=0,
                 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:
             if type(geometry) == Point:
                 pass
                 pass
@@ -4070,7 +4139,12 @@ class FlatCAMGeoEditor(QtCore.QObject):
     def on_shape_complete(self):
     def on_shape_complete(self):
         self.app.log.debug("on_shape_complete()")
         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':
         if self.app.defaults['geometry_editor_milling_type'] == 'cl':
             # reverse the geometry coordinates direction to allow creation of Gcode for  climb milling
             # 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))
                             pl.append(Polygon(p.exterior.coords[::-1], p.interiors))
                         elif isinstance(p, LinearRing):
                         elif isinstance(p, LinearRing):
                             pl.append(Polygon(p.coords[::-1]))
                             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:
             except TypeError:
                 if isinstance(geom, Polygon) and geom is not None:
                 if isinstance(geom, Polygon) and geom is not None:
                     geom = Polygon(geom.exterior.coords[::-1], geom.interiors)
                     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))
                 log.debug("FlatCAMGeoEditor.on_shape_complete() Error --> %s" % str(e))
                 return 'fail'
                 return 'fail'
 
 
+        shape_list = list()
+        try:
+            for geo in geom:
+                shape_list.append(DrawToolShape(geo))
+        except TypeError:
+            shape_list.append(DrawToolShape(geom))
+
         # Add shape
         # Add shape
-        self.add_shape(DrawToolShape(geom))
+        self.add_shape(shape_list)
 
 
         # Remove any utility shapes
         # Remove any utility shapes
         self.delete_utility_geometry()
         self.delete_utility_geometry()
@@ -4170,7 +4256,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # # ## Grid snap
         # # ## Grid snap
         if self.options["grid_snap"]:
         if self.options["grid_snap"]:
             if self.options["global_gridx"] != 0:
             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:
             else:
                 snap_x_ = x
                 snap_x_ = x
 
 
@@ -4178,12 +4264,12 @@ class FlatCAMGeoEditor(QtCore.QObject):
             # and it will use the snap distance from GridX entry
             # and it will use the snap distance from GridX entry
             if self.app.ui.grid_gap_link_cb.isChecked():
             if self.app.ui.grid_gap_link_cb.isChecked():
                 if self.options["global_gridx"] != 0:
                 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:
                 else:
                     snap_y_ = y
                     snap_y_ = y
             else:
             else:
                 if self.options["global_gridy"] != 0:
                 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:
                 else:
                     snap_y_ = y
                     snap_y_ = y
             nearest_grid_distance = distance((x, y), (snap_x_, snap_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            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 8/17/2019                                          #
 # Date: 8/17/2019                                          #
 # MIT Licence                                              #
 # MIT Licence                                              #
@@ -24,6 +23,7 @@ from camlib import *
 from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, LengthEntry, RadioSet, \
 from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, LengthEntry, RadioSet, \
     SpinBoxDelegate, EvalEntry, EvalEntry2, FCInputDialog, FCButton, OptionalInputSection, FCCheckBox
     SpinBoxDelegate, EvalEntry, EvalEntry2, FCInputDialog, FCButton, OptionalInputSection, FCCheckBox
 from FlatCAMObj import FlatCAMGerber
 from FlatCAMObj import FlatCAMGerber
+from flatcamParsers.ParseGerber import Gerber
 from FlatCAMTool import FlatCAMTool
 from FlatCAMTool import FlatCAMTool
 
 
 from numpy.linalg import norm as numpy_norm
 from numpy.linalg import norm as numpy_norm
@@ -1809,28 +1809,46 @@ class FCMarkArea(FCShapeTool):
         self.activate_markarea()
         self.activate_markarea()
 
 
     def activate_markarea(self):
     def activate_markarea(self):
-        self.draw_app.hide_tool('all')
         self.draw_app.ma_tool_frame.show()
         self.draw_app.ma_tool_frame.show()
 
 
         # clear previous marking
         # clear previous marking
         self.draw_app.ma_annotation.clear(update=True)
         self.draw_app.ma_annotation.clear(update=True)
 
 
         try:
         try:
-            self.draw_app.ma_threshold__button.clicked.disconnect()
+            self.draw_app.ma_threshold_button.clicked.disconnect()
         except (TypeError, AttributeError):
         except (TypeError, AttributeError):
             pass
             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):
     def deactivate_markarea(self):
-        self.draw_app.ma_threshold__button.clicked.disconnect()
+        self.draw_app.ma_threshold_button.clicked.disconnect()
         self.complete = True
         self.complete = True
         self.draw_app.select_tool("select")
         self.draw_app.select_tool("select")
         self.draw_app.hide_tool(self.name)
         self.draw_app.hide_tool(self.name)
 
 
     def on_markarea_click(self):
     def on_markarea_click(self):
         self.draw_app.on_markarea()
         self.draw_app.on_markarea()
+
+    def on_markarea_clear(self):
+        self.draw_app.ma_annotation.clear(update=True)
         self.deactivate_markarea()
         self.deactivate_markarea()
 
 
+    def on_markarea_delete(self):
+        self.draw_app.delete_marked_polygons()
+        self.on_markarea_clear()
+
     def clean_up(self):
     def clean_up(self):
         self.draw_app.selected = []
         self.draw_app.selected = []
         self.draw_app.apertures_table.clearSelection()
         self.draw_app.apertures_table.clearSelection()
@@ -2332,6 +2350,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
 
         self.app = app
         self.app = app
         self.canvas = self.app.plotcanvas
         self.canvas = self.app.plotcanvas
+        self.decimals = 4
 
 
         # Current application units in Upper Case
         # Current application units in Upper Case
         self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
         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()
         self.ma_tool_frame.hide()
 
 
         # Title
         # 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(
         ma_title_lbl.setToolTip(
             _("Mark the polygon areas.")
             _("Mark the polygon areas.")
         )
         )
@@ -2596,16 +2615,18 @@ class FlatCAMGrbEditor(QtCore.QObject):
             _("The threshold value, all areas less than this are marked.\n"
             _("The threshold value, all areas less than this are marked.\n"
               "Can have a value between 0.0000 and 9999.9999")
               "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 = QtWidgets.QLabel('%s:' % _("Area LOWER threshold"))
         self.ma_lower_threshold_lbl.setToolTip(
         self.ma_lower_threshold_lbl.setToolTip(
             _("The threshold value, all areas more than this are marked.\n"
             _("The threshold value, all areas more than this are marked.\n"
               "Can have a value between 0.0000 and 9999.9999")
               "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_lower_threshold_lbl, self.ma_lower_threshold_entry)
         ma_form_layout.addRow(self.ma_upper_threshold_lbl, self.ma_upper_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()
         hlay_ma = QtWidgets.QHBoxLayout()
         self.ma_tools_box.addLayout(hlay_ma)
         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 ####
         # ### Add Pad Array ####
@@ -2786,27 +2822,30 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # # ## Data
         # # ## Data
         self.active_tool = None
         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
         # 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
         # dictionary to store the tool_row and aperture codes in Tool_table
         # it will be updated everytime self.build_ui() is called
         # 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
         # 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
         # is cleared but as a side effect also the selected tool is cleared
         self.last_aperture_selected = None
         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)
         # this will flag if the Editor "tools" are launched from key shortcuts (True) or from menu toolbar (False)
         self.launched_from_shortcuts = 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.aptype_cb.currentIndexChanged[str].connect(self.on_aptype_changed)
 
 
         self.addaperture_btn.clicked.connect(self.on_aperture_add)
         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.delaperture_btn.clicked.connect(self.on_aperture_delete)
         self.apertures_table.cellPressed.connect(self.on_row_selected)
         self.apertures_table.cellPressed.connect(self.on_row_selected)
@@ -2955,6 +2994,9 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
 
         self.conversion_factor = 1
         self.conversion_factor = 1
 
 
+        # number of decimals for the tool diameters to be used in this editor
+        self.decimals = 4
+
         self.set_ui()
         self.set_ui()
         log.debug("Initialization of the FlatCAM Gerber Editor is finished ...")
         log.debug("Initialization of the FlatCAM Gerber Editor is finished ...")
 
 
@@ -2966,6 +3008,11 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # updated units
         # updated units
         self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
         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.olddia_newdia.clear()
         self.tool2tooldia.clear()
         self.tool2tooldia.clear()
 
 
@@ -2994,14 +3041,14 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.aptype_cb.set_value(self.app.defaults["gerber_editor_newtype"])
         self.aptype_cb.set_value(self.app.defaults["gerber_editor_newtype"])
         self.apdim_entry.set_value(self.app.defaults["gerber_editor_newdim"])
         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
         # linear array
         self.pad_axis_radio.set_value(self.app.defaults["gerber_editor_lin_axis"])
         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"])
         self.linear_angle_spinner.set_value(self.app.defaults["gerber_editor_lin_angle"])
         # circular array
         # circular array
         self.pad_direction_radio.set_value(self.app.defaults["gerber_editor_circ_dir"])
         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):
     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':
             if str(self.storage_dict[ap_code]['type']) == 'R' or str(self.storage_dict[ap_code]['type']) == 'O':
                 ap_dim_item = QtWidgets.QTableWidgetItem(
                 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)
                 ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled)
             elif str(self.storage_dict[ap_code]['type']) == 'P':
             elif str(self.storage_dict[ap_code]['type']) == 'P':
                 ap_dim_item = QtWidgets.QTableWidgetItem(
                 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)
                 ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled)
             else:
             else:
@@ -3073,8 +3120,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
 
             try:
             try:
                 if self.storage_dict[ap_code]['size'] is not None:
                 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:
                 else:
                     ap_size_item = QtWidgets.QTableWidgetItem('')
                     ap_size_item = QtWidgets.QTableWidgetItem('')
             except KeyError:
             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_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_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_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:
         else:
             self.canvas.graph_event_disconnect(self.app.mp)
             self.canvas.graph_event_disconnect(self.app.mp)
             self.canvas.graph_event_disconnect(self.app.mm)
             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.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.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.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)
         self.app.collection.view.clicked.connect(self.app.collection.on_mouse_down)
 
 
         if self.app.is_legacy is False:
         if self.app.is_legacy is False:
@@ -3691,7 +3738,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.gerber_obj = orig_grb_obj
         self.gerber_obj = orig_grb_obj
         self.gerber_obj_options = orig_grb_obj.options
         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']
         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
         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]
                     conv_apertures[apid][key] = self.gerber_obj.apertures[apid][key]
 
 
         self.gerber_obj.apertures = conv_apertures
         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
         # APPLY CLEAR_GEOMETRY on the SOLID_GEOMETRY
@@ -3987,7 +4034,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
 
             grb_obj.multigeo = False
             grb_obj.multigeo = False
             grb_obj.follow = False
             grb_obj.follow = False
-            grb_obj.gerber_units = app_obj.defaults['units']
+            grb_obj.units = app_obj.defaults['units']
 
 
             try:
             try:
                 grb_obj.create_geometry()
                 grb_obj.create_geometry()
@@ -4096,7 +4143,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         if specific_shape:
         if specific_shape:
             geo = specific_shape
             geo = specific_shape
         else:
         else:
-            geo = self.active_tool.geometry
+            geo = deepcopy(self.active_tool.geometry)
             if geo is None:
             if geo is None:
                 return
                 return
 
 
@@ -4398,7 +4445,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             x, y = self.app.geo_editor.snap(x, y)
             x, y = self.app.geo_editor.snap(x, y)
 
 
             # Update cursor
             # 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"])
                                          size=self.app.defaults["global_cursor_size"])
 
 
         self.snap_x = x
         self.snap_x = x
@@ -4820,16 +4867,15 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.ma_annotation.clear(update=True)
         self.ma_annotation.clear(update=True)
 
 
         self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
         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 = []
         text = []
         position = []
         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:
                         try:
                             upper_threshold_val = self.ma_upper_threshold_entry.get_value()
                             upper_threshold_val = self.ma_upper_threshold_entry.get_value()
                         except Exception as e:
                         except Exception as e:
@@ -4841,20 +4887,29 @@ class FlatCAMGrbEditor(QtCore.QObject):
                             lower_threshold_val = 0.0
                             lower_threshold_val = 0.0
 
 
                         if float(upper_threshold_val) > area > float(lower_threshold_val):
                         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_elem = '%.4f' % area
                             text.append(text_elem)
                             text.append(text_elem)
                             position.append(current_pos)
                             position.append(current_pos)
+                            self.geo_to_delete.append(geo_el)
 
 
         if text:
         if text:
             self.ma_annotation.set(text=text, pos=position, visible=True,
             self.ma_annotation.set(text=text, pos=position, visible=True,
                                    font_size=self.app.defaults["cncjob_annotation_fontsize"],
                                    font_size=self.app.defaults["cncjob_annotation_fontsize"],
                                    color='#000000FF')
                                    color='#000000FF')
             self.app.inform.emit('[success] %s' %
             self.app.inform.emit('[success] %s' %
-                                 _("Polygon areas marked."))
+                                 _("Polygons marked."))
         else:
         else:
             self.app.inform.emit('[WARNING_NOTCL] %s' %
             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):
     def on_eraser(self):
         self.select_tool('eraser')
         self.select_tool('eraser')
@@ -5244,13 +5299,13 @@ class TransformEditorTool(FlatCAMTool):
         self.flipy_button.clicked.connect(self.on_flipy)
         self.flipy_button.clicked.connect(self.on_flipy)
         self.flip_ref_button.clicked.connect(self.on_flip_add_coords)
         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()
         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            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # http://flatcam.org                                       #
 # Author: Juan Pablo Caram (c)                             #
 # Author: Juan Pablo Caram (c)                             #
 # Date: 2/5/2014                                           #
 # Date: 2/5/2014                                           #
 # MIT Licence                                              #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 
-# ########################################################## ##
+# ##########################################################
 # File Modified (major mod): Marius Adrian Stanciu         #
 # File Modified (major mod): Marius Adrian Stanciu         #
 # Date: 3/10/2019                                          #
 # Date: 3/10/2019                                          #
-# ########################################################## ##
+# ##########################################################
 
 
 from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
 from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
@@ -20,6 +20,10 @@ from copy import copy
 import re
 import re
 import logging
 import logging
 import html
 import html
+import webbrowser
+from copy import deepcopy
+import sys
+from datetime import datetime
 
 
 log = logging.getLogger('base')
 log = logging.getLogger('base')
 
 
@@ -507,6 +511,168 @@ class EvalEntry2(QtWidgets.QLineEdit):
         return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
         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):
 class FCCheckBox(QtWidgets.QCheckBox):
     def __init__(self, label='', parent=None):
     def __init__(self, label='', parent=None):
         super(FCCheckBox, self).__init__(str(label), parent)
         super(FCCheckBox, self).__init__(str(label), parent)
@@ -557,7 +723,7 @@ class FCTextAreaRich(QtWidgets.QTextEdit):
 
 
 class FCTextAreaExtended(QtWidgets.QTextEdit):
 class FCTextAreaExtended(QtWidgets.QTextEdit):
     def __init__(self, parent=None):
     def __init__(self, parent=None):
-        super(FCTextAreaExtended, self).__init__(parent)
+        super().__init__(parent)
 
 
         self.completer = MyCompleter()
         self.completer = MyCompleter()
 
 
@@ -1425,7 +1591,7 @@ class FCDetachableTab(QtWidgets.QTabWidget):
 
 
 
 
 class FCDetachableTab2(FCDetachableTab):
 class FCDetachableTab2(FCDetachableTab):
-    tab_closed_signal = pyqtSignal()
+    tab_closed_signal = pyqtSignal(object)
 
 
     def __init__(self, protect=None, protect_by_name=None, parent=None):
     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)
         super(FCDetachableTab2, self).__init__(protect=protect, protect_by_name=protect_by_name, parent=parent)
@@ -1439,9 +1605,7 @@ class FCDetachableTab2(FCDetachableTab):
         """
         """
         idx = self.currentIndex()
         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)
         self.removeTab(currentIndex)
 
 
@@ -1564,9 +1728,33 @@ class OptionalHideInputSection:
 
 
 
 
 class FCTable(QtWidgets.QTableWidget):
 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)
         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):
     def sizeHint(self):
         default_hint_size = super(FCTable, self).sizeHint()
         default_hint_size = super(FCTable, self).sizeHint()
         return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
         return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
@@ -1583,7 +1771,7 @@ class FCTable(QtWidgets.QTableWidget):
             width += self.columnWidth(i)
             width += self.columnWidth(i)
         return width
         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):
     def setColortoRow(self, rowIndex, color):
         for j in range(self.columnCount()):
         for j in range(self.columnCount()):
             self.item(rowIndex, j).setBackground(color)
             self.item(rowIndex, j).setBackground(color)
@@ -1609,6 +1797,124 @@ class FCTable(QtWidgets.QTableWidget):
         self.addAction(action)
         self.addAction(action)
         action.triggered.connect(call_function)
         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):
 class SpinBoxDelegate(QtWidgets.QItemDelegate):
 
 
@@ -1652,103 +1958,27 @@ class SpinBoxDelegate(QtWidgets.QItemDelegate):
         spinbox.setDecimals(digits)
         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):
 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 title: string with the window title
         :param label: string with the message inside the dialog box
         :param label: string with the message inside the dialog box
         """
         """
         super(Dialog_box, self).__init__()
         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
         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.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
         self.readyToEdit = True
 
 
     def mousePressEvent(self, e, parent=None):
     def mousePressEvent(self, e, parent=None):
@@ -1782,7 +2012,7 @@ class _BrowserTextEdit(QTextEdit):
 
 
     def clear(self):
     def clear(self):
         QTextEdit.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 = html.escape(text)
         text = text.replace('\n', '<br/>')
         text = text.replace('\n', '<br/>')
         self.moveCursor(QTextCursor.End)
         self.moveCursor(QTextCursor.End)

Разлика између датотеке није приказан због своје велике величине
+ 345 - 150
flatcamGUI/ObjectUI.py


+ 85 - 14
flatcamGUI/PlotCanvas.py

@@ -1,17 +1,17 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # 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                                              #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 
 from PyQt5 import QtCore
 from PyQt5 import QtCore
 
 
 import logging
 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 flatcamGUI.VisPyVisuals import ShapeGroup, ShapeCollection, TextCollection, TextGroup, Cursor
 from vispy.scene.visuals import InfiniteLine, Line
 from vispy.scene.visuals import InfiniteLine, Line
+
 import numpy as np
 import numpy as np
 from vispy.geometry import Rect
 from vispy.geometry import Rect
 
 
@@ -44,6 +44,17 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         # Parent container
         # Parent container
         self.container = 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,
         # workspace lines; I didn't use the rectangle because I didn't want to add another VisPy Node,
         # which might decrease performance
         # which might decrease performance
         self.b_line, self.r_line, self.t_line, self.l_line = None, None, None, None
         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.draw_workspace()
 
 
         self.line_parent = None
         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,
         self.cursor_v_line = InfiniteLine(pos=None, color=self.line_color, vertical=True,
                                           parent=self.line_parent)
                                           parent=self.line_parent)
 
 
@@ -89,10 +99,12 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         self.text_collection.enabled = True
         self.text_collection.enabled = True
 
 
         self.c = None
         self.c = None
-
+        self.big_cursor = None
         # Keep VisPy canvas happy by letting it be "frozen" again.
         # Keep VisPy canvas happy by letting it be "frozen" again.
         self.freeze()
         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
     # 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
     # all CNC have a limited workspace
     def draw_workspace(self):
     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)])
         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)])
         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)])
         a3p_mm = np.array([(0, 0), (297, 0), (297, 420), (0, 420)])
         a3l_mm = np.array([(0, 0), (420, 0), (420, 297), (0, 297)])
         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.delete_workspace()
 
 
         self.b_line = Line(pos=a[0:2], color=(0.70, 0.3, 0.3, 1.0),
         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),
         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),
         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),
         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:
         if self.fcapp.defaults['global_workspace'] is False:
             self.delete_workspace()
             self.delete_workspace()
@@ -196,13 +208,33 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         return ShapeCollection(parent=self.view.scene, pool=self.fcapp.pool, **kwargs)
         return ShapeCollection(parent=self.view.scene, pool=self.fcapp.pool, **kwargs)
 
 
     def new_cursor(self, big=None):
     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:
         if big is True:
+            self.big_cursor = True
             self.c = CursorBig()
             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_state_updated.connect(self.on_mouse_state)
             self.c.mouse_position_updated.connect(self.on_mouse_position)
             self.c.mouse_position_updated.connect(self.on_mouse_position)
         else:
         else:
+            self.big_cursor = False
             self.c = Cursor(pos=np.empty((0, 2)), parent=self.view.scene)
             self.c = Cursor(pos=np.empty((0, 2)), parent=self.view.scene)
             self.c.antialias = 0
             self.c.antialias = 0
+
         return self.c
         return self.c
 
 
     def on_mouse_state(self, state):
     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.cursor_v_line.set_data(pos=pos[0], color=self.line_color)
         self.view.scene.update()
         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):
     def new_text_group(self, collection=None):
         if collection:
         if collection:
             return TextGroup(collection)
             return TextGroup(collection)
@@ -308,7 +376,10 @@ class CursorBig(QtCore.QObject):
         if 'edge_color' in kwargs:
         if 'edge_color' in kwargs:
             color = kwargs['edge_color']
             color = kwargs['edge_color']
         else:
         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]]
         position = [pos[0][0], pos[0][1]]
         self.mouse_position_updated.emit(position)
         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_xticks([])
         self.axes.set_yticks([])
         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.canvas = FigureCanvas(self.figure)
 
 
         self.cache = None
         self.cache = None
@@ -140,6 +145,13 @@ class PlotCanvasLegacy(QtCore.QObject):
 
 
         self.app = app
         self.app = app
 
 
+        if self.app.defaults['global_theme'] == 'white':
+            theme_color = '#FFFFFF'
+            tick_color = '#000000'
+        else:
+            theme_color = '#000000'
+            tick_color = '#FFFFFF'
+
         # Options
         # Options
         self.x_margin = 15  # pixels
         self.x_margin = 15  # pixels
         self.y_margin = 25  # Pixels
         self.y_margin = 25  # Pixels
@@ -149,16 +161,26 @@ class PlotCanvasLegacy(QtCore.QObject):
 
 
         # Plots go onto a single matplotlib.figure
         # Plots go onto a single matplotlib.figure
         self.figure = Figure(dpi=50)  # TODO: dpi needed?
         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.
         # These axes show the ticks and grid. No plotting done here.
         # New axes must have a label, otherwise mpl returns an existing one.
         # 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 = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
         self.axes.set_aspect(1)
         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.axhline(color=(0.70, 0.3, 0.3), linewidth=2)
         self.axes.axvline(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.ch_line = None
         self.cv_line = None
         self.cv_line = None
 
 
@@ -264,10 +286,18 @@ class PlotCanvasLegacy(QtCore.QObject):
         # else:
         # else:
         #     c = MplCursor(axes=axes, color='black', linewidth=1)
         #     c = MplCursor(axes=axes, color='black', linewidth=1)
 
 
+        if self.app.defaults['global_theme'] == 'white':
+            color = '#000000'
+        else:
+            color = '#FFFFFF'
+
         if big is True:
         if big is True:
             self.big_cursor = 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 = FakeCursor()
         c.mouse_state_updated.connect(self.clear_cursor)
         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
         # 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.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:
             if self.big_cursor is False:
                 try:
                 try:
                     x, y = self.app.geo_editor.snap(x_pos, y_pos)
                     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
                     # 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
                     # one in the OpenGL(3D) graphic engine
                     pointer_size = int(float(self.app.defaults["global_cursor_size"] ) * 1.65)
                     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:
                     for el in elements:
                         self.axes.draw_artist(el)
                         self.axes.draw_artist(el)
                 except Exception as e:
                 except Exception as e:
                     # this happen at app initialization since self.app.geo_editor does not exist yet
                     # 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
                     pass
             else:
             else:
                 self.ch_line.set_ydata(y_pos)
                 self.ch_line.set_ydata(y_pos)
@@ -311,6 +346,11 @@ class PlotCanvasLegacy(QtCore.QObject):
         if state is True:
         if state is True:
             self.draw_cursor(x_pos=self.mouse[0], y_pos=self.mouse[1])
             self.draw_cursor(x_pos=self.mouse[0], y_pos=self.mouse[1])
         else:
         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.restore_region(self.background)
             self.canvas.blit(self.axes.bbox)
             self.canvas.blit(self.axes.bbox)
 
 
@@ -468,7 +508,7 @@ class PlotCanvasLegacy(QtCore.QObject):
 
 
         # Adjust axes
         # Adjust axes
         for ax in self.figure.get_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))
             ax.set_ylim((y - half_height, y + half_height))
 
 
         # Re-draw
         # Re-draw
@@ -802,7 +842,8 @@ class ShapeCollectionLegacy:
             self.axes = self.app.plotcanvas.new_axes(axes_name)
             self.axes = self.app.plotcanvas.new_axes(axes_name)
 
 
     def add(self, shape=None, color=None, face_color=None, alpha=None, visible=True,
     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
         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 gcode_parsed: not used; just for compatibility with VIsPy canvas
         :param tool_tolerance: just for compatibility with VIsPy canvas
         :param tool_tolerance: just for compatibility with VIsPy canvas
         :param tooldia:
         :param tooldia:
+        :param linewidth: the width of the line
         :return:
         :return:
         """
         """
         self._color = color[:-2] if color is not None else None
         self._color = color[:-2] if color is not None else None
@@ -845,6 +887,7 @@ class ShapeCollectionLegacy:
                 self.shape_dict.update({
                 self.shape_dict.update({
                     'color': self._color,
                     'color': self._color,
                     'face_color': self._face_color,
                     'face_color': self._face_color,
+                    'linewidth': linewidth,
                     'alpha': self._alpha,
                     'alpha': self._alpha,
                     'shape': sh
                     'shape': sh
                 })
                 })
@@ -857,6 +900,7 @@ class ShapeCollectionLegacy:
             self.shape_dict.update({
             self.shape_dict.update({
                 'color': self._color,
                 'color': self._color,
                 'face_color': self._face_color,
                 'face_color': self._face_color,
+                'linewidth': linewidth,
                 'alpha': self._alpha,
                 'alpha': self._alpha,
                 'shape': shape
                 'shape': shape
             })
             })
@@ -920,15 +964,21 @@ class ShapeCollectionLegacy:
                 elif obj_type == 'geometry':
                 elif obj_type == 'geometry':
                     if type(local_shapes[element]['shape']) == Polygon:
                     if type(local_shapes[element]['shape']) == Polygon:
                         x, y = local_shapes[element]['shape'].exterior.coords.xy
                         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:
                         for ints in local_shapes[element]['shape'].interiors:
                             x, y = ints.coords.xy
                             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 \
                     elif type(local_shapes[element]['shape']) == LineString or \
                             type(local_shapes[element]['shape']) == LinearRing:
                             type(local_shapes[element]['shape']) == LinearRing:
 
 
                         x, y = local_shapes[element]['shape'].coords.xy
                         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':
                 elif obj_type == 'gerber':
                     if self.obj.options["multicolored"]:
                     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            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # http://flatcam.org                                       #
 # File Author: Dennis Hayrullin                            #
 # File Author: Dennis Hayrullin                            #
 # Date: 2/5/2016                                           #
 # Date: 2/5/2016                                           #
 # MIT Licence                                              #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 
 import numpy as np
 import numpy as np
 from PyQt5.QtGui import QPalette
 from PyQt5.QtGui import QPalette
@@ -25,7 +25,27 @@ class VisPyCanvas(scene.SceneCanvas):
 
 
         self.unfreeze()
         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.bgcolor = back_color
         self.central_widget.border_color = 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 = self.grid_widget.add_widget(row=0, col=0, col_span=2)
         top_padding.height_max = 0
         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.yaxis.width_max = 55
         self.grid_widget.add_widget(self.yaxis, row=1, col=0)
         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.xaxis.height_max = 30
         self.grid_widget.add_widget(self.xaxis, row=2, col=1)
         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 = 24
         right_padding.width_max = 0
         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))
         view.camera = Camera(aspect=1, rect=(-25, -25, 150, 150))
 
 
         # Following function was removed from 'prepare_draw()' of 'Grid' class by patch,
         # 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.xaxis.link_view(view)
         self.yaxis.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.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()
         self.freeze()
 
 
@@ -115,6 +144,9 @@ class Camera(scene.PanZoomCamera):
         if event.handled or not self.interactive:
         if event.handled or not self.interactive:
             return
             return
 
 
+        # key modifiers
+        modifiers = event.mouse_event.modifiers
+
         # Limit mouse move events
         # Limit mouse move events
         last_event = event.last_event
         last_event = event.last_event
         t = time.time()
         t = time.time()
@@ -129,21 +161,21 @@ class Camera(scene.PanZoomCamera):
             event.handled = True
             event.handled = True
             return
             return
 
 
-        # Scrolling
+        # ################### Scrolling ##########################
         BaseCamera.viewbox_mouse_event(self, event)
         BaseCamera.viewbox_mouse_event(self, event)
 
 
         if event.type == 'mouse_wheel':
         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
             event.handled = True
 
 
         elif event.type == 'mouse_move':
         elif event.type == 'mouse_move':
             if event.press_event is None:
             if event.press_event is None:
                 return
                 return
 
 
-            modifiers = event.mouse_event.modifiers
-
+            # ################ Panning ############################
             # self.pan_button_setting is actually self.FlatCAM.APP.defaults['global_pan_button']
             # self.pan_button_setting is actually self.FlatCAM.APP.defaults['global_pan_button']
             if event.button == int(self.pan_button_setting) and not modifiers:
             if event.button == int(self.pan_button_setting) and not modifiers:
                 # Translate
                 # Translate

+ 2 - 2
flatcamGUI/VisPyPatches.py

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

+ 2 - 2
flatcamGUI/VisPyTesselators.py

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

+ 5 - 3
flatcamGUI/VisPyVisuals.py

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

+ 0 - 1
flatcamParsers/ParseDXF.py

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

+ 46 - 34
flatcamParsers/ParseDXF_Spline.py

@@ -2,30 +2,32 @@
 # Vasilis Vlachoudis
 # Vasilis Vlachoudis
 # Date: 20-Oct-2015
 # Date: 20-Oct-2015
 
 
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File modified: Marius Adrian Stanciu                     #
 # File modified: Marius Adrian Stanciu                     #
 # Date: 3/10/2019                                          #
 # Date: 3/10/2019                                          #
-# ########################################################## ##
+# ##########################################################
 
 
 import math
 import math
 import sys
 import sys
 
 
+
 def norm(v):
 def norm(v):
     return math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
     return math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
 
 
+
 def normalize_2(v):
 def normalize_2(v):
     m = norm(v)
     m = norm(v)
     return [v[0]/m, v[1]/m, v[2]/m]
     return [v[0]/m, v[1]/m, v[2]/m]
 
 
+
 # ------------------------------------------------------------------------------
 # ------------------------------------------------------------------------------
 # Convert a B-spline to polyline with a fixed number of segments
 # Convert a B-spline to polyline with a fixed number of segments
 #
 #
 # FIXME to become adaptive
 # FIXME to become adaptive
 # ------------------------------------------------------------------------------
 # ------------------------------------------------------------------------------
 def spline2Polyline(xyz, degree, closed, segments, knots):
 def spline2Polyline(xyz, degree, closed, segments, knots):
-    '''
+    """
     :param xyz: DXF spline control points
     :param xyz: DXF spline control points
     :param degree: degree of the Spline curve
     :param degree: degree of the Spline curve
     :param closed: closed Spline
     :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 segments: how many lines to use for Spline approximation
     :param knots: DXF spline knots
     :param knots: DXF spline knots
     :return: x,y,z coordinates (each is a list)
     :return: x,y,z coordinates (each is a list)
-    '''
+    """
 
 
     # Check if last point coincide with the first one
     # Check if last point coincide with the first one
     if (Vector(xyz[0]) - Vector(xyz[-1])).length2() < 1e-10:
     if (Vector(xyz[0]) - Vector(xyz[-1])).length2() < 1e-10:
@@ -51,16 +53,16 @@ def spline2Polyline(xyz, degree, closed, segments, knots):
 
 
     npts = len(xyz)
     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:
     # order:
     k = degree+1
     k = degree+1
 
 
     if npts < k:
     if npts < k:
-        #print "not enough control points"
-        return None,None,None
+        # print "not enough control points"
+        return None, None, None
 
 
     # resolution:
     # resolution:
     nseg = segments * npts
     nseg = segments * npts
@@ -72,12 +74,12 @@ def spline2Polyline(xyz, degree, closed, segments, knots):
 
 
     i = 1
     i = 1
     for pt in xyz:
     for pt in xyz:
-        b[i]   = pt[0]
+        b[i] = pt[0]
         b[i+1] = pt[1]
         b[i+1] = pt[1]
         b[i+2] = pt[2]
         b[i+2] = pt[2]
-        i +=3
+        i += 3
 
 
-    #if periodic:
+    # if periodic:
     if closed:
     if closed:
         _rbsplinu(npts, k, nseg, b, h, p, knots)
         _rbsplinu(npts, k, nseg, b, h, p, knots)
     else:
     else:
@@ -86,7 +88,7 @@ def spline2Polyline(xyz, degree, closed, segments, knots):
     x = []
     x = []
     y = []
     y = []
     z = []
     z = []
-    for i in range(1,3*nseg+1,3):
+    for i in range(1, 3*nseg+1, 3):
         x.append(p[i])
         x.append(p[i])
         y.append(p[i+1])
         y.append(p[i+1])
         z.append(p[i+2])
         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)):
 #    for i,xyz in enumerate(zip(x,y,z)):
 #        print i,xyz
 #        print i,xyz
 
 
-    return x,y,z
+    return x, y, z
+
 
 
 # ------------------------------------------------------------------------------
 # ------------------------------------------------------------------------------
 # Subroutine to generate a B-spline open knot vector with multiplicity
 # 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):
 def _knot(n, order):
     x = [0.0]*(n+order+1)
     x = [0.0]*(n+order+1)
     for i in range(2, 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
             x[i] = x[i-1] + 1.0
         else:
         else:
             x[i] = x[i-1]
             x[i] = x[i-1]
     return x
     return x
 
 
+
 # ------------------------------------------------------------------------------
 # ------------------------------------------------------------------------------
 # Subroutine to generate a B-spline uniform (periodic) knot vector.
 # Subroutine to generate a B-spline uniform (periodic) knot vector.
 #
 #
@@ -128,6 +132,7 @@ def _knotu(n, order):
         x[i] = float(i-1)
         x[i] = float(i-1)
     return x
     return x
 
 
+
 # ------------------------------------------------------------------------------
 # ------------------------------------------------------------------------------
 # Subroutine to generate rational B-spline basis functions--open knot vector
 # 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
             temp[i] = 0.0
 
 
     # calculate the higher order non-rational basis functions
     # 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 the lower order basis function is zero skip the calculation
             if temp[i] != 0.0:
             if temp[i] != 0.0:
                 d = ((t-x[i])*temp[i])/(x[i+k-1]-x[i])
                 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
     # calculate sum for denominator of rational basis functions
     s = 0.0
     s = 0.0
-    for i in range(1,npts+1):
+    for i in range(1, npts+1):
         s += temp[i]*h[i]
         s += temp[i]*h[i]
 
 
     # form rational basis functions and put in r vector
     # form rational basis functions and put in r vector
@@ -194,6 +199,7 @@ def _rbasis(c, t, npts, x, h, r):
         else:
         else:
             r[i] = 0
             r[i] = 0
 
 
+
 # ------------------------------------------------------------------------------
 # ------------------------------------------------------------------------------
 # Generates a rational B-spline curve using a uniform open knot vector.
 # 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
             p[icount+j] = 0.0
             # Do local matrix multiplication
             # 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]
+                p[icount+j] += nbasis[i]*b[jcount]
                 jcount += 3
                 jcount += 3
         icount += 3
         icount += 3
         t += step
         t += step
 
 
+
 # ------------------------------------------------------------------------------
 # ------------------------------------------------------------------------------
 # Subroutine to generate a rational B-spline curve using an uniform periodic knot vector
 # 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)
         nbasis = [0.0]*(npts+1)
         _rbasis(k, t, npts, x, h, nbasis)
         _rbasis(k, t, npts, x, h, nbasis)
         # generate a point on the curve
         # generate a point on the curve
-        for j in range(1,4):
+        for j in range(1, 4):
             jcount = j
             jcount = j
             p[icount+j] = 0.0
             p[icount+j] = 0.0
             #  Do local matrix multiplication
             #  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]
                 p[icount+j] += nbasis[i]*b[jcount]
                 jcount += 3
                 jcount += 3
         icount += 3
         icount += 3
         t += step
         t += step
 
 
+
 # Accuracy for comparison operators
 # Accuracy for comparison operators
 _accuracy = 1E-15
 _accuracy = 1E-15
 
 
 
 
 def Cmp0(x):
 def Cmp0(x):
     """Compare against zero within _accuracy"""
     """Compare against zero within _accuracy"""
-    return abs(x)<_accuracy
+    return abs(x) < _accuracy
 
 
 
 
 def gauss(A, B):
 def gauss(A, B):
@@ -337,7 +345,8 @@ def gauss(A, B):
                 j = i
                 j = i
                 ap = api
                 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):
         for i in range(k + 1, n):
             z = A[p[i]][k] / A[p[k]][k]
             z = A[p[i]][k] / A[p[k]][k]
@@ -384,20 +393,22 @@ class Vector(list):
         """Set vector"""
         """Set vector"""
         self[0] = x
         self[0] = x
         self[1] = y
         self[1] = y
-        if z: self[2] = z
+        if z:
+            self[2] = z
 
 
     # ----------------------------------------------------------------------
     # ----------------------------------------------------------------------
     def __repr__(self):
     def __repr__(self):
-        return "[%s]" % (", ".join([repr(x) for x in self]))
+        return "[%s]" % ", ".join([repr(x) for x in self])
 
 
     # ----------------------------------------------------------------------
     # ----------------------------------------------------------------------
     def __str__(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):
     def eq(self, v, acc=_accuracy):
         """Test for equality with vector v within 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
         s2 = 0.0
         for a, b in zip(self, v):
         for a, b in zip(self, v):
             s2 += (a - b) ** 2
             s2 += (a - b) ** 2
@@ -523,12 +534,12 @@ class Vector(list):
     # ----------------------------------------------------------------------
     # ----------------------------------------------------------------------
     def norm(self):
     def norm(self):
         """Normalize vector and return length"""
         """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)):
             for i in range(len(self)):
                 self[i] *= invlen
                 self[i] *= invlen
-        return l
+        return length
 
 
     normalize = norm
     normalize = norm
 
 
@@ -580,8 +591,9 @@ class Vector(list):
         """return containing the direction if normalized with any of the axis"""
         """return containing the direction if normalized with any of the axis"""
 
 
         v = self.clone()
         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:
         if abs(v[0] - 1.0) < zero:
             return "X"
             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            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
 # 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 re, os, sys, glob
 import itertools
 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            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # http://flatcam.org                                       #
 # Author: Juan Pablo Caram (c)                             #
 # Author: Juan Pablo Caram (c)                             #
@@ -17,7 +17,7 @@
 #  * All transformations                                   #
 #  * All transformations                                   #
 #                                                          #
 #                                                          #
 #  Reference: www.w3.org/TR/SVG/Overview.html              #
 #  Reference: www.w3.org/TR/SVG/Overview.html              #
-# ########################################################## ##
+# ##########################################################
 
 
 # import xml.etree.ElementTree as ET
 # import xml.etree.ElementTree as ET
 from svg.path import Line, Arc, CubicBezier, QuadraticBezier, parse_path
 from svg.path import Line, Arc, CubicBezier, QuadraticBezier, parse_path
@@ -136,6 +136,7 @@ def path2shapely(path, object_type, res=1.0):
 
 
     return geometry
     return geometry
 
 
+
 def svgrect2shapely(rect, n_points=32):
 def svgrect2shapely(rect, n_points=32):
     """
     """
     Converts an SVG rect into Shapely geometry.
     Converts an SVG rect into Shapely geometry.
@@ -284,7 +285,7 @@ def svgpolygon2shapely(polygon):
     # return LinearRing(points)
     # 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
     Extracts and flattens all geometry from an SVG node
     into a list of Shapely geometry.
     into a list of Shapely geometry.
@@ -482,6 +483,7 @@ def getsvgtext(node, object_type, units='MM'):
 
 
     return geo
     return geo
 
 
+
 def parse_svg_point_list(ptliststr):
 def parse_svg_point_list(ptliststr):
     """
     """
     Returns a list of coordinate pairs extracted from the "points"
     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            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 
 from FlatCAMTool import FlatCAMTool
 from FlatCAMTool import FlatCAMTool
 from FlatCAMObj import *
 from FlatCAMObj import *
@@ -30,6 +29,7 @@ class ToolCalculator(FlatCAMTool):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
 
 
         self.app = app
         self.app = app
+        self.decimals = 6
 
 
         # ## Title
         # ## Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label = QtWidgets.QLabel("%s" % self.toolName)
@@ -63,13 +63,14 @@ class ToolCalculator(FlatCAMTool):
         grid_units_layout.addWidget(inch_label, 0, 1)
         grid_units_layout.addWidget(inch_label, 0, 1)
 
 
         self.inch_entry = FCEntry()
         self.inch_entry = FCEntry()
+
         # self.inch_entry.setFixedWidth(70)
         # 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.inch_entry.setToolTip(_("Here you enter the value to be converted from INCH to MM"))
 
 
         self.mm_entry = FCEntry()
         self.mm_entry = FCEntry()
         # self.mm_entry.setFixedWidth(130)
         # 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"))
         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)
         grid_units_layout.addWidget(self.mm_entry, 1, 0)
@@ -90,31 +91,35 @@ class ToolCalculator(FlatCAMTool):
         self.layout.addLayout(form_layout)
         self.layout.addLayout(form_layout)
 
 
         self.tipDia_label = QtWidgets.QLabel('%s:' % _("Tip Diameter"))
         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(
         self.tipDia_label.setToolTip(
             _("This is the tool tip diameter.\n"
             _("This is the tool tip diameter.\n"
               "It is specified by manufacturer.")
               "It is specified by manufacturer.")
         )
         )
         self.tipAngle_label = QtWidgets.QLabel('%s:' % _("Tip Angle"))
         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"
         self.tipAngle_label.setToolTip(_("This is the angle of the tip of the tool.\n"
                                          "It is specified by manufacturer."))
                                          "It is specified by manufacturer."))
 
 
         self.cutDepth_label = QtWidgets.QLabel('%s:' % _("Cut Z"))
         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"
         self.cutDepth_label.setToolTip(_("This is the depth to cut into the material.\n"
                                          "In the CNCJob is the CutZ parameter."))
                                          "In the CNCJob is the CutZ parameter."))
 
 
         self.effectiveToolDia_label = QtWidgets.QLabel('%s:' % _("Tool Diameter"))
         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"
         self.effectiveToolDia_label.setToolTip(_("This is the tool diameter to be entered into\n"
                                                  "FlatCAM Gerber section.\n"
                                                  "FlatCAM Gerber section.\n"
                                                  "In the CNCJob section it is called >Tool dia<."))
                                                  "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  "
             _("Calculate either the Cut Z or the effective tool diameter,\n  "
               "depending on which is desired and which is known. ")
               "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 ##
         # ## ElectroPlating Tool Calculator ##
@@ -156,48 +160,54 @@ class ToolCalculator(FlatCAMTool):
         self.layout.addLayout(plate_form_layout)
         self.layout.addLayout(plate_form_layout)
 
 
         self.pcblengthlabel = QtWidgets.QLabel('%s:' % _("Board Length"))
         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.pcblengthlabel.setToolTip(_('This is the board length. In centimeters.'))
 
 
         self.pcbwidthlabel = QtWidgets.QLabel('%s:' % _("Board Width"))
         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.pcbwidthlabel.setToolTip(_('This is the board width.In centimeters.'))
 
 
         self.cdensity_label = QtWidgets.QLabel('%s:' % _("Current Density"))
         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"
         self.cdensity_label.setToolTip(_("Current density to pass through the board. \n"
                                          "In Amps per Square Feet ASF."))
                                          "In Amps per Square Feet ASF."))
 
 
         self.growth_label = QtWidgets.QLabel('%s:' % _("Copper Growth"))
         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"
         self.growth_label.setToolTip(_("How thick the copper growth is intended to be.\n"
                                        "In microns."))
                                        "In microns."))
 
 
         # self.growth_entry.setEnabled(False)
         # self.growth_entry.setEnabled(False)
 
 
         self.cvaluelabel = QtWidgets.QLabel('%s:' % _("Current Value"))
         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'
         self.cvaluelabel.setToolTip(_('This is the current intensity value\n'
                                       'to be set on the Power Supply. In Amps.'))
                                       '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.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'
         self.timelabel.setToolTip(_('This is the calculated time required for the procedure.\n'
                                     'In minutes.'))
                                     '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.pcblengthlabel, self.pcblength_entry)
         plate_form_layout.addRow(self.pcbwidthlabel, self.pcbwidth_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"
             _("Calculate the current intensity value and the procedure time,\n"
               "depending on the parameters above")
               "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.layout.addStretch()
 
 
         self.units = ''
         self.units = ''
 
 
         # ## Signals
         # ## 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.calculate_vshape_button.clicked.connect(self.on_calculate_tool_dia)
 
 
         self.mm_entry.editingFinished.connect(self.on_calculate_inch_units)
         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()
         self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
 
 
         # ## Initialize form
         # ## 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"]
         length = self.app.defaults["tools_calc_electro_length"]
         width = self.app.defaults["tools_calc_electro_width"]
         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 * part_of_real_dia_left_side)
         # effective diameter = tip_diameter + (2 * depth_of_cut * tangent(half_tip_angle))
         # 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
         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)))
         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):
     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):
     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):
     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_current = (length * width * density) * 0.0021527820833419
         calculated_time = copper * 2.142857142857143 * float(20 / density)
         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 FlatCAMTool import FlatCAMTool
 from ObjectCollection import *
 from ObjectCollection import *
 from FlatCAMApp import *
 from FlatCAMApp import *
@@ -22,6 +29,7 @@ class CutOut(FlatCAMTool):
 
 
         self.app = app
         self.app = app
         self.canvas = app.plotcanvas
         self.canvas = app.plotcanvas
+        self.decimals = 4
 
 
         # Title
         # Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label = QtWidgets.QLabel("%s" % self.toolName)
@@ -87,7 +95,9 @@ class CutOut(FlatCAMTool):
         form_layout.addRow(self.kindlabel, self.obj_kind_combo)
         form_layout.addRow(self.kindlabel, self.obj_kind_combo)
 
 
         # Tool Diameter
         # 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 = QtWidgets.QLabel('%s:' % _("Tool dia"))
         self.dia_label.setToolTip(
         self.dia_label.setToolTip(
            _("Diameter of the tool used to cutout\n"
            _("Diameter of the tool used to cutout\n"
@@ -96,7 +106,9 @@ class CutOut(FlatCAMTool):
         form_layout.addRow(self.dia_label, self.dia)
         form_layout.addRow(self.dia_label, self.dia)
 
 
         # Margin
         # Margin
-        self.margin = FCEntry()
+        self.margin = FCDoubleSpinner()
+        self.margin.set_precision(self.decimals)
+
         self.margin_label = QtWidgets.QLabel('%s:' % _("Margin:"))
         self.margin_label = QtWidgets.QLabel('%s:' % _("Margin:"))
         self.margin_label.setToolTip(
         self.margin_label.setToolTip(
            _("Margin over bounds. A positive value here\n"
            _("Margin over bounds. A positive value here\n"
@@ -106,7 +118,9 @@ class CutOut(FlatCAMTool):
         form_layout.addRow(self.margin_label, self.margin)
         form_layout.addRow(self.margin_label, self.margin)
 
 
         # Gapsize
         # Gapsize
-        self.gapsize = FCEntry()
+        self.gapsize = FCDoubleSpinner()
+        self.gapsize.set_precision(self.decimals)
+
         self.gapsize_label = QtWidgets.QLabel('%s:' % _("Gap size:"))
         self.gapsize_label = QtWidgets.QLabel('%s:' % _("Gap size:"))
         self.gapsize_label.setToolTip(
         self.gapsize_label.setToolTip(
            _("The size of the bridge gaps in the cutout\n"
            _("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."))
                                  _("There is no object selected for Cutout.\nSelect one and try again."))
             return
             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}:
         if 0 in {dia}:
             self.app.inform.emit('[WARNING_NOTCL] %s' %
             self.app.inform.emit('[WARNING_NOTCL] %s' %
                                  _("Tool Diameter is zero value. Change it to a positive real number."))
                                  _("Tool Diameter is zero value. Change it to a positive real number."))
@@ -402,27 +406,8 @@ class CutOut(FlatCAMTool):
         except ValueError:
         except ValueError:
             return
             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:
         try:
             gaps = self.gaps.get_value()
             gaps = self.gaps.get_value()
@@ -579,17 +564,7 @@ class CutOut(FlatCAMTool):
         if cutout_obj is None:
         if cutout_obj is None:
             self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(name)))
             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}:
         if 0 in {dia}:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
             self.app.inform.emit('[ERROR_NOTCL] %s' %
                                  _("Tool Diameter is zero value. Change it to a positive real number."))
                                  _("Tool Diameter is zero value. Change it to a positive real number."))
@@ -600,27 +575,8 @@ class CutOut(FlatCAMTool):
         except ValueError:
         except ValueError:
             return
             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:
         try:
             gaps = self.gaps.get_value()
             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.inform.emit(_("Click on the selected geometry object perimeter to create a bridge gap ..."))
         self.app.geo_editor.tool_shape.enabled = True
         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}:
         if 0 in {self.cutting_dia}:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
             self.app.inform.emit('[ERROR_NOTCL] %s' %
                                  _("Tool Diameter is zero value. Change it to a positive real number."))
                                  _("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."
             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()
         name = self.man_object_combo.currentText()
         # Get Geometry source object to be used as target for Manual adding Gaps
         # 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.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)
         self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
 
 
-
     def on_manual_cutout(self, click_pos):
     def on_manual_cutout(self, click_pos):
         name = self.man_object_combo.currentText()
         name = self.man_object_combo.currentText()
 
 
@@ -851,17 +787,7 @@ class CutOut(FlatCAMTool):
                                    "Select a Gerber file and try again."))
                                    "Select a Gerber file and try again."))
             return
             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}:
         if 0 in {dia}:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
             self.app.inform.emit('[ERROR_NOTCL] %s' %
                                  _("Tool Diameter is zero value. Change it to a positive real number."))
                                  _("Tool Diameter is zero value. Change it to a positive real number."))
@@ -872,17 +798,7 @@ class CutOut(FlatCAMTool):
         except ValueError:
         except ValueError:
             return
             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()
         convex_box = self.convex_box.get_value()
 
 
         def geo_init(geo_obj, app_obj):
         def geo_init(geo_obj, app_obj):

+ 12 - 6
flatcamTools/ToolDblSided.py

@@ -19,6 +19,7 @@ class DblSidedTool(FlatCAMTool):
 
 
     def __init__(self, app):
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
+        self.decimals = 4
 
 
         # ## Title
         # ## Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
         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.alignment_holes, 0, 0)
         grid_lay3.addWidget(self.add_drill_point_button, 0, 1)
         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
         # ## Drill diameter for alignment holes
         self.dt_label = QtWidgets.QLabel("<b>%s:</b>" % _('Alignment Drill Diameter'))
         self.dt_label = QtWidgets.QLabel("<b>%s:</b>" % _('Alignment Drill Diameter'))
         self.dt_label.setToolTip(
         self.dt_label.setToolTip(
             _("Diameter of the drill for the "
             _("Diameter of the drill for the "
               "alignment holes.")
               "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 = QtWidgets.QLabel('%s:' % _("Drill dia"))
         self.dd_label.setToolTip(
         self.dd_label.setToolTip(
             _("Diameter of the drill for the "
             _("Diameter of the drill for the "
               "alignment holes.")
               "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()
         hlay2 = QtWidgets.QHBoxLayout()
         self.layout.addLayout(hlay2)
         self.layout.addLayout(hlay2)

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

@@ -1,10 +1,9 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 
 from FlatCAMTool import FlatCAMTool
 from FlatCAMTool import FlatCAMTool
 from FlatCAMObj import *
 from FlatCAMObj import *
@@ -21,9 +20,9 @@ if '_' not in builtins.__dict__:
     _ = gettext.gettext
     _ = gettext.gettext
 
 
 
 
-class Measurement(FlatCAMTool):
+class Distance(FlatCAMTool):
 
 
-    toolName = _("Measurement")
+    toolName = _("Distance Tool")
 
 
     def __init__(self, app):
     def __init__(self, app):
         FlatCAMTool.__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 = QtWidgets.QLabel('%s:' % _("Dy"))
         self.distance_y_label.setToolTip(_("This is the distance measured over the Y axis."))
         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 = QtWidgets.QLabel("<b>%s:</b>" % _('DISTANCE'))
         self.total_distance_label.setToolTip(_("This is the point to point Euclidian distance."))
         self.total_distance_label.setToolTip(_("This is the point to point Euclidian distance."))
 
 
         self.start_entry = FCEntry()
         self.start_entry = FCEntry()
+        self.start_entry.setReadOnly(True)
         self.start_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.start_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.start_entry.setToolTip(_("This is measuring Start point coordinates."))
         self.start_entry.setToolTip(_("This is measuring Start point coordinates."))
 
 
         self.stop_entry = FCEntry()
         self.stop_entry = FCEntry()
+        self.stop_entry.setReadOnly(True)
         self.stop_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.stop_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.stop_entry.setToolTip(_("This is the measuring Stop point coordinates."))
         self.stop_entry.setToolTip(_("This is the measuring Stop point coordinates."))
 
 
         self.distance_x_entry = FCEntry()
         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.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.distance_x_entry.setToolTip(_("This is the distance measured over the X axis."))
         self.distance_x_entry.setToolTip(_("This is the distance measured over the X axis."))
 
 
         self.distance_y_entry = FCEntry()
         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.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.distance_y_entry.setToolTip(_("This is the distance measured over the Y axis."))
         self.distance_y_entry.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 = FCEntry()
+        self.total_distance_entry.setReadOnly(True)
         self.total_distance_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.total_distance_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.total_distance_entry.setToolTip(_("This is the point to point Euclidian distance."))
         self.total_distance_entry.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.stop_label, self.stop_entry)
         form_layout.addRow(self.distance_x_label, self.distance_x_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.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.total_distance_label, self.total_distance_entry)
 
 
         # initial view of the layout
         # initial view of the layout
         self.start_entry.set_value('(0, 0)')
         self.start_entry.set_value('(0, 0)')
         self.stop_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()
         self.layout.addStretch()
 
 
@@ -112,6 +126,12 @@ class Measurement(FlatCAMTool):
 
 
         self.original_call_source = 'app'
         self.original_call_source = 'app'
 
 
+        # store here the event connection ID's
+        self.mm = None
+        self.mr = None
+
+        self.decimals = 4
+
         # VisPy visuals
         # VisPy visuals
         if self.app.is_legacy is False:
         if self.app.is_legacy is False:
             self.sel_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1)
             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)
         self.measure_btn.clicked.connect(self.activate_measure_tool)
 
 
     def run(self, toggle=False):
     def run(self, toggle=False):
-        self.app.report_usage("ToolMeasurement()")
+        self.app.report_usage("ToolDistance()")
 
 
         self.points[:] = []
         self.points[:] = []
 
 
@@ -132,7 +152,7 @@ class Measurement(FlatCAMTool):
         if self.app.tool_tab_locked is True:
         if self.app.tool_tab_locked is True:
             return
             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 the splitter is hidden, display it
         if self.app.ui.splitter.sizes()[0] == 0:
         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.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.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
         # initial view of the layout
         self.start_entry.set_value('(0, 0)')
         self.start_entry.set_value('(0, 0)')
         self.stop_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):
     def activate_measure_tool(self):
         # ENABLE the Measuring TOOL
         # ENABLE the Measuring TOOL
@@ -275,13 +296,13 @@ class Measurement(FlatCAMTool):
         # delete the measuring line
         # delete the measuring line
         self.delete_shape()
         self.delete_shape()
 
 
-        log.debug("Measurement Tool --> exit tool")
+        log.debug("Distance Tool --> exit tool")
 
 
     def on_mouse_click_release(self, event):
     def on_mouse_click_release(self, event):
         # mouse click releases will be accepted only if the left button is clicked
         # mouse click releases will be accepted only if the left button is clicked
         # this is necessary because right mouse click or middle mouse click
         # this is necessary because right mouse click or middle mouse click
         # are used for panning on the canvas
         # 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 event.button == 1:
             if self.app.is_legacy is False:
             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
             # Reset here the relative coordinates so there is a new reference on the click position
             if self.rel_point1 is None:
             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
                 self.rel_point1 = pos
             else:
             else:
                 self.rel_point2 = copy(self.rel_point1)
                 self.rel_point2 = copy(self.rel_point1)
                 self.rel_point1 = pos
                 self.rel_point1 = pos
 
 
             if len(self.points) == 1:
             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 ..."))
                 self.app.inform.emit(_("MEASURING: Click on the Destination point ..."))
             elif len(self.points) == 2:
             elif len(self.points) == 2:
                 dx = self.points[1][0] - self.points[0][0]
                 dx = self.points[1][0] - self.points[0][0]
                 dy = self.points[1][1] - self.points[0][1]
                 dy = self.points[1][1] - self.points[0][1]
                 d = sqrt(dx ** 2 + dy ** 2)
                 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(
                 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()
                 self.deactivate_measure_tool()
 
 
     def on_mouse_move_meas(self, event):
     def on_mouse_move_meas(self, event):
@@ -346,13 +381,16 @@ class Measurement(FlatCAMTool):
 
 
                 # Update cursor
                 # Update cursor
                 self.app.app_cursor.set_data(np.asarray([(pos[0], pos[1])]),
                 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"])
                                              size=self.app.defaults["global_cursor_size"])
             else:
             else:
                 pos = (pos_canvas[0], pos_canvas[1])
                 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:
             if self.rel_point1 is not None:
                 dx = pos[0] - float(self.rel_point1[0])
                 dx = pos[0] - float(self.rel_point1[0])
@@ -361,15 +399,24 @@ class Measurement(FlatCAMTool):
                 dx = pos[0]
                 dx = pos[0]
                 dy = pos[1]
                 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
             # update utility geometry
-
             if len(self.points) == 1:
             if len(self.points) == 1:
                 self.utility_geometry(pos=pos)
                 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:
         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.position_label.setText("")
             self.app.ui.rel_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
         # second draw the new shape of the utility geometry
         meas_line = LineString([pos, self.points[0]])
         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)
         self.sel_shapes.add(meas_line, color=color, update=True, layer=0, tolerance=None)
 
 
         if self.app.is_legacy is True:
         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            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 
 from FlatCAMTool import FlatCAMTool
 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 PyQt5 import QtGui, QtCore, QtWidgets
 
 
+from copy import deepcopy
+
 import gettext
 import gettext
 import FlatCAMTranslation as fcTranslate
 import FlatCAMTranslation as fcTranslate
 import builtins
 import builtins
@@ -27,6 +30,8 @@ class Film(FlatCAMTool):
     def __init__(self, app):
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
 
 
+        self.decimals = 4
+
         # Title
         # Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label.setStyleSheet("""
         title_label.setStyleSheet("""
@@ -39,8 +44,11 @@ class Film(FlatCAMTool):
         self.layout.addWidget(title_label)
         self.layout.addWidget(title_label)
 
 
         # Form Layout
         # 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
         # Type of object for which to create the film
         self.tf_type_obj_combo = QtWidgets.QComboBox()
         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"
               "The selection here decide the type of objects that will be\n"
               "in the Film Object combobox.")
               "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
         # List of objects for which we can create the film
         self.tf_object_combo = QtWidgets.QComboBox()
         self.tf_object_combo = QtWidgets.QComboBox()
@@ -72,7 +81,8 @@ class Film(FlatCAMTool):
         self.tf_object_label.setToolTip(
         self.tf_object_label.setToolTip(
             _("Object for which to create the film.")
             _("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
         # Type of Box Object to be used as an envelope for film creation
         # Within this we can create negative
         # 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"
               "The selection here decide the type of objects that will be\n"
               "in the Box Object combobox.")
               "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
         # Box
         self.tf_box_combo = QtWidgets.QComboBox()
         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"
               "Usually it is the PCB outline but it can be also the\n"
               "same object for which the film is created.")
               "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
         # Film Type
         self.film_type = RadioSet([{'label': _('Positive'), 'value': 'pos'},
         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 = QtWidgets.QLabel(_("Film Type:"))
         self.film_type_label.setToolTip(
         self.film_type_label.setToolTip(
             _("Generate a Positive black film or a Negative film.\n"
             _("Generate a Positive black film or a Negative film.\n"
@@ -122,11 +271,15 @@ class Film(FlatCAMTool):
               "with white on a black canvas.\n"
               "with white on a black canvas.\n"
               "The Film format is SVG.")
               "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
         # 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 = QtWidgets.QLabel('%s:' % _("Border"))
         self.boundary_label.setToolTip(
         self.boundary_label.setToolTip(
             _("Specify a border around the object.\n"
             _("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"
               "white color like the rest and which may confound with the\n"
               "surroundings if not for this border.")
               "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
         # Buttons
         hlay = QtWidgets.QHBoxLayout()
         hlay = QtWidgets.QHBoxLayout()
         self.layout.addLayout(hlay)
         self.layout.addLayout(hlay)
-        hlay.addStretch()
 
 
         self.film_object_button = QtWidgets.QPushButton(_("Save Film"))
         self.film_object_button = QtWidgets.QPushButton(_("Save Film"))
         self.film_object_button.setToolTip(
         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_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
         self.tf_type_box_combo.currentIndexChanged.connect(self.on_type_box_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):
     def on_type_obj_index_changed(self, index):
         obj_type = self.tf_type_obj_combo.currentIndex()
         obj_type = self.tf_type_obj_combo.currentIndex()
         self.tf_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
         self.tf_object_combo.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'
         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.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
         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))
         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):
     def on_film_creation(self):
+        log.debug("ToolFilm.Film.on_film_creation() started ...")
+
         try:
         try:
             name = self.tf_object_combo.currentText()
             name = self.tf_object_combo.currentText()
         except Exception as e:
         except Exception as e:
@@ -238,59 +494,201 @@ class Film(FlatCAMTool):
                                  _("No FlatCAM object selected. Load an object for Box and retry."))
                                  _("No FlatCAM object selected. Load an object for Box and retry."))
             return
             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 ..."))
         self.app.inform.emit(_("Generating Film ..."))
 
 
         if self.film_type.get_value() == "pos":
         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:
             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:
         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
                 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:
             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):
     def reset_fields(self):
         self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         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            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 
 from FlatCAMTool import FlatCAMTool
 from FlatCAMTool import FlatCAMTool
 
 
-from flatcamGUI.GUIElements import RadioSet, FCComboBox, IntEntry
+from flatcamGUI.GUIElements import RadioSet, FCComboBox, FCSpinner
 from PyQt5 import QtGui, QtWidgets
 from PyQt5 import QtGui, QtWidgets
 
 
 import gettext
 import gettext
@@ -59,11 +58,9 @@ class ToolImage(FlatCAMTool):
         ti_form_layout.addRow(self.tf_type_obj_combo_label, self.tf_type_obj_combo)
         ti_form_layout.addRow(self.tf_type_obj_combo_label, self.tf_type_obj_combo)
 
 
         # DPI value of the imported image
         # 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 = 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)
         ti_form_layout.addRow(self.dpi_label, self.dpi_entry)
 
 
         self.emty_lbl = QtWidgets.QLabel("")
         self.emty_lbl = QtWidgets.QLabel("")
@@ -86,7 +83,9 @@ class ToolImage(FlatCAMTool):
         ti2_form_layout.addRow(self.image_type_label, self.image_type)
         ti2_form_layout.addRow(self.image_type_label, self.image_type)
 
 
         # Mask value of the imported image when image monochrome
         # 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 = QtWidgets.QLabel("%s <b>B/W</b>:" % _('Mask value'))
         self.mask_bw_label.setToolTip(
         self.mask_bw_label.setToolTip(
             _("Mask for monochrome image.\n"
             _("Mask for monochrome image.\n"
@@ -99,7 +98,9 @@ class ToolImage(FlatCAMTool):
         ti2_form_layout.addRow(self.mask_bw_label, self.mask_bw_entry)
         ti2_form_layout.addRow(self.mask_bw_label, self.mask_bw_entry)
 
 
         # Mask value of the imported image for RED color when image color
         # 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 = QtWidgets.QLabel("%s <b>R:</b>" % _('Mask value'))
         self.mask_r_label.setToolTip(
         self.mask_r_label.setToolTip(
             _("Mask for RED color.\n"
             _("Mask for RED color.\n"
@@ -110,7 +111,9 @@ class ToolImage(FlatCAMTool):
         ti2_form_layout.addRow(self.mask_r_label, self.mask_r_entry)
         ti2_form_layout.addRow(self.mask_r_label, self.mask_r_entry)
 
 
         # Mask value of the imported image for GREEN color when image color
         # 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 = QtWidgets.QLabel("%s <b>G:</b>" % _('Mask value'))
         self.mask_g_label.setToolTip(
         self.mask_g_label.setToolTip(
             _("Mask for GREEN color.\n"
             _("Mask for GREEN color.\n"
@@ -121,7 +124,9 @@ class ToolImage(FlatCAMTool):
         ti2_form_layout.addRow(self.mask_g_label, self.mask_g_entry)
         ti2_form_layout.addRow(self.mask_g_label, self.mask_g_entry)
 
 
         # Mask value of the imported image for BLUE color when image color
         # 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 = QtWidgets.QLabel("%s <b>B:</b>" % _('Mask value'))
         self.mask_b_label.setToolTip(
         self.mask_b_label.setToolTip(
             _("Mask for BLUE color.\n"
             _("Mask for BLUE color.\n"
@@ -132,15 +137,11 @@ class ToolImage(FlatCAMTool):
         ti2_form_layout.addRow(self.mask_b_label, self.mask_b_entry)
         ti2_form_layout.addRow(self.mask_b_label, self.mask_b_entry)
 
 
         # Buttons
         # Buttons
-        hlay = QtWidgets.QHBoxLayout()
-        self.layout.addLayout(hlay)
-        hlay.addStretch()
-
         self.import_button = QtWidgets.QPushButton(_("Import image"))
         self.import_button = QtWidgets.QPushButton(_("Import image"))
         self.import_button.setToolTip(
         self.import_button.setToolTip(
             _("Open a image of raster type and then import it in FlatCAM.")
             _("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()
         self.layout.addStretch()
 
 

+ 39 - 33
flatcamTools/ToolMove.py

@@ -1,6 +1,5 @@
 # ##########################################################
 # ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
 # MIT Licence                                              #
@@ -50,6 +49,10 @@ class ToolMove(FlatCAMTool):
             from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
             from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
             self.sel_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name="move")
             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)
         self.replot_signal[list].connect(self.replot)
 
 
     def install(self, icon=None, separator=None, **kwargs):
     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'
             # signal that there is a command active and it is 'Move'
             self.app.command_active = "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 ..."))
                 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
                 # draw the selection box
                 self.draw_sel_bbox()
                 self.draw_sel_bbox()
             else:
             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."))
                 self.app.inform.emit('[WARNING_NOTCL] %s' % _("MOVE action cancelled. No object(s) to move."))
 
 
     def on_left_click(self, event):
     def on_left_click(self, event):
@@ -143,7 +151,9 @@ class ToolMove(FlatCAMTool):
                     dx = pos[0] - self.point1[0]
                     dx = pos[0] - self.point1[0]
                     dy = pos[1] - self.point1[1]
                     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):
                     def job_move(app_obj):
                         with self.app.proc_container.new(_("Moving...")) as proc:
                         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."))
                                     self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object(s) selected."))
                                     return "fail"
                                     return "fail"
 
 
+                                # remove any mark aperture shape that may be displayed
                                 for sel_obj in obj_list:
                                 for sel_obj in obj_list:
                                     # if the Gerber mark shapes are enabled they need to be disabled before move
                                     # if the Gerber mark shapes are enabled they need to be disabled before move
                                     if isinstance(sel_obj, FlatCAMGerber):
                                     if isinstance(sel_obj, FlatCAMGerber):
                                         sel_obj.ui.aperture_table_visibility_cb.setChecked(False)
                                         sel_obj.ui.aperture_table_visibility_cb.setChecked(False)
 
 
-                                    # offset solid_geometry
-                                    sel_obj.offset((dx, dy))
-                                    # sel_obj.plot()
-
                                     try:
                                     try:
                                         sel_obj.replotApertures.emit()
                                         sel_obj.replotApertures.emit()
                                     except Exception as e:
                                     except Exception as e:
                                         pass
                                         pass
 
 
+                                for sel_obj in obj_list:
+                                    # offset solid_geometry
+                                    sel_obj.offset((dx, dy))
+
                                     # Update the object bounding box options
                                     # Update the object bounding box options
                                     a, b, c, d = sel_obj.bounds()
                                     a, b, c, d = sel_obj.bounds()
                                     sel_obj.options['xmin'] = a
                                     sel_obj.options['xmin'] = a
@@ -254,38 +265,33 @@ class ToolMove(FlatCAMTool):
         ymaxlist = []
         ymaxlist = []
 
 
         obj_list = self.app.collection.get_selected()
         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()
                 xmin, ymin, xmax, ymax = obj.bounds()
                 xminlist.append(xmin)
                 xminlist.append(xmin)
                 yminlist.append(ymin)
                 yminlist.append(ymin)
                 xmaxlist.append(xmax)
                 xmaxlist.append(xmax)
                 ymaxlist.append(ymax)
                 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):
     def update_sel_bbox(self, pos):
         self.delete_shape()
         self.delete_shape()

+ 165 - 108
flatcamTools/ToolNonCopperClear.py

@@ -1,10 +1,9 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Modified by: Marius Adrian Stanciu (c)              #
 # File Modified by: Marius Adrian Stanciu (c)              #
 # Date: 3/10/2019                                          #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 
 from FlatCAMTool import FlatCAMTool
 from FlatCAMTool import FlatCAMTool
 from copy import copy, deepcopy
 from copy import copy, deepcopy
@@ -27,6 +26,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
 
     def __init__(self, app):
     def __init__(self, app):
         self.app = app
         self.app = app
+        self.decimals = 4
 
 
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
         Gerber.__init__(self, steps_per_circle=self.app.defaults["gerber_circle_steps"])
         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(
         self.addtool_entry_lbl.setToolTip(
             _("Diameter for the new tool to add in the Tool Table")
             _("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)
         form.addRow(self.addtool_entry_lbl, self.addtool_entry)
 
 
         # Tip Dia
         # Tip Dia
         self.tipdialabel = QtWidgets.QLabel('%s:' % _('V-Tip Dia'))
         self.tipdialabel = QtWidgets.QLabel('%s:' % _('V-Tip Dia'))
         self.tipdialabel.setToolTip(
         self.tipdialabel.setToolTip(
             _("The tip diameter for V-Shape Tool"))
             _("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)
         form.addRow(self.tipdialabel, self.tipdia_entry)
 
 
         # Tip Angle
         # Tip Angle
@@ -228,7 +233,10 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.tipanglelabel.setToolTip(
         self.tipanglelabel.setToolTip(
             _("The tip angle for V-Shape Tool.\n"
             _("The tip angle for V-Shape Tool.\n"
               "In degree."))
               "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)
         form.addRow(self.tipanglelabel, self.tipangle_entry)
 
 
         grid2 = QtWidgets.QGridLayout()
         grid2 = QtWidgets.QGridLayout()
@@ -271,7 +279,10 @@ class NonCopperClear(FlatCAMTool, Gerber):
            _("Depth of cut into material. Negative value.\n"
            _("Depth of cut into material. Negative value.\n"
              "In FlatCAM units.")
              "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(
         self.cutz_entry.setToolTip(
            _("Depth of cut into material. Negative value.\n"
            _("Depth of cut into material. Negative value.\n"
              "In FlatCAM units.")
              "In FlatCAM units.")
@@ -305,7 +316,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
             _("Bounding box margin.")
             _("Bounding box margin.")
         )
         )
         grid3.addWidget(nccmarginlabel, 3, 0)
         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)
         grid3.addWidget(self.ncc_margin_entry, 3, 1)
 
 
         # Method
         # Method
@@ -448,30 +461,39 @@ class NonCopperClear(FlatCAMTool, Gerber):
         )
         )
         self.tools_box.addWidget(self.generate_ncc_button)
         self.tools_box.addWidget(self.generate_ncc_button)
         self.tools_box.addStretch()
         self.tools_box.addStretch()
+        # ############################ FINSIHED GUI ###################################
+        # #############################################################################
 
 
+        # #############################################################################
+        # ###################### Setup CONTEXT MENU ###################################
+        # #############################################################################
         self.tools_table.setupContextMenu()
         self.tools_table.setupContextMenu()
         self.tools_table.addContextMenu(
         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(
         self.tools_table.addContextMenu(
             "Delete", lambda:
             "Delete", lambda:
             self.on_tool_delete(rows_to_delete=None, all=None), icon=QtGui.QIcon("share/delete32.png"))
             self.on_tool_delete(rows_to_delete=None, all=None), icon=QtGui.QIcon("share/delete32.png"))
 
 
+        # #############################################################################
+        # ########################## VARIABLES ########################################
+        # #############################################################################
         self.units = ''
         self.units = ''
-        self.ncc_tools = {}
+        self.ncc_tools = dict()
         self.tooluid = 0
         self.tooluid = 0
+
         # store here the default data for Geometry Data
         # store here the default data for Geometry Data
-        self.default_data = {}
+        self.default_data = dict()
 
 
         self.obj_name = ""
         self.obj_name = ""
         self.ncc_obj = None
         self.ncc_obj = None
 
 
-        self.sel_rect = []
+        self.sel_rect = list()
 
 
         self.bound_obj_name = ""
         self.bound_obj_name = ""
         self.bound_obj = None
         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.has_offset = None
         self.o_name = None
         self.o_name = None
         self.overlap = None
         self.overlap = None
@@ -485,11 +507,18 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
 
         self.mm = None
         self.mm = None
         self.mr = None
         self.mr = None
+
         # store here solid_geometry when there are tool with isolation job
         # 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_btn.clicked.connect(self.on_tool_add)
         self.addtool_entry.returnPressed.connect(self.on_tool_add)
         self.addtool_entry.returnPressed.connect(self.on_tool_add)
         self.deltool_btn.clicked.connect(self.on_tool_delete)
         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.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
         self.object_combo.setCurrentIndex(0)
         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):
     def install(self, icon=None, separator=None, **kwargs):
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+N', **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"))
         self.app.ui.notebook.setTabText(2, _("NCC Tool"))
 
 
     def set_tool_ui(self):
     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.tools_frame.show()
 
 
         self.ncc_order_radio.set_value(self.app.defaults["tools_nccorder"])
         self.ncc_order_radio.set_value(self.app.defaults["tools_nccorder"])
@@ -619,7 +671,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
             self.tooluid += 1
             self.tooluid += 1
             self.ncc_tools.update({
             self.ncc_tools.update({
                 int(self.tooluid): {
                 int(self.tooluid): {
-                    'tooldia': float('%.4f' % tool_dia),
+                    'tooldia': float('%.*f' % (self.decimals, tool_dia)),
                     'offset': 'Path',
                     'offset': 'Path',
                     'offset_value': 0.0,
                     'offset_value': 0.0,
                     'type': 'Iso',
                     'type': 'Iso',
@@ -651,7 +703,10 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
 
         sorted_tools = []
         sorted_tools = []
         for k, v in self.ncc_tools.items():
         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()
         order = self.ncc_order_radio.get_value()
         if order == 'fwd':
         if order == 'fwd':
@@ -667,7 +722,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
 
         for tool_sorted in sorted_tools:
         for tool_sorted in sorted_tools:
             for tooluid_key, tooluid_value in self.ncc_tools.items():
             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
                     tool_id += 1
                     id_ = QtWidgets.QTableWidgetItem('%d' % int(tool_id))
                     id_ = QtWidgets.QTableWidgetItem('%d' % int(tool_id))
                     id_.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
                     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
                     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
                     # 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)
                     dia.setFlags(QtCore.Qt.ItemIsEnabled)
 
 
@@ -867,36 +919,10 @@ class NonCopperClear(FlatCAMTool, Gerber):
         else:
         else:
             if self.tool_type_radio.get_value() == 'V':
             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
                 # calculated tool diameter so the cut_z parameter is obeyed
                 tool_dia = tip_dia + 2 * cut_z * math.tan(math.radians(tip_angle))
                 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."))
                 self.app.inform.emit('[WARNING_NOTCL] %s' % _("Please enter a tool diameter to add, in Float format."))
                 return
                 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:
         if tool_dia == 0:
             self.app.inform.emit('[WARNING_NOTCL] %s' % _("Please enter a tool diameter with non-zero value, "
             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 k, v in self.ncc_tools.items():
             for tool_v in v.keys():
             for tool_v in v.keys():
                 if tool_v == 'tooldia':
                 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:
             if muted is None:
                 self.app.inform.emit('[WARNING_NOTCL] %s' % _("Adding tool cancelled. Tool already in Tool Table."))
                 self.app.inform.emit('[WARNING_NOTCL] %s' % _("Adding tool cancelled. Tool already in Tool Table."))
             self.tools_table.itemChanged.connect(self.on_tool_edit)
             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.app.inform.emit('[success] %s' % _("New tool added to Tool Table."))
             self.ncc_tools.update({
             self.ncc_tools.update({
                 int(self.tooluid): {
                 int(self.tooluid): {
-                    'tooldia': float('%.4f' % tool_dia),
+                    'tooldia': float('%.*f' % (self.decimals, tool_dia)),
                     'offset': 'Path',
                     'offset': 'Path',
                     'offset_value': 0.0,
                     'offset_value': 0.0,
                     'type': 'Iso',
                     'type': 'Iso',
@@ -981,7 +1004,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         for k, v in self.ncc_tools.items():
         for k, v in self.ncc_tools.items():
             for tool_v in v.keys():
             for tool_v in v.keys():
                 if tool_v == 'tooldia':
                 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()):
         for row in range(self.tools_table.rowCount()):
 
 
@@ -1061,22 +1084,18 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.build_ui()
         self.build_ui()
 
 
     def on_ncc_click(self):
     def on_ncc_click(self):
+        """
+        Slot for clicking signal of the self.generate.ncc_button
+        :return: None
+        """
 
 
         # init values for the next usage
         # init values for the next usage
         self.reset_usage()
         self.reset_usage()
-
         self.app.report_usage("on_paint_button_click")
         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:
         if self.overlap >= 1 or self.overlap < 0:
             self.app.inform.emit('[ERROR_NOTCL] %s' % _("Overlap value must be between "
             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.connect = self.ncc_connect_cb.get_value()
         self.contour = self.ncc_contour_cb.get_value()
         self.contour = self.ncc_contour_cb.get_value()
-
         self.has_offset = self.ncc_choice_offset_cb.isChecked()
         self.has_offset = self.ncc_choice_offset_cb.isChecked()
-
         self.rest = self.ncc_rest_cb.get_value()
         self.rest = self.ncc_rest_cb.get_value()
 
 
         self.obj_name = self.object_combo.currentText()
         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])
             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])]),
             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
         # update the positions on status bar
         self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
         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 rest: True if to use rest-machining
         :param tools_storage: whether to use the current tools_storage self.ncc_tools or a different one.
         :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.
         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
         :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
         run non-threaded for TclShell usage
         :return:
         :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 #########################################
         # ####### Read the parameters #########################################
@@ -1360,22 +1382,15 @@ class NonCopperClear(FlatCAMTool, Gerber):
         if margin is not None:
         if margin is not None:
             ncc_margin = margin
             ncc_margin = margin
         else:
         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:
         if select_method is not None:
             ncc_select = select_method
             ncc_select = select_method
         else:
         else:
             ncc_select = self.reference_radio.get_value()
             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"]
         connect = connect if connect else self.app.defaults["tools_nccconnect"]
         contour = contour if contour else self.app.defaults["tools_ncccontour"]
         contour = contour if contour else self.app.defaults["tools_ncccontour"]
         order = order if order else self.ncc_order_radio.get_value()
         order = order if order else self.ncc_order_radio.get_value()
@@ -1514,6 +1529,10 @@ class NonCopperClear(FlatCAMTool, Gerber):
             assert isinstance(geo_obj, FlatCAMGeometry), \
             assert isinstance(geo_obj, FlatCAMGeometry), \
                 "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
                 "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.")
             log.debug("NCC Tool. Normal copper clearing task started.")
             self.app.inform.emit(_("NCC Tool. Finished non-copper polygons. 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:
                     else:
                         try:
                         try:
                             for geo_elem in isolated_geo:
                             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:
                                 if self.app.abort_flag:
                                     # graceful abort requested by the user
                                     # graceful abort requested by the user
                                     raise FlatCAMApp.GracefulException
                                     raise FlatCAMApp.GracefulException
@@ -1641,7 +1663,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
                                 break
                                 break
 
 
                         for k, v in tools_storage.items():
                         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)
                                 current_uid = int(k)
                                 # add the solid_geometry to the current too in self.paint_tools dictionary
                                 # add the solid_geometry to the current too in self.paint_tools dictionary
                                 # and then reset the temporary list that stored that solid_geometry
                                 # 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
                     # graceful abort requested by the user
                     raise FlatCAMApp.GracefulException
                     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(
                 app_obj.inform.emit(
                     '[success] %s %s%s %s' % (_('NCC Tool clearing with tool diameter = '),
                     '[success] %s %s%s %s' % (_('NCC Tool clearing with tool diameter = '),
                                               str(tool),
                                               str(tool),
@@ -1730,6 +1756,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
                     if len(area.geoms) > 0:
                     if len(area.geoms) > 0:
                         pol_nr = 0
                         pol_nr = 0
                         for p in area.geoms:
                         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:
                             if self.app.abort_flag:
                                 # graceful abort requested by the user
                                 # graceful abort requested by the user
                                 raise FlatCAMApp.GracefulException
                                 raise FlatCAMApp.GracefulException
@@ -1737,15 +1766,15 @@ class NonCopperClear(FlatCAMTool, Gerber):
                                 try:
                                 try:
                                     if isinstance(p, Polygon):
                                     if isinstance(p, Polygon):
                                         if ncc_method == 'standard':
                                         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,
                                                                     overlap=overlap, contour=contour, connect=connect,
                                                                     prog_plot=prog_plot)
                                                                     prog_plot=prog_plot)
                                         elif ncc_method == 'seed':
                                         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,
                                                                      overlap=overlap, contour=contour, connect=connect,
                                                                      prog_plot=prog_plot)
                                                                      prog_plot=prog_plot)
                                         else:
                                         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,
                                                                      overlap=overlap, contour=contour, connect=connect,
                                                                      prog_plot=prog_plot)
                                                                      prog_plot=prog_plot)
                                         if cp:
                                         if cp:
@@ -1755,19 +1784,19 @@ class NonCopperClear(FlatCAMTool, Gerber):
                                             if pol is not None:
                                             if pol is not None:
                                                 if ncc_method == 'standard':
                                                 if ncc_method == 'standard':
                                                     cp = self.clear_polygon(pol, tool,
                                                     cp = self.clear_polygon(pol, tool,
-                                                                            self.app.defaults["gerber_circle_steps"],
+                                                                            self.grb_circle_steps,
                                                                             overlap=overlap, contour=contour,
                                                                             overlap=overlap, contour=contour,
                                                                             connect=connect,
                                                                             connect=connect,
                                                                             prog_plot=prog_plot)
                                                                             prog_plot=prog_plot)
                                                 elif ncc_method == 'seed':
                                                 elif ncc_method == 'seed':
                                                     cp = self.clear_polygon2(pol, tool,
                                                     cp = self.clear_polygon2(pol, tool,
-                                                                             self.app.defaults["gerber_circle_steps"],
+                                                                             self.grb_circle_steps,
                                                                              overlap=overlap, contour=contour,
                                                                              overlap=overlap, contour=contour,
                                                                              connect=connect,
                                                                              connect=connect,
                                                                              prog_plot=prog_plot)
                                                                              prog_plot=prog_plot)
                                                 else:
                                                 else:
                                                     cp = self.clear_polygon3(pol, tool,
                                                     cp = self.clear_polygon3(pol, tool,
-                                                                             self.app.defaults["gerber_circle_steps"],
+                                                                             self.grb_circle_steps,
                                                                              overlap=overlap, contour=contour,
                                                                              overlap=overlap, contour=contour,
                                                                              connect=connect,
                                                                              connect=connect,
                                                                              prog_plot=prog_plot)
                                                                              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
                             # find the tooluid associated with the current tool_dia so we know where to add the tool
                             # solid_geometry
                             # solid_geometry
                             for k, v in tools_storage.items():
                             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)
                                     current_uid = int(k)
 
 
                                     # add the solid_geometry to the current too in self.paint_tools dictionary
                                     # 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']:
                 if geo_obj.tools[tooluid]['solid_geometry']:
                     has_solid_geo += 1
                     has_solid_geo += 1
             if has_solid_geo == 0:
             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 "
                                                      "Usually it means that the tool diameter is too big "
                                                      "for the painted geometry.\n"
                                                      "for the painted geometry.\n"
                                                      "Change the painting parameters and try again."))
                                                      "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.")
             log.debug("NCC Tool. Rest machining copper clearing task started.")
             app_obj.inform.emit('_(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
             # 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
             # will store the number of tools for which the isolation is broken
             warning_flag = 0
             warning_flag = 0
@@ -1920,6 +1954,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
                     else:
                     else:
                         try:
                         try:
                             for geo_elem in isolated_geo:
                             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:
                                 if self.app.abort_flag:
                                     # graceful abort requested by the user
                                     # graceful abort requested by the user
                                     raise FlatCAMApp.GracefulException
                                     raise FlatCAMApp.GracefulException
@@ -1972,7 +2009,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
                                 break
                                 break
 
 
                         for k, v in tools_storage.items():
                         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)
                                 current_uid = int(k)
                                 # add the solid_geometry to the current too in self.paint_tools dictionary
                                 # add the solid_geometry to the current too in self.paint_tools dictionary
                                 # and then reset the temporary list that stored that solid_geometry
                                 # and then reset the temporary list that stored that solid_geometry
@@ -2047,6 +2085,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
 
                 # Area to clear
                 # Area to clear
                 for poly in cleared_by_last_tool:
                 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:
                     if self.app.abort_flag:
                         # graceful abort requested by the user
                         # graceful abort requested by the user
                         raise FlatCAMApp.GracefulException
                         raise FlatCAMApp.GracefulException
@@ -2083,21 +2124,24 @@ class NonCopperClear(FlatCAMTool, Gerber):
                                 raise FlatCAMApp.GracefulException
                                 raise FlatCAMApp.GracefulException
 
 
                             if p is not None:
                             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):
                                 if isinstance(p, Polygon):
                                     try:
                                     try:
                                         if ncc_method == 'standard':
                                         if ncc_method == 'standard':
                                             cp = self.clear_polygon(p, tool_used,
                                             cp = self.clear_polygon(p, tool_used,
-                                                                    self.app.defaults["gerber_circle_steps"],
+                                                                    self.grb_circle_steps,
                                                                     overlap=overlap, contour=contour, connect=connect,
                                                                     overlap=overlap, contour=contour, connect=connect,
                                                                     prog_plot=prog_plot)
                                                                     prog_plot=prog_plot)
                                         elif ncc_method == 'seed':
                                         elif ncc_method == 'seed':
                                             cp = self.clear_polygon2(p, tool_used,
                                             cp = self.clear_polygon2(p, tool_used,
-                                                                     self.app.defaults["gerber_circle_steps"],
+                                                                     self.grb_circle_steps,
                                                                      overlap=overlap, contour=contour, connect=connect,
                                                                      overlap=overlap, contour=contour, connect=connect,
                                                                      prog_plot=prog_plot)
                                                                      prog_plot=prog_plot)
                                         else:
                                         else:
                                             cp = self.clear_polygon3(p, tool_used,
                                             cp = self.clear_polygon3(p, tool_used,
-                                                                     self.app.defaults["gerber_circle_steps"],
+                                                                     self.grb_circle_steps,
                                                                      overlap=overlap, contour=contour, connect=connect,
                                                                      overlap=overlap, contour=contour, connect=connect,
                                                                      prog_plot=prog_plot)
                                                                      prog_plot=prog_plot)
                                         cleared_geo.append(list(cp.get_objects()))
                                         cleared_geo.append(list(cp.get_objects()))
@@ -2109,22 +2153,25 @@ class NonCopperClear(FlatCAMTool, Gerber):
                                 elif isinstance(p, MultiPolygon):
                                 elif isinstance(p, MultiPolygon):
                                     for poly in p:
                                     for poly in p:
                                         if poly is not None:
                                         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:
                                             try:
                                                 if ncc_method == 'standard':
                                                 if ncc_method == 'standard':
                                                     cp = self.clear_polygon(poly, tool_used,
                                                     cp = self.clear_polygon(poly, tool_used,
-                                                                            self.app.defaults["gerber_circle_steps"],
+                                                                            self.grb_circle_steps,
                                                                             overlap=overlap, contour=contour,
                                                                             overlap=overlap, contour=contour,
                                                                             connect=connect,
                                                                             connect=connect,
                                                                             prog_plot=prog_plot)
                                                                             prog_plot=prog_plot)
                                                 elif ncc_method == 'seed':
                                                 elif ncc_method == 'seed':
                                                     cp = self.clear_polygon2(poly, tool_used,
                                                     cp = self.clear_polygon2(poly, tool_used,
-                                                                             self.app.defaults["gerber_circle_steps"],
+                                                                             self.grb_circle_steps,
                                                                              overlap=overlap, contour=contour,
                                                                              overlap=overlap, contour=contour,
                                                                              connect=connect,
                                                                              connect=connect,
                                                                              prog_plot=prog_plot)
                                                                              prog_plot=prog_plot)
                                                 else:
                                                 else:
                                                     cp = self.clear_polygon3(poly, tool_used,
                                                     cp = self.clear_polygon3(poly, tool_used,
-                                                                             self.app.defaults["gerber_circle_steps"],
+                                                                             self.grb_circle_steps,
                                                                              overlap=overlap, contour=contour,
                                                                              overlap=overlap, contour=contour,
                                                                              connect=connect,
                                                                              connect=connect,
                                                                              prog_plot=prog_plot)
                                                                              prog_plot=prog_plot)
@@ -2172,7 +2219,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
                             # find the tooluid associated with the current tool_dia so we know
                             # find the tooluid associated with the current tool_dia so we know
                             # where to add the tool solid_geometry
                             # where to add the tool solid_geometry
                             for k, v in tools_storage.items():
                             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)
                                     current_uid = int(k)
 
 
                                     # add the solid_geometry to the current too in self.paint_tools dictionary
                                     # add the solid_geometry to the current too in self.paint_tools dictionary
@@ -2219,13 +2267,19 @@ class NonCopperClear(FlatCAMTool, Gerber):
                 else:
                 else:
                     app_obj.new_object("geometry", name, gen_clear_area, plot=plot)
                     app_obj.new_object("geometry", name, gen_clear_area, plot=plot)
             except FlatCAMApp.GracefulException:
             except FlatCAMApp.GracefulException:
-                proc.done()
+                if run_threaded:
+                    proc.done()
                 return
                 return
             except Exception as e:
             except Exception as e:
-                proc.done()
+                if run_threaded:
+                    proc.done()
                 traceback.print_stack()
                 traceback.print_stack()
                 return
                 return
-            proc.done()
+            if run_threaded:
+                proc.done()
+            else:
+                app_obj.proc_container.view.set_idle()
+
             # focus on Selected Tab
             # focus on Selected Tab
             self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
             self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
 
 
@@ -2614,6 +2668,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
         except Exception as e:
         except Exception as e:
             try:
             try:
                 for el in target:
                 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:
                     if self.app.abort_flag:
                         # graceful abort requested by the user
                         # graceful abort requested by the user
                         raise FlatCAMApp.GracefulException
                         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            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 4/23/2019                                          #
 # Date: 4/23/2019                                          #
 # MIT Licence                                              #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 
 from FlatCAMTool import FlatCAMTool
 from FlatCAMTool import FlatCAMTool
 from shapely.geometry import Point, Polygon, LineString
 from shapely.geometry import Point, Polygon, LineString

+ 73 - 77
flatcamTools/ToolPaint.py

@@ -1,6 +1,5 @@
 # ##########################################################
 # ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Modified: Marius Adrian Stanciu (c)                 #
 # File Modified: Marius Adrian Stanciu (c)                 #
 # Date: 3/10/2019                                          #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
 # MIT Licence                                              #
@@ -26,6 +25,7 @@ class ToolPaint(FlatCAMTool, Gerber):
 
 
     def __init__(self, app):
     def __init__(self, app):
         self.app = app
         self.app = app
+        self.decimals = 4
 
 
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
         Geometry.__init__(self, geo_steps_per_circle=self.app.defaults["geometry_circle_steps"])
         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)
         form.addRow(self.order_label, self.order_radio)
 
 
         # ### Add a new Tool ## ##
         # ### 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 = QtWidgets.QLabel('<b>%s:</b>' % _('Tool Dia'))
         self.addtool_entry_lbl.setToolTip(
         self.addtool_entry_lbl.setToolTip(
             _("Diameter for the new tool.")
             _("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()
         grid2 = QtWidgets.QGridLayout()
         self.tools_box.addLayout(grid2)
         self.tools_box.addLayout(grid2)
@@ -200,6 +195,8 @@ class ToolPaint(FlatCAMTool, Gerber):
 
 
         grid3 = QtWidgets.QGridLayout()
         grid3 = QtWidgets.QGridLayout()
         self.tools_box.addLayout(grid3)
         self.tools_box.addLayout(grid3)
+        grid3.setColumnStretch(0, 0)
+        grid3.setColumnStretch(1, 1)
 
 
         # Overlap
         # Overlap
         ovlabel = QtWidgets.QLabel('%s:' % _('Overlap Rate'))
         ovlabel = QtWidgets.QLabel('%s:' % _('Overlap Rate'))
@@ -230,7 +227,9 @@ class ToolPaint(FlatCAMTool, Gerber):
               "be painted.")
               "be painted.")
         )
         )
         grid3.addWidget(marginlabel, 2, 0)
         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)
         grid3.addWidget(self.paintmargin_entry, 2, 1)
 
 
         # Method
         # Method
@@ -351,6 +350,8 @@ class ToolPaint(FlatCAMTool, Gerber):
         self.tools_box.addWidget(self.generate_paint_button)
         self.tools_box.addWidget(self.generate_paint_button)
 
 
         self.tools_box.addStretch()
         self.tools_box.addStretch()
+        # #################################### FINSIHED GUI #####################################
+        # #######################################################################################
 
 
         self.obj_name = ""
         self.obj_name = ""
         self.paint_obj = None
         self.paint_obj = None
@@ -412,7 +413,9 @@ class ToolPaint(FlatCAMTool, Gerber):
 
 
         self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
         self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
 
 
-        # ## Signals
+        # #############################################################################
+        # ################################# Signals ###################################
+        # #############################################################################
         self.addtool_btn.clicked.connect(self.on_tool_add)
         self.addtool_btn.clicked.connect(self.on_tool_add)
         self.addtool_entry.returnPressed.connect(self.on_tool_add)
         self.addtool_entry.returnPressed.connect(self.on_tool_add)
         # self.copytool_btn.clicked.connect(lambda: self.on_tool_copy())
         # 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.box_combo_type.currentIndexChanged.connect(self.on_combo_box_type)
         self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
         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):
     def on_type_obj_index_changed(self, index):
         obj_type = self.type_obj_combo.currentIndex()
         obj_type = self.type_obj_combo.currentIndex()
         self.obj_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
         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):
     def install(self, icon=None, separator=None, **kwargs):
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+P', **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):
     def run(self, toggle=True):
         self.app.report_usage("ToolPaint()")
         self.app.report_usage("ToolPaint()")
 
 
@@ -488,15 +517,6 @@ class ToolPaint(FlatCAMTool, Gerber):
             # disable rest-machining for single polygon painting
             # disable rest-machining for single polygon painting
             self.rest_cb.set_value(False)
             self.rest_cb.set_value(False)
             self.rest_cb.setDisabled(True)
             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':
         if self.selectmethod_combo.get_value() == 'area':
             # disable rest-machining for single polygon painting
             # disable rest-machining for single polygon painting
             self.rest_cb.set_value(False)
             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()
         self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
 
 
         if self.units == "IN":
         if self.units == "IN":
+            self.decimals = 4
             self.addtool_entry.set_value(0.039)
             self.addtool_entry.set_value(0.039)
         else:
         else:
+            self.decimals = 2
             self.addtool_entry.set_value(1)
             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
         # set the working variables to a known state
         self.paint_tools.clear()
         self.paint_tools.clear()
         self.tooluid = 0
         self.tooluid = 0
@@ -559,28 +574,28 @@ class ToolPaint(FlatCAMTool, Gerber):
         self.default_data.update({
         self.default_data.update({
             "name": '_paint',
             "name": '_paint',
             "plot": self.app.defaults["geometry_plot"],
             "plot": self.app.defaults["geometry_plot"],
-            "cutz": self.app.defaults["geometry_cutz"],
+            "cutz": float(self.app.defaults["geometry_cutz"]),
             "vtipdia": 0.1,
             "vtipdia": 0.1,
             "vtipangle": 30,
             "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"],
             "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"],
             "multidepth": self.app.defaults["geometry_multidepth"],
             "ppname_g": self.app.defaults["geometry_ppname_g"],
             "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"],
             "extracut": self.app.defaults["geometry_extracut"],
             "toolchange": self.app.defaults["geometry_toolchange"],
             "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"],
             "spindlespeed": self.app.defaults["geometry_spindlespeed"],
             "toolchangexy": self.app.defaults["geometry_toolchangexy"],
             "toolchangexy": self.app.defaults["geometry_toolchangexy"],
             "startz": self.app.defaults["geometry_startz"],
             "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"],
             "paintmethod": self.app.defaults["tools_paintmethod"],
             "selectmethod": self.app.defaults["tools_selectmethod"],
             "selectmethod": self.app.defaults["tools_selectmethod"],
             "pathconnect": self.app.defaults["tools_pathconnect"],
             "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()
         # 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
         # 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 the Paint Method is "Single" disable the tool table context menu
         if self.default_data["selectmethod"] == "single":
         if self.default_data["selectmethod"] == "single":
@@ -608,7 +623,7 @@ class ToolPaint(FlatCAMTool, Gerber):
 
 
         sorted_tools = []
         sorted_tools = []
         for k, v in self.paint_tools.items():
         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()
         order = self.order_radio.get_value()
         if order == 'fwd':
         if order == 'fwd':
@@ -624,7 +639,7 @@ class ToolPaint(FlatCAMTool, Gerber):
 
 
         for tool_sorted in sorted_tools:
         for tool_sorted in sorted_tools:
             for tooluid_key, tooluid_value in self.paint_tools.items():
             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
                     tool_id += 1
                     id = QtWidgets.QTableWidgetItem('%d' % int(tool_id))
                     id = QtWidgets.QTableWidgetItem('%d' % int(tool_id))
                     id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
                     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
                     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
                     # 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)
                     dia.setFlags(QtCore.Qt.ItemIsEnabled)
 
 
@@ -702,16 +715,7 @@ class ToolPaint(FlatCAMTool, Gerber):
         if dia:
         if dia:
             tool_dia = dia
             tool_dia = dia
         else:
         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:
             if tool_dia is None:
                 self.build_ui()
                 self.build_ui()
@@ -736,9 +740,9 @@ class ToolPaint(FlatCAMTool, Gerber):
         for k, v in self.paint_tools.items():
         for k, v in self.paint_tools.items():
             for tool_v in v.keys():
             for tool_v in v.keys():
                 if tool_v == 'tooldia':
                 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:
             if muted is None:
                 self.app.inform.emit('[WARNING_NOTCL] %s' %
                 self.app.inform.emit('[WARNING_NOTCL] %s' %
                                      _("Adding tool cancelled. Tool already in Tool Table."))
                                      _("Adding tool cancelled. Tool already in Tool Table."))
@@ -750,7 +754,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                                      _("New tool added to Tool Table."))
                                      _("New tool added to Tool Table."))
             self.paint_tools.update({
             self.paint_tools.update({
                 int(self.tooluid): {
                 int(self.tooluid): {
-                    'tooldia': float('%.4f' % tool_dia),
+                    'tooldia': float('%.*f' % (self.decimals, tool_dia)),
                     'offset': 'Path',
                     'offset': 'Path',
                     'offset_value': 0.0,
                     'offset_value': 0.0,
                     'type': 'Iso',
                     'type': 'Iso',
@@ -774,7 +778,7 @@ class ToolPaint(FlatCAMTool, Gerber):
         for k, v in self.paint_tools.items():
         for k, v in self.paint_tools.items():
             for tool_v in v.keys():
             for tool_v in v.keys():
                 if tool_v == 'tooldia':
                 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()):
         for row in range(self.tools_table.rowCount()):
             try:
             try:
@@ -925,16 +929,7 @@ class ToolPaint(FlatCAMTool, Gerber):
         # #####################################################
         # #####################################################
         self.app.inform.emit(_("Paint Tool. Reading parameters."))
         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:
         if self.overlap >= 1 or self.overlap < 0:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
             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])
             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])]),
             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
         # update the positions on status bar
         self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
         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:
             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
                 # 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():
                 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)
                         current_uid = int(k)
                         break
                         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
                 # 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():
                 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)
                         current_uid = int(k)
                         break
                         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
                 # 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():
                 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)
                         current_uid = int(k)
                         break
                         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
                 # 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():
                 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)
                         current_uid = int(k)
                         break
                         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
                 # 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():
                 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)
                         current_uid = int(k)
                         break
                         break
 
 

+ 30 - 72
flatcamTools/ToolPanelize.py

@@ -1,10 +1,9 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 
 from FlatCAMTool import FlatCAMTool
 from FlatCAMTool import FlatCAMTool
 from copy import copy, deepcopy
 from copy import copy, deepcopy
@@ -143,7 +142,10 @@ class Panelize(FlatCAMTool):
         form_layout.addRow(panel_data_label)
         form_layout.addRow(panel_data_label)
 
 
         # Spacing Columns
         # 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 = QtWidgets.QLabel('%s:' % _("Spacing cols"))
         self.spacing_columns_label.setToolTip(
         self.spacing_columns_label.setToolTip(
             _("Spacing between columns of the desired panel.\n"
             _("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)
         form_layout.addRow(self.spacing_columns_label, self.spacing_columns)
 
 
         # Spacing Rows
         # 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 = QtWidgets.QLabel('%s:' % _("Spacing rows"))
         self.spacing_rows_label.setToolTip(
         self.spacing_rows_label.setToolTip(
             _("Spacing between rows of the desired panel.\n"
             _("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)
         form_layout.addRow(self.spacing_rows_label, self.spacing_rows)
 
 
         # Columns
         # Columns
-        self.columns = FCEntry()
+        self.columns = FCSpinner()
+        self.columns.set_range(0, 9999)
+
         self.columns_label = QtWidgets.QLabel('%s:' % _("Columns"))
         self.columns_label = QtWidgets.QLabel('%s:' % _("Columns"))
         self.columns_label.setToolTip(
         self.columns_label.setToolTip(
             _("Number of columns of the desired panel")
             _("Number of columns of the desired panel")
@@ -169,7 +176,9 @@ class Panelize(FlatCAMTool):
         form_layout.addRow(self.columns_label, self.columns)
         form_layout.addRow(self.columns_label, self.columns)
 
 
         # Rows
         # Rows
-        self.rows = FCEntry()
+        self.rows = FCSpinner()
+        self.rows.set_range(0, 9999)
+
         self.rows_label = QtWidgets.QLabel('%s:' % _("Rows"))
         self.rows_label = QtWidgets.QLabel('%s:' % _("Rows"))
         self.rows_label.setToolTip(
         self.rows_label.setToolTip(
             _("Number of rows of the desired panel")
             _("Number of rows of the desired panel")
@@ -200,7 +209,10 @@ class Panelize(FlatCAMTool):
         )
         )
         form_layout.addRow(self.constrain_cb)
         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 = QtWidgets.QLabel('%s:' % _("Width (DX)"))
         self.x_width_lbl.setToolTip(
         self.x_width_lbl.setToolTip(
             _("The width (DX) within which the panel must fit.\n"
             _("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)
         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 = QtWidgets.QLabel('%s:' % _("Height (DY)"))
         self.y_height_lbl.setToolTip(
         self.y_height_lbl.setToolTip(
             _("The height (DY)within which the panel must fit.\n"
             _("The height (DY)within which the panel must fit.\n"
@@ -386,77 +401,20 @@ class Panelize(FlatCAMTool):
 
 
         self.outname = name + '_panelized'
         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
         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
         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
         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
         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())
         panel_type = str(self.panel_type_radio.get_value())
 
 

+ 2 - 3
flatcamTools/ToolPcbWizard.py

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

+ 2 - 3
flatcamTools/ToolProperties.py

@@ -1,10 +1,9 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 
 from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5.QtCore import Qt
 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            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # http://flatcam.org                                       #
 # Author: Juan Pablo Caram (c)                             #
 # Author: Juan Pablo Caram (c)                             #
 # Date: 2/5/2014                                           #
 # Date: 2/5/2014                                           #
 # MIT Licence                                              #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 
 # from PyQt5.QtCore import pyqtSignal
 # from PyQt5.QtCore import pyqtSignal
 from PyQt5.QtCore import Qt
 from PyQt5.QtCore import Qt

+ 48 - 23
flatcamTools/ToolSolderPaste.py

@@ -1,14 +1,13 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 
 from FlatCAMTool import FlatCAMTool
 from FlatCAMTool import FlatCAMTool
 from FlatCAMCommon import LoudDict
 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 FlatCAMApp import log
 from camlib import distance
 from camlib import distance
 from FlatCAMObj import FlatCAMCNCjob
 from FlatCAMObj import FlatCAMCNCjob
@@ -402,6 +401,9 @@ class SolderPaste(FlatCAMTool):
         self.units = ''
         self.units = ''
         self.name = ""
         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
         # this will be used in the combobox context menu, for delete entry
         self.obj_to_be_deleted_name = ''
         self.obj_to_be_deleted_name = ''
 
 
@@ -414,7 +416,7 @@ class SolderPaste(FlatCAMTool):
         # ## Signals
         # ## Signals
         self.combo_context_del_action.triggered.connect(self.on_delete_object)
         self.combo_context_del_action.triggered.connect(self.on_delete_object)
         self.addtool_btn.clicked.connect(self.on_tool_add)
         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.deltool_btn.clicked.connect(self.on_tool_delete)
         self.soldergeo_btn.clicked.connect(self.on_create_geo_click)
         self.soldergeo_btn.clicked.connect(self.on_create_geo_click)
         self.solder_gcode_btn.clicked.connect(self.on_create_gcode_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):
     def install(self, icon=None, separator=None, **kwargs):
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+K', **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):
     def set_tool_ui(self):
         self.form_fields.update({
         self.form_fields.update({
             "tools_solderpaste_new": self.addtool_entry,
             "tools_solderpaste_new": self.addtool_entry,
@@ -499,7 +517,7 @@ class SolderPaste(FlatCAMTool):
             self.tooluid += 1
             self.tooluid += 1
             self.tooltable_tools.update({
             self.tooltable_tools.update({
                 int(self.tooluid): {
                 int(self.tooluid): {
-                    'tooldia': float('%.4f' % tool_dia),
+                    'tooldia': float('%.*f' % (self.decimals, tool_dia)),
                     'data': deepcopy(self.options),
                     'data': deepcopy(self.options),
                     'solid_geometry': []
                     '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()
         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()):
         for name in list(self.app.postprocessors.keys()):
             # populate only with postprocessor files that start with 'Paste_'
             # populate only with postprocessor files that start with 'Paste_'
             if name.partition('_')[0] != 'Paste':
             if name.partition('_')[0] != 'Paste':
@@ -530,7 +553,7 @@ class SolderPaste(FlatCAMTool):
 
 
         sorted_tools = []
         sorted_tools = []
         for k, v in self.tooltable_tools.items():
         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)
         sorted_tools.sort(reverse=True)
 
 
         n = len(sorted_tools)
         n = len(sorted_tools)
@@ -539,7 +562,7 @@ class SolderPaste(FlatCAMTool):
 
 
         for tool_sorted in sorted_tools:
         for tool_sorted in sorted_tools:
             for tooluid_key, tooluid_value in self.tooltable_tools.items():
             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
                     tool_id += 1
                     id = QtWidgets.QTableWidgetItem('%d' % int(tool_id))
                     id = QtWidgets.QTableWidgetItem('%d' % int(tool_id))
                     id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
                     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
                     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
                     # 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)
                     dia.setFlags(QtCore.Qt.ItemIsEnabled)
 
 
@@ -678,7 +698,12 @@ class SolderPaste(FlatCAMTool):
         :param status: what kind of change happened: 'append' or 'delete'
         :param status: what kind of change happened: 'append' or 'delete'
         :return:
         :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':
         if status == 'append':
             idx = self.obj_combo.findText(obj_name)
             idx = self.obj_combo.findText(obj_name)
@@ -791,9 +816,9 @@ class SolderPaste(FlatCAMTool):
         for k, v in self.tooltable_tools.items():
         for k, v in self.tooltable_tools.items():
             for tool_v in v.keys():
             for tool_v in v.keys():
                 if tool_v == 'tooldia':
                 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:
             if muted is None:
                 self.app.inform.emit('[WARNING_NOTCL] %s' %
                 self.app.inform.emit('[WARNING_NOTCL] %s' %
                                      _("Adding Nozzle tool cancelled. Tool already in Tool Table."))
                                      _("Adding Nozzle tool cancelled. Tool already in Tool Table."))
@@ -805,7 +830,7 @@ class SolderPaste(FlatCAMTool):
                                      _("New Nozzle tool added to Tool Table."))
                                      _("New Nozzle tool added to Tool Table."))
             self.tooltable_tools.update({
             self.tooltable_tools.update({
                 int(self.tooluid): {
                 int(self.tooluid): {
-                    'tooldia': float('%.4f' % tool_dia),
+                    'tooldia': float('%.*f' % (self.decimals, tool_dia)),
                     'data': deepcopy(self.options),
                     'data': deepcopy(self.options),
                     'solid_geometry': []
                     'solid_geometry': []
                 }
                 }
@@ -824,7 +849,7 @@ class SolderPaste(FlatCAMTool):
         for k, v in self.tooltable_tools.items():
         for k, v in self.tooltable_tools.items():
             for tool_v in v.keys():
             for tool_v in v.keys():
                 if tool_v == 'tooldia':
                 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()):
         for row in range(self.tools_table.rowCount()):
 
 
@@ -991,7 +1016,7 @@ class SolderPaste(FlatCAMTool):
         for k, v in self.tooltable_tools.items():
         for k, v in self.tooltable_tools.items():
             # make sure that the tools diameter is more than zero and not zero
             # make sure that the tools diameter is more than zero and not zero
             if float(v['tooldia']) > 0:
             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)
         sorted_tools.sort(reverse=True)
 
 
         if not sorted_tools:
         if not sorted_tools:
@@ -1049,7 +1074,7 @@ class SolderPaste(FlatCAMTool):
             for tool in sorted_tools:
             for tool in sorted_tools:
                 offset = tool / 2
                 offset = tool / 2
                 for uid, vl in self.tooltable_tools.items():
                 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)
                         tooluid = int(uid)
                         break
                         break
 
 
@@ -1301,13 +1326,13 @@ class SolderPaste(FlatCAMTool):
         time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
         time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
 
 
         # add the tab if it was closed
         # 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)
         # first clear previous text in text editor (if any)
         self.app.ui.code_editor.clear()
         self.app.ui.code_editor.clear()
 
 
         # Switch plot_area to CNCJob tab
         # 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()
         name = self.cnc_obj_combo.currentText()
         obj = self.app.collection.get_by_name(name)
         obj = self.app.collection.get_by_name(name)

+ 2 - 3
flatcamTools/ToolSub.py

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

+ 156 - 254
flatcamTools/ToolTransform.py

@@ -1,10 +1,9 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 
 from FlatCAMTool import FlatCAMTool
 from FlatCAMTool import FlatCAMTool
 from FlatCAMObj import *
 from FlatCAMObj import *
@@ -29,6 +28,7 @@ class ToolTransform(FlatCAMTool):
 
 
     def __init__(self, app):
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
+        self.decimals = 4
 
 
         self.transform_lay = QtWidgets.QVBoxLayout()
         self.transform_lay = QtWidgets.QVBoxLayout()
         self.layout.addLayout(self.transform_lay)
         self.layout.addLayout(self.transform_lay)
@@ -42,28 +42,20 @@ class ToolTransform(FlatCAMTool):
                         }
                         }
                         """)
                         """)
         self.transform_lay.addWidget(title_label)
         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
         rotate_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.rotateName)
         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 = QtWidgets.QLabel('%s:' % _("Angle"))
         self.rotate_label.setToolTip(
         self.rotate_label.setToolTip(
@@ -72,11 +64,14 @@ class ToolTransform(FlatCAMTool):
               "Positive numbers for CW motion.\n"
               "Positive numbers for CW motion.\n"
               "Negative numbers for CCW motion.")
               "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 = FCButton()
         self.rotate_button.set_value(_("Rotate"))
         self.rotate_button.set_value(_("Rotate"))
@@ -87,32 +82,25 @@ class ToolTransform(FlatCAMTool):
         )
         )
         self.rotate_button.setMinimumWidth(90)
         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
         skew_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.skewName)
         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(
         self.skewx_label.setToolTip(
             _("Angle for Skew action, in degrees.\n"
             _("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 = FCButton()
         self.skewx_button.set_value(_("Skew X"))
         self.skewx_button.set_value(_("Skew X"))
@@ -122,15 +110,19 @@ class ToolTransform(FlatCAMTool):
               "the bounding box for all selected objects."))
               "the bounding box for all selected objects."))
         self.skewx_button.setMinimumWidth(90)
         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(
         self.skewy_label.setToolTip(
             _("Angle for Skew action, in degrees.\n"
             _("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 = FCButton()
         self.skewy_button.set_value(_("Skew Y"))
         self.skewy_button.set_value(_("Skew Y"))
@@ -140,35 +132,24 @@ class ToolTransform(FlatCAMTool):
               "the bounding box for all selected objects."))
               "the bounding box for all selected objects."))
         self.skewy_button.setMinimumWidth(90)
         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
         scale_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.scaleName)
         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(
         self.scalex_label.setToolTip(
             _("Factor for scaling on X axis.")
             _("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 = FCButton()
         self.scalex_button.set_value(_("Scale X"))
         self.scalex_button.set_value(_("Scale X"))
@@ -178,14 +159,18 @@ class ToolTransform(FlatCAMTool):
               "the Scale reference checkbox state."))
               "the Scale reference checkbox state."))
         self.scalex_button.setMinimumWidth(90)
         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(
         self.scaley_label.setToolTip(
             _("Factor for scaling on Y axis.")
             _("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 = FCButton()
         self.scaley_button.set_value(_("Scale Y"))
         self.scaley_button.set_value(_("Scale Y"))
@@ -195,6 +180,10 @@ class ToolTransform(FlatCAMTool):
               "the Scale reference checkbox state."))
               "the Scale reference checkbox state."))
         self.scaley_button.setMinimumWidth(90)
         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 = FCCheckBox()
         self.scale_link_cb.set_value(True)
         self.scale_link_cb.set_value(True)
         self.scale_link_cb.setText(_("Link"))
         self.scale_link_cb.setText(_("Link"))
@@ -202,7 +191,6 @@ class ToolTransform(FlatCAMTool):
             _("Scale the selected object(s)\n"
             _("Scale the selected object(s)\n"
               "using the Scale_X factor for both axis.")
               "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 = FCCheckBox()
         self.scale_zero_ref_cb.set_value(True)
         self.scale_zero_ref_cb.set_value(True)
@@ -213,37 +201,24 @@ class ToolTransform(FlatCAMTool):
               "and the center of the biggest bounding box\n"
               "and the center of the biggest bounding box\n"
               "of the selected objects when unchecked."))
               "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.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
         offset_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.offsetName)
         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(
         self.offx_label.setToolTip(
             _("Distance to offset on X axis. In current units.")
             _("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 = FCButton()
         self.offx_button.set_value(_("Offset X"))
         self.offx_button.set_value(_("Offset X"))
@@ -253,14 +228,18 @@ class ToolTransform(FlatCAMTool):
               "the bounding box for all selected objects.\n"))
               "the bounding box for all selected objects.\n"))
         self.offx_button.setMinimumWidth(90)
         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(
         self.offy_label.setToolTip(
             _("Distance to offset on Y axis. In current units.")
             _("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 = FCButton()
         self.offy_button.set_value(_("Offset Y"))
         self.offy_button.set_value(_("Offset Y"))
@@ -270,43 +249,33 @@ class ToolTransform(FlatCAMTool):
               "the bounding box for all selected objects.\n"))
               "the bounding box for all selected objects.\n"))
         self.offy_button.setMinimumWidth(90)
         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
         flip_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.flipName)
         flip_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.flipName)
         self.transform_lay.addWidget(flip_title_label)
         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 = FCButton()
         self.flipx_button.set_value(_("Flip on X"))
         self.flipx_button.set_value(_("Flip on X"))
         self.flipx_button.setToolTip(
         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 = FCButton()
         self.flipy_button.set_value(_("Flip on Y"))
         self.flipy_button.set_value(_("Flip on Y"))
         self.flipy_button.setToolTip(
         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 = FCCheckBox()
         self.flip_ref_cb.set_value(True)
         self.flip_ref_cb.set_value(True)
@@ -321,17 +290,17 @@ class ToolTransform(FlatCAMTool):
               "Then click Add button to insert coordinates.\n"
               "Then click Add button to insert coordinates.\n"
               "Or enter the coords in format (x, y) in the\n"
               "Or enter the coords in format (x, y) in the\n"
               "Point Entry field and click Flip on X(Y)"))
               "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(
         self.flip_ref_label.setToolTip(
             _("Coordinates in format (x, y) used as reference for mirroring.\n"
             _("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 '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 = 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_entry.setFixedWidth(70)
 
 
         self.flip_ref_button = FCButton()
         self.flip_ref_button = FCButton()
@@ -340,19 +309,16 @@ class ToolTransform(FlatCAMTool):
             _("The point coordinates can be captured by\n"
             _("The point coordinates can be captured by\n"
               "left click on canvas together with pressing\n"
               "left click on canvas together with pressing\n"
               "SHIFT key. Then click Add button to insert."))
               "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()
         self.transform_lay.addStretch()
 
 
         # ## Signals
         # ## Signals
@@ -403,7 +369,7 @@ class ToolTransform(FlatCAMTool):
         self.app.ui.notebook.setTabText(2, _("Transform Tool"))
         self.app.ui.notebook.setTabText(2, _("Transform Tool"))
 
 
     def install(self, icon=None, separator=None, **kwargs):
     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):
     def set_tool_ui(self):
         # ## Initialize form
         # ## Initialize form
@@ -463,33 +429,23 @@ class ToolTransform(FlatCAMTool):
             self.flip_ref_entry.set_value((0, 0))
             self.flip_ref_entry.set_value((0, 0))
 
 
     def on_rotate(self):
     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
         return
 
 
     def on_flipx(self):
     def on_flipx(self):
-        # self.on_flip("Y")
         axis = '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
         return
 
 
     def on_flipy(self):
     def on_flipy(self):
-        # self.on_flip("X")
         axis = '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
         return
 
 
     def on_flip_add_coords(self):
     def on_flip_add_coords(self):
@@ -497,56 +453,27 @@ class ToolTransform(FlatCAMTool):
         self.flip_ref_entry.set_value(val)
         self.flip_ref_entry.set_value(val)
 
 
     def on_skewx(self):
     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'
         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
         return
 
 
     def on_skewy(self):
     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'
         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
         return
 
 
     def on_scalex(self):
     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():
         if self.scale_link_cb.get_value():
             yvalue = xvalue
             yvalue = xvalue
         else:
         else:
@@ -555,80 +482,50 @@ class ToolTransform(FlatCAMTool):
         axis = 'X'
         axis = 'X'
         point = (0, 0)
         point = (0, 0)
         if self.scale_zero_ref_cb.get_value():
         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:
         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
         return
 
 
     def on_scaley(self):
     def on_scaley(self):
         xvalue = 1
         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'
         axis = 'Y'
         point = (0, 0)
         point = (0, 0)
         if self.scale_zero_ref_cb.get_value():
         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:
         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
         return
 
 
     def on_offx(self):
     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'
         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
         return
 
 
     def on_offy(self):
     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'
         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
         return
 
 
     def on_rotate_action(self, num):
     def on_rotate_action(self, num):
@@ -764,6 +661,11 @@ class ToolTransform(FlatCAMTool):
         xminlist = []
         xminlist = []
         yminlist = []
         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:
         if not obj_list:
             self.app.inform.emit('[WARNING_NOTCL] %s' %
             self.app.inform.emit('[WARNING_NOTCL] %s' %
                                  _("No object selected. Please Select an object to shear/skew!"))
                                  _("No object selected. Please Select an object to shear/skew!"))

+ 23 - 11
flatcamTools/__init__.py

@@ -1,22 +1,34 @@
 import sys
 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.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.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.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.ToolPcbWizard import PcbWizard
 from flatcamTools.ToolPDF import ToolPDF
 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.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            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # http://flatcam.org                                       #
 # Author: Juan Pablo Caram (c)                             #
 # Author: Juan Pablo Caram (c)                             #
@@ -11,12 +11,12 @@
 # This is not an aid to install FlatCAM from source on     #
 # This is not an aid to install FlatCAM from source on     #
 # Windows platforms. It is only useful when FlatCAM is up  #
 # Windows platforms. It is only useful when FlatCAM is up  #
 # and running and ready to be packaged.                    #
 # and running and ready to be packaged.                    #
-# ########################################################## ##
+# ##########################################################
 
 
-# ########################################################## ##
-# File Modified (major mod): Marius Adrian Stanciu         #
+# ##########################################################
+# File Modified: Marius Adrian Stanciu                     #
 # Date: 3/10/2019                                          #
 # Date: 3/10/2019                                          #
-# ########################################################## ##
+# ##########################################################
 
 
 
 
 # Files not needed: Qt, tk.dll, tcl.dll, tk/, tcl/, vtk/,
 # 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


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