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

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

Beta
Marius Stanciu пре 6 година
родитељ
комит
fcf3f12c8d

+ 15 - 1
FlatCAM.py

@@ -1,7 +1,7 @@
 import sys
 import os
 
-from PyQt5 import QtWidgets
+from PyQt5 import QtWidgets, QtGui
 from PyQt5.QtCore import QSettings, Qt
 from FlatCAMApp import App
 from flatcamGUI import VisPyPatches
@@ -58,5 +58,19 @@ if __name__ == '__main__':
     else:
         app.setAttribute(Qt.AA_EnableHighDpiScaling, False)
 
+    # Create and display the splash screen
+    # from here: https://eli.thegreenplace.net/2009/05/09/creating-splash-screens-in-pyqt
+    # splash_pix = QtGui.QPixmap('share/splash.png')
+    # splash = QtWidgets.QSplashScreen(splash_pix, Qt.WindowStaysOnTopHint)
+    # # splash.setMask(splash_pix.mask())
+    # splash.show()
+    # app.processEvents()
+    # splash.showMessage("FlatCAM is initializing ...",
+    #                    alignment=Qt.AlignBottom | Qt.AlignLeft,
+    #                    color=QtGui.QColor("gray"))
+
     fc = App()
+    # splash.finish(fc.ui)
+    fc.ui.show()
+
     sys.exit(app.exec_())

+ 365 - 115
FlatCAMApp.py

@@ -337,6 +337,29 @@ class App(QtCore.QObject):
         # #############################################################################
         self.pool = Pool()
 
+        # ##########################################################################
+        # ################## Setting the Splash Screen #############################
+        # ##########################################################################
+
+        settings = QSettings("Open Source", "FlatCAM")
+        if settings.contains("splash_screen"):
+            show_splash = settings.value("splash_screen")
+        else:
+            settings.setValue('splash_screen', 1)
+
+            # This will write the setting to the platform specific storage.
+            del settings
+            show_splash = 1
+
+        if show_splash:
+            splash_pix = QtGui.QPixmap('share/splash.png')
+            splash = QtWidgets.QSplashScreen(splash_pix, Qt.WindowStaysOnTopHint)
+            # splash.setMask(splash_pix.mask())
+            splash.show()
+            splash.showMessage(_("FlatCAM is initializing ..."),
+                               alignment=Qt.AlignBottom | Qt.AlignLeft,
+                               color=QtGui.QColor("gray"))
+
         # #############################################################################
         # ##################### Initialize GUI ########################################
         # #############################################################################
@@ -424,6 +447,8 @@ class App(QtCore.QObject):
             "gerber_multicolored": self.ui.gerber_defaults_form.gerber_gen_group.multicolored_cb,
             "gerber_circle_steps": self.ui.gerber_defaults_form.gerber_gen_group.circle_steps_entry,
             "gerber_buffering": self.ui.gerber_defaults_form.gerber_gen_group.buffering_radio,
+            "gerber_simplification": self.ui.gerber_defaults_form.gerber_gen_group.simplify_cb,
+            "gerber_simp_tolerance": self.ui.gerber_defaults_form.gerber_gen_group.simplification_tol_spinner,
 
             # Gerber Options
             "gerber_isotooldia": self.ui.gerber_defaults_form.gerber_opt_group.iso_tool_dia_entry,
@@ -830,6 +855,8 @@ class App(QtCore.QObject):
             "gerber_circle_steps": 128,
             "gerber_use_buffer_for_union": True,
             "gerber_buffering": "full",
+            "gerber_simplification": False,
+            "gerber_simp_tolerance": 0.0005,
 
             # Gerber Options
             "gerber_isotooldia": 0.00787402,
@@ -1465,15 +1492,25 @@ class App(QtCore.QObject):
         # ###############################################
         # ############# SETUP Plot Area #################
         # ###############################################
-
+        if show_splash:
+            splash.showMessage(_("FlatCAM is initializing ...\n"
+                                 "Canvas initialization started."),
+                               alignment=Qt.AlignBottom | Qt.AlignLeft,
+                               color=QtGui.QColor("gray"))
         start_plot_time = time.time()   # debug
         self.plotcanvas = None
         self.app_cursor = None
         self.hover_shapes = None
         self.on_plotcanvas_setup()
         end_plot_time = time.time()
-        self.log.debug("Finished Canvas initialization in %s seconds." % (str(end_plot_time - start_plot_time)))
-
+        used_time = end_plot_time - start_plot_time
+        self.log.debug("Finished Canvas initialization in %s seconds." % str(used_time))
+        if show_splash:
+            splash.showMessage('%s: %ssec' % (_("FlatCAM is initializing ...\n"
+                                                "Canvas initialization started.\n"
+                                                "Canvas initialization finished in"), '%.2f' % used_time),
+                               alignment=Qt.AlignBottom | Qt.AlignLeft,
+                               color=QtGui.QColor("gray"))
         self.ui.splitter.setStretchFactor(1, 2)
 
         # to use for tools like Measurement tool who depends on the event sources who are changed inside the Editors
@@ -1810,6 +1847,9 @@ class App(QtCore.QObject):
         self.ui.fa_defaults_form.fa_gerber_group.grb_list_btn.clicked.connect(
             lambda: self.on_register_files(obj_type='gerber'))
 
+        # splash screen button signal
+        self.ui.general_defaults_form.general_gui_set_group.splash_cb.stateChanged.connect(self.on_splash_changed)
+
         # connect the abort_all_tasks related slots to the related signals
         self.proc_container.idle_flag.connect(self.app_is_idle)
 
@@ -1844,7 +1884,8 @@ class App(QtCore.QObject):
                                   'delete', 'drillcncjob', 'export_gcode', 'export_svg', 'ext', 'exteriors', 'follow',
                                   'geo_union', 'geocutout', 'get_names', 'get_sys', 'getsys', 'help', 'import_svg',
                                   'interiors', 'isolate', 'join_excellon', 'join_excellons', 'join_geometries',
-                                  'join_geometry', 'list_sys', 'listsys', 'mill', 'millholes', 'mirror', 'ncc',
+                                  'join_geometry', 'list_sys', 'listsys', 'milld', 'mills', 'milldrills', 'millslots',
+                                  'mirror', 'ncc',
                                   'ncc_clear', 'ncr', 'new', 'new_geometry', 'non_copper_regions', 'offset',
                                   'open_excellon', 'open_gcode', 'open_gerber', 'open_project', 'options', 'paint',
                                   'pan', 'panel', 'panelize', 'plot', 'save', 'save_project', 'save_sys', 'scale',
@@ -1853,10 +1894,12 @@ class App(QtCore.QObject):
                                   ]
 
         self.ordinary_keywords = ['all', 'angle_x', 'angle_y', 'axis', 'axisoffset', 'box', 'center_x', 'center_y',
-                                  'columns', 'combine', 'connect', 'contour', 'depthperpass', 'dia', 'dist', 'drillz',
+                                  'columns', 'combine', 'connect', 'contour', 'depthperpass', 'dia', 'diatol', 'dist',
+                                  'drilled_dias', 'drillz',
                                   'endz', 'extracut', 'factor', 'False', 'false', 'feedrate', 'feedrate_rapid',
                                   'filename', 'follow', 'gaps', 'gapsize', 'grid', 'gridoffset', 'gridoffsetx',
                                   'gridoffsety', 'gridx', 'gridy', 'has_offset', 'holes', 'margin', 'method',
+                                  'milled_dias',
                                   'minoffset', 'multidepth', 'name', 'offset', 'opt_type', 'order', 'outname',
                                   'overlap', 'passes', 'postamble', 'ppname_e', 'ppname_g', 'preamble', 'radius', 'ref',
                                   'rest', 'rows', 'scale_factor', 'spacing_columns', 'spacing_rows', 'spindlespeed',
@@ -3013,11 +3056,11 @@ class App(QtCore.QObject):
         :return: Output from the command
         """
 
-        text = str(text)
+        tcl_command_string = str(text)
 
         try:
             self.shell.open_proccessing()  # Disables input box.
-            result = self.tcl.eval(str(text))
+            result = self.tcl.eval(str(tcl_command_string))
             if result != 'None':
                 self.shell.append_output(result + '\n')
 
@@ -3029,7 +3072,6 @@ class App(QtCore.QObject):
             # Show error in console and just return or in test raise exception
             if reraise:
                 raise e
-
         finally:
             self.shell.close_proccessing()
             pass
@@ -3537,7 +3579,7 @@ class App(QtCore.QObject):
         # Check units and convert if necessary
         # This condition CAN be true because initialize() can change obj.units
         if self.options["units"].upper() != obj.units.upper():
-            self.inform.emit('[ERROR_NOTCL] %s: %s' %
+            self.inform.emit('%s: %s' %
                              (_("Converting units to "), self.options["units"]))
             obj.convert_units(self.options["units"])
             t3 = time.time()
@@ -3754,7 +3796,8 @@ class App(QtCore.QObject):
 
                 # Icon and title
                 self.setWindowIcon(parent.app_icon)
-                self.setWindowTitle("FlatCAM")
+                self.setWindowTitle(_("About FlatCAM"))
+                self.resize(600, 200)
                 # self.setStyleSheet("background-image: url(share/flatcam_icon256.png); background-attachment: fixed")
                 # self.setStyleSheet(
                 #     "border-image: url(share/flatcam_icon256.png) 0 0 0 0 stretch stretch; "
@@ -3767,58 +3810,182 @@ class App(QtCore.QObject):
                 # palette.setBrush(10, QtGui.QBrush(bgimage))  # 10 = Windowrole
                 # self.setPalette(palette)
 
-                layout1 = QtWidgets.QVBoxLayout()
-                self.setLayout(layout1)
-
-                layout2 = QtWidgets.QHBoxLayout()
-                layout1.addLayout(layout2)
 
                 logo = QtWidgets.QLabel()
                 logo.setPixmap(QtGui.QPixmap('share/flatcam_icon256.png'))
-                layout2.addWidget(logo, stretch=0)
 
                 title = QtWidgets.QLabel(
                     _(
                         "<font size=8><B>FlatCAM</B></font><BR>"
-                        "Version {version} {beta} ({date}) - {arch} <BR>"
-                        "<BR>"
                         "2D Computer-Aided Printed Circuit Board<BR>"
                         "Manufacturing.<BR>"
                         "<BR>"
-                        "<B> License: </B><BR>"
-                        "Licensed under MIT license (2014 - 2019)"
                         "<BR>"
-                        "by (c)Juan Pablo Caram <BR>"
+                        "<B>Development</B> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"
+                        "<a href = \"https://bitbucket.org/jpcgt/flatcam/src/Beta/\">here.</a><BR>"
+                        "<b>DOWNLOAD</B> area &nbsp;&nbsp;&nbsp;&nbsp;"
+                        "<a href = \"https://bitbucket.org/jpcgt/flatcam/downloads/\">here.</a><BR>"
+                        "<b>Issue tracker</B> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"
+                        "<a href = \"https://bitbucket.org/jpcgt/flatcam/issues?status=new&status=open/\">here.</a><BR>"
+                    )
+                )
+                title.setOpenExternalLinks(True)
+
+                closebtn = QtWidgets.QPushButton(_("Close"))
+
+                tab_widget = QtWidgets.QTabWidget()
+                description_label = QtWidgets.QLabel(
+                    _(
+                        "FlatCAM {version} {beta} ({date}) - {arch}<br>"
+                        "<a href = \"http://flatcam.org/\">http://flatcam.org</a><br>"
+                    ).format(version=version,
+                             beta=('BETA' if beta else ''),
+                             date=version_date,
+                             arch=platform.architecture()[0])
+                )
+                description_label.setOpenExternalLinks(True)
+
+                programmers_label = QtWidgets.QLabel(
+                    _(
+                        "Juan Pablo Caram <BR>"
                         "<BR>"
-                        "<B> Programmers:</B><BR>"
                         "Denis Hayrullin<BR>"
                         "Kamil Sopko<BR>"
                         "Marius Stanciu<BR>"
-                        "Matthieu Berthomé<BR>"
+                        "Matthieu Berthomé<BR><Br>"
                         "and many others found "
                         "<a href = \"https://bitbucket.org/jpcgt/flatcam/pull-requests/?state=MERGED\">here.</a><BR>"
                         "<BR>"
-                        "<B>Development</B> is done "
-                        "<a href = \"https://bitbucket.org/jpcgt/flatcam/src/Beta/\">here.</a><BR>"
-                        "<b>DOWNLOAD</B> area "
-                        "<a href = \"https://bitbucket.org/jpcgt/flatcam/downloads/\">here.</a><BR>"
-                        ""
-                    ).format(version=version,
-                             beta=('BETA' if beta else ''),
-                             date=version_date,
-                             arch=platform.architecture()[0])
+                    )
                 )
-                title.setOpenExternalLinks(True)
+                programmers_label.setOpenExternalLinks(True)
+
+                license_label = QtWidgets.QLabel(
+                    _(
+                        '(c) Copyright 2014 Juan Pablo Caram.\n\n'
+                        'Licensed under the MIT license:\n'
+                        'http://www.opensource.org/licenses/mit-license.php\n\n'
+                        'Permission is hereby granted, free of charge, to any person obtaining a copy\n'
+                        'of this software and associated documentation files (the "Software"), to deal\n'
+                        'in the Software without restriction, including without limitation the rights\n'
+                        'to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n'
+                        'copies of the Software, and to permit persons to whom the Software is\n'
+                       ' furnished to do so, subject to the following conditions:\n\n'
+                        
+                        'The above copyright notice and this permission notice shall be included in\n'
+                        'all copies or substantial portions of the Software.\n\n'
+                        
+                        'THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n'
+                        'IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n'
+                        'FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n'
+                        'AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n'
+                        'LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n'
+                        'OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n'
+                        'THE SOFTWARE.'
+                    )
+                )
+                license_label.setOpenExternalLinks(True)
 
-                layout2.addWidget(title, stretch=1)
+                # layouts
+                layout1 = QtWidgets.QVBoxLayout()
+                layout1_1 = QtWidgets.QHBoxLayout()
+                layout1_2 = QtWidgets.QHBoxLayout()
 
+                layout2 = QtWidgets.QHBoxLayout()
                 layout3 = QtWidgets.QHBoxLayout()
+
+                self.setLayout(layout1)
+                layout1.addLayout(layout1_1)
+                layout1.addLayout(layout1_2)
+
+                layout1.addLayout(layout2)
                 layout1.addLayout(layout3)
+
+                layout1_1.addStretch()
+                layout1_1.addWidget(description_label)
+                layout1_2.addWidget(tab_widget)
+
+                self.splash_tab = QtWidgets.QWidget()
+                self.splash_tab.setObjectName("splash_about")
+                self.splash_tab_layout = QtWidgets.QHBoxLayout(self.splash_tab)
+                self.splash_tab_layout.setContentsMargins(2, 2, 2, 2)
+                tab_widget.addTab(self.splash_tab, _("Splash"))
+
+                self.programmmers_tab = QtWidgets.QWidget()
+                self.programmmers_tab.setObjectName("programmers_about")
+                self.programmmers_tab_layout = QtWidgets.QVBoxLayout(self.programmmers_tab)
+                self.programmmers_tab_layout.setContentsMargins(2, 2, 2, 2)
+                tab_widget.addTab(self.programmmers_tab, _("Programmers"))
+
+                self.translators_tab = QtWidgets.QWidget()
+                self.translators_tab.setObjectName("translators_about")
+                self.translators_tab_layout = QtWidgets.QVBoxLayout(self.translators_tab)
+                self.translators_tab_layout.setContentsMargins(2, 2, 2, 2)
+                tab_widget.addTab(self.translators_tab, _("Translators"))
+
+                self.license_tab = QtWidgets.QWidget()
+                self.license_tab.setObjectName("license_about")
+                self.license_tab_layout = QtWidgets.QVBoxLayout(self.license_tab)
+                self.license_tab_layout.setContentsMargins(2, 2, 2, 2)
+                tab_widget.addTab(self.license_tab, _("License"))
+
+                self.splash_tab_layout.addWidget(logo, stretch=0)
+                self.splash_tab_layout.addWidget(title, stretch=1)
+
+                self.prog_grid_lay = QtWidgets.QGridLayout()
+
+                prog_widget = QtWidgets.QWidget()
+                prog_widget.setLayout(self.prog_grid_lay)
+                self.programmmers_tab_layout.addWidget(prog_widget)
+                self.programmmers_tab_layout.addStretch()
+
+                self.prog_grid_lay.addWidget(QtWidgets.QLabel('<b>%s</b>' % _("Programmer")), 0, 0)
+                self.prog_grid_lay.addWidget(QtWidgets.QLabel('<b>%s</b>' % _("Function")), 0, 1)
+                self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Juan Pablo Caram <BR>"), 1, 0)
+                self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % _("Program Author")), 1, 1)
+                self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Denis Hayrullin"), 2, 0)
+                self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % " "), 2, 1)
+                self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Kamil Sopko"), 3, 0)
+                self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % " "), 3, 1)
+                self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Marius Stanciu"), 4, 0)
+                self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Maintainer >=2019"), 4, 1)
+                self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Matthieu Berthomé"), 5, 0)
+                self.prog_grid_lay.addWidget(QtWidgets.QLabel('%s' % " "), 5, 1)
+
+                self.translator_grid_lay = QtWidgets.QGridLayout()
+                trans_widget = QtWidgets.QWidget()
+                trans_widget.setLayout(self.translator_grid_lay)
+                self.translators_tab_layout.addWidget(trans_widget)
+                self.translators_tab_layout.addStretch()
+
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('<b>%s</b>' % _("Language")), 0, 0)
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('<b>%s</b>' % _("Translator")), 0, 1)
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('<b>%s</b>' % _("E-mail")), 0, 2)
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Brasilian - Portuguese"), 1, 0)
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Carlos Stein"), 1, 1)
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % " "),  1, 2)
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "German"), 2, 0)
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Marius Stanciu (Google-Translation)"), 2, 1)
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % " "), 2, 2)
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Romanian"), 3, 0)
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Marius Stanciu"), 3, 1)
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % " "), 3, 2)
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Russian"), 4, 0)
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Andrey Kultyapov"), 4, 1)
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "<camellan@yandex.ru>"), 4, 2)
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Spanish"), 5, 0)
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % "Marius Stanciu (Google-Translation)"), 5, 1)
+                self.translator_grid_lay.addWidget(QtWidgets.QLabel('%s' % " "), 5, 2)
+                self.translator_grid_lay.setColumnStretch(0, 0)
+                # self.translators_tab_layout.addStretch()
+
+                self.license_tab_layout.addWidget(license_label)
+                self.license_tab_layout.addStretch()
+
                 layout3.addStretch()
-                okbtn = QtWidgets.QPushButton(_("Close"))
-                layout3.addWidget(okbtn)
+                layout3.addWidget(closebtn)
 
-                okbtn.clicked.connect(self.accept)
+                closebtn.clicked.connect(self.accept)
 
         AboutDialog(self.ui).exec_()
 
@@ -4296,6 +4463,12 @@ class App(QtCore.QObject):
         geo_type_list = set()
 
         objs = self.collection.get_selected()
+
+        if len(objs) < 2:
+            self.inform.emit('[ERROR_NOTCL] %s: %d' %
+                             (_("At least two objects are required for join. Objects currently selected"), len(objs)))
+            return 'fail'
+
         for obj in objs:
             geo_type_list.add(obj.multigeo)
 
@@ -4347,6 +4520,11 @@ class App(QtCore.QObject):
                                  _("Failed. Excellon joining works only on Excellon objects."))
                 return
 
+        if len(objs) < 2:
+            self.inform.emit('[ERROR_NOTCL] %s: %d' %
+                             (_("At least two objects are required for join. Objects currently selected"), len(objs)))
+            return 'fail'
+
         def initialize(obj, app):
             FlatCAMExcellon.merge(self, exc_list=objs, exc_final=obj)
 
@@ -4370,6 +4548,11 @@ class App(QtCore.QObject):
                                  _("Failed. Gerber joining works only on Gerber objects."))
                 return
 
+        if len(objs) < 2:
+            self.inform.emit('[ERROR_NOTCL] %s: %d' %
+                             (_("At least two objects are required for join. Objects currently selected"), len(objs)))
+            return 'fail'
+
         def initialize(obj, app):
             FlatCAMGerber.merge(self, grb_list=objs, grb_final=obj)
 
@@ -5258,6 +5441,13 @@ class App(QtCore.QObject):
         self.ui.cncjob_defaults_form.cncjob_gen_group.annotation_fontcolor_entry.set_value(new_val_sel)
         self.defaults['global_proj_item_dis_color'] = new_val_sel
 
+    def on_splash_changed(self, state):
+        settings = QSettings("Open Source", "FlatCAM")
+        settings.setValue('splash_screen', 1) if state else settings.setValue('splash_screen', 0)
+
+        # This will write the setting to the platform specific storage.
+        del settings
+
     def on_notebook_tab_rmb_click(self, checked):
         self.ui.notebook.set_detachable(val=checked)
         self.defaults["global_tabs_detachable"] = checked
@@ -5579,6 +5769,24 @@ class App(QtCore.QObject):
             # Mark end of undo block
             cursor.endEditBlock()
 
+    def handleRunCode(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.ui.shell_dock.isHidden():
+            self.ui.shell_dock.show()
+
+        script_code = self.ui.code_editor.toPlainText()
+        for tcl_command_line in 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('\\', '/')
+                # execute the actual Tcl command
+                self.shell._sysShell.exec_command(tcl_command_line)
+
     def on_tool_add_keypress(self):
         # ## Current application units in Upper Case
         self.units = self.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
@@ -5734,7 +5942,8 @@ class App(QtCore.QObject):
                             for el in obj_active.mark_shapes:
                                 obj_active.mark_shapes[el].clear(update=True)
                                 obj_active.mark_shapes[el].enabled = False
-                                obj_active.mark_shapes[el] = None
+                                # obj_active.mark_shapes[el] = None
+                                del el
                         elif isinstance(obj_active, FlatCAMCNCjob):
                             try:
                                 obj_active.annotation.clear(update=True)
@@ -5877,24 +6086,24 @@ class App(QtCore.QObject):
         self.report_usage("on_copy_object()")
 
         def initialize(obj_init, app):
-            obj_init.solid_geometry = obj.solid_geometry
+            obj_init.solid_geometry = deepcopy(obj.solid_geometry)
             try:
-                obj_init.follow_geometry = obj.follow_geometry
+                obj_init.follow_geometry = deepcopy(obj.follow_geometry)
             except AttributeError:
                 pass
             try:
-                obj_init.apertures = obj.apertures
+                obj_init.apertures = deepcopy(obj.apertures)
             except AttributeError:
                 pass
 
             try:
                 if obj.tools:
-                    obj_init.tools = obj.tools
+                    obj_init.tools = deepcopy(obj.tools)
             except Exception as e:
                 log.debug("App.on_copy_object() --> %s" % str(e))
 
         def initialize_excellon(obj_init, app):
-            obj_init.tools = obj.tools
+            obj_init.tools = deepcopy(obj.tools)
 
             # drills are offset, so they need to be deep copied
             obj_init.drills = deepcopy(obj.drills)
@@ -5918,29 +6127,29 @@ class App(QtCore.QObject):
     def on_copy_object2(self, custom_name):
 
         def initialize_geometry(obj_init, app):
-            obj_init.solid_geometry = obj.solid_geometry
+            obj_init.solid_geometry = deepcopy(obj.solid_geometry)
             try:
-                obj_init.follow_geometry = obj.follow_geometry
+                obj_init.follow_geometry = deepcopy(obj.follow_geometry)
             except AttributeError:
                 pass
             try:
-                obj_init.apertures = obj.apertures
+                obj_init.apertures = deepcopy(obj.apertures)
             except AttributeError:
                 pass
 
             try:
                 if obj.tools:
-                    obj_init.tools = obj.tools
+                    obj_init.tools = deepcopy(obj.tools)
             except Exception as e:
                 log.debug("on_copy_object2() --> %s" % str(e))
 
         def initialize_gerber(obj_init, app):
-            obj_init.solid_geometry = obj.solid_geometry
+            obj_init.solid_geometry = deepcopy(obj.solid_geometry)
             obj_init.apertures = deepcopy(obj.apertures)
             obj_init.aperture_macros = deepcopy(obj.aperture_macros)
 
         def initialize_excellon(obj_init, app):
-            obj_init.tools = obj.tools
+            obj_init.tools = deepcopy(obj.tools)
             # drills are offset, so they need to be deep copied
             obj_init.drills = deepcopy(obj.drills)
             # slots are offset, so they need to be deep copied
@@ -7798,8 +8007,15 @@ class App(QtCore.QObject):
     def init_code_editor(self, name):
         # Signals section
         # Disconnect the old signals
-        self.ui.buttonOpen.clicked.disconnect()
-        self.ui.buttonSave.clicked.disconnect()
+        try:
+            self.ui.buttonOpen.clicked.disconnect()
+        except TypeError:
+            pass
+
+        try:
+            self.ui.buttonSave.clicked.disconnect()
+        except TypeError:
+            pass
 
         # add the tab if it was closed
         self.ui.plot_tab_area.addTab(self.ui.cncjob_tab, _('%s') % name)
@@ -7814,49 +8030,59 @@ class App(QtCore.QObject):
         self.ui.code_editor.setReadOnly(False)
         self.toggle_codeeditor = True
         self.ui.code_editor.completer_enable = False
+        self.ui.buttonRun.hide()
 
         # Switch plot_area to CNCJob tab
         self.ui.plot_tab_area.setCurrentWidget(self.ui.cncjob_tab)
 
     def on_view_source(self):
+        self.inform.emit('%s' %
+                         _("Viewing the source code of the selected object."))
+        self.proc_container.view.set_busy(_("Loading..."))
+
         try:
             obj = self.collection.get_active()
-        except:
+        except Exception as e:
+            log.debug("App.on_view_source() --> %s" % str(e))
             self.inform.emit('[WARNING_NOTCL] %s' %
                              _("Select an Gerber or Excellon file to view it's source file."))
             return 'fail'
 
-        # then append the text from GCode to the text editor
-        try:
-            file = StringIO(obj.source_file)
-        except AttributeError:
-            self.inform.emit('[WARNING_NOTCL] %s' %
-                             _("There is no selected object for which to see it's source file code."))
-            return 'fail'
-
         if obj.kind == 'gerber':
             flt = "Gerber Files (*.GBR);;All Files (*.*)"
-        elif obj.kind == 'excellon':
+        else:
             flt = "Excellon Files (*.DRL);;All Files (*.*)"
 
         self.init_code_editor(name=_("Source Editor"))
         self.ui.buttonOpen.clicked.connect(lambda: self.handleOpen(filt=flt))
         self.ui.buttonSave.clicked.connect(lambda: self.handleSaveGCode(filt=flt))
 
+        # then append the text from GCode to the text editor
+        try:
+            file = StringIO(obj.source_file)
+        except AttributeError:
+            self.inform.emit('[WARNING_NOTCL] %s' %
+                             _("There is no selected object for which to see it's source file code."))
+            return 'fail'
+
+        self.ui.cncjob_frame.hide()
         try:
             for line in file:
+                QtWidgets.QApplication.processEvents()
                 proc_line = str(line).strip('\n')
                 self.ui.code_editor.append(proc_line)
         except Exception as e:
             log.debug('App.on_view_source() -->%s' % str(e))
-            self.inform.emit('[ERROR] %s %s' %
-                             (_('App.on_view_source() -->'), str(e)))
+            self.inform.emit('[ERROR] %s: %s' %
+                             (_('Failed to load the source code for the selected object'), str(e)))
             return
 
-        self.ui.code_editor.moveCursor(QtGui.QTextCursor.Start)
-
         self.handleTextChanged()
-        self.ui.show()
+        self.ui.cncjob_frame.show()
+
+        self.ui.code_editor.moveCursor(QtGui.QTextCursor.Start)
+        self.proc_container.view.set_idle()
+        # self.ui.show()
 
     def on_toggle_code_editor(self):
         self.report_usage("on_toggle_code_editor()")
@@ -7882,17 +8108,19 @@ class App(QtCore.QObject):
             "# TCL Tutorial here: https://www.tcl.tk/man/tcl8.5/tutorial/tcltutorial.html\n"
             "#\n\n"
             "# FlatCAM commands list:\n"
-            "# AddCircle, AddPolygon, AddPolyline, AddRectangle, AlignDrill, AlignDrillGrid, ClearShell, Cncjob,\n"
-            "# Cutout, Delete, Drillcncjob, ExportGcode, ExportSVG, Exteriors, GeoCutout, GeoUnion, GetNames, GetSys,\n"
-            "# ImportSvg, Interiors, Isolate, Follow, JoinExcellon, JoinGeometry, ListSys, MillHoles, Mirror, New,\n"
-            "# NewGeometry, Offset, OpenExcellon, OpenGCode, OpenGerber, OpenProject, Options, Paint, Panelize,\n"
-            "# Plot, SaveProject, SaveSys, Scale, SetActive, SetSys, Skew, SubtractPoly,SubtractRectangle, Version,\n"
-            "# WriteGCode\n"
+            "# AddCircle, AddPolygon, AddPolyline, AddRectangle, AlignDrill, AlignDrillGrid, ClearShell, ClearCopper,\n"
+            "# Cncjob, Cutout, Delete, Drillcncjob, ExportGcode, ExportSVG, Exteriors, GeoCutout, GeoUnion, GetNames,\n"
+            "# GetSys, ImportSvg, Interiors, Isolate, Follow, JoinExcellon, JoinGeometry, ListSys, MillDrills,\n"
+            "# MillSlots, Mirror, New, NewGeometry, Offset, OpenExcellon, OpenGCode, OpenGerber, OpenProject,\n"
+            "# Options, Paint, Panelize, Plot, SaveProject, SaveSys, Scale, SetActive, SetSys, Skew, SubtractPoly,\n"
+            "# SubtractRectangle, Version, WriteGCode\n"
             "#\n\n"
         ))
 
         self.ui.buttonOpen.clicked.connect(lambda: self.handleOpen(filt=flt))
         self.ui.buttonSave.clicked.connect(lambda: self.handleSaveGCode(filt=flt))
+        self.ui.buttonRun.show()
+        self.ui.buttonRun.clicked.connect(self.handleRunCode)
 
         self.handleTextChanged()
         self.ui.code_editor.show()
@@ -8415,7 +8643,7 @@ class App(QtCore.QObject):
                                "Most likely another app is holding the file open and not accessible."))
             return 'fail'
 
-    def export_excellon(self, obj_name, filename, use_thread=True):
+    def export_excellon(self, obj_name, filename, local_use=None, use_thread=True):
         """
         Exports a Excellon Object to an Excellon file.
 
@@ -8436,11 +8664,14 @@ class App(QtCore.QObject):
                                                )
         units = ''
 
-        try:
-            obj = self.collection.get_by_name(str(obj_name))
-        except:
-            # TODO: The return behavior has not been established... should raise exception?
-            return "Could not retrieve object: %s" % obj_name
+        if local_use is None:
+            try:
+                obj = self.collection.get_by_name(str(obj_name))
+            except:
+                # TODO: The return behavior has not been established... should raise exception?
+                return "Could not retrieve object: %s" % obj_name
+        else:
+            obj = local_use
 
         # updated units
         eunits = self.defaults["excellon_exp_units"]
@@ -8520,20 +8751,23 @@ class App(QtCore.QObject):
                 exported_excellon += excellon_code
                 exported_excellon += footer
 
-                try:
-                    with open(filename, 'w') as fp:
-                        fp.write(exported_excellon)
-                except PermissionError:
-                    self.inform.emit('[WARNING] %s' %
-                                     _("Permission denied, saving not possible.\n"
-                                       "Most likely another app is holding the file open and not accessible."))
-                    return 'fail'
-
-                if self.defaults["global_open_style"] is False:
-                    self.file_opened.emit("Excellon", filename)
-                self.file_saved.emit("Excellon", filename)
-                self.inform.emit('[success] %s: %s' %
-                                 (_("Excellon file exported to"), filename))
+                if local_use is None:
+                    try:
+                        with open(filename, 'w') as fp:
+                            fp.write(exported_excellon)
+                    except PermissionError:
+                        self.inform.emit('[WARNING] %s' %
+                                         _("Permission denied, saving not possible.\n"
+                                           "Most likely another app is holding the file open and not accessible."))
+                        return 'fail'
+
+                    if self.defaults["global_open_style"] is False:
+                        self.file_opened.emit("Excellon", filename)
+                    self.file_saved.emit("Excellon", filename)
+                    self.inform.emit('[success] %s: %s' %
+                                     (_("Excellon file exported to"), filename))
+                else:
+                    return exported_excellon
             except Exception as e:
                 log.debug("App.export_excellon.make_excellon() --> %s" % str(e))
                 return 'fail'
@@ -8555,14 +8789,18 @@ class App(QtCore.QObject):
             if ret == 'fail':
                 self.inform.emit('[ERROR_NOTCL] %s' %
                                  _('Could not export Excellon file.'))
-                return
+                return 'fail'
+            if local_use is not None:
+                return ret
 
-    def export_gerber(self, obj_name, filename, use_thread=True):
+    def export_gerber(self, obj_name, filename, local_use=None, use_thread=True):
         """
         Exports a Gerber Object to an Gerber file.
 
         :param obj_name: the name of the FlatCAM object to be saved as Gerber
         :param filename: Path to the Gerber file to save to.
+        :param local_use: if the Gerber code is to be saved to a file (None) or used within FlatCAM.
+        When not None, the value will be the actual Gerber object for which to create the Gerber code
         :param use_thread: if to be run in a separate thread
         :return:
         """
@@ -8573,11 +8811,14 @@ class App(QtCore.QObject):
 
         self.log.debug("export_gerber()")
 
-        try:
-            obj = self.collection.get_by_name(str(obj_name))
-        except:
-            # TODO: The return behavior has not been established... should raise exception?
-            return "Could not retrieve object: %s" % obj_name
+        if local_use is None:
+            try:
+                obj = self.collection.get_by_name(str(obj_name))
+            except:
+                # TODO: The return behavior has not been established... should raise exception?
+                return "Could not retrieve object: %s" % obj_name
+        else:
+            obj = local_use
 
         # updated units
         gunits = self.defaults["gerber_exp_units"]
@@ -8649,26 +8890,28 @@ class App(QtCore.QObject):
                 exported_gerber += gerber_code
                 exported_gerber += footer
 
-                try:
-                    with open(filename, 'w') as fp:
-                        fp.write(exported_gerber)
-                except PermissionError:
-                    self.inform.emit('[WARNING] %s' %
-                                     _("Permission denied, saving not possible.\n"
-                                       "Most likely another app is holding the file open and not accessible."))
-                    return 'fail'
-
-                if self.defaults["global_open_style"] is False:
-                    self.file_opened.emit("Gerber", filename)
-                self.file_saved.emit("Gerber", filename)
-                self.inform.emit('[success] %s: %s' %
-                                 (_("Gerber file exported to"), filename))
+                if local_use is None:
+                    try:
+                        with open(filename, 'w') as fp:
+                            fp.write(exported_gerber)
+                    except PermissionError:
+                        self.inform.emit('[WARNING] %s' %
+                                         _("Permission denied, saving not possible.\n"
+                                           "Most likely another app is holding the file open and not accessible."))
+                        return 'fail'
+
+                    if self.defaults["global_open_style"] is False:
+                        self.file_opened.emit("Gerber", filename)
+                    self.file_saved.emit("Gerber", filename)
+                    self.inform.emit('[success] %s: %s' %
+                                     (_("Gerber file exported to"), filename))
+                else:
+                    return exported_gerber
             except Exception as e:
                 log.debug("App.export_gerber.make_gerber() --> %s" % str(e))
                 return 'fail'
 
         if use_thread is True:
-
             with self.proc_container.new(_("Exporting Gerber")) as proc:
 
                 def job_thread_grb(app_obj):
@@ -8684,7 +8927,9 @@ class App(QtCore.QObject):
             if ret == 'fail':
                 self.inform.emit('[ERROR_NOTCL] %s' %
                                  _('Could not export Gerber file.'))
-                return
+                return 'fail'
+            if local_use is not None:
+                return ret
 
     def export_dxf(self, obj_name, filename, use_thread=True):
         """
@@ -9902,6 +10147,11 @@ The normal flow when working in FlatCAM is the following:</span></p>
                 obj.options['plot'] = False
                 obj.options.set_change_callback(obj.on_options_change)
 
+        try:
+            self.delete_selection_shape()
+        except Exception as e:
+            log.debug("App.disable_plots() --> %s" % str(e))
+
         # self.plots_updated.emit()
         def worker_task(objects):
             with self.proc_container.new(_("Disabling plots ...")):

+ 69 - 39
FlatCAMObj.py

@@ -846,40 +846,53 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         self.app.new_object("geometry", name, geo_init)
 
     def on_ext_iso_button_click(self, *args):
+        obj = self.app.collection.get_active()
+
+        def worker_task(obj, app_obj):
+            with self.app.proc_container.new(_("Isolating...")):
+                if self.ui.follow_cb.get_value() is True:
+                    obj.follow_geo()
+                    # in the end toggle the visibility of the origin object so we can see the generated Geometry
+                    obj.ui.plot_cb.toggle()
+                else:
+                    app_obj.report_usage("gerber_on_iso_button")
+                    self.read_form()
+                    self.isolate(iso_type=0)
 
-        if self.ui.follow_cb.get_value() is True:
-            obj = self.app.collection.get_active()
-            obj.follow_geo()
-            # in the end toggle the visibility of the origin object so we can see the generated Geometry
-            obj.ui.plot_cb.toggle()
-        else:
-            self.app.report_usage("gerber_on_iso_button")
-            self.read_form()
-            self.isolate(iso_type=0)
+        self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]})
 
     def on_int_iso_button_click(self, *args):
+        obj = self.app.collection.get_active()
+
+        def worker_task(obj, app_obj):
+            with self.app.proc_container.new(_("Isolating...")):
+                if self.ui.follow_cb.get_value() is True:
+                    obj.follow_geo()
+                    # in the end toggle the visibility of the origin object so we can see the generated Geometry
+                    obj.ui.plot_cb.toggle()
+                else:
+                    app_obj.report_usage("gerber_on_iso_button")
+                    self.read_form()
+                    self.isolate(iso_type=1)
 
-        if self.ui.follow_cb.get_value() is True:
-            obj = self.app.collection.get_active()
-            obj.follow_geo()
-            # in the end toggle the visibility of the origin object so we can see the generated Geometry
-            obj.ui.plot_cb.toggle()
-        else:
-            self.app.report_usage("gerber_on_iso_button")
-            self.read_form()
-            self.isolate(iso_type=1)
+        self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]})
 
     def on_iso_button_click(self, *args):
 
-        if self.ui.follow_cb.get_value() is True:
-            obj = self.app.collection.get_active()
-            obj.follow_geo()
-            # in the end toggle the visibility of the origin object so we can see the generated Geometry
-            obj.ui.plot_cb.toggle()
-        else:
-            self.app.report_usage("gerber_on_iso_button")
-            self.read_form()
-            self.isolate()
+        obj = self.app.collection.get_active()
+
+        def worker_task(obj, app_obj):
+            with self.app.proc_container.new(_("Isolating...")):
+                if self.ui.follow_cb.get_value() is True:
+                    obj.follow_geo()
+                    # in the end toggle the visibility of the origin object so we can see the generated Geometry
+                    obj.ui.plot_cb.toggle()
+                else:
+                    app_obj.report_usage("gerber_on_iso_button")
+                    self.read_form()
+                    self.isolate()
+
+        self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]})
 
     def follow_geo(self, outname=None):
         """
@@ -939,7 +952,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         base_name = self.options["name"] + "_iso"
         base_name = outname or base_name
 
-        def generate_envelope(offset, invert, envelope_iso_type=2, follow=None):
+        def generate_envelope(offset, invert, envelope_iso_type=2, follow=None, passes=0):
             # isolation_geometry produces an envelope that is going on the left of the geometry
             # (the copper features). To leave the least amount of burrs on the features
             # the tool needs to travel on the right side of the features (this is called conventional milling)
@@ -947,7 +960,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             # the other passes overlap preceding ones and cut the left over copper. It is better for them
             # to cut on the right side of the left over copper i.e on the left side of the features.
             try:
-                geom = self.isolation_geometry(offset, iso_type=envelope_iso_type, follow=follow)
+                geom = self.isolation_geometry(offset, iso_type=envelope_iso_type, follow=follow, passes=passes)
             except Exception as e:
                 log.debug('FlatCAMGerber.isolate().generate_envelope() --> %s' % str(e))
                 return 'fail'
@@ -1063,9 +1076,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                     # if milling type is climb then the move is counter-clockwise around features
                     if milling_type == 'cl':
                         # geom = generate_envelope (offset, i == 0)
-                        geom = generate_envelope(iso_offset, 1, envelope_iso_type=self.iso_type, follow=follow)
+                        geom = generate_envelope(iso_offset, 1, envelope_iso_type=self.iso_type, follow=follow,
+                                                 passes=i)
                     else:
-                        geom = generate_envelope(iso_offset, 0, envelope_iso_type=self.iso_type, follow=follow)
+                        geom = generate_envelope(iso_offset, 0, envelope_iso_type=self.iso_type, follow=follow,
+                                                 passes=i)
                     if geom == 'fail':
                         app_obj.inform.emit('[ERROR_NOTCL] %s' %
                                             _("Isolation geometry could not be generated."))
@@ -1139,6 +1154,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                 # ########## AREA SUBTRACTION ################################
                 # ############################################################
                 if self.ui.except_cb.get_value():
+                    self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo"))
                     geo_obj.solid_geometry = area_subtraction(geo_obj.solid_geometry)
 
             # TODO: Do something if this is None. Offer changing name?
@@ -1170,9 +1186,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                     # if milling type is climb then the move is counter-clockwise around features
                     if milling_type == 'cl':
                         # geo_obj.solid_geometry = generate_envelope(offset, i == 0)
-                        geom = generate_envelope(offset, 1, envelope_iso_type=self.iso_type, follow=follow)
+                        geom = generate_envelope(offset, 1, envelope_iso_type=self.iso_type, follow=follow,
+                                                 passes=i)
                     else:
-                        geom = generate_envelope(offset, 0, envelope_iso_type=self.iso_type, follow=follow)
+                        geom = generate_envelope(offset, 0, envelope_iso_type=self.iso_type, follow=follow,
+                                                 passes=i)
                     if geom == 'fail':
                         app_obj.inform.emit('[ERROR_NOTCL] %s' %
                                             _("Isolation geometry could not be generated."))
@@ -1205,6 +1223,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                     # ########## AREA SUBTRACTION ################################
                     # ############################################################
                     if self.ui.except_cb.get_value():
+                        self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo"))
                         geo_obj.solid_geometry = area_subtraction(geo_obj.solid_geometry)
 
                 # TODO: Do something if this is None. Offer changing name?
@@ -1500,7 +1519,12 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
     def export_gerber(self, whole, fract, g_zeros='L', factor=1):
         """
+        Creates a Gerber file content to be exported to a file.
 
+        :param whole: how many digits in the whole part of coordinates
+        :param fract: how many decimals in coordinates
+        :param g_zeros: type of the zero suppression used: LZ or TZ; string
+        :param factor: factor to be applied onto the Gerber coordinates
         :return: Gerber_code
         """
         log.debug("FlatCAMGerber.export_gerber() --> Generating the Gerber code from the selected Gerber file")
@@ -1651,7 +1675,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                         for geo_elem in self.apertures[apid]['geometry']:
                             if 'follow' in geo_elem:
                                 geo = geo_elem['follow']
-                                print(geo)
                                 if not geo.is_empty:
                                     if isinstance(geo, Point):
                                         if g_zeros == 'T':
@@ -2610,8 +2633,10 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
 
         for tool in tools:
             if tooldia > self.tools[tool]["C"]:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Milling tool for DRILLS is larger than hole size. Cancelled."))
+                self.app.inform.emit('[ERROR_NOTCL] %s %s: %s' % (
+                    _("Milling tool for DRILLS is larger than hole size. Cancelled.",
+                      str(tool))
+                ))
                 return False, "Error: Milling tool is larger than hole."
 
         def geo_init(geo_obj, app_obj):
@@ -5221,7 +5246,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             else:
                 try:
                     self.el_count += 1
-                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+                    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
@@ -5296,7 +5321,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             else:
                 try:
                     self.el_count += 1
-                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+                    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
@@ -5881,7 +5906,12 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
 
     def on_plot_kind_change(self):
         kind = self.ui.cncplot_method_combo.get_value()
-        self.plot(kind=kind)
+
+        def worker_task():
+            with self.app.proc_container.new(_("Plotting...")):
+                self.plot(kind=kind)
+
+        self.app.worker_task.emit({'fcn': worker_task, 'params': []})
 
     def on_exportgcode_button_click(self, *args):
         self.app.report_usage("cncjob_on_exportgcode_button")

+ 1 - 1
FlatCAMWorkerStack.py

@@ -25,7 +25,7 @@ class WorkerStack(QtCore.QObject):
             thread.started.connect(worker.run)
             worker.task_completed.connect(self.on_task_completed)
 
-            thread.start(QtCore.QThread.HighPriority)
+            thread.start(QtCore.QThread.NormalPriority)
 
             self.workers.append(worker)
             self.threads.append(thread)

+ 48 - 0
README.md

@@ -9,6 +9,54 @@ CAD program, and create G-Code for Isolation routing.
 
 =================================================
 
+13.09.2019
+
+- added control for simplification when loading a Gerber file in Preferences -> Gerber -> Gerber General -> Simplify
+- added some messages for the Edit -> Conversions -> Join methods() to make sure that there are at least 2 objects selected for join
+- added a grid layout in on_about()
+- upgraded the Script Editor to be able to run Tcl commands in batches
+- added some ToolTips for the buttons in the Code Editor
+
+12.09.2019
+
+- small changes in the TclCommands: MillDrills, MillSlots, DrillCNCJob: the new parameter for tolerance is now named: tooldia
+- cleaned up the 'About FlatCAM' window, started to give credits for the translation team
+- started to add an application splash screen
+- now, Excellon and Gerber edited objects will have the source_code updated and ready to be saved
+- the edited Gerber (or Excellon) object now is kept in the app after editing and the edited object is a new object
+- added a message to the splash screen
+- remade the splash screen to show multiple messages on app initialization
+- added a new splash image
+- added a control in Preferences -> General -> GUI Settings -> Splash Screen that control if the splash screen is shown at startup
+
+11.09.2019
+
+- added the Gerber code as source for the panelized object in Panelize Tool
+- whenever a Gerber file is deleted, the mark_shapes objects are deleted also
+- made faster the Gerber parser for the case of having a not valid geometry when loading a Gerber file without buffering
+- updated code in self.on_view_source() to make it more responsive
+- fixed the TclCommand MillHoles
+- changed the name of TclCommand MillHoles to MillDrills and added a new TclCommand named MillSlots
+- modified the MillDrills and MillSlots TclCommands to accept as parameter a list of tool diameters to be milled instead of tool indexes
+- fixed issue #302 where a copied object lost all the tools
+- modified the TclCommand DrillCncJob to have as parameter a list of tool diameters to be drilled instead of tool indexes
+- updated the Spanish translation (Google-translation)
+- added a new parameter in the TclCommands: DrillCNCJob, MillDrills, MillSlots named tol (from tolerance). If the diameters of the milled (drilled) dias are within the tolerance specified of the diameters in the Excellon object than those diameters will be processed. This is to help account for rounding errors when having units conversion
+
+10.09.2019
+
+- made isolation threaded
+- fixed a small typo in TclCommandCopperCLear
+- made changing the Plot kind in CNCJob selected tab, threaded
+- fixed an object used before declaring it in NCC Tool - Area option
+- added progress for the generation of Isolation geometry
+- added progress and possibility of graceful exit in Panel Tool
+- added graceful exit possibility when creating Isolation
+- changed the workers thread priority back to Normal
+- when disabling plots, if the selection shape is visible, it will be deleted
+- small changes in Tool Panel (eliminating some deepcopy() calls)
+- made sure that all the progress counters count to 100%
+
 9.09.2019
 
 - changed the triangulation type in VisPyVisuals for ShapeCollectionVisual class

+ 169 - 79
camlib.py

@@ -523,7 +523,7 @@ class Geometry(object):
     #
     #     return self.flat_geometry, self.flat_geometry_rtree
 
-    def isolation_geometry(self, offset, iso_type=2, corner=None, follow=None):
+    def isolation_geometry(self, offset, iso_type=2, corner=None, follow=None, passes=0):
         """
         Creates contours around geometry at a given
         offset distance.
@@ -533,36 +533,11 @@ class Geometry(object):
         :param iso_type: type of isolation, can be 0 = exteriors or 1 = interiors or 2 = both (complete)
         :param corner: type of corner for the isolation: 0 = round; 1 = square; 2= beveled (line that connects the ends)
         :param follow: whether the geometry to be isolated is a follow_geometry
+        :param passes: current pass out of possible multiple passes for which the isolation is done
         :return: The buffered geometry.
         :rtype: Shapely.MultiPolygon or Shapely.Polygon
         """
 
-        # geo_iso = []
-        # In case that the offset value is zero we don't use the buffer as the resulting geometry is actually the
-        # original solid_geometry
-        # if offset == 0:
-        #     geo_iso = self.solid_geometry
-        # else:
-        #     flattened_geo = self.flatten_list(self.solid_geometry)
-        #     try:
-        #         for mp_geo in flattened_geo:
-        #             geo_iso.append(mp_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4)))
-        #     except TypeError:
-        #         geo_iso.append(self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4)))
-        # return geo_iso
-
-        # commented this because of the bug with multiple passes cutting out of the copper
-        # geo_iso = []
-        # flattened_geo = self.flatten_list(self.solid_geometry)
-        # try:
-        #     for mp_geo in flattened_geo:
-        #         geo_iso.append(mp_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4)))
-        # except TypeError:
-        #     geo_iso.append(self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4)))
-
-        # the previously commented block is replaced with this block - regression - to solve the bug with multiple
-        # isolation passes cutting from the copper features
-
         if self.app.abort_flag:
             # graceful abort requested by the user
             raise FlatCAMApp.GracefulException
@@ -584,20 +559,62 @@ class Geometry(object):
 
                 # Remember: do not make a buffer for each element in the solid_geometry because it will cut into
                 # other copper features
-                if corner is None:
-                    geo_iso = temp_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4))
-                else:
-                    geo_iso = temp_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4),
-                                              join_style=corner)
+                # if corner is None:
+                #     geo_iso = temp_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4))
+                # else:
+                #     geo_iso = temp_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4),
+                #                               join_style=corner)
 
+                # variables to display the percentage of work done
+                geo_len = 0
+                try:
+                    for pol in self.solid_geometry:
+                        geo_len += 1
+                except TypeError:
+                    geo_len = 1
+                disp_number = 0
+                old_disp_number = 0
+                pol_nr = 0
+                # yet, it can be done by issuing an unary_union in the end, thus getting rid of the overlapping geo
+                try:
+                    for pol in self.solid_geometry:
+                        if self.app.abort_flag:
+                            # graceful abort requested by the user
+                            raise FlatCAMApp.GracefulException
+                        if corner is None:
+                            geo_iso.append(pol.buffer(offset, int(int(self.geo_steps_per_circle) / 4)))
+                        else:
+                            geo_iso.append(pol.buffer(offset, int(int(self.geo_steps_per_circle) / 4)),
+                                           join_style=corner)
+                        pol_nr += 1
+                        disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
+
+                        if  old_disp_number < disp_number <= 100:
+                            self.app.proc_container.update_view_text(' %s %d: %d%%' %
+                                                                     (_("Pass"), int(passes + 1), int(disp_number)))
+                            old_disp_number = disp_number
+                except TypeError:
+                    # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a
+                    # MultiPolygon (not an iterable)
+                    if corner is None:
+                        geo_iso.append(self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4)))
+                    else:
+                        geo_iso.append(self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4)),
+                                       join_style=corner)
+                self.app.proc_container.update_view_text(' %s' % _("Buffering"))
+                geo_iso = unary_union(geo_iso)
+
+        self.app.proc_container.update_view_text('')
         # end of replaced block
         if follow:
             return geo_iso
         elif iso_type == 2:
             return geo_iso
         elif iso_type == 0:
+            self.app.proc_container.update_view_text(' %s' % _("Get Exteriors"))
             return self.get_exteriors(geo_iso)
         elif iso_type == 1:
+            self.app.proc_container.update_view_text(' %s' % _("Get Interiors"))
             return self.get_interiors(geo_iso)
         else:
             log.debug("Geometry.isolation_geometry() --> Type of isolation not supported")
@@ -1483,7 +1500,7 @@ class Geometry(object):
             else:
                 try:
                     self.el_count += 1
-                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+                    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
@@ -1555,7 +1572,7 @@ class Geometry(object):
             else:
                 try:
                     self.el_count += 1
-                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+                    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
@@ -1626,7 +1643,7 @@ class Geometry(object):
             else:
                 try:
                     self.el_count += 1
-                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+                    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
@@ -2440,6 +2457,8 @@ class Gerber (Geometry):
         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:
@@ -2485,7 +2504,10 @@ class Gerber (Geometry):
 
                         geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
                         if not geo_s.is_empty:
-                            poly_buffer.append(geo_s)
+                            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:
@@ -2675,7 +2697,10 @@ class Gerber (Geometry):
                             geo_dict['follow'] = Point([current_x, current_y])
 
                             if not flash.is_empty:
-                                poly_buffer.append(flash)
+                                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:
@@ -2726,7 +2751,10 @@ class Gerber (Geometry):
                             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:
-                                poly_buffer.append(geo_s)
+                                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:
@@ -2759,7 +2787,10 @@ class Gerber (Geometry):
                         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:
-                            poly_buffer.append(geo_s)
+                            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:
@@ -2800,7 +2831,10 @@ class Gerber (Geometry):
                                     geo_dict['follow'] = geo_f
                             if geo_s:
                                 if not geo_s.is_empty:
-                                    poly_buffer.append(geo_s)
+                                    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:
@@ -2833,7 +2867,10 @@ class Gerber (Geometry):
                         region_s = region_s.buffer(0, int(self.steps_per_circle / 4))
 
                     if not region_s.is_empty:
-                        poly_buffer.append(region_s)
+                        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:
@@ -2915,7 +2952,11 @@ class Gerber (Geometry):
                                         geo_dict['follow'] = geo_f
 
                                         geo_s = shply_box(minx, miny, maxx, maxy)
-                                        poly_buffer.append(geo_s)
+                                        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:
@@ -3003,14 +3044,22 @@ class Gerber (Geometry):
                             try:
                                 if self.apertures[last_path_aperture]["type"] != 'R':
                                     if not geo_s.is_empty:
-                                        poly_buffer.append(geo_s)
+                                        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))
-                                poly_buffer.append(geo_s)
+                                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:
@@ -3058,13 +3107,21 @@ class Gerber (Geometry):
                             if not geo_s.is_empty:
                                 try:
                                     if self.apertures[last_path_aperture]["type"] != 'R':
-                                        poly_buffer.append(geo_s)
+                                        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:
-                                    poly_buffer.append(geo_s)
+                                    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:
@@ -3094,7 +3151,11 @@ class Gerber (Geometry):
                             self.steps_per_circle
                         )
                         if not flash.is_empty:
-                            poly_buffer.append(flash)
+                            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:
@@ -3193,7 +3254,11 @@ class Gerber (Geometry):
                             # 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:
-                                poly_buffer.append(buffered)
+                                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:
@@ -3331,7 +3396,11 @@ class Gerber (Geometry):
                     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:
-                        poly_buffer.append(geo_s)
+                        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:
@@ -3378,15 +3447,36 @@ class Gerber (Geometry):
                 log.warning("Union done.")
 
             if current_polarity == 'D':
-                try:
+                self.app.inform.emit('%s' % _("Gerber processing. Applying Gerber polarity."))
+                if new_poly.is_valid:
                     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:
+                    # 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)
         except Exception as err:
@@ -3854,7 +3944,7 @@ class Gerber (Geometry):
             else:
                 try:
                     self.el_count += 1
-                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+                    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
@@ -3916,7 +4006,7 @@ class Gerber (Geometry):
             else:
                 try:
                     self.el_count += 1
-                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+                    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
@@ -5068,7 +5158,7 @@ class Excellon(Geometry):
             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, 99]))
+            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
@@ -5124,7 +5214,7 @@ class Excellon(Geometry):
             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, 99]))
+            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
@@ -5185,7 +5275,7 @@ class Excellon(Geometry):
             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, 99]))
+            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
@@ -5257,7 +5347,7 @@ class Excellon(Geometry):
                                                origin=(px, py))
 
                 self.el_count += 1
-                disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+                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
@@ -5278,7 +5368,7 @@ class Excellon(Geometry):
                                                origin=(px, py))
 
                 self.el_count += 1
-                disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+                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
@@ -5342,7 +5432,7 @@ class Excellon(Geometry):
                 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, 99]))
+                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
@@ -5358,7 +5448,7 @@ class Excellon(Geometry):
                 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, 99]))
+                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
@@ -5920,7 +6010,7 @@ class CNCjob(Geometry):
                                     self.oldy = locy
 
                                     loc_nr += 1
-                                    disp_number = int(np.interp(loc_nr, [0, geo_len], [0, 99]))
+                                    disp_number = int(np.interp(loc_nr, [0, geo_len], [0, 100]))
 
                                     if old_disp_number < disp_number <= 100:
                                         self.app.proc_container.update_view_text(' %d%%' % disp_number)
@@ -6066,7 +6156,7 @@ class CNCjob(Geometry):
                                     self.oldy = locy
 
                                     loc_nr += 1
-                                    disp_number = int(np.interp(loc_nr, [0, geo_len], [0, 99]))
+                                    disp_number = int(np.interp(loc_nr, [0, geo_len], [0, 100]))
 
                                     if old_disp_number < disp_number <= 100:
                                         self.app.proc_container.update_view_text(' %d%%' % disp_number)
@@ -6174,7 +6264,7 @@ class CNCjob(Geometry):
                                 self.oldy = point[1]
 
                                 loc_nr += 1
-                                disp_number = int(np.interp(loc_nr, [0, geo_len], [0, 99]))
+                                disp_number = int(np.interp(loc_nr, [0, geo_len], [0, 100]))
 
                                 if old_disp_number < disp_number <= 100:
                                     self.app.proc_container.update_view_text(' %d%%' % disp_number)
@@ -6473,7 +6563,7 @@ class CNCjob(Geometry):
 
                 pt, geo = storage.nearest(current_pt) # Next
 
-                disp_number = int(np.interp(path_count, [0, geo_len], [0, 99]))
+                disp_number = int(np.interp(path_count, [0, geo_len], [0, 100]))
                 if old_disp_number < disp_number <= 100:
                     self.app.proc_container.update_view_text(' %d%%' % disp_number)
                     old_disp_number = disp_number
@@ -6815,7 +6905,7 @@ class CNCjob(Geometry):
 
                 pt, geo = storage.nearest(current_pt) # Next
 
-                disp_number = int(np.interp(path_count, [0, geo_len], [0, 99]))
+                disp_number = int(np.interp(path_count, [0, geo_len], [0, 100]))
                 if old_disp_number < disp_number <= 100:
                     self.app.proc_container.update_view_text(' %d%%' % disp_number)
                     old_disp_number = disp_number
@@ -6943,7 +7033,7 @@ class CNCjob(Geometry):
                 current_pt = geo.coords[-1]
                 pt, geo = storage.nearest(current_pt)  # Next
 
-                disp_number = int(np.interp(path_count, [0, geo_len], [0, 99]))
+                disp_number = int(np.interp(path_count, [0, geo_len], [0, 100]))
                 if old_disp_number < disp_number <= 100:
                     self.app.proc_container.update_view_text(' %d%%' % disp_number)
                     old_disp_number = disp_number
@@ -8149,7 +8239,7 @@ class CNCjob(Geometry):
                     return g['geom']
 
                 self.el_count += 1
-                disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+                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
@@ -8178,7 +8268,7 @@ class CNCjob(Geometry):
                         return g['geom']
 
                     self.el_count += 1
-                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+                    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
@@ -8259,7 +8349,7 @@ class CNCjob(Geometry):
                     return g['geom']
 
                 self.el_count += 1
-                disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+                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
@@ -8288,7 +8378,7 @@ class CNCjob(Geometry):
                         return g['geom']
 
                     self.el_count += 1
-                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+                    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
@@ -8327,7 +8417,7 @@ class CNCjob(Geometry):
                 return g['geom']
 
             self.el_count += 1
-            disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+            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
@@ -8371,7 +8461,7 @@ class CNCjob(Geometry):
                 return g['geom']
 
             self.el_count += 1
-            disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+            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
@@ -8407,7 +8497,7 @@ class CNCjob(Geometry):
                 return g['geom']
 
             self.el_count += 1
-            disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+            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

+ 4 - 3
flatcamEditors/FlatCAMExcEditor.py

@@ -3125,7 +3125,6 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.app.worker_task.emit({'fcn': self.new_edited_excellon,
                                    'params': [self.edited_obj_name]})
 
-
         self.new_tool_offset = self.exc_obj.tool_offset
 
         # reset the tool table
@@ -3134,8 +3133,8 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.last_tool_selected = None
 
         # delete the edited Excellon object which will be replaced by a new one having the edited content of the first
-        self.app.collection.set_active(self.exc_obj.options['name'])
-        self.app.collection.delete_active()
+        # self.app.collection.set_active(self.exc_obj.options['name'])
+        # self.app.collection.delete_active()
 
         # restore GUI to the Selected TAB
         # Remove anything else in the GUI
@@ -3193,6 +3192,8 @@ class FlatCAMExcEditor(QtCore.QObject):
                 app_obj.inform.emit(msg)
                 raise
                 # raise
+            excellon_obj.source_file = self.app.export_excellon(obj_name=outname, filename=None,
+                                                                local_use=excellon_obj, use_thread=False)
 
         with self.app.proc_container.new(_("Creating Excellon.")):
 

+ 49 - 26
flatcamEditors/FlatCAMGrbEditor.py

@@ -3713,7 +3713,36 @@ class FlatCAMGrbEditor(QtCore.QObject):
         for apid in self.gerber_obj.apertures:
             temp_elem = []
             if 'geometry' in self.gerber_obj.apertures[apid]:
+                # for elem in self.gerber_obj.apertures[apid]['geometry']:
+                #     if 'solid' in elem:
+                #         solid_geo = elem['solid']
+                #         for clear_geo in global_clear_geo:
+                #             # Make sure that the clear_geo is within the solid_geo otherwise we loose
+                #             # the solid_geometry. We want for clear_geometry just to cut into solid_geometry not to
+                #             # delete it
+                #             if clear_geo.within(solid_geo):
+                #                 solid_geo = solid_geo.difference(clear_geo)
+                #         try:
+                #             for poly in solid_geo:
+                #                 new_elem = dict()
+                #
+                #                 new_elem['solid'] = poly
+                #                 if 'clear' in elem:
+                #                     new_elem['clear'] = poly
+                #                 if 'follow' in elem:
+                #                     new_elem['follow'] = poly
+                #                 temp_elem.append(deepcopy(new_elem))
+                #         except TypeError:
+                #             new_elem = dict()
+                #             new_elem['solid'] = solid_geo
+                #             if 'clear' in elem:
+                #                 new_elem['clear'] = solid_geo
+                #             if 'follow' in elem:
+                #                 new_elem['follow'] = solid_geo
+                #             temp_elem.append(deepcopy(new_elem))
                 for elem in self.gerber_obj.apertures[apid]['geometry']:
+                    new_elem = dict()
+
                     if 'solid' in elem:
                         solid_geo = elem['solid']
                         for clear_geo in global_clear_geo:
@@ -3722,24 +3751,14 @@ class FlatCAMGrbEditor(QtCore.QObject):
                             # delete it
                             if clear_geo.within(solid_geo):
                                 solid_geo = solid_geo.difference(clear_geo)
-                        try:
-                            for poly in solid_geo:
-                                new_elem = dict()
-
-                                new_elem['solid'] = poly
-                                if 'clear' in elem:
-                                    new_elem['clear'] = poly
-                                if 'follow' in elem:
-                                    new_elem['follow'] = poly
-                                temp_elem.append(deepcopy(new_elem))
-                        except TypeError:
-                            new_elem = dict()
-                            new_elem['solid'] = solid_geo
-                            if 'clear' in elem:
-                                new_elem['clear'] = solid_geo
-                            if 'follow' in elem:
-                                new_elem['follow'] = solid_geo
-                            temp_elem.append(deepcopy(new_elem))
+
+                        new_elem['solid'] = solid_geo
+                    if 'clear' in elem:
+                        new_elem['clear'] = elem['clear']
+                    if 'follow' in elem:
+                        new_elem['follow'] = elem['follow']
+                    temp_elem.append(deepcopy(new_elem))
+
             self.gerber_obj.apertures[apid]['geometry'] = deepcopy(temp_elem)
         log.warning("Polygon difference done for %d apertures." % len(self.gerber_obj.apertures))
 
@@ -3876,19 +3895,19 @@ class FlatCAMGrbEditor(QtCore.QObject):
                         grb_obj.apertures[storage_apid][k] = []
                         for geo_el in val:
                             geometric_data = geo_el.geo
-
                             new_geo_el = dict()
                             if 'solid' in geometric_data:
                                 new_geo_el['solid'] = geometric_data['solid']
                                 poly_buffer.append(deepcopy(new_geo_el['solid']))
 
                             if 'follow' in geometric_data:
-                                if isinstance(geometric_data['follow'], Polygon):
-                                    buff_val = -(int(storage_apid) / 2)
-                                    geo_f = (geometric_data['follow'].buffer(buff_val)).exterior
-                                    new_geo_el['follow'] = geo_f
-                                else:
-                                    new_geo_el['follow'] = geometric_data['follow']
+                                # if isinstance(geometric_data['follow'], Polygon):
+                                #     buff_val = -(int(storage_val['size']) / 2)
+                                #     geo_f = (geometric_data['follow'].buffer(buff_val)).exterior
+                                #     new_geo_el['follow'] = geo_f
+                                # else:
+                                #     new_geo_el['follow'] = geometric_data['follow']
+                                new_geo_el['follow'] = geometric_data['follow']
                                 follow_buffer.append(deepcopy(new_geo_el['follow']))
                             else:
                                 if 'solid' in geometric_data:
@@ -3910,6 +3929,9 @@ class FlatCAMGrbEditor(QtCore.QObject):
             new_poly = new_poly.buffer(0.00000001)
             new_poly = new_poly.buffer(-0.00000001)
 
+            # for ad in grb_obj.apertures:
+            #     print(ad, grb_obj.apertures[ad])
+
             try:
                 __ = iter(new_poly)
             except TypeError:
@@ -3924,7 +3946,6 @@ class FlatCAMGrbEditor(QtCore.QObject):
                 else:
                     grb_obj.options[k] = deepcopy(v)
 
-            grb_obj.source_file = []
             grb_obj.multigeo = False
             grb_obj.follow = False
             grb_obj.gerber_units = app_obj.defaults['units']
@@ -3940,6 +3961,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
                 msg += traceback.format_exc()
                 app_obj.inform.emit(msg)
                 raise
+            grb_obj.source_file = self.app.export_gerber(obj_name=out_name, filename=None,
+                                                         local_use=grb_obj, use_thread=False)
 
         with self.app.proc_container.new(_("Creating Gerber.")):
             try:

+ 75 - 5
flatcamGUI/FlatCAMGUI.py

@@ -397,14 +397,14 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
         # ## Help ###
         self.menuhelp = self.menu.addMenu(_('&Help'))
-        self.menuhelp_manual = self.menuhelp.addAction(QtGui.QIcon('share/globe16.png'), _('Help\tF1'))
+        self.menuhelp_manual = self.menuhelp.addAction(QtGui.QIcon('share/globe16.png'), _('Online Help\tF1'))
         self.menuhelp_home = self.menuhelp.addAction(QtGui.QIcon('share/home16.png'), _('FlatCAM.org'))
         self.menuhelp.addSeparator()
         self.menuhelp_shortcut_list = self.menuhelp.addAction(QtGui.QIcon('share/shortcuts24.png'),
                                                               _('Shortcuts List\tF3'))
         self.menuhelp_videohelp = self.menuhelp.addAction(QtGui.QIcon('share/youtube32.png'), _('YouTube Channel\tF4')
                                                           )
-        self.menuhelp_about = self.menuhelp.addAction(QtGui.QIcon('share/about32.png'), _('About'))
+        self.menuhelp_about = self.menuhelp.addAction(QtGui.QIcon('share/about32.png'), _('About FlatCAM'))
 
         # ## FlatCAM Editor menu ###
         self.geo_editor_menu = QtWidgets.QMenu(">Geo Editor<")
@@ -1728,10 +1728,23 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         # ###################################
         # ## Here we build the CNCJob Tab ###
         # ###################################
+        # self.cncjob_tab = QtWidgets.QWidget()
+        # self.cncjob_tab_layout = QtWidgets.QGridLayout(self.cncjob_tab)
+        # self.cncjob_tab_layout.setContentsMargins(2, 2, 2, 2)
+        # self.cncjob_tab.setLayout(self.cncjob_tab_layout)
+
         self.cncjob_tab = QtWidgets.QWidget()
-        self.cncjob_tab_layout = QtWidgets.QGridLayout(self.cncjob_tab)
+
+        self.c_temp_layout = QtWidgets.QVBoxLayout(self.cncjob_tab)
+        self.c_temp_layout.setContentsMargins(0, 0, 0, 0)
+
+        self.cncjob_frame = QtWidgets.QFrame()
+        self.cncjob_frame.setContentsMargins(0, 0, 0, 0)
+        self.c_temp_layout.addWidget(self.cncjob_frame)
+
+        self.cncjob_tab_layout = QtWidgets.QGridLayout(self.cncjob_frame)
         self.cncjob_tab_layout.setContentsMargins(2, 2, 2, 2)
-        self.cncjob_tab.setLayout(self.cncjob_tab_layout)
+        self.cncjob_frame.setLayout(self.cncjob_tab_layout)
 
         self.code_editor = FCTextAreaExtended()
         stylesheet = """
@@ -1743,24 +1756,43 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.code_editor.setStyleSheet(stylesheet)
 
         self.buttonPreview = QtWidgets.QPushButton(_('Print Preview'))
+        self.buttonPreview.setToolTip(_("Open a OS standard Preview Print window."))
         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.buttonPreview.setMinimumWidth(100)
+
         self.entryFind = FCEntry()
+        self.entryFind.setToolTip(_("Find box. Enter here the strings to be searched in the text."))
         self.entryFind.setMaximumWidth(200)
+
         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.entryReplace.setMaximumWidth(200)
+
         self.sel_all_cb = QtWidgets.QCheckBox(_('All'))
         self.sel_all_cb.setToolTip(
             _("When checked it will replace all instances in the 'Find' box\n"
               "with the text in the 'Replace' box..")
         )
         self.buttonOpen = QtWidgets.QPushButton(_('Open 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.cncjob_tab_layout.addWidget(self.code_editor, 0, 0, 1, 5)
 
         cnc_tab_lay_1 = QtWidgets.QHBoxLayout()
@@ -1782,6 +1814,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         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.cncjob_tab_layout.addLayout(cnc_tab_lay_4, 2, 4, 1, 1)
 
         # #################################
@@ -1835,7 +1869,6 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                              ('BETA' if beta else ''),
                              platform.architecture()[0])
                             )
-        self.show()
 
         self.filename = ""
         self.units = ""
@@ -3935,6 +3968,7 @@ class GeneralGUISetGroupUI(OptionsGroupUI):
         else:
             self.notebook_font_size_spinner.set_value(12)
 
+        # Axis Font Size
         self.axis_font_size_label = QtWidgets.QLabel('%s:' % _('Axis Font Size'))
         self.axis_font_size_label.setToolTip(
             _("This sets the font size for canvas axis.")
@@ -3953,6 +3987,18 @@ class GeneralGUISetGroupUI(OptionsGroupUI):
         # Just to add empty rows
         self.spacelabel = QtWidgets.QLabel('')
 
+        # Splash Screen
+        self.splash_label = QtWidgets.QLabel('%s:' % _('Splash Screen'))
+        self.splash_label.setToolTip(
+            _("Enable display of the splash screen at application startup.")
+        )
+        self.splash_cb = FCCheckBox()
+        settings = QSettings("Open Source", "FlatCAM")
+        if settings.value("splash_screen"):
+            self.splash_cb.set_value(True)
+        else:
+            self.splash_cb.set_value(False)
+
         # Add (label - input field) pair to the QFormLayout
         self.form_box.addRow(self.spacelabel, self.spacelabel)
 
@@ -3965,6 +4011,8 @@ class GeneralGUISetGroupUI(OptionsGroupUI):
         self.form_box.addRow(QtWidgets.QLabel(''))
         self.form_box.addRow(self.notebook_font_size_label, self.notebook_font_size_spinner)
         self.form_box.addRow(self.axis_font_size_label, self.axis_font_size_spinner)
+        self.form_box.addRow(QtWidgets.QLabel(''))
+        self.form_box.addRow(self.splash_label, self.splash_cb)
 
         # Add the QFormLayout that holds the Application general defaults
         # to the main layout of this TAB
@@ -4273,6 +4321,7 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
             self.portability_label.hide()
             self.portability_cb.hide()
 
+
 class GerberGenPrefGroupUI(OptionsGroupUI):
     def __init__(self, parent=None):
         # OptionsGroupUI.__init__(self, "Gerber General Preferences", parent=parent)
@@ -4331,6 +4380,27 @@ class GerberGenPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(buffering_label, 2, 0)
         grid0.addWidget(self.buffering_radio, 2, 1)
 
+        # Simplification
+        self.simplify_cb = FCCheckBox(label=_('Simplify'))
+        self.simplify_cb.setToolTip(_("When checked all the Gerber polygons will be\n"
+                                      "loaded with simplification having a set tolerance."))
+        grid0.addWidget(self.simplify_cb, 3, 0)
+
+        # Simplification tolerance
+        self.simplification_tol_label = QtWidgets.QLabel(_('Tolerance'))
+        self.simplification_tol_label.setToolTip(_("Tolerance for poligon simplification."))
+
+        self.simplification_tol_spinner = FCDoubleSpinner()
+        self.simplification_tol_spinner.set_precision(5)
+        self.simplification_tol_spinner.setWrapping(True)
+        self.simplification_tol_spinner.setRange(0.00000, 0.01000)
+        self.simplification_tol_spinner.setSingleStep(0.0001)
+
+        grid0.addWidget(self.simplification_tol_label, 4, 0)
+        grid0.addWidget(self.simplification_tol_spinner, 4, 1)
+        self.ois_simplif = OptionalInputSection(self.simplify_cb,
+                                                [self.simplification_tol_label, self.simplification_tol_spinner],
+                                                logic=True)
         self.layout.addStretch()
 
 

+ 7 - 5
flatcamTools/ToolNonCopperClear.py

@@ -1354,6 +1354,8 @@ class NonCopperClear(FlatCAMTool, Gerber):
         # determine if to use the progressive plotting
         if self.app.defaults["tools_ncc_plotting"] == 'progressive':
             prog_plot = True
+        else:
+            prog_plot = False
 
         if tools_storage is not None:
             tools_storage = tools_storage
@@ -1748,7 +1750,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
                                     continue
 
                                 pol_nr += 1
-                                disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 99]))
+                                disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
                                 # log.debug("Polygons cleared: %d" % pol_nr)
 
                                 if old_disp_number < disp_number <= 100:
@@ -2105,10 +2107,10 @@ class NonCopperClear(FlatCAMTool, Gerber):
                                                 rest_geo.append(poly)
 
                                 pol_nr += 1
-                                disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 99]))
+                                disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
                                 # log.debug("Polygons cleared: %d" % pol_nr)
 
-                                if disp_number > old_disp_number and disp_number <= 100:
+                                if old_disp_number < disp_number <= 100:
                                     self.app.proc_container.update_view_text(' %d%%' % disp_number)
                                     old_disp_number = disp_number
                                     # log.debug("Polygons cleared: %d. Percentage done: %d%%" % (pol_nr, disp_number))
@@ -2585,9 +2587,9 @@ class NonCopperClear(FlatCAMTool, Gerber):
                         raise FlatCAMApp.GracefulException
                     boundary = boundary.difference(el)
                     pol_nr += 1
-                    disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 99]))
+                    disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
 
-                    if disp_number > old_disp_number and disp_number <= 100:
+                    if old_disp_number < disp_number <= 100:
                         self.app.proc_container.update_view_text(' %d%%' % disp_number)
                         old_disp_number = disp_number
                 return boundary

+ 4 - 4
flatcamTools/ToolPaint.py

@@ -1694,7 +1694,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                         return "fail"
 
                     pol_nr += 1
-                    disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 99]))
+                    disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
                     # log.debug("Polygons cleared: %d" % pol_nr)
 
                     if old_disp_number < disp_number <= 100:
@@ -1848,7 +1848,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                         return "fail"
 
                     pol_nr += 1
-                    disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 99]))
+                    disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
                     # log.debug("Polygons cleared: %d" % pol_nr)
 
                     if old_disp_number < disp_number <= 100:
@@ -2155,7 +2155,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                         return
 
                     pol_nr += 1
-                    disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 99]))
+                    disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
                     # log.debug("Polygons cleared: %d" % pol_nr)
 
                     if old_disp_number < disp_number <= 100:
@@ -2313,7 +2313,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                         return
 
                     pol_nr += 1
-                    disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 99]))
+                    disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
                     # log.debug("Polygons cleared: %d" % pol_nr)
 
                     if old_disp_number < disp_number <= 100:

+ 192 - 25
flatcamTools/ToolPanelize.py

@@ -487,7 +487,7 @@ class Panelize(FlatCAMTool):
 
         def panelize_2():
             if panel_obj is not None:
-                self.app.inform.emit(_("Generating panel ... Please wait."))
+                self.app.inform.emit(_("Generating panel ... "))
 
                 self.app.progress.emit(0)
 
@@ -506,11 +506,24 @@ class Panelize(FlatCAMTool):
                             except KeyError:
                                 log.warning("Failed to copy option. %s" % str(option))
 
+                    geo_len_drills = len(panel_obj.drills) if panel_obj.drills else 0
+                    geo_len_slots = len(panel_obj.slots) if panel_obj.slots else 0
+
+                    element = 0
                     for row in range(rows):
                         currentx = 0.0
                         for col in range(columns):
+                            element += 1
+                            disp_number = 0
+                            old_disp_number = 0
+
                             if panel_obj.drills:
+                                drill_nr = 0
                                 for tool_dict in panel_obj.drills:
+                                    if self.app.abort_flag:
+                                        # graceful abort requested by the user
+                                        raise FlatCAMApp.GracefulException
+
                                     point_offseted = affinity.translate(tool_dict['point'], currentx, currenty)
                                     obj_fin.drills.append(
                                         {
@@ -518,8 +531,24 @@ class Panelize(FlatCAMTool):
                                             "tool": tool_dict['tool']
                                         }
                                     )
+
+                                    drill_nr += 1
+                                    disp_number = int(np.interp(drill_nr, [0, geo_len_drills], [0, 100]))
+
+                                    if disp_number > old_disp_number and disp_number <= 100:
+                                        self.app.proc_container.update_view_text(' %s: %d D:%d%%' %
+                                                                                 (_("Copy"),
+                                                                                  int(element),
+                                                                                  disp_number))
+                                        old_disp_number = disp_number
+
                             if panel_obj.slots:
+                                slot_nr = 0
                                 for tool_dict in panel_obj.slots:
+                                    if self.app.abort_flag:
+                                        # graceful abort requested by the user
+                                        raise FlatCAMApp.GracefulException
+
                                     start_offseted = affinity.translate(tool_dict['start'], currentx, currenty)
                                     stop_offseted = affinity.translate(tool_dict['stop'], currentx, currenty)
                                     obj_fin.slots.append(
@@ -529,12 +558,24 @@ class Panelize(FlatCAMTool):
                                             "tool": tool_dict['tool']
                                         }
                                     )
+
+                                    slot_nr += 1
+                                    disp_number = int(np.interp(slot_nr, [0, geo_len_slots], [0, 100]))
+
+                                    if disp_number > old_disp_number and disp_number <= 100:
+                                        self.app.proc_container.update_view_text(' %s: %d S:%d%%' %
+                                                                                 (_("Copy"),
+                                                                                  int(element),
+                                                                                  disp_number))
+                                        old_disp_number = disp_number
+
                             currentx += lenghtx
                         currenty += lenghty
 
                     obj_fin.create_geometry()
                     obj_fin.zeros = panel_obj.zeros
                     obj_fin.units = panel_obj.units
+                    self.app.proc_container.update_view_text('')
 
                 def job_init_geometry(obj_fin, app_obj):
                     currentx = 0.0
@@ -555,70 +596,196 @@ class Panelize(FlatCAMTool):
 
                     obj_fin.solid_geometry = []
 
+                    # create the initial structure on which to create the panel
                     if isinstance(panel_obj, FlatCAMGeometry):
                         obj_fin.multigeo = panel_obj.multigeo
                         obj_fin.tools = deepcopy(panel_obj.tools)
                         if panel_obj.multigeo is True:
                             for tool in panel_obj.tools:
                                 obj_fin.tools[tool]['solid_geometry'][:] = []
-
-                    if isinstance(panel_obj, FlatCAMGerber):
+                    elif isinstance(panel_obj, FlatCAMGerber):
                         obj_fin.apertures = deepcopy(panel_obj.apertures)
                         for ap in obj_fin.apertures:
                             obj_fin.apertures[ap]['geometry'] = list()
 
+                    # find the number of polygons in the source solid_geometry
+                    geo_len = 0
+                    if isinstance(panel_obj, FlatCAMGeometry):
+                        if panel_obj.multigeo is True:
+                            for tool in panel_obj.tools:
+                                try:
+                                    for pol in panel_obj.tools[tool]['solid_geometry']:
+                                        geo_len += 1
+                                except TypeError:
+                                    geo_len = 1
+                        else:
+                            try:
+                                for pol in panel_obj.solid_geometry:
+                                    geo_len += 1
+                            except TypeError:
+                                geo_len = 1
+                    elif isinstance(panel_obj, FlatCAMGerber):
+                        for ap in panel_obj.apertures:
+                            for elem in panel_obj.apertures[ap]['geometry']:
+                                geo_len += 1
+
                     self.app.progress.emit(0)
+                    element = 0
                     for row in range(rows):
                         currentx = 0.0
 
                         for col in range(columns):
+                            element += 1
+                            disp_number = 0
+                            old_disp_number = 0
+
                             if isinstance(panel_obj, FlatCAMGeometry):
                                 if panel_obj.multigeo is True:
                                     for tool in panel_obj.tools:
-                                        geo = translate_recursion(panel_obj.tools[tool]['solid_geometry'])
-                                        if isinstance(geo, list):
-                                            obj_fin.tools[tool]['solid_geometry'] += geo
-                                        else:
-                                            obj_fin.tools[tool]['solid_geometry'].append(geo)
+                                        if self.app.abort_flag:
+                                            # graceful abort requested by the user
+                                            raise FlatCAMApp.GracefulException
+
+                                        # geo = translate_recursion(panel_obj.tools[tool]['solid_geometry'])
+                                        # if isinstance(geo, list):
+                                        #     obj_fin.tools[tool]['solid_geometry'] += geo
+                                        # else:
+                                        #     obj_fin.tools[tool]['solid_geometry'].append(geo)
+
+                                        # calculate the number of polygons
+                                        geo_len = len(panel_obj.tools[tool]['solid_geometry'])
+                                        pol_nr = 0
+                                        for geo_el in panel_obj.tools[tool]['solid_geometry']:
+                                            trans_geo = translate_recursion(geo_el)
+                                            obj_fin.tools[tool]['solid_geometry'].append(trans_geo)
+
+                                            pol_nr += 1
+                                            disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
+
+                                            if old_disp_number < disp_number <= 100:
+                                                self.app.proc_container.update_view_text(' %s: %d %d%%' %
+                                                                                         (_("Copy"),
+                                                                                          int(element),
+                                                                                          disp_number))
+                                                old_disp_number = disp_number
                                 else:
-                                    geo = translate_recursion(panel_obj.solid_geometry)
-                                    if isinstance(geo, list):
-                                        obj_fin.solid_geometry += geo
-                                    else:
-                                        obj_fin.solid_geometry.append(geo)
+                                    # geo = translate_recursion(panel_obj.solid_geometry)
+                                    # if isinstance(geo, list):
+                                    #     obj_fin.solid_geometry += geo
+                                    # else:
+                                    #     obj_fin.solid_geometry.append(geo)
+                                    if self.app.abort_flag:
+                                        # graceful abort requested by the user
+                                        raise FlatCAMApp.GracefulException
+
+                                    try:
+                                        # calculate the number of polygons
+                                        geo_len = len(panel_obj.solid_geometry)
+                                    except TypeError:
+                                        geo_len = 1
+                                    pol_nr = 0
+                                    try:
+                                        for geo_el in panel_obj.solid_geometry:
+                                            if self.app.abort_flag:
+                                                # graceful abort requested by the user
+                                                raise FlatCAMApp.GracefulException
+
+                                            trans_geo = translate_recursion(geo_el)
+                                            obj_fin.solid_geometry.append(trans_geo)
+
+                                            pol_nr += 1
+                                            disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
+
+                                            if old_disp_number < disp_number <= 100:
+                                                self.app.proc_container.update_view_text(' %s: %d %d%%' %
+                                                                                         (_("Copy"),
+                                                                                          int(element),
+                                                                                          disp_number))
+                                                old_disp_number = disp_number
+                                    except TypeError:
+                                        trans_geo = translate_recursion(panel_obj.solid_geometry)
+                                        obj_fin.solid_geometry.append(trans_geo)
                             else:
-                                geo = translate_recursion(panel_obj.solid_geometry)
-                                if isinstance(geo, list):
-                                    obj_fin.solid_geometry += geo
-                                else:
-                                    obj_fin.solid_geometry.append(geo)
+                                # geo = translate_recursion(panel_obj.solid_geometry)
+                                # if isinstance(geo, list):
+                                #     obj_fin.solid_geometry += geo
+                                # else:
+                                #     obj_fin.solid_geometry.append(geo)
+                                if self.app.abort_flag:
+                                    # graceful abort requested by the user
+                                    raise FlatCAMApp.GracefulException
+
+                                try:
+                                    for geo_el in panel_obj.solid_geometry:
+                                        if self.app.abort_flag:
+                                            # graceful abort requested by the user
+                                            raise FlatCAMApp.GracefulException
+
+                                        trans_geo = translate_recursion(geo_el)
+                                        obj_fin.solid_geometry.append(trans_geo)
+                                except TypeError:
+                                    trans_geo = translate_recursion(panel_obj.solid_geometry)
+                                    obj_fin.solid_geometry.append(trans_geo)
 
                                 for apid in panel_obj.apertures:
+                                    if self.app.abort_flag:
+                                        # graceful abort requested by the user
+                                        raise FlatCAMApp.GracefulException
+
+                                    try:
+                                        # calculate the number of polygons
+                                        geo_len = len(panel_obj.apertures[apid]['geometry'])
+                                    except TypeError:
+                                        geo_len = 1
+                                    pol_nr = 0
                                     for el in panel_obj.apertures[apid]['geometry']:
+                                        if self.app.abort_flag:
+                                            # graceful abort requested by the user
+                                            raise FlatCAMApp.GracefulException
+
                                         new_el = dict()
                                         if 'solid' in el:
                                             geo_aper = translate_recursion(el['solid'])
-                                            new_el['solid'] = deepcopy(geo_aper)
+                                            new_el['solid'] = geo_aper
 
                                         if 'clear' in el:
                                             geo_aper = translate_recursion(el['clear'])
-                                            new_el['clear'] = deepcopy(geo_aper)
+                                            new_el['clear'] = geo_aper
 
                                         if 'follow' in el:
                                             geo_aper = translate_recursion(el['follow'])
-                                            new_el['follow'] = deepcopy(geo_aper)
+                                            new_el['follow'] = geo_aper
 
                                         obj_fin.apertures[apid]['geometry'].append(deepcopy(new_el))
 
+                                        pol_nr += 1
+                                        disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
+
+                                        if old_disp_number < disp_number <= 100:
+                                            self.app.proc_container.update_view_text(' %s: %d %d%%' %
+                                                                                     (_("Copy"),
+                                                                                      int(element),
+                                                                                      disp_number))
+                                            old_disp_number = disp_number
+
                             currentx += lenghtx
                         currenty += lenghty
 
-                    app_obj.log.debug("Found %s geometries. Creating a panel geometry cascaded union ..." %
-                                      len(obj_fin.solid_geometry))
+                    if panel_type == 'gerber':
+                        self.app.inform.emit('%s %s' %
+                                             (_("Generating panel ..."), _("Adding the Gerber code.")))
+                        obj_fin.source_file = self.app.export_gerber(obj_name=self.outname, filename=None,
+                                                                     local_use=obj_fin, use_thread=False)
+
+                    # app_obj.log.debug("Found %s geometries. Creating a panel geometry cascaded union ..." %
+                    #                   len(obj_fin.solid_geometry))
 
                     # obj_fin.solid_geometry = cascaded_union(obj_fin.solid_geometry)
-                    app_obj.log.debug("Finished creating a cascaded union for the panel.")
+                    # app_obj.log.debug("Finished creating a cascaded union for the panel.")
+                    self.app.proc_container.update_view_text('')
 
+                self.app.inform.emit('%s %s: %d' %
+                                     (_("Generating panel ..."), _("Spawning copies"), (int(rows * columns))))
                 if isinstance(panel_obj, FlatCAMExcellon):
                     self.app.progress.emit(50)
                     self.app.new_object("excellon", self.outname, job_init_excellon, plot=True, autoselected=True)
@@ -635,7 +802,7 @@ class Panelize(FlatCAMTool):
                                    "Final panel has {col} columns and {row} rows").format(
                 text='[WARNING] ', col=columns, row=rows))
 
-        proc = self.app.proc_container.new(_("Generating panel..."))
+        proc = self.app.proc_container.new(_("Working..."))
 
         def job_thread(app_obj):
             try:

+ 11 - 11
flatcamTools/ToolShell.py

@@ -24,8 +24,8 @@ if '_' not in builtins.__dict__:
 
 class TermWidget(QWidget):
     """
-    Widget wich represents terminal. It only displays text and allows to enter text.
-    All highlevel logic should be implemented by client classes
+    Widget which represents terminal. It only displays text and allows to enter text.
+    All high level logic should be implemented by client classes
 
     User pressed Enter. Client class should decide, if command must be executed or user may continue edit it
     """
@@ -57,7 +57,7 @@ class TermWidget(QWidget):
 
     def open_proccessing(self, detail=None):
         """
-        Open processing and disable using shell commands  again until all commands are finished
+        Open processing and disable using shell commands again until all commands are finished
 
         :param detail: text detail about what is currently called from TCL to python
         :return: None
@@ -114,10 +114,10 @@ class TermWidget(QWidget):
         self._browser.moveCursor(QTextCursor.End)
         self._browser.insertHtml(text)
 
-        """TODO When user enters second line to the input, and input is resized, scrollbar changes its positon
+        """TODO When user enters second line to the input, and input is resized, scrollbar changes its position
         and stops moving. As quick fix of this problem, now we always scroll down when add new text.
-        To fix it correctly, srcoll to the bottom, if before intput has been resized,
-        scrollbar was in the bottom, and remove next lien
+        To fix it correctly, scroll to the bottom, if before input has been resized,
+        scrollbar was in the bottom, and remove next line
         """
         scrollattheend = True
 
@@ -129,13 +129,12 @@ class TermWidget(QWidget):
     def exec_current_command(self):
         """
         Save current command in the history. Append it to the log. Clear edit line
-        Reimplement in the child classes to actually execute command
+        Re-implement in the child classes to actually execute command
         """
         text = str(self._edit.toPlainText())
         self._append_to_browser('in', '> ' + text + '\n')
 
-        if len(self._history) < 2 or\
-           self._history[-2] != text:  # don't insert duplicating items
+        if len(self._history) < 2 or self._history[-2] != text:  # don't insert duplicating items
             if text[-1] == '\n':
                 self._history.insert(-1, text[:-1])
             else:
@@ -153,7 +152,7 @@ class TermWidget(QWidget):
 
     def child_exec_command(self, text):
         """
-        Reimplement in the child classes
+        Re-implement in the child classes
         """
         pass
 
@@ -161,7 +160,8 @@ class TermWidget(QWidget):
         self._edit.textCursor().insertText('\n')
 
     def append_output(self, text):
-        """Appent text to output widget
+        """
+        Append text to output widget
         """
         self._append_to_browser('out', text)
 

BIN
locale/es/LC_MESSAGES/strings.mo


Разлика између датотеке није приказан због своје велике величине
+ 480 - 469
locale/es/LC_MESSAGES/strings.po



+ 1 - 1
tclCommands/TclCommandCopperClear.py

@@ -214,7 +214,7 @@ class TclCommandCopperClear(TclCommand):
         if 'all' in args and args['all'] == 1:
             self.app.ncclear_tool.clear_copper(ncc_obj=obj,
                                                select_method='itself',
-                                               tooldia=tooldia,
+                                               ncctooldia=tooldia,
                                                overlap=overlap,
                                                order=order,
                                                margin=margin,

+ 67 - 12
tclCommands/TclCommandDrillcncjob.py

@@ -1,7 +1,6 @@
 from ObjectCollection import *
 from tclCommands.TclCommand import TclCommandSignaled
 
-
 class TclCommandDrillcncjob(TclCommandSignaled):
     """
     Tcl shell command to Generates a Drill CNC Job from a Excellon Object.
@@ -18,6 +17,7 @@ class TclCommandDrillcncjob(TclCommandSignaled):
     # dictionary of types from Tcl command, needs to be ordered , this  is  for options  like -optionname value
     option_types = collections.OrderedDict([
         ('tools', str),
+        ('drilled_dias', str),
         ('drillz', float),
         ('travelz', float),
         ('feedrate', float),
@@ -25,10 +25,12 @@ class TclCommandDrillcncjob(TclCommandSignaled):
         ('spindlespeed', int),
         ('toolchange', bool),
         ('toolchangez', float),
+        ('toolchangexy', tuple),
         ('endz', float),
         ('ppname_e', str),
         ('outname', str),
-        ('opt_type', str)
+        ('opt_type', str),
+        ('diatol', float)
     ])
 
     # array of mandatory options for current Tcl command: required = {'name','outname'}
@@ -39,7 +41,8 @@ class TclCommandDrillcncjob(TclCommandSignaled):
         'main': "Generates a Drill CNC Job from a Excellon Object.",
         'args': collections.OrderedDict([
             ('name', 'Name of the source object.'),
-            ('tools', 'Comma separated indexes of tools (example: 1,3 or 2) or select all if not specified.'),
+            ('drilled_dias',
+             'Comma separated tool diameters of the drills to be drilled (example: 0.6, 1.0 or 3.125).'),
             ('drillz', 'Drill depth into material (example: -2.0).'),
             ('travelz', 'Travel distance above material (example: 2.0).'),
             ('feedrate', 'Drilling feed rate.'),
@@ -51,8 +54,14 @@ class TclCommandDrillcncjob(TclCommandSignaled):
             ('endz', 'Z distance at job end (example: 30.0).'),
             ('ppname_e', 'This is the Excellon postprocessor name: case_sensitive, no_quotes'),
             ('outname', 'Name of the resulting Geometry object.'),
-            ('opt_type', 'Name of move optimization type. R by default from Rtree or '
-                         'T from Travelling Salesman Algorithm')
+            ('opt_type', 'Name of move optimization type. B by default for Basic OR-Tools, M for Metaheuristic OR-Tools'
+                         'T from Travelling Salesman Algorithm. B and M works only for 64bit version of FlatCAM and '
+                         'T works only for 32bit version of FlatCAM'),
+            ('diatol', 'Tolerance. Percentange (0.0 ... 100.0) within which dias in drilled_dias will be judged to be '
+                       'the same as the ones in the tools from the Excellon object. E.g: if in drill_dias we have a '
+                       'diameter with value 1.0, in the Excellon we have a tool with dia = 1.05 and we set a tolerance '
+                       'diatol = 5.0 then the drills with the dia = (0.95 ... 1.05) '
+                       'in Excellon will be processed. Float number.')
         ]),
         'examples': ['drillcncjob test.TXT -drillz -1.5 -travelz 14 -feedrate 222 -feedrate_rapid 456 -spindlespeed 777'
                      ' -toolchange True -toolchangez 33 -endz 22 -ppname_e default\n'
@@ -87,8 +96,61 @@ class TclCommandDrillcncjob(TclCommandSignaled):
         ymax = obj.options['ymax']
 
         def job_init(job_obj, app_obj):
+            # tools = args["tools"] if "tools" in args else 'all'
+            units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+
+            try:
+                if 'drilled_dias' in args and args['drilled_dias'] != 'all':
+                    diameters = [x.strip() for x in args['drilled_dias'].split(",") if x!= '']
+                    nr_diameters = len(diameters)
+
+                    req_tools = set()
+                    for tool in obj.tools:
+                        for req_dia in diameters:
+                            obj_dia_form = float('%.2f' % float(obj.tools[tool]["C"])) if units == 'MM' else \
+                                float('%.4f' % float(obj.tools[tool]["C"]))
+                            req_dia_form = float('%.2f' % float(req_dia)) if units == 'MM' else \
+                                float('%.4f' % float(req_dia))
+
+                            if 'diatol' in args:
+                                tolerance = args['diatol'] / 100
+
+                                tolerance = 0.0 if tolerance < 0.0 else tolerance
+                                tolerance = 1.0 if tolerance > 1.0 else tolerance
+                                if math.isclose(obj_dia_form, req_dia_form, rel_tol=tolerance):
+                                    req_tools.add(tool)
+                                    nr_diameters -= 1
+                            else:
+                                if obj_dia_form == req_dia_form:
+                                    req_tools.add(tool)
+                                    nr_diameters -= 1
+
+                    if nr_diameters > 0:
+                        self.raise_tcl_error("One or more tool diameters of the drills to be drilled passed to the "
+                                             "TclCommand are not actual tool diameters in the Excellon object.")
+
+                    # make a string of diameters separated by comma; this is what generate_from_excellon_by_tool() is
+                    # expecting as tools parameter
+                    tools = ','.join(req_tools)
+
+                    # no longer needed
+                    del args['drilled_dias']
+                    del args['diatol']
+
+                    # Split and put back. We are passing the whole dictionary later.
+                    # args['milled_dias'] = [x.strip() for x in args['tools'].split(",")]
+                else:
+                    tools = 'all'
+            except Exception as e:
+                tools = 'all'
+                self.raise_tcl_error("Bad tools: %s" % str(e))
 
             drillz = args["drillz"] if "drillz" in args else obj.options["drillz"]
+            toolchangez = args["toolchangez"] if "toolchangez" in args else obj.options["toolchangez"]
+            endz = args["endz"] if "endz" in args else obj.options["endz"]
+            toolchange = True if "toolchange" in args and args["toolchange"] == 1 else False
+            opt_type = args["opt_type"] if "opt_type" in args else 'B'
+
             job_obj.z_move = args["travelz"] if "travelz" in args else obj.options["travelz"]
             job_obj.feedrate = args["feedrate"] if "feedrate" in args else obj.options["feedrate"]
             job_obj.feedrate_rapid = args["feedrate_rapid"] \
@@ -103,8 +165,6 @@ class TclCommandDrillcncjob(TclCommandSignaled):
 
             job_obj.options['type'] = 'Excellon'
 
-            toolchange = True if "toolchange" in args and args["toolchange"] == 1 else False
-            toolchangez = args["toolchangez"] if "toolchangez" in args else obj.options["toolchangez"]
             job_obj.toolchangexy = args["toolchangexy"] if "toolchangexy" in args else obj.options["toolchangexy"]
 
             job_obj.toolchange_xy_type = "excellon"
@@ -114,11 +174,6 @@ class TclCommandDrillcncjob(TclCommandSignaled):
             job_obj.options['xmax'] = xmax
             job_obj.options['ymax'] = ymax
 
-            endz = args["endz"] if "endz" in args else obj.options["endz"]
-
-            tools = args["tools"] if "tools" in args else 'all'
-            opt_type = args["opt_type"] if "opt_type" in args else 'B'
-
             job_obj.generate_from_excellon_by_tool(obj, tools, drillz=drillz, toolchangez=toolchangez,
                                                    endz=endz,
                                                    toolchange=toolchange, excellon_optimization_type=opt_type)

+ 138 - 0
tclCommands/TclCommandMillDrills.py

@@ -0,0 +1,138 @@
+from ObjectCollection import *
+from tclCommands.TclCommand import TclCommandSignaled
+
+
+class TclCommandMillDrills(TclCommandSignaled):
+    """
+    Tcl shell command to Create Geometry Object for milling holes from Excellon.
+
+    example:
+        millholes my_drill -tools 1,2,3 -tooldia 0.1 -outname mill_holes_geo
+    """
+
+    # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
+    aliases = ['milldrills', 'milld']
+
+    # Dictionary of types from Tcl command, needs to be ordered
+    arg_names = collections.OrderedDict([
+        ('name', str)
+    ])
+
+    # Dictionary of types from Tcl command, needs to be ordered.
+    # This is  for options  like -optionname value
+    option_types = collections.OrderedDict([
+        ('milled_dias', str),
+        ('outname', str),
+        ('tooldia', float),
+        ('use_threads', bool),
+        ('diatol', float)
+    ])
+
+    # array of mandatory options for current Tcl command: required = {'name','outname'}
+    required = ['name']
+
+    # structured help for current command, args needs to be ordered
+    help = {
+        'main': "Create Geometry Object for milling drill holes from Excellon.",
+        'args': collections.OrderedDict([
+            ('name', 'Name of the Excellon Object.'),
+            ('milled_dias', 'Comma separated tool diameters of the drills to be milled (example: 0.6, 1.0 or 3.125).'),
+            ('tooldia', 'Diameter of the milling tool (example: 0.1).'),
+            ('outname', 'Name of object to create.'),
+            ('use_thread', 'If to use multithreading: True or False.'),
+            ('diatol', 'Tolerance. Percentange (0.0 ... 100.0) within which dias in milled_dias will be judged to be '
+                       'the same as the ones in the tools from the Excellon object. E.g: if in milled_dias we have a '
+                       'diameter with value 1.0, in the Excellon we have a tool with dia = 1.05 and we set a tolerance '
+                       'diatol = 5.0 then the drills with the dia = (0.95 ... 1.05) '
+                       'in Excellon will be processed. Float number.')
+        ]),
+        'examples': ['milldrills mydrills', 'milld my_excellon.drl']
+    }
+
+    def execute(self, args, unnamed_args):
+        """
+
+        :param args: array of known named arguments and options
+        :param unnamed_args: array of other values which were passed into command
+            without -somename and  we do not have them in known arg_names
+        :return: None or exception
+        """
+
+        name = args['name']
+
+        if 'outname' not in args:
+            args['outname'] = name + "_mill_drills"
+
+        try:
+            obj = self.app.collection.get_by_name(str(name))
+        except:
+            obj = None
+            self.raise_tcl_error("Could not retrieve object: %s" % name)
+
+        if not obj.drills:
+            self.raise_tcl_error("The Excellon object has no drills: %s" % name)
+
+        units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+
+        try:
+            if 'milled_dias' in args and args['milled_dias'] != 'all':
+                diameters = [x.strip() for x in args['milled_dias'].split(",") if x != '']
+                nr_diameters = len(diameters)
+
+                req_tools = set()
+                for tool in obj.tools:
+                    for req_dia in diameters:
+                        obj_dia_form = float('%.2f' % float(obj.tools[tool]["C"])) if units == 'MM' else \
+                            float('%.4f' % float(obj.tools[tool]["C"]))
+                        req_dia_form = float('%.2f' % float(req_dia)) if units == 'MM' else \
+                            float('%.4f' % float(req_dia))
+
+                        if 'diatol' in args:
+                            tolerance = args['diatol'] / 100
+
+                            tolerance = 0.0 if tolerance < 0.0 else tolerance
+                            tolerance = 1.0 if tolerance > 1.0 else tolerance
+                            if math.isclose(obj_dia_form, req_dia_form, rel_tol=tolerance):
+                                req_tools.add(tool)
+                                nr_diameters -= 1
+                        else:
+                            if obj_dia_form == req_dia_form:
+                                req_tools.add(tool)
+                                nr_diameters -= 1
+
+                if nr_diameters > 0:
+                    self.raise_tcl_error("One or more tool diameters of the drills to be milled passed to the "
+                                         "TclCommand are not actual tool diameters in the Excellon object.")
+
+                args['tools'] = req_tools
+
+                # no longer needed
+                del args['milled_dias']
+                del args['diatol']
+
+                # Split and put back. We are passing the whole dictionary later.
+                # args['milled_dias'] = [x.strip() for x in args['tools'].split(",")]
+            else:
+                args['tools'] = 'all'
+        except Exception as e:
+            self.raise_tcl_error("Bad tools: %s" % str(e))
+
+        if not isinstance(obj, FlatCAMExcellon):
+            self.raise_tcl_error('Only Excellon objects can be mill-drilled, got %s %s.' % (name, type(obj)))
+
+        if self.app.collection.has_promises():
+            self.raise_tcl_error('!!!Promises exists, but should not here!!!')
+
+        try:
+            # 'name' is not an argument of obj.generate_milling()
+            del args['name']
+
+            # This runs in the background... Is blocking handled?
+            success, msg = obj.generate_milling_drills(**args)
+        except Exception as e:
+            success = None
+            msg = None
+            self.raise_tcl_error("Operation failed: %s" % str(e))
+
+        if not success:
+            self.raise_tcl_error(msg)

+ 0 - 91
tclCommands/TclCommandMillHoles.py

@@ -1,91 +0,0 @@
-from ObjectCollection import *
-from tclCommands.TclCommand import TclCommandSignaled
-
-
-class TclCommandMillHoles(TclCommandSignaled):
-    """
-    Tcl shell command to Create Geometry Object for milling holes from Excellon.
-
-    example:
-        millholes my_drill -tools 1,2,3 -tooldia 0.1 -outname mill_holes_geo
-    """
-
-    # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
-    aliases = ['millholes', 'mill']
-
-    # Dictionary of types from Tcl command, needs to be ordered
-    arg_names = collections.OrderedDict([
-        ('name', str)
-    ])
-
-    # Dictionary of types from Tcl command, needs to be ordered.
-    # This is  for options  like -optionname value
-    option_types = collections.OrderedDict([
-        ('tools', str),
-        ('outname', str),
-        ('tooldia', float),
-        ('use_threads', bool)
-    ])
-
-    # array of mandatory options for current Tcl command: required = {'name','outname'}
-    required = ['name']
-
-    # structured help for current command, args needs to be ordered
-    help = {
-        'main': "Create Geometry Object for milling holes from Excellon.",
-        'args': collections.OrderedDict([
-            ('name', 'Name of the Excellon Object.'),
-            ('tools', 'Comma separated indexes of tools (example: 1,3 or 2).'),
-            ('tooldia', 'Diameter of the milling tool (example: 0.1).'),
-            ('outname', 'Name of object to create.'),
-            ('use_thread', 'If to use multithreading: True or False.')
-        ]),
-        'examples': ['millholes mydrills']
-    }
-
-    def execute(self, args, unnamed_args):
-        """
-
-        :param args: array of known named arguments and options
-        :param unnamed_args: array of other values which were passed into command
-            without -somename and  we do not have them in known arg_names
-        :return: None or exception
-        """
-
-        name = args['name']
-
-        if 'outname' not in args:
-            args['outname'] = name + "_mill"
-
-        try:
-            if 'tools' in args and args['tools'] != 'all':
-                # Split and put back. We are passing the whole dictionary later.
-                args['tools'] = [x.strip() for x in args['tools'].split(",")]
-            else:
-                args['tools'] = 'all'
-        except Exception as e:
-            self.raise_tcl_error("Bad tools: %s" % str(e))
-
-        try:
-            obj = self.app.collection.get_by_name(str(name))
-        except:
-            self.raise_tcl_error("Could not retrieve object: %s" % name)
-
-        if not isinstance(obj, FlatCAMExcellon):
-            self.raise_tcl_error('Only Excellon objects can be mill-drilled, got %s %s.' % (name, type(obj)))
-
-        if self.app.collection.has_promises():
-            self.raise_tcl_error('!!!Promises exists, but should not here!!!')
-
-        try:
-            # 'name' is not an argument of obj.generate_milling()
-            del args['name']
-
-            # This runs in the background... Is blocking handled?
-            success, msg = obj.generate_milling(**args)
-
-        except Exception as e:
-            self.raise_tcl_error("Operation failed: %s" % str(e))
-
-        if not success:
-            self.raise_tcl_error(msg)

+ 138 - 0
tclCommands/TclCommandMillSlots.py

@@ -0,0 +1,138 @@
+from ObjectCollection import *
+from tclCommands.TclCommand import TclCommandSignaled
+
+
+class TclCommandMillSlots(TclCommandSignaled):
+    """
+    Tcl shell command to Create Geometry Object for milling holes from Excellon.
+
+    example:
+        millholes my_drill -tools 1,2,3 -tooldia 0.1 -outname mill_holes_geo
+    """
+
+    # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
+    aliases = ['millslots', 'mills']
+
+    # Dictionary of types from Tcl command, needs to be ordered
+    arg_names = collections.OrderedDict([
+        ('name', str)
+    ])
+
+    # Dictionary of types from Tcl command, needs to be ordered.
+    # This is  for options  like -optionname value
+    option_types = collections.OrderedDict([
+        ('milled_dias', str),
+        ('outname', str),
+        ('tooldia', float),
+        ('use_threads', bool),
+        ('diatol', float)
+    ])
+
+    # array of mandatory options for current Tcl command: required = {'name','outname'}
+    required = ['name']
+
+    # structured help for current command, args needs to be ordered
+    help = {
+        'main': "Create Geometry Object for milling slot holes from Excellon.",
+        'args': collections.OrderedDict([
+            ('name', 'Name of the Excellon Object.'),
+            ('milled_dias', 'Comma separated tool diameters of the slots to be milled (example: 0.6, 1.0 or 3.125).'),
+            ('tooldia', 'Diameter of the milling tool (example: 0.1).'),
+            ('outname', 'Name of object to create.'),
+            ('use_thread', 'If to use multithreading: True or False.'),
+            ('diatol', 'Tolerance. Percentange (0.0 ... 100.0) within which dias in milled_dias will be judged to be '
+                       'the same as the ones in the tools from the Excellon object. E.g: if in milled_dias we have a '
+                       'diameter with value 1.0, in the Excellon we have a tool with dia = 1.05 and we set a tolerance '
+                       'diatol = 5.0 then the slots with the dia = (0.95 ... 1.05) '
+                       'in Excellon will be processed. Float number.')
+        ]),
+        'examples': ['millslots mydrills', 'mills my_excellon.drl']
+    }
+
+    def execute(self, args, unnamed_args):
+        """
+
+        :param args: array of known named arguments and options
+        :param unnamed_args: array of other values which were passed into command
+            without -somename and  we do not have them in known arg_names
+        :return: None or exception
+        """
+
+        name = args['name']
+
+        if 'outname' not in args:
+            args['outname'] = name + "_mill_slots"
+
+        try:
+            obj = self.app.collection.get_by_name(str(name))
+        except:
+            obj = None
+            self.raise_tcl_error("Could not retrieve object: %s" % name)
+
+        if not obj.slots:
+            self.raise_tcl_error("The Excellon object has no slots: %s" % name)
+
+        units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
+        try:
+            if 'milled_dias' in args and args['milled_dias'] != 'all':
+                diameters = [x.strip() for x in args['milled_dias'].split(",")]
+                nr_diameters = len(diameters)
+
+                req_tools = set()
+                for tool in obj.tools:
+                    for req_dia in diameters:
+                        obj_dia_form = float('%.2f' % float(obj.tools[tool]["C"])) if units == 'MM' else \
+                            float('%.4f' % float(obj.tools[tool]["C"]))
+                        req_dia_form = float('%.2f' % float(req_dia)) if units == 'MM' else \
+                            float('%.4f' % float(req_dia))
+
+                        if 'diatol' in args:
+                            tolerance = args['diatol'] / 100
+
+                            tolerance = 0.0 if tolerance < 0.0 else tolerance
+                            tolerance = 1.0 if tolerance > 1.0 else tolerance
+                            if math.isclose(obj_dia_form, req_dia_form, rel_tol=tolerance):
+                                req_tools.add(tool)
+                                nr_diameters -= 1
+                        else:
+                            if obj_dia_form == req_dia_form:
+                                req_tools.add(tool)
+                                nr_diameters -= 1
+
+                if nr_diameters > 0:
+                    self.raise_tcl_error("One or more tool diameters of the slots to be milled passed to the "
+                                         "TclCommand are not actual tool diameters in the Excellon object.")
+
+                args['tools'] = req_tools
+
+                # no longer needed
+                del args['milled_dias']
+                del args['diatol']
+
+                # Split and put back. We are passing the whole dictionary later.
+                # args['milled_dias'] = [x.strip() for x in args['tools'].split(",")]
+            else:
+                args['tools'] = 'all'
+        except Exception as e:
+            self.raise_tcl_error("Bad tools: %s" % str(e))
+
+        if not isinstance(obj, FlatCAMExcellon):
+            self.raise_tcl_error('Only Excellon objects can have mill-slots, got %s %s.' % (name, type(obj)))
+
+        if self.app.collection.has_promises():
+            self.raise_tcl_error('!!!Promises exists, but should not here!!!')
+
+        try:
+            # 'name' is not an argument of obj.generate_milling()
+            del args['name']
+
+            # This runs in the background... Is blocking handled?
+            success, msg = obj.generate_milling_slots(**args)
+
+        except Exception as e:
+            success = None
+            msg = None
+            self.raise_tcl_error("Operation failed: %s" % str(e))
+
+        if not success:
+            self.raise_tcl_error(msg)

+ 2 - 1
tclCommands/__init__.py

@@ -30,7 +30,8 @@ import tclCommands.TclCommandFollow
 import tclCommands.TclCommandJoinExcellon
 import tclCommands.TclCommandJoinGeometry
 import tclCommands.TclCommandListSys
-import tclCommands.TclCommandMillHoles
+import tclCommands.TclCommandMillDrills
+import tclCommands.TclCommandMillSlots
 import tclCommands.TclCommandMirror
 import tclCommands.TclCommandNew
 import tclCommands.TclCommandNregions

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