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

- changes some icons
- added a new GUI element which is a evaluated LineEdit that accepts only float numbers and /,*,+,-,% chars
- finished the Etch Compensation Tool

Marius Stanciu пре 5 година
родитељ
комит
8165c797a4

+ 12 - 1
AppGUI/GUIElements.py

@@ -573,6 +573,7 @@ class EvalEntry(QtWidgets.QLineEdit):
     def __init__(self, parent=None):
         super(EvalEntry, self).__init__(parent)
         self.readyToEdit = True
+
         self.editingFinished.connect(self.on_edit_finished)
 
     def on_edit_finished(self):
@@ -599,7 +600,6 @@ class EvalEntry(QtWidgets.QLineEdit):
 
     def get_value(self):
         raw = str(self.text()).strip(' ')
-        evaled = 0.0
         try:
             evaled = eval(raw)
         except Exception as e:
@@ -656,6 +656,17 @@ class EvalEntry2(QtWidgets.QLineEdit):
         return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
 
 
+class NumericalEvalEntry(EvalEntry):
+    """
+    Will evaluate the input and return a value. Accepts only float numbers and formulas using the operators: /,*,+,-,%
+    """
+    def __init__(self):
+        super().__init__()
+
+        regex = QtCore.QRegExp("[0-9\/\*\+\-\%\.\s]*")
+        validator = QtGui.QRegExpValidator(regex, self)
+        self.setValidator(validator)
+
 class FCSpinner(QtWidgets.QSpinBox):
 
     returnPressed = QtCore.pyqtSignal()

+ 2 - 2
AppGUI/MainGUI.py

@@ -855,7 +855,7 @@ class MainGUI(QtWidgets.QMainWindow):
         self.copy_btn = self.toolbaredit.addAction(
             QtGui.QIcon(self.app.resource_location + '/copy_file32.png'), _("Copy"))
         self.delete_btn = self.toolbaredit.addAction(
-            QtGui.QIcon(self.app.resource_location + '/delete_file32.png'), _("&Delete"))
+            QtGui.QIcon(self.app.resource_location + '/trash32.png'), _("&Delete"))
         self.toolbaredit.addSeparator()
         self.distance_btn = self.toolbaredit.addAction(
             QtGui.QIcon(self.app.resource_location + '/distance32.png'), _("Distance Tool"))
@@ -1851,7 +1851,7 @@ class MainGUI(QtWidgets.QMainWindow):
         self.copy_btn = self.toolbaredit.addAction(
             QtGui.QIcon(self.app.resource_location + '/copy_file32.png'), _("Copy"))
         self.delete_btn = self.toolbaredit.addAction(
-            QtGui.QIcon(self.app.resource_location + '/delete_file32.png'), _("&Delete"))
+            QtGui.QIcon(self.app.resource_location + '/trash32.png'), _("&Delete"))
         self.toolbaredit.addSeparator()
         self.distance_btn = self.toolbaredit.addAction(
             QtGui.QIcon(self.app.resource_location + '/distance32.png'), _("Distance Tool"))

+ 85 - 14
AppObjects/FlatCAMExcellon.py

@@ -31,7 +31,7 @@ if '_' not in builtins.__dict__:
 
 class ExcellonObject(FlatCAMObj, Excellon):
     """
-    Represents Excellon/Drill code.
+    Represents Excellon/Drill code. An object stored in the FlatCAM objects collection (a dict)
     """
 
     ui_type = ExcellonObjectUI
@@ -146,9 +146,11 @@ class ExcellonObject(FlatCAMObj, Excellon):
 
         If only one object is in exc_list parameter then this function will copy that object in the exc_final
 
-        :param exc_list: List or one object of ExcellonObject Objects to join.
-        :param exc_final: Destination ExcellonObject object.
-        :return: None
+        :param exc_list:    List or one object of ExcellonObject Objects to join.
+        :type exc_list:     list
+        :param exc_final:   Destination ExcellonObject object.
+        :type exc_final:    class
+        :return:            None
         """
 
         if decimals is None:
@@ -316,6 +318,12 @@ class ExcellonObject(FlatCAMObj, Excellon):
         exc_final.create_geometry()
 
     def build_ui(self):
+        """
+        Will (re)build the Excellon UI updating it (the tool table)
+
+        :return:    None
+        :rtype:
+        """
         FlatCAMObj.build_ui(self)
 
         # Area Exception - exclusion shape added signal
@@ -586,9 +594,9 @@ class ExcellonObject(FlatCAMObj, Excellon):
         Configures the user interface for this object.
         Connects options to form fields.
 
-        :param ui: User interface object.
-        :type ui: ExcellonObjectUI
-        :return: None
+        :param ui:  User interface object.
+        :type ui:   ExcellonObjectUI
+        :return:    None
         """
         FlatCAMObj.set_ui(self, ui)
 
@@ -729,6 +737,12 @@ class ExcellonObject(FlatCAMObj, Excellon):
         self.ui.operation_radio.setEnabled(False)
 
     def ui_connect(self):
+        """
+        Will connect all signals in the Excellon UI that needs to be connected
+
+        :return:    None
+        :rtype:
+        """
 
         # selective plotting
         for row in range(self.ui.tools_table.rowCount() - 2):
@@ -751,6 +765,12 @@ class ExcellonObject(FlatCAMObj, Excellon):
                 current_widget.returnPressed.connect(self.form_to_storage)
 
     def ui_disconnect(self):
+        """
+        Will disconnect all signals in the Excellon UI that needs to be disconnected
+
+        :return:    None
+        :rtype:
+        """
         # selective plotting
         for row in range(self.ui.tools_table.rowCount()):
             try:
@@ -793,6 +813,12 @@ class ExcellonObject(FlatCAMObj, Excellon):
                     pass
 
     def on_row_selection_change(self):
+        """
+        Called when the user clicks on a row in Tools Table
+
+        :return:    None
+        :rtype:
+        """
         self.ui_disconnect()
 
         sel_rows = []
@@ -843,6 +869,14 @@ class ExcellonObject(FlatCAMObj, Excellon):
         self.ui_connect()
 
     def storage_to_form(self, dict_storage):
+        """
+        Will update the GUI with data from the "storage" in this case the dict self.tools
+
+        :param dict_storage:    A dictionary holding the data relevant for gnerating Gcode from Excellon
+        :type dict_storage:     dict
+        :return:                None
+        :rtype:
+        """
         for form_key in self.form_fields:
             for storage_key in dict_storage:
                 if form_key == storage_key and form_key not in \
@@ -854,6 +888,12 @@ class ExcellonObject(FlatCAMObj, Excellon):
                         pass
 
     def form_to_storage(self):
+        """
+        Will update the 'storage' attribute which is the dict self.tools with data collected from GUI
+
+        :return:    None
+        :rtype:
+        """
         if self.ui.tools_table.rowCount() == 0:
             # there is no tool in tool table so we can't save the GUI elements values to storage
             return
@@ -882,6 +922,14 @@ class ExcellonObject(FlatCAMObj, Excellon):
         self.ui_connect()
 
     def on_operation_type(self, val):
+        """
+        Called by a RadioSet activated_custom signal
+
+        :param val:     Parameter passes by the signal that called this method
+        :type val:      str
+        :return:        None
+        :rtype:
+        """
         if val == 'mill':
             self.ui.mill_type_label.show()
             self.ui.milling_type_radio.show()
@@ -912,8 +960,8 @@ class ExcellonObject(FlatCAMObj, Excellon):
         Returns the keys to the self.tools dictionary corresponding
         to the selections on the tool list in the AppGUI.
 
-        :return: List of tools.
-        :rtype: list
+        :return:    List of tools.
+        :rtype:     list
         """
 
         return [str(x.text()) for x in self.ui.tools_table.selectedItems()]
@@ -922,8 +970,8 @@ class ExcellonObject(FlatCAMObj, Excellon):
         """
         Returns a list of lists, each list in the list is made out of row elements
 
-        :return: List of table_tools items.
-        :rtype: list
+        :return:    List of table_tools items.
+        :rtype:     list
         """
         table_tools_items = []
         for x in self.ui.tools_table.selectedItems():
@@ -951,7 +999,21 @@ class ExcellonObject(FlatCAMObj, Excellon):
     def export_excellon(self, whole, fract, e_zeros=None, form='dec', factor=1, slot_type='routing'):
         """
         Returns two values, first is a boolean , if 1 then the file has slots and second contain the Excellon code
-        :return: has_slots and Excellon_code
+
+        :param whole:       Integer part digits
+        :type whole:        int
+        :param fract:       Fractional part digits
+        :type fract:        int
+        :param e_zeros:     Excellon zeros suppression: LZ or TZ
+        :type e_zeros:      str
+        :param form:        Excellon format: 'dec',
+        :type form:         str
+        :param factor:      Conversion factor
+        :type factor:       float
+        :param slot_type:   How to treat slots: "routing" or "drilling"
+        :type slot_type:    str
+        :return:            A tuple: (has_slots, Excellon_code) -> (bool, str)
+        :rtype:             tuple
         """
 
         excellon_code = ''
@@ -1123,8 +1185,8 @@ class ExcellonObject(FlatCAMObj, Excellon):
         object's options and returns a (success, msg) tuple as feedback
         for shell operations.
 
-        :return: Success/failure condition tuple (bool, str).
-        :rtype: tuple
+        :return:    Success/failure condition tuple (bool, str).
+        :rtype:     tuple
         """
 
         # Get the tools from the list. These are keys
@@ -1167,6 +1229,15 @@ class ExcellonObject(FlatCAMObj, Excellon):
                 return False, "Error: Milling tool is larger than hole."
 
         def geo_init(geo_obj, app_obj):
+            """
+
+            :param geo_obj:     New object
+            :type geo_obj:      GeometryObject
+            :param app_obj:     App
+            :type app_obj:      FlatCAMApp.App
+            :return:
+            :rtype:
+            """
             assert geo_obj.kind == 'geometry', "Initializer expected a GeometryObject, got %s" % type(geo_obj)
 
             # ## Add properties to the object

+ 2 - 3
AppObjects/FlatCAMGerber.py

@@ -896,7 +896,7 @@ class GerberObject(FlatCAMObj, Gerber):
                 })
 
                 for nr_pass in range(passes):
-                    iso_offset = dia * ((2 * nr_pass + 1) / 2.0) - (nr_pass * overlap * dia)
+                    iso_offset = dia * ((2 * nr_pass + 1) / 2.0000001) - (nr_pass * overlap * dia)
 
                     # if milling type is climb then the move is counter-clockwise around features
                     mill_dir = 1 if milling_type == 'cl' else 0
@@ -945,8 +945,7 @@ class GerberObject(FlatCAMObj, Gerber):
             self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot)
         else:
             for i in range(passes):
-
-                offset = dia * ((2 * i + 1) / 2.0) - (i * overlap * dia)
+                offset = dia * ((2 * i + 1) / 2.0000001) - (i * overlap * dia)
                 if passes > 1:
                     if outname is None:
                         if self.iso_type == 0:

+ 97 - 77
AppTools/ToolEtchCompensation.py

@@ -8,9 +8,9 @@
 from PyQt5 import QtWidgets, QtCore
 
 from AppTool import AppTool
-from AppGUI.GUIElements import FCButton, FCDoubleSpinner, RadioSet, FCComboBox
+from AppGUI.GUIElements import FCButton, FCDoubleSpinner, RadioSet, FCComboBox, NumericalEvalEntry
 
-from shapely.geometry import box
+from shapely.ops import unary_union
 
 from copy import deepcopy
 
@@ -95,8 +95,8 @@ class ToolEtchCompensation(AppTool):
         self.thick_entry.set_range(0.0000, 9999.9999)
         self.thick_entry.setObjectName(_("Thickness"))
 
-        grid0.addWidget(self.thick_label, 5, 0, 1, 2)
-        grid0.addWidget(self.thick_entry, 6, 0, 1, 2)
+        grid0.addWidget(self.thick_label, 5, 0)
+        grid0.addWidget(self.thick_entry, 5, 1)
 
         self.ratio_label = QtWidgets.QLabel('%s:' % _("Ratio"))
         self.ratio_label.setToolTip(
@@ -106,17 +106,48 @@ class ToolEtchCompensation(AppTool):
               "- preselection -> value which depends on a selection of etchants")
         )
         self.ratio_radio = RadioSet([
-            {'label': _('PreSelection'), 'value': 'p'},
-            {'label': _('Custom'), 'value': 'c'}
-        ])
+            {'label': _('Custom'), 'value': 'c'},
+            {'label': _('PreSelection'), 'value': 'p'}
+        ], orientation='vertical', stretch=False)
 
         grid0.addWidget(self.ratio_label, 7, 0, 1, 2)
         grid0.addWidget(self.ratio_radio, 8, 0, 1, 2)
 
+        # Etchants
+        self.etchants_label = QtWidgets.QLabel('%s:' % _('Etchants'))
+        self.etchants_label.setToolTip(
+            _("A list of etchants.")
+        )
+        self.etchants_combo = FCComboBox(callback=self.confirmation_message)
+        self.etchants_combo.setObjectName(_("Etchants"))
+        self.etchants_combo.addItems(["CuCl2", "FeCl3"])
+
+        grid0.addWidget(self.etchants_label, 9, 0)
+        grid0.addWidget(self.etchants_combo, 9, 1)
+
+        # Etch Factor
+        self.factor_label = QtWidgets.QLabel('%s:' % _('Etch factor'))
+        self.factor_label.setToolTip(
+            _("The ratio between depth etch and lateral etch .\n"
+              "Accepts real numbers and formulas using the operators: /,*,+,-,%")
+        )
+        self.factor_entry = NumericalEvalEntry()
+        self.factor_entry.setPlaceholderText(_("Real number or formula"))
+        self.factor_entry.setObjectName(_("Etch_factor"))
+
+        # Hide the Etchants and Etch factor
+        self.etchants_label.hide()
+        self.etchants_combo.hide()
+        self.factor_label.hide()
+        self.factor_entry.hide()
+
+        grid0.addWidget(self.factor_label, 10, 0)
+        grid0.addWidget(self.factor_entry, 10, 1)
+
         separator_line = QtWidgets.QFrame()
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
-        grid0.addWidget(separator_line, 9, 0, 1, 2)
+        grid0.addWidget(separator_line, 13, 0, 1, 2)
 
         self.compensate_btn = FCButton(_('Compensate'))
         self.compensate_btn.setToolTip(
@@ -128,7 +159,7 @@ class ToolEtchCompensation(AppTool):
                             font-weight: bold;
                         }
                         """)
-        grid0.addWidget(self.compensate_btn, 10, 0, 1, 2)
+        grid0.addWidget(self.compensate_btn, 14, 0, 1, 2)
 
         self.tools_box.addStretch()
 
@@ -145,7 +176,7 @@ class ToolEtchCompensation(AppTool):
                         """)
         self.tools_box.addWidget(self.reset_button)
 
-        self.compensate_btn.clicked.connect(self.on_grb_invert)
+        self.compensate_btn.clicked.connect(self.on_compensate)
         self.reset_button.clicked.connect(self.set_tool_ui)
         self.ratio_radio.activated_custom.connect(self.on_ratio_change)
 
@@ -153,8 +184,8 @@ class ToolEtchCompensation(AppTool):
         AppTool.install(self, icon, separator, shortcut='', **kwargs)
 
     def run(self, toggle=True):
-        self.app.defaults.report_usage("ToolInvertGerber()")
-        log.debug("ToolInvertGerber() is running ...")
+        self.app.defaults.report_usage("ToolEtchCompensation()")
+        log.debug("ToolEtchCompensation() is running ...")
 
         if toggle:
             # if the splitter is hidden, display it, else hide it but only if the current widget is the same
@@ -178,28 +209,40 @@ class ToolEtchCompensation(AppTool):
         AppTool.run(self)
         self.set_tool_ui()
 
-        self.app.ui.notebook.setTabText(2, _("Invert Tool"))
+        self.app.ui.notebook.setTabText(2, _("Etch Compensation Tool"))
 
     def set_tool_ui(self):
-        self.thick_entry.set_value(18)
-        self.ratio_radio.set_value('p')
+        self.thick_entry.set_value(18.0)
+        self.ratio_radio.set_value('c')
 
     def on_ratio_change(self, val):
-        pass
-
-    def on_grb_invert(self):
-        margin = self.margin_entry.get_value()
-        if round(margin, self.decimals) == 0.0:
-            margin = 1E-10
+        """
+        Called on activated_custom signal of the RadioSet GUI element self.radio_ratio
+
+        :param val:     'c' or 'p': 'c' means custom factor and 'p' means preselected etchants
+        :type val:      str
+        :return:        None
+        :rtype:
+        """
+        if val == 'c':
+            self.etchants_label.hide()
+            self.etchants_combo.hide()
+            self.factor_label.show()
+            self.factor_entry.show()
+        else:
+            self.etchants_label.show()
+            self.etchants_combo.show()
+            self.factor_label.hide()
+            self.factor_entry.hide()
 
-        join_style = {'r': 1, 'b': 3, 's': 2}[self.join_radio.get_value()]
-        if join_style is None:
-            join_style = 'r'
+    def on_compensate(self):
+        ratio_type = self.ratio_radio.get_value()
+        thickness = self.thick_entry.get_value() / 1000     # in microns
 
         grb_circle_steps = int(self.app.defaults["gerber_circle_steps"])
         obj_name = self.gerber_combo.currentText()
 
-        outname = obj_name + "_inverted"
+        outname = obj_name + "_comp"
 
         # Get source object.
         try:
@@ -214,74 +257,51 @@ class ToolEtchCompensation(AppTool):
             self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(obj_name)))
             return
 
-        xmin, ymin, xmax, ymax = grb_obj.bounds()
-
-        grb_box = box(xmin, ymin, xmax, ymax).buffer(margin, resolution=grb_circle_steps, join_style=join_style)
+        if ratio_type == 'c':
+            etch_factor = 1 / self.factor_entry.get_value()
+        else:
+            etchant = self.etchants_combo.get_value()
+            if etchant == "CuCl2":
+                etch_factor = 0.33
+            else:
+                etch_factor = 0.25
+        offset = thickness / etch_factor
 
         try:
             __ = iter(grb_obj.solid_geometry)
         except TypeError:
             grb_obj.solid_geometry = list(grb_obj.solid_geometry)
 
-        new_solid_geometry = deepcopy(grb_box)
+        new_solid_geometry = []
 
         for poly in grb_obj.solid_geometry:
-            new_solid_geometry = new_solid_geometry.difference(poly)
+            new_solid_geometry.append(poly.buffer(offset, int(grb_circle_steps)))
+        new_solid_geometry = unary_union(new_solid_geometry)
 
         new_options = {}
         for opt in grb_obj.options:
             new_options[opt] = deepcopy(grb_obj.options[opt])
 
-        new_apertures = {}
-
-        # for apid, val in grb_obj.apertures.items():
-        #     new_apertures[apid] = {}
-        #     for key in val:
-        #         if key == 'geometry':
-        #             new_apertures[apid]['geometry'] = []
-        #             for elem in val['geometry']:
-        #                 geo_elem = {}
-        #                 if 'follow' in elem:
-        #                     try:
-        #                         geo_elem['clear'] = elem['follow'].buffer(val['size'] / 2.0).exterior
-        #                     except AttributeError:
-        #                         # TODO should test if width or height is bigger
-        #                         geo_elem['clear'] = elem['follow'].buffer(val['width'] / 2.0).exterior
-        #                 if 'clear' in elem:
-        #                     if isinstance(elem['clear'], Polygon):
-        #                         try:
-        #                             geo_elem['solid'] = elem['clear'].buffer(val['size'] / 2.0, grb_circle_steps)
-        #                         except AttributeError:
-        #                             # TODO should test if width or height is bigger
-        #                             geo_elem['solid'] = elem['clear'].buffer(val['width'] / 2.0, grb_circle_steps)
-        #                     else:
-        #                         geo_elem['follow'] = elem['clear']
-        #                 new_apertures[apid]['geometry'].append(deepcopy(geo_elem))
-        #         else:
-        #             new_apertures[apid][key] = deepcopy(val[key])
-
-        if '0' not in new_apertures:
-            new_apertures['0'] = {}
-            new_apertures['0']['type'] = 'C'
-            new_apertures['0']['size'] = 0.0
-            new_apertures['0']['geometry'] = []
-
-        try:
-            for poly in new_solid_geometry:
-                new_el = {}
-                new_el['solid'] = poly
-                new_el['follow'] = poly.exterior
-                new_apertures['0']['geometry'].append(new_el)
-        except TypeError:
-            new_el = {}
-            new_el['solid'] = new_solid_geometry
-            new_el['follow'] = new_solid_geometry.exterior
-            new_apertures['0']['geometry'].append(new_el)
+        new_apertures = deepcopy(grb_obj.apertures)
 
-        for td in new_apertures:
-            print(td, new_apertures[td])
+        for ap in new_apertures:
+            for k in ap:
+                if k == 'geometry':
+                    for geo_el in new_apertures[ap]['geometry']:
+                        if 'solid' in geo_el:
+                            geo_el['solid'] = geo_el['solid'].buffer(offset, int(grb_circle_steps))
 
         def init_func(new_obj, app_obj):
+            """
+            Init a new object in FlatCAM Object collection
+
+            :param new_obj:     New object
+            :type new_obj:      ObjectCollection
+            :param app_obj:     App
+            :type app_obj:      App_Main.App
+            :return:            None
+            :rtype:
+            """
             new_obj.options.update(new_options)
             new_obj.options['name'] = outname
             new_obj.fill_color = deepcopy(grb_obj.fill_color)

+ 0 - 3
AppTools/ToolInvertGerber.py

@@ -278,9 +278,6 @@ class ToolInvertGerber(AppTool):
             new_el['follow'] = new_solid_geometry.exterior
             new_apertures['0']['geometry'].append(new_el)
 
-        for td in new_apertures:
-            print(td, new_apertures[td])
-
         def init_func(new_obj, app_obj):
             new_obj.options.update(new_options)
             new_obj.options['name'] = outname

+ 20 - 10
App_Main.py

@@ -42,7 +42,7 @@ import socket
 # ###################################      Imports part of FlatCAM       #############################################
 # ####################################################################################################################
 
-# Diverse
+# Various
 from Common import LoudDict
 from Common import color_variant
 from Common import ExclusionAreas
@@ -53,8 +53,10 @@ from AppDatabase import ToolsDB2
 from vispy.gloo.util import _screenshot
 from vispy.io import write_png
 
-# FlatCAM Objects
+# FlatCAM defaults (preferences)
 from defaults import FlatCAMDefaults
+
+# FlatCAM Objects
 from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 from AppGUI.preferences.PreferencesUIManager import PreferencesUIManager
 from AppObjects.ObjectCollection import *
@@ -105,7 +107,7 @@ if '_' not in builtins.__dict__:
 
 class App(QtCore.QObject):
     """
-    The main application class. The constructor starts the AppGUI.
+    The main application class. The constructor starts the GUI and all other classes used by the program.
     """
 
     # ###############################################################################################################
@@ -289,7 +291,7 @@ class App(QtCore.QObject):
             self.new_launch.start.emit()
 
         # ############################################################################################################
-        # # ######################################## OS-specific #####################################################
+        # ########################################## OS-specific #####################################################
         # ############################################################################################################
         portable = False
 
@@ -401,13 +403,12 @@ class App(QtCore.QObject):
             json.dump([], fp)
             fp.close()
 
-        # Application directory. CHDIR to it. Otherwise, trying to load
-        # GUI icons will fail as their path is relative.
+        # Application directory. CHDIR to it. Otherwise, trying to load GUI icons will fail as their path is relative.
         # This will fail under cx_freeze ...
         self.app_home = os.path.dirname(os.path.realpath(__file__))
 
-        App.log.debug("Application path is " + self.app_home)
-        App.log.debug("Started in " + os.getcwd())
+        log.debug("Application path is " + self.app_home)
+        log.debug("Started in " + os.getcwd())
 
         # cx_freeze workaround
         if os.path.isfile(self.app_home):
@@ -451,7 +452,6 @@ class App(QtCore.QObject):
         # ###########################################################################################################
         # ###################################### Setting the Splash Screen ##########################################
         # ###########################################################################################################
-
         splash_settings = QSettings("Open Source", "FlatCAM")
         if splash_settings.contains("splash_screen"):
             show_splash = splash_settings.value("splash_screen")
@@ -1923,7 +1923,7 @@ class App(QtCore.QObject):
         self.corners_tool.install(icon=QtGui.QIcon(self.resource_location + '/corners_32.png'), pos=self.ui.menutool)
 
         self.etch_tool = ToolEtchCompensation(self)
-        self.etch_tool.install(icon=QtGui.QIcon(self.resource_location + '/etcg_32.png'), pos=self.ui.menutool)
+        self.etch_tool.install(icon=QtGui.QIcon(self.resource_location + '/etch_32.png'), pos=self.ui.menutool)
 
         self.transform_tool = ToolTransform(self)
         self.transform_tool.install(icon=QtGui.QIcon(self.resource_location + '/transform.png'),
@@ -4811,6 +4811,16 @@ class App(QtCore.QObject):
         self.defaults.report_usage("on_copy_command()")
 
         def initialize(obj_init, app):
+            """
+
+            :param obj_init:    the new object
+            :type obj_init:     class
+            :param app:         An instance of the App class
+            :type app:          App
+            :return:            None
+            :rtype:
+            """
+
             obj_init.solid_geometry = deepcopy(obj.solid_geometry)
             try:
                 obj_init.follow_geometry = deepcopy(obj.follow_geometry)

+ 6 - 0
CHANGELOG.md

@@ -7,6 +7,12 @@ CHANGELOG for FlatCAM beta
 
 =================================================
 
+24.05.2020
+
+- changes some icons
+- added a new GUI element which is a evaluated LineEdit that accepts only float numbers and /,*,+,-,% chars
+- finished the Etch Compensation Tool
+
 23.05.2020
 
 - fixed a issue when testing for Exclusion areas overlap over the Geometry object solid_geometry

+ 52 - 9
Common.py

@@ -12,7 +12,7 @@
 # ##########################################################
 from PyQt5 import QtCore
 
-from shapely.geometry import Polygon, MultiPolygon, Point, LineString
+from shapely.geometry import Polygon, Point, LineString
 from shapely.ops import unary_union
 
 from AppGUI.VisPyVisuals import ShapeCollection
@@ -32,7 +32,9 @@ if '_' not in builtins.__dict__:
 
 
 class GracefulException(Exception):
-    # Graceful Exception raised when the user is requesting to cancel the current threaded task
+    """
+    Graceful Exception raised when the user is requesting to cancel the current threaded task
+    """
     def __init__(self):
         super().__init__()
 
@@ -107,8 +109,11 @@ def color_variant(hex_color, bright_factor=1):
     Takes a color in HEX format #FF00FF and produces a lighter or darker variant
 
     :param hex_color:           color to change
-    :param bright_factor:   factor to change the color brightness [0 ... 1]
-    :return:                    modified color
+    :type hex_color:            str
+    :param bright_factor:       factor to change the color brightness [0 ... 1]
+    :type bright_factor:        float
+    :return:                    Modified color
+    :rtype:                     str
     """
 
     if len(hex_color) != 7:
@@ -133,7 +138,9 @@ def color_variant(hex_color, bright_factor=1):
 
 
 class ExclusionAreas(QtCore.QObject):
-
+    """
+    Functionality for adding Exclusion Areas for the Excellon and Geometry FlatCAM Objects
+    """
     e_shape_modified = QtCore.pyqtSignal()
 
     def __init__(self, app):
@@ -230,6 +237,14 @@ class ExclusionAreas(QtCore.QObject):
 
     # To be called after clicking on the plot.
     def on_mouse_release(self, event):
+        """
+        Called on mouse click release.
+
+        :param event:   Mouse event
+        :type event:
+        :return:        None
+        :rtype:
+        """
         if self.app.is_legacy is False:
             event_pos = event.pos
             # event_is_dragging = event.is_dragging
@@ -417,6 +432,13 @@ class ExclusionAreas(QtCore.QObject):
             self.e_shape_modified.emit()
 
     def area_disconnect(self):
+        """
+        Will do the cleanup. Will disconnect the mouse events for the custom handlers in this class and initialize
+        certain class attributes.
+
+        :return:    None
+        :rtype:
+        """
         if self.app.is_legacy is False:
             self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
             self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
@@ -441,8 +463,15 @@ class ExclusionAreas(QtCore.QObject):
         self.app.call_source = "app"
         self.app.inform.emit("[WARNING_NOTCL] %s" % _("Cancelled. Area exclusion drawing was interrupted."))
 
-    # called on mouse move
     def on_mouse_move(self, event):
+        """
+        Called on mouse move
+
+        :param event:   mouse event
+        :type event:
+        :return:        None
+        :rtype:
+        """
         shape_type = self.shape_type_button.get_value()
 
         if self.app.is_legacy is False:
@@ -513,6 +542,12 @@ class ExclusionAreas(QtCore.QObject):
                 data=(curr_pos[0], curr_pos[1]))
 
     def on_clear_area_click(self):
+        """
+        Slot for clicking the button for Deleting all the Exclusion areas.
+
+        :return:    None
+        :rtype:
+        """
         self.clear_shapes()
 
         # restore the default StyleSheet
@@ -527,6 +562,12 @@ class ExclusionAreas(QtCore.QObject):
         self.cnc_button.setToolTip('%s' % _("Generate the CNC Job object."))
 
     def clear_shapes(self):
+        """
+        Will delete all the Exclusion areas; will delete on canvas any possible selection box for the Exclusion areas.
+
+        :return:    None
+        :rtype:
+        """
         self.exclusion_areas_storage.clear()
         AppTool.delete_moving_selection_shape(self)
         self.app.delete_selection_shape()
@@ -536,8 +577,9 @@ class ExclusionAreas(QtCore.QObject):
     def delete_sel_shapes(self, idxs):
         """
 
-        :param idxs: list of indexes in self.exclusion_areas_storage list to be deleted
-        :return:
+        :param idxs:    list of indexes in self.exclusion_areas_storage list to be deleted
+        :type idxs:     list
+        :return:        None
         """
 
         # delete all plotted shapes
@@ -583,7 +625,8 @@ class ExclusionAreas(QtCore.QObject):
 
     def travel_coordinates(self, start_point, end_point, tooldia):
         """
-        WIll create a path the go around the exclusion areas on the shortest path
+        WIll create a path the go around the exclusion areas on the shortest path when travelling (at a Z above the
+        material).
 
         :param start_point:     X,Y coordinates for the start point of the travel line
         :type start_point:      tuple

BIN
assets/resources/dark_resources/etch_32.png


BIN
assets/resources/etch_32.png


+ 2 - 0
camlib.py

@@ -964,6 +964,8 @@ class Geometry(object):
                     corner_type = 1 if corner is None else corner
                     geo_iso.append(pol.buffer(offset, int(self.geo_steps_per_circle), join_style=corner_type))
                 pol_nr += 1
+
+                # activity view update
                 disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
 
                 if old_disp_number < disp_number <= 100: