Ver código fonte

jpcgt/flatcam/Beta слито с Beta

Camellan 6 anos atrás
pai
commit
f6dedfea49
41 arquivos alterados com 3292 adições e 2124 exclusões
  1. 253 52
      FlatCAMApp.py
  2. 44 15
      FlatCAMCommon.py
  3. 269 61
      FlatCAMObj.py
  4. 30 12
      ObjectCollection.py
  5. 45 1
      README.md
  6. 71 34
      camlib.py
  7. 0 2
      flatcamEditors/FlatCAMGeoEditor.py
  8. 14 1
      flatcamGUI/FlatCAMGUI.py
  9. 133 43
      flatcamGUI/ObjectUI.py
  10. 2 2
      flatcamGUI/PlotCanvas.py
  11. 63 18
      flatcamGUI/PreferencesUI.py
  12. 7 0
      flatcamGUI/VisPyCanvas.py
  13. 10 9
      flatcamParsers/ParseExcellon.py
  14. 423 0
      flatcamParsers/ParseHPGL2.py
  15. 411 213
      flatcamTools/ToolCalibration.py
  16. 1 1
      flatcamTools/ToolCopperThieving.py
  17. 1 1
      flatcamTools/ToolDblSided.py
  18. 4 1
      flatcamTools/ToolDistance.py
  19. 4 1
      flatcamTools/ToolDistanceMin.py
  20. 1 1
      flatcamTools/ToolFiducials.py
  21. 28 12
      flatcamTools/ToolFilm.py
  22. 1 0
      flatcamTools/ToolNonCopperClear.py
  23. 2 0
      flatcamTools/ToolPaint.py
  24. 2 4
      flatcamTools/ToolSolderPaste.py
  25. BIN
      locale/en/LC_MESSAGES/strings.mo
  26. 222 230
      locale/en/LC_MESSAGES/strings.po
  27. BIN
      locale/es/LC_MESSAGES/strings.mo
  28. 225 297
      locale/es/LC_MESSAGES/strings.po
  29. BIN
      locale/pt_BR/LC_MESSAGES/strings.mo
  30. 221 294
      locale/pt_BR/LC_MESSAGES/strings.po
  31. BIN
      locale/ro/LC_MESSAGES/strings.mo
  32. 217 239
      locale/ro/LC_MESSAGES/strings.po
  33. BIN
      locale/ru/LC_MESSAGES/strings.mo
  34. 219 252
      locale/ru/LC_MESSAGES/strings.po
  35. 357 323
      locale_template/strings.pot
  36. 4 4
      requirements.txt
  37. BIN
      share/calibrate_16.png
  38. BIN
      share/calibrate_32.png
  39. 6 1
      tclCommands/TclCommandCncjob.py
  40. 1 0
      tclCommands/TclCommandCopperClear.py
  41. 1 0
      tclCommands/TclCommandPaint.py

+ 253 - 52
FlatCAMApp.py

@@ -23,8 +23,14 @@ from stat import S_IREAD, S_IRGRP, S_IROTH
 import subprocess
 import ctypes
 
-import tkinter as tk
-from PyQt5 import QtPrintSupport
+# import tkinter as tk
+# from PyQt5 import QtPrintSupport
+
+from reportlab.graphics import renderPDF
+from reportlab.pdfgen import canvas
+from reportlab.graphics import renderPM
+from reportlab.lib.units import inch, mm
+from reportlab.lib.pagesizes import landscape, portrait
 
 from contextlib import contextmanager
 import gc
@@ -57,6 +63,8 @@ from flatcamEditors.FlatCAMExcEditor import FlatCAMExcEditor
 from flatcamEditors.FlatCAMGrbEditor import FlatCAMGrbEditor
 from flatcamEditors.FlatCAMTextEditor import TextEditor
 
+from flatcamParsers.ParseHPGL2 import HPGL2
+
 from FlatCAMProcess import *
 from FlatCAMWorkerStack import WorkerStack
 # from flatcamGUI.VisPyVisuals import Color
@@ -133,7 +141,7 @@ class App(QtCore.QObject):
     # ################## Version and VERSION DATE ##############################
     # ##########################################################################
     version = 8.99
-    version_date = "2019/12/12"
+    version_date = "2019/12/15"
     beta = True
     engine = '3D'
 
@@ -591,7 +599,7 @@ class App(QtCore.QObject):
             "excellon_travelz": 2,
             "excellon_endz": 0.5,
             "excellon_feedrate": 300,
-            "excellon_spindlespeed": None,
+            "excellon_spindlespeed": 0,
             "excellon_dwell": False,
             "excellon_dwelltime": 1,
             "excellon_toolchange": False,
@@ -658,7 +666,7 @@ class App(QtCore.QObject):
             "geometry_endz": 15.0,
             "geometry_feedrate": 120,
             "geometry_feedrate_z": 60,
-            "geometry_spindlespeed": None,
+            "geometry_spindlespeed": 0,
             "geometry_dwell": False,
             "geometry_dwelltime": 1,
             "geometry_ppname_g": 'default',
@@ -668,6 +676,7 @@ class App(QtCore.QObject):
             "geometry_startz": None,
             "geometry_feedrate_rapid": 1500,
             "geometry_extracut": False,
+            "geometry_extracut_length": 0.1,
             "geometry_z_pdepth": -0.02,
             "geometry_f_plunge": False,
             "geometry_spindledir": 'CW',
@@ -901,12 +910,14 @@ class App(QtCore.QObject):
             "tools_cal_verz": 0.1,
             "tools_cal_zeroz": False,
             "tools_cal_toolchangez": 15,
+            "tools_cal_toolchange_xy": '',
+            "tools_cal_sec_point": 'tl',
 
             # Utilities
             # file associations
             "fa_excellon": 'drd, drl, exc, ncd, tap, xln',
             "fa_gcode": 'cnc, din, dnc, ecs, eia, fan, fgc, fnc, gc, gcd, gcode, h, hnc, i, min, mpf, mpr, nc, ncc, '
-                        'ncg, ncp, ngc, out, plt, ply, rol, sbp, tap, xpi',
+                        'ncg, ncp, ngc, out, ply, rol, sbp, tap, xpi',
             "fa_gerber": 'art, bot, bsm, cmp, crc, crs, dim, gb0, gb1, gb2, gb3, gb4, gb5, gb6, gb7, gb8, gb9, gbd, '
                          'gbl, gbo, gbp, gbr, gbs, gdo, ger, gko, gm1, gm2, gm3, grb, gtl, gto, gtp, gts, ly15, ly2, '
                          'mil, pho, plc, pls, smb, smt, sol, spb, spt, ssb, sst, stc, sts, top, tsm',
@@ -1260,6 +1271,7 @@ class App(QtCore.QObject):
             "geometry_startz": self.ui.geometry_defaults_form.geometry_adv_opt_group.gstartz_entry,
             "geometry_feedrate_rapid": self.ui.geometry_defaults_form.geometry_adv_opt_group.cncfeedrate_rapid_entry,
             "geometry_extracut": self.ui.geometry_defaults_form.geometry_adv_opt_group.extracut_cb,
+            "geometry_extracut_length": self.ui.geometry_defaults_form.geometry_adv_opt_group.e_cut_entry,
             "geometry_z_pdepth": self.ui.geometry_defaults_form.geometry_adv_opt_group.pdepth_entry,
             "geometry_feedrate_probe": self.ui.geometry_defaults_form.geometry_adv_opt_group.feedrate_probe_entry,
             "geometry_spindledir": self.ui.geometry_defaults_form.geometry_adv_opt_group.spindledir_radio,
@@ -1482,6 +1494,8 @@ class App(QtCore.QObject):
             "tools_cal_verz": self.ui.tools2_defaults_form.tools2_cal_group.verz_entry,
             "tools_cal_zeroz": self.ui.tools2_defaults_form.tools2_cal_group.zeroz_cb,
             "tools_cal_toolchangez": self.ui.tools2_defaults_form.tools2_cal_group.toolchangez_entry,
+            "tools_cal_toolchange_xy": self.ui.tools2_defaults_form.tools2_cal_group.toolchange_xy_entry,
+            "tools_cal_sec_point": self.ui.tools2_defaults_form.tools2_cal_group.second_point_radio,
 
             # Utilities
             # File associations
@@ -1759,7 +1773,7 @@ class App(QtCore.QObject):
 
         self.ui.menufileimportdxf.triggered.connect(lambda: self.on_file_importdxf("geometry"))
         self.ui.menufileimportdxf_as_gerber.triggered.connect(lambda: self.on_file_importdxf("gerber"))
-
+        self.ui.menufileimport_hpgl2_as_geo.triggered.connect(self.on_fileopenhpgl2)
         self.ui.menufileexportsvg.triggered.connect(self.on_file_exportsvg)
         self.ui.menufileexportpng.triggered.connect(self.on_file_exportpng)
         self.ui.menufileexportexcellon.triggered.connect(self.on_file_exportexcellon)
@@ -1770,6 +1784,7 @@ class App(QtCore.QObject):
         self.ui.menufilesaveproject.triggered.connect(self.on_file_saveproject)
         self.ui.menufilesaveprojectas.triggered.connect(self.on_file_saveprojectas)
         self.ui.menufilesaveprojectcopy.triggered.connect(lambda: self.on_file_saveprojectas(make_copy=True))
+        self.ui.menufilesave_object_pdf.triggered.connect(self.on_file_save_object_pdf)
         self.ui.menufilesavedefaults.triggered.connect(self.on_file_savedefaults)
 
         self.ui.menufileexportpref.triggered.connect(self.on_export_preferences)
@@ -2476,6 +2491,7 @@ class App(QtCore.QObject):
 
         # variable to store coordinates
         self.pos = (0, 0)
+        self.pos_canvas = (0, 0)
         self.pos_jump = (0, 0)
 
         # variable to store mouse coordinates
@@ -2545,7 +2561,7 @@ class App(QtCore.QObject):
         self.exc_list = ['drd', 'drl', 'exc', 'ncd', 'tap', 'txt', 'xln']
 
         self.gcode_list = ['cnc', 'din', 'dnc', 'ecs', 'eia', 'fan', 'fgc', 'fnc', 'gc', 'gcd', 'gcode', 'h', 'hnc',
-                           'i', 'min', 'mpf', 'mpr', 'nc', 'ncc', 'ncg', 'ngc', 'ncp', 'out', 'plt', 'ply', 'rol',
+                           'i', 'min', 'mpf', 'mpr', 'nc', 'ncc', 'ncg', 'ngc', 'ncp', 'out', 'ply', 'rol',
                            'sbp', 'tap', 'xpi']
         self.svg_list = ['svg']
         self.dxf_list = ['dxf']
@@ -2979,7 +2995,7 @@ class App(QtCore.QObject):
         self.dblsidedtool.install(icon=QtGui.QIcon('share/doubleside16.png'), separator=True)
 
         self.cal_exc_tool = ToolCalibration(self)
-        self.cal_exc_tool.install(icon=QtGui.QIcon('share/drill16.png'), pos=self.ui.menutool,
+        self.cal_exc_tool.install(icon=QtGui.QIcon('share/calibrate_16.png'), pos=self.ui.menutool,
                                   before=self.dblsidedtool.menuAction,
                                   separator=False)
         self.distance_tool = Distance(self)
@@ -3148,6 +3164,7 @@ class App(QtCore.QObject):
 
         # Tools Toolbar Signals
         self.ui.dblsided_btn.triggered.connect(lambda: self.dblsidedtool.run(toggle=True))
+        self.ui.cal_btn.triggered.connect(lambda: self.cal_exc_tool.run(toggle=True))
         self.ui.cutout_btn.triggered.connect(lambda: self.cutout_tool.run(toggle=True))
         self.ui.ncc_btn.triggered.connect(lambda: self.ncclear_tool.run(toggle=True))
         self.ui.paint_btn.triggered.connect(lambda: self.paint_tool.run(toggle=True))
@@ -4237,7 +4254,7 @@ class App(QtCore.QObject):
             commands_list = "# AddCircle, AddPolygon, AddPolyline, AddRectangle, AlignDrill, " \
                             "AlignDrillGrid, Bbox, Bounds, ClearShell, CopperClear,\n"\
                             "# Cncjob, Cutout, Delete, Drillcncjob, ExportDXF, ExportExcellon, ExportGcode,\n" \
-                            "ExportGerber, ExportSVG, Exteriors, Follow, GeoCutout, GeoUnion, GetNames,\n"\
+                            "# ExportGerber, ExportSVG, Exteriors, Follow, GeoCutout, GeoUnion, GetNames,\n"\
                             "# GetSys, ImportSvg, Interiors, Isolate, JoinExcellon, JoinGeometry, " \
                             "ListSys, MillDrills,\n"\
                             "# MillSlots, Mirror, New, NewExcellon, NewGeometry, NewGerber, Nregions, " \
@@ -4297,23 +4314,41 @@ class App(QtCore.QObject):
         # self.inform.emit('[selected] %s created & selected: %s' %
         #                  (str(obj.kind).capitalize(), str(obj.options['name'])))
         if obj.kind == 'gerber':
-            self.inform.emit(_('[selected] {kind} created/selected: <span style="color:{color};">{name}</span>').format(
-                kind=obj.kind.capitalize(), color='green', name=str(obj.options['name'])))
+            self.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
+                kind=obj.kind.capitalize(),
+                color='green',
+                name=str(obj.options['name']), tx=_("created/selected"))
+            )
         elif obj.kind == 'excellon':
-            self.inform.emit(_('[selected] {kind} created/selected: <span style="color:{color};">{name}</span>').format(
-                kind=obj.kind.capitalize(), color='brown', name=str(obj.options['name'])))
+            self.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
+                kind=obj.kind.capitalize(),
+                color='brown',
+                name=str(obj.options['name']), tx=_("created/selected"))
+            )
         elif obj.kind == 'cncjob':
-            self.inform.emit(_('[selected] {kind} created/selected: <span style="color:{color};">{name}</span>').format(
-                kind=obj.kind.capitalize(), color='blue', name=str(obj.options['name'])))
+            self.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
+                kind=obj.kind.capitalize(),
+                color='blue',
+                name=str(obj.options['name']), tx=_("created/selected"))
+            )
         elif obj.kind == 'geometry':
-            self.inform.emit(_('[selected] {kind} created/selected: <span style="color:{color};">{name}</span>').format(
-                kind=obj.kind.capitalize(), color='red', name=str(obj.options['name'])))
+            self.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
+                kind=obj.kind.capitalize(),
+                color='red',
+                name=str(obj.options['name']), tx=_("created/selected"))
+            )
         elif obj.kind == 'script':
-            self.inform.emit(_('[selected] {kind} created/selected: <span style="color:{color};">{name}</span>').format(
-                kind=obj.kind.capitalize(), color='orange', name=str(obj.options['name'])))
+            self.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
+                kind=obj.kind.capitalize(),
+                color='orange',
+                name=str(obj.options['name']), tx=_("created/selected"))
+            )
         elif obj.kind == 'document':
-            self.inform.emit(_('[selected] {kind} created/selected: <span style="color:{color};">{name}</span>').format(
-                kind=obj.kind.capitalize(), color='darkCyan', name=str(obj.options['name'])))
+            self.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
+                kind=obj.kind.capitalize(),
+                color='darkCyan',
+                name=str(obj.options['name']), tx=_("created/selected"))
+            )
 
         # update the SHELL auto-completer model with the name of the new object
         self.shell._edit.set_model_data(self.myKeywords)
@@ -4468,10 +4503,11 @@ class App(QtCore.QObject):
                 attributions_label = QtWidgets.QLabel(
                     _(
                         'Some of the icons used are from the following sources:<br>'
-                        '<div>Icons made by <a href="https://www.flaticon.com/authors/freepik" '
+                        '<div>Icons by <a href="https://www.flaticon.com/authors/freepik" '
                         'title="Freepik">Freepik</a> from <a href="https://www.flaticon.com/"             '
                         'title="Flaticon">www.flaticon.com</a></div>'
-                        'Icons by <a target="_blank" href="https://icons8.com">Icons8</a>'
+                        '<div>Icons by <a target="_blank" href="https://icons8.com">Icons8</a></div>'
+                        'Icons by <a href="http://www.onlinewebfonts.com">oNline Web Fonts</a>'
                     )
                 )
 
@@ -7346,7 +7382,11 @@ class App(QtCore.QObject):
 
             canvas_origin = self.plotcanvas.native.mapToGlobal(QtCore.QPoint(0, 0))
             jump_loc = self.plotcanvas.translate_coords_2((cal_location[0], cal_location[1]))
-            j_pos = (canvas_origin.x() + jump_loc[0], (canvas_origin.y() + jump_loc[1]))
+
+            j_pos = (
+                int(canvas_origin.x() + round(jump_loc[0])),
+                int(canvas_origin.y() + round(jump_loc[1]))
+            )
             cursor.setPos(j_pos[0], j_pos[1])
         else:
             # find the canvas origin which is in the top left corner
@@ -7358,10 +7398,15 @@ class App(QtCore.QObject):
             # in pixels where the origin 0,0 is in the lowest left point of the display window (in our case is the
             # canvas) and the point (width, height) is in the top-right location
             loc = self.plotcanvas.axes.transData.transform_point(location)
-            j_pos = (x0 + loc[0], y0 - loc[1])
+            j_pos = (
+                int(x0 + loc[0]),
+                int(y0 - loc[1])
+            )
             cursor.setPos(j_pos[0], j_pos[1])
+            self.plotcanvas.mouse = [location[0], location[1]]
+            self.plotcanvas.draw_cursor(x_pos=location[0], y_pos=location[1])
 
-        if self.grid_status() == True:
+        if self.grid_status():
             # Update cursor
             self.app_cursor.set_data(np.asarray([(location[0], location[1])]),
                                      symbol='++', edge_color=self.cursor_color_3D,
@@ -7376,8 +7421,7 @@ class App(QtCore.QObject):
         self.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
                                            "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (dx, dy))
 
-        self.inform.emit('[success] %s' %
-                         _("Done."))
+        self.inform.emit('[success] %s' % _("Done."))
         return location
 
     def on_copy_object(self):
@@ -8431,23 +8475,17 @@ class App(QtCore.QObject):
             was clicked, the pixel coordinates and the axes coordinates.
         :return: None
         """
-        self.pos = []
+        self.pos = list()
 
         if self.is_legacy is False:
             event_pos = event.pos
-            if self.defaults["global_pan_button"] == '2':
-                pan_button = 2
-            else:
-                pan_button = 3
+            pan_button = 2 if self.defaults["global_pan_button"] == '2'else 3
             # Set the mouse button for panning
             self.plotcanvas.view.camera.pan_button_setting = pan_button
         else:
             event_pos = (event.xdata, event.ydata)
             # Matplotlib has the middle and right buttons mapped in reverse compared with VisPy
-            if self.defaults["global_pan_button"] == '2':
-                pan_button = 3
-            else:
-                pan_button = 2
+            pan_button = 3 if self.defaults["global_pan_button"] == '2'else 2
 
         # So it can receive key presses
         self.plotcanvas.native.setFocus()
@@ -8836,17 +8874,29 @@ class App(QtCore.QObject):
     def selected_message(self, curr_sel_obj):
         if curr_sel_obj:
             if curr_sel_obj.kind == 'gerber':
-                self.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                    color='green', name=str(curr_sel_obj.options['name'])))
+                self.inform.emit('[selected]<span style="color:{color};">{name}</span> {tx}'.format(
+                    color='green',
+                    name=str(curr_sel_obj.options['name']),
+                    tx=_("selected"))
+                )
             elif curr_sel_obj.kind == 'excellon':
-                self.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                    color='brown', name=str(curr_sel_obj.options['name'])))
+                self.inform.emit('[selected]<span style="color:{color};">{name}</span> {tx}'.format(
+                    color='brown',
+                    name=str(curr_sel_obj.options['name']),
+                    tx=_("selected"))
+                )
             elif curr_sel_obj.kind == 'cncjob':
-                self.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                    color='blue', name=str(curr_sel_obj.options['name'])))
+                self.inform.emit('[selected]<span style="color:{color};">{name}</span> {tx}'.format(
+                    color='blue',
+                    name=str(curr_sel_obj.options['name']),
+                    tx=_("selected"))
+                )
             elif curr_sel_obj.kind == 'geometry':
-                self.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                    color='red', name=str(curr_sel_obj.options['name'])))
+                self.inform.emit('[selected]<span style="color:{color};">{name}</span> {tx}'.format(
+                    color='red',
+                    name=str(curr_sel_obj.options['name']),
+                    tx=_("selected"))
+                )
 
     def delete_hover_shape(self):
         self.hover_shapes.clear()
@@ -9217,7 +9267,7 @@ class App(QtCore.QObject):
 
         # https://bobcadsupport.com/helpdesk/index.php?/Knowledgebase/Article/View/13/5/known-g-code-file-extensions
         _filter_ = "G-Code Files (*.txt *.nc *.ncc *.tap *.gcode *.cnc *.ecs *.fnc *.dnc *.ncg *.gc *.fan *.fgc" \
-                   " *.din *.xpi *.hnc *.h *.i *.ncp *.min *.gcd *.rol *.mpr *.ply *.out *.eia *.plt *.sbp *.mpf);;" \
+                   " *.din *.xpi *.hnc *.h *.i *.ncp *.min *.gcd *.rol *.mpr *.ply *.out *.eia *.sbp *.mpf);;" \
                    "All Files (*.*)"
 
         if name is None:
@@ -9277,6 +9327,44 @@ class App(QtCore.QObject):
             # thread safe. The new_project()
             self.open_project(filename)
 
+    def on_fileopenhpgl2(self, signal: bool = None, name=None):
+        """
+        File menu callback for opening a HPGL2.
+
+        :param signal: required because clicking the entry will generate a checked signal which needs a container
+        :return: None
+        """
+
+        self.report_usage("on_fileopenhpgl2")
+        App.log.debug("on_fileopenhpgl2()")
+
+        _filter_ = "HPGL2 Files (*.plt);;" \
+                   "All Files (*.*)"
+
+        if name is None:
+            try:
+                filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open HPGL2"),
+                                                                       directory=self.get_last_folder(),
+                                                                       filter=_filter_)
+            except TypeError:
+                filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open HPGL2"), filter=_filter_)
+
+            filenames = [str(filename) for filename in filenames]
+        else:
+            filenames = [name]
+            self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
+                                                         "Canvas initialization finished in"), '%.2f' % self.used_time,
+                                                       _("Opening HPGL2 file.")),
+                                    alignment=Qt.AlignBottom | Qt.AlignLeft,
+                                    color=QtGui.QColor("gray"))
+
+        if len(filenames) == 0:
+            self.inform.emit('[WARNING_NOTCL] %s' % _("Open HPGL2 file cancelled."))
+        else:
+            for filename in filenames:
+                if filename != '':
+                    self.worker_task.emit({'fcn': self.open_hpgl2, 'params': [filename]})
+
     def on_file_openconfig(self, signal: bool = None):
         """
         File menu callback for opening a config file.
@@ -10108,23 +10196,25 @@ class App(QtCore.QObject):
         try:
             filename, _f = QtWidgets.QFileDialog.getSaveFileName(
                 caption=_("Save Project As ..."),
-                directory=_('{l_save}/Project_{date}').format(l_save=str(self.get_last_save_folder()), date=self.date),
-                filter=filter_)
+                directory=('{l_save}/{proj}_{date}').format(l_save=str(self.get_last_save_folder()), date=self.date,
+                                                             proj=_("Project")),
+                filter=filter_
+            )
         except TypeError:
             filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Save Project As ..."), filter=filter_)
 
         filename = str(filename)
 
         if filename == '':
-            self.inform.emit('[WARNING_NOTCL] %s' %
-                             _("Save Project cancelled."))
+            self.inform.emit('[WARNING_NOTCL] %s' % _("Save Project cancelled."))
             return
 
         try:
             f = open(filename, 'r')
             f.close()
         except IOError:
-            pass
+            self.inform.emit('[ERROR_NOTCL] %s' % _("The object is used by another application."))
+            return
 
         if use_thread is True:
             self.worker_task.emit({'fcn': self.save_project,
@@ -10143,6 +10233,50 @@ class App(QtCore.QObject):
         self.set_ui_title(name=self.project_filename)
         self.should_we_save = False
 
+    def on_file_save_object_pdf(self, use_thread=True):
+        self.date = str(datetime.today()).rpartition('.')[0]
+        self.date = ''.join(c for c in self.date if c not in ':-')
+        self.date = self.date.replace(' ', '_')
+
+        try:
+            obj_active = self.collection.get_active()
+            obj_name = _(str(obj_active.options['name']))
+        except AttributeError as err:
+            log.debug("App.on_file_save_object_pdf() --> %s" % str(err))
+            self.inform.emit('[ERROR_NOTCL] %s' % _("No object selected."))
+            return
+
+        filter_ = "PDF File (*.PDF);; All Files (*.*)"
+        try:
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(
+                caption=_("Save Object as PDF ..."),
+                directory=('{l_save}/{obj_name}_{date}').format(l_save=str(self.get_last_save_folder()),
+                                                                 obj_name=obj_name,
+                                                                 date=self.date),
+                filter=filter_
+            )
+        except TypeError:
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Save Object as PDF ..."), filter=filter_)
+
+        filename = str(filename)
+
+        if filename == '':
+            self.inform.emit('[WARNING_NOTCL] %s' % _("Save Object PDF cancelled."))
+            return
+
+        if use_thread is True:
+            self.worker_task.emit({'fcn': self.save_pdf, 'params': [filename, obj_name]})
+        else:
+            self.save_pdf(filename, obj_name)
+
+        # self.save_project(filename)
+        if self.defaults["global_open_style"] is False:
+            self.file_opened.emit("pdf", filename)
+        self.file_saved.emit("pdf", filename)
+
+    def save_pdf(self, file_name, obj_name):
+        self.film_tool.export_positive(obj_name=obj_name, box_name=obj_name, filename=file_name, ftype='pdf')
+
     def export_svg(self, obj_name, filename, scale_stroke_factor=0.00):
         """
         Exports a Geometry Object to an SVG file.
@@ -10870,6 +11004,73 @@ class App(QtCore.QObject):
             self.inform.emit('[success] %s: %s' %
                              (_("Opened"), filename))
 
+    def open_hpgl2(self, filename, outname=None):
+        """
+        Opens a HPGL2 file, parses it and creates a new object for
+        it in the program. Thread-safe.
+
+        :param outname: Name of the resulting object. None causes the
+            name to be that of the file.
+        :param filename: HPGL2 file filename
+        :type filename: str
+        :return: None
+        """
+        filename = filename
+
+        # How the object should be initialized
+        def obj_init(geo_obj, app_obj):
+
+            assert isinstance(geo_obj, FlatCAMGeometry), \
+                "Expected to initialize a FlatCAMGeometry but got %s" % type(geo_obj)
+
+            # Opening the file happens here
+            obj = HPGL2(self)
+            try:
+                HPGL2.parse_file(obj, filename)
+            except IOError:
+                app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open file"), filename))
+                return "fail"
+            except ParseError as err:
+                app_obj.inform.emit('[ERROR_NOTCL] %s: %s. %s' % (_("Failed to parse file"), filename, str(err)))
+                app_obj.log.error(str(err))
+                return "fail"
+            except Exception as e:
+                log.debug("App.open_hpgl2() --> %s" % str(e))
+                msg = '[ERROR] %s' % _("An internal error has occurred. See shell.\n")
+                msg += traceback.format_exc()
+                app_obj.inform.emit(msg)
+                return "fail"
+
+            geo_obj.multigeo = True
+            geo_obj.solid_geometry = deepcopy(obj.solid_geometry)
+            geo_obj.tools = deepcopy(obj.tools)
+            geo_obj.source_file = deepcopy(obj.source_file)
+
+            del obj
+
+            if not geo_obj.solid_geometry:
+                app_obj.inform.emit('[ERROR_NOTCL] %s' %
+                                    _("Object is not HPGL2 file or empty. Aborting object creation."))
+                return "fail"
+
+        App.log.debug("open_hpgl2()")
+
+        with self.proc_container.new(_("Opening HPGL2")) as proc:
+            # Object name
+            name = outname or filename.split('/')[-1].split('\\')[-1]
+
+            # # ## Object creation # ##
+            ret = self.new_object("geometry", name, obj_init, autoselected=False)
+            if ret == 'fail':
+                self.inform.emit('[ERROR_NOTCL]%s' %  _(' Open HPGL2 failed. Probable not a HPGL2 file.'))
+                return 'fail'
+
+            # Register recent file
+            self.file_opened.emit("geometry", filename)
+
+            # GUI feedback
+            self.inform.emit('[success] %s: %s' % (_("Opened"), filename))
+
     def open_script(self, filename, outname=None, silent=False):
         """
         Opens a Script file, parses it and creates a new object for

+ 44 - 15
FlatCAMCommon.py

@@ -12,7 +12,7 @@
 # ##########################################################
 
 from PyQt5 import QtGui, QtCore, QtWidgets
-from flatcamGUI.GUIElements import FCTable, FCEntry, FCButton, FCDoubleSpinner, FCComboBox, FCCheckBox
+from flatcamGUI.GUIElements import FCTable, FCEntry, FCButton, FCDoubleSpinner, FCComboBox, FCCheckBox, FCSpinner
 from camlib import to_dict
 
 import sys
@@ -505,7 +505,7 @@ class ToolsDB(QtWidgets.QWidget):
         self.table_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
         table_hlay.addWidget(self.table_widget)
 
-        self.table_widget.setColumnCount(26)
+        self.table_widget.setColumnCount(27)
         # self.table_widget.setColumnWidth(0, 20)
         self.table_widget.setHorizontalHeaderLabels(
             [
@@ -530,6 +530,7 @@ class ToolsDB(QtWidgets.QWidget):
                 _("Dwelltime"),
                 _("Preprocessor"),
                 _("ExtraCut"),
+                _("E-Cut Length"),
                 _("Toolchange"),
                 _("Toolchange XY"),
                 _("Toolchange Z"),
@@ -620,23 +621,30 @@ class ToolsDB(QtWidgets.QWidget):
               "such as that this point is covered by this extra cut to\n"
               "ensure a complete isolation."))
         self.table_widget.horizontalHeaderItem(21).setToolTip(
+            _("Extra Cut length.\n"
+              "If checked, after a isolation is finished an extra cut\n"
+              "will be added where the start and end of isolation meet\n"
+              "such as that this point is covered by this extra cut to\n"
+              "ensure a complete isolation. This is the length of\n"
+              "the extra cut."))
+        self.table_widget.horizontalHeaderItem(22).setToolTip(
             _("Toolchange.\n"
               "It will create a toolchange event.\n"
               "The kind of toolchange is determined by\n"
               "the preprocessor file."))
-        self.table_widget.horizontalHeaderItem(22).setToolTip(
+        self.table_widget.horizontalHeaderItem(23).setToolTip(
             _("Toolchange XY.\n"
               "A set of coordinates in the format (x, y).\n"
               "Will determine the cartesian position of the point\n"
               "where the tool change event take place."))
-        self.table_widget.horizontalHeaderItem(23).setToolTip(
+        self.table_widget.horizontalHeaderItem(24).setToolTip(
             _("Toolchange Z.\n"
               "The position on Z plane where the tool change event take place."))
-        self.table_widget.horizontalHeaderItem(24).setToolTip(
+        self.table_widget.horizontalHeaderItem(25).setToolTip(
             _("Start Z.\n"
               "If it's left empty it will not be used.\n"
               "A position on Z plane to move immediately after job start."))
-        self.table_widget.horizontalHeaderItem(25).setToolTip(
+        self.table_widget.horizontalHeaderItem(26).setToolTip(
             _("End Z.\n"
               "A position on Z plane to move immediately after job stop."))
 
@@ -840,6 +848,16 @@ class ToolsDB(QtWidgets.QWidget):
         multidepth_item.set_value(data['multidepth'])
         widget.setCellWidget(row, 8, multidepth_item)
 
+        # to make the checkbox centered but it can no longer have it's value accessed - needs a fix using findchild()
+        # multidepth_item = QtWidgets.QWidget()
+        # cb = FCCheckBox()
+        # cb.set_value(data['multidepth'])
+        # qhboxlayout = QtWidgets.QHBoxLayout(multidepth_item)
+        # qhboxlayout.addWidget(cb)
+        # qhboxlayout.setAlignment(QtCore.Qt.AlignCenter)
+        # qhboxlayout.setContentsMargins(0, 0, 0, 0)
+        # widget.setCellWidget(row, 8, multidepth_item)
+
         depth_per_pass_item = FCDoubleSpinner()
         depth_per_pass_item.set_precision(self.decimals)
         depth_per_pass_item.setSingleStep(0.1)
@@ -890,8 +908,11 @@ class ToolsDB(QtWidgets.QWidget):
         frrapids_item.set_value(float(data['feedrate_rapid']))
         widget.setCellWidget(row, 15, frrapids_item)
 
-        spindlespeed_item = QtWidgets.QTableWidgetItem(str(data['spindlespeed']) if data['spindlespeed'] else '')
-        widget.setItem(row, 16, spindlespeed_item)
+        spindlespeed_item = FCSpinner()
+        spindlespeed_item.set_range(0, 1000000)
+        spindlespeed_item.set_value(int(data['spindlespeed']))
+        spindlespeed_item.setSingleStep(100)
+        widget.setCellWidget(row, 16, spindlespeed_item)
 
         dwell_item = FCCheckBox()
         dwell_item.set_value(data['dwell'])
@@ -913,12 +934,18 @@ class ToolsDB(QtWidgets.QWidget):
         ecut_item.set_value(data['extracut'])
         widget.setCellWidget(row, 20, ecut_item)
 
+        ecut_length_item = FCDoubleSpinner()
+        ecut_length_item.set_precision(self.decimals)
+        ecut_length_item.set_range(0.0, 9999.9999)
+        ecut_length_item.set_value(data['extracut_length'])
+        widget.setCellWidget(row, 21, ecut_length_item)
+
         toolchange_item = FCCheckBox()
         toolchange_item.set_value(data['toolchange'])
-        widget.setCellWidget(row, 21, toolchange_item)
+        widget.setCellWidget(row, 22, toolchange_item)
 
         toolchangexy_item = QtWidgets.QTableWidgetItem(str(data['toolchangexy']) if data['toolchangexy'] else '')
-        widget.setItem(row, 22, toolchangexy_item)
+        widget.setItem(row, 23, toolchangexy_item)
 
         toolchangez_item = FCDoubleSpinner()
         toolchangez_item.set_precision(self.decimals)
@@ -929,10 +956,10 @@ class ToolsDB(QtWidgets.QWidget):
             toolchangez_item.set_range(0.0000, 9999.9999)
 
         toolchangez_item.set_value(float(data['toolchangez']))
-        widget.setCellWidget(row, 23, toolchangez_item)
+        widget.setCellWidget(row, 24, toolchangez_item)
 
         startz_item = QtWidgets.QTableWidgetItem(str(data['startz']) if data['startz'] else '')
-        widget.setItem(row, 24, startz_item)
+        widget.setItem(row, 25, startz_item)
 
         endz_item = FCDoubleSpinner()
         endz_item.set_precision(self.decimals)
@@ -943,7 +970,7 @@ class ToolsDB(QtWidgets.QWidget):
             endz_item.set_range(0.0000, 9999.9999)
 
         endz_item.set_value(float(data['endz']))
-        widget.setCellWidget(row, 25, endz_item)
+        widget.setCellWidget(row, 26, endz_item)
 
     def on_tool_add(self):
         """
@@ -970,6 +997,7 @@ class ToolsDB(QtWidgets.QWidget):
             "dwelltime": float(self.app.defaults["geometry_dwelltime"]),
             "ppname_g": self.app.defaults["geometry_ppname_g"],
             "extracut": self.app.defaults["geometry_extracut"],
+            "extracut_length": self.app.defaults["geometry_extracut_length"],
             "toolchange": self.app.defaults["geometry_toolchange"],
             "toolchangexy": self.app.defaults["geometry_toolchangexy"],
             "toolchangez": float(self.app.defaults["geometry_toolchangez"]),
@@ -1257,8 +1285,7 @@ class ToolsDB(QtWidgets.QWidget):
                     elif column_header_text == 'FR Rapids':
                         default_data['feedrate_rapid'] = self.table_widget.cellWidget(row, col).get_value()
                     elif column_header_text == 'Spindle Speed':
-                        default_data['spindlespeed'] = float(self.table_widget.item(row, col).text()) \
-                            if self.table_widget.item(row, col).text() is not '' else None
+                        default_data['spindlespeed'] = self.table_widget.cellWidget(row, col).get_value()
                     elif column_header_text == 'Dwell':
                         default_data['dwell'] = self.table_widget.cellWidget(row, col).get_value()
                     elif column_header_text == 'Dwelltime':
@@ -1267,6 +1294,8 @@ class ToolsDB(QtWidgets.QWidget):
                         default_data['ppname_g'] = self.table_widget.cellWidget(row, col).get_value()
                     elif column_header_text == 'ExtraCut':
                         default_data['extracut'] = self.table_widget.cellWidget(row, col).get_value()
+                    elif column_header_text == "E-Cut Length":
+                        default_data['extracut_length'] = self.table_widget.cellWidget(row, col).get_value()
                     elif column_header_text == 'Toolchange':
                         default_data['toolchange'] = self.table_widget.cellWidget(row, col).get_value()
                     elif column_header_text == 'Toolchange XY':

+ 269 - 61
FlatCAMObj.py

@@ -195,10 +195,11 @@ class FlatCAMObj(QtCore.QObject):
             pass
 
         # Creates problems on focusOut
-        # try:
-        #     self.ui.scale_entry.returnPressed.connect(self.on_scale_button_click)
-        # except (TypeError, AttributeError):
-        #     pass
+        try:
+            self.ui.scale_entry.returnPressed.connect(self.on_scale_button_click)
+        except (TypeError, AttributeError):
+            pass
+
         # self.ui.skew_button.clicked.connect(self.on_skew_button_click)
 
     def build_ui(self):
@@ -267,9 +268,19 @@ class FlatCAMObj(QtCore.QObject):
 
     def on_scale_button_click(self):
         self.read_form()
-        factor = self.ui.scale_entry.get_value()
+        try:
+            factor = float(eval(self.ui.scale_entry.get_value()))
+        except Exception as e:
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Scaling could not be executed."))
+            log.debug("FlatCAMObj.on_scale_button_click() -- %s" % str(e))
+            return
+
+        if type(factor) != float:
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Scaling could not be executed."))
+
         # if factor is 1.0 do nothing, there is no point in scaling with a factor of 1.0
         if factor == 1.0:
+            self.app.inform.emit('[success] %s' % _("Scale done."))
             return
 
         log.debug("FlatCAMObj.on_scale_button_click()")
@@ -277,6 +288,8 @@ class FlatCAMObj(QtCore.QObject):
         def worker_task():
             with self.app.proc_container.new(_("Scaling...")):
                 self.scale(factor)
+                self.app.inform.emit('[success] %s' % _("Scale done."))
+
             self.app.proc_container.update_view_text('')
             with self.app.proc_container.new('%s...' % _("Plotting")):
                 self.plot()
@@ -625,6 +638,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
         # Mouse events
         self.mr = None
+        self.mm = None
+        self.mp = None
 
         # dict to store the polygons selected for isolation; key is the shape added to be plotted and value is the poly
         self.poly_dict = dict()
@@ -1066,11 +1081,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         if self.app.is_legacy is False:
             event_pos = event.pos
             right_button = 2
-            event_is_dragging = self.app.event_is_dragging
+            self.app.event_is_dragging = self.app.event_is_dragging
         else:
             event_pos = (event.xdata, event.ydata)
             right_button = 3
-            event_is_dragging = self.app.ui.popMenu.mouse_is_panning
+            self.app.event_is_dragging = self.app.ui.popMenu.mouse_is_panning
 
         try:
             x = float(event_pos[0])
@@ -1080,11 +1095,18 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
         event_pos = (x, y)
         curr_pos = self.app.plotcanvas.translate_coords(event_pos)
+        if self.app.grid_status():
+            curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
+        else:
+            curr_pos = (curr_pos[0], curr_pos[1])
 
         if event.button == 1:
             clicked_poly = self.find_polygon(point=(curr_pos[0], curr_pos[1]))
 
-            if clicked_poly:
+            if self.app.selection_type is not None:
+                self.selection_area_handler(self.app.pos, curr_pos, self.app.selection_type)
+                self.app.selection_type = None
+            elif clicked_poly:
                 if clicked_poly not in self.poly_dict.values():
                     shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0, shape=clicked_poly,
                                                         color=self.app.defaults['global_sel_draw_color'] + 'AF',
@@ -1113,8 +1135,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                 self.app.tool_shapes.redraw()
             else:
                 self.app.inform.emit(_("No polygon detected under click position."))
-
-        elif event.button == right_button and event_is_dragging is False:
+        elif event.button == right_button and self.app.event_is_dragging is False:
             # restore the Grid snapping if it was active before
             if self.grid_status_memory is True:
                 self.app.ui.grid_snap_btn.trigger()
@@ -1136,6 +1157,75 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             else:
                 self.app.inform.emit('[ERROR_NOTCL] %s' % _("List of single polygons is empty. Aborting."))
 
+    def selection_area_handler(self, start_pos, end_pos, sel_type):
+        """
+        :param start_pos: mouse position when the selection LMB click was done
+        :param end_pos: mouse position when the left mouse button is released
+        :param sel_type: if True it's a left to right selection (enclosure), if False it's a 'touch' selection
+        :return:
+        """
+        poly_selection = Polygon([start_pos, (end_pos[0], start_pos[1]), end_pos, (start_pos[0], end_pos[1])])
+
+        # delete previous selection shape
+        self.app.delete_selection_shape()
+
+        added_poly_count = 0
+        try:
+            for geo in self.solid_geometry:
+                if geo not in self.poly_dict.values():
+                    if sel_type is True:
+                        if geo.within(poly_selection):
+                            shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0,
+                                                                shape=geo,
+                                                                color=self.app.defaults['global_sel_draw_color'] + 'AF',
+                                                                face_color=self.app.defaults[
+                                                                               'global_sel_draw_color'] + 'AF',
+                                                                visible=True)
+                            self.poly_dict[shape_id] = geo
+                            added_poly_count += 1
+                    else:
+                        if poly_selection.intersects(geo):
+                            shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0,
+                                                                shape=geo,
+                                                                color=self.app.defaults['global_sel_draw_color'] + 'AF',
+                                                                face_color=self.app.defaults[
+                                                                               'global_sel_draw_color'] + 'AF',
+                                                                visible=True)
+                            self.poly_dict[shape_id] = geo
+                            added_poly_count += 1
+        except TypeError:
+            if self.solid_geometry not in self.poly_dict.values():
+                if sel_type is True:
+                    if self.solid_geometry.within(poly_selection):
+                        shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0,
+                                                            shape=self.solid_geometry,
+                                                            color=self.app.defaults['global_sel_draw_color'] + 'AF',
+                                                            face_color=self.app.defaults[
+                                                                           'global_sel_draw_color'] + 'AF',
+                                                            visible=True)
+                        self.poly_dict[shape_id] = self.solid_geometry
+                        added_poly_count += 1
+                else:
+                    if poly_selection.intersects(self.solid_geometry):
+                        shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0,
+                                                            shape=self.solid_geometry,
+                                                            color=self.app.defaults['global_sel_draw_color'] + 'AF',
+                                                            face_color=self.app.defaults[
+                                                                           'global_sel_draw_color'] + 'AF',
+                                                            visible=True)
+                        self.poly_dict[shape_id] = self.solid_geometry
+                        added_poly_count += 1
+
+        if added_poly_count > 0:
+            self.app.tool_shapes.redraw()
+            self.app.inform.emit(
+                '%s: %d. %s' % (_("Added polygon"),
+                                int(added_poly_count),
+                                _("Click to add next polygon or right click to start isolation."))
+            )
+        else:
+            self.app.inform.emit(_("No polygon in selection."))
+
     def isolate(self, iso_type=None, geometry=None, dia=None, passes=None, overlap=None, outname=None, combine=None,
                 milling_type=None, follow=None, plot=True):
         """
@@ -1242,6 +1332,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                         "ppname_g": self.app.defaults['geometry_ppname_g'],
                         "depthperpass": self.app.defaults['geometry_depthperpass'],
                         "extracut": self.app.defaults['geometry_extracut'],
+                        "extracut_length": self.app.defaults['geometry_extracut_length'],
                         "toolchange": self.app.defaults['geometry_toolchange'],
                         "toolchangez": self.app.defaults['geometry_toolchangez'],
                         "endz": self.app.defaults['geometry_endz'],
@@ -1718,18 +1809,25 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         :param aperture: string; aperture for which to clear the mark shapes
         :return:
         """
-        try:
+
+        if self.mark_shapes:
             if aperture == 'all':
                 for apid in list(self.apertures.keys()):
+                    try:
+                        if self.app.is_legacy is True:
+                            self.mark_shapes[apid].clear(update=False)
+                        else:
+                            self.mark_shapes[apid].clear(update=True)
+                    except Exception as e:
+                        log.debug("FlatCAMGerber.clear_plot_apertures() 'all' --> %s" % str(e))
+            else:
+                try:
                     if self.app.is_legacy is True:
-                        self.mark_shapes[apid].clear(update=False)
+                        self.mark_shapes[aperture].clear(update=False)
                     else:
-                        self.mark_shapes[apid].clear(update=True)
-
-            else:
-                self.mark_shapes[aperture].clear(update=True)
-        except Exception as e:
-            log.debug("FlatCAMGerber.clear_plot_apertures() --> %s" % str(e))
+                        self.mark_shapes[aperture].clear(update=True)
+                except Exception as e:
+                    log.debug("FlatCAMGerber.clear_plot_apertures() 'aperture' --> %s" % str(e))
 
     def clear_mark_all(self):
         self.ui.mark_all_cb.set_value(False)
@@ -2117,7 +2215,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
             "toolchangexy": "0.0, 0.0",
             "endz": 2.0,
             "startz": None,
-            "spindlespeed": None,
+            "spindlespeed": 0,
             "dwell": True,
             "dwelltime": 1000,
             "ppname_e": 'defaults',
@@ -2128,10 +2226,10 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         })
 
         # TODO: Document this.
-        self.tool_cbs = {}
+        self.tool_cbs = dict()
 
         # dict to hold the tool number as key and tool offset as value
-        self.tool_offset = {}
+        self.tool_offset = dict()
 
         # variable to store the total amount of drills per job
         self.tot_drill_cnt = 0
@@ -3175,7 +3273,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
             job_obj.feedrate = float(self.options["feedrate"])
             job_obj.feedrate_rapid = float(self.options["feedrate_rapid"])
 
-            job_obj.spindlespeed = float(self.options["spindlespeed"]) if self.options["spindlespeed"] else None
+            job_obj.spindlespeed = float(self.options["spindlespeed"]) if self.options["spindlespeed"] != 0 else None
             job_obj.spindledir = self.app.defaults['excellon_spindledir']
             job_obj.dwell = self.options["dwell"]
             job_obj.dwelltime = float(self.options["dwelltime"])
@@ -3254,30 +3352,31 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
     def convert_units(self, units):
         log.debug("FlatCAMObj.FlatCAMExcellon.convert_units()")
 
-        factor = Excellon.convert_units(self, units)
-
-        self.options['drillz'] = float(self.options['drillz']) * factor
-        self.options['travelz'] = float(self.options['travelz']) * factor
-        self.options['feedrate'] = float(self.options['feedrate']) * factor
-        self.options['feedrate_rapid'] = float(self.options['feedrate_rapid']) * factor
-        self.options['toolchangez'] = float(self.options['toolchangez']) * factor
-
-        if self.app.defaults["excellon_toolchangexy"] == '':
-            self.options['toolchangexy'] = "0.0, 0.0"
-        else:
-            coords_xy = [float(eval(coord)) for coord in self.app.defaults["excellon_toolchangexy"].split(",")]
-            if len(coords_xy) < 2:
-                self.app.inform.emit('[ERROR] %s' % _("The Toolchange X,Y field in Edit -> Preferences has to be "
-                                                      "in the format (x, y) \n"
-                                                      "but now there is only one value, not two. "))
-                return 'fail'
-            coords_xy[0] *= factor
-            coords_xy[1] *= factor
-            self.options['toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1])
+        Excellon.convert_units(self, units)
 
-        if self.options['startz'] is not None:
-            self.options['startz'] = float(self.options['startz']) * factor
-        self.options['endz'] = float(self.options['endz']) * factor
+        # factor = Excellon.convert_units(self, units)
+        # self.options['drillz'] = float(self.options['drillz']) * factor
+        # self.options['travelz'] = float(self.options['travelz']) * factor
+        # self.options['feedrate'] = float(self.options['feedrate']) * factor
+        # self.options['feedrate_rapid'] = float(self.options['feedrate_rapid']) * factor
+        # self.options['toolchangez'] = float(self.options['toolchangez']) * factor
+        #
+        # if self.app.defaults["excellon_toolchangexy"] == '':
+        #     self.options['toolchangexy'] = "0.0, 0.0"
+        # else:
+        #     coords_xy = [float(eval(coord)) for coord in self.app.defaults["excellon_toolchangexy"].split(",")]
+        #     if len(coords_xy) < 2:
+        #         self.app.inform.emit('[ERROR] %s' % _("The Toolchange X,Y field in Edit -> Preferences has to be "
+        #                                               "in the format (x, y) \n"
+        #                                               "but now there is only one value, not two. "))
+        #         return 'fail'
+        #     coords_xy[0] *= factor
+        #     coords_xy[1] *= factor
+        #     self.options['toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1])
+        #
+        # if self.options['startz'] is not None:
+        #     self.options['startz'] = float(self.options['startz']) * factor
+        # self.options['endz'] = float(self.options['endz']) * factor
 
     def on_solid_cb_click(self, *args):
         if self.muted_ui:
@@ -3439,12 +3538,13 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             "feedrate": 5.0,
             "feedrate_z": 5.0,
             "feedrate_rapid": 5.0,
-            "spindlespeed": None,
+            "spindlespeed": 0,
             "dwell": True,
             "dwelltime": 1000,
             "multidepth": False,
             "depthperpass": 0.002,
             "extracut": False,
+            "extracut_length": 0.1,
             "endz": 2.0,
             "startz": None,
             "toolchange": False,
@@ -3684,6 +3784,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             "feedrate_probe": self.ui.feedrate_probe_entry,
             "depthperpass": self.ui.maxdepth_entry,
             "extracut": self.ui.extracut_cb,
+            "extracut_length": self.ui.e_cut_entry,
             "toolchange": self.ui.toolchangeg_cb,
             "toolchangez": self.ui.toolchangez_entry,
             "endz": self.ui.gendz_entry,
@@ -3722,10 +3823,11 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             "ppname_g": None,
             "depthperpass": None,
             "extracut": None,
+            "extracut_length": None,
             "toolchange": None,
             "toolchangez": None,
             "endz": None,
-            "spindlespeed": None,
+            "spindlespeed": 0,
             "toolchangexy": None,
             "startz": None
         })
@@ -3814,6 +3916,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             self.ui.fr_rapidlabel.hide()
             self.ui.cncfeedrate_rapid_entry.hide()
             self.ui.extracut_cb.hide()
+            self.ui.e_cut_entry.hide()
             self.ui.pdepth_label.hide()
             self.ui.pdepth_entry.hide()
             self.ui.feedrate_probe_label.hide()
@@ -3821,9 +3924,12 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         else:
             self.ui.level.setText('<span style="color:red;"><b>%s</b></span>' % _('Advanced'))
 
+        self.ui.e_cut_entry.setDisabled(True)
+
         self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
         self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click)
         self.ui.paint_tool_button.clicked.connect(lambda: self.app.paint_tool.run(toggle=False))
+        self.ui.generate_ncc_button.clicked.connect(lambda: self.app.ncclear_tool.run(toggle=False))
         self.ui.pp_geometry_name_cb.activated.connect(self.on_pp_changed)
         self.ui.addtool_entry.returnPressed.connect(lambda: self.on_tool_add())
 
@@ -4975,6 +5081,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 feedrate_rapid = tools_dict[tooluid_key]['data']["feedrate_rapid"]
                 multidepth = tools_dict[tooluid_key]['data']["multidepth"]
                 extracut = tools_dict[tooluid_key]['data']["extracut"]
+                extracut_length = tools_dict[tooluid_key]['data']["extracut_length"]
                 depthpercut = tools_dict[tooluid_key]['data']["depthperpass"]
                 toolchange = tools_dict[tooluid_key]['data']["toolchange"]
                 toolchangez = tools_dict[tooluid_key]['data']["toolchangez"]
@@ -5006,7 +5113,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                     feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid,
                     spindlespeed=spindlespeed, spindledir=spindledir, dwell=dwell, dwelltime=dwelltime,
                     multidepth=multidepth, depthpercut=depthpercut,
-                    extracut=extracut, startz=startz, endz=endz,
+                    extracut=extracut, extracut_length=extracut_length, startz=startz, endz=endz,
                     toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy,
                     pp_geometry_name=pp_geometry_name,
                     tool_no=tool_cnt)
@@ -5127,6 +5234,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 feedrate_rapid = tools_dict[tooluid_key]['data']["feedrate_rapid"]
                 multidepth = tools_dict[tooluid_key]['data']["multidepth"]
                 extracut = tools_dict[tooluid_key]['data']["extracut"]
+                extracut_length = tools_dict[tooluid_key]['data']["extracut_length"]
                 depthpercut = tools_dict[tooluid_key]['data']["depthperpass"]
                 toolchange = tools_dict[tooluid_key]['data']["toolchange"]
                 toolchangez = tools_dict[tooluid_key]['data']["toolchangez"]
@@ -5158,7 +5266,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                     feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid,
                     spindlespeed=spindlespeed, spindledir=spindledir, dwell=dwell, dwelltime=dwelltime,
                     multidepth=multidepth, depthpercut=depthpercut,
-                    extracut=extracut, startz=startz, endz=endz,
+                    extracut=extracut, extracut_length=extracut_length, startz=startz, endz=endz,
                     toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy,
                     pp_geometry_name=pp_geometry_name,
                     tool_no=tool_cnt)
@@ -5226,7 +5334,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             spindlespeed=None, dwell=None, dwelltime=None,
             multidepth=None, depthperpass=None,
             toolchange=None, toolchangez=None, toolchangexy=None,
-            extracut=None, startz=None, endz=None,
+            extracut=None, extracut_length=None, startz=None, endz=None,
             pp=None,
             segx=None, segy=None,
             use_thread=True,
@@ -5266,6 +5374,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         segy = segy if segy is not None else float(self.app.defaults['geometry_segy'])
 
         extracut = extracut if extracut is not None else float(self.options["extracut"])
+        extracut_length = extracut_length if extracut_length is not None else float(self.options["extracut_length"])
+
         startz = startz if startz is not None else self.options["startz"]
         endz = endz if endz is not None else float(self.options["endz"])
 
@@ -5320,7 +5430,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 spindlespeed=spindlespeed, dwell=dwell, dwelltime=dwelltime,
                 multidepth=multidepth, depthpercut=depthperpass,
                 toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy,
-                extracut=extracut, startz=startz, endz=endz,
+                extracut=extracut, extracut_length=extracut_length, startz=startz, endz=endz,
                 pp_geometry_name=ppname_g
             )
 
@@ -5625,7 +5735,6 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         tooldia = self.ui.addtool_entry.get_value()
         if tooldia:
             tooldia *= factor
-            # limit the decimals to 2 for METRIC and 3 for INCH
             tooldia = float('%.*f' % (self.decimals, tooldia))
 
             self.ui.addtool_entry.set_value(tooldia)
@@ -5635,7 +5744,6 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
     def plot_element(self, element, color='#FF0000FF', visible=None):
 
         visible = visible if visible else self.options['plot']
-
         try:
             for sub_el in element:
                 self.plot_element(sub_el)
@@ -5951,15 +6059,22 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         self.ui_disconnect()
 
         FlatCAMObj.build_ui(self)
+        self.units = self.app.defaults['units'].upper()
 
         # if the FlatCAM object is Excellon don't build the CNC Tools Table but hide it
+        self.ui.cnc_tools_table.hide()
         if self.cnc_tools:
             self.ui.cnc_tools_table.show()
-        else:
-            self.ui.cnc_tools_table.hide()
+            self.build_cnc_tools_table()
 
-        self.units = self.app.defaults['units'].upper()
+        self.ui.exc_cnc_tools_table.hide()
+        if self.exc_cnc_tools:
+            self.ui.exc_cnc_tools_table.show()
+            self.build_excellon_cnc_tools()
+        #
+        self.ui_connect()
 
+    def build_cnc_tools_table(self):
         offset = 0
         tool_idx = 0
 
@@ -6058,7 +6173,90 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         self.ui.cnc_tools_table.setMinimumHeight(self.ui.cnc_tools_table.getHeight())
         self.ui.cnc_tools_table.setMaximumHeight(self.ui.cnc_tools_table.getHeight())
 
-        self.ui_connect()
+    def build_excellon_cnc_tools(self):
+        tool_idx = 0
+
+        n = len(self.exc_cnc_tools)
+        self.ui.exc_cnc_tools_table.setRowCount(n)
+
+        for tooldia_key, dia_value in self.exc_cnc_tools.items():
+
+            tool_idx += 1
+            row_no = tool_idx - 1
+
+            id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
+            dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(tooldia_key)))
+            nr_drills_item = QtWidgets.QTableWidgetItem('%d' % int(dia_value['nr_drills']))
+            nr_slots_item = QtWidgets.QTableWidgetItem('%d' % int(dia_value['nr_slots']))
+            cutz_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(dia_value['offset_z']) + self.z_cut))
+
+            id.setFlags(QtCore.Qt.ItemIsEnabled)
+            dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
+            nr_drills_item.setFlags(QtCore.Qt.ItemIsEnabled)
+            nr_slots_item.setFlags(QtCore.Qt.ItemIsEnabled)
+            cutz_item.setFlags(QtCore.Qt.ItemIsEnabled)
+
+            # hack so the checkbox stay centered in the table cell
+            # used this:
+            # https://stackoverflow.com/questions/32458111/pyqt-allign-checkbox-and-put-it-in-every-row
+            # plot_item = QtWidgets.QWidget()
+            # checkbox = FCCheckBox()
+            # checkbox.setCheckState(QtCore.Qt.Checked)
+            # qhboxlayout = QtWidgets.QHBoxLayout(plot_item)
+            # qhboxlayout.addWidget(checkbox)
+            # qhboxlayout.setAlignment(QtCore.Qt.AlignCenter)
+            # qhboxlayout.setContentsMargins(0, 0, 0, 0)
+
+            plot_item = FCCheckBox()
+            plot_item.setLayoutDirection(QtCore.Qt.RightToLeft)
+            tool_uid_item = QtWidgets.QTableWidgetItem(str(dia_value['tool']))
+            if self.ui.plot_cb.isChecked():
+                plot_item.setChecked(True)
+
+            # TODO until the feature of individual plot for an Excellon tool is implemented
+            plot_item.setDisabled(True)
+
+            self.ui.exc_cnc_tools_table.setItem(row_no, 0, id)  # Tool name/id
+            self.ui.exc_cnc_tools_table.setItem(row_no, 1, dia_item)  # Diameter
+            self.ui.exc_cnc_tools_table.setItem(row_no, 2, nr_drills_item)  # Nr of drills
+            self.ui.exc_cnc_tools_table.setItem(row_no, 3, nr_slots_item)  # Nr of slots
+
+            # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY # ##
+            self.ui.exc_cnc_tools_table.setItem(row_no, 4, tool_uid_item)  # Tool unique ID)
+            self.ui.exc_cnc_tools_table.setItem(row_no, 5, cutz_item)
+            self.ui.exc_cnc_tools_table.setCellWidget(row_no, 6, plot_item)
+
+        for row in range(tool_idx):
+            self.ui.exc_cnc_tools_table.item(row, 0).setFlags(
+                self.ui.exc_cnc_tools_table.item(row, 0).flags() ^ QtCore.Qt.ItemIsSelectable)
+
+        self.ui.exc_cnc_tools_table.resizeColumnsToContents()
+        self.ui.exc_cnc_tools_table.resizeRowsToContents()
+
+        vertical_header = self.ui.exc_cnc_tools_table.verticalHeader()
+        vertical_header.hide()
+        self.ui.exc_cnc_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header = self.ui.exc_cnc_tools_table.horizontalHeader()
+        horizontal_header.setMinimumSectionSize(10)
+        horizontal_header.setDefaultSectionSize(70)
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(0, 20)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+        horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(5, QtWidgets.QHeaderView.ResizeToContents)
+
+        horizontal_header.setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed)
+
+        # horizontal_header.setStretchLastSection(True)
+        self.ui.exc_cnc_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        self.ui.exc_cnc_tools_table.setColumnWidth(0, 20)
+        self.ui.exc_cnc_tools_table.setColumnWidth(6, 17)
+
+        self.ui.exc_cnc_tools_table.setMinimumHeight(self.ui.exc_cnc_tools_table.getHeight())
+        self.ui.exc_cnc_tools_table.setMaximumHeight(self.ui.exc_cnc_tools_table.getHeight())
 
     def set_ui(self, ui):
         FlatCAMObj.set_ui(self, ui)
@@ -6645,10 +6843,20 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
                 self.plot2(dia_plot, obj=self, visible=visible, kind=kind)
             else:
                 # multiple tools usage
-                for tooluid_key in self.cnc_tools:
-                    tooldia = float('%.*f' % (self.decimals, float(self.cnc_tools[tooluid_key]['tooldia'])))
-                    gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
-                    self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
+                if self.cnc_tools:
+                    for tooluid_key in self.cnc_tools:
+                        tooldia = float('%.*f' % (self.decimals, float(self.cnc_tools[tooluid_key]['tooldia'])))
+                        gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
+                        self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
+
+                # TODO: until the gcode parsed will be stored on each Excellon tool this will not get executed
+                if self.exc_cnc_tools:
+                    for tooldia_key in self.exc_cnc_tools:
+                        tooldia = float('%.*f' % (self.decimals, float(tooldia_key)))
+                        # gcode_parsed = self.cnc_tools[tooldia_key]['gcode_parsed']
+                        gcode_parsed = self.gcode_parsed
+                        self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
+
             self.shapes.redraw()
         except (ObjectDeleted, AttributeError):
             self.shapes.clear(update=True)

+ 30 - 12
ObjectCollection.py

@@ -785,23 +785,41 @@ class ObjectCollection(QtCore.QAbstractItemModel):
             self.item_selected.emit(obj.options['name'])
 
             if obj.kind == 'gerber':
-                self.app.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                    color='green', name=str(obj.options['name'])))
+                self.app.inform.emit('[selected]<span style="color:{color};">{name}</span> {tx}'.format(
+                    color='green',
+                    name=str(obj.options['name']),
+                    tx=_("selected"))
+                )
             elif obj.kind == 'excellon':
-                self.app.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                    color='brown', name=str(obj.options['name'])))
+                self.app.inform.emit('[selected]<span style="color:{color};">{name}</span> {tx}'.format(
+                    color='brown',
+                    name=str(obj.options['name']),
+                    tx=_("selected"))
+                )
             elif obj.kind == 'cncjob':
-                self.app.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                    color='blue', name=str(obj.options['name'])))
+                self.app.inform.emit('[selected]<span style="color:{color};">{name}</span> {tx}'.format(
+                    color='blue',
+                    name=str(obj.options['name']),
+                    tx=_("selected"))
+                )
             elif obj.kind == 'geometry':
-                self.app.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                    color='red', name=str(obj.options['name'])))
+                self.app.inform.emit('[selected]<span style="color:{color};">{name}</span> {tx}'.format(
+                    color='red',
+                    name=str(obj.options['name']),
+                    tx=_("selected"))
+                )
             elif obj.kind == 'script':
-                self.app.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                    color='orange', name=str(obj.options['name'])))
+                self.app.inform.emit('[selected]<span style="color:{color};">{name}</span> {tx}'.format(
+                    color='orange',
+                    name=str(obj.options['name']),
+                    tx=_("selected"))
+                )
             elif obj.kind == 'document':
-                self.app.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                    color='darkCyan', name=str(obj.options['name'])))
+                self.app.inform.emit('[selected]<span style="color:{color};">{name}</span> {tx}'.format(
+                    color='darkCyan',
+                    name=str(obj.options['name']),
+                    tx=_("selected"))
+                )
         except IndexError:
             self.item_selected.emit('none')
             # FlatCAMApp.App.log.debug("on_list_selection_change(): Index Error (Nothing selected?)")

+ 45 - 1
README.md

@@ -9,19 +9,63 @@ CAD program, and create G-Code for Isolation routing.
 
 =================================================
 
+14.12.2019
+
+- finished the strings update in the Google-translated Spanish 
+
+13.12.2019
+
+- HPGL2 import: added support for circles, arcs and 3-point arcs. Everything works only for absolute coordinates.
+- removed the .plt extension from Gcode extensions
+- some strings updated; update on the Romanian translate
+- more strings updated; finished the Romanian translation update
+- some work in updating the Spanish Google-translation
+- small updates (Google Translate) in Russian and Brazilian-PT languages
+
+12.12.2019
+
+- finished the Calibration Tool
+- changed the Scale Entry in Object UI to FCEntry() GUI element in order to allow expressions to be entered. E.g: 1/25.4
+- some small changes in the Scale button handler in FlatCAMObj() class
+- added option to save objects as PDF files in File -> Save menu
+- optimized the FlatCAMGerber.clear_plot_apertures() method
+- some changes in the ObjectUI and for the Geometry UI
+- finished a very rough and limited HPGL2 file import 
+
+11.12.2019
+
+- started work in HPGL2 parser
+- some more work in Calibration Tool
+
+10.12.2019
+
+- small changes in the Geometry UI
+- now extracut option in the Geometry Object will recut as many points as many they are within the specified re-cut length
+- if extracut_length is zero then the extracut will cut up until the first point in path no matter what the distance is
+- in Gerber isolation, when selection mode is checked, now area selection works too
+- in CNCJob UI, now the CNCJob objects made out of Excellon objects will display their CNC tools (drill bits)
+- fixed a cumulative error when using the Tool Offset for Excellon objects
+- added the display of the real depth of cut (cut z + offset_z) for CNC tools made out of an Excellon object
+- for OpenGL graphic mode added a fit_view() execution on canvas initialization
+- fixed Excellon scaling the UI values
+- replaced the SpindleSpeed entry with a FCSpinner() GUI element; if speed is set to 0 it will amount to None
+
 9.12.2019 
 
 - updated the border for fit view on OpenGL graphic mode
 - Calibration Tool - added preferences values
 - Calibration Tool - more work on it
 - reverted this change: "selected object in Project used to ask twice for UI build" because it will not build the UI when a tab is closed for Document object and the object is selected
-- fixed issue after Geometry object edit; the GCode made from and edited object did not reflect the changes in the object
+- fixed issue after Geometry object edit; the GCode made from an edited object did not reflect the changes in the object
 - in Object UI, the Scale FCDoubleSpinner will no longer work for Return key press due of issues of unwanted scaling on focusOut event
 - in FlatCAMGeometry fixed the scale and offset methods to always process the self.solid_geometry
 - Calibration Tool - finished the calibrated object creation method
 - updated the POT file
 - fixed an error in the German PO file
 - updated the languages PO files
+- some fixes on the app.jump_to() method
+- made sure that the ToolFilm will not start saving a file if there are no objects loaded
+- some fixes on the app.jump_to() method for the Legacy(2D) graphic mode
 
 8.12.2019
 

+ 71 - 34
camlib.py

@@ -459,7 +459,7 @@ class Geometry(object):
 
     defaults = {
         "units": 'in',
-        "geo_steps_per_circle": 128
+        "geo_steps_per_circle": 64
     }
 
     def __init__(self, geo_steps_per_circle=None):
@@ -1824,7 +1824,7 @@ class Geometry(object):
         """
 
         # Make sure we see a Shapely Geometry class and not a list
-        if str(type(self)) == "<class 'FlatCAMObj.FlatCAMGeometry'>":
+        if self.kind.lower() == 'geometry':
             flat_geo = []
             if self.multigeo:
                 for tool in self.tools:
@@ -2158,7 +2158,7 @@ class CNCjob(Geometry):
         self.units = units
 
         self.z_cut = z_cut
-        self.tool_offset = {}
+        self.tool_offset = dict()
 
         self.z_move = z_move
 
@@ -2359,7 +2359,9 @@ class CNCjob(Geometry):
         self.exc_drills = deepcopy(exobj.drills)
         self.exc_tools = deepcopy(exobj.tools)
 
-        self.z_cut = drillz
+        self.z_cut = deepcopy(drillz)
+        old_zcut = deepcopy(drillz)
+
         if self.machinist_setting == 0:
             if drillz > 0:
                 self.app.inform.emit('[WARNING] %s' %
@@ -2441,10 +2443,16 @@ class CNCjob(Geometry):
                                 LineString([start, stop]).buffer((it[1] / 2.0), resolution=self.geo_steps_per_circle)
                             )
 
+                    try:
+                        z_off = float(self.tool_offset[it[1]]) * (-1)
+                    except KeyError:
+                        z_off = 0
+
                     self.exc_cnc_tools[it[1]] = dict()
                     self.exc_cnc_tools[it[1]]['tool'] = it[0]
                     self.exc_cnc_tools[it[1]]['nr_drills'] = drill_no
                     self.exc_cnc_tools[it[1]]['nr_slots'] = slot_no
+                    self.exc_cnc_tools[it[1]]['offset_z'] = z_off
                     self.exc_cnc_tools[it[1]]['solid_geometry'] = deepcopy(sol_geo)
 
         self.app.inform.emit(_("Creating a list of points to drill..."))
@@ -2635,7 +2643,7 @@ class CNCjob(Geometry):
                                 z_offset = float(self.tool_offset[current_tooldia]) * (-1)
                             except KeyError:
                                 z_offset = 0
-                            self.z_cut += z_offset
+                            self.z_cut = z_offset + old_zcut
 
                             self.coordinates_type = self.app.defaults["cncjob_coords_type"]
                             if self.coordinates_type == "G90":
@@ -2682,11 +2690,11 @@ class CNCjob(Geometry):
                             else:
                                 self.app.inform.emit('[ERROR_NOTCL] %s...' % _('G91 coordinates not implemented'))
                                 return 'fail'
+                        self.z_cut = deepcopy(old_zcut)
                 else:
                     log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> "
                               "The loaded Excellon file has no drills ...")
-                    self.app.inform.emit('[ERROR_NOTCL] %s...' %
-                                         _('The loaded Excellon file has no drills'))
+                    self.app.inform.emit('[ERROR_NOTCL] %s...' % _('The loaded Excellon file has no drills'))
                     return 'fail'
 
                 log.debug("The total travel distance with OR-TOOLS Metaheuristics is: %s" % str(measured_distance))
@@ -2778,7 +2786,7 @@ class CNCjob(Geometry):
                                 z_offset = float(self.tool_offset[current_tooldia]) * (-1)
                             except KeyError:
                                 z_offset = 0
-                            self.z_cut += z_offset
+                            self.z_cut = z_offset + old_zcut
 
                             self.coordinates_type = self.app.defaults["cncjob_coords_type"]
                             if self.coordinates_type == "G90":
@@ -2825,6 +2833,7 @@ class CNCjob(Geometry):
                             else:
                                 self.app.inform.emit('[ERROR_NOTCL] %s...' % _('G91 coordinates not implemented'))
                                 return 'fail'
+                        self.z_cut = deepcopy(old_zcut)
                 else:
                     log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> "
                               "The loaded Excellon file has no drills ...")
@@ -2879,7 +2888,7 @@ class CNCjob(Geometry):
                             z_offset = float(self.tool_offset[current_tooldia]) * (-1)
                         except KeyError:
                             z_offset = 0
-                        self.z_cut += z_offset
+                        self.z_cut = z_offset + old_zcut
 
                         self.coordinates_type = self.app.defaults["cncjob_coords_type"]
                         if self.coordinates_type == "G90":
@@ -2933,6 +2942,7 @@ class CNCjob(Geometry):
                         self.app.inform.emit('[ERROR_NOTCL] %s...' %
                                              _('The loaded Excellon file has no drills'))
                         return 'fail'
+                self.z_cut = deepcopy(old_zcut)
             log.debug("The total travel distance with Travelling Salesman Algorithm is: %s" % str(measured_distance))
 
         gcode += self.doformat(p.spindle_stop_code)  # Spindle stop
@@ -2962,7 +2972,7 @@ class CNCjob(Geometry):
             feedrate=2.0, feedrate_z=2.0, feedrate_rapid=30,
             spindlespeed=None, spindledir='CW', dwell=False, dwelltime=1.0,
             multidepth=False, depthpercut=None,
-            toolchange=False, toolchangez=1.0, toolchangexy="0.0, 0.0", extracut=False,
+            toolchange=False, toolchangez=1.0, toolchangexy="0.0, 0.0", extracut=False, extracut_length=0.2,
             startz=None, endz=2.0, pp_geometry_name=None, tool_no=1):
         """
         Algorithm to generate from multitool Geometry.
@@ -2992,6 +3002,7 @@ class CNCjob(Geometry):
         :param toolchangexy:
         :param extracut:            Adds (or not) an extra cut at the end of each path overlapping the
                                     first point in path to ensure complete copper removal
+        :param extracut_length:     Extra cut legth at the end of the path
         :param startz:
         :param endz:
         :param pp_geometry_name:
@@ -3025,7 +3036,7 @@ class CNCjob(Geometry):
         self.z_feedrate = float(feedrate_z) if feedrate_z is not None else None
         self.feedrate_rapid = float(feedrate_rapid) if feedrate_rapid else None
 
-        self.spindlespeed = int(spindlespeed) if spindlespeed else None
+        self.spindlespeed = int(spindlespeed) if spindlespeed != 0 else None
         self.spindledir = spindledir
         self.dwell = dwell
         self.dwelltime = float(dwelltime) if dwelltime else None
@@ -3213,7 +3224,8 @@ class CNCjob(Geometry):
                     # calculate the cut distance
                     total_cut = total_cut + geo.length
 
-                    self.gcode += self.create_gcode_single_pass(geo, extracut, tolerance, old_point=current_pt)
+                    self.gcode += self.create_gcode_single_pass(geo, extracut, extracut_length, tolerance,
+                                                                old_point=current_pt)
 
                 # --------- Multi-pass ---------
                 else:
@@ -3227,7 +3239,7 @@ class CNCjob(Geometry):
 
                     total_cut += (geo.length * nr_cuts)
 
-                    self.gcode += self.create_gcode_multi_pass(geo, extracut, tolerance,
+                    self.gcode += self.create_gcode_multi_pass(geo, extracut, extracut_length, tolerance,
                                                                postproc=p, old_point=current_pt)
 
                 # calculate the total distance
@@ -3270,7 +3282,7 @@ class CNCjob(Geometry):
             spindlespeed=None, spindledir='CW', dwell=False, dwelltime=1.0,
             multidepth=False, depthpercut=None,
             toolchange=False, toolchangez=1.0, toolchangexy="0.0, 0.0",
-            extracut=False, startz=None, endz=2.0,
+            extracut=False, extracut_length=0.1, startz=None, endz=2.0,
             pp_geometry_name=None, tool_no=1):
         """
         Second algorithm to generate from Geometry.
@@ -3288,6 +3300,7 @@ class CNCjob(Geometry):
         :param depthpercut: Maximum depth in each pass.
         :param extracut: Adds (or not) an extra cut at the end of each path
             overlapping the first point in path to ensure complete copper removal
+        :param extracut_length: The extra cut length
         :return: None
         """
 
@@ -3375,7 +3388,7 @@ class CNCjob(Geometry):
         self.z_feedrate = float(feedrate_z) if feedrate_z is not None else None
         self.feedrate_rapid = float(feedrate_rapid) if feedrate_rapid else None
 
-        self.spindlespeed = int(spindlespeed) if spindlespeed else None
+        self.spindlespeed = int(spindlespeed) if spindlespeed != 0 else None
         self.spindledir = spindledir
         self.dwell = dwell
         self.dwelltime = float(dwelltime) if dwelltime else None
@@ -3559,7 +3572,8 @@ class CNCjob(Geometry):
                 if not multidepth:
                     # calculate the cut distance
                     total_cut += geo.length
-                    self.gcode += self.create_gcode_single_pass(geo, extracut, tolerance, old_point=current_pt)
+                    self.gcode += self.create_gcode_single_pass(geo, extracut, extracut_length, tolerance,
+                                                                old_point=current_pt)
 
                 # --------- Multi-pass ---------
                 else:
@@ -3573,7 +3587,7 @@ class CNCjob(Geometry):
 
                     total_cut += (geo.length * nr_cuts)
 
-                    self.gcode += self.create_gcode_multi_pass(geo, extracut, tolerance,
+                    self.gcode += self.create_gcode_multi_pass(geo, extracut, extracut_length, tolerance,
                                                                postproc=p, old_point=current_pt)
 
                 # calculate the travel distance
@@ -3798,7 +3812,7 @@ class CNCjob(Geometry):
             gcode += self.doformat(p.lift_code)
         return gcode
 
-    def create_gcode_single_pass(self, geometry, extracut, tolerance, old_point=(0, 0)):
+    def create_gcode_single_pass(self, geometry, extracut, extracut_length, tolerance, old_point=(0, 0)):
         # G-code. Note: self.linear2gcode() and self.point2gcode() will lower and raise the tool every time.
         gcode_single_pass = ''
 
@@ -3807,7 +3821,8 @@ class CNCjob(Geometry):
                 gcode_single_pass = self.linear2gcode(geometry, tolerance=tolerance, old_point=old_point)
             else:
                 if geometry.is_ring:
-                    gcode_single_pass = self.linear2gcode_extra(geometry, tolerance=tolerance, old_point=old_point)
+                    gcode_single_pass = self.linear2gcode_extra(geometry, extracut_length, tolerance=tolerance,
+                                                                old_point=old_point)
                 else:
                     gcode_single_pass = self.linear2gcode(geometry, tolerance=tolerance, old_point=old_point)
         elif type(geometry) == Point:
@@ -3818,7 +3833,7 @@ class CNCjob(Geometry):
 
         return gcode_single_pass
 
-    def create_gcode_multi_pass(self, geometry, extracut, tolerance, postproc, old_point=(0, 0)):
+    def create_gcode_multi_pass(self, geometry, extracut, extracut_length, tolerance, postproc, old_point=(0, 0)):
 
         gcode_multi_pass = ''
 
@@ -3851,8 +3866,8 @@ class CNCjob(Geometry):
                                                           old_point=old_point)
                 else:
                     if geometry.is_ring:
-                        gcode_multi_pass += self.linear2gcode_extra(geometry, tolerance=tolerance, z_cut=depth,
-                                                                    up=False, old_point=old_point)
+                        gcode_multi_pass += self.linear2gcode_extra(geometry, extracut_length, tolerance=tolerance,
+                                                                    z_cut=depth, up=False, old_point=old_point)
                     else:
                         gcode_multi_pass += self.linear2gcode(geometry, tolerance=tolerance, z_cut=depth, up=False,
                                                               old_point=old_point)
@@ -4513,13 +4528,14 @@ class CNCjob(Geometry):
             gcode += self.doformat(p.lift_code, x=prev_x, y=prev_y, z_move=z_move)  # Stop cutting
         return gcode
 
-    def linear2gcode_extra(self, linear, tolerance=0, down=True, up=True,
+    def linear2gcode_extra(self, linear, extracut_length, tolerance=0, down=True, up=True,
                      z_cut=None, z_move=None, zdownrate=None,
                      feedrate=None, feedrate_z=None, feedrate_rapid=None, cont=False, old_point=(0, 0)):
         """
         Generates G-code to cut along the linear feature.
 
         :param linear: The path to cut along.
+        :param extracut_length: how much to cut extra over the first point at the end of the path
         :type: Shapely.LinearRing or Shapely.Linear String
         :param tolerance: All points in the simplified object will be within the
             tolerance distance of the original geometry.
@@ -4602,8 +4618,7 @@ class CNCjob(Geometry):
                 # For Incremental coordinates type G91
                 # next_x = pt[0] - prev_x
                 # next_y = pt[1] - prev_y
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _('G91 coordinates not implemented ...'))
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _('G91 coordinates not implemented ...'))
                 next_x = pt[0]
                 next_y = pt[1]
 
@@ -4614,19 +4629,41 @@ class CNCjob(Geometry):
         # this line is added to create an extra cut over the first point in patch
         # to make sure that we remove the copper leftovers
         # Linear motion to the 1st point in the cut path
-        if self.coordinates_type == "G90":
-            # For Absolute coordinates type G90
-            last_x = path[1][0]
-            last_y = path[1][1]
+        # if self.coordinates_type == "G90":
+        #     # For Absolute coordinates type G90
+        #     last_x = path[1][0]
+        #     last_y = path[1][1]
+        # else:
+        #     # For Incremental coordinates type G91
+        #     last_x = path[1][0] - first_x
+        #     last_y = path[1][1] - first_y
+        # gcode += self.doformat(p.linear_code, x=last_x, y=last_y)
+
+        # the first point for extracut is always mandatory if the extracut is enabled. But if the length of distance
+        # between point 0 and point 1 is more than the distance we set for the extra cut then make an interpolation
+        # along the path and find the point at the distance extracut_length
+
+        if extracut_length == 0.0:
+            gcode += self.doformat(p.linear_code, x=path[1][0], y=path[1][1])
+            last_pt = path[1]
         else:
-            # For Incremental coordinates type G91
-            last_x = path[1][0] - first_x
-            last_y = path[1][1] - first_y
-        gcode += self.doformat(p.linear_code, x=last_x, y=last_y)
+            if abs(distance(path[1], path[0])) > extracut_length:
+                i_point = LineString([path[0], path[1]]).interpolate(extracut_length)
+                gcode += self.doformat(p.linear_code, x=i_point.x, y=i_point.y)
+                last_pt = (i_point.x, i_point.y)
+            else:
+                last_pt = path[0]
+                for pt in path[1:]:
+                    extracut_distance = abs(distance(pt, last_pt))
+                    if extracut_distance <= extracut_length:
+                        gcode += self.doformat(p.linear_code, x=pt[0], y=pt[1])
+                        last_pt = pt
+                    else:
+                        break
 
         # Up to travelling height.
         if up:
-            gcode += self.doformat(p.lift_code, x=last_x, y=last_y, z_move=z_move)  # Stop cutting
+            gcode += self.doformat(p.lift_code, x=last_pt[0], y=last_pt[1], z_move=z_move)  # Stop cutting
 
         return gcode
 

+ 0 - 2
flatcamEditors/FlatCAMGeoEditor.py

@@ -455,8 +455,6 @@ class PaintOptionsTool(FlatCAMTool):
         ovlabel = QtWidgets.QLabel('%s:' % _('Overlap Rate'))
         ovlabel.setToolTip(
             _("How much (fraction) of the tool width to overlap each tool pass.\n"
-              "Example:\n"
-              "A value here of 0.25 means 25%% from the tool diameter found above.\n\n"
               "Adjust the value starting with lower values\n"
               "and increasing it if areas that should be painted are still \n"
               "not painted.\n"

+ 14 - 1
flatcamGUI/FlatCAMGUI.py

@@ -168,6 +168,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                                                              _('&DXF as Gerber Object ...'), self)
         self.menufileimport.addAction(self.menufileimportdxf_as_gerber)
         self.menufileimport.addSeparator()
+        self.menufileimport_hpgl2_as_geo = QtWidgets.QAction(QtGui.QIcon('share/dxf16.png'),
+                                                             _('HPGL2 as Geometry Object ...'), self)
+        self.menufileimport.addAction(self.menufileimport_hpgl2_as_geo)
+        self.menufileimport.addSeparator()
 
         # Export ...
         self.menufileexport = self.menufile.addMenu(QtGui.QIcon('share/export.png'), _('Export'))
@@ -250,6 +254,13 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                                                          _('Save Project C&opy ...'), self)
         self.menufile_save.addAction(self.menufilesaveprojectcopy)
 
+        self.menufile_save.addSeparator()
+
+        # Save Object PDF
+        self.menufilesave_object_pdf = QtWidgets.QAction(QtGui.QIcon('share/pdf32.png'),
+                                                         _('Save Object as PDF ...'), self)
+        self.menufile_save.addAction(self.menufilesave_object_pdf)
+
         # Separator
         self.menufile.addSeparator()
 
@@ -754,6 +765,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                                                           _("Copper Thieving Tool"))
 
         self.fiducials_btn = self.toolbartools.addAction(QtGui.QIcon('share/fiducials_32.png'), _("Fiducials Tool"))
+        self.cal_btn = self.toolbartools.addAction(QtGui.QIcon('share/calibrate_32.png'), _("Calibration Tool"))
 
         # ########################################################################
         # ########################## Excellon Editor Toolbar# ####################
@@ -2198,6 +2210,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                                                           _("Copper Thieving Tool"))
 
         self.fiducials_btn = self.toolbartools.addAction(QtGui.QIcon('share/fiducials_32.png'), _("Fiducials Tool"))
+        self.cal_btn = self.toolbartools.addAction(QtGui.QIcon('share/calibrate_32.png'), _("Calibration Tool"))
 
         # ## Excellon Editor Toolbar # ##
         self.select_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), _("Select"))
@@ -2530,7 +2543,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.dblsidedtool.run(toggle=True)
                     return
 
-                # Calibrate  Tool
+                # Calibration  Tool
                 if key == QtCore.Qt.Key_E:
                     self.app.cal_exc_tool.run(toggle=True)
                     return

+ 133 - 43
flatcamGUI/ObjectUI.py

@@ -100,13 +100,10 @@ class ObjectUI(QtWidgets.QWidget):
             faclabel = QtWidgets.QLabel('%s:' % _('Factor'))
             faclabel.setToolTip(
                 _("Factor by which to multiply\n"
-                  "geometric features of this object.")
+                  "geometric features of this object.\n"
+                  "Expressions are allowed. E.g: 1/25.4")
             )
-            self.scale_entry = FCDoubleSpinner()
-            self.scale_entry.set_precision(self.decimals)
-            self.scale_entry.setRange(0.0, 9999.9999)
-            self.scale_entry.setSingleStep(0.1)
-
+            self.scale_entry = FCEntry()
             self.scale_entry.set_value(1.0)
 
             # GO Button
@@ -131,7 +128,8 @@ class ObjectUI(QtWidgets.QWidget):
             self.offset_vectorlabel = QtWidgets.QLabel('%s:' % _('Vector'))
             self.offset_vectorlabel.setToolTip(
                 _("Amount by which to move the object\n"
-                  "in the x and y axes in (x, y) format.")
+                  "in the x and y axes in (x, y) format.\n"
+                  "Expressions are allowed. E.g: (1/3.2, 0.5*3)")
             )
             self.offsetvector_entry = EvalEntry2()
             self.offsetvector_entry.setText("(0.0, 0.0)")
@@ -158,6 +156,8 @@ class GerberObjectUI(ObjectUI):
         ObjectUI.__init__(self, title=_('Gerber Object'), parent=parent, decimals=decimals)
         self.decimals = decimals
 
+        self.custom_box.addWidget(QtWidgets.QLabel(''))
+
         # Plot options
         grid0 = QtWidgets.QGridLayout()
         grid0.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
@@ -165,10 +165,20 @@ class GerberObjectUI(ObjectUI):
         grid0.setColumnStretch(0, 0)
         grid0.setColumnStretch(1, 1)
 
+        # Plot CB
+        self.plot_cb = FCCheckBox()
+        self.plot_cb.setToolTip(
+            _("Plot (show) this object.")
+        )
+        plot_label = QtWidgets.QLabel('<b>%s:</b>' % _("Plot"))
+
+        grid0.addWidget(plot_label, 0, 0)
+        grid0.addWidget(self.plot_cb, 0, 1)
+
         self.plot_options_label = QtWidgets.QLabel("<b>%s:</b>" % _("Plot Options"))
         self.plot_options_label.setMinimumWidth(90)
 
-        grid0.addWidget(self.plot_options_label, 0, 0)
+        grid0.addWidget(self.plot_options_label, 1, 0)
 
         # Solid CB
         self.solid_cb = FCCheckBox(label=_('Solid'))
@@ -176,23 +186,15 @@ class GerberObjectUI(ObjectUI):
             _("Solid color polygons.")
         )
         self.solid_cb.setMinimumWidth(50)
-        grid0.addWidget(self.solid_cb, 0, 1)
+        grid0.addWidget(self.solid_cb, 1, 1)
 
         # Multicolored CB
-        self.multicolored_cb = FCCheckBox(label=_('M-Color'))
+        self.multicolored_cb = FCCheckBox(label=_('Multi-Color'))
         self.multicolored_cb.setToolTip(
             _("Draw polygons in different colors.")
         )
         self.multicolored_cb.setMinimumWidth(55)
-        grid0.addWidget(self.multicolored_cb, 0, 2)
-
-        # Plot CB
-        self.plot_cb = FCCheckBox(_('Plot'))
-        self.plot_cb.setToolTip(
-            _("Plot (show) this object.")
-        )
-        self.plot_cb.setMinimumWidth(59)
-        grid0.addWidget(self.plot_cb, 0, 3)
+        grid0.addWidget(self.multicolored_cb, 1, 2)
 
         # ## Object name
         self.name_hlay = QtWidgets.QHBoxLayout()
@@ -264,7 +266,10 @@ class GerberObjectUI(ObjectUI):
         # start with apertures table hidden
         self.apertures_table.setVisible(False)
 
-        self.custom_box.addWidget(QtWidgets.QLabel(''))
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.custom_box.addWidget(separator_line)
 
         # Isolation Routing
         self.isolation_routing_label = QtWidgets.QLabel("<b>%s</b>" % _("Isolation Routing"))
@@ -552,6 +557,12 @@ class GerberObjectUI(ObjectUI):
             _("Create the Geometry Object\n"
               "for non-copper routing.")
         )
+        self.generate_ncc_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         grid2.addWidget(self.clearcopper_label, 1, 0)
         grid2.addWidget(self.generate_ncc_button, 1, 1)
 
@@ -568,6 +579,12 @@ class GerberObjectUI(ObjectUI):
             _("Generate the geometry for\n"
               "the board cutout.")
         )
+        self.generate_cutout_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         grid2.addWidget(self.board_cutout_label, 2, 0)
         grid2.addWidget(self.generate_cutout_button, 2, 1)
 
@@ -913,7 +930,9 @@ class ExcellonObjectUI(ObjectUI):
               "in RPM (optional)")
         )
         grid1.addWidget(spdlabel, 8, 0)
-        self.spindlespeed_entry = IntEntry(allow_empty=True)
+        self.spindlespeed_entry = FCSpinner()
+        self.spindlespeed_entry.set_range(0, 1000000)
+        self.spindlespeed_entry.setSingleStep(100)
         grid1.addWidget(self.spindlespeed_entry, 8, 1)
 
         # Dwell
@@ -1011,6 +1030,12 @@ class ExcellonObjectUI(ObjectUI):
         self.generate_cnc_button.setToolTip(
             _("Generate the CNC Job.")
         )
+        self.generate_cnc_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
         grid2.addWidget(self.generate_cnc_button, 2, 0, 1, 3)
 
         # ### Milling Holes Drills ####
@@ -1036,6 +1061,12 @@ class ExcellonObjectUI(ObjectUI):
             _("Create the Geometry Object\n"
               "for milling DRILLS toolpaths.")
         )
+        self.generate_milling_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
 
         grid2.addWidget(self.tdlabel, 4, 0)
         grid2.addWidget(self.tooldia_entry, 4, 1)
@@ -1057,6 +1088,12 @@ class ExcellonObjectUI(ObjectUI):
             _("Create the Geometry Object\n"
               "for milling SLOTS toolpaths.")
         )
+        self.generate_milling_slots_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
 
         grid2.addWidget(self.stdlabel, 5, 0)
         grid2.addWidget(self.slot_tooldia_entry, 5, 1)
@@ -1501,14 +1538,29 @@ class GeometryObjectUI(ObjectUI):
         self.cncfeedrate_rapid_entry.hide()
 
         # Cut over 1st point in path
-        self.extracut_cb = FCCheckBox('%s' % _('Re-cut 1st pt.'))
+        self.extracut_cb = FCCheckBox('%s' % _('Re-cut'))
         self.extracut_cb.setToolTip(
             _("In order to remove possible\n"
               "copper leftovers where first cut\n"
               "meet with last cut, we generate an\n"
               "extended cut over the first cut section.")
         )
+
+        self.e_cut_entry = FCDoubleSpinner()
+        self.e_cut_entry.set_range(0, 99999)
+        self.e_cut_entry.set_precision(self.decimals)
+        self.e_cut_entry.setSingleStep(0.1)
+        self.e_cut_entry.setWrapping(True)
+        self.e_cut_entry.setToolTip(
+            _("In order to remove possible\n"
+              "copper leftovers where first cut\n"
+              "meet with last cut, we generate an\n"
+              "extended cut over the first cut section.")
+        )
         self.grid3.addWidget(self.extracut_cb, 13, 0)
+        self.grid3.addWidget(self.e_cut_entry, 13, 1)
+
+        self.ois_e_cut = OptionalInputSection(self.extracut_cb, [self.e_cut_entry])
 
         # Spindlespeed
         spdlabel = QtWidgets.QLabel('%s:' % _('Spindle speed'))
@@ -1519,7 +1571,9 @@ class GeometryObjectUI(ObjectUI):
                 "this value is the power of laser."
             )
         )
-        self.cncspindlespeed_entry = IntEntry(allow_empty=True)
+        self.cncspindlespeed_entry = FCSpinner()
+        self.cncspindlespeed_entry.set_range(0, 1000000)
+        self.cncspindlespeed_entry.setSingleStep(100)
 
         self.grid3.addWidget(spdlabel, 14, 0)
         self.grid3.addWidget(self.cncspindlespeed_entry, 14, 1)
@@ -1617,13 +1671,28 @@ class GeometryObjectUI(ObjectUI):
         self.generate_cnc_button.setToolTip(
             _("Generate the CNC Job object.")
         )
-        self.geo_param_box.addWidget(self.generate_cnc_button)
+        self.generate_cnc_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.grid3.addWidget(self.generate_cnc_button, 23, 0, 1, 2)
+
+        self.grid3.addWidget(QtWidgets.QLabel(''), 24, 0, 1, 2)
 
         # ##############
         # Paint area ##
         # ##############
-        self.paint_label = QtWidgets.QLabel('<b>%s</b>' % _('Paint Area'))
-        self.paint_label.setToolTip(
+        self.tools_label = QtWidgets.QLabel('<b>%s</b>' % _('TOOLS'))
+        self.tools_label.setToolTip(
+            _("Launch Paint Tool in Tools Tab.")
+        )
+        self.grid3.addWidget(self.tools_label, 25, 0, 1, 2)
+
+        # Paint Button
+        self.paint_tool_button = QtWidgets.QPushButton(_('Paint Tool'))
+        self.paint_tool_button.setToolTip(
             _(
                 "Creates tool paths to cover the\n"
                 "whole area of a polygon (remove\n"
@@ -1631,14 +1700,27 @@ class GeometryObjectUI(ObjectUI):
                 "to click on the desired polygon."
             )
         )
-        self.geo_tools_box.addWidget(self.paint_label)
+        self.paint_tool_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.grid3.addWidget(self.paint_tool_button, 26, 0, 1, 2)
 
-        # GO Button
-        self.paint_tool_button = QtWidgets.QPushButton(_('Paint Tool'))
-        self.paint_tool_button.setToolTip(
-            _("Launch Paint Tool in Tools Tab.")
+        # NCC Tool
+        self.generate_ncc_button = QtWidgets.QPushButton(_('NCC Tool'))
+        self.generate_ncc_button.setToolTip(
+            _("Create the Geometry Object\n"
+              "for non-copper routing.")
         )
-        self.geo_tools_box.addWidget(self.paint_tool_button)
+        self.generate_ncc_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.grid3.addWidget(self.generate_ncc_button, 27, 0, 1, 2)
 
 
 class CNCObjectUI(ObjectUI):
@@ -1781,12 +1863,20 @@ class CNCObjectUI(ObjectUI):
 
         self.cnc_tools_table.setColumnCount(7)
         self.cnc_tools_table.setColumnWidth(0, 20)
-        self.cnc_tools_table.setHorizontalHeaderLabels(['#', _('Dia'), _('Offset'), _('Type'), _('TT'), '',
-                                                        _('P')])
+        self.cnc_tools_table.setHorizontalHeaderLabels(['#', _('Dia'), _('Offset'), _('Type'), _('TT'), '', _('P')])
         self.cnc_tools_table.setColumnHidden(5, True)
         # stylesheet = "::section{Background-color:rgb(239,239,245)}"
         # self.cnc_tools_table.horizontalHeader().setStyleSheet(stylesheet)
 
+        self.exc_cnc_tools_table = FCTable()
+        self.custom_box.addWidget(self.exc_cnc_tools_table)
+
+        self.exc_cnc_tools_table.setColumnCount(7)
+        self.exc_cnc_tools_table.setColumnWidth(0, 20)
+        self.exc_cnc_tools_table.setHorizontalHeaderLabels(['#', _('Dia'), _('Drills'), _('Slots'), '', _("Cut Z"),
+                                                            _('P')])
+        self.exc_cnc_tools_table.setColumnHidden(4, True)
+
         self.tooldia_entry = FCDoubleSpinner()
         self.tooldia_entry.set_range(0, 9999.9999)
         self.tooldia_entry.set_precision(self.decimals)
@@ -1820,7 +1910,7 @@ class CNCObjectUI(ObjectUI):
 
         self.prepend_text = FCTextArea()
         self.prepend_text.setPlaceholderText(
-            _("Type here any G-Code commands you would "
+            _("Type here any G-Code commands you would\n"
               "like to add at the beginning of the G-Code file.")
         )
         self.custom_box.addWidget(self.prepend_text)
@@ -1836,8 +1926,8 @@ class CNCObjectUI(ObjectUI):
 
         self.append_text = FCTextArea()
         self.append_text.setPlaceholderText(
-            _("Type here any G-Code commands you would "
-              "like to append to the generated file. "
+            _("Type here any G-Code commands you would\n"
+              "like to append to the generated file.\n"
               "I.e.: M2 (End of program)")
         )
         self.custom_box.addWidget(self.append_text)
@@ -1868,12 +1958,12 @@ class CNCObjectUI(ObjectUI):
         self.toolchange_text = FCTextArea()
         self.toolchange_text.setPlaceholderText(
             _(
-                "Type here any G-Code commands you would "
-                "like to be executed when Toolchange event is encountered. "
-                "This will constitute a Custom Toolchange GCode, "
-                "or a Toolchange Macro. "
-                "The FlatCAM variables are surrounded by '%' symbol. \n"
-                "WARNING: it can be used only with a preprocessor file "
+                "Type here any G-Code commands you would\n"
+                "like to be executed when Toolchange event is encountered.\n"
+                "This will constitute a Custom Toolchange GCode,\n"
+                "or a Toolchange Macro.\n"
+                "The FlatCAM variables are surrounded by '%' symbol.\n"
+                "WARNING: it can be used only with a preprocessor file\n"
                 "that has 'toolchange_custom' in it's name."
             )
         )

+ 2 - 2
flatcamGUI/PlotCanvas.py

@@ -156,7 +156,7 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         self.big_cursor = None
         # Keep VisPy canvas happy by letting it be "frozen" again.
         self.freeze()
-
+        self.fit_view()
         self.graph_event_connect('mouse_wheel', self.on_mouse_scroll)
 
     def draw_workspace(self, workspace_size):
@@ -303,7 +303,7 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
             p2 = np.array(curr_pos)[:2]
             self.view.camera.pan(p2 - p1)
 
-        if self.fcapp.grid_status() == True:
+        if self.fcapp.grid_status():
             pos_canvas = self.translate_coords(curr_pos)
             pos = self.fcapp.geo_editor.snap(pos_canvas[0], pos_canvas[1])
 

+ 63 - 18
flatcamGUI/PreferencesUI.py

@@ -2430,7 +2430,9 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI):
               "in RPM (optional)")
         )
         grid2.addWidget(spdlabel, 6, 0)
-        self.spindlespeed_entry = IntEntry(allow_empty=True)
+        self.spindlespeed_entry = FCSpinner()
+        self.spindlespeed_entry.set_range(0, 1000000)
+        self.spindlespeed_entry.setSingleStep(100)
         grid2.addWidget(self.spindlespeed_entry, 6, 1)
 
         # Dwell
@@ -3341,7 +3343,10 @@ class GeometryOptPrefGroupUI(OptionsGroupUI):
             )
         )
         grid1.addWidget(spdlabel, 9, 0)
-        self.cncspindlespeed_entry = IntEntry(allow_empty=True)
+        self.cncspindlespeed_entry = FCSpinner()
+        self.cncspindlespeed_entry.set_range(0, 1000000)
+        self.cncspindlespeed_entry.setSingleStep(100)
+
         grid1.addWidget(self.cncspindlespeed_entry, 9, 1)
 
         # Dwell
@@ -3440,14 +3445,27 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI):
         grid1.addWidget(self.cncfeedrate_rapid_entry, 4, 1)
 
         # End move extra cut
-        self.extracut_cb = FCCheckBox(label='%s' % _('Re-cut 1st pt.'))
+        self.extracut_cb = FCCheckBox('%s' % _('Re-cut'))
         self.extracut_cb.setToolTip(
             _("In order to remove possible\n"
               "copper leftovers where first cut\n"
               "meet with last cut, we generate an\n"
               "extended cut over the first cut section.")
         )
+
+        self.e_cut_entry = FCDoubleSpinner()
+        self.e_cut_entry.set_range(0, 99999)
+        self.e_cut_entry.set_precision(self.decimals)
+        self.e_cut_entry.setSingleStep(0.1)
+        self.e_cut_entry.setWrapping(True)
+        self.e_cut_entry.setToolTip(
+            _("In order to remove possible\n"
+              "copper leftovers where first cut\n"
+              "meet with last cut, we generate an\n"
+              "extended cut over the first cut section.")
+        )
         grid1.addWidget(self.extracut_cb, 5, 0)
+        grid1.addWidget(self.e_cut_entry, 5, 1)
 
         # Probe depth
         self.pdepth_label = QtWidgets.QLabel('%s:' % _("Probe Z depth"))
@@ -3762,7 +3780,7 @@ class CNCJobOptPrefGroupUI(OptionsGroupUI):
 
         self.prepend_text = FCTextArea()
         self.prepend_text.setPlaceholderText(
-            _("Type here any G-Code commands you would "
+            _("Type here any G-Code commands you would\n"
               "like to add at the beginning of the G-Code file.")
         )
         self.layout.addWidget(self.prepend_text)
@@ -3779,8 +3797,8 @@ class CNCJobOptPrefGroupUI(OptionsGroupUI):
 
         self.append_text = FCTextArea()
         self.append_text.setPlaceholderText(
-            _("Type here any G-Code commands you would "
-              "like to append to the generated file. "
+            _("Type here any G-Code commands you would\n"
+              "like to append to the generated file.\n"
               "I.e.: M2 (End of program)")
         )
         self.layout.addWidget(self.append_text)
@@ -3832,12 +3850,12 @@ class CNCJobAdvOptPrefGroupUI(OptionsGroupUI):
         self.toolchange_text = FCTextArea()
         self.toolchange_text.setPlaceholderText(
             _(
-                "Type here any G-Code commands you would "
-                "like to be executed when Toolchange event is encountered. "
-                "This will constitute a Custom Toolchange GCode, "
-                "or a Toolchange Macro. "
-                "The FlatCAM variables are surrounded by '%' symbol. \n"
-                "WARNING: it can be used only with a preprocessor file "
+                "Type here any G-Code commands you would\n"
+                "like to be executed when Toolchange event is encountered.\n"
+                "This will constitute a Custom Toolchange GCode,\n"
+                "or a Toolchange Macro.\n"
+                "The FlatCAM variables are surrounded by '%' symbol.\n"
+                "WARNING: it can be used only with a preprocessor file\n"
                 "that has 'toolchange_custom' in it's name."
             )
         )
@@ -4834,7 +4852,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         self.orientation_label = QtWidgets.QLabel('%s:' % _("Page Orientation"))
         self.orientation_label.setToolTip(_("Can be:\n"
                                             "- Portrait\n"
-                                            "- Lanscape"))
+                                            "- Landscape"))
 
         self.orientation_radio = RadioSet([{'label': _('Portrait'), 'value': 'p'},
                                            {'label': _('Landscape'), 'value': 'l'},
@@ -6105,7 +6123,7 @@ class Tools2CThievingPrefGroupUI(OptionsGroupUI):
         ], orientation='vertical', stretch=False)
         self.reference_label = QtWidgets.QLabel(_("Reference:"))
         self.reference_label.setToolTip(
-            _("- 'Itself' - the copper Thieving extent is based on the object that is copper cleared.\n "
+            _("- 'Itself' - the copper Thieving extent is based on the object extent.\n"
               "- 'Area Selection' - left mouse click to start selection of the area to be filled.\n"
               "- 'Reference Object' - will do copper thieving within the area specified by another object.")
         )
@@ -6119,7 +6137,7 @@ class Tools2CThievingPrefGroupUI(OptionsGroupUI):
         ], stretch=False)
         self.bbox_type_label = QtWidgets.QLabel(_("Box Type:"))
         self.bbox_type_label.setToolTip(
-            _("- 'Rectangular' - the bounding box will be of rectangular shape.\n "
+            _("- 'Rectangular' - the bounding box will be of rectangular shape.\n"
               "- 'Minimal' - the bounding box will be the convex hull shape.")
         )
         grid_lay.addWidget(self.bbox_type_label, 5, 0)
@@ -6139,7 +6157,7 @@ class Tools2CThievingPrefGroupUI(OptionsGroupUI):
         ], orientation='vertical', stretch=False)
         self.fill_type_label = QtWidgets.QLabel(_("Fill Type:"))
         self.fill_type_label.setToolTip(
-            _("- 'Solid' - copper thieving will be a solid polygon.\n "
+            _("- 'Solid' - copper thieving will be a solid polygon.\n"
               "- 'Dots Grid' - the empty area will be filled with a pattern of dots.\n"
               "- 'Squares Grid' - the empty area will be filled with a pattern of squares.\n"
               "- 'Lines Grid' - the empty area will be filled with a pattern of lines.")
@@ -6346,7 +6364,7 @@ class Tools2FiducialsPrefGroupUI(OptionsGroupUI):
         ], stretch=False)
         self.mode_label = QtWidgets.QLabel(_("Mode:"))
         self.mode_label.setToolTip(
-            _("- 'Auto' - automatic placement of fiducials in the corners of the bounding box.\n "
+            _("- 'Auto' - automatic placement of fiducials in the corners of the bounding box.\n"
               "- 'Manual' - manual placement of fiducials.")
         )
         grid_lay.addWidget(self.mode_label, 3, 0)
@@ -6361,7 +6379,7 @@ class Tools2FiducialsPrefGroupUI(OptionsGroupUI):
         self.pos_label = QtWidgets.QLabel('%s:' % _("Second fiducial"))
         self.pos_label.setToolTip(
             _("The position for the second fiducial.\n"
-              "- 'Up' - the order is: bottom-left, top-left, top-right.\n "
+              "- 'Up' - the order is: bottom-left, top-left, top-right.\n"
               "- 'Down' - the order is: bottom-left, bottom-right, top-right.\n"
               "- 'None' - there is no second fiducial. The order is: bottom-left, top-right.")
         )
@@ -6495,6 +6513,33 @@ class Tools2CalPrefGroupUI(OptionsGroupUI):
         grid_lay.addWidget(toolchangez_lbl, 6, 0)
         grid_lay.addWidget(self.toolchangez_entry, 6, 1, 1, 2)
 
+        # Toolchange X-Y entry
+        toolchangexy_lbl = QtWidgets.QLabel('%s:' % _('Toolchange X-Y'))
+        toolchangexy_lbl.setToolTip(
+            _("Toolchange X,Y position.\n"
+              "If no value is entered then the current\n"
+              "(x, y) point will be used,")
+        )
+
+        self.toolchange_xy_entry = FCEntry()
+
+        grid_lay.addWidget(toolchangexy_lbl, 7, 0)
+        grid_lay.addWidget(self.toolchange_xy_entry, 7, 1, 1, 2)
+
+        # Second point choice
+        second_point_lbl = QtWidgets.QLabel('%s:' % _("Second point"))
+        second_point_lbl.setToolTip(
+            _("Second point in the Gcode verification can be:\n"
+              "- top-left -> the user will align the PCB vertically\n"
+              "- bottom-right -> the user will align the PCB horizontally")
+        )
+        self.second_point_radio = RadioSet([{'label': _('Top-Left'), 'value': 'tl'},
+                                            {'label': _('Bottom-Right'), 'value': 'br'}],
+                                           orientation='vertical')
+
+        grid_lay.addWidget(second_point_lbl, 8, 0)
+        grid_lay.addWidget(self.second_point_radio, 8, 1, 1, 2)
+
         self.layout.addStretch()
 
 

+ 7 - 0
flatcamGUI/VisPyCanvas.py

@@ -108,10 +108,17 @@ class VisPyCanvas(scene.SceneCanvas):
         # self.measure_fps()
 
     def translate_coords(self, pos):
+        """
+        Translate pixels to FlatCAM units.
+
+        """
         tr = self.grid.get_transform('canvas', 'visual')
         return tr.map(pos)
 
     def translate_coords_2(self, pos):
+        """
+        Translate FlatCAM units to pixels.
+        """
         tr = self.grid.get_transform('visual', 'document')
         return tr.map(pos)
 

+ 10 - 9
flatcamParsers/ParseExcellon.py

@@ -94,11 +94,11 @@ class Excellon(Geometry):
         Geometry.__init__(self, geo_steps_per_circle=int(geo_steps_per_circle))
 
         # dictionary to store tools, see above for description
-        self.tools = {}
+        self.tools = dict()
         # list to store the drills, see above for description
-        self.drills = []
+        self.drills = list()
         # self.slots (list) to store the slots; each is a dictionary
-        self.slots = []
+        self.slots = list()
 
         self.source_file = ''
 
@@ -109,8 +109,8 @@ class Excellon(Geometry):
         self.match_routing_start = None
         self.match_routing_stop = None
 
-        self.num_tools = []  # List for keeping the tools sorted
-        self.index_per_tool = {}  # Dictionary to store the indexed points for each tool
+        self.num_tools = list()  # List for keeping the tools sorted
+        self.index_per_tool = dict()  # Dictionary to store the indexed points for each tool
 
         # ## IN|MM -> Units are inherited from Geometry
         self.units = self.app.defaults['units']
@@ -118,8 +118,8 @@ class Excellon(Geometry):
         # Trailing "T" or leading "L" (default)
         # self.zeros = "T"
         self.zeros = zeros or self.defaults["zeros"]
-        self.zeros_found = self.zeros
-        self.units_found = self.units
+        self.zeros_found = deepcopy(self.zeros)
+        self.units_found = deepcopy(self.units)
 
         # this will serve as a default if the Excellon file has no info regarding of tool diameters (this info may be
         # in another file like for PCB WIzard ECAD software
@@ -790,7 +790,7 @@ class Excellon(Geometry):
                     # ## Units and number format # ##
                     match = self.units_re.match(eline)
                     if match:
-                        self.units = self.units = {"METRIC": "MM", "INCH": "IN"}[match.group(1)]
+                        self.units = {"METRIC": "MM", "INCH": "IN"}[match.group(1)]
                         self.zeros = match.group(2)  # "T" or "L". Might be empty
                         self.excellon_format = match.group(3)
                         if self.excellon_format:
@@ -884,8 +884,9 @@ class Excellon(Geometry):
             log.error("Excellon PARSING FAILED. Line %d: %s" % (line_num, eline))
             msg = '[ERROR_NOTCL] %s' % \
                   _("An internal error has ocurred. See shell.\n")
-            msg += _('{e_code} Excellon Parser error.\nParsing Failed. Line {l_nr}: {line}\n').format(
+            msg += ('{e_code} {tx} {l_nr}: {line}\n').format(
                 e_code='[ERROR]',
+                tx=_("Excellon Parser error.\nParsing Failed. Line"),
                 l_nr=line_num,
                 line=eline)
             msg += traceback.format_exc()

+ 423 - 0
flatcamParsers/ParseHPGL2.py

@@ -0,0 +1,423 @@
+# ############################################################
+# FlatCAM: 2D Post-processing for Manufacturing              #
+# http://flatcam.org                                         #
+# File Author: Marius Adrian Stanciu (c)                     #
+# Date: 12/12/2019                                           #
+# MIT Licence                                                #
+# ############################################################
+
+from camlib import arc, three_point_circle
+import FlatCAMApp
+
+import numpy as np
+import re
+import logging
+import traceback
+from copy import deepcopy
+import sys
+
+from shapely.ops import unary_union
+from shapely.geometry import LineString, Point
+
+import FlatCAMTranslation as fcTranslate
+import gettext
+import builtins
+
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+log = logging.getLogger('base')
+
+
+class HPGL2:
+    """
+    HPGL2 parsing.
+    """
+
+    def __init__(self, app):
+        """
+        The constructor takes FlatCAMApp.App as parameter.
+
+        """
+        self.app = app
+
+        # How to approximate a circle with lines.
+        self.steps_per_circle = int(self.app.defaults["geometry_circle_steps"])
+        self.decimals = self.app.decimals
+
+        # store the file units here
+        self.units = 'MM'
+
+        # storage for the tools
+        self.tools = dict()
+
+        self.default_data = dict()
+        self.default_data.update({
+            "name": '_ncc',
+            "plot": self.app.defaults["geometry_plot"],
+            "cutz": self.app.defaults["geometry_cutz"],
+            "vtipdia": self.app.defaults["geometry_vtipdia"],
+            "vtipangle": self.app.defaults["geometry_vtipangle"],
+            "travelz": self.app.defaults["geometry_travelz"],
+            "feedrate": self.app.defaults["geometry_feedrate"],
+            "feedrate_z": self.app.defaults["geometry_feedrate_z"],
+            "feedrate_rapid": self.app.defaults["geometry_feedrate_rapid"],
+            "dwell": self.app.defaults["geometry_dwell"],
+            "dwelltime": self.app.defaults["geometry_dwelltime"],
+            "multidepth": self.app.defaults["geometry_multidepth"],
+            "ppname_g": self.app.defaults["geometry_ppname_g"],
+            "depthperpass": self.app.defaults["geometry_depthperpass"],
+            "extracut": self.app.defaults["geometry_extracut"],
+            "extracut_length": self.app.defaults["geometry_extracut_length"],
+            "toolchange": self.app.defaults["geometry_toolchange"],
+            "toolchangez": self.app.defaults["geometry_toolchangez"],
+            "endz": self.app.defaults["geometry_endz"],
+            "spindlespeed": self.app.defaults["geometry_spindlespeed"],
+            "toolchangexy": self.app.defaults["geometry_toolchangexy"],
+            "startz": self.app.defaults["geometry_startz"],
+
+            "tooldia": self.app.defaults["tools_painttooldia"],
+            "paintmargin": self.app.defaults["tools_paintmargin"],
+            "paintmethod": self.app.defaults["tools_paintmethod"],
+            "selectmethod": self.app.defaults["tools_selectmethod"],
+            "pathconnect": self.app.defaults["tools_pathconnect"],
+            "paintcontour": self.app.defaults["tools_paintcontour"],
+            "paintoverlap": self.app.defaults["tools_paintoverlap"],
+
+            "nccoverlap": self.app.defaults["tools_nccoverlap"],
+            "nccmargin": self.app.defaults["tools_nccmargin"],
+            "nccmethod": self.app.defaults["tools_nccmethod"],
+            "nccconnect": self.app.defaults["tools_nccconnect"],
+            "ncccontour": self.app.defaults["tools_ncccontour"],
+            "nccrest": self.app.defaults["tools_nccrest"]
+        })
+
+        # will store the geometry here for compatibility reason
+        self.solid_geometry = None
+
+        self.source_file = ''
+
+        # ### Parser patterns ## ##
+
+        # comment
+        self.comment_re = re.compile(r"^CO\s*[\"']([a-zA-Z0-9\s]*)[\"'];?$")
+
+        # select pen
+        self.sp_re = re.compile(r'SP(\d);?$')
+        # pen position
+        self.pen_re = re.compile(r"^(P[U|D]);?$")
+
+        # Initialize
+        self.initialize_re = re.compile(r'^(IN);?$')
+
+        # Absolute linear interpolation
+        self.abs_move_re = re.compile(r"^PA\s*(-?\d+\.?\d+?),?\s*(-?\d+\.?\d+?)*;?$")
+        # Relative linear interpolation
+        self.rel_move_re = re.compile(r"^PR\s*(-?\d+\.\d+?),?\s*(-?\d+\.\d+?)*;?$")
+
+        # Circular interpolation with radius
+        self.circ_re = re.compile(r"^CI\s*(\+?\d+\.?\d+?)?\s*;?\s*$")
+
+        # Arc interpolation with radius
+        self.arc_re = re.compile(r"^AA\s*([+-]?\d+),?\s*([+-]?\d+),?\s*([+-]?\d+);?$")
+
+        # Arc interpolation with 3 points
+        self.arc_3pt_re = re.compile(r"^AT\s*([+-]?\d+),?\s*([+-]?\d+),?\s*([+-]?\d+),?\s*([+-]?\d+);?$")
+
+        self.init_done = None
+
+    def parse_file(self, filename):
+        """
+        Creates a list of lines from the HPGL2 file and send it to the main parser.
+
+        :param filename: HPGL2 file to parse.
+        :type filename: str
+        :return: None
+        """
+
+        with open(filename, 'r') as gfile:
+            glines = [line.rstrip('\n') for line in gfile]
+            self.parse_lines(glines=glines)
+
+    def parse_lines(self, glines):
+        """
+        Main HPGL2 parser.
+
+        :param glines: HPGL2 code as list of strings, each element being
+            one line of the source file.
+        :type glines: list
+        :return: None
+        :rtype: None
+        """
+
+        # Coordinates of the current path, each is [x, y]
+        path = list()
+
+        geo_buffer = []
+
+        # Current coordinates
+        current_x = None
+        current_y = None
+
+        # Found coordinates
+        linear_x = None
+        linear_y = None
+
+        # store the pen (tool) status
+        pen_status = 'up'
+
+        # store the current tool here
+        current_tool = None
+
+        # ### Parsing starts here ## ##
+        line_num = 0
+        gline = ""
+
+        self.app.inform.emit('%s %d %s.' % (_("HPGL2 processing. Parsing"), len(glines), _("lines")))
+        try:
+            for gline in glines:
+                if self.app.abort_flag:
+                    # graceful abort requested by the user
+                    raise FlatCAMApp.GracefulException
+
+                line_num += 1
+                self.source_file += gline + '\n'
+
+                # Cleanup #
+                gline = gline.strip(' \r\n')
+                # log.debug("Line=%3s %s" % (line_num, gline))
+
+                # ###################
+                # Ignored lines #####
+                # Comments      #####
+                # ###################
+                match = self.comment_re.search(gline)
+                if match:
+                    log.debug(str(match.group(1)))
+                    continue
+
+                # search for the initialization
+                match = self.initialize_re.search(gline)
+                if match:
+                    self.init_done = True
+                    continue
+
+                if self.init_done is True:
+                    # tools detection
+                    match = self.sp_re.search(gline)
+                    if match:
+                        tool = match.group(1)
+                        # self.tools[tool] = dict()
+                        self.tools.update({
+                            tool: {
+                                'tooldia': float('%.*f' %
+                                                 (
+                                                     self.decimals,
+                                                     float(self.app.defaults['geometry_cnctooldia'])
+                                                 )
+                                                 ),
+                                'offset': 'Path',
+                                'offset_value': 0.0,
+                                'type': 'Iso',
+                                'tool_type': 'C1',
+                                'data': deepcopy(self.default_data),
+                                'solid_geometry': list()
+                            }
+                        })
+
+                        if current_tool:
+                            if path:
+                                geo = LineString(path)
+                                self.tools[current_tool]['solid_geometry'].append(geo)
+                                geo_buffer.append(geo)
+                                path[:] = []
+
+                        current_tool = tool
+                        continue
+
+                    # pen status detection
+                    match = self.pen_re.search(gline)
+                    if match:
+                        pen_status = {'PU': 'up', 'PD': 'down'}[match.group(1)]
+                        continue
+
+                    # Linear interpolation
+                    match = self.abs_move_re.search(gline)
+                    if match:
+                        # Parse coordinates
+                        if match.group(1) is not None:
+                            linear_x = parse_number(match.group(1))
+                            current_x = linear_x
+                        else:
+                            linear_x = current_x
+
+                        if match.group(2) is not None:
+                            linear_y = parse_number(match.group(2))
+                            current_y = linear_y
+                        else:
+                            linear_y = current_y
+
+                        # Pen down: add segment
+                        if pen_status == 'down':
+                            # if linear_x or linear_y are None, ignore those
+                            if current_x is not None and current_y is not None:
+                                # only add the point if it's a new one otherwise skip it (harder to process)
+                                if path[-1] != [current_x, current_y]:
+                                    path.append([current_x, current_y])
+                            else:
+                                self.app.inform.emit('[WARNING] %s: %s' %
+                                                     (_("Coordinates missing, line ignored"), str(gline)))
+
+                        elif pen_status == 'up':
+                            if len(path) > 1:
+                                geo = LineString(path)
+                                self.tools[current_tool]['solid_geometry'].append(geo)
+                                geo_buffer.append(geo)
+                                path[:] = []
+
+                            # if linear_x or linear_y are None, ignore those
+                            if linear_x is not None and linear_y is not None:
+                                path = [[linear_x, linear_y]]  # Start new path
+                            else:
+                                self.app.inform.emit('[WARNING] %s: %s' %
+                                                     (_("Coordinates missing, line ignored"), str(gline)))
+
+                        # log.debug("Line_number=%3s X=%s Y=%s (%s)" % (line_num, linear_x, linear_y, gline))
+                        continue
+
+                    # Circular interpolation
+                    match = self.circ_re.search(gline)
+                    if match:
+                        if len(path) > 1:
+                            geo = LineString(path)
+                            self.tools[current_tool]['solid_geometry'].append(geo)
+                            geo_buffer.append(geo)
+                            path[:] = []
+
+                        # if linear_x or linear_y are None, ignore those
+                        if linear_x is not None and linear_y is not None:
+                            path = [[linear_x, linear_y]]  # Start new path
+                        else:
+                            self.app.inform.emit('[WARNING] %s: %s' %
+                                                 (_("Coordinates missing, line ignored"), str(gline)))
+
+                        if current_x is not None and current_y is not None:
+                            radius = match.group(1)
+                            geo = Point((current_x, current_y)).buffer(radius, int(self.steps_per_circle))
+                            geo_line = geo.exterior
+                            self.tools[current_tool]['solid_geometry'].append(geo_line)
+                            geo_buffer.append(geo_line)
+                            continue
+
+                    # Arc interpolation with radius
+                    match = self.arc_re.search(gline)
+                    if match:
+                        if len(path) > 1:
+                            geo = LineString(path)
+                            self.tools[current_tool]['solid_geometry'].append(geo)
+                            geo_buffer.append(geo)
+                            path[:] = []
+
+                        # if linear_x or linear_y are None, ignore those
+                        if linear_x is not None and linear_y is not None:
+                            path = [[linear_x, linear_y]]  # Start new path
+                        else:
+                            self.app.inform.emit('[WARNING] %s: %s' %
+                                                 (_("Coordinates missing, line ignored"), str(gline)))
+
+                        if current_x is not None and current_y is not None:
+                            center = [parse_number(match.group(1)), parse_number(match.group(2))]
+                            angle = np.deg2rad(float(match.group(3)))
+                            p1 = [current_x, current_y]
+
+                            arcdir = "ccw" if angle >= 0.0 else "cw"
+                            radius = np.sqrt((center[0] - p1[0]) ** 2 + (center[1] - p1[1]) ** 2)
+                            startangle = np.arctan2(p1[1] - center[1], p1[0] - center[0])
+                            stopangle = startangle + angle
+
+                            geo = LineString(arc(center, radius, startangle, stopangle, arcdir, self.steps_per_circle))
+                            self.tools[current_tool]['solid_geometry'].append(geo)
+                            geo_buffer.append(geo)
+
+                            line_coords = list(geo.coords)
+                            current_x = line_coords[0]
+                            current_y = line_coords[1]
+                            continue
+
+                    # Arc interpolation with 3 points
+                    match = self.arc_3pt_re.search(gline)
+                    if match:
+                        if len(path) > 1:
+                            geo = LineString(path)
+                            self.tools[current_tool]['solid_geometry'].append(geo)
+                            geo_buffer.append(geo)
+                            path[:] = []
+
+                        # if linear_x or linear_y are None, ignore those
+                        if linear_x is not None and linear_y is not None:
+                            path = [[linear_x, linear_y]]  # Start new path
+                        else:
+                            self.app.inform.emit('[WARNING] %s: %s' %
+                                                 (_("Coordinates missing, line ignored"), str(gline)))
+
+                        if current_x is not None and current_y is not None:
+                            p1 = [current_x, current_y]
+                            p3 = [parse_number(match.group(1)), parse_number(match.group(2))]
+                            p2 = [parse_number(match.group(3)), parse_number(match.group(4))]
+
+                            try:
+                                center, radius, t = three_point_circle(p1, p2, p3)
+                            except TypeError:
+                                return
+
+                            direction = 'cw' if np.sign(t) > 0 else 'ccw'
+
+                            startangle = np.arctan2(p1[1] - center[1], p1[0] - center[0])
+                            stopangle = np.arctan2(p3[1] - center[1], p3[0] - center[0])
+
+                            geo = LineString(arc(center, radius, startangle, stopangle,
+                                                 direction, self.steps_per_circle))
+                            self.tools[current_tool]['solid_geometry'].append(geo)
+                            geo_buffer.append(geo)
+
+                            # p2 is the end point for the 3-pt circle
+                            current_x = p2[0]
+                            current_y = p2[1]
+                            continue
+
+                # ## Line did not match any pattern. Warn user.
+                log.warning("Line ignored (%d): %s" % (line_num, gline))
+
+            if not geo_buffer and not self.solid_geometry:
+                log.error("Object is not HPGL2 file or empty. Aborting Object creation.")
+                return 'fail'
+
+            log.warning("Joining %d polygons." % len(geo_buffer))
+            self.app.inform.emit('%s: %d.' % (_("Gerber processing. Joining polygons"), len(geo_buffer)))
+
+            new_poly = unary_union(geo_buffer)
+            self.solid_geometry = new_poly
+
+        except Exception as err:
+            ex_type, ex, tb = sys.exc_info()
+            traceback.print_tb(tb)
+            print(traceback.format_exc())
+
+            log.error("HPGL2 PARSING FAILED. Line %d: %s" % (line_num, gline))
+
+            loc = '%s #%d %s: %s\n' % (_("HPGL2 Line"), line_num, _("HPGL2 Line Content"), gline) + repr(err)
+            self.app.inform.emit('[ERROR] %s\n%s:' % (_("HPGL2 Parser ERROR"), loc))
+
+
+def parse_number(strnumber):
+    """
+    Parse a single number of HPGL2 coordinates.
+
+    :param strnumber: String containing a number
+    from a coordinate data block, possibly with a leading sign.
+    :type strnumber: str
+    :return: The number in floating point.
+    :rtype: float
+    """
+
+    return float(strnumber) / 40.0  # in milimeters

Diferenças do arquivo suprimidas por serem muito extensas
+ 411 - 213
flatcamTools/ToolCalibration.py


+ 1 - 1
flatcamTools/ToolCopperThieving.py

@@ -128,7 +128,7 @@ class ToolCopperThieving(FlatCAMTool):
         ], orientation='vertical', stretch=False)
         self.reference_label = QtWidgets.QLabel(_("Reference:"))
         self.reference_label.setToolTip(
-            _("- 'Itself' - the copper thieving extent is based on the object that is copper cleared.\n"
+            _("- 'Itself' - the copper thieving extent is based on the object extent.\n"
               "- 'Area Selection' - left mouse click to start selection of the area to be filled.\n"
               "- 'Reference Object' - will do copper thieving within the area specified by another object.")
         )

+ 1 - 1
flatcamTools/ToolDblSided.py

@@ -153,7 +153,7 @@ class DblSidedTool(FlatCAMTool):
         # ## Axis Location
         self.axis_location = RadioSet([{'label': _('Point'), 'value': 'point'},
                                        {'label': _('Box'), 'value': 'box'}])
-        self.axloc_label = QtWidgets.QLabel(_("Axis Ref:"))
+        self.axloc_label = QtWidgets.QLabel('%s:' % _("Axis Ref"))
         self.axloc_label.setToolTip(
             _("The axis should pass through a <b>point</b> or cut\n "
               "a specified <b>box</b> (in a FlatCAM object) through \n"

+ 4 - 1
flatcamTools/ToolDistance.py

@@ -349,7 +349,10 @@ class Distance(FlatCAMTool):
                 d = math.sqrt(dx ** 2 + dy ** 2)
                 self.stop_entry.set_value("(%.*f, %.*f)" % (self.decimals, pos[0], self.decimals, pos[1]))
 
-                self.app.inform.emit(_("MEASURING: Result D(x) = {d_x} | D(y) = {d_y} | Distance = {d_z}").format(
+                self.app.inform.emit("{tx1}: {tx2} D(x) = {d_x} | D(y) = {d_y} | (tx3} = {d_z}".format(
+                    tx1=_("MEASURING"),
+                    tx2=_("Result"),
+                    tx3=_("Distance"),
                     d_x='%*f' % (self.decimals, abs(dx)),
                     d_y='%*f' % (self.decimals, abs(dy)),
                     d_z='%*f' % (self.decimals, abs(d)))

+ 4 - 1
flatcamTools/ToolDistanceMin.py

@@ -278,7 +278,10 @@ class DistanceMin(FlatCAMTool):
             )
 
         if d != 0:
-            self.app.inform.emit(_("MEASURING: Result D(x) = {d_x} | D(y) = {d_y} | Distance = {d_z}").format(
+            self.app.inform.emit("{tx1}: {tx2} D(x) = {d_x} | D(y) = {d_y} | (tx3} = {d_z}".format(
+                tx1=_("MEASURING"),
+                tx2=_("Result"),
+                tx3=_("Distance"),
                 d_x='%*f' % (self.decimals, abs(dx)),
                 d_y='%*f' % (self.decimals, abs(dy)),
                 d_z='%*f' % (self.decimals, abs(d)))

+ 1 - 1
flatcamTools/ToolFiducials.py

@@ -203,7 +203,7 @@ class ToolFiducials(FlatCAMTool):
         self.pos_label = QtWidgets.QLabel('%s:' % _("Second fiducial"))
         self.pos_label.setToolTip(
             _("The position for the second fiducial.\n"
-              "- 'Up' - the order is: bottom-left, top-left, top-right.\n "
+              "- 'Up' - the order is: bottom-left, top-left, top-right.\n"
               "- 'Down' - the order is: bottom-left, bottom-right, top-right.\n"
               "- 'None' - there is no second fiducial. The order is: bottom-left, top-right.")
         )

+ 28 - 12
flatcamTools/ToolFilm.py

@@ -19,7 +19,7 @@ from reportlab.graphics import renderPDF
 from reportlab.pdfgen import canvas
 from reportlab.graphics import renderPM
 from reportlab.lib.units import inch, mm
-from reportlab.lib.pagesizes import landscape, portrait, A4
+from reportlab.lib.pagesizes import landscape, portrait
 
 from svglib.svglib import svg2rlg
 from xml.dom.minidom import parseString as parse_xml_string
@@ -669,6 +669,10 @@ class Film(FlatCAMTool):
                                  _("No FlatCAM object selected. Load an object for Box and retry."))
             return
 
+        if name == '' or boxname == '':
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("No FlatCAM object selected."))
+            return
+
         scale_stroke_width = float(self.film_scale_stroke_entry.get_value())
         source = self.source_punch.get_value()
         file_type = self.file_type_radio.get_value()
@@ -738,12 +742,18 @@ class Film(FlatCAMTool):
             self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export positive film cancelled."))
             return
         else:
+            pagesize = self.pagesize_combo.get_value()
+            orientation = self.orientation_radio.get_value()
+            color = self.app.defaults['tools_film_color']
+
             self.export_positive(name, boxname, filename,
                                  scale_stroke_factor=factor,
                                  scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
                                  skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
                                  skew_reference=skew_reference,
-                                 mirror=mirror, ftype=ftype
+                                 mirror=mirror,
+                                 pagesize=pagesize, orientation=orientation, color=color, opacity=1.0,
+                                 ftype=ftype
                                  )
 
     def generate_positive_punched_film(self, name, boxname, source, factor, ftype='svg'):
@@ -1068,7 +1078,7 @@ class Film(FlatCAMTool):
                         scale_stroke_factor=0.00,
                         scale_factor_x=None, scale_factor_y=None,
                         skew_factor_x=None, skew_factor_y=None, skew_reference='center',
-                        mirror=None,
+                        mirror=None,  orientation_val='p', pagesize_val='A4', color_val='black', opacity_val=1.0,
                         use_thread=True, ftype='svg'):
         """
         Exports a Geometry Object to an SVG file in positive black.
@@ -1112,7 +1122,12 @@ class Film(FlatCAMTool):
             self.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), obj))
             box = obj
 
-        def make_positive_film():
+        p_size = pagesize_val
+        orientation = orientation_val
+        color = color_val
+        transparency_level = opacity_val
+
+        def make_positive_film(p_size, orientation, color, transparency_level):
             log.debug("FilmTool.export_positive().make_positive_film()")
 
             exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor,
@@ -1127,9 +1142,9 @@ class Film(FlatCAMTool):
             # We set the colour to WHITE
             root = ET.fromstring(exported_svg)
             for child in root:
-                child.set('fill', str(self.app.defaults['tools_film_color']))
-                child.set('opacity', '1.0')
-                child.set('stroke', str(self.app.defaults['tools_film_color']))
+                child.set('fill', str(color))
+                child.set('opacity', str(transparency_level))
+                child.set('stroke', str(color))
 
             exported_svg = ET.tostring(root)
 
@@ -1190,7 +1205,7 @@ class Film(FlatCAMTool):
                     return 'fail'
             else:
                 try:
-                    if self.units == 'INCH':
+                    if self.units == 'IN':
                         unit = inch
                     else:
                         unit = mm
@@ -1198,11 +1213,10 @@ class Film(FlatCAMTool):
                     doc_final = StringIO(doc_final)
                     drawing = svg2rlg(doc_final)
 
-                    p_size = self.pagesize_combo.get_value()
                     if p_size == 'Bounds':
                         renderPDF.drawToFile(drawing, filename)
                     else:
-                        if self.orientation_radio.get_value() == 'p':
+                        if orientation == 'p':
                             page_size = portrait(self.pagesize[p_size])
                         else:
                             page_size = landscape(self.pagesize[p_size])
@@ -1225,7 +1239,8 @@ class Film(FlatCAMTool):
 
             def job_thread_film(app_obj):
                 try:
-                    make_positive_film()
+                    make_positive_film(p_size=p_size, orientation=orientation, color=color,
+                                       transparency_level=transparency_level)
                 except Exception:
                     proc.done()
                     return
@@ -1233,7 +1248,8 @@ class Film(FlatCAMTool):
 
             self.app.worker_task.emit({'fcn': job_thread_film, 'params': [self]})
         else:
-            make_positive_film()
+            make_positive_film(p_size=p_size, orientation=orientation, color=color,
+                               transparency_level=transparency_level)
 
     def reset_fields(self):
         self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))

+ 1 - 0
flatcamTools/ToolNonCopperClear.py

@@ -681,6 +681,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
             "ppname_g": self.app.defaults["geometry_ppname_g"],
             "depthperpass": self.app.defaults["geometry_depthperpass"],
             "extracut": self.app.defaults["geometry_extracut"],
+            "extracut_length": self.app.defaults["geometry_extracut_length"],
             "toolchange": self.app.defaults["geometry_toolchange"],
             "toolchangez": self.app.defaults["geometry_toolchangez"],
             "endz": self.app.defaults["geometry_endz"],

+ 2 - 0
flatcamTools/ToolPaint.py

@@ -440,6 +440,7 @@ class ToolPaint(FlatCAMTool, Gerber):
             "ppname_g": self.app.defaults["geometry_ppname_g"],
             "depthperpass": self.app.defaults["geometry_depthperpass"],
             "extracut": self.app.defaults["geometry_extracut"],
+            "extracut_length": self.app.defaults["geometry_extracut_length"],
             "toolchange": self.app.defaults["geometry_toolchange"],
             "toolchangez": self.app.defaults["geometry_toolchangez"],
             "endz": self.app.defaults["geometry_endz"],
@@ -633,6 +634,7 @@ class ToolPaint(FlatCAMTool, Gerber):
             "ppname_g": self.app.defaults["geometry_ppname_g"],
             "depthperpass": float(self.app.defaults["geometry_depthperpass"]),
             "extracut": self.app.defaults["geometry_extracut"],
+            "extracut_length": self.app.defaults["geometry_extracut_length"],
             "toolchange": self.app.defaults["geometry_toolchange"],
             "toolchangez": float(self.app.defaults["geometry_toolchangez"]),
             "endz": float(self.app.defaults["geometry_endz"]),

+ 2 - 4
flatcamTools/ToolSolderPaste.py

@@ -1283,8 +1283,7 @@ class SolderPaste(FlatCAMTool):
         obj = self.app.collection.get_by_name(name)
 
         if name == '':
-            self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                 _("There is no Geometry object available."))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Geometry object available."))
             return 'fail'
 
         if obj.special_group != 'solder_paste_tool':
@@ -1298,8 +1297,7 @@ class SolderPaste(FlatCAMTool):
             if obj.tools[tooluid_key]['solid_geometry'] is None:
                 a += 1
         if a == len(obj.tools):
-            self.app.inform.emit('[ERROR_NOTCL] %s...' %
-                                 _('Cancelled. Empty file, it has no geometry'))
+            self.app.inform.emit('[ERROR_NOTCL] %s...' %  _('Cancelled. Empty file, it has no geometry'))
             return 'fail'
 
         # use the name of the first tool selected in self.geo_tools_table which has the diameter passed as tool_dia

BIN
locale/en/LC_MESSAGES/strings.mo


Diferenças do arquivo suprimidas por serem muito extensas
+ 222 - 230
locale/en/LC_MESSAGES/strings.po


BIN
locale/es/LC_MESSAGES/strings.mo


Diferenças do arquivo suprimidas por serem muito extensas
+ 225 - 297
locale/es/LC_MESSAGES/strings.po


BIN
locale/pt_BR/LC_MESSAGES/strings.mo


Diferenças do arquivo suprimidas por serem muito extensas
+ 221 - 294
locale/pt_BR/LC_MESSAGES/strings.po


BIN
locale/ro/LC_MESSAGES/strings.mo


Diferenças do arquivo suprimidas por serem muito extensas
+ 217 - 239
locale/ro/LC_MESSAGES/strings.po


BIN
locale/ru/LC_MESSAGES/strings.mo


Diferenças do arquivo suprimidas por serem muito extensas
+ 219 - 252
locale/ru/LC_MESSAGES/strings.po


Diferenças do arquivo suprimidas por serem muito extensas
+ 357 - 323
locale_template/strings.pot


+ 4 - 4
requirements.txt

@@ -1,11 +1,11 @@
 # This file contains python only requirements to be installed with pip
 # Python packages that cannot be installed with pip (e.g. PyQt5, GDAL) are not included.
 # Usage: pip3 install -r requirements.txt
-numpy>=1.16
+numpy >=1.16
 matplotlib>=3.1
 cycler>=0.10
 python-dateutil>=2.1
-kiwisolver>=1.0.1
+kiwisolver>=1.1
 six
 setuptools
 dill
@@ -21,6 +21,6 @@ fontTools
 rasterio
 lxml
 ezdxf
-qrcode>=6.0
-reportlab>=3.0
+qrcode>=6.1
+reportlab>=3.5
 svglib

BIN
share/calibrate_16.png


BIN
share/calibrate_32.png


+ 6 - 1
tclCommands/TclCommandCncjob.py

@@ -35,6 +35,7 @@ class TclCommandCncjob(TclCommandSignaled):
         ('feedrate_rapid', float),
         ('multidepth', bool),
         ('extracut', bool),
+        ('extracut_length', float),
         ('depthperpass', float),
         ('toolchange', int),
         ('toolchangez', float),
@@ -65,6 +66,7 @@ class TclCommandCncjob(TclCommandSignaled):
             ('feedrate_rapid', 'Rapid moving at speed when cutting.'),
             ('multidepth', 'Use or not multidepth cnc cut. (True or False)'),
             ('extracut', 'Use or not an extra cnccut over the first point in path,in the job end (example: True)'),
+            ('extracut', 'The value for extra cnccut over the first point in path,in the job end; float'),
             ('depthperpass', 'Height of one layer for multidepth.'),
             ('toolchange', 'Enable tool changes (example: True).'),
             ('toolchangez', 'Z distance for toolchange (example: 30.0).'),
@@ -136,6 +138,8 @@ class TclCommandCncjob(TclCommandSignaled):
 
         args["multidepth"] = bool(args["multidepth"]) if "multidepth" in args else obj.options["multidepth"]
         args["extracut"] = bool(args["extracut"]) if "extracut" in args else obj.options["extracut"]
+        args["extracut_length"] = float(args["extracut_length"]) if "extracut_length" in args else \
+            obj.options["extracut_length"]
         args["depthperpass"] = args["depthperpass"] if "depthperpass" in args and args["depthperpass"] else \
             obj.options["depthperpass"]
 
@@ -143,7 +147,7 @@ class TclCommandCncjob(TclCommandSignaled):
             self.app.defaults["geometry_startz"]
         args["endz"] = args["endz"] if "endz" in args and args["endz"] else obj.options["endz"]
 
-        args["spindlespeed"] = args["spindlespeed"] if "spindlespeed" in args and args["spindlespeed"] else None
+        args["spindlespeed"] = args["spindlespeed"] if "spindlespeed" in args and args["spindlespeed"] != 0 else None
         args["dwell"] = bool(args["dwell"]) if "dwell" in args else obj.options["dwell"]
         args["dwelltime"] = args["dwelltime"] if "dwelltime" in args and args["dwelltime"] else obj.options["dwelltime"]
 
@@ -189,6 +193,7 @@ class TclCommandCncjob(TclCommandSignaled):
                     local_tools_dict[tool_uid]['data']['feedrate_rapid'] = args["feedrate_rapid"]
                     local_tools_dict[tool_uid]['data']['multidepth'] = args["multidepth"]
                     local_tools_dict[tool_uid]['data']['extracut'] = args["extracut"]
+                    local_tools_dict[tool_uid]['data']['extracut_length'] = args["extracut_length"]
                     local_tools_dict[tool_uid]['data']['depthperpass'] = args["depthperpass"]
                     local_tools_dict[tool_uid]['data']['toolchange'] = args["toolchange"]
                     local_tools_dict[tool_uid]['data']['toolchangez'] = args["toolchangez"]

+ 1 - 0
tclCommands/TclCommandCopperClear.py

@@ -170,6 +170,7 @@ class TclCommandCopperClear(TclCommand):
             "ppname_g": self.app.defaults["geometry_ppname_g"],
             "depthperpass": self.app.defaults["geometry_depthperpass"],
             "extracut": self.app.defaults["geometry_extracut"],
+            "extracut_length": self.app.defaults["geometry_extracut_length"],
             "toolchange": self.app.defaults["geometry_toolchange"],
             "toolchangez": self.app.defaults["geometry_toolchangez"],
             "endz": self.app.defaults["geometry_endz"],

+ 1 - 0
tclCommands/TclCommandPaint.py

@@ -159,6 +159,7 @@ class TclCommandPaint(TclCommand):
             "ppname_g": self.app.defaults["geometry_ppname_g"],
             "depthperpass": self.app.defaults["geometry_depthperpass"],
             "extracut": self.app.defaults["geometry_extracut"],
+            "extracut_length": self.app.defaults["geometry_extracut_length"],
             "toolchange": self.app.defaults["geometry_toolchange"],
             "toolchangez": self.app.defaults["geometry_toolchangez"],
             "endz": self.app.defaults["geometry_endz"],

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff