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

- 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 лет назад
Родитель
Сommit
8165c797a4

+ 12 - 1
AppGUI/GUIElements.py

@@ -573,6 +573,7 @@ class EvalEntry(QtWidgets.QLineEdit):
     def __init__(self, parent=None):
     def __init__(self, parent=None):
         super(EvalEntry, self).__init__(parent)
         super(EvalEntry, self).__init__(parent)
         self.readyToEdit = True
         self.readyToEdit = True
+
         self.editingFinished.connect(self.on_edit_finished)
         self.editingFinished.connect(self.on_edit_finished)
 
 
     def on_edit_finished(self):
     def on_edit_finished(self):
@@ -599,7 +600,6 @@ class EvalEntry(QtWidgets.QLineEdit):
 
 
     def get_value(self):
     def get_value(self):
         raw = str(self.text()).strip(' ')
         raw = str(self.text()).strip(' ')
-        evaled = 0.0
         try:
         try:
             evaled = eval(raw)
             evaled = eval(raw)
         except Exception as e:
         except Exception as e:
@@ -656,6 +656,17 @@ 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 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):
 class FCSpinner(QtWidgets.QSpinBox):
 
 
     returnPressed = QtCore.pyqtSignal()
     returnPressed = QtCore.pyqtSignal()

+ 2 - 2
AppGUI/MainGUI.py

@@ -855,7 +855,7 @@ class MainGUI(QtWidgets.QMainWindow):
         self.copy_btn = self.toolbaredit.addAction(
         self.copy_btn = self.toolbaredit.addAction(
             QtGui.QIcon(self.app.resource_location + '/copy_file32.png'), _("Copy"))
             QtGui.QIcon(self.app.resource_location + '/copy_file32.png'), _("Copy"))
         self.delete_btn = self.toolbaredit.addAction(
         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.toolbaredit.addSeparator()
         self.distance_btn = self.toolbaredit.addAction(
         self.distance_btn = self.toolbaredit.addAction(
             QtGui.QIcon(self.app.resource_location + '/distance32.png'), _("Distance Tool"))
             QtGui.QIcon(self.app.resource_location + '/distance32.png'), _("Distance Tool"))
@@ -1851,7 +1851,7 @@ class MainGUI(QtWidgets.QMainWindow):
         self.copy_btn = self.toolbaredit.addAction(
         self.copy_btn = self.toolbaredit.addAction(
             QtGui.QIcon(self.app.resource_location + '/copy_file32.png'), _("Copy"))
             QtGui.QIcon(self.app.resource_location + '/copy_file32.png'), _("Copy"))
         self.delete_btn = self.toolbaredit.addAction(
         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.toolbaredit.addSeparator()
         self.distance_btn = self.toolbaredit.addAction(
         self.distance_btn = self.toolbaredit.addAction(
             QtGui.QIcon(self.app.resource_location + '/distance32.png'), _("Distance Tool"))
             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):
 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
     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
         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:
         if decimals is None:
@@ -316,6 +318,12 @@ class ExcellonObject(FlatCAMObj, Excellon):
         exc_final.create_geometry()
         exc_final.create_geometry()
 
 
     def build_ui(self):
     def build_ui(self):
+        """
+        Will (re)build the Excellon UI updating it (the tool table)
+
+        :return:    None
+        :rtype:
+        """
         FlatCAMObj.build_ui(self)
         FlatCAMObj.build_ui(self)
 
 
         # Area Exception - exclusion shape added signal
         # Area Exception - exclusion shape added signal
@@ -586,9 +594,9 @@ class ExcellonObject(FlatCAMObj, Excellon):
         Configures the user interface for this object.
         Configures the user interface for this object.
         Connects options to form fields.
         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)
         FlatCAMObj.set_ui(self, ui)
 
 
@@ -729,6 +737,12 @@ class ExcellonObject(FlatCAMObj, Excellon):
         self.ui.operation_radio.setEnabled(False)
         self.ui.operation_radio.setEnabled(False)
 
 
     def ui_connect(self):
     def ui_connect(self):
+        """
+        Will connect all signals in the Excellon UI that needs to be connected
+
+        :return:    None
+        :rtype:
+        """
 
 
         # selective plotting
         # selective plotting
         for row in range(self.ui.tools_table.rowCount() - 2):
         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)
                 current_widget.returnPressed.connect(self.form_to_storage)
 
 
     def ui_disconnect(self):
     def ui_disconnect(self):
+        """
+        Will disconnect all signals in the Excellon UI that needs to be disconnected
+
+        :return:    None
+        :rtype:
+        """
         # selective plotting
         # selective plotting
         for row in range(self.ui.tools_table.rowCount()):
         for row in range(self.ui.tools_table.rowCount()):
             try:
             try:
@@ -793,6 +813,12 @@ class ExcellonObject(FlatCAMObj, Excellon):
                     pass
                     pass
 
 
     def on_row_selection_change(self):
     def on_row_selection_change(self):
+        """
+        Called when the user clicks on a row in Tools Table
+
+        :return:    None
+        :rtype:
+        """
         self.ui_disconnect()
         self.ui_disconnect()
 
 
         sel_rows = []
         sel_rows = []
@@ -843,6 +869,14 @@ class ExcellonObject(FlatCAMObj, Excellon):
         self.ui_connect()
         self.ui_connect()
 
 
     def storage_to_form(self, dict_storage):
     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 form_key in self.form_fields:
             for storage_key in dict_storage:
             for storage_key in dict_storage:
                 if form_key == storage_key and form_key not in \
                 if form_key == storage_key and form_key not in \
@@ -854,6 +888,12 @@ class ExcellonObject(FlatCAMObj, Excellon):
                         pass
                         pass
 
 
     def form_to_storage(self):
     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:
         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
             # there is no tool in tool table so we can't save the GUI elements values to storage
             return
             return
@@ -882,6 +922,14 @@ class ExcellonObject(FlatCAMObj, Excellon):
         self.ui_connect()
         self.ui_connect()
 
 
     def on_operation_type(self, val):
     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':
         if val == 'mill':
             self.ui.mill_type_label.show()
             self.ui.mill_type_label.show()
             self.ui.milling_type_radio.show()
             self.ui.milling_type_radio.show()
@@ -912,8 +960,8 @@ class ExcellonObject(FlatCAMObj, Excellon):
         Returns the keys to the self.tools dictionary corresponding
         Returns the keys to the self.tools dictionary corresponding
         to the selections on the tool list in the AppGUI.
         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()]
         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
         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 = []
         table_tools_items = []
         for x in self.ui.tools_table.selectedItems():
         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'):
     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
         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 = ''
         excellon_code = ''
@@ -1123,8 +1185,8 @@ class ExcellonObject(FlatCAMObj, Excellon):
         object's options and returns a (success, msg) tuple as feedback
         object's options and returns a (success, msg) tuple as feedback
         for shell operations.
         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
         # 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."
                 return False, "Error: Milling tool is larger than hole."
 
 
         def geo_init(geo_obj, app_obj):
         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)
             assert geo_obj.kind == 'geometry', "Initializer expected a GeometryObject, got %s" % type(geo_obj)
 
 
             # ## Add properties to the object
             # ## Add properties to the object

+ 2 - 3
AppObjects/FlatCAMGerber.py

@@ -896,7 +896,7 @@ class GerberObject(FlatCAMObj, Gerber):
                 })
                 })
 
 
                 for nr_pass in range(passes):
                 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
                     # if milling type is climb then the move is counter-clockwise around features
                     mill_dir = 1 if milling_type == 'cl' else 0
                     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)
             self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot)
         else:
         else:
             for i in range(passes):
             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 passes > 1:
                     if outname is None:
                     if outname is None:
                         if self.iso_type == 0:
                         if self.iso_type == 0:

+ 97 - 77
AppTools/ToolEtchCompensation.py

@@ -8,9 +8,9 @@
 from PyQt5 import QtWidgets, QtCore
 from PyQt5 import QtWidgets, QtCore
 
 
 from AppTool import AppTool
 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
 from copy import deepcopy
 
 
@@ -95,8 +95,8 @@ class ToolEtchCompensation(AppTool):
         self.thick_entry.set_range(0.0000, 9999.9999)
         self.thick_entry.set_range(0.0000, 9999.9999)
         self.thick_entry.setObjectName(_("Thickness"))
         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 = QtWidgets.QLabel('%s:' % _("Ratio"))
         self.ratio_label.setToolTip(
         self.ratio_label.setToolTip(
@@ -106,17 +106,48 @@ class ToolEtchCompensation(AppTool):
               "- preselection -> value which depends on a selection of etchants")
               "- preselection -> value which depends on a selection of etchants")
         )
         )
         self.ratio_radio = RadioSet([
         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_label, 7, 0, 1, 2)
         grid0.addWidget(self.ratio_radio, 8, 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 = QtWidgets.QFrame()
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
         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 = FCButton(_('Compensate'))
         self.compensate_btn.setToolTip(
         self.compensate_btn.setToolTip(
@@ -128,7 +159,7 @@ class ToolEtchCompensation(AppTool):
                             font-weight: bold;
                             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()
         self.tools_box.addStretch()
 
 
@@ -145,7 +176,7 @@ class ToolEtchCompensation(AppTool):
                         """)
                         """)
         self.tools_box.addWidget(self.reset_button)
         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.reset_button.clicked.connect(self.set_tool_ui)
         self.ratio_radio.activated_custom.connect(self.on_ratio_change)
         self.ratio_radio.activated_custom.connect(self.on_ratio_change)
 
 
@@ -153,8 +184,8 @@ class ToolEtchCompensation(AppTool):
         AppTool.install(self, icon, separator, shortcut='', **kwargs)
         AppTool.install(self, icon, separator, shortcut='', **kwargs)
 
 
     def run(self, toggle=True):
     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 toggle:
             # if the splitter is hidden, display it, else hide it but only if the current widget is the same
             # 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)
         AppTool.run(self)
         self.set_tool_ui()
         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):
     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):
     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"])
         grb_circle_steps = int(self.app.defaults["gerber_circle_steps"])
         obj_name = self.gerber_combo.currentText()
         obj_name = self.gerber_combo.currentText()
 
 
-        outname = obj_name + "_inverted"
+        outname = obj_name + "_comp"
 
 
         # Get source object.
         # Get source object.
         try:
         try:
@@ -214,74 +257,51 @@ class ToolEtchCompensation(AppTool):
             self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(obj_name)))
             self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(obj_name)))
             return
             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:
         try:
             __ = iter(grb_obj.solid_geometry)
             __ = iter(grb_obj.solid_geometry)
         except TypeError:
         except TypeError:
             grb_obj.solid_geometry = list(grb_obj.solid_geometry)
             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:
         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 = {}
         new_options = {}
         for opt in grb_obj.options:
         for opt in grb_obj.options:
             new_options[opt] = deepcopy(grb_obj.options[opt])
             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):
         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.update(new_options)
             new_obj.options['name'] = outname
             new_obj.options['name'] = outname
             new_obj.fill_color = deepcopy(grb_obj.fill_color)
             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_el['follow'] = new_solid_geometry.exterior
             new_apertures['0']['geometry'].append(new_el)
             new_apertures['0']['geometry'].append(new_el)
 
 
-        for td in new_apertures:
-            print(td, new_apertures[td])
-
         def init_func(new_obj, app_obj):
         def init_func(new_obj, app_obj):
             new_obj.options.update(new_options)
             new_obj.options.update(new_options)
             new_obj.options['name'] = outname
             new_obj.options['name'] = outname

+ 20 - 10
App_Main.py

@@ -42,7 +42,7 @@ import socket
 # ###################################      Imports part of FlatCAM       #############################################
 # ###################################      Imports part of FlatCAM       #############################################
 # ####################################################################################################################
 # ####################################################################################################################
 
 
-# Diverse
+# Various
 from Common import LoudDict
 from Common import LoudDict
 from Common import color_variant
 from Common import color_variant
 from Common import ExclusionAreas
 from Common import ExclusionAreas
@@ -53,8 +53,10 @@ from AppDatabase import ToolsDB2
 from vispy.gloo.util import _screenshot
 from vispy.gloo.util import _screenshot
 from vispy.io import write_png
 from vispy.io import write_png
 
 
-# FlatCAM Objects
+# FlatCAM defaults (preferences)
 from defaults import FlatCAMDefaults
 from defaults import FlatCAMDefaults
+
+# FlatCAM Objects
 from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 from AppGUI.preferences.PreferencesUIManager import PreferencesUIManager
 from AppGUI.preferences.PreferencesUIManager import PreferencesUIManager
 from AppObjects.ObjectCollection import *
 from AppObjects.ObjectCollection import *
@@ -105,7 +107,7 @@ if '_' not in builtins.__dict__:
 
 
 class App(QtCore.QObject):
 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()
             self.new_launch.start.emit()
 
 
         # ############################################################################################################
         # ############################################################################################################
-        # # ######################################## OS-specific #####################################################
+        # ########################################## OS-specific #####################################################
         # ############################################################################################################
         # ############################################################################################################
         portable = False
         portable = False
 
 
@@ -401,13 +403,12 @@ class App(QtCore.QObject):
             json.dump([], fp)
             json.dump([], fp)
             fp.close()
             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 ...
         # This will fail under cx_freeze ...
         self.app_home = os.path.dirname(os.path.realpath(__file__))
         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
         # cx_freeze workaround
         if os.path.isfile(self.app_home):
         if os.path.isfile(self.app_home):
@@ -451,7 +452,6 @@ class App(QtCore.QObject):
         # ###########################################################################################################
         # ###########################################################################################################
         # ###################################### Setting the Splash Screen ##########################################
         # ###################################### Setting the Splash Screen ##########################################
         # ###########################################################################################################
         # ###########################################################################################################
-
         splash_settings = QSettings("Open Source", "FlatCAM")
         splash_settings = QSettings("Open Source", "FlatCAM")
         if splash_settings.contains("splash_screen"):
         if splash_settings.contains("splash_screen"):
             show_splash = splash_settings.value("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.corners_tool.install(icon=QtGui.QIcon(self.resource_location + '/corners_32.png'), pos=self.ui.menutool)
 
 
         self.etch_tool = ToolEtchCompensation(self)
         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 = ToolTransform(self)
         self.transform_tool.install(icon=QtGui.QIcon(self.resource_location + '/transform.png'),
         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()")
         self.defaults.report_usage("on_copy_command()")
 
 
         def initialize(obj_init, app):
         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)
             obj_init.solid_geometry = deepcopy(obj.solid_geometry)
             try:
             try:
                 obj_init.follow_geometry = deepcopy(obj.follow_geometry)
                 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
 23.05.2020
 
 
 - fixed a issue when testing for Exclusion areas overlap over the Geometry object solid_geometry
 - 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 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 shapely.ops import unary_union
 
 
 from AppGUI.VisPyVisuals import ShapeCollection
 from AppGUI.VisPyVisuals import ShapeCollection
@@ -32,7 +32,9 @@ if '_' not in builtins.__dict__:
 
 
 
 
 class GracefulException(Exception):
 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):
     def __init__(self):
         super().__init__()
         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
     Takes a color in HEX format #FF00FF and produces a lighter or darker variant
 
 
     :param hex_color:           color to change
     :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:
     if len(hex_color) != 7:
@@ -133,7 +138,9 @@ def color_variant(hex_color, bright_factor=1):
 
 
 
 
 class ExclusionAreas(QtCore.QObject):
 class ExclusionAreas(QtCore.QObject):
-
+    """
+    Functionality for adding Exclusion Areas for the Excellon and Geometry FlatCAM Objects
+    """
     e_shape_modified = QtCore.pyqtSignal()
     e_shape_modified = QtCore.pyqtSignal()
 
 
     def __init__(self, app):
     def __init__(self, app):
@@ -230,6 +237,14 @@ class ExclusionAreas(QtCore.QObject):
 
 
     # To be called after clicking on the plot.
     # To be called after clicking on the plot.
     def on_mouse_release(self, event):
     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:
         if self.app.is_legacy is False:
             event_pos = event.pos
             event_pos = event.pos
             # event_is_dragging = event.is_dragging
             # event_is_dragging = event.is_dragging
@@ -417,6 +432,13 @@ class ExclusionAreas(QtCore.QObject):
             self.e_shape_modified.emit()
             self.e_shape_modified.emit()
 
 
     def area_disconnect(self):
     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:
         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_release', self.on_mouse_release)
             self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
             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.call_source = "app"
         self.app.inform.emit("[WARNING_NOTCL] %s" % _("Cancelled. Area exclusion drawing was interrupted."))
         self.app.inform.emit("[WARNING_NOTCL] %s" % _("Cancelled. Area exclusion drawing was interrupted."))
 
 
-    # called on mouse move
     def on_mouse_move(self, event):
     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()
         shape_type = self.shape_type_button.get_value()
 
 
         if self.app.is_legacy is False:
         if self.app.is_legacy is False:
@@ -513,6 +542,12 @@ class ExclusionAreas(QtCore.QObject):
                 data=(curr_pos[0], curr_pos[1]))
                 data=(curr_pos[0], curr_pos[1]))
 
 
     def on_clear_area_click(self):
     def on_clear_area_click(self):
+        """
+        Slot for clicking the button for Deleting all the Exclusion areas.
+
+        :return:    None
+        :rtype:
+        """
         self.clear_shapes()
         self.clear_shapes()
 
 
         # restore the default StyleSheet
         # restore the default StyleSheet
@@ -527,6 +562,12 @@ class ExclusionAreas(QtCore.QObject):
         self.cnc_button.setToolTip('%s' % _("Generate the CNC Job object."))
         self.cnc_button.setToolTip('%s' % _("Generate the CNC Job object."))
 
 
     def clear_shapes(self):
     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()
         self.exclusion_areas_storage.clear()
         AppTool.delete_moving_selection_shape(self)
         AppTool.delete_moving_selection_shape(self)
         self.app.delete_selection_shape()
         self.app.delete_selection_shape()
@@ -536,8 +577,9 @@ class ExclusionAreas(QtCore.QObject):
     def delete_sel_shapes(self, idxs):
     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
         # delete all plotted shapes
@@ -583,7 +625,8 @@ class ExclusionAreas(QtCore.QObject):
 
 
     def travel_coordinates(self, start_point, end_point, tooldia):
     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
         :param start_point:     X,Y coordinates for the start point of the travel line
         :type start_point:      tuple
         :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
                     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))
                     geo_iso.append(pol.buffer(offset, int(self.geo_steps_per_circle), join_style=corner_type))
                 pol_nr += 1
                 pol_nr += 1
+
+                # activity view update
                 disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
                 disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
 
 
                 if old_disp_number < disp_number <= 100:
                 if old_disp_number < disp_number <= 100: