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

Merged in test_beta_8.906 (pull request #131)

Test beta 8.906
Marius Stanciu пре 7 година
родитељ
комит
c14f28d35e
42 измењених фајлова са 4528 додато и 949 уклоњено
  1. 352 92
      FlatCAMApp.py
  2. 201 139
      FlatCAMEditor.py
  3. 771 66
      FlatCAMGUI.py
  4. 395 93
      FlatCAMObj.py
  5. 475 1
      GUIElements.py
  6. 104 75
      ObjectCollection.py
  7. 76 15
      ObjectUI.py
  8. 2 2
      ParseFont.py
  9. 2 2
      ParseSVG.py
  10. 85 1
      README.md
  11. 310 82
      camlib.py
  12. 239 38
      flatcamTools/ToolCalculators.py
  13. 79 37
      flatcamTools/ToolCutOut.py
  14. 44 40
      flatcamTools/ToolDblSided.py
  15. 60 17
      flatcamTools/ToolFilm.py
  16. 11 4
      flatcamTools/ToolImage.py
  17. 20 16
      flatcamTools/ToolMeasurement.py
  18. 39 37
      flatcamTools/ToolMove.py
  19. 66 21
      flatcamTools/ToolNonCopperClear.py
  20. 86 31
      flatcamTools/ToolPaint.py
  21. 119 32
      flatcamTools/ToolPanelize.py
  22. 8 6
      flatcamTools/ToolProperties.py
  23. 15 1
      flatcamTools/ToolShell.py
  24. 82 38
      flatcamTools/ToolTransform.py
  25. 2 1
      flatcamTools/__init__.py
  26. 261 0
      postprocessors/Toolchange_Probe_MACH3.py
  27. 191 0
      postprocessors/Toolchange_Probe_general.py
  28. 75 12
      postprocessors/Toolchange_manual.py
  29. 64 9
      postprocessors/default.py
  30. 67 8
      postprocessors/grbl_11.py
  31. 14 2
      postprocessors/grbl_laser.py
  32. 48 8
      postprocessors/line_xyz.py
  33. 79 13
      postprocessors/marlin.py
  34. BIN
      share/fscreen32.png
  35. BIN
      share/plot32.png
  36. 7 0
      tclCommands/TclCommandCncjob.py
  37. 1 1
      tclCommands/TclCommandCutout.py
  38. 4 4
      tclCommands/TclCommandCutoutAny.py
  39. 6 2
      tclCommands/TclCommandDrillcncjob.py
  40. 1 1
      tclCommands/TclCommandListSys.py
  41. 2 2
      tclCommands/TclCommandOpenGerber.py
  42. 65 0
      tests/new_window_test.py

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


+ 201 - 139
FlatCAMEditor.py

@@ -7,7 +7,7 @@
 ############################################################
 ############################################################
 
 
 from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5 import QtGui, QtCore, QtWidgets
-from PyQt5.QtCore import Qt
+from PyQt5.QtCore import Qt, QSettings
 import FlatCAMApp
 import FlatCAMApp
 from camlib import *
 from camlib import *
 from FlatCAMTool import FlatCAMTool
 from FlatCAMTool import FlatCAMTool
@@ -590,6 +590,7 @@ class FCCircle(FCShapeTool):
         self.points.append(point)
         self.points.append(point)
 
 
         if len(self.points) == 1:
         if len(self.points) == 1:
+            self.draw_app.app.inform.emit("Click on Circle perimeter point to complete ...")
             return "Click on perimeter to complete ..."
             return "Click on perimeter to complete ..."
 
 
         if len(self.points) == 2:
         if len(self.points) == 2:
@@ -638,9 +639,11 @@ class FCArc(FCShapeTool):
         self.points.append(point)
         self.points.append(point)
 
 
         if len(self.points) == 1:
         if len(self.points) == 1:
+            self.draw_app.app.inform.emit("Click on Start arc point ...")
             return "Click on 1st point ..."
             return "Click on 1st point ..."
 
 
         if len(self.points) == 2:
         if len(self.points) == 2:
+            self.draw_app.app.inform.emit("Click on End arc point to complete ...")
             return "Click on 2nd point to complete ..."
             return "Click on 2nd point to complete ..."
 
 
         if len(self.points) == 3:
         if len(self.points) == 3:
@@ -850,6 +853,7 @@ class FCPolygon(FCShapeTool):
         self.points.append(point)
         self.points.append(point)
 
 
         if len(self.points) > 0:
         if len(self.points) > 0:
+            self.draw_app.app.inform.emit("Click on next Point or click Right mouse button to complete ...")
             return "Click on next point or hit ENTER to complete ..."
             return "Click on next point or hit ENTER to complete ..."
 
 
         return ""
         return ""
@@ -1239,7 +1243,7 @@ class FCText(FCShapeTool):
             self.geometry = DrawToolShape(affinity.translate(self.text_gui.text_path, xoff=dx, yoff=dy))
             self.geometry = DrawToolShape(affinity.translate(self.text_gui.text_path, xoff=dx, yoff=dy))
         except Exception as e:
         except Exception as e:
             log.debug("Font geometry is empty or incorrect: %s" % str(e))
             log.debug("Font geometry is empty or incorrect: %s" % str(e))
-            self.draw_app.app.inform.emit("[error]Font not supported. Only Regular, Bold, Italic and BoldItalic are "
+            self.draw_app.app.inform.emit("[ERROR]Font not supported. Only Regular, Bold, Italic and BoldItalic are "
                                           "supported. Error: %s" % str(e))
                                           "supported. Error: %s" % str(e))
             self.text_gui.text_path = []
             self.text_gui.text_path = []
             self.text_gui.hide_tool()
             self.text_gui.hide_tool()
@@ -1416,7 +1420,7 @@ class FCDrillAdd(FCShapeTool):
             self.draw_app.tools_table_exc.setCurrentItem(item)
             self.draw_app.tools_table_exc.setCurrentItem(item)
 
 
         except KeyError:
         except KeyError:
-            self.draw_app.app.inform.emit("[warning_notcl] To add a drill first select a tool")
+            self.draw_app.app.inform.emit("[WARNING_NOTCL] To add a drill first select a tool")
             self.draw_app.select_tool("select")
             self.draw_app.select_tool("select")
             return
             return
 
 
@@ -1500,7 +1504,7 @@ class FCDrillArray(FCShapeTool):
             item = self.draw_app.tools_table_exc.item((self.draw_app.last_tool_selected - 1), 1)
             item = self.draw_app.tools_table_exc.item((self.draw_app.last_tool_selected - 1), 1)
             self.draw_app.tools_table_exc.setCurrentItem(item)
             self.draw_app.tools_table_exc.setCurrentItem(item)
         except KeyError:
         except KeyError:
-            self.draw_app.app.inform.emit("[warning_notcl] To add an Drill Array first select a tool in Tool Table")
+            self.draw_app.app.inform.emit("[WARNING_NOTCL] To add an Drill Array first select a tool in Tool Table")
             return
             return
 
 
         geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y), static=True)
         geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y), static=True)
@@ -1525,7 +1529,7 @@ class FCDrillArray(FCShapeTool):
 
 
                 self.flag_for_circ_array = True
                 self.flag_for_circ_array = True
                 self.set_origin(point)
                 self.set_origin(point)
-                self.draw_app.app.inform.emit("Click on the circular array Start position")
+                self.draw_app.app.inform.emit("Click on the Drill Circular Array Start position")
             else:
             else:
                 self.destination = point
                 self.destination = point
                 self.make()
                 self.make()
@@ -1547,10 +1551,10 @@ class FCDrillArray(FCShapeTool):
                 self.drill_angle = float(self.draw_app.drill_angle_entry.get_value())
                 self.drill_angle = float(self.draw_app.drill_angle_entry.get_value())
             except TypeError:
             except TypeError:
                 self.draw_app.app.inform.emit(
                 self.draw_app.app.inform.emit(
-                    "[error_notcl] The value is not Float. Check for comma instead of dot separator.")
+                    "[ERROR_NOTCL] The value is not Float. Check for comma instead of dot separator.")
                 return
                 return
         except Exception as e:
         except Exception as e:
-            self.draw_app.app.inform.emit("[error_notcl] The value is mistyped. Check the value.")
+            self.draw_app.app.inform.emit("[ERROR_NOTCL] The value is mistyped. Check the value.")
             return
             return
 
 
         if self.drill_array == 'Linear':
         if self.drill_array == 'Linear':
@@ -1630,7 +1634,7 @@ class FCDrillArray(FCShapeTool):
                 self.geometry.append(DrawToolShape(geo))
                 self.geometry.append(DrawToolShape(geo))
         else:
         else:
             if (self.drill_angle * self.drill_array_size) > 360:
             if (self.drill_angle * self.drill_array_size) > 360:
-                self.draw_app.app.inform.emit("[warning_notcl]Too many drills for the selected spacing angle.")
+                self.draw_app.app.inform.emit("[WARNING_NOTCL]Too many drills for the selected spacing angle.")
                 return
                 return
 
 
             radius = distance(self.destination, self.origin)
             radius = distance(self.destination, self.origin)
@@ -1676,7 +1680,7 @@ class FCDrillResize(FCShapeTool):
         try:
         try:
             new_dia = self.draw_app.resdrill_entry.get_value()
             new_dia = self.draw_app.resdrill_entry.get_value()
         except:
         except:
-            self.draw_app.app.inform.emit("[error_notcl]Resize drill(s) failed. Please enter a diameter for resize.")
+            self.draw_app.app.inform.emit("[ERROR_NOTCL]Resize drill(s) failed. Please enter a diameter for resize.")
             return
             return
 
 
         if new_dia not in self.draw_app.olddia_newdia:
         if new_dia not in self.draw_app.olddia_newdia:
@@ -1890,9 +1894,6 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.app = app
         self.app = app
         self.canvas = app.plotcanvas
         self.canvas = app.plotcanvas
 
 
-        self.app.ui.geo_edit_toolbar.setDisabled(disabled)
-        self.app.ui.snap_max_dist_entry.setDisabled(disabled)
-
         self.app.ui.geo_add_circle_menuitem.triggered.connect(lambda: self.select_tool('circle'))
         self.app.ui.geo_add_circle_menuitem.triggered.connect(lambda: self.select_tool('circle'))
         self.app.ui.geo_add_arc_menuitem.triggered.connect(lambda: self.select_tool('arc'))
         self.app.ui.geo_add_arc_menuitem.triggered.connect(lambda: self.select_tool('arc'))
         self.app.ui.geo_add_rectangle_menuitem.triggered.connect(lambda: self.select_tool('rectangle'))
         self.app.ui.geo_add_rectangle_menuitem.triggered.connect(lambda: self.select_tool('rectangle'))
@@ -1969,6 +1970,9 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.move_timer = QtCore.QTimer()
         self.move_timer = QtCore.QTimer()
         self.move_timer.setSingleShot(True)
         self.move_timer.setSingleShot(True)
 
 
+        # this var will store the state of the toolbar before starting the editor
+        self.toolbar_old_state = False
+
         self.key = None  # Currently pressed key
         self.key = None  # Currently pressed key
         self.geo_key_modifiers = None
         self.geo_key_modifiers = None
         self.x = None  # Current mouse cursor pos
         self.x = None  # Current mouse cursor pos
@@ -1991,12 +1995,13 @@ class FlatCAMGeoEditor(QtCore.QObject):
             self.tools[tool]["button"].setCheckable(True)  # Checkable
             self.tools[tool]["button"].setCheckable(True)  # Checkable
 
 
         self.app.ui.grid_snap_btn.triggered.connect(self.on_grid_toggled)
         self.app.ui.grid_snap_btn.triggered.connect(self.on_grid_toggled)
+        self.app.ui.corner_snap_btn.setCheckable(True)
         self.app.ui.corner_snap_btn.triggered.connect(lambda: self.toolbar_tool_toggle("corner_snap"))
         self.app.ui.corner_snap_btn.triggered.connect(lambda: self.toolbar_tool_toggle("corner_snap"))
 
 
         self.options = {
         self.options = {
             "global_gridx": 0.1,
             "global_gridx": 0.1,
             "global_gridy": 0.1,
             "global_gridy": 0.1,
-            "snap_max": 0.05,
+            "global_snap_max": 0.05,
             "grid_snap": True,
             "grid_snap": True,
             "corner_snap": False,
             "corner_snap": False,
             "grid_gap_link": True
             "grid_gap_link": True
@@ -2009,7 +2014,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
 
         self.app.ui.grid_gap_x_entry.setText(str(self.options["global_gridx"]))
         self.app.ui.grid_gap_x_entry.setText(str(self.options["global_gridx"]))
         self.app.ui.grid_gap_y_entry.setText(str(self.options["global_gridy"]))
         self.app.ui.grid_gap_y_entry.setText(str(self.options["global_gridy"]))
-        self.app.ui.snap_max_dist_entry.setText(str(self.options["snap_max"]))
+        self.app.ui.snap_max_dist_entry.setText(str(self.options["global_snap_max"]))
         self.app.ui.grid_gap_link_cb.setChecked(True)
         self.app.ui.grid_gap_link_cb.setChecked(True)
 
 
         self.rtree_index = rtindex.Index()
         self.rtree_index = rtindex.Index()
@@ -2048,10 +2053,27 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.shapes.enabled = True
         self.shapes.enabled = True
         self.tool_shape.enabled = True
         self.tool_shape.enabled = True
         self.app.app_cursor.enabled = True
         self.app.app_cursor.enabled = True
-        self.app.ui.snap_max_dist_entry.setDisabled(False)
+
+        self.app.ui.snap_max_dist_entry.setEnabled(True)
         self.app.ui.corner_snap_btn.setEnabled(True)
         self.app.ui.corner_snap_btn.setEnabled(True)
+        self.app.ui.snap_magnet.setVisible(True)
+        self.app.ui.corner_snap_btn.setVisible(True)
 
 
         self.app.ui.geo_editor_menu.setDisabled(False)
         self.app.ui.geo_editor_menu.setDisabled(False)
+        self.app.ui.geo_editor_menu.menuAction().setVisible(True)
+
+        self.app.ui.update_obj_btn.setEnabled(True)
+        self.app.ui.g_editor_cmenu.setEnabled(True)
+
+        self.app.ui.geo_edit_toolbar.setDisabled(False)
+        self.app.ui.geo_edit_toolbar.setVisible(True)
+        self.app.ui.snap_toolbar.setDisabled(False)
+
+        # prevent the user to change anything in the Selected Tab while the Geo Editor is active
+        sel_tab_widget_list = self.app.ui.selected_tab.findChildren(QtWidgets.QWidget)
+        for w in sel_tab_widget_list:
+            w.setEnabled(False)
+
         # Tell the App that the editor is active
         # Tell the App that the editor is active
         self.editor_active = True
         self.editor_active = True
 
 
@@ -2059,11 +2081,33 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.disconnect_canvas_event_handlers()
         self.disconnect_canvas_event_handlers()
         self.clear()
         self.clear()
         self.app.ui.geo_edit_toolbar.setDisabled(True)
         self.app.ui.geo_edit_toolbar.setDisabled(True)
-        self.app.ui.geo_edit_toolbar.setVisible(False)
-        self.app.ui.snap_max_dist_entry.setDisabled(True)
-        self.app.ui.corner_snap_btn.setEnabled(False)
-        # never deactivate the snap toolbar - MS
-        # self.app.ui.snap_toolbar.setDisabled(True)  # TODO: Combine and move into tool
+
+        settings = QSettings("Open Source", "FlatCAM")
+        if settings.contains("theme"):
+            theme = settings.value('theme', type=str)
+            if theme == 'standard':
+                # self.app.ui.geo_edit_toolbar.setVisible(False)
+
+                self.app.ui.snap_max_dist_entry.setEnabled(False)
+                self.app.ui.corner_snap_btn.setEnabled(False)
+                self.app.ui.snap_magnet.setVisible(False)
+                self.app.ui.corner_snap_btn.setVisible(False)
+            elif theme == 'compact':
+                # self.app.ui.geo_edit_toolbar.setVisible(True)
+
+                self.app.ui.snap_max_dist_entry.setEnabled(False)
+                self.app.ui.corner_snap_btn.setEnabled(False)
+        else:
+            # self.app.ui.geo_edit_toolbar.setVisible(False)
+
+            self.app.ui.snap_magnet.setVisible(False)
+            self.app.ui.corner_snap_btn.setVisible(False)
+            self.app.ui.snap_max_dist_entry.setEnabled(False)
+            self.app.ui.corner_snap_btn.setEnabled(False)
+
+        # set the Editor Toolbar visibility to what was before entering in the Editor
+        self.app.ui.geo_edit_toolbar.setVisible(False) if self.toolbar_old_state is False \
+            else self.app.ui.geo_edit_toolbar.setVisible(True)
 
 
         # Disable visuals
         # Disable visuals
         self.shapes.enabled = False
         self.shapes.enabled = False
@@ -2071,6 +2115,13 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.app.app_cursor.enabled = False
         self.app.app_cursor.enabled = False
 
 
         self.app.ui.geo_editor_menu.setDisabled(True)
         self.app.ui.geo_editor_menu.setDisabled(True)
+        self.app.ui.geo_editor_menu.menuAction().setVisible(False)
+
+        self.app.ui.update_obj_btn.setEnabled(False)
+
+        self.app.ui.g_editor_cmenu.setEnabled(False)
+        self.app.ui.e_editor_cmenu.setEnabled(False)
+
         # Tell the app that the editor is no longer active
         # Tell the app that the editor is no longer active
         self.editor_active = False
         self.editor_active = False
 
 
@@ -2230,9 +2281,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
                     self.add_shape(DrawToolShape(shape))
                     self.add_shape(DrawToolShape(shape))
 
 
         self.replot()
         self.replot()
-        self.app.ui.geo_edit_toolbar.setDisabled(False)
-        self.app.ui.geo_edit_toolbar.setVisible(True)
-        self.app.ui.snap_toolbar.setDisabled(False)
+
 
 
         # start with GRID toolbar activated
         # start with GRID toolbar activated
         if self.app.ui.grid_snap_btn.isChecked() == False:
         if self.app.ui.grid_snap_btn.isChecked() == False:
@@ -2561,7 +2610,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         if event.key.name == 'Escape':
         if event.key.name == 'Escape':
             # TODO: ...?
             # TODO: ...?
             # self.on_tool_select("select")
             # self.on_tool_select("select")
-            self.app.inform.emit("[warning_notcl]Cancelled.")
+            self.app.inform.emit("[WARNING_NOTCL]Cancelled.")
 
 
             self.delete_utility_geometry()
             self.delete_utility_geometry()
 
 
@@ -2723,47 +2772,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
 
         # Show Shortcut list
         # Show Shortcut list
         if event.key.name == '`':
         if event.key.name == '`':
-            self.on_shortcut_list()
-
-    def on_shortcut_list(self):
-        msg = '''<b>Shortcut list in Geometry Editor</b><br>
-<br>
-<b>1:</b>       Zoom Fit<br>
-<b>2:</b>       Zoom Out<br>
-<b>3:</b>       Zoom In<br>
-<b>A:</b>       Add an 'Arc'<br>
-<b>B:</b>       Add a Buffer Geo<br>
-<b>C:</b>       Copy Geo Item<br>
-<b>E:</b>       Intersection Tool<br>
-<b>G:</b>       Grid Snap On/Off<br>
-<b>I:</b>       Paint Tool<br>
-<b>K:</b>       Corner Snap On/Off<br>
-<b>M:</b>       Move Geo Item<br>
-<br>
-<b>N:</b>       Add an 'Polygon'<br>
-<b>O:</b>       Add a 'Circle'<br>
-<b>P:</b>       Add a 'Path'<br>
-<b>R:</b>       Add an 'Rectangle'<br>
-<b>S:</b>       Substraction Tool<br>
-<b>T:</b>       Add Text Geometry<br>
-<b>U:</b>       Union Tool<br>
-<br>
-<b>X:</b>       Cut Path<br>
-<br>
-<b>~:</b>       Show Shortcut List<br>
-<br>
-<b>Space:</b>   Rotate selected Geometry<br>
-<b>Enter:</b>   Finish Current Action<br>
-<b>Escape:</b>  Select Tool (Exit any other Tool)<br>
-<b>Delete:</b>  Delete Obj'''
-
-        helpbox =QtWidgets.QMessageBox()
-        helpbox.setText(msg)
-        helpbox.setWindowTitle("Help")
-        helpbox.setWindowIcon(QtGui.QIcon('share/help.png'))
-        helpbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
-        helpbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
-        helpbox.exec_()
+            self.app.on_shortcut_list()
 
 
     def on_canvas_key_release(self, event):
     def on_canvas_key_release(self, event):
         self.key = None
         self.key = None
@@ -2945,7 +2954,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
                 nearest_pt, shape = self.storage.nearest((x, y))
                 nearest_pt, shape = self.storage.nearest((x, y))
 
 
                 nearest_pt_distance = distance((x, y), nearest_pt)
                 nearest_pt_distance = distance((x, y), nearest_pt)
-                if nearest_pt_distance <= self.options["snap_max"]:
+                if nearest_pt_distance <= float(self.options["global_snap_max"]):
                     snap_distance = nearest_pt_distance
                     snap_distance = nearest_pt_distance
                     snap_x, snap_y = nearest_pt
                     snap_x, snap_y = nearest_pt
             except (StopIteration, AssertionError):
             except (StopIteration, AssertionError):
@@ -3037,7 +3046,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
             results = shapes[0].geo
             results = shapes[0].geo
         except Exception as e:
         except Exception as e:
             log.debug("FlatCAMGeoEditor.intersection() --> %s" % str(e))
             log.debug("FlatCAMGeoEditor.intersection() --> %s" % str(e))
-            self.app.inform.emit("[warning_notcl]A selection of at least 2 geo items is required to do Intersection.")
+            self.app.inform.emit("[WARNING_NOTCL]A selection of at least 2 geo items is required to do Intersection.")
             self.select_tool('select')
             self.select_tool('select')
             return
             return
 
 
@@ -3075,7 +3084,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
 
         if buf_distance < 0:
         if buf_distance < 0:
             self.app.inform.emit(
             self.app.inform.emit(
-                "[error_notcl]Negative buffer value is not accepted. Use Buffer interior to generate an 'inside' shape")
+                "[ERROR_NOTCL]Negative buffer value is not accepted. Use Buffer interior to generate an 'inside' shape")
 
 
             # deselect everything
             # deselect everything
             self.selected = []
             self.selected = []
@@ -3083,11 +3092,11 @@ class FlatCAMGeoEditor(QtCore.QObject):
             return
             return
 
 
         if len(selected) == 0:
         if len(selected) == 0:
-            self.app.inform.emit("[warning_notcl] Nothing selected for buffering.")
+            self.app.inform.emit("[WARNING_NOTCL] Nothing selected for buffering.")
             return
             return
 
 
         if not isinstance(buf_distance, float):
         if not isinstance(buf_distance, float):
-            self.app.inform.emit("[warning_notcl] Invalid distance for buffering.")
+            self.app.inform.emit("[WARNING_NOTCL] Invalid distance for buffering.")
 
 
             # deselect everything
             # deselect everything
             self.selected = []
             self.selected = []
@@ -3097,7 +3106,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         pre_buffer = cascaded_union([t.geo for t in selected])
         pre_buffer = cascaded_union([t.geo for t in selected])
         results = pre_buffer.buffer(buf_distance - 1e-10, resolution=32, join_style=join_style)
         results = pre_buffer.buffer(buf_distance - 1e-10, resolution=32, join_style=join_style)
         if results.is_empty:
         if results.is_empty:
-            self.app.inform.emit("[error_notcl]Failed, the result is empty. Choose a different buffer value.")
+            self.app.inform.emit("[ERROR_NOTCL]Failed, the result is empty. Choose a different buffer value.")
             # deselect everything
             # deselect everything
             self.selected = []
             self.selected = []
             self.replot()
             self.replot()
@@ -3112,18 +3121,18 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
 
         if buf_distance < 0:
         if buf_distance < 0:
             self.app.inform.emit(
             self.app.inform.emit(
-                "[error_notcl]Negative buffer value is not accepted. Use Buffer interior to generate an 'inside' shape")
+                "[ERROR_NOTCL]Negative buffer value is not accepted. Use Buffer interior to generate an 'inside' shape")
             # deselect everything
             # deselect everything
             self.selected = []
             self.selected = []
             self.replot()
             self.replot()
             return
             return
 
 
         if len(selected) == 0:
         if len(selected) == 0:
-            self.app.inform.emit("[warning_notcl] Nothing selected for buffering.")
+            self.app.inform.emit("[WARNING_NOTCL] Nothing selected for buffering.")
             return
             return
 
 
         if not isinstance(buf_distance, float):
         if not isinstance(buf_distance, float):
-            self.app.inform.emit("[warning_notcl] Invalid distance for buffering.")
+            self.app.inform.emit("[WARNING_NOTCL] Invalid distance for buffering.")
             # deselect everything
             # deselect everything
             self.selected = []
             self.selected = []
             self.replot()
             self.replot()
@@ -3132,7 +3141,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         pre_buffer = cascaded_union([t.geo for t in selected])
         pre_buffer = cascaded_union([t.geo for t in selected])
         results = pre_buffer.buffer(-buf_distance + 1e-10, resolution=32, join_style=join_style)
         results = pre_buffer.buffer(-buf_distance + 1e-10, resolution=32, join_style=join_style)
         if results.is_empty:
         if results.is_empty:
-            self.app.inform.emit("[error_notcl]Failed, the result is empty. Choose a smaller buffer value.")
+            self.app.inform.emit("[ERROR_NOTCL]Failed, the result is empty. Choose a smaller buffer value.")
             # deselect everything
             # deselect everything
             self.selected = []
             self.selected = []
             self.replot()
             self.replot()
@@ -3152,7 +3161,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         #     return
         #     return
         #
         #
         # if not isinstance(buf_distance, float):
         # if not isinstance(buf_distance, float):
-        #     self.app.inform.emit("[warning] Invalid distance for buffering.")
+        #     self.app.inform.emit("[WARNING] Invalid distance for buffering.")
         #     return
         #     return
         #
         #
         # pre_buffer = cascaded_union([t.geo for t in selected])
         # pre_buffer = cascaded_union([t.geo for t in selected])
@@ -3182,7 +3191,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         selected = self.get_selected()
         selected = self.get_selected()
 
 
         if buf_distance < 0:
         if buf_distance < 0:
-            self.app.inform.emit("[error_notcl]Negative buffer value is not accepted. "
+            self.app.inform.emit("[ERROR_NOTCL]Negative buffer value is not accepted. "
                                  "Use Buffer interior to generate an 'inside' shape")
                                  "Use Buffer interior to generate an 'inside' shape")
             # deselect everything
             # deselect everything
             self.selected = []
             self.selected = []
@@ -3190,11 +3199,11 @@ class FlatCAMGeoEditor(QtCore.QObject):
             return
             return
 
 
         if len(selected) == 0:
         if len(selected) == 0:
-            self.app.inform.emit("[warning_notcl] Nothing selected for buffering.")
+            self.app.inform.emit("[WARNING_NOTCL] Nothing selected for buffering.")
             return
             return
 
 
         if not isinstance(buf_distance, float):
         if not isinstance(buf_distance, float):
-            self.app.inform.emit("[warning_notcl] Invalid distance for buffering.")
+            self.app.inform.emit("[WARNING_NOTCL] Invalid distance for buffering.")
             # deselect everything
             # deselect everything
             self.selected = []
             self.selected = []
             self.replot()
             self.replot()
@@ -3203,7 +3212,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         pre_buffer = cascaded_union([t.geo for t in selected])
         pre_buffer = cascaded_union([t.geo for t in selected])
         results = pre_buffer.buffer(buf_distance - 1e-10, resolution=32, join_style=join_style)
         results = pre_buffer.buffer(buf_distance - 1e-10, resolution=32, join_style=join_style)
         if results.is_empty:
         if results.is_empty:
-            self.app.inform.emit("[error_notcl]Failed, the result is empty. Choose a different buffer value.")
+            self.app.inform.emit("[ERROR_NOTCL]Failed, the result is empty. Choose a different buffer value.")
             # deselect everything
             # deselect everything
             self.selected = []
             self.selected = []
             self.replot()
             self.replot()
@@ -3221,13 +3230,13 @@ class FlatCAMGeoEditor(QtCore.QObject):
     #     selected = self.get_selected()
     #     selected = self.get_selected()
     #
     #
     #     if len(selected) == 0:
     #     if len(selected) == 0:
-    #         self.app.inform.emit("[warning] Nothing selected for painting.")
+    #         self.app.inform.emit("[WARNING] Nothing selected for painting.")
     #         return
     #         return
     #
     #
     #     for param in [tooldia, overlap, margin]:
     #     for param in [tooldia, overlap, margin]:
     #         if not isinstance(param, float):
     #         if not isinstance(param, float):
     #             param_name = [k for k, v in locals().items() if v is param][0]
     #             param_name = [k for k, v in locals().items() if v is param][0]
-    #             self.app.inform.emit("[warning] Invalid value for {}".format(param))
+    #             self.app.inform.emit("[WARNING] Invalid value for {}".format(param))
     #
     #
     #     # Todo: Check for valid method.
     #     # Todo: Check for valid method.
     #
     #
@@ -3279,19 +3288,19 @@ class FlatCAMGeoEditor(QtCore.QObject):
         selected = self.get_selected()
         selected = self.get_selected()
 
 
         if len(selected) == 0:
         if len(selected) == 0:
-            self.app.inform.emit("[warning_notcl]Nothing selected for painting.")
+            self.app.inform.emit("[WARNING_NOTCL]Nothing selected for painting.")
             return
             return
 
 
         for param in [tooldia, overlap, margin]:
         for param in [tooldia, overlap, margin]:
             if not isinstance(param, float):
             if not isinstance(param, float):
                 param_name = [k for k, v in locals().items() if v is param][0]
                 param_name = [k for k, v in locals().items() if v is param][0]
-                self.app.inform.emit("[warning] Invalid value for {}".format(param))
+                self.app.inform.emit("[WARNING] Invalid value for {}".format(param))
 
 
         results = []
         results = []
 
 
         if tooldia >= overlap:
         if tooldia >= overlap:
             self.app.inform.emit(
             self.app.inform.emit(
-                "[error_notcl] Could not do Paint. Overlap value has to be less than Tool Dia value.")
+                "[ERROR_NOTCL] Could not do Paint. Overlap value has to be less than Tool Dia value.")
             return
             return
 
 
         def recurse(geometry, reset=True):
         def recurse(geometry, reset=True):
@@ -3350,7 +3359,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
                 except Exception as e:
                 except Exception as e:
                     log.debug("Could not Paint the polygons. %s" % str(e))
                     log.debug("Could not Paint the polygons. %s" % str(e))
                     self.app.inform.emit(
                     self.app.inform.emit(
-                        "[error] Could not do Paint. Try a different combination of parameters. "
+                        "[ERROR] Could not do Paint. Try a different combination of parameters. "
                         "Or a different method of Paint\n%s" % str(e))
                         "Or a different method of Paint\n%s" % str(e))
                     return
                     return
 
 
@@ -3694,6 +3703,9 @@ class FlatCAMExcEditor(QtCore.QObject):
         # this will flag if the Editor "tools" are launched from key shortcuts (True) or from menu toolbar (False)
         # this will flag if the Editor "tools" are launched from key shortcuts (True) or from menu toolbar (False)
         self.launched_from_shortcuts = False
         self.launched_from_shortcuts = False
 
 
+        # this var will store the state of the toolbar before starting the editor
+        self.toolbar_old_state = False
+
         self.app.ui.delete_drill_btn.triggered.connect(self.on_delete_btn)
         self.app.ui.delete_drill_btn.triggered.connect(self.on_delete_btn)
         self.name_entry.returnPressed.connect(self.on_name_activate)
         self.name_entry.returnPressed.connect(self.on_name_activate)
         self.addtool_btn.clicked.connect(self.on_tool_add)
         self.addtool_btn.clicked.connect(self.on_tool_add)
@@ -3704,6 +3716,17 @@ class FlatCAMExcEditor(QtCore.QObject):
 
 
         self.drill_axis_radio.activated_custom.connect(self.on_linear_angle_radio)
         self.drill_axis_radio.activated_custom.connect(self.on_linear_angle_radio)
 
 
+        self.app.ui.exc_add_array_drill_menuitem.triggered.connect(self.exc_add_drill_array)
+        self.app.ui.exc_add_drill_menuitem.triggered.connect(self.exc_add_drill)
+
+        self.app.ui.exc_resize_drill_menuitem.triggered.connect(self.exc_resize_drills)
+        self.app.ui.exc_copy_drill_menuitem.triggered.connect(self.exc_copy_drills)
+        self.app.ui.exc_delete_drill_menuitem.triggered.connect(self.on_delete_btn)
+
+        self.app.ui.exc_move_drill_menuitem.triggered.connect(self.exc_move_drills)
+
+
+        # Init GUI
         self.drill_array_size_entry.set_value(5)
         self.drill_array_size_entry.set_value(5)
         self.drill_pitch_entry.set_value(2.54)
         self.drill_pitch_entry.set_value(2.54)
         self.drill_angle_entry.set_value(12)
         self.drill_angle_entry.set_value(12)
@@ -4046,7 +4069,7 @@ class FlatCAMExcEditor(QtCore.QObject):
             # each time a tool diameter is edited or added
             # each time a tool diameter is edited or added
             self.olddia_newdia[tool_dia] = tool_dia
             self.olddia_newdia[tool_dia] = tool_dia
         else:
         else:
-            self.app.inform.emit("[warning_notcl]Tool already in the original or actual tool list.\n"
+            self.app.inform.emit("[WARNING_NOTCL]Tool already in the original or actual tool list.\n"
                                  "Save and reedit Excellon if you need to add this tool. ")
                                  "Save and reedit Excellon if you need to add this tool. ")
             return
             return
 
 
@@ -4084,7 +4107,7 @@ class FlatCAMExcEditor(QtCore.QObject):
                 else:
                 else:
                     deleted_tool_dia_list.append(float('%.4f' % dia))
                     deleted_tool_dia_list.append(float('%.4f' % dia))
         except:
         except:
-            self.app.inform.emit("[warning_notcl]Select a tool in Tool Table")
+            self.app.inform.emit("[WARNING_NOTCL]Select a tool in Tool Table")
             return
             return
 
 
         for deleted_tool_dia in deleted_tool_dia_list:
         for deleted_tool_dia in deleted_tool_dia_list:
@@ -4172,8 +4195,26 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.shapes.enabled = True
         self.shapes.enabled = True
         self.tool_shape.enabled = True
         self.tool_shape.enabled = True
         # self.app.app_cursor.enabled = True
         # self.app.app_cursor.enabled = True
-        self.app.ui.snap_max_dist_entry.setDisabled(False)
+
+        self.app.ui.snap_max_dist_entry.setEnabled(True)
         self.app.ui.corner_snap_btn.setEnabled(True)
         self.app.ui.corner_snap_btn.setEnabled(True)
+        self.app.ui.snap_magnet.setVisible(True)
+        self.app.ui.corner_snap_btn.setVisible(True)
+
+        self.app.ui.exc_editor_menu.setDisabled(False)
+        self.app.ui.exc_editor_menu.menuAction().setVisible(True)
+
+        self.app.ui.update_obj_btn.setEnabled(True)
+        self.app.ui.e_editor_cmenu.setEnabled(True)
+
+        self.app.ui.exc_edit_toolbar.setDisabled(False)
+        self.app.ui.exc_edit_toolbar.setVisible(True)
+        # self.app.ui.snap_toolbar.setDisabled(False)
+
+        # start with GRID toolbar activated
+        if self.app.ui.grid_snap_btn.isChecked() is False:
+            self.app.ui.grid_snap_btn.trigger()
+
         # Tell the App that the editor is active
         # Tell the App that the editor is active
         self.editor_active = True
         self.editor_active = True
 
 
@@ -4181,9 +4222,35 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.disconnect_canvas_event_handlers()
         self.disconnect_canvas_event_handlers()
         self.clear()
         self.clear()
         self.app.ui.exc_edit_toolbar.setDisabled(True)
         self.app.ui.exc_edit_toolbar.setDisabled(True)
-        self.app.ui.exc_edit_toolbar.setVisible(False)
-        self.app.ui.snap_max_dist_entry.setDisabled(True)
-        self.app.ui.corner_snap_btn.setEnabled(False)
+
+        settings = QSettings("Open Source", "FlatCAM")
+        if settings.contains("theme"):
+            theme = settings.value('theme', type=str)
+            if theme == 'standard':
+                # self.app.ui.exc_edit_toolbar.setVisible(False)
+
+                self.app.ui.snap_max_dist_entry.setEnabled(False)
+                self.app.ui.corner_snap_btn.setEnabled(False)
+                self.app.ui.snap_magnet.setVisible(False)
+                self.app.ui.corner_snap_btn.setVisible(False)
+            elif theme == 'compact':
+                # self.app.ui.exc_edit_toolbar.setVisible(True)
+
+                self.app.ui.snap_max_dist_entry.setEnabled(False)
+                self.app.ui.corner_snap_btn.setEnabled(False)
+                self.app.ui.snap_magnet.setVisible(True)
+                self.app.ui.corner_snap_btn.setVisible(True)
+        else:
+            # self.app.ui.exc_edit_toolbar.setVisible(False)
+
+            self.app.ui.snap_max_dist_entry.setEnabled(False)
+            self.app.ui.corner_snap_btn.setEnabled(False)
+            self.app.ui.snap_magnet.setVisible(False)
+            self.app.ui.corner_snap_btn.setVisible(False)
+
+        # set the Editor Toolbar visibility to what was before entering in the Editor
+        self.app.ui.exc_edit_toolbar.setVisible(False) if self.toolbar_old_state is False \
+            else self.app.ui.exc_edit_toolbar.setVisible(True)
 
 
         # Disable visuals
         # Disable visuals
         self.shapes.enabled = False
         self.shapes.enabled = False
@@ -4193,6 +4260,14 @@ class FlatCAMExcEditor(QtCore.QObject):
         # Tell the app that the editor is no longer active
         # Tell the app that the editor is no longer active
         self.editor_active = False
         self.editor_active = False
 
 
+        self.app.ui.exc_editor_menu.setDisabled(True)
+        self.app.ui.exc_editor_menu.menuAction().setVisible(False)
+
+        self.app.ui.update_obj_btn.setEnabled(False)
+
+        self.app.ui.g_editor_cmenu.setEnabled(False)
+        self.app.ui.e_editor_cmenu.setEnabled(False)
+
         # Show original geometry
         # Show original geometry
         if self.exc_obj:
         if self.exc_obj:
             self.exc_obj.visible = True
             self.exc_obj.visible = True
@@ -4250,7 +4325,7 @@ class FlatCAMExcEditor(QtCore.QObject):
         # self.storage = FlatCAMExcEditor.make_storage()
         # self.storage = FlatCAMExcEditor.make_storage()
         self.replot()
         self.replot()
 
 
-    def edit_exc_obj(self, exc_obj):
+    def edit_fcexcellon(self, exc_obj):
         """
         """
         Imports the geometry from the given FlatCAM Excellon object
         Imports the geometry from the given FlatCAM Excellon object
         into the editor.
         into the editor.
@@ -4298,15 +4373,8 @@ class FlatCAMExcEditor(QtCore.QObject):
             self.storage_dict[tool_dia] = storage_elem
             self.storage_dict[tool_dia] = storage_elem
 
 
         self.replot()
         self.replot()
-        self.app.ui.exc_edit_toolbar.setDisabled(False)
-        self.app.ui.exc_edit_toolbar.setVisible(True)
-        self.app.ui.snap_toolbar.setDisabled(False)
-
-        # start with GRID toolbar activated
-        if self.app.ui.grid_snap_btn.isChecked() is False:
-            self.app.ui.grid_snap_btn.trigger()
 
 
-    def update_exc_obj(self, exc_obj):
+    def update_fcexcellon(self, exc_obj):
         """
         """
         Create a new Excellon object that contain the edited content of the source Excellon object
         Create a new Excellon object that contain the edited content of the source Excellon object
 
 
@@ -4411,6 +4479,21 @@ class FlatCAMExcEditor(QtCore.QObject):
         # Switch notebook to Selected page
         # Switch notebook to Selected page
         self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
         self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
 
 
+    def update_options(self, obj):
+        try:
+            if not obj.options:
+                obj.options = {}
+                obj.options['xmin'] = 0
+                obj.options['ymin'] = 0
+                obj.options['xmax'] = 0
+                obj.options['ymax'] = 0
+                return True
+            else:
+                return False
+        except AttributeError:
+            obj.options = {}
+            return True
+
     def new_edited_excellon(self, outname):
     def new_edited_excellon(self, outname):
         """
         """
         Creates a new Excellon object for the edited Excellon. Thread-safe.
         Creates a new Excellon object for the edited Excellon. Thread-safe.
@@ -4430,14 +4513,15 @@ class FlatCAMExcEditor(QtCore.QObject):
             excellon_obj.drills = self.new_drills
             excellon_obj.drills = self.new_drills
             excellon_obj.tools = self.new_tools
             excellon_obj.tools = self.new_tools
             excellon_obj.slots = self.new_slots
             excellon_obj.slots = self.new_slots
+            excellon_obj.options['name'] = outname
 
 
             try:
             try:
                 excellon_obj.create_geometry()
                 excellon_obj.create_geometry()
             except KeyError:
             except KeyError:
                 self.app.inform.emit(
                 self.app.inform.emit(
-                    "[error_notcl] There are no Tools definitions in the file. Aborting Excellon creation.")
+                    "[ERROR_NOTCL] There are no Tools definitions in the file. Aborting Excellon creation.")
             except:
             except:
-                msg = "[error] An internal error has ocurred. See shell.\n"
+                msg = "[ERROR] An internal error has ocurred. See shell.\n"
                 msg += traceback.format_exc()
                 msg += traceback.format_exc()
                 app_obj.inform.emit(msg)
                 app_obj.inform.emit(msg)
                 raise
                 raise
@@ -4469,7 +4553,7 @@ class FlatCAMExcEditor(QtCore.QObject):
             # self.draw_app.select_tool('select')
             # self.draw_app.select_tool('select')
             self.complete = True
             self.complete = True
             current_tool = 'select'
             current_tool = 'select'
-            self.app.inform.emit("[warning_notcl]Cancelled. There is no Tool/Drill selected")
+            self.app.inform.emit("[WARNING_NOTCL]Cancelled. There is no Tool/Drill selected")
 
 
         # This is to make the group behave as radio group
         # This is to make the group behave as radio group
         if current_tool in self.tools_exc:
         if current_tool in self.tools_exc:
@@ -4813,7 +4897,7 @@ class FlatCAMExcEditor(QtCore.QObject):
         if event.key.name == 'Escape':
         if event.key.name == 'Escape':
             # TODO: ...?
             # TODO: ...?
             # self.on_tool_select("select")
             # self.on_tool_select("select")
-            self.app.inform.emit("[warning_notcl]Cancelled.")
+            self.app.inform.emit("[WARNING_NOTCL]Cancelled.")
 
 
             self.delete_utility_geometry()
             self.delete_utility_geometry()
 
 
@@ -4830,7 +4914,7 @@ class FlatCAMExcEditor(QtCore.QObject):
                 self.delete_selected()
                 self.delete_selected()
                 self.replot()
                 self.replot()
             else:
             else:
-                self.app.inform.emit("[warning_notcl]Cancelled. Nothing selected to delete.")
+                self.app.inform.emit("[WARNING_NOTCL]Cancelled. Nothing selected to delete.")
             return
             return
 
 
         if event.key == '1':
         if event.key == '1':
@@ -4862,7 +4946,7 @@ class FlatCAMExcEditor(QtCore.QObject):
                 self.on_tool_select('copy')
                 self.on_tool_select('copy')
                 self.active_tool.set_origin((self.snap_x, self.snap_y))
                 self.active_tool.set_origin((self.snap_x, self.snap_y))
             else:
             else:
-                self.app.inform.emit("[warning_notcl]Cancelled. Nothing selected to copy.")
+                self.app.inform.emit("[WARNING_NOTCL]Cancelled. Nothing selected to copy.")
             return
             return
 
 
         # Add Drill Hole Tool
         # Add Drill Hole Tool
@@ -4899,7 +4983,7 @@ class FlatCAMExcEditor(QtCore.QObject):
                 self.on_tool_select('move')
                 self.on_tool_select('move')
                 self.active_tool.set_origin((self.snap_x, self.snap_y))
                 self.active_tool.set_origin((self.snap_x, self.snap_y))
             else:
             else:
-                self.app.inform.emit("[warning_notcl]Cancelled. Nothing selected to move.")
+                self.app.inform.emit("[WARNING_NOTCL]Cancelled. Nothing selected to move.")
             return
             return
 
 
         # Resize Tool
         # Resize Tool
@@ -4923,39 +5007,9 @@ class FlatCAMExcEditor(QtCore.QObject):
 
 
         # Show Shortcut list
         # Show Shortcut list
         if event.key.name == '`':
         if event.key.name == '`':
-            self.on_shortcut_list()
+            self.app.on_shortcut_list()
             return
             return
 
 
-    def on_shortcut_list(self):
-        msg = '''<b>Shortcut list in Geometry Editor</b><br>
-<br>
-<b>1:</b>       Zoom Fit<br>
-<b>2:</b>       Zoom Out<br>
-<b>3:</b>       Zoom In<br>
-<b>A:</b>       Add an 'Drill Array'<br>
-<b>C:</b>       Copy Drill Hole<br>
-<b>D:</b>       Add an Drill Hole<br>
-<b>G:</b>       Grid Snap On/Off<br>
-<b>K:</b>       Corner Snap On/Off<br>
-<b>M:</b>       Move Drill Hole<br>
-<br>
-<b>R:</b>       Resize a 'Drill Hole'<br>
-<b>S:</b>       Select Tool Active<br>
-<br>
-<b>~:</b>       Show Shortcut List<br>
-<br>
-<b>Enter:</b>   Finish Current Action<br>
-<b>Escape:</b>  Abort Current Action<br>
-<b>Delete:</b>  Delete Drill Hole'''
-
-        helpbox =QtWidgets.QMessageBox()
-        helpbox.setText(msg)
-        helpbox.setWindowTitle("Help")
-        helpbox.setWindowIcon(QtGui.QIcon('share/help.png'))
-        helpbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
-        helpbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
-        helpbox.exec_()
-
     def on_canvas_key_release(self, event):
     def on_canvas_key_release(self, event):
         self.key = None
         self.key = None
 
 
@@ -5197,10 +5251,18 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.select_tool('add_array')
         self.select_tool('add_array')
         return
         return
 
 
+    def exc_resize_drills(self):
+        self.select_tool('resize')
+        return
+
     def exc_copy_drills(self):
     def exc_copy_drills(self):
         self.select_tool('copy')
         self.select_tool('copy')
         return
         return
 
 
+    def exc_move_drills(self):
+        self.select_tool('move')
+        return
+
 def distance(pt1, pt2):
 def distance(pt1, pt2):
     return sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2)
     return sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2)
 
 

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


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


+ 475 - 1
GUIElements.py

@@ -1,4 +1,6 @@
-from PyQt5 import QtGui, QtCore, QtWidgets, QtWidgets
+from PyQt5 import QtGui, QtCore, QtWidgets
+from PyQt5.QtCore import pyqtSignal, pyqtSlot
+
 from copy import copy
 from copy import copy
 import re
 import re
 import logging
 import logging
@@ -550,6 +552,478 @@ class FCTab(QtWidgets.QTabWidget):
         self.tabBar().setTabButton(currentIndex, QtWidgets.QTabBar.RightSide, None)
         self.tabBar().setTabButton(currentIndex, QtWidgets.QTabBar.RightSide, None)
 
 
 
 
+class FCDetachableTab(QtWidgets.QTabWidget):
+    # From here: https://stackoverflow.com/questions/47267195/in-pyqt4-is-it-possible-to-detach-tabs-from-a-qtabwidget
+    def __init__(self, protect=None, protect_by_name=None, parent=None):
+
+        super().__init__()
+
+        self.tabBar = self.FCTabBar(self)
+        self.tabBar.onDetachTabSignal.connect(self.detachTab)
+        self.tabBar.onMoveTabSignal.connect(self.moveTab)
+        self.tabBar.detachedTabDropSignal.connect(self.detachedTabDrop)
+
+        self.setTabBar(self.tabBar)
+
+        # Used to keep a reference to detached tabs since their QMainWindow
+        # does not have a parent
+        self.detachedTabs = {}
+
+        # a way to make sure that tabs can't be closed after they attach to the parent tab
+        self.protect_tab = True if protect is not None and protect is True else False
+
+        self.protect_by_name = protect_by_name if isinstance(protect_by_name, list) else None
+
+        # Close all detached tabs if the application is closed explicitly
+        QtWidgets.qApp.aboutToQuit.connect(self.closeDetachedTabs) # @UndefinedVariable
+
+        # used by the property self.useOldIndex(param)
+        self.use_old_index = None
+        self.old_index = None
+
+        self.setTabsClosable(True)
+        self.tabCloseRequested.connect(self.closeTab)
+
+    def useOldIndex(self, param):
+        if param:
+            self.use_old_index = True
+        else:
+            self.use_old_index = False
+
+    def deleteTab(self, currentIndex):
+        widget = self.widget(currentIndex)
+        if widget is not None:
+            widget.deleteLater()
+        self.removeTab(currentIndex)
+
+    def closeTab(self, currentIndex):
+        self.removeTab(currentIndex)
+
+    def protectTab(self, currentIndex):
+        # self.FCTabBar().setTabButton(currentIndex, QtWidgets.QTabBar.RightSide, None)
+        self.tabBar.setTabButton(currentIndex, QtWidgets.QTabBar.RightSide, None)
+
+    ##
+    #  The default movable functionality of QTabWidget must remain disabled
+    #  so as not to conflict with the added features
+    def setMovable(self, movable):
+        pass
+
+    ##
+    #  Move a tab from one position (index) to another
+    #
+    #  @param    fromIndex    the original index location of the tab
+    #  @param    toIndex      the new index location of the tab
+    @pyqtSlot(int, int)
+    def moveTab(self, fromIndex, toIndex):
+        widget = self.widget(fromIndex)
+        icon = self.tabIcon(fromIndex)
+        text = self.tabText(fromIndex)
+
+        self.removeTab(fromIndex)
+        self.insertTab(toIndex, widget, icon, text)
+        self.setCurrentIndex(toIndex)
+
+    ##
+    #  Detach the tab by removing it's contents and placing them in
+    #  a DetachedTab window
+    #
+    #  @param    index    the index location of the tab to be detached
+    #  @param    point    the screen position for creating the new DetachedTab window
+    @pyqtSlot(int, QtCore.QPoint)
+    def detachTab(self, index, point):
+
+        self.old_index = index
+
+        # Get the tab content
+        name = self.tabText(index)
+        icon = self.tabIcon(index)
+        if icon.isNull():
+            icon = self.window().windowIcon()
+        contentWidget = self.widget(index)
+
+        try:
+            contentWidgetRect = contentWidget.frameGeometry()
+        except AttributeError:
+            return
+
+        # Create a new detached tab window
+        detachedTab = self.FCDetachedTab(name, contentWidget)
+        detachedTab.setWindowModality(QtCore.Qt.NonModal)
+        detachedTab.setWindowIcon(icon)
+        detachedTab.setGeometry(contentWidgetRect)
+        detachedTab.onCloseSignal.connect(self.attachTab)
+        detachedTab.onDropSignal.connect(self.tabBar.detachedTabDrop)
+        detachedTab.move(point)
+        detachedTab.show()
+
+
+        # Create a reference to maintain access to the detached tab
+        self.detachedTabs[name] = detachedTab
+
+
+    ##
+    #  Re-attach the tab by removing the content from the DetachedTab window,
+    #  closing it, and placing the content back into the DetachableTabWidget
+    #
+    #  @param    contentWidget    the content widget from the DetachedTab window
+    #  @param    name             the name of the detached tab
+    #  @param    icon             the window icon for the detached tab
+    #  @param    insertAt         insert the re-attached tab at the given index
+    def attachTab(self, contentWidget, name, icon, insertAt=None):
+
+        # Make the content widget a child of this widget
+        contentWidget.setParent(self)
+
+        # Remove the reference
+        del self.detachedTabs[name]
+
+        # helps in restoring the tab to the same index that it was before was detached
+        insert_index = self.old_index if self.use_old_index is True else insertAt
+
+        # Create an image from the given icon (for comparison)
+        if not icon.isNull():
+            try:
+                tabIconPixmap = icon.pixmap(icon.availableSizes()[0])
+                tabIconImage = tabIconPixmap.toImage()
+            except IndexError:
+                tabIconImage = None
+        else:
+            tabIconImage = None
+
+        # Create an image of the main window icon (for comparison)
+        if not icon.isNull():
+            try:
+                windowIconPixmap = self.window().windowIcon().pixmap(icon.availableSizes()[0])
+                windowIconImage = windowIconPixmap.toImage()
+            except IndexError:
+                windowIconImage = None
+        else:
+            windowIconImage = None
+
+        # Determine if the given image and the main window icon are the same.
+        # If they are, then do not add the icon to the tab
+        if tabIconImage == windowIconImage:
+            if insert_index is None:
+                index = self.addTab(contentWidget, name)
+            else:
+                index = self.insertTab(insert_index, contentWidget, name)
+        else:
+            if insert_index is None:
+                index = self.addTab(contentWidget, icon, name)
+            else:
+                index = self.insertTab(insert_index, contentWidget, icon, name)
+
+        # on reattaching the tab if protect is true then the closure button is not added
+        if self.protect_tab is True:
+            self.protectTab(index)
+
+        # on reattaching the tab disable the closure button for the tabs with the name in the self.protect_by_name list
+        if self.protect_by_name is not None:
+            for tab_name in self.protect_by_name:
+                for index in range(self.count()):
+                    if str(tab_name) == str(self.tabText(index)):
+                        self.protectTab(index)
+
+        # Make this tab the current tab
+            if index > -1:
+                self.setCurrentIndex(insert_index) if self.use_old_index else self.setCurrentIndex(index)
+
+    ##
+    #  Remove the tab with the given name, even if it is detached
+    #
+    #  @param    name    the name of the tab to be removed
+    def removeTabByName(self, name):
+
+        # Remove the tab if it is attached
+        attached = False
+        for index in range(self.count()):
+            if str(name) == str(self.tabText(index)):
+                self.removeTab(index)
+                attached = True
+                break
+
+
+        # If the tab is not attached, close it's window and
+        # remove the reference to it
+        if not attached:
+            for key in self.detachedTabs:
+                if str(name) == str(key):
+                    self.detachedTabs[key].onCloseSignal.disconnect()
+                    self.detachedTabs[key].close()
+                    del self.detachedTabs[key]
+                    break
+
+
+    ##
+    #  Handle dropping of a detached tab inside the DetachableTabWidget
+    #
+    #  @param    name     the name of the detached tab
+    #  @param    index    the index of an existing tab (if the tab bar
+    #                     determined that the drop occurred on an
+    #                     existing tab)
+    #  @param    dropPos  the mouse cursor position when the drop occurred
+    @QtCore.pyqtSlot(str, int, QtCore.QPoint)
+    def detachedTabDrop(self, name, index, dropPos):
+
+        # If the drop occurred on an existing tab, insert the detached
+        # tab at the existing tab's location
+        if index > -1:
+
+            # Create references to the detached tab's content and icon
+            contentWidget = self.detachedTabs[name].contentWidget
+            icon = self.detachedTabs[name].windowIcon()
+
+            # Disconnect the detached tab's onCloseSignal so that it
+            # does not try to re-attach automatically
+            self.detachedTabs[name].onCloseSignal.disconnect()
+
+            # Close the detached
+            self.detachedTabs[name].close()
+
+            # Re-attach the tab at the given index
+            self.attachTab(contentWidget, name, icon, index)
+
+
+        # If the drop did not occur on an existing tab, determine if the drop
+        # occurred in the tab bar area (the area to the side of the QTabBar)
+        else:
+
+            # Find the drop position relative to the DetachableTabWidget
+            tabDropPos = self.mapFromGlobal(dropPos)
+
+            # If the drop position is inside the DetachableTabWidget...
+            if self.rect().contains(tabDropPos):
+
+                # If the drop position is inside the tab bar area (the
+                # area to the side of the QTabBar) or there are not tabs
+                # currently attached...
+                if tabDropPos.y() < self.tabBar.height() or self.count() == 0:
+
+                    # Close the detached tab and allow it to re-attach
+                    # automatically
+                    self.detachedTabs[name].close()
+
+
+    ##
+    #  Close all tabs that are currently detached.
+    def closeDetachedTabs(self):
+        listOfDetachedTabs = []
+
+        for key in self.detachedTabs:
+            listOfDetachedTabs.append(self.detachedTabs[key])
+
+        for detachedTab in listOfDetachedTabs:
+            detachedTab.close()
+
+
+    ##
+    #  When a tab is detached, the contents are placed into this QMainWindow.  The tab
+    #  can be re-attached by closing the dialog or by dragging the window into the tab bar
+    class FCDetachedTab(QtWidgets.QMainWindow):
+        onCloseSignal = pyqtSignal(QtWidgets.QWidget, str, QtGui.QIcon)
+        onDropSignal = pyqtSignal(str, QtCore.QPoint)
+
+        def __init__(self, name, contentWidget):
+            QtWidgets.QMainWindow.__init__(self, None)
+
+            self.setObjectName(name)
+            self.setWindowTitle(name)
+
+            self.contentWidget = contentWidget
+            self.setCentralWidget(self.contentWidget)
+            self.contentWidget.show()
+
+            self.windowDropFilter = self.WindowDropFilter()
+            self.installEventFilter(self.windowDropFilter)
+            self.windowDropFilter.onDropSignal.connect(self.windowDropSlot)
+
+
+        ##
+        #  Handle a window drop event
+        #
+        #  @param    dropPos    the mouse cursor position of the drop
+        @QtCore.pyqtSlot(QtCore.QPoint)
+        def windowDropSlot(self, dropPos):
+            self.onDropSignal.emit(self.objectName(), dropPos)
+
+
+        ##
+        #  If the window is closed, emit the onCloseSignal and give the
+        #  content widget back to the DetachableTabWidget
+        #
+        #  @param    event    a close event
+        def closeEvent(self, event):
+            self.onCloseSignal.emit(self.contentWidget, self.objectName(), self.windowIcon())
+
+
+        ##
+        #  An event filter class to detect a QMainWindow drop event
+        class WindowDropFilter(QtCore.QObject):
+            onDropSignal = pyqtSignal(QtCore.QPoint)
+
+            def __init__(self):
+                QtCore.QObject.__init__(self)
+                self.lastEvent = None
+
+
+            ##
+            #  Detect a QMainWindow drop event by looking for a NonClientAreaMouseMove (173)
+            #  event that immediately follows a Move event
+            #
+            #  @param    obj    the object that generated the event
+            #  @param    event  the current event
+            def eventFilter(self, obj, event):
+
+                # If a NonClientAreaMouseMove (173) event immediately follows a Move event...
+                if self.lastEvent == QtCore.QEvent.Move and event.type() == 173:
+
+                    # Determine the position of the mouse cursor and emit it with the
+                    # onDropSignal
+                    mouseCursor = QtGui.QCursor()
+                    dropPos = mouseCursor.pos()
+                    self.onDropSignal.emit(dropPos)
+                    self.lastEvent = event.type()
+                    return True
+
+                else:
+                    self.lastEvent = event.type()
+                    return False
+
+    class FCTabBar(QtWidgets.QTabBar):
+        onDetachTabSignal = pyqtSignal(int, QtCore.QPoint)
+        onMoveTabSignal = pyqtSignal(int, int)
+        detachedTabDropSignal = pyqtSignal(str, int, QtCore.QPoint)
+
+        def __init__(self, parent=None):
+            QtWidgets.QTabBar.__init__(self, parent)
+
+            self.setAcceptDrops(True)
+            self.setElideMode(QtCore.Qt.ElideRight)
+            self.setSelectionBehaviorOnRemove(QtWidgets.QTabBar.SelectLeftTab)
+
+            self.dragStartPos = QtCore.QPoint()
+            self.dragDropedPos = QtCore.QPoint()
+            self.mouseCursor = QtGui.QCursor()
+            self.dragInitiated = False
+
+
+        #  Send the onDetachTabSignal when a tab is double clicked
+        #
+        #  @param    event    a mouse double click event
+        def mouseDoubleClickEvent(self, event):
+            event.accept()
+            self.onDetachTabSignal.emit(self.tabAt(event.pos()), self.mouseCursor.pos())
+
+
+        #  Set the starting position for a drag event when the mouse button is pressed
+        #
+        #  @param    event    a mouse press event
+        def mousePressEvent(self, event):
+            if event.button() == QtCore.Qt.LeftButton:
+                self.dragStartPos = event.pos()
+
+            self.dragDropedPos.setX(0)
+            self.dragDropedPos.setY(0)
+
+            self.dragInitiated = False
+
+            QtWidgets.QTabBar.mousePressEvent(self, event)
+
+
+        #  Determine if the current movement is a drag.  If it is, convert it into a QDrag.  If the
+        #  drag ends inside the tab bar, emit an onMoveTabSignal.  If the drag ends outside the tab
+        #  bar, emit an onDetachTabSignal.
+        #
+        #  @param    event    a mouse move event
+        def mouseMoveEvent(self, event):
+
+            # Determine if the current movement is detected as a drag
+            if not self.dragStartPos.isNull() and ((event.pos() - self.dragStartPos).manhattanLength() < QtWidgets.QApplication.startDragDistance()):
+                self.dragInitiated = True
+
+            # If the current movement is a drag initiated by the left button
+            if (((event.buttons() & QtCore.Qt.LeftButton)) and self.dragInitiated):
+
+                # Stop the move event
+                finishMoveEvent = QtGui.QMouseEvent(QtCore.QEvent.MouseMove, event.pos(), QtCore.Qt.NoButton, QtCore.Qt.NoButton, QtCore.Qt.NoModifier)
+                QtWidgets.QTabBar.mouseMoveEvent(self, finishMoveEvent)
+
+                # Convert the move event into a drag
+                drag = QtGui.QDrag(self)
+                mimeData = QtCore.QMimeData()
+                # mimeData.setData('action', 'application/tab-detach')
+                drag.setMimeData(mimeData)
+                # screen = QScreen(self.parentWidget().currentWidget().winId())
+                # Create the appearance of dragging the tab content
+                try:
+                    pixmap = self.parent().widget(self.tabAt(self.dragStartPos)).grab()
+                except Exception as e:
+                    log.debug("GUIElements.FCDetachable. FCTabBar.mouseMoveEvent() --> %s" % str(e))
+                    return
+
+                targetPixmap = QtGui.QPixmap(pixmap.size())
+                targetPixmap.fill(QtCore.Qt.transparent)
+                painter = QtGui.QPainter(targetPixmap)
+                painter.setOpacity(0.85)
+                painter.drawPixmap(0, 0, pixmap)
+                painter.end()
+                drag.setPixmap(targetPixmap)
+
+                # Initiate the drag
+                dropAction = drag.exec_(QtCore.Qt.MoveAction | QtCore.Qt.CopyAction)
+
+
+                # For Linux:  Here, drag.exec_() will not return MoveAction on Linux.  So it
+                #             must be set manually
+                if self.dragDropedPos.x() != 0 and self.dragDropedPos.y() != 0:
+                    dropAction = QtCore.Qt.MoveAction
+
+
+                # If the drag completed outside of the tab bar, detach the tab and move
+                # the content to the current cursor position
+                if dropAction == QtCore.Qt.IgnoreAction:
+                    event.accept()
+                    self.onDetachTabSignal.emit(self.tabAt(self.dragStartPos), self.mouseCursor.pos())
+
+                # Else if the drag completed inside the tab bar, move the selected tab to the new position
+                elif dropAction == QtCore.Qt.MoveAction:
+                    if not self.dragDropedPos.isNull():
+                        event.accept()
+                        self.onMoveTabSignal.emit(self.tabAt(self.dragStartPos), self.tabAt(self.dragDropedPos))
+            else:
+                QtWidgets.QTabBar.mouseMoveEvent(self, event)
+
+        #  Determine if the drag has entered a tab position from another tab position
+        #
+        #  @param    event    a drag enter event
+        def dragEnterEvent(self, event):
+            mimeData = event.mimeData()
+            # formats = mcd imeData.formats()
+
+        # if formats.contains('action') and mimeData.data('action') == 'application/tab-detach':
+        # event.acceptProposedAction()
+
+            QtWidgets.QTabBar.dragMoveEvent(self, event)
+
+        #  Get the position of the end of the drag
+        #
+        #  @param    event    a drop event
+        def dropEvent(self, event):
+            self.dragDropedPos = event.pos()
+            QtWidgets.QTabBar.dropEvent(self, event)
+
+        #  Determine if the detached tab drop event occurred on an existing tab,
+        #  then send the event to the DetachableTabWidget
+        def detachedTabDrop(self, name, dropPos):
+
+            tabDropPos = self.mapFromGlobal(dropPos)
+
+            index = self.tabAt(tabDropPos)
+
+            self.detachedTabDropSignal.emit(name, index, dropPos)
+
+
 class VerticalScrollArea(QtWidgets.QScrollArea):
 class VerticalScrollArea(QtWidgets.QScrollArea):
     """
     """
     This widget extends QtGui.QScrollArea to make a vertical-only
     This widget extends QtGui.QScrollArea to make a vertical-only

+ 104 - 75
ObjectCollection.py

@@ -46,57 +46,66 @@ class KeySensitiveListView(QtWidgets.QTreeView):
             event.ignore()
             event.ignore()
 
 
     def dragMoveEvent(self, event):
     def dragMoveEvent(self, event):
+        self.setDropIndicatorShown(True)
         if event.mimeData().hasUrls:
         if event.mimeData().hasUrls:
             event.accept()
             event.accept()
         else:
         else:
             event.ignore()
             event.ignore()
 
 
     def dropEvent(self, event):
     def dropEvent(self, event):
-        if event.mimeData().hasUrls:
-            event.setDropAction(QtCore.Qt.CopyAction)
-            event.accept()
-            for url in event.mimeData().urls():
-                self.filename = str(url.toLocalFile())
-
-            if self.filename == "":
-                self.app.inform.emit("Open cancelled.")
-            else:
-                if self.filename.lower().rpartition('.')[-1] in self.app.grb_list:
-                    self.app.worker_task.emit({'fcn': self.app.open_gerber,
-                                               'params': [self.filename]})
-                else:
-                    event.ignore()
+        drop_indicator = self.dropIndicatorPosition()
 
 
-                if self.filename.lower().rpartition('.')[-1] in self.app.exc_list:
-                    self.app.worker_task.emit({'fcn': self.app.open_excellon,
-                                               'params': [self.filename]})
-                else:
-                    event.ignore()
-
-                if self.filename.lower().rpartition('.')[-1] in self.app.gcode_list:
-                    self.app.worker_task.emit({'fcn': self.app.open_gcode,
-                                               'params': [self.filename]})
-                else:
-                    event.ignore()
-
-                if self.filename.lower().rpartition('.')[-1] in self.app.svg_list:
-                    object_type = 'geometry'
-                    self.app.worker_task.emit({'fcn': self.app.import_svg,
-                                               'params': [self.filename, object_type, None]})
+        m = event.mimeData()
+        if m.hasUrls:
+            event.accept()
 
 
-                if self.filename.lower().rpartition('.')[-1] in self.app.dxf_list:
-                    object_type = 'geometry'
-                    self.app.worker_task.emit({'fcn': self.app.import_dxf,
-                                               'params': [self.filename, object_type, None]})
+            for url in m.urls():
+                self.filename = str(url.toLocalFile())
 
 
-                if self.filename.lower().rpartition('.')[-1] in self.app.prj_list:
-                    # self.app.open_project() is not Thread Safe
-                    self.app.open_project(self.filename)
+            # file drop from outside application
+            if drop_indicator == QtWidgets.QAbstractItemView.OnItem:
+                if self.filename == "":
+                    self.app.inform.emit("Open cancelled.")
                 else:
                 else:
-                    event.ignore()
+                    if self.filename.lower().rpartition('.')[-1] in self.app.grb_list:
+                        self.app.worker_task.emit({'fcn': self.app.open_gerber,
+                                                   'params': [self.filename]})
+                    else:
+                        event.ignore()
+
+                    if self.filename.lower().rpartition('.')[-1] in self.app.exc_list:
+                        self.app.worker_task.emit({'fcn': self.app.open_excellon,
+                                                   'params': [self.filename]})
+                    else:
+                        event.ignore()
+
+                    if self.filename.lower().rpartition('.')[-1] in self.app.gcode_list:
+                        self.app.worker_task.emit({'fcn': self.app.open_gcode,
+                                                   'params': [self.filename]})
+                    else:
+                        event.ignore()
+
+                    if self.filename.lower().rpartition('.')[-1] in self.app.svg_list:
+                        object_type = 'geometry'
+                        self.app.worker_task.emit({'fcn': self.app.import_svg,
+                                                   'params': [self.filename, object_type, None]})
+
+                    if self.filename.lower().rpartition('.')[-1] in self.app.dxf_list:
+                        object_type = 'geometry'
+                        self.app.worker_task.emit({'fcn': self.app.import_dxf,
+                                                   'params': [self.filename, object_type, None]})
+
+                    if self.filename.lower().rpartition('.')[-1] in self.app.prj_list:
+                        # self.app.open_project() is not Thread Safe
+                        self.app.open_project(self.filename)
+                    else:
+                        event.ignore()
+            else:
+                pass
         else:
         else:
             event.ignore()
             event.ignore()
 
 
+
 class TreeItem:
 class TreeItem:
     """
     """
     Item of a tree model
     Item of a tree model
@@ -221,9 +230,15 @@ class ObjectCollection(QtCore.QAbstractItemModel):
 
 
         ### View
         ### View
         self.view = KeySensitiveListView(app)
         self.view = KeySensitiveListView(app)
-        self.view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
         self.view.setModel(self)
         self.view.setModel(self)
+
         self.view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
         self.view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+        self.view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+        # self.view.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
+        # self.view.setDragEnabled(True)
+        # self.view.setAcceptDrops(True)
+        # self.view.setDropIndicatorShown(True)
+
         font = QtGui.QFont()
         font = QtGui.QFont()
         font.setPixelSize(12)
         font.setPixelSize(12)
         font.setFamily("Seagoe UI")
         font.setFamily("Seagoe UI")
@@ -273,6 +288,11 @@ class ObjectCollection(QtCore.QAbstractItemModel):
 
 
             if key == QtCore.Qt.Key_S:
             if key == QtCore.Qt.Key_S:
                 self.app.on_file_saveproject()
                 self.app.on_file_saveproject()
+
+            # Toggle Plot Area
+            if key == QtCore.Qt.Key_F10:
+                self.app.on_toggle_plotarea()
+
             return
             return
         elif modifiers == QtCore.Qt.ShiftModifier:
         elif modifiers == QtCore.Qt.ShiftModifier:
 
 
@@ -324,7 +344,20 @@ class ObjectCollection(QtCore.QAbstractItemModel):
             if key == QtCore.Qt.Key_Y:
             if key == QtCore.Qt.Key_Y:
                 self.app.on_skewy()
                 self.app.on_skewy()
                 return
                 return
+
         elif modifiers == QtCore.Qt.AltModifier:
         elif modifiers == QtCore.Qt.AltModifier:
+            # Eanble all plots
+            if key == Qt.Key_1:
+                self.app.enable_all_plots()
+
+            # Disable all plots
+            if key == Qt.Key_2:
+                self.app.disable_all_plots()
+
+            # Disable all other plots
+            if key == Qt.Key_3:
+                self.app.disable_other_plots()
+
             # 2-Sided PCB Tool
             # 2-Sided PCB Tool
             if key == QtCore.Qt.Key_D:
             if key == QtCore.Qt.Key_D:
                 self.app.dblsidedtool.run()
                 self.app.dblsidedtool.run()
@@ -354,17 +387,17 @@ class ObjectCollection(QtCore.QAbstractItemModel):
             if key == QtCore.Qt.Key_F2:
             if key == QtCore.Qt.Key_F2:
                 webbrowser.open(self.app.video_url)
                 webbrowser.open(self.app.video_url)
 
 
-            # Zoom Fit
+            # Switch to Project Tab
             if key == QtCore.Qt.Key_1:
             if key == QtCore.Qt.Key_1:
-                self.app.on_zoom_fit(None)
+                self.app.on_select_tab('project')
 
 
-            # Zoom In
+            # Switch to Selected Tab
             if key == QtCore.Qt.Key_2:
             if key == QtCore.Qt.Key_2:
-                self.app.plotcanvas.zoom(1 / self.app.defaults['zoom_ratio'], self.app.mouse)
+                self.app.on_select_tab('selected')
 
 
-            # Zoom Out
+            # Switch to Tool Tab
             if key == QtCore.Qt.Key_3:
             if key == QtCore.Qt.Key_3:
-                self.app.plotcanvas.zoom(self.app.defaults['zoom_ratio'], self.app.mouse)
+                self.app.on_select_tab('tool')
 
 
             # Delete
             # Delete
             if key == QtCore.Qt.Key_Delete and active:
             if key == QtCore.Qt.Key_Delete and active:
@@ -444,6 +477,14 @@ class ObjectCollection(QtCore.QAbstractItemModel):
             if key == QtCore.Qt.Key_Y:
             if key == QtCore.Qt.Key_Y:
                 self.app.on_flipy()
                 self.app.on_flipy()
 
 
+            # Zoom In
+            if key == QtCore.Qt.Key_Equal:
+                self.app.plotcanvas.zoom(1 / self.app.defaults['zoom_ratio'], self.app.mouse)
+
+            # Zoom Out
+            if key == QtCore.Qt.Key_Minus:
+                self.app.plotcanvas.zoom(self.app.defaults['zoom_ratio'], self.app.mouse)
+
             # Show shortcut list
             # Show shortcut list
             if key == QtCore.Qt.Key_Ampersand:
             if key == QtCore.Qt.Key_Ampersand:
                 self.app.on_shortcut_list()
                 self.app.on_shortcut_list()
@@ -483,13 +524,13 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         if not self.hasIndex(row, column, parent):
         if not self.hasIndex(row, column, parent):
             return QtCore.QModelIndex()
             return QtCore.QModelIndex()
 
 
-        if not parent.isValid():
-            parent_item = self.root_item
-        else:
-            parent_item = parent.internalPointer()
+        # if not parent.isValid():
+        #     parent_item = self.root_item
+        # else:
+        #     parent_item = parent.internalPointer()
+        parent_item = parent.internalPointer() if parent.isValid() else self.root_item
 
 
         child_item = parent_item.child(row)
         child_item = parent_item.child(row)
-
         if child_item:
         if child_item:
             return self.createIndex(row, column, child_item)
             return self.createIndex(row, column, child_item)
         else:
         else:
@@ -569,39 +610,27 @@ class ObjectCollection(QtCore.QAbstractItemModel):
                         "setData() --> Could not remove the old object name from auto-completer model list")
                         "setData() --> Could not remove the old object name from auto-completer model list")
 
 
                 obj.build_ui()
                 obj.build_ui()
-            self.app.inform.emit("Object renamed from %s to %s" % (old_name, new_name))
+                self.app.inform.emit("Object renamed from %s to %s" % (old_name, new_name))
 
 
         return True
         return True
 
 
+    def supportedDropActions(self):
+        return Qt.MoveAction
+
     def flags(self, index):
     def flags(self, index):
+        default_flags = QtCore.QAbstractItemModel.flags(self, index)
+
         if not index.isValid():
         if not index.isValid():
-            return 0
+            return Qt.ItemIsEnabled | default_flags
 
 
         # Prevent groups from selection
         # Prevent groups from selection
         if not index.internalPointer().obj:
         if not index.internalPointer().obj:
             return Qt.ItemIsEnabled
             return Qt.ItemIsEnabled
         else:
         else:
-            return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
-
-        return QtWidgets.QAbstractItemModel.flags(self, index)
-
-    # def data(self, index, role=Qt.Qt.DisplayRole):
-    #     if not index.isValid() or not 0 <= index.row() < self.rowCount():
-    #         return QtCore.QVariant()
-    #     row = index.row()
-    #     if role == Qt.Qt.DisplayRole:
-    #         return self.object_list[row].options["name"]
-    #     if role == Qt.Qt.DecorationRole:
-    #         return self.icons[self.object_list[row].kind]
-    #     # if role == Qt.Qt.CheckStateRole:
-    #     #     if row in self.checked_indexes:
-    #     #         return Qt.Qt.Checked
-    #     #     else:
-    #     #         return Qt.Qt.Unchecked
-
-    def print_list(self):
-        for obj in self.get_list():
-            print(obj)
+            return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable | \
+                   Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled
+
+        # return QtWidgets.QAbstractItemModel.flags(self, index)
 
 
     def append(self, obj, active=False):
     def append(self, obj, active=False):
         FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> OC.append()")
         FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> OC.append()")
@@ -611,8 +640,8 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         # Check promises and clear if exists
         # Check promises and clear if exists
         if name in self.promises:
         if name in self.promises:
             self.promises.remove(name)
             self.promises.remove(name)
-            FlatCAMApp.App.log.debug("Promised object %s became available." % name)
-            FlatCAMApp.App.log.debug("%d promised objects remaining." % len(self.promises))
+            # FlatCAMApp.App.log.debug("Promised object %s became available." % name)
+            # FlatCAMApp.App.log.debug("%d promised objects remaining." % len(self.promises))
         # Prevent same name
         # Prevent same name
         while name in self.get_names():
         while name in self.get_names():
             ## Create a new name
             ## Create a new name

+ 76 - 15
ObjectUI.py

@@ -556,15 +556,38 @@ class ExcellonObjectUI(ObjectUI):
         self.ois_dwell = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
         self.ois_dwell = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
 
 
         # postprocessor selection
         # postprocessor selection
-        pp_excellon_label = QtWidgets.QLabel("Postprocessor")
+        pp_excellon_label = QtWidgets.QLabel("Postprocessor:")
         pp_excellon_label.setToolTip(
         pp_excellon_label.setToolTip(
             "The json file that dictates\n"
             "The json file that dictates\n"
             "gcode output."
             "gcode output."
         )
         )
-        self.tools_box.addWidget(pp_excellon_label)
         self.pp_excellon_name_cb = FCComboBox()
         self.pp_excellon_name_cb = FCComboBox()
         self.pp_excellon_name_cb.setFocusPolicy(QtCore.Qt.StrongFocus)
         self.pp_excellon_name_cb.setFocusPolicy(QtCore.Qt.StrongFocus)
-        self.tools_box.addWidget(self.pp_excellon_name_cb)
+        grid1.addWidget(pp_excellon_label, 10, 0)
+        grid1.addWidget(self.pp_excellon_name_cb, 10, 1)
+
+        # Probe depth
+        self.pdepth_label = QtWidgets.QLabel("Probe Z depth:")
+        self.pdepth_label.setToolTip(
+            "The maximum depth that the probe is allowed\n"
+            "to probe. Negative value, in current units."
+        )
+        grid1.addWidget(self.pdepth_label, 11, 0)
+        self.pdepth_entry = FCEntry()
+        grid1.addWidget(self.pdepth_entry, 11, 1)
+        self.pdepth_label.hide()
+        self.pdepth_entry.setVisible(False)
+
+        # Probe feedrate
+        self.feedrate_probe_label = QtWidgets.QLabel("Feedrate Probe:")
+        self.feedrate_probe_label.setToolTip(
+            "The feedrate used while the probe is probing."
+        )
+        grid1.addWidget(self.feedrate_probe_label, 12, 0)
+        self.feedrate_probe_entry = FCEntry()
+        grid1.addWidget(self.feedrate_probe_entry, 12, 1)
+        self.feedrate_probe_label.hide()
+        self.feedrate_probe_entry.setVisible(False)
 
 
         choose_tools_label = QtWidgets.QLabel(
         choose_tools_label = QtWidgets.QLabel(
             "Select from the Tools Table above\n"
             "Select from the Tools Table above\n"
@@ -708,6 +731,8 @@ class GeometryObjectUI(ObjectUI):
         self.geo_tools_table.setColumnWidth(0, 20)
         self.geo_tools_table.setColumnWidth(0, 20)
         self.geo_tools_table.setHorizontalHeaderLabels(['#', 'Dia', 'Offset', 'Type', 'TT', '', 'P'])
         self.geo_tools_table.setHorizontalHeaderLabels(['#', 'Dia', 'Offset', 'Type', 'TT', '', 'P'])
         self.geo_tools_table.setColumnHidden(5, True)
         self.geo_tools_table.setColumnHidden(5, True)
+        # stylesheet = "::section{Background-color:rgb(239,239,245)}"
+        # self.geo_tools_table.horizontalHeader().setStyleSheet(stylesheet)
 
 
         self.geo_tools_table.horizontalHeaderItem(0).setToolTip(
         self.geo_tools_table.horizontalHeaderItem(0).setToolTip(
             "This is the Tool Number.\n"
             "This is the Tool Number.\n"
@@ -758,7 +783,7 @@ class GeometryObjectUI(ObjectUI):
             "cut and negative for 'inside' cut."
             "cut and negative for 'inside' cut."
         )
         )
         self.grid1.addWidget(self.tool_offset_lbl, 0, 0)
         self.grid1.addWidget(self.tool_offset_lbl, 0, 0)
-        self.tool_offset_entry = FloatEntry()
+        self.tool_offset_entry = FCEntry()
         spacer_lbl = QtWidgets.QLabel(" ")
         spacer_lbl = QtWidgets.QLabel(" ")
         spacer_lbl.setFixedWidth(80)
         spacer_lbl.setFixedWidth(80)
 
 
@@ -777,7 +802,7 @@ class GeometryObjectUI(ObjectUI):
         self.addtool_entry_lbl.setToolTip(
         self.addtool_entry_lbl.setToolTip(
             "Diameter for the new tool"
             "Diameter for the new tool"
         )
         )
-        self.addtool_entry = FloatEntry()
+        self.addtool_entry = FCEntry()
 
 
         # hlay.addWidget(self.addtool_label)
         # hlay.addWidget(self.addtool_label)
         # hlay.addStretch()
         # hlay.addStretch()
@@ -1004,11 +1029,34 @@ class GeometryObjectUI(ObjectUI):
         self.pp_geometry_name_cb.setFocusPolicy(QtCore.Qt.StrongFocus)
         self.pp_geometry_name_cb.setFocusPolicy(QtCore.Qt.StrongFocus)
         self.grid3.addWidget(self.pp_geometry_name_cb, 16, 1)
         self.grid3.addWidget(self.pp_geometry_name_cb, 16, 1)
 
 
+        # Probe depth
+        self.pdepth_label = QtWidgets.QLabel("Probe Z depth:")
+        self.pdepth_label.setToolTip(
+            "The maximum depth that the probe is allowed\n"
+            "to probe. Negative value, in current units."
+        )
+        self.grid3.addWidget(self.pdepth_label, 17, 0)
+        self.pdepth_entry = FCEntry()
+        self.grid3.addWidget(self.pdepth_entry, 17, 1)
+        self.pdepth_label.hide()
+        self.pdepth_entry.setVisible(False)
+
+        # Probe feedrate
+        self.feedrate_probe_label = QtWidgets.QLabel("Feedrate Probe:")
+        self.feedrate_probe_label.setToolTip(
+            "The feedrate used while the probe is probing."
+        )
+        self.grid3.addWidget(self.feedrate_probe_label, 18, 0)
+        self.feedrate_probe_entry = FCEntry()
+        self.grid3.addWidget(self.feedrate_probe_entry, 18, 1)
+        self.feedrate_probe_label.hide()
+        self.feedrate_probe_entry.setVisible(False)
+
         warning_lbl = QtWidgets.QLabel(
         warning_lbl = QtWidgets.QLabel(
             "Add at least one tool in the tool-table.\n"
             "Add at least one tool in the tool-table.\n"
             "Click the header to select all, or Ctrl + LMB\n"
             "Click the header to select all, or Ctrl + LMB\n"
             "for custom selection of tools.")
             "for custom selection of tools.")
-        self.grid3.addWidget(warning_lbl, 17, 0, 1, 2)
+        self.grid3.addWidget(warning_lbl, 19, 0, 1, 2)
 
 
         # Button
         # Button
         self.generate_cnc_button = QtWidgets.QPushButton('Generate')
         self.generate_cnc_button = QtWidgets.QPushButton('Generate')
@@ -1067,15 +1115,26 @@ class CNCObjectUI(ObjectUI):
         self.plot_options_label = QtWidgets.QLabel("<b>Plot Options:</b>")
         self.plot_options_label = QtWidgets.QLabel("<b>Plot Options:</b>")
         self.custom_box.addWidget(self.plot_options_label)
         self.custom_box.addWidget(self.plot_options_label)
 
 
-        # # Tool dia for plot
-        # tdlabel = QtWidgets.QLabel('Tool dia:')
-        # tdlabel.setToolTip(
-        #     "Diameter of the tool to be\n"
-        #     "rendered in the plot."
-        # )
-        # grid0.addWidget(tdlabel, 1, 0)
-        # self.tooldia_entry = LengthEntry()
-        # grid0.addWidget(self.tooldia_entry, 1, 1)
+        self.cncplot_method_label = QtWidgets.QLabel("Plot kind:")
+        self.cncplot_method_label.setToolTip(
+            "This selects the kind of geometries on the canvas to plot.\n"
+            "Those can be either of type 'Travel' which means the moves\n"
+            "above the work piece or it can be of type 'Cut',\n"
+            "which means the moves that cut into the material."
+        )
+
+        self.cncplot_method_combo = RadioSet([
+            {"label": "All", "value": "all"},
+            {"label": "Travel", "value": "travel"},
+            {"label": "Cut", "value": "cut"}
+        ], stretch=False)
+
+        f_lay = QtWidgets.QFormLayout()
+        self.custom_box.addLayout(f_lay)
+        f_lay.addRow(self.cncplot_method_label, self.cncplot_method_combo)
+
+        e1_lbl = QtWidgets.QLabel('')
+        self.custom_box.addWidget(e1_lbl)
 
 
         hlay = QtWidgets.QHBoxLayout()
         hlay = QtWidgets.QHBoxLayout()
         self.custom_box.addLayout(hlay)
         self.custom_box.addLayout(hlay)
@@ -1115,6 +1174,8 @@ class CNCObjectUI(ObjectUI):
         self.cnc_tools_table.setColumnWidth(0, 20)
         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)
         self.cnc_tools_table.setColumnHidden(5, True)
+        # stylesheet = "::section{Background-color:rgb(239,239,245)}"
+        # self.cnc_tools_table.horizontalHeader().setStyleSheet(stylesheet)
 
 
         # Update plot button
         # Update plot button
         self.updateplot_button = QtWidgets.QPushButton('Update Plot')
         self.updateplot_button = QtWidgets.QPushButton('Update Plot')

+ 2 - 2
ParseFont.py

@@ -287,8 +287,8 @@ class ParseFont():
             elif font_type == 'regular':
             elif font_type == 'regular':
                 path_filename = regular_dict[font_name]
                 path_filename = regular_dict[font_name]
         except Exception as e:
         except Exception as e:
-            self.app.inform.emit("[error_notcl] Font not supported, try another one.")
-            log.debug("[error_notcl] Font Loading: %s" % str(e))
+            self.app.inform.emit("[ERROR_NOTCL] Font not supported, try another one.")
+            log.debug("[ERROR_NOTCL] Font Loading: %s" % str(e))
             return "flatcam font parse failed"
             return "flatcam font parse failed"
 
 
         face = ft.Face(path_filename)
         face = ft.Face(path_filename)

+ 2 - 2
ParseSVG.py

@@ -121,7 +121,7 @@ def path2shapely(path, object_type, res=1.0):
                 # geo_element = Polygon(points)
                 # geo_element = Polygon(points)
                 geo_element = LineString(points)
                 geo_element = LineString(points)
             else:
             else:
-                log.error("[error]: Not a valid target object.")
+                log.error("[ERROR]: Not a valid target object.")
             if not points:
             if not points:
                 continue
                 continue
             else:
             else:
@@ -639,7 +639,7 @@ def parse_svg_transform(trstr):
             continue
             continue
 
 
         # raise Exception("Don't know how to parse: %s" % trstr)
         # raise Exception("Don't know how to parse: %s" % trstr)
-        log.error("[error] Don't know how to parse: %s" % trstr)
+        log.error("[ERROR] Don't know how to parse: %s" % trstr)
 
 
     return trlist
     return trlist
 
 

+ 85 - 1
README.md

@@ -9,9 +9,93 @@ CAD program, and create G-Code for Isolation routing.
 
 
 =================================================
 =================================================
 
 
+6.02.2019
+
+- fixed the units calculators crash FlatCAM when using comma as decimal separator
+
+5.02.3019
+
+- added a text in the Selected Tab which is showed whenever the Selected Tab is selected but without having an object selected to display it's properties
+- added an initial text in the Tools tab
+- added possibility to use the shortcut key for shortcut list in the Notebook tabs
+- added a way to set the Probe depth if Toolchange_Probe postprocessors are selected
+- finished the postprocessor file for MACH3 tool probing on toolchange event
+- added a new parameter to set the feedrate of the probing in case the used postprocessor does probing (has toolchange_probe in it's name)
+- fixed bug in Marlin postprocessor for the Excellon files; the header and toolchange event always used the parenthesis witch is not compatible with GCode for Marlin
+- fixed a issue with a move to Z_move before any toolchange
+
+4.02.2019
+
+- modified the Toolchange_Probe_general postprocessor file to remove any Z moves before the actual toolchange event
+- created a prototype postprocessor file for usage with tool probing in MACH3
+- added the default values for Tool Film and Tool Panelize to the Edit -> Preferences
+- added a new parameter in the Tool Film which control the thickness of the stroke width in the resulting SVG. It's a scale parameter.
+- whatever was the visibility of the corresponding toolbar when we enter in the Editor, it will be set after exit from the Editor (either Geometry Editor or Excellon Editor).
+- added ability to be detached for the tabs in the Notebook section (Project, Selected and Tool)
+- added ability for all detachable tabs to be restored to the same position from where they were detached.
+- changed the shortcut keys for Zoom In, Zoom Out and Zoom Fit from 1, 2, 3 to '-', '=' respectively 'V'. Added new shortcut keys '1', '2', '3' for Select Project Tab, Select Selected Tab and Select Tool Tab.
+- formatted the Shortcut List Tab into a HTML table
+
+3.3.2019
+
+- updated the new shortcut list with the shortcuts added lately
+- now the special messages in the Shell are color coded according to the level. Before they all were RED. Now the WARNINGS are yellow, ERRORS are red and SUCCESS is a dark green. Also the level is in CAPS LOCK to make them more obvious
+- some more changes to GUI interface (solved issues)
+- added some status bar messages in the Geometry Editor to guide the user when using the Geometry Tools
+- now the '`' shortcut key that shows the 'shortcut key list' in Editors points to the same window which is created in a tab no longer as a pop-up window. This tab can be detached if needed.
+- added a remove_tools() function before install_tools() in the init_tools() that is called when creating a new project. Should solve the issue with having double menu entry's in the TOOLS menu
+- fixed remove_tools() so the Tcl Shell action is readded to the Tools menu and reconnected to it's slot function
+- added an automatic name on each save operation based on the object name and/or the current date
+- added more information's for the statistics
+
+2.2.2019
+
+- code cleanup in Tools
+- some GUI structure optimization's
+- added protection against entering float numbers with comma separator instead of decimal dot separator in key points of FlatCAM (not everywhere)
+- added a choice of plotting the kind of geometry for the CNC plot (all, travel and cut kind of geometries) in CNCJob Selected Tab
+- added a new postprocessor file named: 'probe_from_zmove' which allow probing to be done from z_move position on toolchange event 
+- fixed the snap magnet button in Geometry Editor, restored the checkable property to True
+- some more changes in the Editors GUI in deactivate() function
+- a fix for saving as empty an edited new and empty Excellon Object
+
+1.02.2019
+
+- fixed postprocessor files so now the bounds values are right aligned (assuming max string length of 9 chars which means 4 digits and 4 decimals)
+- corrected small type in list_sys Tcl command; added a protection of the Plot Area Tab after a successful edit.
+- remade the way FlatCAM saves the GUI position data from a file (previously) to use PyQt QSettings
+- added a 'theme' combo selection in Edit -> Preferences. Two themes are available: standard and compact.
+- some code cleanup
+- fixed a source of possible errors in DetachableTab Widget.
+- fixed gcode conversion/scale (on units change) when multiple values are found on each line
+- replaced the pop-up window for the shortcut list with a new detachable tab
+- removed the pop-up messages from the rotate, skew, flip commands
+
+31.01.2019
+
+- added a parameter ('Fast plunge' in Edit -> Preferences -> Geometry Options and Excellon Options) to control if the fast move to Z_move is done or not
+- added new function to toggle fullscreen status in Menu -> View -> Toggle Full Screen. Shortcut key: Alt+F10
+- added key shortcuts for Enable Plots, Disable Plots and Disable other plots functions (Alt+1, Alt+2, Alt+3)
+- hidden the snap magnet entry and snap magnet toggle from the main view; they are now active only in Editor Mode
+- updated the camlib.CNCJob.scale() function so now the GCode is scaled also (quite a HACK :( it will need to be replaced at some point)). Units change work now on the GCODE also.
+- added the bounds coordinates to the GCODE header
+- FlatCAM saves now to a file in self.data_path the toolbar positions and the position of TCL Shell
+- Plot Area Tab view can now be toggled, added entry in View Menu and shortcut key CTRL+F10
+- All the tabs in the GUI right side are (Plot Are, Preferences etc) are now detachable to a separate windows which when closed it returns in the previous location in the toolbar. Those detached tabs can be also reattached by drag and drop.
+
 30.01.2019
 30.01.2019
 
 
 - added a space before Y coordinate in end_code() function in some of the postprocessor files
 - added a space before Y coordinate in end_code() function in some of the postprocessor files
+- added in Calculators Tool an Electroplating Calculator.
+- remade the App Menu for Editors: now they will be showed only when the respective Editor is active and hidden when the Editor is closed.
+- added a traceback report in the TCL Shell for the errors that don't allow creation of an object; useful to trace exceptions/errors
+- in case that the Toolchange X,Y parameter in Selected (or in Preferences) are deleted then the app will still do the job using the current coordinates for toolchange
+- fixed an issue in camlib.CNCJob where tha variable self.toolchange_xy was used for 2 different purposes which created loss of information.
+- fixed unit conversion functions in case the toolchange_xy parameter is None
+- more fixes in camlib.CNCJob regarding usage of toolchange (in case it is None)
+- fixed postprocessor files to work with toolchange_xy parameter value = None (no values in Edit - Preferences fields)
+- fixed Tcl commands CncJob and DrillCncJob to work with toolchange
+- added to the postprocessor files the command after toolchange to go with G00 (fastest) to "Z Move" value of Z pozition.
 
 
 29.01.2019
 29.01.2019
 
 
@@ -243,7 +327,7 @@ CAD program, and create G-Code for Isolation routing.
 - solved a small bug that didn't allow the Paint Job to be done with lines when the results were geometries not iterable 
 - solved a small bug that didn't allow the Paint Job to be done with lines when the results were geometries not iterable 
 - added protection for the case when trying to run the cncjob Tcl Command on a Geometry object that do not have solid geometry or one that is multi-tool
 - added protection for the case when trying to run the cncjob Tcl Command on a Geometry object that do not have solid geometry or one that is multi-tool
 - Paint Tool Table: now it is possible to edit a tool to a new diameter and then edit another tool to the former diameter of the first edited tool
 - Paint Tool Table: now it is possible to edit a tool to a new diameter and then edit another tool to the former diameter of the first edited tool
-- added a new type of warning, [warning_notcl]
+- added a new type of warning, [WARNING_NOTCL]
 - fixed conflict with "space" keyboard shortcut for CNC job
 - fixed conflict with "space" keyboard shortcut for CNC job
 
 
 16.12.2018
 16.12.2018

+ 310 - 82
camlib.py

@@ -186,7 +186,7 @@ class Geometry(object):
         if isinstance(self.solid_geometry, list):
         if isinstance(self.solid_geometry, list):
             return len(self.solid_geometry) == 0
             return len(self.solid_geometry) == 0
 
 
-        self.app.inform.emit("[error_notcl] self.solid_geometry is neither BaseGeometry or list.")
+        self.app.inform.emit("[ERROR_NOTCL] self.solid_geometry is neither BaseGeometry or list.")
         return
         return
 
 
     def subtract_polygon(self, points):
     def subtract_polygon(self, points):
@@ -300,7 +300,7 @@ class Geometry(object):
         #     else:
         #     else:
         #         return self.solid_geometry.bounds
         #         return self.solid_geometry.bounds
         # except Exception as e:
         # except Exception as e:
-        #     self.app.inform.emit("[error_notcl] Error cause: %s" % str(e))
+        #     self.app.inform.emit("[ERROR_NOTCL] Error cause: %s" % str(e))
 
 
         # log.debug("Geometry->bounds()")
         # log.debug("Geometry->bounds()")
         # if self.solid_geometry is None:
         # if self.solid_geometry is None:
@@ -1361,7 +1361,7 @@ class Geometry(object):
                 self.solid_geometry = mirror_geom(self.solid_geometry)
                 self.solid_geometry = mirror_geom(self.solid_geometry)
             self.app.inform.emit('[success]Object was mirrored ...')
             self.app.inform.emit('[success]Object was mirrored ...')
         except AttributeError:
         except AttributeError:
-            self.app.inform.emit("[error_notcl] Failed to mirror. No object selected")
+            self.app.inform.emit("[ERROR_NOTCL] Failed to mirror. No object selected")
 
 
 
 
 
 
@@ -1401,7 +1401,7 @@ class Geometry(object):
                 self.solid_geometry = rotate_geom(self.solid_geometry)
                 self.solid_geometry = rotate_geom(self.solid_geometry)
             self.app.inform.emit('[success]Object was rotated ...')
             self.app.inform.emit('[success]Object was rotated ...')
         except AttributeError:
         except AttributeError:
-            self.app.inform.emit("[error_notcl] Failed to rotate. No object selected")
+            self.app.inform.emit("[ERROR_NOTCL] Failed to rotate. No object selected")
 
 
     def skew(self, angle_x, angle_y, point):
     def skew(self, angle_x, angle_y, point):
         """
         """
@@ -1437,7 +1437,7 @@ class Geometry(object):
                 self.solid_geometry = skew_geom(self.solid_geometry)
                 self.solid_geometry = skew_geom(self.solid_geometry)
             self.app.inform.emit('[success]Object was skewed ...')
             self.app.inform.emit('[success]Object was skewed ...')
         except AttributeError:
         except AttributeError:
-            self.app.inform.emit("[error_notcl] Failed to skew. No object selected")
+            self.app.inform.emit("[ERROR_NOTCL] Failed to skew. No object selected")
 
 
         # if type(self.solid_geometry) == list:
         # if type(self.solid_geometry) == list:
         #     self.solid_geometry = [affinity.skew(g, angle_x, angle_y, origin=(px, py))
         #     self.solid_geometry = [affinity.skew(g, angle_x, angle_y, origin=(px, py))
@@ -2454,9 +2454,11 @@ class Gerber (Geometry):
                         region = Polygon()
                         region = Polygon()
                     else:
                     else:
                         region = Polygon(path)
                         region = Polygon(path)
+
                     if not region.is_valid:
                     if not region.is_valid:
                         if not follow:
                         if not follow:
                             region = region.buffer(0, int(self.steps_per_circle / 4))
                             region = region.buffer(0, int(self.steps_per_circle / 4))
+
                     if not region.is_empty:
                     if not region.is_empty:
                         poly_buffer.append(region)
                         poly_buffer.append(region)
 
 
@@ -2531,8 +2533,8 @@ class Gerber (Geometry):
                                     pass
                                     pass
                             last_path_aperture = current_aperture
                             last_path_aperture = current_aperture
                         else:
                         else:
-                            self.app.inform.emit("[warning] Coordinates missing, line ignored: %s" % str(gline))
-                            self.app.inform.emit("[warning_notcl] GERBER file might be CORRUPT. Check the file !!!")
+                            self.app.inform.emit("[WARNING] Coordinates missing, line ignored: %s" % str(gline))
+                            self.app.inform.emit("[WARNING_NOTCL] GERBER file might be CORRUPT. Check the file !!!")
 
 
                     elif current_operation_code == 2:
                     elif current_operation_code == 2:
                         if len(path) > 1:
                         if len(path) > 1:
@@ -2550,7 +2552,7 @@ class Gerber (Geometry):
                                         geo = Polygon(path)
                                         geo = Polygon(path)
                                     except ValueError:
                                     except ValueError:
                                         log.warning("Problem %s %s" % (gline, line_num))
                                         log.warning("Problem %s %s" % (gline, line_num))
-                                        self.app.inform.emit("[error] Region does not have enough points. "
+                                        self.app.inform.emit("[ERROR] Region does not have enough points. "
                                                              "File will be processed but there are parser errors. "
                                                              "File will be processed but there are parser errors. "
                                                              "Line number: %s" % str(line_num))
                                                              "Line number: %s" % str(line_num))
                             else:
                             else:
@@ -2574,8 +2576,8 @@ class Gerber (Geometry):
                         if linear_x is not None and linear_y is not None:
                         if linear_x is not None and linear_y is not None:
                             path = [[linear_x, linear_y]]  # Start new path
                             path = [[linear_x, linear_y]]  # Start new path
                         else:
                         else:
-                            self.app.inform.emit("[warning] Coordinates missing, line ignored: %s" % str(gline))
-                            self.app.inform.emit("[warning_notcl] GERBER file might be CORRUPT. Check the file !!!")
+                            self.app.inform.emit("[WARNING] Coordinates missing, line ignored: %s" % str(gline))
+                            self.app.inform.emit("[WARNING_NOTCL] GERBER file might be CORRUPT. Check the file !!!")
 
 
                     # Flash
                     # Flash
                     # Not allowed in region mode.
                     # Not allowed in region mode.
@@ -2838,6 +2840,7 @@ class Gerber (Geometry):
 
 
             if self.use_buffer_for_union:
             if self.use_buffer_for_union:
                 log.debug("Union by buffer...")
                 log.debug("Union by buffer...")
+
                 new_poly = MultiPolygon(poly_buffer)
                 new_poly = MultiPolygon(poly_buffer)
                 new_poly = new_poly.buffer(0.00000001)
                 new_poly = new_poly.buffer(0.00000001)
                 new_poly = new_poly.buffer(-0.00000001)
                 new_poly = new_poly.buffer(-0.00000001)
@@ -2857,8 +2860,9 @@ class Gerber (Geometry):
             traceback.print_tb(tb)
             traceback.print_tb(tb)
             #print traceback.format_exc()
             #print traceback.format_exc()
 
 
-            log.error("PARSING FAILED. Line %d: %s" % (line_num, gline))
-            self.app.inform.emit("[error] Gerber Parser ERROR.\n Line %d: %s" % (line_num, gline), repr(err))
+            log.error("Gerber PARSING FAILED. Line %d: %s" % (line_num, gline))
+            loc = 'Gerber Line #%d Gerber Line Content: %s\n' % (line_num, gline) + repr(err)
+            self.app.inform.emit("[ERROR]Gerber Parser ERROR.\n%s:" % loc)
 
 
     @staticmethod
     @staticmethod
     def create_flash_geometry(location, aperture, steps_per_circle=None):
     def create_flash_geometry(location, aperture, steps_per_circle=None):
@@ -3035,7 +3039,7 @@ class Gerber (Geometry):
         try:
         try:
             xfactor = float(xfactor)
             xfactor = float(xfactor)
         except:
         except:
-            self.app.inform.emit("[error_notcl] Scale factor has to be a number: integer or float.")
+            self.app.inform.emit("[ERROR_NOTCL] Scale factor has to be a number: integer or float.")
             return
             return
 
 
         if yfactor is None:
         if yfactor is None:
@@ -3044,7 +3048,7 @@ class Gerber (Geometry):
             try:
             try:
                 yfactor = float(yfactor)
                 yfactor = float(yfactor)
             except:
             except:
-                self.app.inform.emit("[error_notcl] Scale factor has to be a number: integer or float.")
+                self.app.inform.emit("[ERROR_NOTCL] Scale factor has to be a number: integer or float.")
                 return
                 return
 
 
         if point is None:
         if point is None:
@@ -3096,7 +3100,7 @@ class Gerber (Geometry):
         try:
         try:
             dx, dy = vect
             dx, dy = vect
         except TypeError:
         except TypeError:
-            self.app.inform.emit("[error_notcl]An (x,y) pair of values are needed. "
+            self.app.inform.emit("[ERROR_NOTCL]An (x,y) pair of values are needed. "
                                  "Probable you entered only one value in the Offset field.")
                                  "Probable you entered only one value in the Offset field.")
             return
             return
 
 
@@ -3460,7 +3464,7 @@ class Excellon(Geometry):
                 # and we need to exit from here
                 # and we need to exit from here
                 if self.detect_gcode_re.search(eline):
                 if self.detect_gcode_re.search(eline):
                     log.warning("This is GCODE mark: %s" % eline)
                     log.warning("This is GCODE mark: %s" % eline)
-                    self.app.inform.emit('[error_notcl] This is GCODE mark: %s' % eline)
+                    self.app.inform.emit('[ERROR_NOTCL] This is GCODE mark: %s' % eline)
                     return
                     return
 
 
                 # Header Begin (M48) #
                 # Header Begin (M48) #
@@ -3987,10 +3991,13 @@ class Excellon(Geometry):
             # from self.defaults['excellon_units']
             # from self.defaults['excellon_units']
             log.info("Zeros: %s, Units %s." % (self.zeros, self.units))
             log.info("Zeros: %s, Units %s." % (self.zeros, self.units))
 
 
-
         except Exception as e:
         except Exception as e:
-            log.error("PARSING FAILED. Line %d: %s" % (line_num, eline))
-            self.app.inform.emit('[error] Excellon Parser ERROR.\nPARSING FAILED. Line %d: %s' % (line_num, eline))
+            log.error("Excellon PARSING FAILED. Line %d: %s" % (line_num, eline))
+            msg = "[ERROR_NOTCL] An internal error has ocurred. See shell.\n"
+            msg += '[ERROR] Excellon Parser error.\nParsing Failed. Line %d: %s\n' % (line_num, eline)
+            msg += traceback.format_exc()
+            self.app.inform.emit(msg)
+
             return "fail"
             return "fail"
         
         
     def parse_number(self, number_str):
     def parse_number(self, number_str):
@@ -4059,7 +4066,7 @@ class Excellon(Geometry):
             for drill in self.drills:
             for drill in self.drills:
                 # poly = drill['point'].buffer(self.tools[drill['tool']]["C"]/2.0)
                 # poly = drill['point'].buffer(self.tools[drill['tool']]["C"]/2.0)
                 if drill['tool'] is '':
                 if drill['tool'] is '':
-                    self.app.inform.emit("[warning] Excellon.create_geometry() -> a drill location was skipped "
+                    self.app.inform.emit("[WARNING] Excellon.create_geometry() -> a drill location was skipped "
                                          "due of not having a tool associated.\n"
                                          "due of not having a tool associated.\n"
                                          "Check the resulting GCode.")
                                          "Check the resulting GCode.")
                     log.debug("Excellon.create_geometry() -> a drill location was skipped "
                     log.debug("Excellon.create_geometry() -> a drill location was skipped "
@@ -4363,11 +4370,11 @@ class CNCjob(Geometry):
     def __init__(self,
     def __init__(self,
                  units="in", kind="generic", tooldia=0.0,
                  units="in", kind="generic", tooldia=0.0,
                  z_cut=-0.002, z_move=0.1,
                  z_cut=-0.002, z_move=0.1,
-                 feedrate=3.0, feedrate_z=3.0, feedrate_rapid=3.0,
+                 feedrate=3.0, feedrate_z=3.0, feedrate_rapid=3.0, feedrate_probe=3.0,
                  pp_geometry_name='default', pp_excellon_name='default',
                  pp_geometry_name='default', pp_excellon_name='default',
-                 depthpercut = 0.1,
+                 depthpercut=0.1,z_pdepth=-0.02,
                  spindlespeed=None, dwell=True, dwelltime=1000,
                  spindlespeed=None, dwell=True, dwelltime=1000,
-                 toolchangez=0.787402,
+                 toolchangez=0.787402, toolchange_xy=[0.0, 0.0],
                  endz=2.0,
                  endz=2.0,
                  segx=None,
                  segx=None,
                  segy=None,
                  segy=None,
@@ -4392,7 +4399,8 @@ class CNCjob(Geometry):
 
 
         self.tooldia = tooldia
         self.tooldia = tooldia
         self.toolchangez = toolchangez
         self.toolchangez = toolchangez
-        self.toolchange_xy = None
+        self.toolchange_xy = toolchange_xy
+        self.toolchange_xy_type = None
 
 
         self.endz = endz
         self.endz = endz
         self.depthpercut = depthpercut
         self.depthpercut = depthpercut
@@ -4411,6 +4419,15 @@ class CNCjob(Geometry):
         self.pp_excellon_name = pp_excellon_name
         self.pp_excellon_name = pp_excellon_name
         self.pp_excellon = self.app.postprocessors[self.pp_excellon_name]
         self.pp_excellon = self.app.postprocessors[self.pp_excellon_name]
 
 
+        # Controls if the move from Z_Toolchange to Z_Move is done fast with G0 or normally with G1
+        self.f_plunge = None
+
+        # how much depth the probe can probe before error
+        self.z_pdepth = z_pdepth if z_pdepth else None
+
+        # the feedrate(speed) with which the probel travel while probing
+        self.feedrate_probe = feedrate_probe if feedrate_probe else None
+
         self.spindlespeed = spindlespeed
         self.spindlespeed = spindlespeed
         self.dwell = dwell
         self.dwell = dwell
         self.dwelltime = dwelltime
         self.dwelltime = dwelltime
@@ -4420,6 +4437,9 @@ class CNCjob(Geometry):
 
 
         self.input_geometry_bounds = None
         self.input_geometry_bounds = None
 
 
+        self.oldx = None
+        self.oldy = None
+
         # Attributes to be included in serialization
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # Always append to it because it carries contents
         # from Geometry.
         # from Geometry.
@@ -4490,7 +4510,7 @@ class CNCjob(Geometry):
         return path
         return path
 
 
     def generate_from_excellon_by_tool(self, exobj, tools="all", drillz = 3.0,
     def generate_from_excellon_by_tool(self, exobj, tools="all", drillz = 3.0,
-                                       toolchange=False, toolchangez=0.1, toolchangexy="0.0, 0.0",
+                                       toolchange=False, toolchangez=0.1, toolchangexy='',
                                        endz=2.0, startz=None,
                                        endz=2.0, startz=None,
                                        excellon_optimization_type='B'):
                                        excellon_optimization_type='B'):
         """
         """
@@ -4520,25 +4540,40 @@ class CNCjob(Geometry):
         :rtype: None
         :rtype: None
         """
         """
         if drillz > 0:
         if drillz > 0:
-            self.app.inform.emit("[warning] The Cut Z parameter has positive value. "
+            self.app.inform.emit("[WARNING] The Cut Z parameter has positive value. "
                                  "It is the depth value to drill into material.\n"
                                  "It is the depth value to drill into material.\n"
                                  "The Cut Z parameter needs to have a negative value, assuming it is a typo "
                                  "The Cut Z parameter needs to have a negative value, assuming it is a typo "
                                  "therefore the app will convert the value to negative. "
                                  "therefore the app will convert the value to negative. "
                                  "Check the resulting CNC code (Gcode etc).")
                                  "Check the resulting CNC code (Gcode etc).")
             self.z_cut = -drillz
             self.z_cut = -drillz
         elif drillz == 0:
         elif drillz == 0:
-            self.app.inform.emit("[warning] The Cut Z parameter is zero. "
+            self.app.inform.emit("[WARNING] The Cut Z parameter is zero. "
                                  "There will be no cut, skipping %s file" % exobj.options['name'])
                                  "There will be no cut, skipping %s file" % exobj.options['name'])
             return
             return
         else:
         else:
             self.z_cut = drillz
             self.z_cut = drillz
 
 
         self.toolchangez = toolchangez
         self.toolchangez = toolchangez
-        self.toolchange_xy = [float(eval(a)) for a in toolchangexy.split(",")]
+
+        try:
+            if toolchangexy == '':
+                self.toolchange_xy = None
+            else:
+                self.toolchange_xy = [float(eval(a)) for a in toolchangexy.split(",")]
+                if len(self.toolchange_xy) < 2:
+                    self.app.inform.emit("[ERROR]The Toolchange X,Y field in Edit -> Preferences has to be "
+                                         "in the format (x, y) \nbut now there is only one value, not two. ")
+                    return 'fail'
+        except Exception as e:
+            log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> %s" % str(e))
+            pass
 
 
         self.startz = startz
         self.startz = startz
         self.endz = endz
         self.endz = endz
 
 
+        self.pp_excellon = self.app.postprocessors[self.pp_excellon_name]
+        p = self.pp_excellon
+
         log.debug("Creating CNC Job from Excellon...")
         log.debug("Creating CNC Job from Excellon...")
 
 
         # Tools
         # Tools
@@ -4576,15 +4611,19 @@ class CNCjob(Geometry):
 
 
         self.gcode = []
         self.gcode = []
 
 
-        # Basic G-Code macros
-        self.pp_excellon = self.app.postprocessors[self.pp_excellon_name]
-        p = self.pp_excellon
+        self.f_plunge = self.app.defaults["excellon_f_plunge"]
 
 
         # Initialization
         # Initialization
         gcode = self.doformat(p.start_code)
         gcode = self.doformat(p.start_code)
         gcode += self.doformat(p.feedrate_code)
         gcode += self.doformat(p.feedrate_code)
-        gcode += self.doformat(p.lift_code, x=0, y=0)
-        gcode += self.doformat(p.startz_code, x=0, y=0)
+
+        if toolchange is False:
+            if self.toolchange_xy is not None:
+                gcode += self.doformat(p.lift_code, x=self.toolchange_xy[0], y=self.toolchange_xy[1])
+                gcode += self.doformat(p.startz_code, x=self.toolchange_xy[0], y=self.toolchange_xy[1])
+            else:
+                gcode += self.doformat(p.lift_code, x=0.0, y=0.0)
+                gcode += self.doformat(p.startz_code, x=0.0, y=0.0)
 
 
         # Distance callback
         # Distance callback
         class CreateDistanceCallback(object):
         class CreateDistanceCallback(object):
@@ -4618,8 +4657,13 @@ class CNCjob(Geometry):
                 locations.append((point.coords.xy[0][0], point.coords.xy[1][0]))
                 locations.append((point.coords.xy[0][0], point.coords.xy[1][0]))
             return locations
             return locations
 
 
-        oldx = 0
-        oldy = 0
+        if self.toolchange_xy is not None:
+            self.oldx = self.toolchange_xy[0]
+            self.oldy = self.toolchange_xy[1]
+        else:
+            self.oldx = 0.0
+            self.oldy = 0.0
+
         measured_distance = 0
         measured_distance = 0
 
 
         current_platform = platform.architecture()[0]
         current_platform = platform.architecture()[0]
@@ -4684,7 +4728,7 @@ class CNCjob(Geometry):
                     if tool in points:
                     if tool in points:
                         # Tool change sequence (optional)
                         # Tool change sequence (optional)
                         if toolchange:
                         if toolchange:
-                            gcode += self.doformat(p.toolchange_code,toolchangexy=(oldx, oldy))
+                            gcode += self.doformat(p.toolchange_code,toolchangexy=(self.oldx, self.oldy))
                             gcode += self.doformat(p.spindle_code)  # Spindle start
                             gcode += self.doformat(p.spindle_code)  # Spindle start
                             if self.dwell is True:
                             if self.dwell is True:
                                 gcode += self.doformat(p.dwell_code)  # Dwell time
                                 gcode += self.doformat(p.dwell_code)  # Dwell time
@@ -4702,9 +4746,9 @@ class CNCjob(Geometry):
                             gcode += self.doformat(p.down_code, x=locx, y=locy)
                             gcode += self.doformat(p.down_code, x=locx, y=locy)
                             gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
                             gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
                             gcode += self.doformat(p.lift_code, x=locx, y=locy)
                             gcode += self.doformat(p.lift_code, x=locx, y=locy)
-                            measured_distance += abs(distance_euclidian(locx, locy, oldx, oldy))
-                            oldx = locx
-                            oldy = locy
+                            measured_distance += abs(distance_euclidian(locx, locy, self.oldx, self.oldy))
+                            self.oldx = locx
+                            self.oldy = locy
                 log.debug("The total travel distance with OR-TOOLS Metaheuristics is: %s" % str(measured_distance))
                 log.debug("The total travel distance with OR-TOOLS Metaheuristics is: %s" % str(measured_distance))
             elif excellon_optimization_type == 'B':
             elif excellon_optimization_type == 'B':
                 log.debug("Using OR-Tools Basic drill path optimization.")
                 log.debug("Using OR-Tools Basic drill path optimization.")
@@ -4758,7 +4802,7 @@ class CNCjob(Geometry):
                     if tool in points:
                     if tool in points:
                         # Tool change sequence (optional)
                         # Tool change sequence (optional)
                         if toolchange:
                         if toolchange:
-                            gcode += self.doformat(p.toolchange_code,toolchangexy=(oldx, oldy))
+                            gcode += self.doformat(p.toolchange_code,toolchangexy=(self.oldx, self.oldy))
                             gcode += self.doformat(p.spindle_code)  # Spindle start)
                             gcode += self.doformat(p.spindle_code)  # Spindle start)
                             if self.dwell is True:
                             if self.dwell is True:
                                 gcode += self.doformat(p.dwell_code)  # Dwell time
                                 gcode += self.doformat(p.dwell_code)  # Dwell time
@@ -4775,12 +4819,12 @@ class CNCjob(Geometry):
                             gcode += self.doformat(p.down_code, x=locx, y=locy)
                             gcode += self.doformat(p.down_code, x=locx, y=locy)
                             gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
                             gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
                             gcode += self.doformat(p.lift_code, x=locx, y=locy)
                             gcode += self.doformat(p.lift_code, x=locx, y=locy)
-                            measured_distance += abs(distance_euclidian(locx, locy, oldx, oldy))
-                            oldx = locx
-                            oldy = locy
+                            measured_distance += abs(distance_euclidian(locx, locy, self.oldx, self.oldy))
+                            self.oldx = locx
+                            self.oldy = locy
                 log.debug("The total travel distance with OR-TOOLS Basic Algorithm is: %s" % str(measured_distance))
                 log.debug("The total travel distance with OR-TOOLS Basic Algorithm is: %s" % str(measured_distance))
             else:
             else:
-                self.app.inform.emit("[error_notcl] Wrong optimization type selected.")
+                self.app.inform.emit("[ERROR_NOTCL] Wrong optimization type selected.")
                 return
                 return
         else:
         else:
             log.debug("Using Travelling Salesman drill path optimization.")
             log.debug("Using Travelling Salesman drill path optimization.")
@@ -4792,7 +4836,7 @@ class CNCjob(Geometry):
                 if tool in points:
                 if tool in points:
                     # Tool change sequence (optional)
                     # Tool change sequence (optional)
                     if toolchange:
                     if toolchange:
-                        gcode += self.doformat(p.toolchange_code, toolchangexy=(oldx, oldy))
+                        gcode += self.doformat(p.toolchange_code, toolchangexy=(self.oldx, self.oldy))
                         gcode += self.doformat(p.spindle_code)  # Spindle start)
                         gcode += self.doformat(p.spindle_code)  # Spindle start)
                         if self.dwell is True:
                         if self.dwell is True:
                             gcode += self.doformat(p.dwell_code)  # Dwell time
                             gcode += self.doformat(p.dwell_code)  # Dwell time
@@ -4811,15 +4855,15 @@ class CNCjob(Geometry):
                         gcode += self.doformat(p.down_code, x=point[0], y=point[1])
                         gcode += self.doformat(p.down_code, x=point[0], y=point[1])
                         gcode += self.doformat(p.up_to_zero_code, x=point[0], y=point[1])
                         gcode += self.doformat(p.up_to_zero_code, x=point[0], y=point[1])
                         gcode += self.doformat(p.lift_code, x=point[0], y=point[1])
                         gcode += self.doformat(p.lift_code, x=point[0], y=point[1])
-                        measured_distance += abs(distance_euclidian(point[0], point[1], oldx, oldy))
-                        oldx = point[0]
-                        oldy = point[1]
+                        measured_distance += abs(distance_euclidian(point[0], point[1], self.oldx, self.oldy))
+                        self.oldx = point[0]
+                        self.oldy = point[1]
             log.debug("The total travel distance with Travelling Salesman Algorithm is: %s" % str(measured_distance))
             log.debug("The total travel distance with Travelling Salesman Algorithm is: %s" % str(measured_distance))
 
 
         gcode += self.doformat(p.spindle_stop_code)  # Spindle stop
         gcode += self.doformat(p.spindle_stop_code)  # Spindle stop
         gcode += self.doformat(p.end_code, x=0, y=0)
         gcode += self.doformat(p.end_code, x=0, y=0)
 
 
-        measured_distance += abs(distance_euclidian(oldx, oldy, 0, 0))
+        measured_distance += abs(distance_euclidian(self.oldx, self.oldy, 0, 0))
         log.debug("The total travel distance including travel to end position is: %s" %
         log.debug("The total travel distance including travel to end position is: %s" %
                   str(measured_distance) + '\n')
                   str(measured_distance) + '\n')
         self.gcode = gcode
         self.gcode = gcode
@@ -4887,19 +4931,32 @@ class CNCjob(Geometry):
         self.multidepth = multidepth
         self.multidepth = multidepth
 
 
         self.toolchangez = toolchangez
         self.toolchangez = toolchangez
-        self.toolchange_xy = [float(eval(a)) for a in toolchangexy.split(",")]
+
+        try:
+            if toolchangexy == '':
+                self.toolchange_xy = None
+            else:
+                self.toolchange_xy = [float(eval(a)) for a in toolchangexy.split(",")]
+                if len(self.toolchange_xy) < 2:
+                    self.app.inform.emit("[ERROR]The Toolchange X,Y field in Edit -> Preferences has to be "
+                                         "in the format (x, y) \nbut now there is only one value, not two. ")
+                    return 'fail'
+        except Exception as e:
+            log.debug("camlib.CNCJob.generate_from_multitool_geometry() --> %s" % str(e))
+            pass
 
 
         self.pp_geometry_name = pp_geometry_name if pp_geometry_name else 'default'
         self.pp_geometry_name = pp_geometry_name if pp_geometry_name else 'default'
+        self.f_plunge = self.app.defaults["geometry_f_plunge"]
 
 
         if self.z_cut > 0:
         if self.z_cut > 0:
-            self.app.inform.emit("[warning] The Cut Z parameter has positive value. "
+            self.app.inform.emit("[WARNING] The Cut Z parameter has positive value. "
                                  "It is the depth value to cut into material.\n"
                                  "It is the depth value to cut into material.\n"
                                  "The Cut Z parameter needs to have a negative value, assuming it is a typo "
                                  "The Cut Z parameter needs to have a negative value, assuming it is a typo "
                                  "therefore the app will convert the value to negative."
                                  "therefore the app will convert the value to negative."
                                  "Check the resulting CNC code (Gcode etc).")
                                  "Check the resulting CNC code (Gcode etc).")
             self.z_cut = -self.z_cut
             self.z_cut = -self.z_cut
         elif self.z_cut == 0:
         elif self.z_cut == 0:
-            self.app.inform.emit("[warning] The Cut Z parameter is zero. "
+            self.app.inform.emit("[WARNING] The Cut Z parameter is zero. "
                                  "There will be no cut, skipping %s file" % self.options['name'])
                                  "There will be no cut, skipping %s file" % self.options['name'])
 
 
         ## Index first and last points in paths
         ## Index first and last points in paths
@@ -4936,14 +4993,17 @@ class CNCjob(Geometry):
         self.gcode = self.doformat(p.start_code)
         self.gcode = self.doformat(p.start_code)
 
 
         self.gcode += self.doformat(p.feedrate_code)        # sets the feed rate
         self.gcode += self.doformat(p.feedrate_code)        # sets the feed rate
-        self.gcode += self.doformat(p.lift_code, x=0, y=0)  # Move (up) to travel height
-        self.gcode += self.doformat(p.startz_code, x=0, y=0)
+
+        if toolchange is False:
+            self.gcode += self.doformat(p.lift_code, x=0, y=0)  # Move (up) to travel height
+            self.gcode += self.doformat(p.startz_code, x=0, y=0)
 
 
         if toolchange:
         if toolchange:
-            if "line_xyz" in self.pp_geometry_name:
-                self.gcode += self.doformat(p.toolchange_code, x=self.toolchange_xy[0], y=self.toolchange_xy[1])
-            else:
-                self.gcode += self.doformat(p.toolchange_code)
+            # if "line_xyz" in self.pp_geometry_name:
+            #     self.gcode += self.doformat(p.toolchange_code, x=self.toolchange_xy[0], y=self.toolchange_xy[1])
+            # else:
+            #     self.gcode += self.doformat(p.toolchange_code)
+            self.gcode += self.doformat(p.toolchange_code)
 
 
             self.gcode += self.doformat(p.spindle_code)     # Spindle start
             self.gcode += self.doformat(p.spindle_code)     # Spindle start
             if self.dwell is True:
             if self.dwell is True:
@@ -5023,13 +5083,13 @@ class CNCjob(Geometry):
         """
         """
 
 
         if not isinstance(geometry, Geometry):
         if not isinstance(geometry, Geometry):
-            self.app.inform.emit("[error]Expected a Geometry, got %s" % type(geometry))
+            self.app.inform.emit("[ERROR]Expected a Geometry, got %s" % type(geometry))
             return 'fail'
             return 'fail'
         log.debug("Generate_from_geometry_2()")
         log.debug("Generate_from_geometry_2()")
 
 
         # if solid_geometry is empty raise an exception
         # if solid_geometry is empty raise an exception
         if not geometry.solid_geometry:
         if not geometry.solid_geometry:
-            self.app.inform.emit("[error_notcl]Trying to generate a CNC Job "
+            self.app.inform.emit("[ERROR_NOTCL]Trying to generate a CNC Job "
                                  "from a Geometry object without solid_geometry.")
                                  "from a Geometry object without solid_geometry.")
 
 
         temp_solid_geometry = []
         temp_solid_geometry = []
@@ -5067,19 +5127,32 @@ class CNCjob(Geometry):
         self.multidepth = multidepth
         self.multidepth = multidepth
 
 
         self.toolchangez = toolchangez
         self.toolchangez = toolchangez
-        self.toolchange_xy = [float(eval(a)) for a in toolchangexy.split(",")]
+
+        try:
+            if toolchangexy == '':
+                self.toolchange_xy = None
+            else:
+                self.toolchange_xy = [float(eval(a)) for a in toolchangexy.split(",")]
+                if len(self.toolchange_xy) < 2:
+                    self.app.inform.emit("[ERROR]The Toolchange X,Y field in Edit -> Preferences has to be "
+                                         "in the format (x, y) \nbut now there is only one value, not two. ")
+                    return 'fail'
+        except Exception as e:
+            log.debug("camlib.CNCJob.generate_from_geometry_2() --> %s" % str(e))
+            pass
 
 
         self.pp_geometry_name = pp_geometry_name if pp_geometry_name else 'default'
         self.pp_geometry_name = pp_geometry_name if pp_geometry_name else 'default'
+        self.f_plunge = self.app.defaults["geometry_f_plunge"]
 
 
         if self.z_cut > 0:
         if self.z_cut > 0:
-            self.app.inform.emit("[warning] The Cut Z parameter has positive value. "
+            self.app.inform.emit("[WARNING] The Cut Z parameter has positive value. "
                                  "It is the depth value to cut into material.\n"
                                  "It is the depth value to cut into material.\n"
                                  "The Cut Z parameter needs to have a negative value, assuming it is a typo "
                                  "The Cut Z parameter needs to have a negative value, assuming it is a typo "
                                  "therefore the app will convert the value to negative."
                                  "therefore the app will convert the value to negative."
                                  "Check the resulting CNC code (Gcode etc).")
                                  "Check the resulting CNC code (Gcode etc).")
             self.z_cut = -self.z_cut
             self.z_cut = -self.z_cut
         elif self.z_cut == 0:
         elif self.z_cut == 0:
-            self.app.inform.emit("[warning] The Cut Z parameter is zero. "
+            self.app.inform.emit("[WARNING] The Cut Z parameter is zero. "
                                  "There will be no cut, skipping %s file" % geometry.options['name'])
                                  "There will be no cut, skipping %s file" % geometry.options['name'])
 
 
         ## Index first and last points in paths
         ## Index first and last points in paths
@@ -5113,18 +5186,23 @@ class CNCjob(Geometry):
         self.pp_geometry = self.app.postprocessors[self.pp_geometry_name]
         self.pp_geometry = self.app.postprocessors[self.pp_geometry_name]
         p = self.pp_geometry
         p = self.pp_geometry
 
 
+        self.oldx = 0.0
+        self.oldy = 0.0
+
         self.gcode = self.doformat(p.start_code)
         self.gcode = self.doformat(p.start_code)
 
 
         self.gcode += self.doformat(p.feedrate_code)        # sets the feed rate
         self.gcode += self.doformat(p.feedrate_code)        # sets the feed rate
 
 
-        self.gcode += self.doformat(p.lift_code, x=0, y=0)  # Move (up) to travel height
-        self.gcode += self.doformat(p.startz_code, x=0, y=0)
+        if toolchange is False:
+            self.gcode += self.doformat(p.lift_code, x=self.oldx , y=self.oldy )  # Move (up) to travel height
+            self.gcode += self.doformat(p.startz_code, x=self.oldx , y=self.oldy )
 
 
         if toolchange:
         if toolchange:
-            if "line_xyz" in self.pp_geometry_name:
-                self.gcode += self.doformat(p.toolchange_code, x=self.toolchange_xy[0], y=self.toolchange_xy[1])
-            else:
-                self.gcode += self.doformat(p.toolchange_code)
+            # if "line_xyz" in self.pp_geometry_name:
+            #     self.gcode += self.doformat(p.toolchange_code, x=self.toolchange_xy[0], y=self.toolchange_xy[1])
+            # else:
+            #     self.gcode += self.doformat(p.toolchange_code)
+            self.gcode += self.doformat(p.toolchange_code)
 
 
             self.gcode += self.doformat(p.spindle_code)     # Spindle start
             self.gcode += self.doformat(p.spindle_code)     # Spindle start
 
 
@@ -5328,10 +5406,17 @@ class CNCjob(Geometry):
 
 
         # Current path: temporary storage until tool is
         # Current path: temporary storage until tool is
         # lifted or lowered.
         # lifted or lowered.
-        if self.toolchange_xy == "excellon":
-            pos_xy = [float(eval(a)) for a in self.app.defaults["excellon_toolchangexy"].split(",")]
+        if self.toolchange_xy_type == "excellon":
+            if self.app.defaults["excellon_toolchangexy"] == '':
+                pos_xy = [0, 0]
+            else:
+                pos_xy = [float(eval(a)) for a in self.app.defaults["excellon_toolchangexy"].split(",")]
         else:
         else:
-            pos_xy = [float(eval(a)) for a in self.app.defaults["geometry_toolchangexy"].split(",")]
+            if self.app.defaults["geometry_toolchangexy"] == '':
+                pos_xy = [0, 0]
+            else:
+                pos_xy = [float(eval(a)) for a in self.app.defaults["geometry_toolchangexy"].split(",")]
+
         path = [pos_xy]
         path = [pos_xy]
         # path = [(0, 0)]
         # path = [(0, 0)]
 
 
@@ -5456,7 +5541,7 @@ class CNCjob(Geometry):
         
         
     def plot2(self, tooldia=None, dpi=75, margin=0.1, gcode_parsed=None,
     def plot2(self, tooldia=None, dpi=75, margin=0.1, gcode_parsed=None,
               color={"T": ["#F0E24D4C", "#B5AB3A4C"], "C": ["#5E6CFFFF", "#4650BDFF"]},
               color={"T": ["#F0E24D4C", "#B5AB3A4C"], "C": ["#5E6CFFFF", "#4650BDFF"]},
-              alpha={"T": 0.3, "C": 1.0}, tool_tolerance=0.0005, obj=None, visible=False):
+              alpha={"T": 0.3, "C": 1.0}, tool_tolerance=0.0005, obj=None, visible=False, kind='all'):
         """
         """
         Plots the G-code job onto the given axes.
         Plots the G-code job onto the given axes.
 
 
@@ -5477,7 +5562,15 @@ class CNCjob(Geometry):
 
 
         if tooldia == 0:
         if tooldia == 0:
             for geo in gcode_parsed:
             for geo in gcode_parsed:
-                obj.add_shape(shape=geo['geom'], color=color[geo['kind'][0]][1], visible=visible)
+                if kind == 'all':
+                    obj.add_shape(shape=geo['geom'], color=color[geo['kind'][0]][1], visible=visible)
+                elif kind == 'travel':
+                    if geo['kind'][0] == 'T':
+                        obj.add_shape(shape=geo['geom'], color=color['T'][1], visible=visible)
+                elif kind == 'cut':
+                    if geo['kind'][0] == 'C':
+                        obj.add_shape(shape=geo['geom'], color=color['C'][1], visible=visible)
+
         else:
         else:
             text = []
             text = []
             pos = []
             pos = []
@@ -5488,8 +5581,17 @@ class CNCjob(Geometry):
                 pos.append(geo['geom'].coords[0])
                 pos.append(geo['geom'].coords[0])
 
 
                 poly = geo['geom'].buffer(tooldia / 2.0).simplify(tool_tolerance)
                 poly = geo['geom'].buffer(tooldia / 2.0).simplify(tool_tolerance)
-                obj.add_shape(shape=poly, color=color[geo['kind'][0]][1], face_color=color[geo['kind'][0]][0],
+                if kind == 'all':
+                    obj.add_shape(shape=poly, color=color[geo['kind'][0]][1], face_color=color[geo['kind'][0]][0],
                               visible=visible, layer=1 if geo['kind'][0] == 'C' else 2)
                               visible=visible, layer=1 if geo['kind'][0] == 'C' else 2)
+                elif kind == 'travel':
+                    if geo['kind'][0] == 'T':
+                        obj.add_shape(shape=poly, color=color['T'][1], face_color=color['T'][0],
+                                      visible=visible, layer=2)
+                elif kind == 'cut':
+                    if geo['kind'][0] == 'C':
+                        obj.add_shape(shape=poly, color=color['C'][1], face_color=color['C'][0],
+                                      visible=visible, layer=1)
 
 
             obj.annotation.set(text=text, pos=pos, visible=obj.options['plot'])
             obj.annotation.set(text=text, pos=pos, visible=obj.options['plot'])
 
 
@@ -5798,6 +5900,7 @@ class CNCjob(Geometry):
             else:
             else:
                 # it's a Shapely object, return it's bounds
                 # it's a Shapely object, return it's bounds
                 return obj.bounds
                 return obj.bounds
+
         if self.multitool is False:
         if self.multitool is False:
             log.debug("CNCJob->bounds()")
             log.debug("CNCJob->bounds()")
             if self.solid_geometry is None:
             if self.solid_geometry is None:
@@ -5806,21 +5909,30 @@ class CNCjob(Geometry):
 
 
             bounds_coords = bounds_rec(self.solid_geometry)
             bounds_coords = bounds_rec(self.solid_geometry)
         else:
         else:
+
             for k, v in self.cnc_tools.items():
             for k, v in self.cnc_tools.items():
                 minx = Inf
                 minx = Inf
                 miny = Inf
                 miny = Inf
                 maxx = -Inf
                 maxx = -Inf
                 maxy = -Inf
                 maxy = -Inf
-
-                for k in v['solid_geometry']:
-                    minx_, miny_, maxx_, maxy_ = bounds_rec(k)
+                try:
+                    for k in v['solid_geometry']:
+                        minx_, miny_, maxx_, maxy_ = bounds_rec(k)
+                        minx = min(minx, minx_)
+                        miny = min(miny, miny_)
+                        maxx = max(maxx, maxx_)
+                        maxy = max(maxy, maxy_)
+                except TypeError:
+                    minx_, miny_, maxx_, maxy_ = bounds_rec(v['solid_geometry'])
                     minx = min(minx, minx_)
                     minx = min(minx, minx_)
                     miny = min(miny, miny_)
                     miny = min(miny, miny_)
                     maxx = max(maxx, maxx_)
                     maxx = max(maxx, maxx_)
                     maxy = max(maxy, maxy_)
                     maxy = max(maxy, maxy_)
+
             bounds_coords = minx, miny, maxx, maxy
             bounds_coords = minx, miny, maxx, maxy
         return bounds_coords
         return bounds_coords
 
 
+    # TODO This function should be replaced at some point with a "real" function. Until then it's an ugly hack ...
     def scale(self, xfactor, yfactor=None, point=None):
     def scale(self, xfactor, yfactor=None, point=None):
         """
         """
         Scales all the geometry on the XY plane in the object by the
         Scales all the geometry on the XY plane in the object by the
@@ -5844,8 +5956,124 @@ class CNCjob(Geometry):
         else:
         else:
             px, py = point
             px, py = point
 
 
-        for g in self.gcode_parsed:
-            g['geom'] = affinity.scale(g['geom'], xfactor, yfactor, origin=(px, py))
+        def scale_g(g):
+            """
+
+            :param g: 'g' parameter it's a gcode string
+            :return:  scaled gcode string
+            """
+
+            temp_gcode = ''
+            header_start = False
+            header_stop = False
+            units = self.app.general_options_form.general_app_group.units_radio.get_value().upper()
+
+            lines = StringIO(g)
+            for line in lines:
+
+                # this changes the GCODE header ---- UGLY HACK
+                if "TOOL DIAMETER" in line or "Feedrate:" in line:
+                    header_start = True
+
+                if "G20" in line or "G21" in line:
+                    header_start = False
+                    header_stop = True
+
+                if header_start is True:
+                    header_stop = False
+                    if "in" in line:
+                        if units == 'MM':
+                            line = line.replace("in", "mm")
+                    if "mm" in line:
+                        if units == 'IN':
+                            line = line.replace("mm", "in")
+
+                    # find any float number in header (even multiple on the same line) and convert it
+                    numbers_in_header = re.findall(self.g_nr_re, line)
+                    if numbers_in_header:
+                        for nr in numbers_in_header:
+                            new_nr = float(nr) * xfactor
+                            # replace the updated string
+                            line = line.replace(nr, ('%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_nr))
+                            )
+
+                # this scales all the X and Y and Z and F values and also the Tool Dia in the toolchange message
+                if header_stop is True:
+                    if "G20" in line:
+                        if units == 'MM':
+                            line = line.replace("G20", "G21")
+                    if "G21" in line:
+                        if units == 'IN':
+                            line = line.replace("G21", "G20")
+
+                    # find the X group
+                    match_x = self.g_x_re.search(line)
+                    if match_x:
+                        if match_x.group(1) is not None:
+                            new_x = float(match_x.group(1)[1:]) * xfactor
+                            # replace the updated string
+                            line = line.replace(
+                                match_x.group(1),
+                                'X%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_x)
+                            )
+                    # find the Y group
+                    match_y = self.g_y_re.search(line)
+                    if match_y:
+                        if match_y.group(1) is not None:
+                            new_y = float(match_y.group(1)[1:]) * yfactor
+                            line = line.replace(
+                                match_y.group(1),
+                                'Y%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_y)
+                            )
+                    # find the Z group
+                    match_z = self.g_z_re.search(line)
+                    if match_z:
+                        if match_z.group(1) is not None:
+                            new_z = float(match_z.group(1)[1:]) * xfactor
+                            line = line.replace(
+                                match_z.group(1),
+                                'Z%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_z)
+                            )
+
+                    # find the F group
+                    match_f = self.g_f_re.search(line)
+                    if match_f:
+                        if match_f.group(1) is not None:
+                            new_f = float(match_f.group(1)[1:]) * xfactor
+                            line = line.replace(
+                                match_f.group(1),
+                                'F%.*f' % (self.app.defaults["cncjob_fr_decimals"], new_f)
+                            )
+                    # find the T group (tool dia on toolchange)
+                    match_t = self.g_t_re.search(line)
+                    if match_t:
+                        if match_t.group(1) is not None:
+                            new_t = float(match_t.group(1)[1:]) * xfactor
+                            line = line.replace(
+                                match_t.group(1),
+                                '= %.*f' % (self.app.defaults["cncjob_coords_decimals"], new_t)
+                            )
+
+                temp_gcode += line
+            lines.close()
+            header_stop = False
+            return temp_gcode
+
+        if self.multitool is False:
+            # offset Gcode
+            self.gcode = scale_g(self.gcode)
+            # offset geometry
+            for g in self.gcode_parsed:
+                g['geom'] = affinity.scale(g['geom'], xfactor, yfactor, origin=(px, py))
+            self.create_geometry()
+        else:
+            for k, v in self.cnc_tools.items():
+                # scale Gcode
+                v['gcode'] = scale_g(v['gcode'])
+                # scale gcode_parsed
+                for g in v['gcode_parsed']:
+                    g['geom'] = affinity.scale(g['geom'], xfactor, yfactor, origin=(px, py))
+                v['solid_geometry'] = cascaded_union([geo['geom'] for geo in v['gcode_parsed']])
 
 
         self.create_geometry()
         self.create_geometry()
 
 
@@ -5875,7 +6103,7 @@ class CNCjob(Geometry):
             lines = StringIO(g)
             lines = StringIO(g)
             for line in lines:
             for line in lines:
                 # find the X group
                 # find the X group
-                match_x = self.g_offsetx_re.search(line)
+                match_x = self.g_x_re.search(line)
                 if match_x:
                 if match_x:
                     if match_x.group(1) is not None:
                     if match_x.group(1) is not None:
                         # get the coordinate and add X offset
                         # get the coordinate and add X offset
@@ -5885,7 +6113,7 @@ class CNCjob(Geometry):
                             match_x.group(1),
                             match_x.group(1),
                             'X%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_x)
                             'X%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_x)
                         )
                         )
-                match_y = self.g_offsety_re.search(line)
+                match_y = self.g_y_re.search(line)
                 if match_y:
                 if match_y:
                     if match_y.group(1) is not None:
                     if match_y.group(1) is not None:
                         new_y = float(match_y.group(1)[1:]) + dy
                         new_y = float(match_y.group(1)[1:]) + dy

+ 239 - 38
flatcamTools/ToolCalculators.py

@@ -10,6 +10,7 @@ class ToolCalculator(FlatCAMTool):
     toolName = "Calculators"
     toolName = "Calculators"
     v_shapeName = "V-Shape Tool Calculator"
     v_shapeName = "V-Shape Tool Calculator"
     unitsName = "Units Calculator"
     unitsName = "Units Calculator"
+    eplateName = "ElectroPlating Calculator"
 
 
     def __init__(self, app):
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
@@ -20,7 +21,9 @@ class ToolCalculator(FlatCAMTool):
         title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
         title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
         self.layout.addWidget(title_label)
         self.layout.addWidget(title_label)
 
 
-        ## V-shape Tool Calculator
+        ############################
+        ## V-shape Tool Calculator ##
+        ############################
 
 
         self.v_shape_spacer_label = QtWidgets.QLabel(" ")
         self.v_shape_spacer_label = QtWidgets.QLabel(" ")
         self.layout.addWidget(self.v_shape_spacer_label)
         self.layout.addWidget(self.v_shape_spacer_label)
@@ -35,30 +38,30 @@ class ToolCalculator(FlatCAMTool):
 
 
         self.tipDia_label = QtWidgets.QLabel("Tip Diameter:")
         self.tipDia_label = QtWidgets.QLabel("Tip Diameter:")
         self.tipDia_entry = FCEntry()
         self.tipDia_entry = FCEntry()
-        self.tipDia_entry.setFixedWidth(70)
+        # self.tipDia_entry.setFixedWidth(70)
         self.tipDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.tipDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.tipDia_entry.setToolTip('This is the diameter of the tool tip.\n'
+        self.tipDia_label.setToolTip('This is the diameter of the tool tip.\n'
                                      'The manufacturer specifies it.')
                                      'The manufacturer specifies it.')
 
 
         self.tipAngle_label = QtWidgets.QLabel("Tip Angle:")
         self.tipAngle_label = QtWidgets.QLabel("Tip Angle:")
         self.tipAngle_entry = FCEntry()
         self.tipAngle_entry = FCEntry()
-        self.tipAngle_entry.setFixedWidth(70)
+        # self.tipAngle_entry.setFixedWidth(70)
         self.tipAngle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.tipAngle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.tipAngle_entry.setToolTip("This is the angle of the tip of the tool.\n"
+        self.tipAngle_label.setToolTip("This is the angle of the tip of the tool.\n"
                                        "It is specified by manufacturer.")
                                        "It is specified by manufacturer.")
 
 
         self.cutDepth_label = QtWidgets.QLabel("Cut Z:")
         self.cutDepth_label = QtWidgets.QLabel("Cut Z:")
         self.cutDepth_entry = FCEntry()
         self.cutDepth_entry = FCEntry()
-        self.cutDepth_entry.setFixedWidth(70)
+        # self.cutDepth_entry.setFixedWidth(70)
         self.cutDepth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.cutDepth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.cutDepth_entry.setToolTip("This is the depth to cut into the material.\n"
+        self.cutDepth_label.setToolTip("This is the depth to cut into the material.\n"
                                        "In the CNCJob is the CutZ parameter.")
                                        "In the CNCJob is the CutZ parameter.")
 
 
         self.effectiveToolDia_label = QtWidgets.QLabel("Tool Diameter:")
         self.effectiveToolDia_label = QtWidgets.QLabel("Tool Diameter:")
         self.effectiveToolDia_entry = FCEntry()
         self.effectiveToolDia_entry = FCEntry()
-        self.effectiveToolDia_entry.setFixedWidth(70)
+        # self.effectiveToolDia_entry.setFixedWidth(70)
         self.effectiveToolDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.effectiveToolDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.effectiveToolDia_entry.setToolTip("This is the tool diameter to be entered into\n"
+        self.effectiveToolDia_label.setToolTip("This is the tool diameter to be entered into\n"
                                                "FlatCAM Gerber section.\n"
                                                "FlatCAM Gerber section.\n"
                                                "In the CNCJob section it is called >Tool dia<.")
                                                "In the CNCJob section it is called >Tool dia<.")
         # self.effectiveToolDia_entry.setEnabled(False)
         # self.effectiveToolDia_entry.setEnabled(False)
@@ -69,19 +72,21 @@ class ToolCalculator(FlatCAMTool):
         form_layout.addRow(self.cutDepth_label, self.cutDepth_entry)
         form_layout.addRow(self.cutDepth_label, self.cutDepth_entry)
         form_layout.addRow(self.effectiveToolDia_label, self.effectiveToolDia_entry)
         form_layout.addRow(self.effectiveToolDia_label, self.effectiveToolDia_entry)
 
 
-
         ## Buttons
         ## Buttons
-        self.calculate_button = QtWidgets.QPushButton("Calculate")
-        self.calculate_button.setFixedWidth(70)
-        self.calculate_button.setToolTip(
+        self.calculate_vshape_button = QtWidgets.QPushButton("Calculate")
+        # self.calculate_button.setFixedWidth(70)
+        self.calculate_vshape_button.setToolTip(
             "Calculate either the Cut Z or the effective tool diameter,\n  "
             "Calculate either the Cut Z or the effective tool diameter,\n  "
             "depending on which is desired and which is known. "
             "depending on which is desired and which is known. "
         )
         )
         self.empty_label = QtWidgets.QLabel(" ")
         self.empty_label = QtWidgets.QLabel(" ")
 
 
-        form_layout.addRow(self.empty_label, self.calculate_button)
+        form_layout.addRow(self.empty_label, self.calculate_vshape_button)
+
+        ######################
+        ## Units Calculator ##
+        ######################
 
 
-        ## Units Calculator
         self.unists_spacer_label = QtWidgets.QLabel(" ")
         self.unists_spacer_label = QtWidgets.QLabel(" ")
         self.layout.addWidget(self.unists_spacer_label)
         self.layout.addWidget(self.unists_spacer_label)
 
 
@@ -89,25 +94,109 @@ class ToolCalculator(FlatCAMTool):
         units_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.unitsName)
         units_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.unitsName)
         self.layout.addWidget(units_label)
         self.layout.addWidget(units_label)
 
 
-        #Form Layout
-        form_units_layout = QtWidgets.QFormLayout()
-        self.layout.addLayout(form_units_layout)
+        #Grid Layout
+        grid_units_layout = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_units_layout)
 
 
         inch_label = QtWidgets.QLabel("INCH")
         inch_label = QtWidgets.QLabel("INCH")
         mm_label = QtWidgets.QLabel("MM")
         mm_label = QtWidgets.QLabel("MM")
+        grid_units_layout.addWidget(mm_label, 0, 0)
+        grid_units_layout.addWidget( inch_label, 0, 1)
 
 
         self.inch_entry = FCEntry()
         self.inch_entry = FCEntry()
-        self.inch_entry.setFixedWidth(70)
+        # self.inch_entry.setFixedWidth(70)
         self.inch_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.inch_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.inch_entry.setToolTip("Here you enter the value to be converted from INCH to MM")
         self.inch_entry.setToolTip("Here you enter the value to be converted from INCH to MM")
 
 
         self.mm_entry = FCEntry()
         self.mm_entry = FCEntry()
-        self.mm_entry.setFixedWidth(70)
+        # self.mm_entry.setFixedWidth(130)
         self.mm_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.mm_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.mm_entry.setToolTip("Here you enter the value to be converted from MM to INCH")
         self.mm_entry.setToolTip("Here you enter the value to be converted from MM to INCH")
 
 
-        form_units_layout.addRow(mm_label, inch_label)
-        form_units_layout.addRow(self.mm_entry, self.inch_entry)
+        grid_units_layout.addWidget(self.mm_entry, 1, 0)
+        grid_units_layout.addWidget(self.inch_entry, 1, 1)
+
+        ####################################
+        ## ElectroPlating Tool Calculator ##
+        ####################################
+
+        self.plate_spacer_label = QtWidgets.QLabel(" ")
+        self.layout.addWidget(self.plate_spacer_label)
+
+        ## Title of the ElectroPlating Tools Calculator
+        plate_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.eplateName)
+        plate_title_label.setToolTip(
+            "This calculator is useful for those who plate the via/pad/drill holes,\n"
+            "using a method like grahite ink or calcium hypophosphite ink or palladium chloride."
+        )
+        self.layout.addWidget(plate_title_label)
+
+        ## Plate Form Layout
+        plate_form_layout = QtWidgets.QFormLayout()
+        self.layout.addLayout(plate_form_layout)
+
+        self.pcblengthlabel = QtWidgets.QLabel("Board Length:")
+        self.pcblength_entry = FCEntry()
+        # self.pcblengthlabel.setFixedWidth(70)
+        self.pcblength_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.pcblengthlabel.setToolTip('This is the board length. In centimeters.')
+
+        self.pcbwidthlabel = QtWidgets.QLabel("Board Width:")
+        self.pcbwidth_entry = FCEntry()
+        # self.pcbwidthlabel.setFixedWidth(70)
+        self.pcbwidth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.pcbwidthlabel.setToolTip('This is the board width.In centimeters.')
+
+        self.cdensity_label = QtWidgets.QLabel("Current Density:")
+        self.cdensity_entry = FCEntry()
+        # self.cdensity_entry.setFixedWidth(70)
+        self.cdensity_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.cdensity_label.setToolTip("Current density to pass through the board. \n"
+                                       "In Amps per Square Feet ASF.")
+
+
+        self.growth_label = QtWidgets.QLabel("Copper Growth:")
+        self.growth_entry = FCEntry()
+        # self.growth_entry.setFixedWidth(70)
+        self.growth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.growth_label.setToolTip("How thick the copper growth is intended to be.\n"
+                                     "In microns.")
+
+        # self.growth_entry.setEnabled(False)
+
+        self.cvaluelabel = QtWidgets.QLabel("Current Value:")
+        self.cvalue_entry = FCEntry()
+        # self.cvaluelabel.setFixedWidth(70)
+        self.cvalue_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.cvaluelabel.setToolTip('This is the current intensity value\n'
+                                     'to be set on the Power Supply. In Amps.')
+        self.cvalue_entry.setDisabled(True)
+
+        self.timelabel = QtWidgets.QLabel("Time:")
+        self.time_entry = FCEntry()
+        # self.timelabel.setFixedWidth(70)
+        self.time_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.timelabel.setToolTip('This is the calculated time required for the procedure.\n'
+                                  'In minutes.')
+        self.time_entry.setDisabled(True)
+
+        plate_form_layout.addRow(self.pcblengthlabel, self.pcblength_entry)
+        plate_form_layout.addRow(self.pcbwidthlabel, self.pcbwidth_entry)
+        plate_form_layout.addRow(self.cdensity_label, self.cdensity_entry)
+        plate_form_layout.addRow(self.growth_label, self.growth_entry)
+        plate_form_layout.addRow(self.cvaluelabel, self.cvalue_entry)
+        plate_form_layout.addRow(self.timelabel, self.time_entry)
+
+        ## Buttons
+        self.calculate_plate_button = QtWidgets.QPushButton("Calculate")
+        # self.calculate_button.setFixedWidth(70)
+        self.calculate_plate_button.setToolTip(
+            "Calculate the current intensity value and the procedure time,\n  "
+            "depending on the parameters above"
+        )
+        self.empty_label_2 = QtWidgets.QLabel(" ")
+
+        plate_form_layout.addRow(self.empty_label_2, self.calculate_plate_button)
 
 
         self.layout.addStretch()
         self.layout.addStretch()
 
 
@@ -116,13 +205,36 @@ class ToolCalculator(FlatCAMTool):
         self.cutDepth_entry.editingFinished.connect(self.on_calculate_tool_dia)
         self.cutDepth_entry.editingFinished.connect(self.on_calculate_tool_dia)
         self.tipDia_entry.editingFinished.connect(self.on_calculate_tool_dia)
         self.tipDia_entry.editingFinished.connect(self.on_calculate_tool_dia)
         self.tipAngle_entry.editingFinished.connect(self.on_calculate_tool_dia)
         self.tipAngle_entry.editingFinished.connect(self.on_calculate_tool_dia)
-        self.calculate_button.clicked.connect(self.on_calculate_tool_dia)
+        self.calculate_vshape_button.clicked.connect(self.on_calculate_tool_dia)
 
 
         self.mm_entry.editingFinished.connect(self.on_calculate_inch_units)
         self.mm_entry.editingFinished.connect(self.on_calculate_inch_units)
         self.inch_entry.editingFinished.connect(self.on_calculate_mm_units)
         self.inch_entry.editingFinished.connect(self.on_calculate_mm_units)
 
 
+        self.calculate_plate_button.clicked.connect(self.on_calculate_eplate)
+
+
+    def run(self):
+        self.app.report_usage("ToolCalculators()")
+
+        FlatCAMTool.run(self)
+        self.set_tool_ui()
+        self.app.ui.notebook.setTabText(2, "Calc. Tool")
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+C', **kwargs)
 
 
+    def set_tool_ui(self):
         ## Initialize form
         ## Initialize form
+        self.mm_entry.set_value('0')
+        self.inch_entry.set_value('0')
+
+        self.pcblength_entry.set_value('10')
+        self.pcbwidth_entry.set_value('10')
+        self.cdensity_entry.set_value('13')
+        self.growth_entry.set_value('10')
+        self.cvalue_entry.set_value(2.80)
+        self.time_entry.set_value(33.0)
+
         if self.app.defaults["units"] == 'MM':
         if self.app.defaults["units"] == 'MM':
             self.tipDia_entry.set_value('0.2')
             self.tipDia_entry.set_value('0.2')
             self.tipAngle_entry.set_value('45')
             self.tipAngle_entry.set_value('45')
@@ -134,16 +246,6 @@ class ToolCalculator(FlatCAMTool):
             self.cutDepth_entry.set_value('9.84252')
             self.cutDepth_entry.set_value('9.84252')
             self.effectiveToolDia_entry.set_value('15.35433')
             self.effectiveToolDia_entry.set_value('15.35433')
 
 
-        self.mm_entry.set_value('0')
-        self.inch_entry.set_value('0')
-
-    def run(self):
-        FlatCAMTool.run(self)
-        self.app.ui.notebook.setTabText(2, "Calc. Tool")
-
-    def install(self, icon=None, separator=None, **kwargs):
-        FlatCAMTool.install(self, icon, separator, shortcut='ALT+C', **kwargs)
-
     def on_calculate_tool_dia(self):
     def on_calculate_tool_dia(self):
         # Calculation:
         # Calculation:
         # Manufacturer gives total angle of the the tip but we need only half of it
         # Manufacturer gives total angle of the the tip but we need only half of it
@@ -155,18 +257,117 @@ class ToolCalculator(FlatCAMTool):
 
 
         try:
         try:
             tip_diameter = float(self.tipDia_entry.get_value())
             tip_diameter = float(self.tipDia_entry.get_value())
-            half_tip_angle = float(self.tipAngle_entry.get_value()) / 2
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                tip_diameter = float(self.tipDia_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
+
+        try:
+            half_tip_angle = float(self.tipAngle_entry.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                half_tip_angle = float(self.tipAngle_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
+        half_tip_angle /= 2
+
+        try:
             cut_depth = float(self.cutDepth_entry.get_value())
             cut_depth = float(self.cutDepth_entry.get_value())
-        except:
-            return
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                cut_depth = float(self.cutDepth_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
 
 
         tool_diameter = tip_diameter + (2 * cut_depth * math.tan(math.radians(half_tip_angle)))
         tool_diameter = tip_diameter + (2 * cut_depth * math.tan(math.radians(half_tip_angle)))
         self.effectiveToolDia_entry.set_value("%.4f" % tool_diameter)
         self.effectiveToolDia_entry.set_value("%.4f" % tool_diameter)
 
 
     def on_calculate_inch_units(self):
     def on_calculate_inch_units(self):
-        self.inch_entry.set_value('%.6f' % (float(self.mm_entry.get_value()) / 25.4))
+        try:
+            mm_val = float(self.mm_entry.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                mm_val = float(self.mm_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
+        self.inch_entry.set_value('%.6f' % (mm_val / 25.4))
 
 
     def on_calculate_mm_units(self):
     def on_calculate_mm_units(self):
-        self.mm_entry.set_value('%.6f' % (float(self.inch_entry.get_value()) * 25.4))
+        try:
+            inch_val = float(self.inch_entry.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                inch_val = float(self.inch_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
+        self.mm_entry.set_value('%.6f' % (inch_val * 25.4))
+
+    def on_calculate_eplate(self):
+
+        try:
+            length = float(self.pcblength_entry.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                length = float(self.pcblength_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
+
+        try:
+            width = float(self.pcbwidth_entry.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                width = float(self.pcbwidth_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
+
+        try:
+            density = float(self.cdensity_entry.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                density = float(self.cdensity_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
+
+        try:
+            copper = float(self.growth_entry.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                copper = float(self.growth_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
+
+        calculated_current = (length * width * density) * 0.0021527820833419
+        calculated_time = copper * 2.142857142857143 * float(20 / density)
+
+        self.cvalue_entry.set_value('%.2f' % calculated_current)
+        self.time_entry.set_value('%.1f' % calculated_time)
 
 
 # end of file
 # end of file

+ 79 - 37
flatcamTools/ToolCutout.py → flatcamTools/ToolCutOut.py

@@ -7,7 +7,8 @@ from GUIElements import IntEntry, RadioSet, LengthEntry
 
 
 from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber
 from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber
 
 
-class ToolCutout(FlatCAMTool):
+
+class ToolCutOut(FlatCAMTool):
 
 
     toolName = "Cutout PCB"
     toolName = "Cutout PCB"
 
 
@@ -48,6 +49,7 @@ class ToolCutout(FlatCAMTool):
         self.obj_combo.setModel(self.app.collection)
         self.obj_combo.setModel(self.app.collection)
         self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.obj_combo.setCurrentIndex(1)
         self.obj_combo.setCurrentIndex(1)
+
         self.object_label = QtWidgets.QLabel("Object:")
         self.object_label = QtWidgets.QLabel("Object:")
         self.object_label.setToolTip(
         self.object_label.setToolTip(
             "Object to be cutout.                        "
             "Object to be cutout.                        "
@@ -172,11 +174,11 @@ class ToolCutout(FlatCAMTool):
         self.layout.addStretch()
         self.layout.addStretch()
 
 
         ## Init GUI
         ## Init GUI
-        self.dia.set_value(1)
-        self.margin.set_value(0)
-        self.gapsize.set_value(1)
-        self.gaps.set_value(4)
-        self.gaps_rect_radio.set_value("4")
+        # self.dia.set_value(1)
+        # self.margin.set_value(0)
+        # self.gapsize.set_value(1)
+        # self.gaps.set_value(4)
+        # self.gaps_rect_radio.set_value("4")
 
 
         ## Signals
         ## Signals
         self.ff_cutout_object_btn.clicked.connect(self.on_freeform_cutout)
         self.ff_cutout_object_btn.clicked.connect(self.on_freeform_cutout)
@@ -190,14 +192,18 @@ class ToolCutout(FlatCAMTool):
         self.obj_combo.setCurrentIndex(0)
         self.obj_combo.setCurrentIndex(0)
 
 
     def run(self):
     def run(self):
+        self.app.report_usage("ToolCutOut()")
+
         FlatCAMTool.run(self)
         FlatCAMTool.run(self)
-        self.set_ui()
+        self.set_tool_ui()
         self.app.ui.notebook.setTabText(2, "Cutout Tool")
         self.app.ui.notebook.setTabText(2, "Cutout Tool")
 
 
     def install(self, icon=None, separator=None, **kwargs):
     def install(self, icon=None, separator=None, **kwargs):
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+U', **kwargs)
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+U', **kwargs)
 
 
-    def set_ui(self):
+    def set_tool_ui(self):
+        self.reset_fields()
+
         self.dia.set_value(float(self.app.defaults["tools_cutouttooldia"]))
         self.dia.set_value(float(self.app.defaults["tools_cutouttooldia"]))
         self.margin.set_value(float(self.app.defaults["tools_cutoutmargin"]))
         self.margin.set_value(float(self.app.defaults["tools_cutoutmargin"]))
         self.gapsize.set_value(float(self.app.defaults["tools_cutoutgapsize"]))
         self.gapsize.set_value(float(self.app.defaults["tools_cutoutgapsize"]))
@@ -216,45 +222,63 @@ class ToolCutout(FlatCAMTool):
         try:
         try:
             cutout_obj = self.app.collection.get_by_name(str(name))
             cutout_obj = self.app.collection.get_by_name(str(name))
         except:
         except:
-            self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % name)
+            self.app.inform.emit("[ERROR_NOTCL]Could not retrieve object: %s" % name)
             return "Could not retrieve object: %s" % name
             return "Could not retrieve object: %s" % name
 
 
         if cutout_obj is None:
         if cutout_obj is None:
-            self.app.inform.emit("[error_notcl]There is no object selected for Cutout.\nSelect one and try again.")
+            self.app.inform.emit("[ERROR_NOTCL]There is no object selected for Cutout.\nSelect one and try again.")
             return
             return
 
 
         try:
         try:
             dia = float(self.dia.get_value())
             dia = float(self.dia.get_value())
-        except TypeError:
-            self.app.inform.emit("[warning_notcl] Tool diameter value is missing. Add it and retry.")
-            return
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                dia = float(self.dia.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[WARNING_NOTCL] Tool diameter value is missing or wrong format. "
+                                     "Add it and retry.")
+                return
+
         try:
         try:
             margin = float(self.margin.get_value())
             margin = float(self.margin.get_value())
-        except TypeError:
-            self.app.inform.emit("[warning_notcl] Margin value is missing. Add it and retry.")
-            return
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                margin = float(self.margin.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[WARNING_NOTCL] Margin value is missing or wrong format. "
+                                     "Add it and retry.")
+                return
+
         try:
         try:
             gapsize = float(self.gapsize.get_value())
             gapsize = float(self.gapsize.get_value())
-        except TypeError:
-            self.app.inform.emit("[warning_notcl] Gap size value is missing. Add it and retry.")
-            return
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                gapsize = float(self.gapsize.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[WARNING_NOTCL] Gap size value is missing or wrong format. "
+                                     "Add it and retry.")
+                return
+
         try:
         try:
             gaps = self.gaps.get_value()
             gaps = self.gaps.get_value()
         except TypeError:
         except TypeError:
-            self.app.inform.emit("[warning_notcl] Number of gaps value is missing. Add it and retry.")
+            self.app.inform.emit("[WARNING_NOTCL] Number of gaps value is missing. Add it and retry.")
             return
             return
 
 
         if 0 in {dia}:
         if 0 in {dia}:
-            self.app.inform.emit("[warning_notcl]Tool Diameter is zero value. Change it to a positive integer.")
+            self.app.inform.emit("[WARNING_NOTCL]Tool Diameter is zero value. Change it to a positive integer.")
             return "Tool Diameter is zero value. Change it to a positive integer."
             return "Tool Diameter is zero value. Change it to a positive integer."
 
 
         if gaps not in ['lr', 'tb', '2lr', '2tb', '4', '8']:
         if gaps not in ['lr', 'tb', '2lr', '2tb', '4', '8']:
-            self.app.inform.emit("[warning_notcl] Gaps value can be only one of: 'lr', 'tb', '2lr', '2tb', 4 or 8. "
+            self.app.inform.emit("[WARNING_NOTCL] Gaps value can be only one of: 'lr', 'tb', '2lr', '2tb', 4 or 8. "
                                  "Fill in a correct value and retry. ")
                                  "Fill in a correct value and retry. ")
             return
             return
 
 
         if cutout_obj.multigeo is True:
         if cutout_obj.multigeo is True:
-            self.app.inform.emit("[error]Cutout operation cannot be done on a multi-geo Geometry.\n"
+            self.app.inform.emit("[ERROR]Cutout operation cannot be done on a multi-geo Geometry.\n"
                                  "Optionally, this Multi-geo Geometry can be converted to Single-geo Geometry,\n"
                                  "Optionally, this Multi-geo Geometry can be converted to Single-geo Geometry,\n"
                                  "and after that perform Cutout.")
                                  "and after that perform Cutout.")
             return
             return
@@ -338,39 +362,57 @@ class ToolCutout(FlatCAMTool):
         try:
         try:
             cutout_obj = self.app.collection.get_by_name(str(name))
             cutout_obj = self.app.collection.get_by_name(str(name))
         except:
         except:
-            self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % name)
+            self.app.inform.emit("[ERROR_NOTCL]Could not retrieve object: %s" % name)
             return "Could not retrieve object: %s" % name
             return "Could not retrieve object: %s" % name
 
 
         if cutout_obj is None:
         if cutout_obj is None:
-            self.app.inform.emit("[error_notcl]Object not found: %s" % cutout_obj)
+            self.app.inform.emit("[ERROR_NOTCL]Object not found: %s" % cutout_obj)
 
 
         try:
         try:
             dia = float(self.dia.get_value())
             dia = float(self.dia.get_value())
-        except TypeError:
-            self.app.inform.emit("[warning_notcl] Tool diameter value is missing. Add it and retry.")
-            return
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                dia = float(self.dia.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[WARNING_NOTCL] Tool diameter value is missing or wrong format. "
+                                     "Add it and retry.")
+                return
+
         try:
         try:
             margin = float(self.margin.get_value())
             margin = float(self.margin.get_value())
-        except TypeError:
-            self.app.inform.emit("[warning_notcl] Margin value is missing. Add it and retry.")
-            return
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                margin = float(self.margin.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[WARNING_NOTCL] Margin value is missing or wrong format. "
+                                     "Add it and retry.")
+                return
+
         try:
         try:
             gapsize = float(self.gapsize.get_value())
             gapsize = float(self.gapsize.get_value())
-        except TypeError:
-            self.app.inform.emit("[warning_notcl] Gap size value is missing. Add it and retry.")
-            return
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                gapsize = float(self.gapsize.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[WARNING_NOTCL] Gap size value is missing or wrong format. "
+                                     "Add it and retry.")
+                return
+
         try:
         try:
             gaps = self.gaps_rect_radio.get_value()
             gaps = self.gaps_rect_radio.get_value()
         except TypeError:
         except TypeError:
-            self.app.inform.emit("[warning_notcl] Number of gaps value is missing. Add it and retry.")
+            self.app.inform.emit("[WARNING_NOTCL] Number of gaps value is missing. Add it and retry.")
             return
             return
 
 
         if 0 in {dia}:
         if 0 in {dia}:
-            self.app.inform.emit("[error_notcl]Tool Diameter is zero value. Change it to a positive integer.")
+            self.app.inform.emit("[ERROR_NOTCL]Tool Diameter is zero value. Change it to a positive integer.")
             return "Tool Diameter is zero value. Change it to a positive integer."
             return "Tool Diameter is zero value. Change it to a positive integer."
 
 
         if cutout_obj.multigeo is True:
         if cutout_obj.multigeo is True:
-            self.app.inform.emit("[error]Cutout operation cannot be done on a multi-geo Geometry.\n"
+            self.app.inform.emit("[ERROR]Cutout operation cannot be done on a multi-geo Geometry.\n"
                                  "Optionally, this Multi-geo Geometry can be converted to Single-geo Geometry,\n"
                                  "Optionally, this Multi-geo Geometry can be converted to Single-geo Geometry,\n"
                                  "and after that perform Cutout.")
                                  "and after that perform Cutout.")
             return
             return

+ 44 - 40
flatcamTools/ToolDblSided.py

@@ -6,6 +6,7 @@ from shapely.geometry import Point
 from shapely import affinity
 from shapely import affinity
 from PyQt5 import QtCore
 from PyQt5 import QtCore
 
 
+
 class DblSidedTool(FlatCAMTool):
 class DblSidedTool(FlatCAMTool):
 
 
     toolName = "2-Sided PCB"
     toolName = "2-Sided PCB"
@@ -115,8 +116,8 @@ class DblSidedTool(FlatCAMTool):
         self.axloc_label = QtWidgets.QLabel("Axis Ref:")
         self.axloc_label = QtWidgets.QLabel("Axis Ref:")
         self.axloc_label.setToolTip(
         self.axloc_label.setToolTip(
             "The axis should pass through a <b>point</b> or cut\n "
             "The axis should pass through a <b>point</b> or cut\n "
-            "a specified <b>box</b> (in a Geometry object) in \n"
-            "the middle."
+            "a specified <b>box</b> (in a FlatCAM object) through \n"
+            "the center."
         )
         )
         # grid_lay.addRow("Axis Location:", self.axis_location)
         # grid_lay.addRow("Axis Location:", self.axis_location)
         grid_lay.addWidget(self.axloc_label, 8, 0)
         grid_lay.addWidget(self.axloc_label, 8, 0)
@@ -129,19 +130,18 @@ class DblSidedTool(FlatCAMTool):
         self.point_box_container = QtWidgets.QVBoxLayout()
         self.point_box_container = QtWidgets.QVBoxLayout()
         self.pb_label = QtWidgets.QLabel("<b>Point/Box:</b>")
         self.pb_label = QtWidgets.QLabel("<b>Point/Box:</b>")
         self.pb_label.setToolTip(
         self.pb_label.setToolTip(
-            "Specify the point (x, y) through which the mirror axis \n "
-            "passes or the Geometry object containing a rectangle \n"
-            "that the mirror axis cuts in half."
+            "If 'Point' is selected above it store the coordinates (x, y) through which\n"
+            "the mirroring axis passes.\n"
+            "If 'Box' is selected above, select here a FlatCAM object (Gerber, Exc or Geo).\n"
+            "Through the center of this object pass the mirroring axis selected above."
         )
         )
-        # grid_lay.addRow("Point/Box:", self.point_box_container)
 
 
         self.add_point_button = QtWidgets.QPushButton("Add")
         self.add_point_button = QtWidgets.QPushButton("Add")
         self.add_point_button.setToolTip(
         self.add_point_button.setToolTip(
-            "Add the <b>point (x, y)</b> through which the mirror axis \n "
-            "passes or the Object containing a rectangle \n"
-            "that the mirror axis cuts in half.\n"
-            "The point is captured by pressing SHIFT key\n"
-            "and left mouse clicking on canvas or you can enter them manually."
+            "Add the coordinates in format <b>(x, y)</b> through which the mirroring axis \n "
+            "selected in 'MIRROR AXIS' pass.\n"
+            "The (x, y) coordinates are captured by pressing SHIFT key\n"
+            "and left mouse button click on canvas or you can enter the coords manually."
         )
         )
         self.add_point_button.setFixedWidth(40)
         self.add_point_button.setFixedWidth(40)
 
 
@@ -173,9 +173,9 @@ class DblSidedTool(FlatCAMTool):
         self.ah_label.setToolTip(
         self.ah_label.setToolTip(
             "Alignment holes (x1, y1), (x2, y2), ... "
             "Alignment holes (x1, y1), (x2, y2), ... "
             "on one side of the mirror axis. For each set of (x, y) coordinates\n"
             "on one side of the mirror axis. For each set of (x, y) coordinates\n"
-            "entered here, a pair of drills will be created: one on the\n"
-            "coordinates entered and one in mirror position over the axis\n"
-            "selected above in the 'Mirror Axis'."
+            "entered here, a pair of drills will be created:\n\n"
+            "- one drill at the coordinates from the field\n"
+            "- one drill in mirror position over the axis selected above in the 'Mirror Axis'."
         )
         )
         self.layout.addWidget(self.ah_label)
         self.layout.addWidget(self.ah_label)
 
 
@@ -186,10 +186,13 @@ class DblSidedTool(FlatCAMTool):
 
 
         self.add_drill_point_button = QtWidgets.QPushButton("Add")
         self.add_drill_point_button = QtWidgets.QPushButton("Add")
         self.add_drill_point_button.setToolTip(
         self.add_drill_point_button.setToolTip(
-            "Add alignment drill holes coords (x1, y1), (x2, y2), ... \n"
-            "on one side of the mirror axis.\n"
-            "The point(s) can be captured by pressing SHIFT key\n"
-            "and left mouse clicking on canvas. Or you can enter them manually."
+            "Add alignment drill holes coords in the format: (x1, y1), (x2, y2), ... \n"
+            "on one side of the mirror axis.\n\n"
+            "The coordinates set can be obtained:\n"
+            "- press SHIFT key and left mouse clicking on canvas. Then click Add.\n"
+            "- press SHIFT key and left mouse clicking on canvas. Then CTRL+V in the field.\n"
+            "- press SHIFT key and left mouse clicking on canvas. Then RMB click in the field and click Paste.\n"
+            "- by entering the coords manually in the format: (x1, y1), (x2, y2), ..."
         )
         )
         self.add_drill_point_button.setFixedWidth(40)
         self.add_drill_point_button.setFixedWidth(40)
 
 
@@ -197,11 +200,10 @@ class DblSidedTool(FlatCAMTool):
         grid_lay1.addWidget(self.add_drill_point_button, 0, 3)
         grid_lay1.addWidget(self.add_drill_point_button, 0, 3)
 
 
         ## Drill diameter for alignment holes
         ## Drill diameter for alignment holes
-        self.dt_label = QtWidgets.QLabel("<b>Alignment Drill Creation</b>:")
+        self.dt_label = QtWidgets.QLabel("<b>Alignment Drill Diameter</b>:")
         self.dt_label.setToolTip(
         self.dt_label.setToolTip(
-            "Create a set of alignment drill holes\n"
-            "with the specified diameter,\n"
-            "at the specified coordinates."
+            "Diameter of the drill for the "
+            "alignment holes."
         )
         )
         self.layout.addWidget(self.dt_label)
         self.layout.addWidget(self.dt_label)
 
 
@@ -249,20 +251,19 @@ class DblSidedTool(FlatCAMTool):
 
 
         self.drill_values = ""
         self.drill_values = ""
 
 
-        self.set_ui()
-
     def install(self, icon=None, separator=None, **kwargs):
     def install(self, icon=None, separator=None, **kwargs):
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+D', **kwargs)
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+D', **kwargs)
 
 
     def run(self):
     def run(self):
-        FlatCAMTool.run(self)
+        self.app.report_usage("Tool2Sided()")
 
 
+        FlatCAMTool.run(self)
+        self.set_tool_ui()
         self.app.ui.notebook.setTabText(2, "2-Sided Tool")
         self.app.ui.notebook.setTabText(2, "2-Sided Tool")
+
+    def set_tool_ui(self):
         self.reset_fields()
         self.reset_fields()
-        self.set_ui()
 
 
-    def set_ui(self):
-        ## Initialize form
         self.point_entry.set_value("")
         self.point_entry.set_value("")
         self.alignment_holes.set_value("")
         self.alignment_holes.set_value("")
 
 
@@ -283,7 +284,7 @@ class DblSidedTool(FlatCAMTool):
             try:
             try:
                 px, py = self.point_entry.get_value()
                 px, py = self.point_entry.get_value()
             except TypeError:
             except TypeError:
-                self.app.inform.emit("[warning_notcl] 'Point' reference is selected and 'Point' coordinates "
+                self.app.inform.emit("[WARNING_NOTCL] 'Point' reference is selected and 'Point' coordinates "
                                      "are missing. Add them and retry.")
                                      "are missing. Add them and retry.")
                 return
                 return
         else:
         else:
@@ -297,12 +298,15 @@ class DblSidedTool(FlatCAMTool):
         xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
         xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
 
 
         dia = self.drill_dia.get_value()
         dia = self.drill_dia.get_value()
+        if dia is None:
+            self.app.inform.emit("[WARNING_NOTCL]No value or wrong format in Drill Dia entry. Add it and retry.")
+            return
         tools = {"1": {"C": dia}}
         tools = {"1": {"C": dia}}
 
 
         # holes = self.alignment_holes.get_value()
         # holes = self.alignment_holes.get_value()
         holes = eval('[{}]'.format(self.alignment_holes.text()))
         holes = eval('[{}]'.format(self.alignment_holes.text()))
         if not holes:
         if not holes:
-            self.app.inform.emit("[warning_notcl] There are no Alignment Drill Coordinates to use. Add them and retry.")
+            self.app.inform.emit("[WARNING_NOTCL] There are no Alignment Drill Coordinates to use. Add them and retry.")
             return
             return
 
 
         drills = []
         drills = []
@@ -328,11 +332,11 @@ class DblSidedTool(FlatCAMTool):
         try:
         try:
             fcobj = model_index.internalPointer().obj
             fcobj = model_index.internalPointer().obj
         except Exception as e:
         except Exception as e:
-            self.app.inform.emit("[warning_notcl] There is no Gerber object loaded ...")
+            self.app.inform.emit("[WARNING_NOTCL] There is no Gerber object loaded ...")
             return
             return
 
 
         if not isinstance(fcobj, FlatCAMGerber):
         if not isinstance(fcobj, FlatCAMGerber):
-            self.app.inform.emit("[error_notcl] Only Gerber, Excellon and Geometry objects can be mirrored.")
+            self.app.inform.emit("[ERROR_NOTCL] Only Gerber, Excellon and Geometry objects can be mirrored.")
             return
             return
 
 
         axis = self.mirror_axis.get_value()
         axis = self.mirror_axis.get_value()
@@ -342,7 +346,7 @@ class DblSidedTool(FlatCAMTool):
             try:
             try:
                 px, py = self.point_entry.get_value()
                 px, py = self.point_entry.get_value()
             except TypeError:
             except TypeError:
-                self.app.inform.emit("[warning_notcl] 'Point' coordinates missing. "
+                self.app.inform.emit("[WARNING_NOTCL] 'Point' coordinates missing. "
                                      "Using Origin (0, 0) as mirroring reference.")
                                      "Using Origin (0, 0) as mirroring reference.")
                 px, py = (0, 0)
                 px, py = (0, 0)
 
 
@@ -352,7 +356,7 @@ class DblSidedTool(FlatCAMTool):
             try:
             try:
                 bb_obj = model_index_box.internalPointer().obj
                 bb_obj = model_index_box.internalPointer().obj
             except Exception as e:
             except Exception as e:
-                self.app.inform.emit("[warning_notcl] There is no Box object loaded ...")
+                self.app.inform.emit("[WARNING_NOTCL] There is no Box object loaded ...")
                 return
                 return
 
 
             xmin, ymin, xmax, ymax = bb_obj.bounds()
             xmin, ymin, xmax, ymax = bb_obj.bounds()
@@ -370,11 +374,11 @@ class DblSidedTool(FlatCAMTool):
         try:
         try:
             fcobj = model_index.internalPointer().obj
             fcobj = model_index.internalPointer().obj
         except Exception as e:
         except Exception as e:
-            self.app.inform.emit("[warning_notcl] There is no Excellon object loaded ...")
+            self.app.inform.emit("[WARNING_NOTCL] There is no Excellon object loaded ...")
             return
             return
 
 
         if not isinstance(fcobj, FlatCAMExcellon):
         if not isinstance(fcobj, FlatCAMExcellon):
-            self.app.inform.emit("[error_notcl] Only Gerber, Excellon and Geometry objects can be mirrored.")
+            self.app.inform.emit("[ERROR_NOTCL] Only Gerber, Excellon and Geometry objects can be mirrored.")
             return
             return
 
 
         axis = self.mirror_axis.get_value()
         axis = self.mirror_axis.get_value()
@@ -388,7 +392,7 @@ class DblSidedTool(FlatCAMTool):
             try:
             try:
                 bb_obj = model_index_box.internalPointer().obj
                 bb_obj = model_index_box.internalPointer().obj
             except Exception as e:
             except Exception as e:
-                self.app.inform.emit("[warning_notcl] There is no Box object loaded ...")
+                self.app.inform.emit("[WARNING_NOTCL] There is no Box object loaded ...")
                 return
                 return
 
 
             xmin, ymin, xmax, ymax = bb_obj.bounds()
             xmin, ymin, xmax, ymax = bb_obj.bounds()
@@ -406,11 +410,11 @@ class DblSidedTool(FlatCAMTool):
         try:
         try:
             fcobj = model_index.internalPointer().obj
             fcobj = model_index.internalPointer().obj
         except Exception as e:
         except Exception as e:
-            self.app.inform.emit("[warning_notcl] There is no Geometry object loaded ...")
+            self.app.inform.emit("[WARNING_NOTCL] There is no Geometry object loaded ...")
             return
             return
 
 
         if not isinstance(fcobj, FlatCAMGeometry):
         if not isinstance(fcobj, FlatCAMGeometry):
-            self.app.inform.emit("[error_notcl] Only Gerber, Excellon and Geometry objects can be mirrored.")
+            self.app.inform.emit("[ERROR_NOTCL] Only Gerber, Excellon and Geometry objects can be mirrored.")
             return
             return
 
 
         axis = self.mirror_axis.get_value()
         axis = self.mirror_axis.get_value()
@@ -424,7 +428,7 @@ class DblSidedTool(FlatCAMTool):
             try:
             try:
                 bb_obj = model_index_box.internalPointer().obj
                 bb_obj = model_index_box.internalPointer().obj
             except Exception as e:
             except Exception as e:
-                self.app.inform.emit("[warning_notcl] There is no Box object loaded ...")
+                self.app.inform.emit("[WARNING_NOTCL] There is no Box object loaded ...")
                 return
                 return
 
 
             xmin, ymin, xmax, ymax = bb_obj.bounds()
             xmin, ymin, xmax, ymax = bb_obj.bounds()

+ 60 - 17
flatcamTools/ToolFilm.py

@@ -1,6 +1,6 @@
 from FlatCAMTool import FlatCAMTool
 from FlatCAMTool import FlatCAMTool
 
 
-from GUIElements import RadioSet, FloatEntry
+from GUIElements import RadioSet, FCEntry
 from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5 import QtGui, QtCore, QtWidgets
 
 
 
 
@@ -44,6 +44,7 @@ class Film(FlatCAMTool):
         self.tf_object_combo.setModel(self.app.collection)
         self.tf_object_combo.setModel(self.app.collection)
         self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.tf_object_combo.setCurrentIndex(1)
         self.tf_object_combo.setCurrentIndex(1)
+
         self.tf_object_label = QtWidgets.QLabel("Film Object:")
         self.tf_object_label = QtWidgets.QLabel("Film Object:")
         self.tf_object_label.setToolTip(
         self.tf_object_label.setToolTip(
             "Object for which to create the film."
             "Object for which to create the film."
@@ -101,7 +102,7 @@ class Film(FlatCAMTool):
 
 
         # Boundary for negative film generation
         # Boundary for negative film generation
 
 
-        self.boundary_entry = FloatEntry()
+        self.boundary_entry = FCEntry()
         self.boundary_label = QtWidgets.QLabel("Border:")
         self.boundary_label = QtWidgets.QLabel("Border:")
         self.boundary_label.setToolTip(
         self.boundary_label.setToolTip(
             "Specify a border around the object.\n"
             "Specify a border around the object.\n"
@@ -115,6 +116,15 @@ class Film(FlatCAMTool):
         )
         )
         tf_form_layout.addRow(self.boundary_label, self.boundary_entry)
         tf_form_layout.addRow(self.boundary_label, self.boundary_entry)
 
 
+        self.film_scale_entry = FCEntry()
+        self.film_scale_label = QtWidgets.QLabel("Scale Stroke:")
+        self.film_scale_label.setToolTip(
+            "Scale the line stroke thickness of each feature in the SVG file.\n"
+            "It means that the line that envelope each SVG feature will be thicker or thinner,\n"
+            "therefore the fine features may be more affected by this parameter."
+        )
+        tf_form_layout.addRow(self.film_scale_label, self.film_scale_entry)
+
         # Buttons
         # Buttons
         hlay = QtWidgets.QHBoxLayout()
         hlay = QtWidgets.QHBoxLayout()
         self.layout.addLayout(hlay)
         self.layout.addLayout(hlay)
@@ -136,10 +146,6 @@ class Film(FlatCAMTool):
         self.tf_type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
         self.tf_type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
         self.tf_type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed)
         self.tf_type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed)
 
 
-        ## Initialize form
-        self.film_type.set_value('neg')
-        self.boundary_entry.set_value(0.0)
-
     def on_type_obj_index_changed(self, index):
     def on_type_obj_index_changed(self, index):
         obj_type = self.tf_type_obj_combo.currentIndex()
         obj_type = self.tf_type_obj_combo.currentIndex()
         self.tf_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
         self.tf_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
@@ -151,25 +157,58 @@ class Film(FlatCAMTool):
         self.tf_box_combo.setCurrentIndex(0)
         self.tf_box_combo.setCurrentIndex(0)
 
 
     def run(self):
     def run(self):
+        self.app.report_usage("ToolFilm()")
+
         FlatCAMTool.run(self)
         FlatCAMTool.run(self)
+        self.set_tool_ui()
         self.app.ui.notebook.setTabText(2, "Film Tool")
         self.app.ui.notebook.setTabText(2, "Film Tool")
 
 
     def install(self, icon=None, separator=None, **kwargs):
     def install(self, icon=None, separator=None, **kwargs):
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+L', **kwargs)
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+L', **kwargs)
 
 
+    def set_tool_ui(self):
+        self.reset_fields()
+
+        f_type = self.app.defaults["tools_film_type"] if self.app.defaults["tools_film_type"] else 'neg'
+        self.film_type.set_value(str(f_type))
+
+        b_entry = self.app.defaults[ "tools_film_boundary"] if self.app.defaults[ "tools_film_boundary"] else 0.0
+        self.boundary_entry.set_value(float(b_entry))
+
+        scale_stroke_width = self.app.defaults["tools_film_scale"] if self.app.defaults["tools_film_scale"] else 0.0
+        self.film_scale_entry.set_value(int(scale_stroke_width))
+
     def on_film_creation(self):
     def on_film_creation(self):
         try:
         try:
             name = self.tf_object_combo.currentText()
             name = self.tf_object_combo.currentText()
         except:
         except:
-            self.app.inform.emit("[error_notcl] No Film object selected. Load a Film object and retry.")
+            self.app.inform.emit("[ERROR_NOTCL] No FlatCAM object selected. Load an object for Film and retry.")
             return
             return
+
         try:
         try:
             boxname = self.tf_box_combo.currentText()
             boxname = self.tf_box_combo.currentText()
         except:
         except:
-            self.app.inform.emit("[error_notcl] No Box object selected. Load a Box object and retry.")
+            self.app.inform.emit("[ERROR_NOTCL] No FlatCAM object selected. Load an object for Box and retry.")
+            return
+
+        try:
+            border = float(self.boundary_entry.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                border = float(self.boundary_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
+
+        try:
+            scale_stroke_width = int(self.film_scale_entry.get_value())
+        except ValueError:
+            self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                 "use a number.")
             return
             return
 
 
-        border = float(self.boundary_entry.get_value())
         if border is None:
         if border is None:
             border = 0
             border = 0
 
 
@@ -177,32 +216,36 @@ class Film(FlatCAMTool):
 
 
         if self.film_type.get_value() == "pos":
         if self.film_type.get_value() == "pos":
             try:
             try:
-                filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG positive",
-                                                             directory=self.app.get_last_save_folder(), filter="*.svg")
+                filename, _ = QtWidgets.QFileDialog.getSaveFileName(
+                    caption="Export SVG positive",
+                    directory=self.app.get_last_save_folder() + '/' + name,
+                    filter="*.svg")
             except TypeError:
             except TypeError:
                 filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG positive")
                 filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG positive")
 
 
             filename = str(filename)
             filename = str(filename)
 
 
             if str(filename) == "":
             if str(filename) == "":
-                self.app.inform.emit("Export SVG positive cancelled.")
+                self.app.inform.emit("[WARNING_NOTCL]Export SVG positive cancelled.")
                 return
                 return
             else:
             else:
-                self.app.export_svg_black(name, boxname, filename)
+                self.app.export_svg_black(name, boxname, filename, scale_factor=scale_stroke_width)
         else:
         else:
             try:
             try:
-                filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG negative",
-                                                             directory=self.app.get_last_save_folder(), filter="*.svg")
+                filename, _ = QtWidgets.QFileDialog.getSaveFileName(
+                    caption="Export SVG negative",
+                    directory=self.app.get_last_save_folder() + '/' + name,
+                    filter="*.svg")
             except TypeError:
             except TypeError:
                 filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG negative")
                 filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG negative")
 
 
             filename = str(filename)
             filename = str(filename)
 
 
             if str(filename) == "":
             if str(filename) == "":
-                self.app.inform.emit("Export SVG negative cancelled.")
+                self.app.inform.emit("[WARNING_NOTCL]Export SVG negative cancelled.")
                 return
                 return
             else:
             else:
-                self.app.export_svg_negative(name, boxname, filename, border)
+                self.app.export_svg_negative(name, boxname, filename, border, scale_factor=scale_stroke_width)
 
 
     def reset_fields(self):
     def reset_fields(self):
         self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))

+ 11 - 4
flatcamTools/ToolImage.py

@@ -124,6 +124,17 @@ class ToolImage(FlatCAMTool):
         ## Signals
         ## Signals
         self.import_button.clicked.connect(self.on_file_importimage)
         self.import_button.clicked.connect(self.on_file_importimage)
 
 
+    def run(self):
+        self.app.report_usage("ToolImage()")
+
+        FlatCAMTool.run(self)
+        self.set_tool_ui()
+        self.app.ui.notebook.setTabText(2, "Image Tool")
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, **kwargs)
+
+    def set_tool_ui(self):
         ## Initialize form
         ## Initialize form
         self.dpi_entry.set_value(96)
         self.dpi_entry.set_value(96)
         self.image_type.set_value('black')
         self.image_type.set_value('black')
@@ -132,10 +143,6 @@ class ToolImage(FlatCAMTool):
         self.mask_g_entry.set_value(250)
         self.mask_g_entry.set_value(250)
         self.mask_b_entry.set_value(250)
         self.mask_b_entry.set_value(250)
 
 
-    def run(self):
-        FlatCAMTool.run(self)
-        self.app.ui.notebook.setTabText(2, "Image Tool")
-
     def on_file_importimage(self):
     def on_file_importimage(self):
         """
         """
         Callback for menu item File->Import IMAGE.
         Callback for menu item File->Import IMAGE.

+ 20 - 16
flatcamTools/ToolMeasurement.py

@@ -152,11 +152,19 @@ class Measurement(FlatCAMTool):
         self.measure_btn.clicked.connect(self.toggle)
         self.measure_btn.clicked.connect(self.toggle)
 
 
     def run(self):
     def run(self):
+        self.app.report_usage("ToolMeasurement()")
+
         if self.app.tool_tab_locked is True:
         if self.app.tool_tab_locked is True:
             return
             return
-
         self.toggle()
         self.toggle()
 
 
+        self.set_tool_ui()
+        self.app.ui.notebook.setTabText(2, "Meas. Tool")
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='CTRL+M', **kwargs)
+
+    def set_tool_ui(self):
         # Remove anything else in the GUI
         # Remove anything else in the GUI
         self.app.ui.tool_scroll_area.takeWidget()
         self.app.ui.tool_scroll_area.takeWidget()
 
 
@@ -167,21 +175,6 @@ class Measurement(FlatCAMTool):
         self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
         self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
         self.units = self.app.general_options_form.general_app_group.units_radio.get_value().lower()
         self.units = self.app.general_options_form.general_app_group.units_radio.get_value().lower()
         self.show()
         self.show()
-        self.app.ui.notebook.setTabText(2, "Meas. Tool")
-
-    def install(self, icon=None, separator=None, **kwargs):
-        FlatCAMTool.install(self, icon, separator, shortcut='CTRL+M', **kwargs)
-
-    def on_key_release_meas(self, event):
-        if event.key == 'escape':
-            # abort the measurement action
-            self.toggle()
-            return
-
-        if event.key == 'G':
-            # toggle grid status
-            self.app.ui.grid_snap_btn.trigger()
-            return
 
 
     def toggle(self):
     def toggle(self):
         # the self.active var is doing the 'toggle'
         # the self.active var is doing the 'toggle'
@@ -264,6 +257,17 @@ class Measurement(FlatCAMTool):
 
 
             self.app.inform.emit("MEASURING: Click on the Start point ...")
             self.app.inform.emit("MEASURING: Click on the Start point ...")
 
 
+    def on_key_release_meas(self, event):
+        if event.key == 'escape':
+            # abort the measurement action
+            self.toggle()
+            return
+
+        if event.key == 'G':
+            # toggle grid status
+            self.app.ui.grid_snap_btn.trigger()
+            return
+
     def on_click_meas(self, event):
     def on_click_meas(self, event):
         # mouse click will be accepted only if the left button is clicked
         # mouse click will be accepted only if the left button is clicked
         # this is necessary because right mouse click and middle mouse click
         # this is necessary because right mouse click and middle mouse click

+ 39 - 37
flatcamTools/ToolMove.py

@@ -34,10 +34,44 @@ class ToolMove(FlatCAMTool):
         FlatCAMTool.install(self, icon, separator, shortcut='M', **kwargs)
         FlatCAMTool.install(self, icon, separator, shortcut='M', **kwargs)
 
 
     def run(self):
     def run(self):
+        self.app.report_usage("ToolMove()")
+
         if self.app.tool_tab_locked is True:
         if self.app.tool_tab_locked is True:
             return
             return
         self.toggle()
         self.toggle()
 
 
+    def toggle(self):
+        if self.isVisible():
+            self.setVisible(False)
+
+            self.app.plotcanvas.vis_disconnect('mouse_move', self.on_move)
+            self.app.plotcanvas.vis_disconnect('mouse_press', self.on_left_click)
+            self.app.plotcanvas.vis_disconnect('key_release', self.on_key_press)
+            self.app.plotcanvas.vis_connect('key_press', self.app.on_key_over_plot)
+
+            self.clicked_move = 0
+
+            # signal that there is no command active
+            self.app.command_active = None
+
+            # delete the selection box
+            self.delete_shape()
+            return
+        else:
+            self.setVisible(True)
+            # signal that there is a command active and it is 'Move'
+            self.app.command_active = "Move"
+
+            if self.app.collection.get_selected():
+                self.app.inform.emit("MOVE: Click on the Start point ...")
+                # draw the selection box
+                self.draw_sel_bbox()
+            else:
+                self.setVisible(False)
+                # signal that there is no command active
+                self.app.command_active = None
+                self.app.inform.emit("[WARNING_NOTCL]MOVE action cancelled. No object(s) to move.")
+
     def on_left_click(self, event):
     def on_left_click(self, event):
         # mouse click will be accepted only if the left button is clicked
         # mouse click will be accepted only if the left button is clicked
         # this is necessary because right mouse click and middle mouse click
         # this is necessary because right mouse click and middle mouse click
@@ -83,7 +117,7 @@ class ToolMove(FlatCAMTool):
 
 
                         try:
                         try:
                             if not obj_list:
                             if not obj_list:
-                                self.app.inform.emit("[warning_notcl] No object(s) selected.")
+                                self.app.inform.emit("[WARNING_NOTCL] No object(s) selected.")
                                 return "fail"
                                 return "fail"
                             else:
                             else:
                                 for sel_obj in obj_list:
                                 for sel_obj in obj_list:
@@ -99,7 +133,7 @@ class ToolMove(FlatCAMTool):
                                     # self.app.collection.set_active(sel_obj.options['name'])
                                     # self.app.collection.set_active(sel_obj.options['name'])
                         except Exception as e:
                         except Exception as e:
                             proc.done()
                             proc.done()
-                            self.app.inform.emit('[error_notcl] '
+                            self.app.inform.emit('[ERROR_NOTCL] '
                                                  'ToolMove.on_left_click() --> %s' % str(e))
                                                  'ToolMove.on_left_click() --> %s' % str(e))
                             return "fail"
                             return "fail"
                         proc.done()
                         proc.done()
@@ -114,7 +148,7 @@ class ToolMove(FlatCAMTool):
                     return
                     return
 
 
                 except TypeError:
                 except TypeError:
-                    self.app.inform.emit('[error_notcl] '
+                    self.app.inform.emit('[ERROR_NOTCL] '
                                          'ToolMove.on_left_click() --> Error when mouse left click.')
                                          'ToolMove.on_left_click() --> Error when mouse left click.')
                     return
                     return
 
 
@@ -142,42 +176,10 @@ class ToolMove(FlatCAMTool):
     def on_key_press(self, event):
     def on_key_press(self, event):
         if event.key == 'escape':
         if event.key == 'escape':
             # abort the move action
             # abort the move action
-            self.app.inform.emit("[warning_notcl]Move action cancelled.")
+            self.app.inform.emit("[WARNING_NOTCL]Move action cancelled.")
             self.toggle()
             self.toggle()
         return
         return
 
 
-    def toggle(self):
-        if self.isVisible():
-            self.setVisible(False)
-
-            self.app.plotcanvas.vis_disconnect('mouse_move', self.on_move)
-            self.app.plotcanvas.vis_disconnect('mouse_press', self.on_left_click)
-            self.app.plotcanvas.vis_disconnect('key_release', self.on_key_press)
-            self.app.plotcanvas.vis_connect('key_press', self.app.on_key_over_plot)
-
-            self.clicked_move = 0
-
-            # signal that there is no command active
-            self.app.command_active = None
-
-            # delete the selection box
-            self.delete_shape()
-            return
-        else:
-            self.setVisible(True)
-            # signal that there is a command active and it is 'Move'
-            self.app.command_active = "Move"
-
-            if self.app.collection.get_selected():
-                self.app.inform.emit("MOVE: Click on the Start point ...")
-                # draw the selection box
-                self.draw_sel_bbox()
-            else:
-                self.setVisible(False)
-                # signal that there is no command active
-                self.app.command_active = None
-                self.app.inform.emit("[warning_notcl]MOVE action cancelled. No object(s) to move.")
-
     def draw_sel_bbox(self):
     def draw_sel_bbox(self):
         xminlist = []
         xminlist = []
         yminlist = []
         yminlist = []
@@ -186,7 +188,7 @@ class ToolMove(FlatCAMTool):
 
 
         obj_list = self.app.collection.get_selected()
         obj_list = self.app.collection.get_selected()
         if not obj_list:
         if not obj_list:
-            self.app.inform.emit("[warning_notcl]Object(s) not selected")
+            self.app.inform.emit("[WARNING_NOTCL]Object(s) not selected")
             self.toggle()
             self.toggle()
         else:
         else:
             # if we have an object selected then we can safely activate the mouse events
             # if we have an object selected then we can safely activate the mouse events

+ 66 - 21
flatcamTools/ToolNonCopperClear.py

@@ -35,6 +35,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.object_combo.setModel(self.app.collection)
         self.object_combo.setModel(self.app.collection)
         self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.object_combo.setCurrentIndex(1)
         self.object_combo.setCurrentIndex(1)
+
         self.object_label = QtWidgets.QLabel("Gerber:")
         self.object_label = QtWidgets.QLabel("Gerber:")
         self.object_label.setToolTip(
         self.object_label.setToolTip(
             "Gerber object to be cleared of excess copper.                        "
             "Gerber object to be cleared of excess copper.                        "
@@ -97,7 +98,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.addtool_entry_lbl.setToolTip(
         self.addtool_entry_lbl.setToolTip(
             "Diameter for the new tool to add in the Tool Table"
             "Diameter for the new tool to add in the Tool Table"
         )
         )
-        self.addtool_entry = FloatEntry()
+        self.addtool_entry = FCEntry()
 
 
         # hlay.addWidget(self.addtool_label)
         # hlay.addWidget(self.addtool_label)
         # hlay.addStretch()
         # hlay.addStretch()
@@ -151,7 +152,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
             "due of too many paths."
             "due of too many paths."
         )
         )
         grid3.addWidget(nccoverlabel, 1, 0)
         grid3.addWidget(nccoverlabel, 1, 0)
-        self.ncc_overlap_entry = FloatEntry()
+        self.ncc_overlap_entry = FCEntry()
         grid3.addWidget(self.ncc_overlap_entry, 1, 1)
         grid3.addWidget(self.ncc_overlap_entry, 1, 1)
 
 
         nccmarginlabel = QtWidgets.QLabel('Margin:')
         nccmarginlabel = QtWidgets.QLabel('Margin:')
@@ -159,7 +160,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
             "Bounding box margin."
             "Bounding box margin."
         )
         )
         grid3.addWidget(nccmarginlabel, 2, 0)
         grid3.addWidget(nccmarginlabel, 2, 0)
-        self.ncc_margin_entry = FloatEntry()
+        self.ncc_margin_entry = FCEntry()
         grid3.addWidget(self.ncc_margin_entry, 2, 1)
         grid3.addWidget(self.ncc_margin_entry, 2, 1)
 
 
         # Method
         # Method
@@ -237,13 +238,16 @@ class NonCopperClear(FlatCAMTool, Gerber):
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+N', **kwargs)
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+N', **kwargs)
 
 
     def run(self):
     def run(self):
+        self.app.report_usage("ToolNonCopperClear()")
+
         FlatCAMTool.run(self)
         FlatCAMTool.run(self)
-        self.tools_frame.show()
-        self.set_ui()
+        self.set_tool_ui()
         self.build_ui()
         self.build_ui()
         self.app.ui.notebook.setTabText(2, "NCC Tool")
         self.app.ui.notebook.setTabText(2, "NCC Tool")
 
 
-    def set_ui(self):
+    def set_tool_ui(self):
+        self.tools_frame.show()
+
         self.ncc_overlap_entry.set_value(self.app.defaults["tools_nccoverlap"])
         self.ncc_overlap_entry.set_value(self.app.defaults["tools_nccoverlap"])
         self.ncc_margin_entry.set_value(self.app.defaults["tools_nccmargin"])
         self.ncc_margin_entry.set_value(self.app.defaults["tools_nccmargin"])
         self.ncc_method_radio.set_value(self.app.defaults["tools_nccmethod"])
         self.ncc_method_radio.set_value(self.app.defaults["tools_nccmethod"])
@@ -408,7 +412,6 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.tools_table.setMinimumHeight(self.tools_table.getHeight())
         self.tools_table.setMinimumHeight(self.tools_table.getHeight())
         self.tools_table.setMaximumHeight(self.tools_table.getHeight())
         self.tools_table.setMaximumHeight(self.tools_table.getHeight())
 
 
-        self.app.report_usage("gerber_on_ncc_button")
         self.ui_connect()
         self.ui_connect()
 
 
     def ui_connect(self):
     def ui_connect(self):
@@ -428,10 +431,19 @@ class NonCopperClear(FlatCAMTool, Gerber):
         if dia:
         if dia:
             tool_dia = dia
             tool_dia = dia
         else:
         else:
-            tool_dia = self.addtool_entry.get_value()
+            try:
+                tool_dia = float(self.addtool_entry.get_value())
+            except ValueError:
+                # try to convert comma to decimal point. if it's still not working error message and return
+                try:
+                    tool_dia = float(self.addtool_entry.get_value().replace(',', '.'))
+                except ValueError:
+                    self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                         "use a number.")
+                    return
             if tool_dia is None:
             if tool_dia is None:
                 self.build_ui()
                 self.build_ui()
-                self.app.inform.emit("[warning_notcl] Please enter a tool diameter to add, in Float format.")
+                self.app.inform.emit("[WARNING_NOTCL] Please enter a tool diameter to add, in Float format.")
                 return
                 return
 
 
         # construct a list of all 'tooluid' in the self.tools
         # construct a list of all 'tooluid' in the self.tools
@@ -455,7 +467,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
 
         if float('%.4f' % tool_dia) in tool_dias:
         if float('%.4f' % tool_dia) in tool_dias:
             if muted is None:
             if muted is None:
-                self.app.inform.emit("[warning_notcl]Adding tool cancelled. Tool already in Tool Table.")
+                self.app.inform.emit("[WARNING_NOTCL]Adding tool cancelled. Tool already in Tool Table.")
             self.tools_table.itemChanged.connect(self.on_tool_edit)
             self.tools_table.itemChanged.connect(self.on_tool_edit)
             return
             return
         else:
         else:
@@ -485,7 +497,18 @@ class NonCopperClear(FlatCAMTool, Gerber):
                     tool_dias.append(float('%.4f' % v[tool_v]))
                     tool_dias.append(float('%.4f' % v[tool_v]))
 
 
         for row in range(self.tools_table.rowCount()):
         for row in range(self.tools_table.rowCount()):
-            new_tool_dia = float(self.tools_table.item(row, 1).text())
+
+            try:
+                new_tool_dia = float(self.tools_table.item(row, 1).text())
+            except ValueError:
+                # try to convert comma to decimal point. if it's still not working error message and return
+                try:
+                    new_tool_dia = float(self.tools_table.item(row, 1).text().replace(',', '.'))
+                except ValueError:
+                    self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                         "use a number.")
+                    return
+
             tooluid = int(self.tools_table.item(row, 3).text())
             tooluid = int(self.tools_table.item(row, 3).text())
 
 
             # identify the tool that was edited and get it's tooluid
             # identify the tool that was edited and get it's tooluid
@@ -502,7 +525,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
                         break
                         break
                 restore_dia_item = self.tools_table.item(row, 1)
                 restore_dia_item = self.tools_table.item(row, 1)
                 restore_dia_item.setText(str(old_tool_dia))
                 restore_dia_item.setText(str(old_tool_dia))
-                self.app.inform.emit("[warning_notcl] Edit cancelled. New diameter value is already in the Tool Table.")
+                self.app.inform.emit("[WARNING_NOTCL] Edit cancelled. New diameter value is already in the Tool Table.")
         self.build_ui()
         self.build_ui()
 
 
     def on_tool_delete(self, rows_to_delete=None, all=None):
     def on_tool_delete(self, rows_to_delete=None, all=None):
@@ -541,7 +564,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
                     self.ncc_tools.pop(t, None)
                     self.ncc_tools.pop(t, None)
 
 
         except AttributeError:
         except AttributeError:
-            self.app.inform.emit("[warning_notcl]Delete failed. Select a tool to delete.")
+            self.app.inform.emit("[WARNING_NOTCL]Delete failed. Select a tool to delete.")
             return
             return
         except Exception as e:
         except Exception as e:
             log.debug(str(e))
             log.debug(str(e))
@@ -551,10 +574,28 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
 
     def on_ncc(self):
     def on_ncc(self):
 
 
-        over = self.ncc_overlap_entry.get_value()
+        try:
+            over = float(self.ncc_overlap_entry.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                over = float(self.ncc_overlap_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
         over = over if over else self.app.defaults["tools_nccoverlap"]
         over = over if over else self.app.defaults["tools_nccoverlap"]
 
 
-        margin = self.ncc_margin_entry.get_value()
+        try:
+            margin = float(self.ncc_margin_entry.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                margin = float(self.ncc_margin_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
         margin = margin if margin else self.app.defaults["tools_nccmargin"]
         margin = margin if margin else self.app.defaults["tools_nccmargin"]
 
 
         connect = self.ncc_connect_cb.get_value()
         connect = self.ncc_connect_cb.get_value()
@@ -574,7 +615,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         try:
         try:
             self.ncc_obj = self.app.collection.get_by_name(self.obj_name)
             self.ncc_obj = self.app.collection.get_by_name(self.obj_name)
         except:
         except:
-            self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % self.obj_name)
+            self.app.inform.emit("[ERROR_NOTCL]Could not retrieve object: %s" % self.obj_name)
             return "Could not retrieve object: %s" % self.obj_name
             return "Could not retrieve object: %s" % self.obj_name
 
 
 
 
@@ -582,7 +623,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         try:
         try:
             bounding_box = self.ncc_obj.solid_geometry.envelope.buffer(distance=margin, join_style=JOIN_STYLE.mitre)
             bounding_box = self.ncc_obj.solid_geometry.envelope.buffer(distance=margin, join_style=JOIN_STYLE.mitre)
         except AttributeError:
         except AttributeError:
-            self.app.inform.emit("[error_notcl]No Gerber file available.")
+            self.app.inform.emit("[ERROR_NOTCL]No Gerber file available.")
             return
             return
 
 
         # calculate the empty area by substracting the solid_geometry from the object bounding box geometry
         # calculate the empty area by substracting the solid_geometry from the object bounding box geometry
@@ -707,14 +748,14 @@ class NonCopperClear(FlatCAMTool, Gerber):
                 app_obj.new_object("geometry", name, initialize)
                 app_obj.new_object("geometry", name, initialize)
             except Exception as e:
             except Exception as e:
                 proc.done()
                 proc.done()
-                self.app.inform.emit('[error_notcl] NCCTool.clear_non_copper() --> %s' % str(e))
+                self.app.inform.emit('[ERROR_NOTCL] NCCTool.clear_non_copper() --> %s' % str(e))
                 return
                 return
             proc.done()
             proc.done()
 
 
             if app_obj.poly_not_cleared is False:
             if app_obj.poly_not_cleared is False:
                 self.app.inform.emit('[success] NCC Tool finished.')
                 self.app.inform.emit('[success] NCC Tool finished.')
             else:
             else:
-                self.app.inform.emit('[warning_notcl] NCC Tool finished but some PCB features could not be cleared. '
+                self.app.inform.emit('[WARNING_NOTCL] NCC Tool finished but some PCB features could not be cleared. '
                                      'Check the result.')
                                      'Check the result.')
             # reset the variable for next use
             # reset the variable for next use
             app_obj.poly_not_cleared = False
             app_obj.poly_not_cleared = False
@@ -858,7 +899,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
                 app_obj.new_object("geometry", name, initialize_rm)
                 app_obj.new_object("geometry", name, initialize_rm)
             except Exception as e:
             except Exception as e:
                 proc.done()
                 proc.done()
-                self.app.inform.emit('[error_notcl] NCCTool.clear_non_copper_rest() --> %s' % str(e))
+                self.app.inform.emit('[ERROR_NOTCL] NCCTool.clear_non_copper_rest() --> %s' % str(e))
                 return
                 return
 
 
             if app_obj.poly_not_cleared is True:
             if app_obj.poly_not_cleared is True:
@@ -866,7 +907,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
                 # focus on Selected Tab
                 # focus on Selected Tab
                 self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
                 self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
             else:
             else:
-                self.app.inform.emit('[error_notcl] NCC Tool finished but could not clear the object '
+                self.app.inform.emit('[ERROR_NOTCL] NCC Tool finished but could not clear the object '
                                      'with current settings.')
                                      'with current settings.')
                 # focus on Project Tab
                 # focus on Project Tab
                 self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
                 self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
@@ -882,3 +923,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
 
         # Background
         # Background
         self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
         self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
+
+    def reset_fields(self):
+        self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+

+ 86 - 31
flatcamTools/ToolPaint.py

@@ -33,6 +33,7 @@ class ToolPaint(FlatCAMTool, Gerber):
         self.object_combo.setModel(self.app.collection)
         self.object_combo.setModel(self.app.collection)
         self.object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
         self.object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
         self.object_combo.setCurrentIndex(1)
         self.object_combo.setCurrentIndex(1)
+
         self.object_label = QtWidgets.QLabel("Geometry:")
         self.object_label = QtWidgets.QLabel("Geometry:")
         self.object_label.setToolTip(
         self.object_label.setToolTip(
             "Geometry object to be painted.                        "
             "Geometry object to be painted.                        "
@@ -94,7 +95,7 @@ class ToolPaint(FlatCAMTool, Gerber):
         self.addtool_entry_lbl.setToolTip(
         self.addtool_entry_lbl.setToolTip(
             "Diameter for the new tool."
             "Diameter for the new tool."
         )
         )
-        self.addtool_entry = FloatEntry()
+        self.addtool_entry = FCEntry()
 
 
         # hlay.addWidget(self.addtool_label)
         # hlay.addWidget(self.addtool_label)
         # hlay.addStretch()
         # hlay.addStretch()
@@ -146,7 +147,7 @@ class ToolPaint(FlatCAMTool, Gerber):
             "due of too many paths."
             "due of too many paths."
         )
         )
         grid3.addWidget(ovlabel, 1, 0)
         grid3.addWidget(ovlabel, 1, 0)
-        self.paintoverlap_entry = LengthEntry()
+        self.paintoverlap_entry = FCEntry()
         grid3.addWidget(self.paintoverlap_entry, 1, 1)
         grid3.addWidget(self.paintoverlap_entry, 1, 1)
 
 
         # Margin
         # Margin
@@ -157,7 +158,7 @@ class ToolPaint(FlatCAMTool, Gerber):
             "be painted."
             "be painted."
         )
         )
         grid3.addWidget(marginlabel, 2, 0)
         grid3.addWidget(marginlabel, 2, 0)
-        self.paintmargin_entry = LengthEntry()
+        self.paintmargin_entry = FCEntry()
         grid3.addWidget(self.paintmargin_entry, 2, 1)
         grid3.addWidget(self.paintmargin_entry, 2, 1)
 
 
         # Method
         # Method
@@ -294,9 +295,10 @@ class ToolPaint(FlatCAMTool, Gerber):
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+P', **kwargs)
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+P', **kwargs)
 
 
     def run(self):
     def run(self):
+        self.app.report_usage("ToolPaint()")
+
         FlatCAMTool.run(self)
         FlatCAMTool.run(self)
-        self.tools_frame.show()
-        self.set_ui()
+        self.set_tool_ui()
         self.app.ui.notebook.setTabText(2, "Paint Tool")
         self.app.ui.notebook.setTabText(2, "Paint Tool")
 
 
     def on_radio_selection(self):
     def on_radio_selection(self):
@@ -320,7 +322,10 @@ class ToolPaint(FlatCAMTool, Gerber):
             self.deltool_btn.setDisabled(False)
             self.deltool_btn.setDisabled(False)
             self.tools_table.setContextMenuPolicy(Qt.ActionsContextMenu)
             self.tools_table.setContextMenuPolicy(Qt.ActionsContextMenu)
 
 
-    def set_ui(self):
+    def set_tool_ui(self):
+        self.tools_frame.show()
+        self.reset_fields()
+
         ## Init the GUI interface
         ## Init the GUI interface
         self.paintmargin_entry.set_value(self.default_data["paintmargin"])
         self.paintmargin_entry.set_value(self.default_data["paintmargin"])
         self.paintmethod_combo.set_value(self.default_data["paintmethod"])
         self.paintmethod_combo.set_value(self.default_data["paintmethod"])
@@ -484,10 +489,20 @@ class ToolPaint(FlatCAMTool, Gerber):
         if dia:
         if dia:
             tool_dia = dia
             tool_dia = dia
         else:
         else:
-            tool_dia = self.addtool_entry.get_value()
+            try:
+                tool_dia = float(self.addtool_entry.get_value())
+            except ValueError:
+                # try to convert comma to decimal point. if it's still not working error message and return
+                try:
+                    tool_dia = float(self.addtool_entry.get_value().replace(',', '.'))
+                except ValueError:
+                    self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                         "use a number.")
+                    return
+
             if tool_dia is None:
             if tool_dia is None:
                 self.build_ui()
                 self.build_ui()
-                self.app.inform.emit("[warning_notcl] Please enter a tool diameter to add, in Float format.")
+                self.app.inform.emit("[WARNING_NOTCL] Please enter a tool diameter to add, in Float format.")
                 return
                 return
 
 
         # construct a list of all 'tooluid' in the self.tools
         # construct a list of all 'tooluid' in the self.tools
@@ -511,7 +526,7 @@ class ToolPaint(FlatCAMTool, Gerber):
 
 
         if float('%.4f' % tool_dia) in tool_dias:
         if float('%.4f' % tool_dia) in tool_dias:
             if muted is None:
             if muted is None:
-                self.app.inform.emit("[warning_notcl]Adding tool cancelled. Tool already in Tool Table.")
+                self.app.inform.emit("[WARNING_NOTCL]Adding tool cancelled. Tool already in Tool Table.")
             self.tools_table.itemChanged.connect(self.on_tool_edit)
             self.tools_table.itemChanged.connect(self.on_tool_edit)
             return
             return
         else:
         else:
@@ -544,7 +559,16 @@ class ToolPaint(FlatCAMTool, Gerber):
                     tool_dias.append(float('%.4f' % v[tool_v]))
                     tool_dias.append(float('%.4f' % v[tool_v]))
 
 
         for row in range(self.tools_table.rowCount()):
         for row in range(self.tools_table.rowCount()):
-            new_tool_dia = float(self.tools_table.item(row, 1).text())
+            try:
+                new_tool_dia = float(self.tools_table.item(row, 1).text())
+            except ValueError:
+                # try to convert comma to decimal point. if it's still not working error message and return
+                try:
+                    new_tool_dia = float(self.tools_table.item(row, 1).text().replace(',', '.'))
+                except ValueError:
+                    self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                         "use a number.")
+                    return
             tooluid = int(self.tools_table.item(row, 3).text())
             tooluid = int(self.tools_table.item(row, 3).text())
 
 
             # identify the tool that was edited and get it's tooluid
             # identify the tool that was edited and get it's tooluid
@@ -561,7 +585,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                         break
                         break
                 restore_dia_item = self.tools_table.item(row, 1)
                 restore_dia_item = self.tools_table.item(row, 1)
                 restore_dia_item.setText(str(old_tool_dia))
                 restore_dia_item.setText(str(old_tool_dia))
-                self.app.inform.emit("[warning_notcl] Edit cancelled. New diameter value is already in the Tool Table.")
+                self.app.inform.emit("[WARNING_NOTCL] Edit cancelled. New diameter value is already in the Tool Table.")
         self.build_ui()
         self.build_ui()
 
 
     # def on_tool_copy(self, all=None):
     # def on_tool_copy(self, all=None):
@@ -594,7 +618,7 @@ class ToolPaint(FlatCAMTool, Gerber):
     #                         print("COPIED", self.paint_tools[td])
     #                         print("COPIED", self.paint_tools[td])
     #                     self.build_ui()
     #                     self.build_ui()
     #                 except AttributeError:
     #                 except AttributeError:
-    #                     self.app.inform.emit("[warning_notcl]Failed. Select a tool to copy.")
+    #                     self.app.inform.emit("[WARNING_NOTCL]Failed. Select a tool to copy.")
     #                     self.build_ui()
     #                     self.build_ui()
     #                     return
     #                     return
     #                 except Exception as e:
     #                 except Exception as e:
@@ -602,7 +626,7 @@ class ToolPaint(FlatCAMTool, Gerber):
     #             # deselect the table
     #             # deselect the table
     #             # self.ui.geo_tools_table.clearSelection()
     #             # self.ui.geo_tools_table.clearSelection()
     #         else:
     #         else:
-    #             self.app.inform.emit("[warning_notcl]Failed. Select a tool to copy.")
+    #             self.app.inform.emit("[WARNING_NOTCL]Failed. Select a tool to copy.")
     #             self.build_ui()
     #             self.build_ui()
     #             return
     #             return
     #     else:
     #     else:
@@ -658,7 +682,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                     self.paint_tools.pop(t, None)
                     self.paint_tools.pop(t, None)
 
 
         except AttributeError:
         except AttributeError:
-            self.app.inform.emit("[warning_notcl]Delete failed. Select a tool to delete.")
+            self.app.inform.emit("[WARNING_NOTCL]Delete failed. Select a tool to delete.")
             return
             return
         except Exception as e:
         except Exception as e:
             log.debug(str(e))
             log.debug(str(e))
@@ -669,9 +693,18 @@ class ToolPaint(FlatCAMTool, Gerber):
     def on_paint_button_click(self):
     def on_paint_button_click(self):
         self.app.report_usage("geometry_on_paint_button")
         self.app.report_usage("geometry_on_paint_button")
 
 
-        self.app.inform.emit("[warning_notcl]Click inside the desired polygon.")
+        self.app.inform.emit("[WARNING_NOTCL]Click inside the desired polygon.")
+        try:
+            overlap = float(self.paintoverlap_entry.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                overlap = float(self.paintoverlap_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
 
 
-        overlap = self.paintoverlap_entry.get_value()
         connect = self.pathconnect_cb.get_value()
         connect = self.pathconnect_cb.get_value()
         contour = self.paintcontour_cb.get_value()
         contour = self.paintcontour_cb.get_value()
         select_method = self.selectmethod_combo.get_value()
         select_method = self.selectmethod_combo.get_value()
@@ -682,11 +715,11 @@ class ToolPaint(FlatCAMTool, Gerber):
         try:
         try:
             self.paint_obj = self.app.collection.get_by_name(str(self.obj_name))
             self.paint_obj = self.app.collection.get_by_name(str(self.obj_name))
         except:
         except:
-            self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % self.obj_name)
+            self.app.inform.emit("[ERROR_NOTCL]Could not retrieve object: %s" % self.obj_name)
             return
             return
 
 
         if self.paint_obj is None:
         if self.paint_obj is None:
-            self.app.inform.emit("[error_notcl]Object not found: %s" % self.paint_obj)
+            self.app.inform.emit("[ERROR_NOTCL]Object not found: %s" % self.paint_obj)
             return
             return
 
 
         o_name = '%s_multitool_paint' % (self.obj_name)
         o_name = '%s_multitool_paint' % (self.obj_name)
@@ -699,7 +732,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                                 contour=contour)
                                 contour=contour)
 
 
         if select_method == "single":
         if select_method == "single":
-            self.app.inform.emit("[warning_notcl]Click inside the desired polygon.")
+            self.app.inform.emit("[WARNING_NOTCL]Click inside the desired polygon.")
 
 
             # use the first tool in the tool table; get the diameter
             # use the first tool in the tool table; get the diameter
             tooldia = float('%.4f' % float(self.tools_table.item(0, 1).text()))
             tooldia = float('%.4f' % float(self.tools_table.item(0, 1).text()))
@@ -742,12 +775,22 @@ class ToolPaint(FlatCAMTool, Gerber):
         # poly = find_polygon(self.solid_geometry, inside_pt)
         # poly = find_polygon(self.solid_geometry, inside_pt)
         poly = obj.find_polygon(inside_pt)
         poly = obj.find_polygon(inside_pt)
         paint_method = self.paintmethod_combo.get_value()
         paint_method = self.paintmethod_combo.get_value()
-        paint_margin = self.paintmargin_entry.get_value()
+
+        try:
+            paint_margin = float(self.paintmargin_entry.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                paint_margin = float(self.paintmargin_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
 
 
         # No polygon?
         # No polygon?
         if poly is None:
         if poly is None:
             self.app.log.warning('No polygon found.')
             self.app.log.warning('No polygon found.')
-            self.app.inform.emit('[warning] No polygon found.')
+            self.app.inform.emit('[WARNING] No polygon found.')
             return
             return
 
 
         proc = self.app.proc_container.new("Painting polygon.")
         proc = self.app.proc_container.new("Painting polygon.")
@@ -792,7 +835,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                     geo_obj.solid_geometry += list(cp.get_objects())
                     geo_obj.solid_geometry += list(cp.get_objects())
                     return cp
                     return cp
                 else:
                 else:
-                    self.app.inform.emit('[error_notcl] Geometry could not be painted completely')
+                    self.app.inform.emit('[ERROR_NOTCL] Geometry could not be painted completely')
                     return None
                     return None
 
 
             geo_obj.solid_geometry = []
             geo_obj.solid_geometry = []
@@ -807,7 +850,7 @@ class ToolPaint(FlatCAMTool, Gerber):
             except Exception as e:
             except Exception as e:
                 log.debug("Could not Paint the polygons. %s" % str(e))
                 log.debug("Could not Paint the polygons. %s" % str(e))
                 self.app.inform.emit(
                 self.app.inform.emit(
-                    "[error] Could not do Paint. Try a different combination of parameters. "
+                    "[ERROR] Could not do Paint. Try a different combination of parameters. "
                     "Or a different strategy of paint\n%s" % str(e))
                     "Or a different strategy of paint\n%s" % str(e))
                 return
                 return
 
 
@@ -838,7 +881,7 @@ class ToolPaint(FlatCAMTool, Gerber):
             #     self.app.inform.emit("[success] Paint single polygon Done")
             #     self.app.inform.emit("[success] Paint single polygon Done")
             # else:
             # else:
             #     print("[WARNING] Paint single polygon done with errors")
             #     print("[WARNING] Paint single polygon done with errors")
-            #     self.app.inform.emit("[warning] Paint single polygon done with errors. "
+            #     self.app.inform.emit("[WARNING] Paint single polygon done with errors. "
             #                          "%d area(s) could not be painted.\n"
             #                          "%d area(s) could not be painted.\n"
             #                          "Use different paint parameters or edit the paint geometry and correct"
             #                          "Use different paint parameters or edit the paint geometry and correct"
             #                          "the issue."
             #                          "the issue."
@@ -849,7 +892,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                 app_obj.new_object("geometry", name, gen_paintarea)
                 app_obj.new_object("geometry", name, gen_paintarea)
             except Exception as e:
             except Exception as e:
                 proc.done()
                 proc.done()
-                self.app.inform.emit('[error_notcl] PaintTool.paint_poly() --> %s' % str(e))
+                self.app.inform.emit('[ERROR_NOTCL] PaintTool.paint_poly() --> %s' % str(e))
                 return
                 return
             proc.done()
             proc.done()
             # focus on Selected Tab
             # focus on Selected Tab
@@ -876,10 +919,19 @@ class ToolPaint(FlatCAMTool, Gerber):
         :return:
         :return:
         """
         """
         paint_method = self.paintmethod_combo.get_value()
         paint_method = self.paintmethod_combo.get_value()
-        paint_margin = self.paintmargin_entry.get_value()
 
 
-        proc = self.app.proc_container.new("Painting polygon.")
+        try:
+            paint_margin = float(self.paintmargin_entry.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                paint_margin = float(self.paintmargin_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
 
 
+        proc = self.app.proc_container.new("Painting polygon.")
         name = outname if outname else self.obj_name + "_paint"
         name = outname if outname else self.obj_name + "_paint"
         over = overlap
         over = overlap
         conn = connect
         conn = connect
@@ -984,7 +1036,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                     except Exception as e:
                     except Exception as e:
                         log.debug("Could not Paint the polygons. %s" % str(e))
                         log.debug("Could not Paint the polygons. %s" % str(e))
                         self.app.inform.emit(
                         self.app.inform.emit(
-                            "[error] Could not do Paint All. Try a different combination of parameters. "
+                            "[ERROR] Could not do Paint All. Try a different combination of parameters. "
                             "Or a different Method of paint\n%s" % str(e))
                             "Or a different Method of paint\n%s" % str(e))
                         return
                         return
 
 
@@ -1008,7 +1060,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                 if geo_obj.tools[tooluid]['solid_geometry']:
                 if geo_obj.tools[tooluid]['solid_geometry']:
                     has_solid_geo += 1
                     has_solid_geo += 1
             if has_solid_geo == 0:
             if has_solid_geo == 0:
-                self.app.inform.emit("[error] There is no Painting Geometry in the file.\n"
+                self.app.inform.emit("[ERROR] There is no Painting Geometry in the file.\n"
                                       "Usually it means that the tool diameter is too big for the painted geometry.\n"
                                       "Usually it means that the tool diameter is too big for the painted geometry.\n"
                                       "Change the painting parameters and try again.")
                                       "Change the painting parameters and try again.")
                 return
                 return
@@ -1063,7 +1115,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                     except Exception as e:
                     except Exception as e:
                         log.debug("Could not Paint the polygons. %s" % str(e))
                         log.debug("Could not Paint the polygons. %s" % str(e))
                         self.app.inform.emit(
                         self.app.inform.emit(
-                            "[error] Could not do Paint All. Try a different combination of parameters. "
+                            "[ERROR] Could not do Paint All. Try a different combination of parameters. "
                             "Or a different Method of paint\n%s" % str(e))
                             "Or a different Method of paint\n%s" % str(e))
                         return
                         return
 
 
@@ -1093,7 +1145,7 @@ class ToolPaint(FlatCAMTool, Gerber):
                 if geo_obj.tools[tooluid]['solid_geometry']:
                 if geo_obj.tools[tooluid]['solid_geometry']:
                     has_solid_geo += 1
                     has_solid_geo += 1
             if has_solid_geo == 0:
             if has_solid_geo == 0:
-                self.app.inform.emit("[error_notcl] There is no Painting Geometry in the file.\n"
+                self.app.inform.emit("[ERROR_NOTCL] There is no Painting Geometry in the file.\n"
                                       "Usually it means that the tool diameter is too big for the painted geometry.\n"
                                       "Usually it means that the tool diameter is too big for the painted geometry.\n"
                                       "Change the painting parameters and try again.")
                                       "Change the painting parameters and try again.")
                 return
                 return
@@ -1125,3 +1177,6 @@ class ToolPaint(FlatCAMTool, Gerber):
 
 
         # Background
         # Background
         self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
         self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
+
+    def reset_fields(self):
+        self.object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))

+ 119 - 32
flatcamTools/ToolPanelize.py

@@ -44,6 +44,7 @@ class Panelize(FlatCAMTool):
         self.object_combo.setModel(self.app.collection)
         self.object_combo.setModel(self.app.collection)
         self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.object_combo.setCurrentIndex(1)
         self.object_combo.setCurrentIndex(1)
+
         self.object_label = QtWidgets.QLabel("Object:")
         self.object_label = QtWidgets.QLabel("Object:")
         self.object_label.setToolTip(
         self.object_label.setToolTip(
             "Object to be panelized. This means that it will\n"
             "Object to be panelized. This means that it will\n"
@@ -76,6 +77,7 @@ class Panelize(FlatCAMTool):
         self.box_combo.setModel(self.app.collection)
         self.box_combo.setModel(self.app.collection)
         self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.box_combo.setCurrentIndex(1)
         self.box_combo.setCurrentIndex(1)
+
         self.box_combo_label = QtWidgets.QLabel("Box Object:")
         self.box_combo_label = QtWidgets.QLabel("Box Object:")
         self.box_combo_label.setToolTip(
         self.box_combo_label.setToolTip(
             "The actual object that is used a container for the\n "
             "The actual object that is used a container for the\n "
@@ -84,8 +86,7 @@ class Panelize(FlatCAMTool):
         form_layout.addRow(self.box_combo_label, self.box_combo)
         form_layout.addRow(self.box_combo_label, self.box_combo)
 
 
         ## Spacing Columns
         ## Spacing Columns
-        self.spacing_columns = FloatEntry()
-        self.spacing_columns.set_value(0.0)
+        self.spacing_columns = FCEntry()
         self.spacing_columns_label = QtWidgets.QLabel("Spacing cols:")
         self.spacing_columns_label = QtWidgets.QLabel("Spacing cols:")
         self.spacing_columns_label.setToolTip(
         self.spacing_columns_label.setToolTip(
             "Spacing between columns of the desired panel.\n"
             "Spacing between columns of the desired panel.\n"
@@ -94,8 +95,7 @@ class Panelize(FlatCAMTool):
         form_layout.addRow(self.spacing_columns_label, self.spacing_columns)
         form_layout.addRow(self.spacing_columns_label, self.spacing_columns)
 
 
         ## Spacing Rows
         ## Spacing Rows
-        self.spacing_rows = FloatEntry()
-        self.spacing_rows.set_value(0.0)
+        self.spacing_rows = FCEntry()
         self.spacing_rows_label = QtWidgets.QLabel("Spacing rows:")
         self.spacing_rows_label = QtWidgets.QLabel("Spacing rows:")
         self.spacing_rows_label.setToolTip(
         self.spacing_rows_label.setToolTip(
             "Spacing between rows of the desired panel.\n"
             "Spacing between rows of the desired panel.\n"
@@ -104,8 +104,7 @@ class Panelize(FlatCAMTool):
         form_layout.addRow(self.spacing_rows_label, self.spacing_rows)
         form_layout.addRow(self.spacing_rows_label, self.spacing_rows)
 
 
         ## Columns
         ## Columns
-        self.columns = IntEntry()
-        self.columns.set_value(1)
+        self.columns = FCEntry()
         self.columns_label = QtWidgets.QLabel("Columns:")
         self.columns_label = QtWidgets.QLabel("Columns:")
         self.columns_label.setToolTip(
         self.columns_label.setToolTip(
             "Number of columns of the desired panel"
             "Number of columns of the desired panel"
@@ -113,8 +112,7 @@ class Panelize(FlatCAMTool):
         form_layout.addRow(self.columns_label, self.columns)
         form_layout.addRow(self.columns_label, self.columns)
 
 
         ## Rows
         ## Rows
-        self.rows = IntEntry()
-        self.rows.set_value(1)
+        self.rows = FCEntry()
         self.rows_label = QtWidgets.QLabel("Rows:")
         self.rows_label = QtWidgets.QLabel("Rows:")
         self.rows_label.setToolTip(
         self.rows_label.setToolTip(
             "Number of rows of the desired panel"
             "Number of rows of the desired panel"
@@ -132,8 +130,7 @@ class Panelize(FlatCAMTool):
         )
         )
         form_layout.addRow(self.constrain_cb)
         form_layout.addRow(self.constrain_cb)
 
 
-        self.x_width_entry = FloatEntry()
-        self.x_width_entry.set_value(0.0)
+        self.x_width_entry = FCEntry()
         self.x_width_lbl = QtWidgets.QLabel("Width (DX):")
         self.x_width_lbl = QtWidgets.QLabel("Width (DX):")
         self.x_width_lbl.setToolTip(
         self.x_width_lbl.setToolTip(
             "The width (DX) within which the panel must fit.\n"
             "The width (DX) within which the panel must fit.\n"
@@ -141,8 +138,7 @@ class Panelize(FlatCAMTool):
         )
         )
         form_layout.addRow(self.x_width_lbl, self.x_width_entry)
         form_layout.addRow(self.x_width_lbl, self.x_width_entry)
 
 
-        self.y_height_entry = FloatEntry()
-        self.y_height_entry.set_value(0.0)
+        self.y_height_entry = FCEntry()
         self.y_height_lbl = QtWidgets.QLabel("Height (DY):")
         self.y_height_lbl = QtWidgets.QLabel("Height (DY):")
         self.y_height_lbl.setToolTip(
         self.y_height_lbl.setToolTip(
             "The height (DY)within which the panel must fit.\n"
             "The height (DY)within which the panel must fit.\n"
@@ -183,6 +179,47 @@ class Panelize(FlatCAMTool):
         # flag to signal the constrain was activated
         # flag to signal the constrain was activated
         self.constrain_flag = False
         self.constrain_flag = False
 
 
+    def run(self):
+        self.app.report_usage("ToolPanelize()")
+
+        FlatCAMTool.run(self)
+        self.set_tool_ui()
+        self.app.ui.notebook.setTabText(2, "Panel. Tool")
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+Z', **kwargs)
+
+    def set_tool_ui(self):
+        self.reset_fields()
+
+        sp_c = self.app.defaults["tools_panelize_spacing_columns"] if \
+            self.app.defaults["tools_panelize_spacing_columns"] else 0.0
+        self.spacing_columns.set_value(float(sp_c))
+
+        sp_r = self.app.defaults["tools_panelize_spacing_rows"] if \
+            self.app.defaults["tools_panelize_spacing_rows"] else 0.0
+        self.spacing_rows.set_value(float(sp_r))
+
+        rr = self.app.defaults["tools_panelize_rows"] if \
+            self.app.defaults["tools_panelize_rows"] else 0.0
+        self.rows.set_value(int(rr))
+
+        cc = self.app.defaults["tools_panelize_columns"] if \
+            self.app.defaults["tools_panelize_columns"] else 0.0
+        self.columns.set_value(int(cc))
+
+        c_cb = self.app.defaults["tools_panelize_constrain"] if \
+            self.app.defaults["tools_panelize_constrain"] else False
+        self.constrain_cb.set_value(c_cb)
+
+        x_w = self.app.defaults["tools_panelize_constrainx"] if \
+            self.app.defaults["tools_panelize_constrainx"] else 0.0
+        self.x_width_entry.set_value(float(x_w))
+
+        y_w = self.app.defaults["tools_panelize_constrainy"] if \
+            self.app.defaults["tools_panelize_constrainy"] else 0.0
+        self.y_height_entry.set_value(float(y_w))
+
     def on_type_obj_index_changed(self):
     def on_type_obj_index_changed(self):
         obj_type = self.type_obj_combo.currentIndex()
         obj_type = self.type_obj_combo.currentIndex()
         self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
         self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
@@ -193,13 +230,6 @@ class Panelize(FlatCAMTool):
         self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
         self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
         self.box_combo.setCurrentIndex(0)
         self.box_combo.setCurrentIndex(0)
 
 
-    def run(self):
-        FlatCAMTool.run(self)
-        self.app.ui.notebook.setTabText(2, "Panel. Tool")
-
-    def install(self, icon=None, separator=None, **kwargs):
-        FlatCAMTool.install(self, icon, separator, shortcut='ALT+Z', **kwargs)
-
     def on_panelize(self):
     def on_panelize(self):
         name = self.object_combo.currentText()
         name = self.object_combo.currentText()
 
 
@@ -207,13 +237,13 @@ class Panelize(FlatCAMTool):
         try:
         try:
             obj = self.app.collection.get_by_name(str(name))
             obj = self.app.collection.get_by_name(str(name))
         except:
         except:
-            self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % name)
+            self.app.inform.emit("[ERROR_NOTCL]Could not retrieve object: %s" % name)
             return "Could not retrieve object: %s" % name
             return "Could not retrieve object: %s" % name
 
 
         panel_obj = obj
         panel_obj = obj
 
 
         if panel_obj is None:
         if panel_obj is None:
-            self.app.inform.emit("[error_notcl]Object not found: %s" % panel_obj)
+            self.app.inform.emit("[ERROR_NOTCL]Object not found: %s" % panel_obj)
             return "Object not found: %s" % panel_obj
             return "Object not found: %s" % panel_obj
 
 
         boxname = self.box_combo.currentText()
         boxname = self.box_combo.currentText()
@@ -221,32 +251,89 @@ class Panelize(FlatCAMTool):
         try:
         try:
             box = self.app.collection.get_by_name(boxname)
             box = self.app.collection.get_by_name(boxname)
         except:
         except:
-            self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % boxname)
+            self.app.inform.emit("[ERROR_NOTCL]Could not retrieve object: %s" % boxname)
             return "Could not retrieve object: %s" % boxname
             return "Could not retrieve object: %s" % boxname
 
 
         if box is None:
         if box is None:
-            self.app.inform.emit("[warning]No object Box. Using instead %s" % panel_obj)
+            self.app.inform.emit("[WARNING]No object Box. Using instead %s" % panel_obj)
             box = panel_obj
             box = panel_obj
 
 
         self.outname = name + '_panelized'
         self.outname = name + '_panelized'
 
 
-        spacing_columns = self.spacing_columns.get_value()
+        try:
+            spacing_columns = float(self.spacing_columns.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                spacing_columns = float(self.spacing_columns.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
         spacing_columns = spacing_columns if spacing_columns is not None else 0
         spacing_columns = spacing_columns if spacing_columns is not None else 0
 
 
-        spacing_rows = self.spacing_rows.get_value()
+        try:
+            spacing_rows = float(self.spacing_rows.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                spacing_rows = float(self.spacing_rows.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
         spacing_rows = spacing_rows if spacing_rows is not None else 0
         spacing_rows = spacing_rows if spacing_rows is not None else 0
 
 
-        rows = self.rows.get_value()
+        try:
+            rows = int(self.rows.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                rows = float(self.rows.get_value().replace(',', '.'))
+                rows = int(rows)
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
         rows = rows if rows is not None else 1
         rows = rows if rows is not None else 1
 
 
-        columns = self.columns.get_value()
+        try:
+            columns = int(self.columns.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                columns = float(self.columns.get_value().replace(',', '.'))
+                columns = int(columns)
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
         columns = columns if columns is not None else 1
         columns = columns if columns is not None else 1
 
 
-        constrain_dx = self.x_width_entry.get_value()
-        constrain_dy = self.y_height_entry.get_value()
+        try:
+            constrain_dx = float(self.x_width_entry.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                constrain_dx = float(self.x_width_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
+
+        try:
+            constrain_dy = float(self.y_height_entry.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                constrain_dy = float(self.y_height_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                     "use a number.")
+                return
 
 
         if 0 in {columns, rows}:
         if 0 in {columns, rows}:
-            self.app.inform.emit("[error_notcl]Columns or Rows are zero value. Change them to a positive integer.")
+            self.app.inform.emit("[ERROR_NOTCL]Columns or Rows are zero value. Change them to a positive integer.")
             return "Columns or Rows are zero value. Change them to a positive integer."
             return "Columns or Rows are zero value. Change them to a positive integer."
 
 
         xmin, ymin, xmax, ymax = box.bounds()
         xmin, ymin, xmax, ymax = box.bounds()
@@ -342,7 +429,7 @@ class Panelize(FlatCAMTool):
         #             self.app.new_object("geometry", self.outname, job_init_geometry, plot=True, autoselected=True)
         #             self.app.new_object("geometry", self.outname, job_init_geometry, plot=True, autoselected=True)
         #
         #
         #     else:
         #     else:
-        #         self.app.inform.emit("[error_notcl] Obj is None")
+        #         self.app.inform.emit("[ERROR_NOTCL] Obj is None")
         #         return "ERROR: Obj is None"
         #         return "ERROR: Obj is None"
 
 
         # panelize()
         # panelize()
@@ -455,7 +542,7 @@ class Panelize(FlatCAMTool):
             self.app.inform.emit("[success]Panel done...")
             self.app.inform.emit("[success]Panel done...")
         else:
         else:
             self.constrain_flag = False
             self.constrain_flag = False
-            self.app.inform.emit("[warning] Too big for the constrain area. Final panel has %s columns and %s rows" %
+            self.app.inform.emit("[WARNING] Too big for the constrain area. Final panel has %s columns and %s rows" %
                                  (columns, rows))
                                  (columns, rows))
 
 
         proc = self.app.proc_container.new("Generating panel ... Please wait.")
         proc = self.app.proc_container.new("Generating panel ... Please wait.")

+ 8 - 6
flatcamTools/ToolProperties.py

@@ -42,24 +42,26 @@ class Properties(FlatCAMTool):
         self.vlay.setStretch(0,0)
         self.vlay.setStretch(0,0)
 
 
     def run(self):
     def run(self):
+        self.app.report_usage("ToolProperties()")
 
 
         if self.app.tool_tab_locked is True:
         if self.app.tool_tab_locked is True:
             return
             return
-
-        # this reset the TreeWidget
-        self.treeWidget.clear()
-        self.properties_frame.show()
-
+        self.set_tool_ui()
         FlatCAMTool.run(self)
         FlatCAMTool.run(self)
         self.properties()
         self.properties()
 
 
     def install(self, icon=None, separator=None, **kwargs):
     def install(self, icon=None, separator=None, **kwargs):
         FlatCAMTool.install(self, icon, separator, shortcut='P', **kwargs)
         FlatCAMTool.install(self, icon, separator, shortcut='P', **kwargs)
 
 
+    def set_tool_ui(self):
+        # this reset the TreeWidget
+        self.treeWidget.clear()
+        self.properties_frame.show()
+
     def properties(self):
     def properties(self):
         obj_list = self.app.collection.get_selected()
         obj_list = self.app.collection.get_selected()
         if not obj_list:
         if not obj_list:
-            self.app.inform.emit("[error_notcl] Properties Tool was not displayed. No object selected.")
+            self.app.inform.emit("[ERROR_NOTCL] Properties Tool was not displayed. No object selected.")
             self.app.ui.notebook.setTabText(2, "Tools")
             self.app.ui.notebook.setTabText(2, "Tools")
             self.properties_frame.hide()
             self.properties_frame.hide()
             self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
             self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)

+ 15 - 1
flatcamTools/ToolShell.py

@@ -234,7 +234,7 @@ class TermWidget(QWidget):
         """
         """
         Convert text to HTML for inserting it to browser
         Convert text to HTML for inserting it to browser
         """
         """
-        assert style in ('in', 'out', 'err')
+        assert style in ('in', 'out', 'err', 'warning', 'success')
 
 
         text = html.escape(text)
         text = html.escape(text)
         text = text.replace('\n', '<br/>')
         text = text.replace('\n', '<br/>')
@@ -243,6 +243,10 @@ class TermWidget(QWidget):
             text = '<span style="font-weight: bold;">%s</span>' % text
             text = '<span style="font-weight: bold;">%s</span>' % text
         elif style == 'err':
         elif style == 'err':
             text = '<span style="font-weight: bold; color: red;">%s</span>' % text
             text = '<span style="font-weight: bold; color: red;">%s</span>' % text
+        elif style == 'warning':
+            text = '<span style="font-weight: bold; color: rgb(244, 182, 66);">%s</span>' % text
+        elif style == 'success':
+            text = '<span style="font-weight: bold; color: rgb(8, 68, 0);">%s</span>' % text
         else:
         else:
             text = '<span>%s</span>' % text  # without span <br/> is ignored!!!
             text = '<span>%s</span>' % text  # without span <br/> is ignored!!!
 
 
@@ -304,6 +308,16 @@ class TermWidget(QWidget):
         """
         """
         self._append_to_browser('out', text)
         self._append_to_browser('out', text)
 
 
+    def append_success(self, text):
+        """Appent text to output widget
+        """
+        self._append_to_browser('success', text)
+
+    def append_warning(self, text):
+        """Appent text to output widget
+        """
+        self._append_to_browser('warning', text)
+
     def append_error(self, text):
     def append_error(self, text):
         """Appent error text to output widget. Text is drawn with red background
         """Appent error text to output widget. Text is drawn with red background
         """
         """

+ 82 - 38
flatcamTools/ToolTransform.py

@@ -355,7 +355,17 @@ class ToolTransform(FlatCAMTool):
         self.offx_entry.returnPressed.connect(self.on_offx)
         self.offx_entry.returnPressed.connect(self.on_offx)
         self.offy_entry.returnPressed.connect(self.on_offy)
         self.offy_entry.returnPressed.connect(self.on_offy)
 
 
+    def run(self):
+        self.app.report_usage("ToolTransform()")
+
+        FlatCAMTool.run(self)
+        self.set_tool_ui()
+        self.app.ui.notebook.setTabText(2, "Transform Tool")
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+R', **kwargs)
 
 
+    def set_tool_ui(self):
         ## Initialize form
         ## Initialize form
         self.rotate_entry.set_value('0')
         self.rotate_entry.set_value('0')
         self.skewx_entry.set_value('0')
         self.skewx_entry.set_value('0')
@@ -366,19 +376,17 @@ class ToolTransform(FlatCAMTool):
         self.offy_entry.set_value('0')
         self.offy_entry.set_value('0')
         self.flip_ref_cb.setChecked(False)
         self.flip_ref_cb.setChecked(False)
 
 
-    def run(self):
-        FlatCAMTool.run(self)
-        self.app.ui.notebook.setTabText(2, "Transform Tool")
-
-    def install(self, icon=None, separator=None, **kwargs):
-        FlatCAMTool.install(self, icon, separator, shortcut='ALT+R', **kwargs)
-
     def on_rotate(self):
     def on_rotate(self):
         try:
         try:
             value = float(self.rotate_entry.get_value())
             value = float(self.rotate_entry.get_value())
-        except Exception as e:
-            self.app.inform.emit("[error] Failed to rotate due of: %s" % str(e))
-            return
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                value = float(self.rotate_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Rotate, "
+                                     "use a number.")
+                return
         self.app.worker_task.emit({'fcn': self.on_rotate_action,
         self.app.worker_task.emit({'fcn': self.on_rotate_action,
                                        'params': [value]})
                                        'params': [value]})
         # self.on_rotate_action(value)
         # self.on_rotate_action(value)
@@ -405,9 +413,15 @@ class ToolTransform(FlatCAMTool):
     def on_skewx(self):
     def on_skewx(self):
         try:
         try:
             value = float(self.skewx_entry.get_value())
             value = float(self.skewx_entry.get_value())
-        except:
-            self.app.inform.emit("[warning_notcl] No value for Skew!")
-            return
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                value = float(self.skewx_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Skew X, "
+                                     "use a number.")
+                return
+
         # self.on_skew("X", value)
         # self.on_skew("X", value)
         axis = 'X'
         axis = 'X'
         self.app.worker_task.emit({'fcn': self.on_skew,
         self.app.worker_task.emit({'fcn': self.on_skew,
@@ -417,9 +431,15 @@ class ToolTransform(FlatCAMTool):
     def on_skewy(self):
     def on_skewy(self):
         try:
         try:
             value = float(self.skewy_entry.get_value())
             value = float(self.skewy_entry.get_value())
-        except:
-            self.app.inform.emit("[warning_notcl] No value for Skew!")
-            return
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                value = float(self.skewy_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Skew Y, "
+                                     "use a number.")
+                return
+
         # self.on_skew("Y", value)
         # self.on_skew("Y", value)
         axis = 'Y'
         axis = 'Y'
         self.app.worker_task.emit({'fcn': self.on_skew,
         self.app.worker_task.emit({'fcn': self.on_skew,
@@ -429,9 +449,15 @@ class ToolTransform(FlatCAMTool):
     def on_scalex(self):
     def on_scalex(self):
         try:
         try:
             xvalue = float(self.scalex_entry.get_value())
             xvalue = float(self.scalex_entry.get_value())
-        except:
-            self.app.inform.emit("[warning_notcl] No value for Scale!")
-            return
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                xvalue = float(self.scalex_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Scale X, "
+                                     "use a number.")
+                return
+
         # scaling to zero has no sense so we remove it, because scaling with 1 does nothing
         # scaling to zero has no sense so we remove it, because scaling with 1 does nothing
         if xvalue == 0:
         if xvalue == 0:
             xvalue = 1
             xvalue = 1
@@ -457,9 +483,15 @@ class ToolTransform(FlatCAMTool):
         xvalue = 1
         xvalue = 1
         try:
         try:
             yvalue = float(self.scaley_entry.get_value())
             yvalue = float(self.scaley_entry.get_value())
-        except:
-            self.app.inform.emit("[warning_notcl] No value for Scale!")
-            return
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                yvalue = float(self.scaley_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Scale Y, "
+                                     "use a number.")
+                return
+
         # scaling to zero has no sense so we remove it, because scaling with 1 does nothing
         # scaling to zero has no sense so we remove it, because scaling with 1 does nothing
         if yvalue == 0:
         if yvalue == 0:
             yvalue = 1
             yvalue = 1
@@ -480,9 +512,15 @@ class ToolTransform(FlatCAMTool):
     def on_offx(self):
     def on_offx(self):
         try:
         try:
             value = float(self.offx_entry.get_value())
             value = float(self.offx_entry.get_value())
-        except:
-            self.app.inform.emit("[warning_notcl] No value for Offset!")
-            return
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                value = float(self.offx_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Offset X, "
+                                     "use a number.")
+                return
+
         # self.on_offset("X", value)
         # self.on_offset("X", value)
         axis = 'X'
         axis = 'X'
         self.app.worker_task.emit({'fcn': self.on_offset,
         self.app.worker_task.emit({'fcn': self.on_offset,
@@ -492,9 +530,15 @@ class ToolTransform(FlatCAMTool):
     def on_offy(self):
     def on_offy(self):
         try:
         try:
             value = float(self.offy_entry.get_value())
             value = float(self.offy_entry.get_value())
-        except:
-            self.app.inform.emit("[warning_notcl] No value for Offset!")
-            return
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                value = float(self.offy_entry.get_value().replace(',', '.'))
+            except ValueError:
+                self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered for Offset Y, "
+                                     "use a number.")
+                return
+
         # self.on_offset("Y", value)
         # self.on_offset("Y", value)
         axis = 'Y'
         axis = 'Y'
         self.app.worker_task.emit({'fcn': self.on_offset,
         self.app.worker_task.emit({'fcn': self.on_offset,
@@ -509,7 +553,7 @@ class ToolTransform(FlatCAMTool):
         ymaxlist = []
         ymaxlist = []
 
 
         if not obj_list:
         if not obj_list:
-            self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to rotate!")
+            self.app.inform.emit("[WARNING_NOTCL] No object selected. Please Select an object to rotate!")
             return
             return
         else:
         else:
             with self.app.proc_container.new("Appying Rotate"):
             with self.app.proc_container.new("Appying Rotate"):
@@ -550,7 +594,7 @@ class ToolTransform(FlatCAMTool):
                     self.app.progress.emit(100)
                     self.app.progress.emit(100)
 
 
                 except Exception as e:
                 except Exception as e:
-                    self.app.inform.emit("[error_notcl] Due of %s, rotation movement was not executed." % str(e))
+                    self.app.inform.emit("[ERROR_NOTCL] Due of %s, rotation movement was not executed." % str(e))
                     return
                     return
 
 
     def on_flip(self, axis):
     def on_flip(self, axis):
@@ -561,7 +605,7 @@ class ToolTransform(FlatCAMTool):
         ymaxlist = []
         ymaxlist = []
 
 
         if not obj_list:
         if not obj_list:
-            self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to flip!")
+            self.app.inform.emit("[WARNING_NOTCL] No object selected. Please Select an object to flip!")
             return
             return
         else:
         else:
             with self.app.proc_container.new("Applying Flip"):
             with self.app.proc_container.new("Applying Flip"):
@@ -623,7 +667,7 @@ class ToolTransform(FlatCAMTool):
                     self.app.progress.emit(100)
                     self.app.progress.emit(100)
 
 
                 except Exception as e:
                 except Exception as e:
-                    self.app.inform.emit("[error_notcl] Due of %s, Flip action was not executed." % str(e))
+                    self.app.inform.emit("[ERROR_NOTCL] Due of %s, Flip action was not executed." % str(e))
                     return
                     return
 
 
     def on_skew(self, axis, num):
     def on_skew(self, axis, num):
@@ -632,7 +676,7 @@ class ToolTransform(FlatCAMTool):
         yminlist = []
         yminlist = []
 
 
         if not obj_list:
         if not obj_list:
-            self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to shear/skew!")
+            self.app.inform.emit("[WARNING_NOTCL] No object selected. Please Select an object to shear/skew!")
             return
             return
         else:
         else:
             with self.app.proc_container.new("Applying Skew"):
             with self.app.proc_container.new("Applying Skew"):
@@ -670,7 +714,7 @@ class ToolTransform(FlatCAMTool):
                     self.app.progress.emit(100)
                     self.app.progress.emit(100)
 
 
                 except Exception as e:
                 except Exception as e:
-                    self.app.inform.emit("[error_notcl] Due of %s, Skew action was not executed." % str(e))
+                    self.app.inform.emit("[ERROR_NOTCL] Due of %s, Skew action was not executed." % str(e))
                     return
                     return
 
 
     def on_scale(self, axis, xfactor, yfactor, point=None):
     def on_scale(self, axis, xfactor, yfactor, point=None):
@@ -681,7 +725,7 @@ class ToolTransform(FlatCAMTool):
         ymaxlist = []
         ymaxlist = []
 
 
         if not obj_list:
         if not obj_list:
-            self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to scale!")
+            self.app.inform.emit("[WARNING_NOTCL] No object selected. Please Select an object to scale!")
             return
             return
         else:
         else:
             with self.app.proc_container.new("Applying Scale"):
             with self.app.proc_container.new("Applying Scale"):
@@ -725,7 +769,7 @@ class ToolTransform(FlatCAMTool):
                     self.app.inform.emit('Object(s) were scaled on %s axis ...' % str(axis))
                     self.app.inform.emit('Object(s) were scaled on %s axis ...' % str(axis))
                     self.app.progress.emit(100)
                     self.app.progress.emit(100)
                 except Exception as e:
                 except Exception as e:
-                    self.app.inform.emit("[error_notcl] Due of %s, Scale action was not executed." % str(e))
+                    self.app.inform.emit("[ERROR_NOTCL] Due of %s, Scale action was not executed." % str(e))
                     return
                     return
 
 
     def on_offset(self, axis, num):
     def on_offset(self, axis, num):
@@ -734,7 +778,7 @@ class ToolTransform(FlatCAMTool):
         yminlist = []
         yminlist = []
 
 
         if not obj_list:
         if not obj_list:
-            self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to offset!")
+            self.app.inform.emit("[WARNING_NOTCL] No object selected. Please Select an object to offset!")
             return
             return
         else:
         else:
             with self.app.proc_container.new("Applying Offset"):
             with self.app.proc_container.new("Applying Offset"):
@@ -771,7 +815,7 @@ class ToolTransform(FlatCAMTool):
                     self.app.progress.emit(100)
                     self.app.progress.emit(100)
 
 
                 except Exception as e:
                 except Exception as e:
-                    self.app.inform.emit("[error_notcl] Due of %s, Offset action was not executed." % str(e))
+                    self.app.inform.emit("[ERROR_NOTCL] Due of %s, Offset action was not executed." % str(e))
                     return
                     return
 
 
 # end of file
 # end of file

+ 2 - 1
flatcamTools/__init__.py

@@ -5,7 +5,8 @@ from flatcamTools.ToolPanelize import Panelize
 from flatcamTools.ToolFilm import Film
 from flatcamTools.ToolFilm import Film
 from flatcamTools.ToolMove import ToolMove
 from flatcamTools.ToolMove import ToolMove
 from flatcamTools.ToolDblSided import DblSidedTool
 from flatcamTools.ToolDblSided import DblSidedTool
-from flatcamTools.ToolCutout import ToolCutout
+
+from flatcamTools.ToolCutOut import ToolCutOut
 from flatcamTools.ToolCalculators import ToolCalculator
 from flatcamTools.ToolCalculators import ToolCalculator
 from flatcamTools.ToolProperties import Properties
 from flatcamTools.ToolProperties import Properties
 from flatcamTools.ToolImage import ToolImage
 from flatcamTools.ToolImage import ToolImage

+ 261 - 0
postprocessors/Toolchange_Probe_MACH3.py

@@ -0,0 +1,261 @@
+from FlatCAMPostProc import *
+
+
+class Toolchange_Probe_MACH3(FlatCAMPostProc):
+
+    coordinate_format = "%.*f"
+    feedrate_format = '%.*f'
+
+    def start_code(self, p):
+        units = ' ' + str(p['units']).lower()
+        coords_xy = p['toolchange_xy']
+        gcode = ''
+
+        xmin = '%.*f' % (p.coords_decimals, p['options']['xmin'])
+        xmax = '%.*f' % (p.coords_decimals, p['options']['xmax'])
+        ymin = '%.*f' % (p.coords_decimals, p['options']['ymin'])
+        ymax = '%.*f' % (p.coords_decimals, p['options']['ymax'])
+
+        if str(p['options']['type']) == 'Geometry':
+            gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n'
+
+        gcode += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n'
+
+        if str(p['options']['type']) == 'Geometry':
+            gcode += '(Feedrate_Z: ' + str(p['feedrate_z']) + units + '/min' + ')\n'
+
+        gcode += '(Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n'
+        gcode += '(Feedrate Probe ' + str(p['feedrate_probe']) + units + '/min' + ')\n' + '\n'
+        gcode += '(Z_Cut: ' + str(p['z_cut']) + units + ')\n'
+
+        if str(p['options']['type']) == 'Geometry':
+            if p['multidepth'] is True:
+                gcode += '(DepthPerCut: ' + str(p['depthpercut']) + units + ' <=>' + \
+                         str(math.ceil(abs(p['z_cut']) / p['depthpercut'])) + ' passes' + ')\n'
+
+        gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
+        gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n'
+
+        if coords_xy is not None:
+            gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
+        else:
+            gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
+
+        gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
+        gcode += '(Z End: ' + str(p['endz']) + units + ')\n'
+        gcode += '(Z Probe Depth: ' + str(p['z_pdepth']) + units + ')\n'
+        gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
+
+        if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
+            gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
+        else:
+            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
+
+        gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
+        gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
+
+        gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed'])
+
+        gcode += ('G20\n' if p.units.upper() == 'IN' else 'G21\n')
+        gcode += 'G90\n'
+        gcode += 'G17\n'
+        gcode += 'G94\n'
+
+        return gcode
+
+    def startz_code(self, p):
+        return ''
+
+    def lift_code(self, p):
+        return 'G00 Z' + self.coordinate_format%(p.coords_decimals, p.z_move)
+
+    def down_code(self, p):
+        return 'G01 Z' + self.coordinate_format%(p.coords_decimals, p.z_cut)
+
+    def toolchange_code(self, p):
+        toolchangez = p.toolchangez
+        toolchangexy = p.toolchange_xy
+        f_plunge = p.f_plunge
+        gcode = ''
+
+        if toolchangexy is not None:
+            toolchangex = toolchangexy[0]
+            toolchangey = toolchangexy[1]
+
+        no_drills = 1
+
+        if int(p.tool) == 1 and p.startz is not None:
+            toolchangez = p.startz
+
+        if p.units.upper() == 'MM':
+            toolC_formatted = format(p.toolC, '.2f')
+        else:
+            toolC_formatted = format(p.toolC, '.4f')
+
+        if str(p['options']['type']) == 'Excellon':
+            for i in p['options']['Tools_in_use']:
+                if i[0] == p.tool:
+                    no_drills = i[2]
+
+            if toolchangexy is not None:
+                gcode = """               
+T{tool}
+M5
+M6
+G00 Z{toolchangez}
+G00 X{toolchangex} Y{toolchangey}
+(MSG, Change to Tool Dia = {toolC} ||| Drills for this tool = {t_drills} ||| Tool Probing MACH3)
+M0
+G00 Z{z_move}
+F{feedrate_probe}
+G31 Z{z_pdepth}
+G92 Z0
+G00 Z{z_move}
+F{feedrate_probe_slow}
+G31 Z{z_pdepth}
+G92 Z0
+(MSG, Remove any clips or other devices used for probing. CNC work is resuming ...)
+M0
+G00 Z{z_move}
+""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex),
+           toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey),
+           toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez),
+           z_move=self.coordinate_format % (p.coords_decimals, p.z_move),
+           feedrate_probe=str(self.feedrate_format %(p.fr_decimals, p.feedrate_probe)),
+           feedrate_probe_slow=str(self.feedrate_format % (p.fr_decimals, (p.feedrate_probe / 2))),
+           z_pdepth=self.coordinate_format % (p.coords_decimals, p.z_pdepth),
+           tool=int(p.tool),
+           t_drills=no_drills,
+           toolC=toolC_formatted)
+            else:
+                gcode = """
+T{tool}
+M5
+M6
+G00 Z{toolchangez}
+(MSG, Change to Tool Dia = {toolC} ||| Drills for this tool = {t_drills} ||| Tool Probing MACH3)
+M0
+G00 Z{z_move}
+F{feedrate_probe}
+G31 Z{z_pdepth}
+G92 Z0
+G00 Z{z_move}
+F{feedrate_probe_slow}
+G31 Z{z_pdepth}
+G92 Z0
+(MSG, Remove any clips or other devices used for probing. CNC work is resuming ...)
+M0
+G00 Z{z_move}
+""".format(toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez),
+           z_move=self.coordinate_format % (p.coords_decimals, p.z_move),
+           feedrate_probe=str(self.feedrate_format %(p.fr_decimals, p.feedrate_probe)),
+           feedrate_probe_slow=str(self.feedrate_format % (p.fr_decimals, (p.feedrate_probe / 2))),
+           z_pdepth=self.coordinate_format % (p.coords_decimals, p.z_pdepth),
+           tool=int(p.tool),
+           t_drills=no_drills,
+           toolC=toolC_formatted)
+
+            # if f_plunge is True:
+            #     gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
+            return gcode
+
+        else:
+            if toolchangexy is not None:
+                gcode = """
+T{tool}
+M5
+M6
+G00 Z{toolchangez}
+G00 X{toolchangex} Y{toolchangey}
+(MSG, Change to Tool Dia = {toolC} ||| Tool Probing MACH3)
+M0
+G00 Z{z_move}
+F{feedrate_probe}
+G31 Z{z_pdepth}
+G92 Z0
+G00 Z{z_move}
+F{feedrate_probe_slow}
+G31 Z{z_pdepth}
+G92 Z0
+(MSG, Remove any clips or other devices used for probing. CNC work is resuming ...)
+M0
+G00 Z{z_move}
+""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex),
+           toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey),
+           toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez),
+           z_move=self.coordinate_format % (p.coords_decimals, p.z_move),
+           feedrate_probe=str(self.feedrate_format %(p.fr_decimals, p.feedrate_probe)),
+           feedrate_probe_slow=str(self.feedrate_format % (p.fr_decimals, (p.feedrate_probe / 2))),
+           z_pdepth=self.coordinate_format % (p.coords_decimals, p.z_pdepth),
+           tool=int(p.tool),
+           toolC=toolC_formatted)
+            else:
+                gcode = """
+T{tool}
+M5
+M6
+G00 Z{toolchangez}
+(MSG, Change to Tool Dia = {toolC} ||| Tool Probing MACH3)
+M0
+G00 Z{z_move}
+F{feedrate_probe}
+G31 Z{z_pdepth}
+G92 Z0
+G00 Z{z_move}
+F{feedrate_probe_slow}
+G31 Z{z_pdepth}
+G92 Z0
+(MSG, Remove any clips or other devices used for probing. CNC work is resuming ...)
+M0
+G00 Z{z_move}
+""".format(toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez),
+           z_move=self.coordinate_format % (p.coords_decimals, p.z_move),
+           feedrate_probe=str(self.feedrate_format %(p.fr_decimals, p.feedrate_probe)),
+           feedrate_probe_slow=str(self.feedrate_format % (p.fr_decimals, (p.feedrate_probe / 2))),
+           z_pdepth=self.coordinate_format % (p.coords_decimals, p.z_pdepth),
+           tool=int(p.tool),
+           toolC=toolC_formatted)
+
+            # if f_plunge is True:
+            #     gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
+            return gcode
+
+    def up_to_zero_code(self, p):
+        return 'G01 Z0'
+
+    def position_code(self, p):
+        return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \
+               (p.coords_decimals, p.x, p.coords_decimals, p.y)
+
+    def rapid_code(self, p):
+        return ('G00 ' + self.position_code(p)).format(**p)
+
+    def linear_code(self, p):
+        return ('G01 ' + self.position_code(p)).format(**p)
+
+    def end_code(self, p):
+        coords_xy = p['toolchange_xy']
+        gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n")
+
+        if coords_xy is not None:
+            gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n"
+        return gcode
+
+    def feedrate_code(self, p):
+        return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
+
+    def feedrate_z_code(self, p):
+        return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate_z))
+
+    def spindle_code(self, p):
+        if p.spindlespeed:
+            return 'M03 S' + str(p.spindlespeed)
+        else:
+            return 'M03'
+
+    def dwell_code(self, p):
+        if p.dwelltime:
+            return 'G4 P' + str(p.dwelltime)
+
+    def spindle_stop_code(self,p):
+        return 'M05'

+ 191 - 0
postprocessors/Toolchange_Probe_general.py

@@ -0,0 +1,191 @@
+from FlatCAMPostProc import *
+
+
+class Toolchange_Probe_general(FlatCAMPostProc):
+
+    coordinate_format = "%.*f"
+    feedrate_format = '%.*f'
+
+    def start_code(self, p):
+        units = ' ' + str(p['units']).lower()
+        coords_xy = p['toolchange_xy']
+        gcode = ''
+
+        xmin = '%.*f' % (p.coords_decimals, p['options']['xmin'])
+        xmax = '%.*f' % (p.coords_decimals, p['options']['xmax'])
+        ymin = '%.*f' % (p.coords_decimals, p['options']['ymin'])
+        ymax = '%.*f' % (p.coords_decimals, p['options']['ymax'])
+
+        if str(p['options']['type']) == 'Geometry':
+            gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n'
+
+        gcode += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n'
+
+        if str(p['options']['type']) == 'Geometry':
+            gcode += '(Feedrate_Z: ' + str(p['feedrate_z']) + units + '/min' + ')\n'
+
+        gcode += '(Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n'
+        gcode += '(Z_Cut: ' + str(p['z_cut']) + units + ')\n'
+
+        if str(p['options']['type']) == 'Geometry':
+            if p['multidepth'] is True:
+                gcode += '(DepthPerCut: ' + str(p['depthpercut']) + units + ' <=>' + \
+                         str(math.ceil(abs(p['z_cut']) / p['depthpercut'])) + ' passes' + ')\n'
+
+        gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
+        gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n'
+
+        if coords_xy is not None:
+            gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
+        else:
+            gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
+
+        gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
+        gcode += '(Z End: ' + str(p['endz']) + units + ')\n'
+        gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
+
+        if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
+            gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
+        else:
+            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
+
+        gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
+        gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
+
+        gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed'])
+
+        gcode += ('G20\n' if p.units.upper() == 'IN' else 'G21\n')
+        gcode += 'G90\n'
+        gcode += 'G17\n'
+        gcode += 'G94\n'
+
+        return gcode
+
+    def startz_code(self, p):
+        return ''
+
+    def lift_code(self, p):
+        return 'G00 Z' + self.coordinate_format%(p.coords_decimals, p.z_move)
+
+    def down_code(self, p):
+        return 'G01 Z' + self.coordinate_format%(p.coords_decimals, p.z_cut)
+
+    def toolchange_code(self, p):
+        toolchangez = p.toolchangez
+        toolchangexy = p.toolchange_xy
+        f_plunge = p.f_plunge
+        gcode = ''
+
+        if toolchangexy is not None:
+            toolchangex = toolchangexy[0]
+            toolchangey = toolchangexy[1]
+
+        no_drills = 1
+
+        if int(p.tool) == 1 and p.startz is not None:
+            toolchangez = p.startz
+
+        if p.units.upper() == 'MM':
+            toolC_formatted = format(p.toolC, '.2f')
+        else:
+            toolC_formatted = format(p.toolC, '.4f')
+
+        if str(p['options']['type']) == 'Excellon':
+            for i in p['options']['Tools_in_use']:
+                if i[0] == p.tool:
+                    no_drills = i[2]
+
+            if toolchangexy is not None:
+                gcode = """
+G00 X{toolchangex} Y{toolchangey}                
+T{tool}
+M5
+M6
+(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
+M0
+""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex),
+             toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey),
+             tool=int(p.tool),
+             t_drills=no_drills,
+             toolC=toolC_formatted)
+
+            else:
+                gcode = """
+T{tool}
+M5
+M6
+(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
+M0
+""".format(tool=int(p.tool),
+             t_drills=no_drills,
+             toolC=toolC_formatted)
+
+            if f_plunge is True:
+                gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
+            return gcode
+
+        else:
+            if toolchangexy is not None:
+                gcode = """
+G00 X{toolchangex} Y{toolchangey}
+T{tool}
+M5
+M6    
+(MSG, Change to Tool Dia = {toolC})
+M0
+""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex),
+             toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey),
+             tool=int(p.tool),
+             toolC=toolC_formatted)
+            else:
+                gcode = """
+T{tool}
+M5
+M6    
+(MSG, Change to Tool Dia = {toolC})
+M0""".format(tool=int(p.tool),
+             toolC=toolC_formatted)
+
+            if f_plunge is True:
+                gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
+            return gcode
+
+    def up_to_zero_code(self, p):
+        return 'G01 Z0'
+
+    def position_code(self, p):
+        return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \
+               (p.coords_decimals, p.x, p.coords_decimals, p.y)
+
+    def rapid_code(self, p):
+        return ('G00 ' + self.position_code(p)).format(**p)
+
+    def linear_code(self, p):
+        return ('G01 ' + self.position_code(p)).format(**p)
+
+    def end_code(self, p):
+        coords_xy = p['toolchange_xy']
+        gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n")
+
+        if coords_xy is not None:
+            gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n"
+        return gcode
+
+    def feedrate_code(self, p):
+        return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
+
+    def feedrate_z_code(self, p):
+        return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate_z))
+
+    def spindle_code(self, p):
+        if p.spindlespeed:
+            return 'M03 S' + str(p.spindlespeed)
+        else:
+            return 'M03'
+
+    def dwell_code(self, p):
+        if p.dwelltime:
+            return 'G4 P' + str(p.dwelltime)
+
+    def spindle_stop_code(self,p):
+        return 'M05'

+ 75 - 12
postprocessors/manual_toolchange.py → postprocessors/Toolchange_manual.py

@@ -1,7 +1,7 @@
 from FlatCAMPostProc import *
 from FlatCAMPostProc import *
 
 
 
 
-class manual_toolchange(FlatCAMPostProc):
+class Toolchange_manual(FlatCAMPostProc):
 
 
     coordinate_format = "%.*f"
     coordinate_format = "%.*f"
     feedrate_format = '%.*f'
     feedrate_format = '%.*f'
@@ -11,6 +11,11 @@ class manual_toolchange(FlatCAMPostProc):
         coords_xy = p['toolchange_xy']
         coords_xy = p['toolchange_xy']
         gcode = ''
         gcode = ''
 
 
+        xmin = '%.*f' % (p.coords_decimals, p['options']['xmin'])
+        xmax = '%.*f' % (p.coords_decimals, p['options']['xmax'])
+        ymin = '%.*f' % (p.coords_decimals, p['options']['ymin'])
+        ymax = '%.*f' % (p.coords_decimals, p['options']['ymax'])
+
         if str(p['options']['type']) == 'Geometry':
         if str(p['options']['type']) == 'Geometry':
             gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n'
             gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n'
 
 
@@ -29,20 +34,27 @@ class manual_toolchange(FlatCAMPostProc):
 
 
         gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
         gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
         gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n'
         gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n'
-        gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
+        if coords_xy is not None:
+            gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
+        else:
+            gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
         gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
         gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
         gcode += '(Z End: ' + str(p['endz']) + units + ')\n'
         gcode += '(Z End: ' + str(p['endz']) + units + ')\n'
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
 
 
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
-            gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
+            gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n' + '\n'
         else:
         else:
-            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n'
+            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
+
+        gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
+        gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
 
 
         gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed'])
         gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed'])
 
 
         gcode += ('G20\n' if p.units.upper() == 'IN' else 'G21\n')
         gcode += ('G20\n' if p.units.upper() == 'IN' else 'G21\n')
         gcode += 'G90\n'
         gcode += 'G90\n'
+        gcode += 'G17\n'
         gcode += 'G94\n'
         gcode += 'G94\n'
 
 
         return gcode
         return gcode
@@ -62,8 +74,15 @@ class manual_toolchange(FlatCAMPostProc):
     def toolchange_code(self, p):
     def toolchange_code(self, p):
         toolchangez = p.toolchangez
         toolchangez = p.toolchangez
         toolchangexy = p.toolchange_xy
         toolchangexy = p.toolchange_xy
-        toolchangex = toolchangexy[0]
-        toolchangey = toolchangexy[1]
+        f_plunge = p.f_plunge
+        gcode = ''
+
+        if toolchangexy is not None:
+            toolchangex = toolchangexy[0]
+            toolchangey = toolchangexy[1]
+        # else:
+        #     toolchangex = p.oldx
+        #     toolchangey = p.oldy
 
 
         no_drills = 1
         no_drills = 1
 
 
@@ -79,11 +98,13 @@ class manual_toolchange(FlatCAMPostProc):
             for i in p['options']['Tools_in_use']:
             for i in p['options']['Tools_in_use']:
                 if i[0] == p.tool:
                 if i[0] == p.tool:
                     no_drills = i[2]
                     no_drills = i[2]
-            return """G00 Z{toolchangez}
+
+            if toolchangexy is not None:
+                gcode =  """G00 Z{toolchangez}
 T{tool}
 T{tool}
-M5
-G00 X{toolchangex} Y{toolchangey}    
-(MSG, Change to Tool Dia = {toolC}, Total drills for current tool = {t_drills})
+M5   
+G00 X{toolchangex} Y{toolchangey} 
+(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
 M0
 M0
 G01 Z0
 G01 Z0
 M0
 M0
@@ -95,8 +116,30 @@ M0
            tool=int(p.tool),
            tool=int(p.tool),
            t_drills=no_drills,
            t_drills=no_drills,
            toolC=toolC_formatted)
            toolC=toolC_formatted)
+
+            else:
+                gcode =  """G00 Z{toolchangez}
+T{tool}
+M5  
+(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
+M0
+G01 Z0
+M0
+G00 Z{toolchangez}
+M0
+""".format(
+           toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
+           tool=int(p.tool),
+           t_drills=no_drills,
+           toolC=toolC_formatted)
+
+            if f_plunge is True:
+                gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
+            return gcode
+
         else:
         else:
-            return """G00 Z{toolchangez}
+            if toolchangexy is not None:
+                gcode =  """G00 Z{toolchangez}
 T{tool}
 T{tool}
 M5
 M5
 G00 X{toolchangex}Y{toolchangey}    
 G00 X{toolchangex}Y{toolchangey}    
@@ -111,6 +154,23 @@ M0
            toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
            toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
            tool=int(p.tool),
            tool=int(p.tool),
            toolC=toolC_formatted)
            toolC=toolC_formatted)
+            else:
+                gcode =  """G00 Z{toolchangez}
+T{tool}
+M5  
+(MSG, Change to Tool Dia = {toolC})
+M0
+G01 Z0
+M0
+G00 Z{toolchangez}
+M0
+""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
+           tool=int(p.tool),
+           toolC=toolC_formatted)
+
+            if f_plunge is True:
+                gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
+            return gcode
 
 
     def up_to_zero_code(self, p):
     def up_to_zero_code(self, p):
         return 'G01 Z0'
         return 'G01 Z0'
@@ -128,7 +188,10 @@ M0
     def end_code(self, p):
     def end_code(self, p):
         coords_xy = p['toolchange_xy']
         coords_xy = p['toolchange_xy']
         gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n")
         gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n")
-        gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n"
+        if coords_xy is not None:
+            gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n"
+        else:
+            gcode += 'G00 X0 Y0' + "\n"
         return gcode
         return gcode
 
 
     def feedrate_code(self, p):
     def feedrate_code(self, p):

+ 64 - 9
postprocessors/default.py

@@ -11,6 +11,11 @@ class default(FlatCAMPostProc):
         coords_xy = p['toolchange_xy']
         coords_xy = p['toolchange_xy']
         gcode = ''
         gcode = ''
 
 
+        xmin = '%.*f' % (p.coords_decimals, p['options']['xmin'])
+        xmax = '%.*f' % (p.coords_decimals, p['options']['xmax'])
+        ymin = '%.*f' % (p.coords_decimals, p['options']['ymin'])
+        ymax = '%.*f' % (p.coords_decimals, p['options']['ymax'])
+
         if str(p['options']['type']) == 'Geometry':
         if str(p['options']['type']) == 'Geometry':
             gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n'
             gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n'
 
 
@@ -29,7 +34,12 @@ class default(FlatCAMPostProc):
 
 
         gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
         gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
         gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n'
         gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n'
-        gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
+
+        if coords_xy is not None:
+            gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
+        else:
+            gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
+
         gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
         gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
         gcode += '(Z End: ' + str(p['endz']) + units + ')\n'
         gcode += '(Z End: ' + str(p['endz']) + units + ')\n'
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
@@ -37,7 +47,10 @@ class default(FlatCAMPostProc):
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
             gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
             gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
         else:
         else:
-            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n'
+            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
+
+        gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
+        gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
 
 
         gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed'])
         gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed'])
 
 
@@ -62,8 +75,12 @@ class default(FlatCAMPostProc):
     def toolchange_code(self, p):
     def toolchange_code(self, p):
         toolchangez = p.toolchangez
         toolchangez = p.toolchangez
         toolchangexy = p.toolchange_xy
         toolchangexy = p.toolchange_xy
-        toolchangex = toolchangexy[0]
-        toolchangey = toolchangexy[1]
+        f_plunge = p.f_plunge
+        gcode = ''
+
+        if toolchangexy is not None:
+            toolchangex = toolchangexy[0]
+            toolchangey = toolchangexy[1]
 
 
         no_drills = 1
         no_drills = 1
 
 
@@ -79,17 +96,49 @@ class default(FlatCAMPostProc):
             for i in p['options']['Tools_in_use']:
             for i in p['options']['Tools_in_use']:
                 if i[0] == p.tool:
                 if i[0] == p.tool:
                     no_drills = i[2]
                     no_drills = i[2]
-            return """G00 Z{toolchangez}
+
+            if toolchangexy is not None:
+                gcode = """G00 Z{toolchangez}
+G00 X{toolchangex} Y{toolchangey}                
 T{tool}
 T{tool}
 M5
 M5
 M6
 M6
-(MSG, Change to Tool Dia = {toolC}, Total drills for current tool = {t_drills})
-M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
+(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
+M0""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex),
+             toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey),
+             toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez),
+             tool=int(p.tool),
+             t_drills=no_drills,
+             toolC=toolC_formatted)
+            else:
+                gcode = """G00 Z{toolchangez}
+T{tool}
+M5
+M6
+(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
+M0""".format(toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez),
              tool=int(p.tool),
              tool=int(p.tool),
              t_drills=no_drills,
              t_drills=no_drills,
              toolC=toolC_formatted)
              toolC=toolC_formatted)
+            if f_plunge is True:
+                gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
+            return gcode
+
         else:
         else:
-            return """G00 Z{toolchangez}
+            if toolchangexy is not None:
+                gcode = """G00 Z{toolchangez}
+G00 X{toolchangex} Y{toolchangey}
+T{tool}
+M5
+M6    
+(MSG, Change to Tool Dia = {toolC})
+M0""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex),
+             toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey),
+             toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez),
+             tool=int(p.tool),
+             toolC=toolC_formatted)
+            else:
+                gcode = """G00 Z{toolchangez}
 T{tool}
 T{tool}
 M5
 M5
 M6    
 M6    
@@ -98,6 +147,10 @@ M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez)
              tool=int(p.tool),
              tool=int(p.tool),
              toolC=toolC_formatted)
              toolC=toolC_formatted)
 
 
+            if f_plunge is True:
+                gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
+            return gcode
+
     def up_to_zero_code(self, p):
     def up_to_zero_code(self, p):
         return 'G01 Z0'
         return 'G01 Z0'
 
 
@@ -114,7 +167,9 @@ M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez)
     def end_code(self, p):
     def end_code(self, p):
         coords_xy = p['toolchange_xy']
         coords_xy = p['toolchange_xy']
         gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n")
         gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n")
-        gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n"
+
+        if coords_xy is not None:
+            gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n"
         return gcode
         return gcode
 
 
     def feedrate_code(self, p):
     def feedrate_code(self, p):

+ 67 - 8
postprocessors/grbl_11.py

@@ -11,6 +11,11 @@ class grbl_11(FlatCAMPostProc):
         coords_xy = p['toolchange_xy']
         coords_xy = p['toolchange_xy']
         gcode = ''
         gcode = ''
 
 
+        xmin = '%.*f' % (p.coords_decimals, p['options']['xmin'])
+        xmax = '%.*f' % (p.coords_decimals, p['options']['xmax'])
+        ymin = '%.*f' % (p.coords_decimals, p['options']['ymin'])
+        ymax = '%.*f' % (p.coords_decimals, p['options']['ymax'])
+
         if str(p['options']['type']) == 'Geometry':
         if str(p['options']['type']) == 'Geometry':
             gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n' + '\n'
             gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n' + '\n'
 
 
@@ -29,7 +34,10 @@ class grbl_11(FlatCAMPostProc):
 
 
         gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
         gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
         gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n'
         gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n'
-        gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
+        if coords_xy is not None:
+            gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
+        else:
+            gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
         gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
         gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
         gcode += '(Z End: ' + str(p['endz']) + units + ')\n'
         gcode += '(Z End: ' + str(p['endz']) + units + ')\n'
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
@@ -37,7 +45,10 @@ class grbl_11(FlatCAMPostProc):
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
             gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
             gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
         else:
         else:
-            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n'
+            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
+
+        gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
+        gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
 
 
         gcode += '(Spindle Speed: ' + str(p['spindlespeed']) + ' RPM' + ')\n' + '\n'
         gcode += '(Spindle Speed: ' + str(p['spindlespeed']) + ' RPM' + ')\n' + '\n'
 
 
@@ -62,6 +73,15 @@ class grbl_11(FlatCAMPostProc):
 
 
     def toolchange_code(self, p):
     def toolchange_code(self, p):
         toolchangez = p.toolchangez
         toolchangez = p.toolchangez
+        toolchangexy = p.toolchange_xy
+        f_plunge = p.f_plunge
+        gcode = ''
+
+        if toolchangexy is not None:
+            toolchangex = toolchangexy[0]
+            toolchangey = toolchangexy[1]
+
+        no_drills = 1
 
 
         if int(p.tool) == 1 and p.startz is not None:
         if int(p.tool) == 1 and p.startz is not None:
             toolchangez = p.startz
             toolchangez = p.startz
@@ -75,17 +95,50 @@ class grbl_11(FlatCAMPostProc):
             for i in p['options']['Tools_in_use']:
             for i in p['options']['Tools_in_use']:
                 if i[0] == p.tool:
                 if i[0] == p.tool:
                     no_drills = i[2]
                     no_drills = i[2]
-            return """G00 Z{toolchangez}
+
+            if toolchangexy is not None:
+                gcode = """G00 Z{toolchangez}
+G00 X{toolchangex} Y{toolchangey}                
 T{tool}
 T{tool}
 M5
 M5
 M6
 M6
-(MSG, Change to Tool Dia = {toolC}, Total drills for current tool = {t_drills})
-M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
+(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
+M0""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex),
+             toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey),
+             toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez),
+             tool=int(p.tool),
+             t_drills=no_drills,
+             toolC=toolC_formatted)
+            else:
+                gcode = """G00 Z{toolchangez}
+T{tool}
+M5
+M6
+(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
+M0""".format(toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez),
              tool=int(p.tool),
              tool=int(p.tool),
              t_drills=no_drills,
              t_drills=no_drills,
              toolC=toolC_formatted)
              toolC=toolC_formatted)
+
+            if f_plunge is True:
+                gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
+            return gcode
+
         else:
         else:
-            return """G00 Z{toolchangez}
+            if toolchangexy is not None:
+                gcode = """G00 Z{toolchangez}
+G00 X{toolchangex} Y{toolchangey}
+T{tool}
+M5
+M6    
+(MSG, Change to Tool Dia = {toolC})
+M0""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex),
+             toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey),
+             toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez),
+             tool=int(p.tool),
+             toolC=toolC_formatted)
+            else:
+                gcode = """G00 Z{toolchangez}
 T{tool}
 T{tool}
 M5
 M5
 M6    
 M6    
@@ -94,6 +147,10 @@ M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez)
              tool=int(p.tool),
              tool=int(p.tool),
              toolC=toolC_formatted)
              toolC=toolC_formatted)
 
 
+            if f_plunge is True:
+                gcode += '\nG00 Z%.*f' % (p.coords_decimals, p.z_move)
+            return gcode
+
     def up_to_zero_code(self, p):
     def up_to_zero_code(self, p):
         return 'G01 Z0'
         return 'G01 Z0'
 
 
@@ -110,8 +167,10 @@ M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez)
 
 
     def end_code(self, p):
     def end_code(self, p):
         coords_xy = p['toolchange_xy']
         coords_xy = p['toolchange_xy']
-        gcode = ('G00 Z' + self.feedrate_format % (p.fr_decimals, p.endz) + "\n")
-        gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n"
+        gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n")
+
+        if coords_xy is not None:
+            gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n"
         return gcode
         return gcode
 
 
     def feedrate_code(self, p):
     def feedrate_code(self, p):

+ 14 - 2
postprocessors/grbl_laser.py

@@ -13,6 +13,11 @@ class grbl_laser(FlatCAMPostProc):
         units = ' ' + str(p['units']).lower()
         units = ' ' + str(p['units']).lower()
         gcode = ''
         gcode = ''
 
 
+        xmin = '%.*f' % (p.coords_decimals, p['options']['xmin'])
+        xmax = '%.*f' % (p.coords_decimals, p['options']['xmax'])
+        ymin = '%.*f' % (p.coords_decimals, p['options']['ymin'])
+        ymax = '%.*f' % (p.coords_decimals, p['options']['ymax'])
+
         gcode += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n'
         gcode += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n'
         gcode += '(Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n'
         gcode += '(Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n'
 
 
@@ -22,7 +27,11 @@ class grbl_laser(FlatCAMPostProc):
             gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
             gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
         else:
         else:
             gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n'
             gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n'
-        gcode += ('G20' if p.units.upper() == 'IN' else 'G21') + "\n"
+        gcode += ('G20' if p.units.upper() == 'IN' else 'G21') + "\n" + '\n'
+
+        gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
+        gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
+
         gcode += 'G90\n'
         gcode += 'G90\n'
         gcode += 'G94\n'
         gcode += 'G94\n'
         gcode += 'G17\n'
         gcode += 'G17\n'
@@ -59,8 +68,11 @@ class grbl_laser(FlatCAMPostProc):
                ' F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
                ' F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
 
 
     def end_code(self, p):
     def end_code(self, p):
+        coords_xy = p['toolchange_xy']
         gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n")
         gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n")
-        gcode += 'G00 X0Y0'
+
+        if coords_xy is not None:
+            gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n"
         return gcode
         return gcode
 
 
     def feedrate_code(self, p):
     def feedrate_code(self, p):

+ 48 - 8
postprocessors/line_xyz.py

@@ -11,6 +11,11 @@ class line_xyz(FlatCAMPostProc):
         coords_xy = p['toolchange_xy']
         coords_xy = p['toolchange_xy']
         gcode = ''
         gcode = ''
 
 
+        xmin = '%.*f' % (p.coords_decimals, p['options']['xmin'])
+        xmax = '%.*f' % (p.coords_decimals, p['options']['xmax'])
+        ymin = '%.*f' % (p.coords_decimals, p['options']['ymin'])
+        ymax = '%.*f' % (p.coords_decimals, p['options']['ymax'])
+
         if str(p['options']['type']) == 'Geometry':
         if str(p['options']['type']) == 'Geometry':
             gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n'
             gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n'
 
 
@@ -29,7 +34,10 @@ class line_xyz(FlatCAMPostProc):
 
 
         gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
         gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
         gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n'
         gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n'
-        gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
+        if coords_xy is not None:
+            gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
+        else:
+            gcode += '(X,Y Toolchange: ' + "None" + units + ')\n'
         gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
         gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
         gcode += '(Z End: ' + str(p['endz']) + units + ')\n'
         gcode += '(Z End: ' + str(p['endz']) + units + ')\n'
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
         gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
@@ -37,7 +45,10 @@ class line_xyz(FlatCAMPostProc):
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
             gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
             gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
         else:
         else:
-            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n'
+            gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n' + '\n'
+
+        gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
+        gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
 
 
         gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed'])
         gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed'])
 
 
@@ -71,8 +82,19 @@ class line_xyz(FlatCAMPostProc):
     def toolchange_code(self, p):
     def toolchange_code(self, p):
         toolchangez = p.toolchangez
         toolchangez = p.toolchangez
         toolchangexy = p.toolchange_xy
         toolchangexy = p.toolchange_xy
-        toolchangex = toolchangexy[0]
-        toolchangey = toolchangexy[1]
+        f_plunge = p.f_plunge
+        gcode = ''
+
+        if toolchangexy is not None:
+            toolchangex = toolchangexy[0]
+            toolchangey = toolchangexy[1]
+        else:
+            if str(p['options']['type']) == 'Excellon':
+                toolchangex = p.oldx
+                toolchangey = p.oldy
+            else:
+                toolchangex = p.x
+                toolchangey = p.y
 
 
         no_drills = 1
         no_drills = 1
 
 
@@ -88,19 +110,26 @@ class line_xyz(FlatCAMPostProc):
             for i in p['options']['Tools_in_use']:
             for i in p['options']['Tools_in_use']:
                 if i[0] == p.tool:
                 if i[0] == p.tool:
                     no_drills = i[2]
                     no_drills = i[2]
-            return """G00 X{toolchangex} Y{toolchangey} Z{toolchangez}
+            gcode =  """G00 X{toolchangex} Y{toolchangey} Z{toolchangez}
 T{tool}
 T{tool}
 M5
 M5
 M6
 M6
-(MSG, Change to Tool Dia = {toolC}, Total drills for current tool = {t_drills})
+(MSG, Change to Tool Dia = {toolC} ||| Total drills for tool T{tool} = {t_drills})
 M0""".format(toolchangex=self.coordinate_format%(p.coords_decimals, toolchangex),
 M0""".format(toolchangex=self.coordinate_format%(p.coords_decimals, toolchangex),
              toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey),
              toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey),
              toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez),
              toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez),
              tool=int(p.tool),
              tool=int(p.tool),
              t_drills=no_drills,
              t_drills=no_drills,
              toolC=toolC_formatted)
              toolC=toolC_formatted)
+
+            if f_plunge is True:
+                gcode += """\nG00 X{toolchangex} Y{toolchangey} Z{z_move}""".format(
+                    toolchangex=self.coordinate_format%(p.coords_decimals, toolchangex),
+                    toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey),
+                    z_move=self.coordinate_format % (p.coords_decimals, p.z_move))
+            return gcode
         else:
         else:
-            return """G00 X{toolchangex} Y{toolchangey} Z{toolchangez}
+            gcode =  """G00 X{toolchangex} Y{toolchangey} Z{toolchangez}
 T{tool}
 T{tool}
 M5
 M5
 M6    
 M6    
@@ -111,6 +140,13 @@ M0""".format(toolchangex=self.coordinate_format%(p.coords_decimals, toolchangex)
              tool=int(p.tool),
              tool=int(p.tool),
              toolC=toolC_formatted)
              toolC=toolC_formatted)
 
 
+            if f_plunge is True:
+                gcode += """\nG00 X{toolchangex} Y{toolchangey} Z{z_move}""".format(
+                    toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex),
+                    toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey),
+                    z_move=self.coordinate_format % (p.coords_decimals, p.z_move))
+            return gcode
+
     def up_to_zero_code(self, p):
     def up_to_zero_code(self, p):
         g = 'G01 ' + 'X' + self.coordinate_format % (p.coords_decimals, p.x) + \
         g = 'G01 ' + 'X' + self.coordinate_format % (p.coords_decimals, p.x) + \
             ' Y' + self.coordinate_format % (p.coords_decimals, p.y) + \
             ' Y' + self.coordinate_format % (p.coords_decimals, p.y) + \
@@ -132,7 +168,11 @@ M0""".format(toolchangex=self.coordinate_format%(p.coords_decimals, toolchangex)
         return g
         return g
 
 
     def end_code(self, p):
     def end_code(self, p):
-        g = ('G00 ' + self.position_code(p)).format(**p)
+        coords_xy = p['toolchange_xy']
+        if coords_xy is not None:
+            g = 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n"
+        else:
+            g = ('G00 ' + self.position_code(p)).format(**p)
         g += ' Z' + self.coordinate_format % (p.coords_decimals, p.endz)
         g += ' Z' + self.coordinate_format % (p.coords_decimals, p.endz)
         return g
         return g
 
 

+ 79 - 13
postprocessors/marlin.py

@@ -9,8 +9,14 @@ class marlin(FlatCAMPostProc):
 
 
     def start_code(self, p):
     def start_code(self, p):
         units = ' ' + str(p['units']).lower()
         units = ' ' + str(p['units']).lower()
+        coords_xy = p['toolchange_xy']
         gcode = ''
         gcode = ''
 
 
+        xmin = '%.*f' % (p.coords_decimals, p['options']['xmin'])
+        xmax = '%.*f' % (p.coords_decimals, p['options']['xmax'])
+        ymin = '%.*f' % (p.coords_decimals, p['options']['ymin'])
+        ymax = '%.*f' % (p.coords_decimals, p['options']['ymax'])
+
         if str(p['options']['type']) == 'Geometry':
         if str(p['options']['type']) == 'Geometry':
             gcode += ';TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + '\n' + '\n'
             gcode += ';TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + '\n' + '\n'
 
 
@@ -29,6 +35,12 @@ class marlin(FlatCAMPostProc):
 
 
         gcode += ';Z_Move: ' + str(p['z_move']) + units + '\n'
         gcode += ';Z_Move: ' + str(p['z_move']) + units + '\n'
         gcode += ';Z Toolchange: ' + str(p['toolchangez']) + units + '\n'
         gcode += ';Z Toolchange: ' + str(p['toolchangez']) + units + '\n'
+
+        if coords_xy is not None:
+            gcode += ';X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + '\n'
+        else:
+            gcode += ';X,Y Toolchange: ' + "None" + units + '\n'
+
         gcode += ';Z Start: ' + str(p['startz']) + units + '\n'
         gcode += ';Z Start: ' + str(p['startz']) + units + '\n'
         gcode += ';Z End: ' + str(p['endz']) + units + '\n'
         gcode += ';Z End: ' + str(p['endz']) + units + '\n'
         gcode += ';Steps per circle: ' + str(p['steps_per_circle']) + '\n'
         gcode += ';Steps per circle: ' + str(p['steps_per_circle']) + '\n'
@@ -36,7 +48,10 @@ class marlin(FlatCAMPostProc):
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
         if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
             gcode += ';Postprocessor Excellon: ' + str(p['pp_excellon_name']) + '\n'
             gcode += ';Postprocessor Excellon: ' + str(p['pp_excellon_name']) + '\n'
         else:
         else:
-            gcode += ';Postprocessor Geometry: ' + str(p['pp_geometry_name']) + '\n'
+            gcode += ';Postprocessor Geometry: ' + str(p['pp_geometry_name']) + '\n' + '\n'
+
+        gcode += ';X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + '\n'
+        gcode += ';Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + '\n\n'
 
 
         gcode += ';Spindle Speed: ' + str(p['spindlespeed']) + ' RPM' + '\n' + '\n'
         gcode += ';Spindle Speed: ' + str(p['spindlespeed']) + ' RPM' + '\n' + '\n'
 
 
@@ -59,6 +74,14 @@ class marlin(FlatCAMPostProc):
 
 
     def toolchange_code(self, p):
     def toolchange_code(self, p):
         toolchangez = p.toolchangez
         toolchangez = p.toolchangez
+        toolchangexy = p.toolchange_xy
+        f_plunge = p.f_plunge
+        gcode = ''
+
+        if toolchangexy is not None:
+            toolchangex = toolchangexy[0]
+            toolchangey = toolchangexy[1]
+
         no_drills = 1
         no_drills = 1
 
 
         if int(p.tool) == 1 and p.startz is not None:
         if int(p.tool) == 1 and p.startz is not None:
@@ -73,20 +96,61 @@ class marlin(FlatCAMPostProc):
             for i in p['options']['Tools_in_use']:
             for i in p['options']['Tools_in_use']:
                 if i[0] == p.tool:
                 if i[0] == p.tool:
                     no_drills = i[2]
                     no_drills = i[2]
-            return """G0 Z{toolchangez}
+
+            if toolchangexy is not None:
+                gcode = """G0 Z{toolchangez}
+G0 X{toolchangex} Y{toolchangey}                
+T{tool}
 M5
 M5
-M0 Change to Tool Dia = {toolC}, Total drills for current tool = {t_drills}
-""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
-           tool=int(p.tool),
-           t_drills=no_drills,
-           toolC=toolC_formatted)
+M6
+;MSG, Change to Tool Dia = {toolC}, Total drills for tool T{tool} = {t_drills}
+M0""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex),
+             toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey),
+             toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez),
+             tool=int(p.tool),
+             t_drills=no_drills,
+             toolC=toolC_formatted)
+            else:
+                gcode = """G0 Z{toolchangez}
+T{tool}
+M5
+M6
+;MSG, Change to Tool Dia = {toolC}, Total drills for tool T{tool} = {t_drills}
+M0""".format(toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez),
+             tool=int(p.tool),
+             t_drills=no_drills,
+             toolC=toolC_formatted)
+
+            if f_plunge is True:
+                gcode += '\nG0 Z%.*f' % (p.coords_decimals, p.z_move)
+            return gcode
+
         else:
         else:
-            return """G0 Z{toolchangez}
+            if toolchangexy is not None:
+                gcode = """G0 Z{toolchangez}
+G0 X{toolchangex} Y{toolchangey}
+T{tool}
+M5
+M6    
+;MSG, Change to Tool Dia = {toolC}
+M0""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex),
+             toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey),
+             toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez),
+             tool=int(p.tool),
+             toolC=toolC_formatted)
+            else:
+                gcode = """G0 Z{toolchangez}
+T{tool}
 M5
 M5
-M0 Change to Tool Dia = {toolC}
-""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
-           tool=int(p.tool),
-           toolC=toolC_formatted)
+M6    
+;MSG, Change to Tool Dia = {toolC}
+M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
+             tool=int(p.tool),
+             toolC=toolC_formatted)
+
+            if f_plunge is True:
+                gcode += '\nG0 Z%.*f' % (p.coords_decimals, p.z_move)
+            return gcode
 
 
     def up_to_zero_code(self, p):
     def up_to_zero_code(self, p):
         return 'G1 Z0' + " " + self.feedrate_code(p)
         return 'G1 Z0' + " " + self.feedrate_code(p)
@@ -104,7 +168,9 @@ M0 Change to Tool Dia = {toolC}
     def end_code(self, p):
     def end_code(self, p):
         coords_xy = p['toolchange_xy']
         coords_xy = p['toolchange_xy']
         gcode = ('G0 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + " " + self.feedrate_rapid_code(p) + "\n")
         gcode = ('G0 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + " " + self.feedrate_rapid_code(p) + "\n")
-        gcode += 'G0 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + " " + self.feedrate_rapid_code(p) + "\n"
+
+        if coords_xy is not None:
+            gcode += 'G0 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + " " + self.feedrate_rapid_code(p) + "\n"
 
 
         return gcode
         return gcode
 
 

BIN
share/fscreen32.png



+ 7 - 0
tclCommands/TclCommandCncjob.py

@@ -55,6 +55,9 @@ class TclCommandCncjob(TclCommandSignaled):
             ('multidepth', 'Use or not multidepth cnccut. (True or False)'),
             ('multidepth', 'Use or not multidepth cnccut. (True or False)'),
             ('depthperpass', 'Height of one layer for multidepth.'),
             ('depthperpass', 'Height of one layer for multidepth.'),
             ('extracut', 'Use or not an extra cnccut over the first point in path,in the job end (example: True)'),
             ('extracut', 'Use or not an extra cnccut over the first point in path,in the job end (example: True)'),
+            ('toolchange', 'Enable tool changes (example: True).'),
+            ('toolchangez', 'Z distance for toolchange (example: 30.0).'),
+            ('toolchangexy', 'X, Y coordonates for toolchange in format (x, y) (example: (2.0, 3.1) ).'),
             ('endz', 'Height where the last move will park.'),
             ('endz', 'Height where the last move will park.'),
             ('outname', 'Name of the resulting Geometry object.'),
             ('outname', 'Name of the resulting Geometry object.'),
             ('ppname_g', 'Name of the Geometry postprocessor. No quotes, case sensitive')
             ('ppname_g', 'Name of the Geometry postprocessor. No quotes, case sensitive')
@@ -96,6 +99,10 @@ class TclCommandCncjob(TclCommandSignaled):
         args["endz"]= args["endz"] if "endz" in args else obj.options["endz"]
         args["endz"]= args["endz"] if "endz" in args else obj.options["endz"]
         args["ppname_g"] = args["ppname_g"] if "ppname_g" in args else obj.options["ppname_g"]
         args["ppname_g"] = args["ppname_g"] if "ppname_g" in args else obj.options["ppname_g"]
 
 
+        args["toolchange"] = True if "toolchange" in args and args["toolchange"] == 1 else False
+        args["toolchangez"] = args["toolchangez"] if "toolchangez" in args else obj.options["toolchangez"]
+        args["toolchangexy"] = args["toolchangexy"] if "toolchangexy" in args else obj.options["toolchangexy"]
+
         del args['name']
         del args['name']
 
 
         # HACK !!! Should be solved elsewhere!!!
         # HACK !!! Should be solved elsewhere!!!

+ 1 - 1
tclCommands/TclCommandCutout.py

@@ -56,7 +56,7 @@ class TclCommandCutout(TclCommand):
             name = args['name']
             name = args['name']
         else:
         else:
             self.app.inform.emit(
             self.app.inform.emit(
-                "[warning]The name of the object for which cutout is done is missing. Add it and retry.")
+                "[WARNING]The name of the object for which cutout is done is missing. Add it and retry.")
             return
             return
 
 
         if 'margin' in args:
         if 'margin' in args:

+ 4 - 4
tclCommands/TclCommandCutoutAny.py

@@ -61,7 +61,7 @@ class TclCommandCutoutAny(TclCommand):
             name = args['name']
             name = args['name']
         else:
         else:
             self.app.inform.emit(
             self.app.inform.emit(
-                "[warning]The name of the object for which cutout is done is missing. Add it and retry.")
+                "[WARNING]The name of the object for which cutout is done is missing. Add it and retry.")
             return
             return
 
 
         if 'margin' in args:
         if 'margin' in args:
@@ -91,11 +91,11 @@ class TclCommandCutoutAny(TclCommand):
             return "Could not retrieve object: %s" % name
             return "Could not retrieve object: %s" % name
 
 
         if 0 in {dia}:
         if 0 in {dia}:
-            self.app.inform.emit("[warning]Tool Diameter is zero value. Change it to a positive integer.")
+            self.app.inform.emit("[WARNING]Tool Diameter is zero value. Change it to a positive integer.")
             return "Tool Diameter is zero value. Change it to a positive integer."
             return "Tool Diameter is zero value. Change it to a positive integer."
 
 
         if gaps not in ['lr', 'tb', '2lr', '2tb', 4, 8]:
         if gaps not in ['lr', 'tb', '2lr', '2tb', 4, 8]:
-            self.app.inform.emit("[warning]Gaps value can be only one of: 'lr', 'tb', '2lr', '2tb', 4 or 8. "
+            self.app.inform.emit("[WARNING]Gaps value can be only one of: 'lr', 'tb', '2lr', '2tb', 4 or 8. "
                                  "Fill in a correct value and retry. ")
                                  "Fill in a correct value and retry. ")
             return
             return
 
 
@@ -129,7 +129,7 @@ class TclCommandCutoutAny(TclCommand):
 
 
             cutout_obj = self.app.collection.get_by_name(outname)
             cutout_obj = self.app.collection.get_by_name(outname)
         else:
         else:
-            self.app.inform.emit("[error]Cancelled. Object type is not supported.")
+            self.app.inform.emit("[ERROR]Cancelled. Object type is not supported.")
             return
             return
 
 
         try:
         try:

+ 6 - 2
tclCommands/TclCommandDrillcncjob.py

@@ -47,6 +47,7 @@ class TclCommandDrillcncjob(TclCommandSignaled):
             ('spindlespeed', 'Speed of the spindle in rpm (example: 4000).'),
             ('spindlespeed', 'Speed of the spindle in rpm (example: 4000).'),
             ('toolchange', 'Enable tool changes (example: True).'),
             ('toolchange', 'Enable tool changes (example: True).'),
             ('toolchangez', 'Z distance for toolchange (example: 30.0).'),
             ('toolchangez', 'Z distance for toolchange (example: 30.0).'),
+            ('toolchangexy', 'X, Y coordonates for toolchange in format (x, y) (example: (2.0, 3.1) ).'),
             ('endz', 'Z distance at job end (example: 30.0).'),
             ('endz', 'Z distance at job end (example: 30.0).'),
             ('ppname_e', 'This is the Excellon postprocessor name: case_sensitive, no_quotes'),
             ('ppname_e', 'This is the Excellon postprocessor name: case_sensitive, no_quotes'),
             ('outname', 'Name of the resulting Geometry object.'),
             ('outname', 'Name of the resulting Geometry object.'),
@@ -85,7 +86,8 @@ class TclCommandDrillcncjob(TclCommandSignaled):
             drillz = args["drillz"] if "drillz" in args else obj.options["drillz"]
             drillz = args["drillz"] if "drillz" in args else obj.options["drillz"]
             job_obj.z_move = args["travelz"] if "travelz" in args else obj.options["travelz"]
             job_obj.z_move = args["travelz"] if "travelz" in args else obj.options["travelz"]
             job_obj.feedrate = args["feedrate"] if "feedrate" in args else obj.options["feedrate"]
             job_obj.feedrate = args["feedrate"] if "feedrate" in args else obj.options["feedrate"]
-            job_obj.feedrate_rapid = args["feedrate_rapid"] if "feedrate_rapid" in args else obj.options["feedrate_rapid"]
+            job_obj.feedrate_rapid = args["feedrate_rapid"] \
+                if "feedrate_rapid" in args else obj.options["feedrate_rapid"]
 
 
             job_obj.spindlespeed = args["spindlespeed"] if "spindlespeed" in args else None
             job_obj.spindlespeed = args["spindlespeed"] if "spindlespeed" in args else None
             job_obj.pp_excellon_name = args["ppname_e"] if "ppname_e" in args \
             job_obj.pp_excellon_name = args["ppname_e"] if "ppname_e" in args \
@@ -93,13 +95,15 @@ class TclCommandDrillcncjob(TclCommandSignaled):
 
 
             toolchange = True if "toolchange" in args and args["toolchange"] == 1 else False
             toolchange = True if "toolchange" in args and args["toolchange"] == 1 else False
             toolchangez = args["toolchangez"] if "toolchangez" in args else obj.options["toolchangez"]
             toolchangez = args["toolchangez"] if "toolchangez" in args else obj.options["toolchangez"]
+            job_obj.toolchangexy = args["toolchangexy"] if "toolchangexy" in args else obj.options["toolchangexy"]
 
 
             endz = args["endz"] if "endz" in args else obj.options["endz"]
             endz = args["endz"] if "endz" in args else obj.options["endz"]
 
 
             tools = args["tools"] if "tools" in args else 'all'
             tools = args["tools"] if "tools" in args else 'all'
             opt_type = args["opt_type"] if "opt_type" in args else 'B'
             opt_type = args["opt_type"] if "opt_type" in args else 'B'
 
 
-            job_obj.generate_from_excellon_by_tool(obj, tools, drillz=drillz, toolchangez=toolchangez, endz=endz,
+            job_obj.generate_from_excellon_by_tool(obj, tools, drillz=drillz, toolchangez=toolchangez,
+                                                   endz=endz,
                                                    toolchange=toolchange, excellon_optimization_type=opt_type)
                                                    toolchange=toolchange, excellon_optimization_type=opt_type)
             job_obj.gcode_parse()
             job_obj.gcode_parse()
             job_obj.create_geometry()
             job_obj.create_geometry()

+ 1 - 1
tclCommands/TclCommandListSys.py

@@ -37,7 +37,7 @@ class TclCommandListSys(TclCommand):
         'args': collections.OrderedDict([
         'args': collections.OrderedDict([
         ]),
         ]),
         'examples': ['list_sys',
         'examples': ['list_sys',
-                     'list_sys ser'
+                     'list_sys ser',
                      'list_sys gerber',
                      'list_sys gerber',
                      'list_sys cncj']
                      'list_sys cncj']
     }
     }

+ 2 - 2
tclCommands/TclCommandOpenGerber.py

@@ -57,12 +57,12 @@ class TclCommandOpenGerber(TclCommandSignaled):
                 gerber_obj.parse_file(filename, follow=follow)
                 gerber_obj.parse_file(filename, follow=follow)
 
 
             except IOError:
             except IOError:
-                app_obj.inform.emit("[error_notcl] Failed to open file: %s " % filename)
+                app_obj.inform.emit("[ERROR_NOTCL] Failed to open file: %s " % filename)
                 app_obj.progress.emit(0)
                 app_obj.progress.emit(0)
                 self.raise_tcl_error('Failed to open file: %s' % filename)
                 self.raise_tcl_error('Failed to open file: %s' % filename)
 
 
             except ParseError as e:
             except ParseError as e:
-                app_obj.inform.emit("[error_notcl] Failed to parse file: %s, %s " % (filename, str(e)))
+                app_obj.inform.emit("[ERROR_NOTCL] Failed to parse file: %s, %s " % (filename, str(e)))
                 app_obj.progress.emit(0)
                 app_obj.progress.emit(0)
                 self.log.error(str(e))
                 self.log.error(str(e))
                 return
                 return

+ 65 - 0
tests/new_window_test.py

@@ -0,0 +1,65 @@
+import sys
+from PyQt5.Qt import *
+from PyQt5 import QtGui, QtWidgets
+
+class MyPopup(QWidget):
+    def __init__(self):
+        QWidget.__init__(self)
+        lay = QtWidgets.QVBoxLayout()
+        self.setLayout(lay)
+        lay.setContentsMargins(0, 0, 0, 0)
+        le = QtWidgets.QLineEdit()
+        le.setText("Abracadabra")
+        le.setReadOnly(True)
+        # le.setStyleSheet("QLineEdit { qproperty-frame: false }")
+        le.setFrame(False)
+        le.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
+
+        # lay.addStretch()
+        but = QtWidgets.QPushButton("OK")
+        hlay = QtWidgets.QHBoxLayout()
+        hlay.setContentsMargins(0, 5, 5, 5)
+
+        hlay.addStretch()
+        hlay.addWidget(but)
+
+        lay.addWidget(le)
+        lay.addLayout(hlay)
+    # def paintEvent(self, e):
+    #     dc = QtGui.QPainter(self)
+    #     dc.drawLine(0, 0, 100, 100)
+    #     dc.drawLine(100, 0, 0, 100)
+
+class MainWindow(QMainWindow):
+    def __init__(self, *args):
+        QtWidgets.QMainWindow.__init__(self, *args)
+        self.cw = QtWidgets.QWidget(self)
+        self.setCentralWidget(self.cw)
+        self.btn1 = QtWidgets.QPushButton("Click me", self.cw)
+        self.btn1.setGeometry(QRect(0, 0, 100, 30))
+        self.btn1.clicked.connect(self.doit)
+        self.w = None
+
+    def doit(self):
+        print("Opening a new popup window...")
+        self.w = MyPopup()
+        self.w.setGeometry(QRect(100, 100, 400, 200))
+        self.w.show()
+
+class App(QApplication):
+    def __init__(self, *args):
+        QtWidgets.QApplication.__init__(self, *args)
+        self.main = MainWindow()
+        # self.lastWindowClosed.connect(self.byebye)
+        self.main.show()
+
+    def byebye(self):
+        self.exit(0)
+
+def main(args):
+    global app
+    app = App(args)
+    app.exec_()
+
+if __name__ == "__main__":
+    main(sys.argv)

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