Browse Source

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

Camellan 6 years ago
parent
commit
d37875898e
87 changed files with 7144 additions and 3183 deletions
  1. 415 287
      FlatCAMApp.py
  2. 148 55
      FlatCAMObj.py
  3. 2 0
      FlatCAMWorker.py
  4. 10 4
      ObjectCollection.py
  5. 193 0
      README.md
  6. 253 66
      camlib.py
  7. 1 0
      config/configuration.txt
  8. 733 70
      flatcamEditors/FlatCAMExcEditor.py
  9. 240 168
      flatcamEditors/FlatCAMGeoEditor.py
  10. 165 79
      flatcamEditors/FlatCAMGrbEditor.py
  11. 445 59
      flatcamGUI/FlatCAMGUI.py
  12. 127 50
      flatcamGUI/GUIElements.py
  13. 149 174
      flatcamGUI/ObjectUI.py
  14. 51 41
      flatcamGUI/PlotCanvas.py
  15. 13 5
      flatcamGUI/VisPyCanvas.py
  16. BIN
      flatcamGUI/VisPyData/data/fonts/opensans-regular.ttf
  17. BIN
      flatcamGUI/VisPyData/data/freetype/freetype253.dll
  18. BIN
      flatcamGUI/VisPyData/data/freetype/freetype253_x64.dll
  19. 2 2
      flatcamParsers/ParseDXF.py
  20. 14 2
      flatcamParsers/ParseSVG.py
  21. 20 14
      flatcamTools/ToolCalculators.py
  22. 261 137
      flatcamTools/ToolCutOut.py
  23. 13 8
      flatcamTools/ToolDblSided.py
  24. 11 6
      flatcamTools/ToolFilm.py
  25. 9 4
      flatcamTools/ToolImage.py
  26. 8 8
      flatcamTools/ToolMeasurement.py
  27. 4 4
      flatcamTools/ToolMove.py
  28. 606 172
      flatcamTools/ToolNonCopperClear.py
  29. 502 146
      flatcamTools/ToolPaint.py
  30. 20 15
      flatcamTools/ToolPanelize.py
  31. 13 8
      flatcamTools/ToolPcbWizard.py
  32. 49 20
      flatcamTools/ToolProperties.py
  33. 26 21
      flatcamTools/ToolSolderPaste.py
  34. 116 37
      flatcamTools/ToolSub.py
  35. 22 16
      flatcamTools/ToolTransform.py
  36. BIN
      locale/de/LC_MESSAGES/strings.mo
  37. 222 195
      locale/de/LC_MESSAGES/strings.po
  38. BIN
      locale/en/LC_MESSAGES/strings.mo
  39. 228 201
      locale/en/LC_MESSAGES/strings.po
  40. BIN
      locale/es/LC_MESSAGES/strings.mo
  41. 222 195
      locale/es/LC_MESSAGES/strings.po
  42. BIN
      locale/pt_BR/LC_MESSAGES/strings.mo
  43. 228 201
      locale/pt_BR/LC_MESSAGES/strings.po
  44. BIN
      locale/ro/LC_MESSAGES/strings.mo
  45. 228 201
      locale/ro/LC_MESSAGES/strings.po
  46. BIN
      locale/ru/LC_MESSAGES/strings.mo
  47. 227 200
      locale/ru/LC_MESSAGES/strings.po
  48. 282 249
      locale_template/strings.pot
  49. 2 0
      make_win.py
  50. BIN
      share/aero_arc.png
  51. BIN
      share/aero_array.png
  52. BIN
      share/aero_buffer.png
  53. BIN
      share/aero_circle.png
  54. BIN
      share/aero_circle_geo.png
  55. BIN
      share/aero_disc.png
  56. BIN
      share/aero_drill.png
  57. BIN
      share/aero_drill_array.png
  58. BIN
      share/aero_path1.png
  59. BIN
      share/aero_path2.png
  60. BIN
      share/aero_path3.png
  61. BIN
      share/aero_path4.png
  62. BIN
      share/aero_path5.png
  63. BIN
      share/aero_semidisc.png
  64. BIN
      share/aero_slot.png
  65. BIN
      share/aero_text.png
  66. BIN
      share/backup24.png
  67. BIN
      share/backup_export24.png
  68. BIN
      share/backup_import24.png
  69. BIN
      share/slot26.png
  70. BIN
      share/slot_array26.png
  71. 2 3
      tclCommands/TclCommand.py
  72. 2 1
      tclCommands/TclCommandAddPolygon.py
  73. 2 1
      tclCommands/TclCommandAddPolyline.py
  74. 95 0
      tclCommands/TclCommandBbox.py
  75. 1 1
      tclCommands/TclCommandClearShell.py
  76. 5 2
      tclCommands/TclCommandCncjob.py
  77. 266 0
      tclCommands/TclCommandCopperClear.py
  78. 1 1
      tclCommands/TclCommandFollow.py
  79. 97 0
      tclCommands/TclCommandNregions.py
  80. 204 28
      tclCommands/TclCommandPaint.py
  81. 60 7
      tclCommands/TclCommandScale.py
  82. 7 4
      tclCommands/TclCommandSkew.py
  83. 16 13
      tclCommands/TclCommandSubtractRectangle.py
  84. 1 1
      tclCommands/TclCommandVersion.py
  85. 1 1
      tclCommands/TclCommandWriteGCode.py
  86. 3 0
      tclCommands/__init__.py
  87. 101 0
      tests/svg/use.svg

File diff suppressed because it is too large
+ 415 - 287
FlatCAMApp.py


+ 148 - 55
FlatCAMObj.py

@@ -73,7 +73,7 @@ class FlatCAMObj(QtCore.QObject):
 
         self.kind = None  # Override with proper name
 
-        # self.shapes = ShapeCollection(parent=self.app.plotcanvas.vispy_canvas.view.scene)
+        # self.shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene)
         self.shapes = self.app.plotcanvas.new_shape_group()
 
         # self.mark_shapes = self.app.plotcanvas.new_shape_collection(layers=2)
@@ -136,7 +136,6 @@ class FlatCAMObj(QtCore.QObject):
     def on_options_change(self, key):
         # Update form on programmatically options change
         self.set_form_item(key)
-
         # Set object visibility
         if key == 'plot':
             self.visible = self.options['plot']
@@ -358,7 +357,7 @@ class FlatCAMObj(QtCore.QObject):
                 pass
 
         if threaded is False:
-            worker_task(self)
+            worker_task(app_obj=self)
         else:
             self.app.worker_task.emit({'fcn': worker_task, 'params': [self]})
 
@@ -774,7 +773,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
         if self.ui.follow_cb.get_value() is True:
             obj = self.app.collection.get_active()
-            obj.follow()
+            obj.follow_geo()
             # in the end toggle the visibility of the origin object so we can see the generated Geometry
             obj.ui.plot_cb.toggle()
         else:
@@ -786,7 +785,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
         if self.ui.follow_cb.get_value() is True:
             obj = self.app.collection.get_active()
-            obj.follow()
+            obj.follow_geo()
             # in the end toggle the visibility of the origin object so we can see the generated Geometry
             obj.ui.plot_cb.toggle()
         else:
@@ -879,23 +878,48 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
             if invert:
                 try:
-                    if type(geom) is MultiPolygon:
+                    try:
                         pl = []
                         for p in geom:
                             if p is not None:
-                                pl.append(Polygon(p.exterior.coords[::-1], p.interiors))
+                                if isinstance(p, Polygon):
+                                    pl.append(Polygon(p.exterior.coords[::-1], p.interiors))
+                                elif isinstance(p, LinearRing):
+                                    pl.append(Polygon(p.coords[::-1]))
                         geom = MultiPolygon(pl)
-                    elif type(geom) is Polygon and geom is not None:
-                        geom = Polygon(geom.exterior.coords[::-1], geom.interiors)
-                    else:
-                        log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> Unexpected Geometry")
+                    except TypeError:
+                        if isinstance(geom, Polygon) and geom is not None:
+                            geom = Polygon(geom.exterior.coords[::-1], geom.interiors)
+                        elif isinstance(geom, LinearRing) and geom is not None:
+                            geom = Polygon(geom.coords[::-1])
+                        else:
+                            log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> Unexpected Geometry %s" %
+                                      type(geom))
                 except Exception as e:
                     log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> %s" % str(e))
                     return 'fail'
             return geom
 
-        if float(self.options["isotooldia"]) < 0:
-            self.options["isotooldia"] = -self.options["isotooldia"]
+            # if invert:
+            #     try:
+            #         if type(geom) is MultiPolygon:
+            #             pl = []
+            #             for p in geom:
+            #                 if p is not None:
+            #                     pl.append(Polygon(p.exterior.coords[::-1], p.interiors))
+            #             geom = MultiPolygon(pl)
+            #         elif type(geom) is Polygon and geom is not None:
+            #             geom = Polygon(geom.exterior.coords[::-1], geom.interiors)
+            #         else:
+            #             log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> Unexpected Geometry %s" %
+            #                       type(geom))
+            #     except Exception as e:
+            #         log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> %s" % str(e))
+            #         return 'fail'
+            # return geom
+
+        # if float(self.options["isotooldia"]) < 0:
+        #     self.options["isotooldia"] = -self.options["isotooldia"]
 
         if combine:
             if self.iso_type == 0:
@@ -983,7 +1007,12 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
                 if empty_cnt == len(geo_obj.solid_geometry):
                     raise ValidationError("Empty Geometry", None)
-                geo_obj.multigeo = True
+
+                # even if combine is checked, one pass is still singlegeo
+                if passes > 1:
+                    geo_obj.multigeo = True
+                else:
+                    geo_obj.multigeo = False
 
             # TODO: Do something if this is None. Offer changing name?
             self.app.new_object("geometry", iso_name, iso_init)
@@ -1100,6 +1129,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         :return: None
         :rtype: None
         """
+        log.debug("FlatCAMObj.FlatCAMGerber.convert_units()")
 
         factor = Gerber.convert_units(self, units)
 
@@ -2082,6 +2112,11 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         # Fill form fields
         self.to_form()
 
+        # update the changes in UI depending on the selected postprocessor in Preferences
+        # after this moment all the changes in the Posprocessor combo will be handled by the activated signal of the
+        # self.ui.pp_excellon_name_cb combobox
+        self.on_pp_changed()
+
         # initialize the dict that holds the tools offset
         t_default_offset = self.app.defaults["excellon_offset"]
         if not self.tool_offset:
@@ -2207,7 +2242,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
             item[0] = str(item[0])
         return table_tools_items
 
-    def export_excellon(self, whole, fract, e_zeros=None, form='dec', factor=1):
+    def export_excellon(self, whole, fract, e_zeros=None, form='dec', factor=1, slot_type='routing'):
         """
         Returns two values, first is a boolean , if 1 then the file has slots and second contain the Excellon code
         :return: has_slots and Excellon_code
@@ -2273,6 +2308,8 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
             if self.slots:
                 has_slots = 1
                 for tool in self.tools:
+                    excellon_code += 'G05\n'
+
                     if int(tool) < 10:
                         excellon_code += 'T0' + str(tool) + '\n'
                     else:
@@ -2284,13 +2321,17 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
                             start_slot_y = slot['start'].y * factor
                             stop_slot_x = slot['stop'].x * factor
                             stop_slot_y = slot['stop'].y * factor
-
-                            excellon_code += "G00X{:.{dec}f}Y{:.{dec}f}\nM15\n".format(start_slot_x,
-                                                                                       start_slot_y,
-                                                                                       dec=fract)
-                            excellon_code += "G00X{:.{dec}f}Y{:.{dec}f}\nM16\n".format(stop_slot_x,
-                                                                                       stop_slot_y,
-                                                                                       dec=fract)
+                            if slot_type == 'routing':
+                                excellon_code += "G00X{:.{dec}f}Y{:.{dec}f}\nM15\n".format(start_slot_x,
+                                                                                           start_slot_y,
+                                                                                           dec=fract)
+                                excellon_code += "G01X{:.{dec}f}Y{:.{dec}f}\nM16\n".format(stop_slot_x,
+                                                                                           stop_slot_y,
+                                                                                           dec=fract)
+                            elif slot_type == 'drilling':
+                                excellon_code += "X{:.{dec}f}Y{:.{dec}f}G85X{:.{dec}f}Y{:.{dec}f}\nG05\n".format(
+                                    start_slot_x, start_slot_y, stop_slot_x, stop_slot_y, dec=fract
+                                )
 
                         elif e_zeros == 'LZ' and tool == slot['tool']:
                             start_slot_x = slot['start'].x * factor
@@ -2322,10 +2363,16 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
                             stop_slot_x_formatted = stop_x_whole + stop_slot_x_formatted[2]
                             stop_slot_y_formatted = stop_y_whole + stop_slot_y_formatted[2]
 
-                            excellon_code += "G00X{xstart}Y{ystart}\nM15\n".format(xstart=start_slot_x_formatted,
-                                                                                   ystart=start_slot_y_formatted)
-                            excellon_code += "G00X{xstop}Y{ystop}\nM16\n".format(xstop=stop_slot_x_formatted,
-                                                                                 ystop=stop_slot_y_formatted)
+                            if slot_type == 'routing':
+                                excellon_code += "G00X{xstart}Y{ystart}\nM15\n".format(xstart=start_slot_x_formatted,
+                                                                                       ystart=start_slot_y_formatted)
+                                excellon_code += "G01X{xstop}Y{ystop}\nM16\n".format(xstop=stop_slot_x_formatted,
+                                                                                     ystop=stop_slot_y_formatted)
+                            elif slot_type == 'drilling':
+                                excellon_code += "{xstart}Y{ystart}G85X{xstop}Y{ystop}\nG05\n".format(
+                                    xstart=start_slot_x_formatted, ystart=start_slot_y_formatted,
+                                    xstop=stop_slot_x_formatted, ystop=stop_slot_y_formatted
+                                )
                         elif tool == slot['tool']:
                             start_slot_x = slot['start'].x * factor
                             start_slot_y = slot['start'].y * factor
@@ -2344,10 +2391,16 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
                             stop_slot_x_formatted.ljust(length, '0')
                             stop_slot_y_formatted.ljust(length, '0')
 
-                            excellon_code += "G00X{xstart}Y{ystart}\nM15\n".format(xstart=start_slot_x_formatted,
-                                                                                   ystart=start_slot_y_formatted)
-                            excellon_code += "G00X{xstop}Y{ystop}\nM16\n".format(xstop=stop_slot_x_formatted,
-                                                                                 ystop=stop_slot_y_formatted)
+                            if slot_type == 'routing':
+                                excellon_code += "G00X{xstart}Y{ystart}\nM15\n".format(xstart=start_slot_x_formatted,
+                                                                                       ystart=start_slot_y_formatted)
+                                excellon_code += "G01X{xstop}Y{ystop}\nM16\n".format(xstop=stop_slot_x_formatted,
+                                                                                     ystop=stop_slot_y_formatted)
+                            elif slot_type == 'drilling':
+                                excellon_code += "{xstart}Y{ystart}G85X{xstop}Y{ystop}\nG05\n".format(
+                                    xstart=start_slot_x_formatted, ystart=start_slot_y_formatted,
+                                    xstop=stop_slot_x_formatted, ystop=stop_slot_y_formatted
+                                )
         except Exception as e:
             log.debug(str(e))
 
@@ -2721,8 +2774,9 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
 
     def convert_units(self, units):
-        factor = Excellon.convert_units(self, units)
+        log.debug("FlatCAMObj.FlatCAMExcellon.convert_units()")
 
+        factor = Excellon.convert_units(self, units)
         self.options['drillz'] = float(self.options['drillz']) * factor
         self.options['travelz'] = float(self.options['travelz']) * factor
         self.options['feedrate'] = float(self.options['feedrate']) * factor
@@ -3060,8 +3114,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         # engine of FlatCAM. Most likely are generated by some of tools and are special cases of geometries.
         self. special_group = None
 
-        self.old_pp_state = ''
-        self.old_toolchangeg_state = ''
+        self.old_pp_state = self.app.defaults["geometry_multidepth"]
+        self.old_toolchangeg_state = self.app.defaults["geometry_toolchange"]
 
         # Attributes to be included in serialization
         # Always append to it because it carries contents
@@ -3251,6 +3305,11 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         # Fill form fields only on object create
         self.to_form()
 
+        # update the changes in UI depending on the selected postprocessor in Preferences
+        # after this moment all the changes in the Posprocessor combo will be handled by the activated signal of the
+        # self.ui.pp_geometry_name_cb combobox
+        self.on_pp_changed()
+
         self.ui.tipdialabel.hide()
         self.ui.tipdia_entry.hide()
         self.ui.tipanglelabel.hide()
@@ -3375,7 +3434,6 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             self.ui.level.setText(_(
                 '<span style="color:red;"><b>Advanced</b></span>'
             ))
-
         self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
         self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click)
         self.ui.paint_tool_button.clicked.connect(lambda: self.app.paint_tool.run(toggle=False))
@@ -4910,6 +4968,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         :return: None
         :rtype: None
         """
+        log.debug("FlatCAMObj.FlatCAMGeometry.scale()")
 
         try:
             xfactor = float(xfactor)
@@ -4952,14 +5011,20 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                     geoms.append(scale_recursion(local_geom))
                 return geoms
             else:
-                return affinity.scale(geom, xfactor, yfactor, origin=(px, py))
+                try:
+                    return affinity.scale(geom, xfactor, yfactor, origin=(px, py))
+                except AttributeError:
+                    return geom
 
         if self.multigeo is True:
             for tool in self.tools:
                 self.tools[tool]['solid_geometry'] = scale_recursion(self.tools[tool]['solid_geometry'])
         else:
-            self.solid_geometry = scale_recursion(self.solid_geometry)
-
+            try:
+                self.solid_geometry = scale_recursion(self.solid_geometry)
+            except AttributeError:
+                self.solid_geometry = []
+                return
         self.app.inform.emit(_(
             "[success] Geometry Scale done."
         ))
@@ -4973,6 +5038,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         :return: None
         :rtype: None
         """
+        log.debug("FlatCAMObj.FlatCAMGeometry.offset()")
 
         try:
             dx, dy = vect
@@ -4990,7 +5056,10 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                     geoms.append(translate_recursion(local_geom))
                 return geoms
             else:
-                return affinity.translate(geom, xoff=dx, yoff=dy)
+                try:
+                    return affinity.translate(geom, xoff=dx, yoff=dy)
+                except AttributeError:
+                    return geom
 
         if self.multigeo is True:
             for tool in self.tools:
@@ -5000,6 +5069,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         self.app.inform.emit(_("[success] Geometry Offset done."))
 
     def convert_units(self, units):
+        log.debug("FlatCAMObj.FlatCAMGeometry.convert_units()")
+
         self.ui_disconnect()
 
         factor = Geometry.convert_units(self, units)
@@ -5146,11 +5217,11 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 for tooluid_key in self.tools:
                     solid_geometry = self.tools[tooluid_key]['solid_geometry']
                     self.plot_element(solid_geometry, visible=visible)
-
-            # plot solid geometry that may be an direct attribute of the geometry object
-            # for SingleGeo
-            if self.solid_geometry:
-                self.plot_element(self.solid_geometry, visible=visible)
+            else:
+                # plot solid geometry that may be an direct attribute of the geometry object
+                # for SingleGeo
+                if self.solid_geometry:
+                    self.plot_element(self.solid_geometry, visible=visible)
 
             # self.plot_element(self.solid_geometry, visible=self.options['plot'])
             self.shapes.redraw()
@@ -5160,8 +5231,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
     def on_plot_cb_click(self, *args):
         if self.muted_ui:
             return
-        self.plot()
         self.read_form_item('plot')
+        self.plot()
 
         self.ui_disconnect()
         cb_flag = self.ui.plot_cb.isChecked()
@@ -5315,7 +5386,9 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         # from predecessors.
         self.ser_attrs += ['options', 'kind', 'cnc_tools', 'multitool']
 
-        self.annotation = self.app.plotcanvas.new_text_group()
+        self.text_col = self.app.plotcanvas.new_text_collection()
+        self.text_col.enabled = True
+        self.annotation = self.app.plotcanvas.new_text_group(collection=self.text_col)
 
     def build_ui(self):
         self.ui_disconnect()
@@ -5456,7 +5529,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         # Fill form fields only on object create
         self.to_form()
 
-        # this means that the object that created this CNCJob was an Excellon
+        # this means that the object that created this CNCJob was an Excellon or Geometry
         try:
             if self.travel_distance:
                 self.ui.t_distance_label.show()
@@ -5465,6 +5538,19 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
                 self.ui.t_distance_entry.set_value('%.4f' % float(self.travel_distance))
                 self.ui.units_label.setText(str(self.units).lower())
                 self.ui.units_label.setDisabled(True)
+
+                self.ui.t_time_label.show()
+                self.ui.t_time_entry.setVisible(True)
+                self.ui.t_time_entry.setDisabled(True)
+                # if time is more than 1 then we have minutes, else we have seconds
+                if self.routing_time > 1:
+                    self.ui.t_time_entry.set_value('%.4f' % math.ceil(float(self.routing_time)))
+                    self.ui.units_time_label.setText('min')
+                else:
+                    time_r = self.routing_time * 60
+                    self.ui.t_time_entry.set_value('%.4f' % math.ceil(float(time_r)))
+                    self.ui.units_time_label.setText('sec')
+                self.ui.units_time_label.setDisabled(True)
         except AttributeError:
             pass
 
@@ -5922,9 +6008,20 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
 
         visible = visible if visible else self.options['plot']
 
+        if self.ui.annotation_cb.get_value() and self.ui.plot_cb.get_value():
+            self.text_col.enabled = True
+        else:
+            self.text_col.enabled = False
+        self.annotation.redraw()
+
         try:
             if self.multitool is False:  # single tool usage
-                self.plot2(tooldia=float(self.options["tooldia"]), obj=self, visible=visible, kind=kind)
+                try:
+                    dia_plot = float(self.options["tooldia"])
+                except ValueError:
+                    # we may have a tuple with only one element and a comma
+                    dia_plot = [float(el) for el in self.options["tooldia"].split(',') if el != ''][0]
+                self.plot2(dia_plot, obj=self, visible=visible, kind=kind)
             else:
                 # multiple tools usage
                 for tooluid_key in self.cnc_tools:
@@ -5936,23 +6033,19 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
             self.shapes.clear(update=True)
             self.annotation.clear(update=True)
 
-        if self.ui.annotation_cb.get_value() and self.ui.plot_cb.get_value():
-            self.app.plotcanvas.text_collection.enabled = True
-        else:
-            self.app.plotcanvas.text_collection.enabled = False
-
     def on_annotation_change(self):
         if self.ui.annotation_cb.get_value():
-            self.app.plotcanvas.text_collection.enabled = True
+            self.text_col.enabled = True
         else:
-            self.app.plotcanvas.text_collection.enabled = False
+            self.text_col.enabled = False
         # kind = self.ui.cncplot_method_combo.get_value()
         # self.plot(kind=kind)
         self.annotation.redraw()
 
     def convert_units(self, units):
+        log.debug("FlatCAMObj.FlatCAMECNCjob.convert_units()")
+
         factor = CNCjob.convert_units(self, units)
-        FlatCAMApp.App.log.debug("FlatCAMCNCjob.convert_units()")
         self.options["tooldia"] = float(self.options["tooldia"]) * factor
 
         param_list = ['cutz', 'depthperpass', 'travelz', 'feedrate', 'feedrate_z', 'feedrate_rapid',

+ 2 - 0
FlatCAMWorker.py

@@ -7,6 +7,7 @@
 # ########################################################## ##
 
 from PyQt5 import QtCore
+# import traceback
 
 
 class Worker(QtCore.QObject):
@@ -60,6 +61,7 @@ class Worker(QtCore.QObject):
                 task['fcn'](*task['params'])
             except Exception as e:
                 self.app.thread_exception.emit(e)
+                # print(traceback.format_exc())
                 # raise e
             finally:
                 self.task_completed.emit(self.name)

+ 10 - 4
ObjectCollection.py

@@ -15,7 +15,7 @@ from FlatCAMObj import *
 import inspect  # TODO: Remove
 import FlatCAMApp
 from PyQt5 import QtGui, QtCore, QtWidgets
-from PyQt5.QtCore import Qt
+from PyQt5.QtCore import Qt, QSettings
 # import webbrowser
 
 import gettext
@@ -256,8 +256,14 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         # self.view.setAcceptDrops(True)
         # self.view.setDropIndicatorShown(True)
 
+        settings = QSettings("Open Source", "FlatCAM")
+        if settings.contains("notebook_font_size"):
+            fsize = settings.value('notebook_font_size', type=int)
+        else:
+            fsize = 12
+
         font = QtGui.QFont()
-        font.setPixelSize(12)
+        font.setPixelSize(fsize)
         font.setFamily("Seagoe UI")
         self.view.setFont(font)
 
@@ -312,7 +318,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
             for obj in self.get_selected():
                 if type(obj) != FlatCAMGeometry:
                     self.app.ui.menuprojectgeneratecnc.setVisible(False)
-                if type(obj) != FlatCAMGeometry and type(obj) != FlatCAMExcellon:
+                if type(obj) != FlatCAMGeometry and type(obj) != FlatCAMExcellon and type(obj) != FlatCAMGerber:
                     self.app.ui.menuprojectedit.setVisible(False)
                 if type(obj) != FlatCAMGerber and type(obj) != FlatCAMExcellon:
                     self.app.ui.menuprojectviewsource.setVisible(False)
@@ -687,7 +693,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         :param name: Name of the FlatCAM Object
         :return: None
         """
-        log.debug("ObjectCollection.set_inactive()")
+        # log.debug("ObjectCollection.set_inactive()")
 
         obj = self.get_by_name(name)
         item = obj.item

+ 193 - 0
README.md

@@ -9,6 +9,199 @@ CAD program, and create G-Code for Isolation routing.
 
 =================================================
 
+25.08.2019
+
+- initial add of a new Tcl Command named CopperClear
+- remade the NCC Tool in preparation for the newly added TclCommand CopperClear
+- finished adding the TclCommandCopperClear that can be called with alias: 'ncc'
+- added new capability in NCC Tool when the reference object is of Gerber type and fixed some newly introduced errors
+- fixed issue #298. The changes in postprocessors done in Preferences dis not update the object UI layout as it was supposed to. The selection of Marlin postproc. did not unhidden the Feedrate Rapids entry.
+- fixed minor issues
+- fixed Tcl Command AddPolygon, AddPolyline
+- fixed Tcl Command CncJob
+- fixed crash due of Properties Tool trying to have a convex hull area on FlatCAMCNCJob objects which is not possible due of their nature
+- modified Tcl Command SubtractRectangle
+- fixed and modernized the Tcl Command Scale to be able to scale on X axis or on Y axis or on both and having as scale reference either the (0, 0) point or the minimum point of the bounding box or the center of the bounding box.
+- fixed and modernized the Tcl Command Skew
+
+24.08.2019
+
+- modified CutOut Tool so now the manual gaps adding will continue until the user is clicking the RMB
+- added ability to turn on/off the grid snapping and to jump to a location while in CutOut Tool manual gap adding action
+- made PlotCanvas class inherit from VisPy Canvas instead of creating an instance of it (work of JP)
+- fixed selection by dragging a selection shape in Geometry Editor
+- modified the Paint Tool. Now the Single Polygon and Area/Reference Object painting works with multiple tools too. The tools have to be selected in the Tool Table.
+- remade the TclCommand Paint to work in the new configuration of the the app (the painting functions are now in their own tool, Paint Tool)
+- fixed a bug in the Properties Tool
+- added a new TcL Command named Nregions who generate non-copper regions
+- added a new TclCommand named Bbox who generate a bounding box.
+
+23.08.2019
+
+- in Tool Cutout for the manual gaps, right mouse button click will exit from the action of adding gaps
+- in Tool Cutout tool I've added the possibility to create a cutout without bridge gaps; added the 'None' option in the Gaps combobox
+- in NCC Tool added ability to add multiple zones to clear when Area option is checked and the modifier key is pressed (either CTRL or SHIFT as set in Preferences). Right click of the mouse is an additional way to finish the job.
+- fixed a bug in Excellon Editor that made that the selection of drills is always cumulative
+- in Paint Tool added ability to add multiple zones to paint when Area option is checked and the modifier key is pressed (either CTRL or SHIFT as set in Preferences). Right click of the mouse is an additional way to finish the job.
+- in Paint Tool and NCC Tool, for the Area option, now mouse panning is allowed while adding areas to process
+- for all the FlatCAM tools launched from toolbar the behavior is modified: first click it will launch the tool; second click: if the Tool tab has focus it will close the tool but if another tab is selected, the tool will have focus
+- modified the NCC Tool and Paint Tool to work multiple times after first launch
+- fixed the issue with GUI entries content being deselected on right click in the box in order to copy the value
+- some changes in GUI tooltips
+- modified the way key modifiers are detected in Gerber Editor Selection class and in Excellon Editor Selection class
+- updated the translations
+- fixed aperture move in Gerber Editor
+- fixed drills/slots move in Excellon Editor
+- RELEASE 8.96
+
+22.08.2019
+
+- added ability to turn ON/OFF the detachable capability of the tabs in Notebook through a context menu activated by right mouse button click on the Notebook header
+- added ability to turn ON/OFF the detachable capability of the tabs in Plot Tab Area through a context menu activated by right mouse button click on the Notebook header
+- added possibility to turn application portable from the Edit -> Preferences -> General -> App. Preferences -> Portable checkbox
+- moved the canvas setup into it's own function and called it in the init() function
+- fixed the Buffer Tool in Geometry Editor; made the Buffer entry field a QDoubleSpinner and set the lower limit to zero.
+- fixed Tool Cutout so when the target Gerber is a single Polygon then the created manual geometry will follow the shape if shape is freeform
+- fixed TclCommandFollow command; an older function name was used who yielded wrong results
+- in Tool Cutout for the manual gaps, now the moving geometry that cuts gaps will orient itself to fit the angle of the cutout geometry
+
+21.08.2019
+
+- added feature in Paint Tool allowing the painting to be done on Gerber objects
+- added feature in Paint Tool to set how (and if) the tools are sorted
+- added Edit -> Preferences GUI entries for the above just added features
+- added new entry in Properties Tool which is the calculated Convex Hull Area (should give a more precise area for the irregular shapes than the box area)
+- added some more strings in Properties Tool for the translation
+- in NCC Tool added area selection feature
+- fixed bug in Excellon parser for the Excellon files that do not put the type of zero suppression they use in the file (like DipTrace eCAD)
+- fixed some issues introduced in NCC Tool
+
+20.08.2019
+
+- added ability to do copper clearing through NCC Tool on Geometry objects
+- replaced the layout from Grid to Form for the Reference objects comboboxes in Paint Tool and in NCC Tool
+
+19.08.2019
+
+- updated the Edit -> Preferences to include also the Gerber Editor complete Preferences
+- started to update the app strings to make it easier for future translations
+- fixed the POT file and the German translation
+- some mods in the Tool Sub
+- fixed bug in Tool Sub that created issues when toggling visibility of the plots
+- fixed the Spanish, Brazilian Portuguese and Romanian translations
+
+18.08.2019
+
+- made the exported preferences formatted therefore more easily read
+- projects at startup don't work in another thread so there is no multithreading if I want to double click an project and to load it
+- added messages in the application window title which show the progress in loading a project (which is not thread-safe therefore keeping the app from fully initialize until finished)
+- in NCC Tool added a new parameter (radio button) that offer the choice on the order of the tools both in tools table and in execution of engraving; added as a parameter also in Edit -> Preferences -> Tools -> NCC Tool
+- added possibility to drag & drop FlatCAM config files (*.FlatConfig) into the canvas to be opened into the application
+- added GUI in Paint tool in beginning to add Paint by external reference object 
+- finished adding in Paint Tool the usage of an external object to set the extent of th area painted. For simple shapes (single Polygon) the shape can be anything, for the rest will be a convex hull of the reference object
+- modified NCC tool so for simple objects (single Polygon) the external object used as reference can have any shape, for the other types of objects the copper cleared area will be the convex hull of the reference object
+- modified the strings of the app wherever they contained the char seq <b> </b> so it is not included in the translated string
+- updated the translation files for the modified strings (and for the newly added strings)
+- added ability to lock toolbars within the context menu that is popped up on any toolbars right mouse click. The value is saved in QSettings and it is persistent between application startup's.
+
+17.08.2019
+
+- added estimated time of routing for the CNCJob and added travelled distance parameter for geometry, too
+- fixed error when creating CNCJob due of having the annotations disabled from preferences but the plot2() function from camlib.CNCJob class still performed operations who yielded TypeError exceptions
+- coded a more accurate way to estimate the job time in CNCJob, taking into consideration if there is a usage of multi depth which generate more passes
+- another fix (final one) for the Exception generated by the annotations set not to show in Preferences
+- updated translations and changed version
+- fixed installer issue for the x64 version due of the used CX_FREEZE python package which was in unofficial version (obviously not ready to be used)
+- fixed bug in Geometry Editor, in disconnect_canvas_event_handlers() where I left some part of code without adding a try - except block which was required
+- moved the initialization of the FlatCAM editors after a read of the default values. If I don't do this then only at the first start of the application the Editors are not functional as the Editor objects are most likely destroyed
+- fixed bug in FlatCAM editors that caused the shapes to be drawn without resolution when the app units where INCH
+- modified the transformation functions in all classes in camlib.py and FlatCAMObj.py to work with empty geometries
+- RELEASE 8.95
+
+17.08.2019
+
+- updated the translations for the new strings
+- RELEASE 8.94
+
+16.08.2019
+
+- working in Excellon Editor to Tool Resize to consider the slots, too
+- fixed a weird error that created a crash in the following scenario: create a new excellon, edit it, add some drills/slots, delete it without saving, create a new excellon, try to edit and a crash is issued due of a wrapped C++ error
+- fixed bug selection in Excellon editor that caused not to select the corresponding row (tool dia) in the tool table when a selection rectangle selected an even number of geometric elements
+- updated the default values to more convenient ones
+- remade the enable/disable plots functions to work only where it needs to (no sense in disabling a plot already disabled)
+- made sure that if multi depth is choosed when creating GCode then if the multidepth is more than the depth of cut only one cut is made (to the depth of cut)
+- each CNCJob object has now it's own text_collection for the annotations which allow for the individual enabling and disabling of the annotations
+- added new menu category in File -> Backup with two menu entries that duplicate the functions of the export/import preferences buttons from the bottom of the Preferences window
+- in Excellon Editor fixed the display of the number of slots in the Tool Table after the resize done with the Resize tool
+- in Excellon Editor -> Resize tool, made sure that when the slot is resized, it's length remain the same, because the tool should influence only the 'thickness' of the slot. Since I don't know anything but the geometry and tool diameters (old and new), this is only an approximation and computationally intensive
+- in Excellon Editor -> remade the Tool edit made by editing the diameter values in the Tools Table to work for slots too
+- In Excellon Editor -> fixed bug that caused incorrect display of the relative coordinates in the status bar
+
+15.08.2019
+
+- added Edit -> Preferences GUI and storage for the Excellon Editor Add Slots
+- added a confirmation message for objects delete and a setting to activate it in Edit -> Preferences -> Global
+- merged pull request from Mike Smith which fix an application crash when attempting to open a not-a-FlatCAM-project file as project
+- merged pull request from Mike Smith that add support for a new SVG element: <use>
+- stored inside FlatCAM app the VisPy data files and at the first start the application will try to copy those files to the APPDATA (roaming) folder in case of running under Windows OS
+- created a configuration file in the root/config/configuration.txt with a configuration line for portability. Set portable to True to run the app as portable
+- working on the Slots Array in Excellon Editor - building the GUI
+- added a failsafe path to the source folder from which to copy the VisPy data
+- fixed the GUI for Slot Arrays in Excellon Editor
+- finished the Slot Array tool in Excellon Editor
+- added the key shortcut handlers for Add Slot and Add Slot Array tools in Excellon Editor
+- started to work on the Resize tool for the case of Excellon slots in Excellon Editor
+- final fix for the VisPy data files; the defaults files are saved to the Config folder when the app is set to be portable
+- added the Slot Type parameter for exporting Excellon in Edit -> Preferences -> Excellon -> Export Excellon. Now the Excellon object can be exported also with drilled slot command G85
+- fixed bug in Excellon export when there are no zero suppression (coordinates with decimals)
+
+14.08.2019
+
+- fixed the loading of Excellon with slots and the saving of edited Excellon object in regard of slots, in Excellon Editor
+- fixed the Delete tool, Select tool in Excellon Editor to work for Slots too
+- changes in the way the edited Excellon with added slots is saved
+- added more icons and cursor in Excellon Editor for Slots related functions
+- in Excellon Editor fixed the selection issue which in a certain step created a failure in the Copy and Move tools.
+- in Excellon Editor fixed the selection with key modifier pressed
+- edited the mouse cursors and saved them without included thumbnail in a bid to remove some CRC warnings made by libpng
+
+13.08.2019
+
+- added new option in ToolSub: the ability to close (or not) the resulting paths when using tool on Geometry objects. Added also a new category in the Edit -> Preferences -> Tools, the Substractor Tool Options
+- some PEP8 changes in FlatCAMApp.py
+- added new settings in Edit -> Preferences -> General for Notebook Font size (set font size for the items in Project Tree and for text in Selected Tab) and for canvas Axis font size. The values are stored in QSettings.
+- updated translations
+- fixed a bug in FCDoubleSpinner GUI element
+- added a new parameter in NCC tool named offset. If the offset is used then the copper clearing will finish to a set distance of the copper features
+- fixed bugs in Geometry Editor
+- added protection's against the 'bowtie' geometries for Subtract Tool in Geometry Editor
+- added all the tools from Geometry Editor to the the contextual menu
+- fixed bug in Add Text Tool in Geometry Editor that gave error when clicking to place text without having text in the box
+- added all the tools from Gerber Editor to the the contextual menu
+- added the menu entry "Edit" in the Project contextual menu for Gerber objects
+- started to work in adding slots and slots array in Excellon Editor
+- in FCSlot finished the utility geometry and the GUI for it
+
+12.08.2019
+
+- done regression to solve the bug with multiple passes cutting from the copper features (I should remember not to make mods here)
+- if 'combine' is checked in Gerber isolation but there is only one pass, the resulting geometry will still be single geo
+- the 'passes' entry was changed to a IntSpinner so it will allow passes to be entered only in range (1, 999) - it will not allow entry of 0 which may create some issues
+- improved the FlatCAMGerber.isolate() function to work for geometry in the form of list and also in case that the elements of the list are LinearRings (like when doing the Exterior Isolation)
+- in NCC Tool made sure that at each run the old objects are deleted
+- fixed bug in camlib.Gerber.parse_lines() Gerber parser where for Allegro Gerber files the Gerber units were incorrectly detected
+- improved Mark Area Tool in Gerber Editor such that at each launch the previous markings are deleted
+
+11.08.2019
+
+- small changes regarding the Project Title
+- trying to fix reported bugs
+- made sure that the annotations are deleted when the object that contain them is deleted
+- fixed issue where the annotations for all the CNCJob objects are toggled together whenever the ones for an single object are toggled
+- optimizations in GeoEditor
+- updated translations
+
 10.08.2019
 
 - added new feature in NCC Tool: now another object can be used as reference for the area extent to be cleared of copper

+ 253 - 66
camlib.py

@@ -229,7 +229,8 @@ class Geometry(object):
         # fixed issue of getting bounds only for one level lists of objects
         # now it can get bounds for nested lists of objects
 
-        log.debug("Geometry->bounds()")
+        log.debug("camlib.Geometry.bounds()")
+
         if self.solid_geometry is None:
             log.debug("solid_geometry is None")
             return 0, 0, 0, 0
@@ -554,22 +555,18 @@ class Geometry(object):
             if follow:
                 geo_iso = self.follow_geometry
             else:
+                if isinstance(self.solid_geometry, list):
+                    temp_geo = cascaded_union(self.solid_geometry)
+                else:
+                    temp_geo = self.solid_geometry
+
+                # Remember: do not make a buffer for each element in the solid_geometry because it will cut into
+                # other copper features
                 if corner is None:
-                    try:
-                        __ = iter(self.solid_geometry)
-                        for el in self.solid_geometry:
-                            geo_iso.append(el.buffer(offset, int(int(self.geo_steps_per_circle) / 4)))
-                    except TypeError:
-                        geo_iso = self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4))
+                    geo_iso = temp_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4))
                 else:
-                    try:
-                        __ = iter(self.solid_geometry)
-                        for el in self.solid_geometry:
-                            geo_iso.append(el.buffer(offset, int(int(self.geo_steps_per_circle) / 4),
-                                                     join_style=corner))
-                    except TypeError:
-                        geo_iso = self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4),
-                                                             join_style=corner)
+                    geo_iso = temp_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4),
+                                              join_style=corner)
 
         # end of replaced block
         if follow:
@@ -1282,7 +1279,7 @@ class Geometry(object):
         :return: Scaling factor resulting from unit change.
         :rtype: float
         """
-        log.debug("Geometry.convert_units()")
+        log.debug("camlib.Geometry.convert_units()")
 
         if units.upper() == self.units.upper():
             return 1.0
@@ -1382,6 +1379,7 @@ class Geometry(object):
         :type point: list
         :return: None
         """
+        log.debug("camlib.Geometry.mirror()")
 
         px, py = point
         xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
@@ -1393,7 +1391,10 @@ class Geometry(object):
                     new_obj.append(mirror_geom(g))
                 return new_obj
             else:
-                return affinity.scale(obj, xscale, yscale, origin=(px, py))
+                try:
+                    return affinity.scale(obj, xscale, yscale, origin=(px, py))
+                except AttributeError:
+                    return obj
 
         try:
             if self.multigeo is True:
@@ -1421,6 +1422,7 @@ class Geometry(object):
         See shapely manual for more information:
         http://toblerity.org/shapely/manual.html#affine-transformations
         """
+        log.debug("camlib.Geometry.rotate()")
 
         px, py = point
 
@@ -1431,7 +1433,10 @@ class Geometry(object):
                     new_obj.append(rotate_geom(g))
                 return new_obj
             else:
-                return affinity.rotate(obj, angle, origin=(px, py))
+                try:
+                    return affinity.rotate(obj, angle, origin=(px, py))
+                except AttributeError:
+                    return obj
 
         try:
             if self.multigeo is True:
@@ -1458,6 +1463,8 @@ class Geometry(object):
         See shapely manual for more information:
         http://toblerity.org/shapely/manual.html#affine-transformations
         """
+        log.debug("camlib.Geometry.skew()")
+
         px, py = point
 
         def skew_geom(obj):
@@ -1467,7 +1474,10 @@ class Geometry(object):
                     new_obj.append(skew_geom(g))
                 return new_obj
             else:
-                return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
+                try:
+                    return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
+                except AttributeError:
+                    return obj
 
         try:
             if self.multigeo is True:
@@ -2362,7 +2372,7 @@ class Gerber (Geometry):
                         "D-no zero suppression)" % self.gerber_zeros)
                     log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute)
 
-                    self.gerber_units = match.group(1)
+                    self.gerber_units = match.group(5)
                     log.debug("Gerber units found = %s" % self.gerber_units)
                     # Changed for issue #80
                     self.convert_units(match.group(5))
@@ -3293,7 +3303,8 @@ class Gerber (Geometry):
         # fixed issue of getting bounds only for one level lists of objects
         # now it can get bounds for nested lists of objects
 
-        log.debug("Gerber->bounds()")
+        log.debug("camlib.Gerber.bounds()")
+
         if self.solid_geometry is None:
             log.debug("solid_geometry is None")
             return 0, 0, 0, 0
@@ -3384,7 +3395,10 @@ class Gerber (Geometry):
                     new_obj.append(scale_geom(g))
                 return new_obj
             else:
-                return affinity.scale(obj, xfactor, yfactor, origin=(px, py))
+                try:
+                    return affinity.scale(obj, xfactor, yfactor, origin=(px, py))
+                except AttributeError:
+                    return obj
 
         self.solid_geometry = scale_geom(self.solid_geometry)
         self.follow_geometry = scale_geom(self.follow_geometry)
@@ -3434,6 +3448,8 @@ class Gerber (Geometry):
         :type vect: tuple
         :return: None
         """
+        log.debug("camlib.Gerber.offset()")
+
         try:
             dx, dy = vect
         except TypeError:
@@ -3448,7 +3464,10 @@ class Gerber (Geometry):
                     new_obj.append(offset_geom(g))
                 return new_obj
             else:
-                return affinity.translate(obj, xoff=dx, yoff=dy)
+                try:
+                    return affinity.translate(obj, xoff=dx, yoff=dy)
+                except AttributeError:
+                    return obj
 
         # ## Solid geometry
         self.solid_geometry = offset_geom(self.solid_geometry)
@@ -3493,6 +3512,7 @@ class Gerber (Geometry):
         :type point: list
         :return: None
         """
+        log.debug("camlib.Gerber.mirror()")
 
         px, py = point
         xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
@@ -3504,7 +3524,10 @@ class Gerber (Geometry):
                     new_obj.append(mirror_geom(g))
                 return new_obj
             else:
-                return affinity.scale(obj, xscale, yscale, origin=(px, py))
+                try:
+                    return affinity.scale(obj, xscale, yscale, origin=(px, py))
+                except AttributeError:
+                    return obj
 
         self.solid_geometry = mirror_geom(self.solid_geometry)
         self.follow_geometry = mirror_geom(self.follow_geometry)
@@ -3540,6 +3563,7 @@ class Gerber (Geometry):
         See shapely manual for more information:
         http://toblerity.org/shapely/manual.html#affine-transformations
         """
+        log.debug("camlib.Gerber.skew()")
 
         px, py = point
 
@@ -3550,7 +3574,10 @@ class Gerber (Geometry):
                     new_obj.append(skew_geom(g))
                 return new_obj
             else:
-                return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
+                try:
+                    return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
+                except AttributeError:
+                    return obj
 
         self.solid_geometry = skew_geom(self.solid_geometry)
         self.follow_geometry = skew_geom(self.follow_geometry)
@@ -3579,6 +3606,7 @@ class Gerber (Geometry):
         :param point:
         :return:
         """
+        log.debug("camlib.Gerber.rotate()")
 
         px, py = point
 
@@ -3589,7 +3617,10 @@ class Gerber (Geometry):
                     new_obj.append(rotate_geom(g))
                 return new_obj
             else:
-                return affinity.rotate(obj, angle, origin=(px, py))
+                try:
+                    return affinity.rotate(obj, angle, origin=(px, py))
+                except AttributeError:
+                    return obj
 
         self.solid_geometry = rotate_geom(self.solid_geometry)
         self.follow_geometry = rotate_geom(self.follow_geometry)
@@ -3983,10 +4014,10 @@ class Excellon(Geometry):
                         ':' + str(self.excellon_format_lower_in))
                     continue
 
-                #### Body ## ##
+                # ### Body ####
                 if not in_header:
 
-                    # ## Tool change # ##
+                    # ## Tool change ###
                     match = self.toolsel_re.search(eline)
                     if match:
                         current_tool = str(int(match.group(1)))
@@ -4026,7 +4057,7 @@ class Excellon(Geometry):
 
                         continue
 
-                    # ## Allegro Type Tool change # ##
+                    # ## Allegro Type Tool change ###
                     if allegro_warning is True:
                         match = self.absinc_re.search(eline)
                         match1 = self.stop_re.search(eline)
@@ -4118,7 +4149,7 @@ class Excellon(Geometry):
                             )
                             continue
 
-                        # Slot coordinates with period: Use literally. # ##
+                        # Slot coordinates with period: Use literally. ###
                         # get the coordinates for slot start and for slot stop into variables
                         start_coords_period = self.coordsperiod_re.search(start_coords_match)
                         stop_coords_period = self.coordsperiod_re.search(stop_coords_match)
@@ -4278,7 +4309,6 @@ class Excellon(Geometry):
                     if match:
                         # signal that there are drill operations
                         self.defaults['excellon_drills'] = True
-
                         try:
                             x = float(match.group(1))
                             repeating_x = current_x
@@ -4350,7 +4380,7 @@ class Excellon(Geometry):
                             # log.debug("{:15} {:8} {:8}".format(eline, x, y))
                             continue
 
-                #### Header ## ##
+                # ### Header ####
                 if in_header:
 
                     # ## Tool definitions # ##
@@ -4488,7 +4518,7 @@ class Excellon(Geometry):
         match = self.leadingzeros_re.search(number_str)
         nr_length = len(match.group(1)) + len(match.group(2))
         try:
-            if self.zeros == "L" or self.zeros == "LZ":
+            if self.zeros == "L" or self.zeros == "LZ": # Leading
                 # With leading zeros, when you type in a coordinate,
                 # the leading zeros must always be included.  Trailing zeros
                 # are unneeded and may be left off. The CNC-7 will automatically add them.
@@ -4609,10 +4639,10 @@ class Excellon(Geometry):
         # fixed issue of getting bounds only for one level lists of objects
         # now it can get bounds for nested lists of objects
 
-        log.debug("Excellon() -> bounds()")
-        # if self.solid_geometry is None:
-        #     log.debug("solid_geometry is None")
-        #     return 0, 0, 0, 0
+        log.debug("camlib.Excellon.bounds()")
+        if self.solid_geometry is None:
+            log.debug("solid_geometry is None")
+            return 0, 0, 0, 0
 
         def bounds_rec(obj):
             if type(obj) is list:
@@ -4669,6 +4699,8 @@ class Excellon(Geometry):
         :type str: IN or MM
         :return:
         """
+        log.debug("camlib.Excellon.convert_units()")
+
         factor = Geometry.convert_units(self, units)
 
         # Tools
@@ -4689,6 +4721,8 @@ class Excellon(Geometry):
         :return: None
         :rtype: NOne
         """
+        log.debug("camlib.Excellon.scale()")
+
         if yfactor is None:
             yfactor = xfactor
 
@@ -4705,8 +4739,10 @@ class Excellon(Geometry):
                     new_obj.append(scale_geom(g))
                 return new_obj
             else:
-                return affinity.scale(obj, xfactor,
-                                             yfactor, origin=(px, py))
+                try:
+                    return affinity.scale(obj, xfactor, yfactor, origin=(px, py))
+                except AttributeError:
+                    return obj
 
         # Drills
         for drill in self.drills:
@@ -4731,6 +4767,7 @@ class Excellon(Geometry):
         :type vect: tuple
         :return: None
         """
+        log.debug("camlib.Excellon.offset()")
 
         dx, dy = vect
 
@@ -4741,7 +4778,10 @@ class Excellon(Geometry):
                     new_obj.append(offset_geom(g))
                 return new_obj
             else:
-                return affinity.translate(obj, xoff=dx, yoff=dy)
+                try:
+                    return affinity.translate(obj, xoff=dx, yoff=dy)
+                except AttributeError:
+                    return obj
 
         # Drills
         for drill in self.drills:
@@ -4768,6 +4808,8 @@ class Excellon(Geometry):
         :type point: list
         :return: None
         """
+        log.debug("camlib.Excellon.mirror()")
+
         px, py = point
         xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
 
@@ -4778,7 +4820,10 @@ class Excellon(Geometry):
                     new_obj.append(mirror_geom(g))
                 return new_obj
             else:
-                return affinity.scale(obj, xscale, yscale, origin=(px, py))
+                try:
+                    return affinity.scale(obj, xscale, yscale, origin=(px, py))
+                except AttributeError:
+                    return obj
 
         # Modify data
         # Drills
@@ -4812,6 +4857,8 @@ class Excellon(Geometry):
         See shapely manual for more information:
         http://toblerity.org/shapely/manual.html#affine-transformations
         """
+        log.debug("camlib.Excellon.skew()")
+
         if angle_x is None:
             angle_x = 0.0
 
@@ -4825,7 +4872,10 @@ class Excellon(Geometry):
                     new_obj.append(skew_geom(g))
                 return new_obj
             else:
-                return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
+                try:
+                    return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
+                except AttributeError:
+                    return obj
 
         if point is None:
             px, py = 0, 0
@@ -4867,6 +4917,7 @@ class Excellon(Geometry):
         :param point: tuple of coordinates (x, y)
         :return:
         """
+        log.debug("camlib.Excellon.rotate()")
 
         def rotate_geom(obj, origin=None):
             if type(obj) is list:
@@ -4876,9 +4927,15 @@ class Excellon(Geometry):
                 return new_obj
             else:
                 if origin:
-                    return affinity.rotate(obj, angle, origin=origin)
+                    try:
+                        return affinity.rotate(obj, angle, origin=origin)
+                    except AttributeError:
+                        return obj
                 else:
-                    return affinity.rotate(obj, angle, origin=(px, py))
+                    try:
+                        return affinity.rotate(obj, angle, origin=(px, py))
+                    except AttributeError:
+                        return obj
 
         if point is None:
             # Drills
@@ -5026,6 +5083,11 @@ class CNCjob(Geometry):
 
         self.tool = 0.0
 
+        # here store the travelled distance
+        self.travel_distance = 0.0
+        # here store the routing time
+        self.routing_time = 0.0
+
         # used for creating drill CCode geometry; will be updated in the generate_from_excellon_by_tool()
         self.exc_drills = None
         self.exc_tools = None
@@ -5048,8 +5110,9 @@ class CNCjob(Geometry):
         return self.__dict__
 
     def convert_units(self, units):
+        log.debug("camlib.CNCJob.convert_units()")
+
         factor = Geometry.convert_units(self, units)
-        log.debug("CNCjob.convert_units()")
 
         self.z_cut = float(self.z_cut) * factor
         self.z_move *= factor
@@ -5287,7 +5350,10 @@ class CNCjob(Geometry):
             self.oldx = 0.0
             self.oldy = 0.0
 
-        measured_distance = 0
+        measured_distance = 0.0
+        measured_down_distance = 0.0
+        measured_up_to_zero_distance = 0.0
+        measured_lift_distance = 0.0
 
         current_platform = platform.architecture()[0]
         if current_platform == '64bit':
@@ -5384,8 +5450,16 @@ class CNCjob(Geometry):
 
                                 gcode += self.doformat(p.rapid_code, x=locx, y=locy)
                                 gcode += self.doformat(p.down_code, x=locx, y=locy)
+
+                                measured_down_distance += abs(self.z_cut) + abs(self.z_move)
+
                                 if self.f_retract is False:
                                     gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
+                                    measured_up_to_zero_distance += abs(self.z_cut)
+                                    measured_lift_distance += abs(self.z_move)
+                                else:
+                                    measured_lift_distance += abs(self.z_cut) + abs(self.z_move)
+
                                 gcode += self.doformat(p.lift_code, x=locx, y=locy)
                                 measured_distance += abs(distance_euclidian(locx, locy, self.oldx, self.oldy))
                                 self.oldx = locx
@@ -5479,10 +5553,19 @@ class CNCjob(Geometry):
                             for k in node_list:
                                 locx = locations[k][0]
                                 locy = locations[k][1]
+
                                 gcode += self.doformat(p.rapid_code, x=locx, y=locy)
                                 gcode += self.doformat(p.down_code, x=locx, y=locy)
+
+                                measured_down_distance += abs(self.z_cut) + abs(self.z_move)
+
                                 if self.f_retract is False:
                                     gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
+                                    measured_up_to_zero_distance += abs(self.z_cut)
+                                    measured_lift_distance += abs(self.z_move)
+                                else:
+                                    measured_lift_distance += abs(self.z_cut) + abs(self.z_move)
+
                                 gcode += self.doformat(p.lift_code, x=locx, y=locy)
                                 measured_distance += abs(distance_euclidian(locx, locy, self.oldx, self.oldy))
                                 self.oldx = locx
@@ -5539,8 +5622,16 @@ class CNCjob(Geometry):
                         for point in self.optimized_travelling_salesman(altPoints):
                             gcode += self.doformat(p.rapid_code, x=point[0], y=point[1])
                             gcode += self.doformat(p.down_code, x=point[0], y=point[1])
+
+                            measured_down_distance += abs(self.z_cut) + abs(self.z_move)
+
                             if self.f_retract is False:
                                 gcode += self.doformat(p.up_to_zero_code, x=point[0], y=point[1])
+                                measured_up_to_zero_distance += abs(self.z_cut)
+                                measured_lift_distance += abs(self.z_move)
+                            else:
+                                measured_lift_distance += abs(self.z_cut) + abs(self.z_move)
+
                             gcode += self.doformat(p.lift_code, x=point[0], y=point[1])
                             measured_distance += abs(distance_euclidian(point[0], point[1], self.oldx, self.oldy))
                             self.oldx = point[0]
@@ -5560,6 +5651,15 @@ class CNCjob(Geometry):
                   str(measured_distance) + '\n')
         self.travel_distance = measured_distance
 
+        # I use the value of self.feedrate_rapid for the feadrate in case of the measure_lift_distance and for
+        # traveled_time because it is not always possible to determine the feedrate that the CNC machine uses
+        # for G0 move (the fastest speed available to the CNC router). Although self.feedrate_rapids is used only with
+        # Marlin postprocessor and derivatives.
+        self.routing_time = (measured_down_distance + measured_up_to_zero_distance) / self.feedrate
+        lift_time = measured_lift_distance / self.feedrate_rapid
+        traveled_time = measured_distance / self.feedrate_rapid
+        self.routing_time += lift_time + traveled_time
+
         self.gcode = gcode
         return 'OK'
 
@@ -5664,6 +5764,10 @@ class CNCjob(Geometry):
                                  "There will be no cut, skipping %s file") % self.options['name'])
             return 'fail'
 
+        # made sure that depth_per_cut is no more then the z_cut
+        if self.z_cut < self.z_depthpercut:
+            self.z_depthpercut = self.z_cut
+
         if self.z_move is None:
             self.app.inform.emit(_("[ERROR_NOTCL] Travel Z parameter is None or zero."))
             return 'fail'
@@ -5734,6 +5838,9 @@ class CNCjob(Geometry):
             if self.dwell is True:
                 self.gcode += self.doformat(p.dwell_code)   # Dwell time
 
+        total_travel = 0.0
+        total_cut = 0.0
+
         # ## Iterate over geometry paths getting the nearest each time.
         log.debug("Starting G-Code...")
         path_count = 0
@@ -5755,21 +5862,41 @@ class CNCjob(Geometry):
 
                 # ---------- Single depth/pass --------
                 if not multidepth:
+                    # calculate the cut distance
+                    total_cut = total_cut + geo.length
+
                     self.gcode += self.create_gcode_single_pass(geo, extracut, tolerance)
 
                 # --------- Multi-pass ---------
                 else:
+                    # calculate the cut distance
+                    # due of the number of cuts (multi depth) it has to multiplied by the number of cuts
+                    nr_cuts = 0
+                    depth = abs(self.z_cut)
+                    while depth > 0:
+                        nr_cuts += 1
+                        depth -= float(self.z_depthpercut)
+
+                    total_cut += (geo.length * nr_cuts)
+
                     self.gcode += self.create_gcode_multi_pass(geo, extracut, tolerance,
                                                                postproc=p, current_point=current_pt)
 
+                # calculate the total distance
+                total_travel = total_travel + abs(distance(pt1=current_pt, pt2=pt))
                 current_pt = geo.coords[-1]
-                pt, geo = storage.nearest(current_pt) # Next
 
+                pt, geo = storage.nearest(current_pt) # Next
         except StopIteration:  # Nothing found in storage.
             pass
 
         log.debug("Finishing G-Code... %s paths traced." % path_count)
 
+        # add move to end position
+        total_travel += abs(distance_euclidian(current_pt[0], current_pt[1], 0, 0))
+        self.travel_distance += total_travel + total_cut
+        self.routing_time += total_cut / self.feedrate
+
         # Finish
         self.gcode += self.doformat(p.spindle_stop_code)
         self.gcode += self.doformat(p.lift_code, x=current_pt[0], y=current_pt[1])
@@ -5874,7 +6001,10 @@ class CNCjob(Geometry):
         flat_geometry = self.flatten(temp_solid_geometry, pathonly=True)
         log.debug("%d paths" % len(flat_geometry))
 
-        self.tooldia = float(tooldia) if tooldia else None
+        try:
+            self.tooldia = float(tooldia) if tooldia else None
+        except ValueError:
+            self.tooldia = [float(el) for el in tooldia.split(',') if el != ''] if tooldia else None
 
         self.z_cut = float(z_cut) if z_cut else None
         self.z_move = float(z_move) if z_move else None
@@ -6000,6 +6130,9 @@ class CNCjob(Geometry):
             if self.dwell is True:
                 self.gcode += self.doformat(p.dwell_code)   # Dwell time
 
+        total_travel = 0.0
+        total_cut = 0.0
+
         # Iterate over geometry paths getting the nearest each time.
         log.debug("Starting G-Code...")
         path_count = 0
@@ -6019,21 +6152,40 @@ class CNCjob(Geometry):
 
                 # ---------- Single depth/pass --------
                 if not multidepth:
+                    # calculate the cut distance
+                    total_cut += geo.length
                     self.gcode += self.create_gcode_single_pass(geo, extracut, tolerance)
 
                 # --------- Multi-pass ---------
                 else:
+                    # calculate the cut distance
+                    # due of the number of cuts (multi depth) it has to multiplied by the number of cuts
+                    nr_cuts = 0
+                    depth = abs(self.z_cut)
+                    while depth > 0:
+                        nr_cuts += 1
+                        depth -= float(self.z_depthpercut)
+
+                    total_cut += (geo.length * nr_cuts)
+
                     self.gcode += self.create_gcode_multi_pass(geo, extracut, tolerance,
                                                                postproc=p, current_point=current_pt)
 
+                # calculate the travel distance
+                total_travel += abs(distance(pt1=current_pt, pt2=pt))
                 current_pt = geo.coords[-1]
-                pt, geo = storage.nearest(current_pt) # Next
 
+                pt, geo = storage.nearest(current_pt) # Next
         except StopIteration:  # Nothing found in storage.
             pass
 
         log.debug("Finishing G-Code... %s paths traced." % path_count)
 
+        # add move to end position
+        total_travel += abs(distance_euclidian(current_pt[0], current_pt[1], 0, 0))
+        self.travel_distance += total_travel + total_cut
+        self.routing_time += total_cut / self.feedrate
+
         # Finish
         self.gcode += self.doformat(p.spindle_stop_code)
         self.gcode += self.doformat(p.lift_code, x=current_pt[0], y=current_pt[1])
@@ -6520,6 +6672,10 @@ class CNCjob(Geometry):
         if tooldia is None:
             tooldia = self.tooldia
 
+        # this should be unlikely unless when upstream the tooldia is a tuple made by one dia and a comma like (2.4,)
+        if isinstance(tooldia, list):
+            tooldia = tooldia[0] if tooldia[0] is not None else self.tooldia
+
         if tooldia == 0:
             for geo in gcode_parsed:
                 if kind == 'all':
@@ -6570,10 +6726,12 @@ class CNCjob(Geometry):
                     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'],
-                               font_size=self.app.defaults["cncjob_annotation_fontsize"],
-                               color=self.app.defaults["cncjob_annotation_fontcolor"])
+            try:
+                obj.annotation.set(text=text, pos=pos, visible=obj.options['plot'],
+                                   font_size=self.app.defaults["cncjob_annotation_fontsize"],
+                                   color=self.app.defaults["cncjob_annotation_fontcolor"])
+            except Exception as e:
+                pass
 
     def create_geometry(self):
         # TODO: This takes forever. Too much data?
@@ -6855,6 +7013,8 @@ class CNCjob(Geometry):
         # fixed issue of getting bounds only for one level lists of objects
         # now it can get bounds for nested lists of objects
 
+        log.debug("camlib.CNCJob.bounds()")
+
         def bounds_rec(obj):
             if type(obj) is list:
                 minx = Inf
@@ -6889,7 +7049,10 @@ class CNCjob(Geometry):
 
             bounds_coords = bounds_rec(self.solid_geometry)
         else:
-
+            minx = Inf
+            miny = Inf
+            maxx = -Inf
+            maxy = -Inf
             for k, v in self.cnc_tools.items():
                 minx = Inf
                 miny = Inf
@@ -6926,6 +7089,7 @@ class CNCjob(Geometry):
         :return: None
         :rtype: None
         """
+        log.debug("camlib.CNCJob.scale()")
 
         if yfactor is None:
             yfactor = xfactor
@@ -7044,7 +7208,10 @@ class CNCjob(Geometry):
             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))
+                try:
+                    g['geom'] = affinity.scale(g['geom'], xfactor, yfactor, origin=(px, py))
+                except AttributeError:
+                    return g['geom']
             self.create_geometry()
         else:
             for k, v in self.cnc_tools.items():
@@ -7052,9 +7219,11 @@ class CNCjob(Geometry):
                 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))
+                    try:
+                        g['geom'] = affinity.scale(g['geom'], xfactor, yfactor, origin=(px, py))
+                    except AttributeError:
+                        return g['geom']
                 v['solid_geometry'] = cascaded_union([geo['geom'] for geo in v['gcode_parsed']])
-
         self.create_geometry()
 
     def offset(self, vect):
@@ -7070,6 +7239,8 @@ class CNCjob(Geometry):
         :type vect: tuple
         :return: None
         """
+        log.debug("camlib.CNCJob.offset()")
+
         dx, dy = vect
 
         def offset_g(g):
@@ -7110,7 +7281,10 @@ class CNCjob(Geometry):
             self.gcode = offset_g(self.gcode)
             # offset geometry
             for g in self.gcode_parsed:
-                g['geom'] = affinity.translate(g['geom'], xoff=dx, yoff=dy)
+                try:
+                    g['geom'] = affinity.translate(g['geom'], xoff=dx, yoff=dy)
+                except AttributeError:
+                    return g['geom']
             self.create_geometry()
         else:
             for k, v in self.cnc_tools.items():
@@ -7118,7 +7292,10 @@ class CNCjob(Geometry):
                 v['gcode'] = offset_g(v['gcode'])
                 # offset gcode_parsed
                 for g in v['gcode_parsed']:
-                    g['geom'] = affinity.translate(g['geom'], xoff=dx, yoff=dy)
+                    try:
+                        g['geom'] = affinity.translate(g['geom'], xoff=dx, yoff=dy)
+                    except AttributeError:
+                        return g['geom']
                 v['solid_geometry'] = cascaded_union([geo['geom'] for geo in v['gcode_parsed']])
 
     def mirror(self, axis, point):
@@ -7128,12 +7305,16 @@ class CNCjob(Geometry):
         :param point: tupple of coordinates (x,y)
         :return:
         """
+        log.debug("camlib.CNCJob.mirror()")
+
         px, py = point
         xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
 
         for g in self.gcode_parsed:
-            g['geom'] = affinity.scale(g['geom'], xscale, yscale, origin=(px, py))
-
+            try:
+                g['geom'] = affinity.scale(g['geom'], xscale, yscale, origin=(px, py))
+            except AttributeError:
+                return g['geom']
         self.create_geometry()
 
     def skew(self, angle_x, angle_y, point):
@@ -7151,12 +7332,15 @@ class CNCjob(Geometry):
         See shapely manual for more information:
         http://toblerity.org/shapely/manual.html#affine-transformations
         """
+        log.debug("camlib.CNCJob.skew()")
+
         px, py = point
 
         for g in self.gcode_parsed:
-            g['geom'] = affinity.skew(g['geom'], angle_x, angle_y,
-                                      origin=(px, py))
-
+            try:
+                g['geom'] = affinity.skew(g['geom'], angle_x, angle_y, origin=(px, py))
+            except AttributeError:
+                return g['geom']
         self.create_geometry()
 
     def rotate(self, angle, point):
@@ -7166,12 +7350,15 @@ class CNCjob(Geometry):
         :param point: tupple of coordinates (x,y)
         :return:
         """
+        log.debug("camlib.CNCJob.rotate()")
 
         px, py = point
 
         for g in self.gcode_parsed:
-            g['geom'] = affinity.rotate(g['geom'], angle, origin=(px, py))
-
+            try:
+                g['geom'] = affinity.rotate(g['geom'], angle, origin=(px, py))
+            except AttributeError:
+                return g['geom']
         self.create_geometry()
 
 

+ 1 - 0
config/configuration.txt

@@ -0,0 +1 @@
+portable=False

File diff suppressed because it is too large
+ 733 - 70
flatcamEditors/FlatCAMExcEditor.py


+ 240 - 168
flatcamEditors/FlatCAMGeoEditor.py

@@ -76,7 +76,9 @@ class BufferSelectionTool(FlatCAMTool):
         self.buffer_tools_box.addLayout(form_layout)
 
         # Buffer distance
-        self.buffer_distance_entry = FCEntry()
+        self.buffer_distance_entry = FCDoubleSpinner()
+        self.buffer_distance_entry.set_precision(4)
+        self.buffer_distance_entry.set_range(0.0000, 999999.9999)
         form_layout.addRow(_("Buffer distance:"), self.buffer_distance_entry)
         self.buffer_corner_lbl = QtWidgets.QLabel(_("Buffer corner:"))
         self.buffer_corner_lbl.setToolTip(
@@ -2358,10 +2360,6 @@ class FCSelect(DrawTool):
 
     def click_release(self, point):
 
-        self.select_shapes(point)
-        return ""
-
-    def select_shapes(self, pos):
         # list where we store the overlapped shapes under our mouse left click position
         over_shape_list = []
 
@@ -2381,7 +2379,7 @@ class FCSelect(DrawTool):
 
             # 3rd method of click selection -> inconvenient
             try:
-                _, closest_shape = self.storage.nearest(pos)
+                _, closest_shape = self.storage.nearest(point)
             except StopIteration:
                 return ""
 
@@ -2400,30 +2398,28 @@ class FCSelect(DrawTool):
                 obj_to_add = over_shape_list[int(FlatCAMGeoEditor.draw_shape_idx)]
 
                 key_modifier = QtWidgets.QApplication.keyboardModifiers()
-                if self.draw_app.app.defaults["global_mselect_key"] == 'Control':
-                    # if CONTROL key is pressed then we add to the selected list the current shape but if it's already
+
+                if key_modifier == QtCore.Qt.ShiftModifier:
+                    mod_key = 'Shift'
+                elif key_modifier == QtCore.Qt.ControlModifier:
+                    mod_key = 'Control'
+                else:
+                    mod_key = None
+
+                if mod_key == self.draw_app.app.defaults["global_mselect_key"]:
+                    # if modifier key is pressed then we add to the selected list the current shape but if it's already
                     # in the selected list, we removed it. Therefore first click selects, second deselects.
-                    if key_modifier == Qt.ControlModifier:
-                        if obj_to_add in self.draw_app.selected:
-                            self.draw_app.selected.remove(obj_to_add)
-                        else:
-                            self.draw_app.selected.append(obj_to_add)
+                    if obj_to_add in self.draw_app.selected:
+                        self.draw_app.selected.remove(obj_to_add)
                     else:
-                        self.draw_app.selected = []
                         self.draw_app.selected.append(obj_to_add)
                 else:
-                    if key_modifier == Qt.ShiftModifier:
-                        if obj_to_add in self.draw_app.selected:
-                            self.draw_app.selected.remove(obj_to_add)
-                        else:
-                            self.draw_app.selected.append(obj_to_add)
-                    else:
-                        self.draw_app.selected = []
-                        self.draw_app.selected.append(obj_to_add)
-
+                    self.draw_app.selected = []
+                    self.draw_app.selected.append(obj_to_add)
         except Exception as e:
             log.error("[ERROR] Something went bad. %s" % str(e))
             raise
+        return ""
 
 
 class FCMove(FCShapeTool):
@@ -2633,15 +2629,20 @@ class FCText(FCShapeTool):
         # Create new geometry
         dx = point[0]
         dy = point[1]
-        try:
-            self.geometry = DrawToolShape(affinity.translate(self.text_gui.text_path, xoff=dx, yoff=dy))
-        except Exception as 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 "
-                                          "supported. Error: %s") % str(e))
-            self.text_gui.text_path = []
-            self.text_gui.hide_tool()
-            self.draw_app.select_tool('select')
+
+        if self.text_gui.text_path:
+            try:
+                self.geometry = DrawToolShape(affinity.translate(self.text_gui.text_path, xoff=dx, yoff=dy))
+            except Exception as 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 "
+                                              "supported. Error: %s") % str(e))
+                self.text_gui.text_path = []
+                self.text_gui.hide_tool()
+                self.draw_app.select_tool('select')
+                return
+        else:
+            self.draw_app.app.inform.emit(_("[WARNING_NOTCL] No text to add."))
             return
 
         self.text_gui.text_path = []
@@ -2703,11 +2704,13 @@ class FCBuffer(FCShapeTool):
         # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment
         # I populated the combobox such that the index coincide with the join styles value (whcih is really an INT)
         join_style = self.buff_tool.buffer_corner_cb.currentIndex() + 1
-        self.draw_app.buffer(buffer_distance, join_style)
+        ret_val = self.draw_app.buffer(buffer_distance, join_style)
         self.app.ui.notebook.setTabText(2, _("Tools"))
         self.draw_app.app.ui.splitter.setSizes([0, 1])
 
         self.disactivate()
+        if ret_val == 'fail':
+            return
         self.draw_app.app.inform.emit(_("[success] Done. Buffer Tool completed."))
 
     def on_buffer_int(self):
@@ -2729,11 +2732,13 @@ class FCBuffer(FCShapeTool):
         # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment
         # I populated the combobox such that the index coincide with the join styles value (whcih is really an INT)
         join_style = self.buff_tool.buffer_corner_cb.currentIndex() + 1
-        self.draw_app.buffer_int(buffer_distance, join_style)
+        ret_val = self.draw_app.buffer_int(buffer_distance, join_style)
         self.app.ui.notebook.setTabText(2, _("Tools"))
         self.draw_app.app.ui.splitter.setSizes([0, 1])
 
         self.disactivate()
+        if ret_val == 'fail':
+            return
         self.draw_app.app.inform.emit(_("[success] Done. Buffer Int Tool completed."))
 
     def on_buffer_ext(self):
@@ -2755,11 +2760,13 @@ class FCBuffer(FCShapeTool):
         # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment
         # I populated the combobox such that the index coincide with the join styles value (whcih is really an INT)
         join_style = self.buff_tool.buffer_corner_cb.currentIndex() + 1
-        self.draw_app.buffer_ext(buffer_distance, join_style)
+        ret_val = self.draw_app.buffer_ext(buffer_distance, join_style)
         self.app.ui.notebook.setTabText(2, _("Tools"))
         self.draw_app.app.ui.splitter.setSizes([0, 1])
 
         self.disactivate()
+        if ret_val == 'fail':
+            return
         self.draw_app.app.inform.emit(_("[success] Done. Buffer Ext Tool completed."))
 
     def activate(self):
@@ -2912,14 +2919,14 @@ class FCTransform(FCShapeTool):
         self.draw_app = draw_app
         self.app = draw_app.app
 
-        self.draw_app.app.infrom.emit(_("Shape transformations ..."))
+        self.draw_app.app.inform.emit(_("Shape transformations ..."))
         self.origin = (0, 0)
         self.draw_app.transform_tool.run()
 
 
-# ##################### ##
-# # ## Main Application # ##
-# ##################### ##
+# ###############################################
+# ################ Main Application #############
+# ###############################################
 class FlatCAMGeoEditor(QtCore.QObject):
 
     transform_complete = QtCore.pyqtSignal()
@@ -3113,6 +3120,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         # store the status of the editor so the Delete at object level will not work until the edit is finished
         self.editor_active = False
+        log.debug("Initialization of the FlatCAM Geometry Editor is finished ...")
 
     def pool_recreated(self, pool):
         self.shapes.pool = pool
@@ -3169,6 +3177,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         # Tell the App that the editor is active
         self.editor_active = True
+        log.debug("Finished activating the Geometry Editor...")
 
     def deactivate(self):
         try:
@@ -3248,6 +3257,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # Show original geometry
         if self.fcgeometry:
             self.fcgeometry.visible = True
+        log.debug("Finished deactivating the Geometry Editor...")
 
     def connect_canvas_event_handlers(self):
         # Canvas events
@@ -3277,7 +3287,22 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # Geometry Editor
         self.app.ui.draw_line.triggered.connect(self.draw_tool_path)
         self.app.ui.draw_rect.triggered.connect(self.draw_tool_rectangle)
+
+        self.app.ui.draw_circle.triggered.connect(lambda: self.select_tool('circle'))
+        self.app.ui.draw_poly.triggered.connect(lambda: self.select_tool('polygon'))
+        self.app.ui.draw_arc.triggered.connect(lambda: self.select_tool('arc'))
+
+        self.app.ui.draw_text.triggered.connect(lambda: self.select_tool('text'))
+        self.app.ui.draw_buffer.triggered.connect(lambda: self.select_tool('buffer'))
+        self.app.ui.draw_paint.triggered.connect(lambda: self.select_tool('paint'))
+        self.app.ui.draw_eraser.triggered.connect(lambda: self.select_tool('eraser'))
+
+        self.app.ui.draw_union.triggered.connect(self.union)
+        self.app.ui.draw_intersect.triggered.connect(self.intersection)
+        self.app.ui.draw_substract.triggered.connect(self.subtract)
         self.app.ui.draw_cut.triggered.connect(self.cutpath)
+        self.app.ui.draw_transform.triggered.connect(lambda: self.select_tool('transform'))
+
         self.app.ui.draw_move.triggered.connect(self.on_move)
 
     def disconnect_canvas_event_handlers(self):
@@ -3332,6 +3357,62 @@ class FlatCAMGeoEditor(QtCore.QObject):
         except (TypeError, AttributeError):
             pass
 
+        try:
+            self.app.ui.draw_circle.triggered.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.ui.draw_poly.triggered.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.ui.draw_arc.triggered.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+
+        try:
+            self.app.ui.draw_text.triggered.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.ui.draw_buffer.triggered.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.ui.draw_paint.triggered.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.ui.draw_eraser.triggered.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.ui.draw_union.triggered.disconnect(self.union)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.ui.draw_intersect.triggered.disconnect(self.intersection)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.ui.draw_substract.triggered.disconnect(self.subtract)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.ui.draw_transform.triggered.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
     def add_shape(self, shape):
         """
         Adds a shape to the shape storage.
@@ -3495,9 +3576,9 @@ class FlatCAMGeoEditor(QtCore.QObject):
         :return: None
         """
 
-        self.pos = self.canvas.vispy_canvas.translate_coords(event.pos)
+        self.pos = self.canvas.translate_coords(event.pos)
 
-        if self.app.grid_status():
+        if self.app.grid_status() == True:
             self.pos = self.app.geo_editor.snap(self.pos[0], self.pos[1])
             self.app.app_cursor.enabled = True
             # Update cursor
@@ -3507,7 +3588,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
             self.pos = (self.pos[0], self.pos[1])
             self.app.app_cursor.enabled = False
 
-        if event.button is 1:
+        if event.button == 1:
             self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
                                                    "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
 
@@ -3536,21 +3617,18 @@ class FlatCAMGeoEditor(QtCore.QObject):
                 if isinstance(self.active_tool, FCSelect):
                     # self.app.log.debug("Replotting after click.")
                     self.replot()
-
             else:
                 self.app.log.debug("No active tool to respond to click!")
 
     def on_canvas_move(self, event):
         """
         Called on 'mouse_move' event
-
         event.pos have canvas screen coordinates
 
         :param event: Event object dispatched by VisPy SceneCavas
         :return: None
         """
-
-        pos = self.canvas.vispy_canvas.translate_coords(event.pos)
+        pos = self.canvas.translate_coords(event.pos)
         event.xdata, event.ydata = pos[0], pos[1]
 
         self.x = event.xdata
@@ -3559,8 +3637,14 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.app.ui.popMenu.mouse_is_panning = False
 
         # if the RMB is clicked and mouse is moving over plot then 'panning_action' is True
-        if event.button == 2 and event.is_dragging == 1:
-            self.app.ui.popMenu.mouse_is_panning = True
+        if event.button == 2:
+            if event.is_dragging:
+                self.app.ui.popMenu.mouse_is_panning = True
+                # return
+            else:
+                self.app.ui.popMenu.mouse_is_panning = False
+
+        if self.active_tool is None:
             return
 
         try:
@@ -3569,11 +3653,8 @@ class FlatCAMGeoEditor(QtCore.QObject):
         except TypeError:
             return
 
-        if self.active_tool is None:
-            return
-
-        # # ## Snap coordinates
-        if self.app.grid_status():
+        # ### Snap coordinates ###
+        if self.app.grid_status() == True:
             x, y = self.snap(x, y)
             self.app.app_cursor.enabled = True
             # Update cursor
@@ -3597,19 +3678,19 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
                                            "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (dx, dy))
 
-        if event.button == 1 and event.is_dragging == 1 and isinstance(self.active_tool, FCEraser):
+        if event.button == 1 and event.is_dragging and isinstance(self.active_tool, FCEraser):
             pass
         else:
-            # # ## Utility geometry (animated)
+            # ### Utility geometry (animated) ###
             geo = self.active_tool.utility_geometry(data=(x, y))
             if isinstance(geo, DrawToolShape) and geo.geo is not None:
                 # Remove any previous utility shape
                 self.tool_shape.clear(update=True)
                 self.draw_utility_geometry(geo=geo)
 
-        # # ## Selection area on canvas section # ##
+        # ### Selection area on canvas section ###
         dx = pos[0] - self.pos[0]
-        if event.is_dragging == 1 and event.button == 1:
+        if event.is_dragging and event.button == 1:
             self.app.delete_selection_shape()
             if dx < 0:
                 self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x, y),
@@ -3623,9 +3704,9 @@ class FlatCAMGeoEditor(QtCore.QObject):
             self.app.selection_type = None
 
     def on_geo_click_release(self, event):
-        pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
+        pos_canvas = self.canvas.translate_coords(event.pos)
 
-        if self.app.grid_status():
+        if self.app.grid_status() == True:
             pos = self.snap(pos_canvas[0], pos_canvas[1])
         else:
             pos = (pos_canvas[0], pos_canvas[1])
@@ -3633,8 +3714,20 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # if the released mouse button was RMB then test if it was a panning motion or not, if not it was a context
         # canvas menu
         try:
-            if event.button == 2:  # right click
-                if self.app.ui.popMenu.mouse_is_panning is False:
+            # if the released mouse button was LMB then test if we had a right-to-left selection or a left-to-right
+            # selection and then select a type of selection ("enclosing" or "touching")
+            if event.button == 1:  # left click
+                if self.app.selection_type is not None:
+                    self.draw_selection_area_handler(self.pos, pos, self.app.selection_type)
+                    self.app.selection_type = None
+                elif isinstance(self.active_tool, FCSelect):
+                    # Dispatch event to active_tool
+                    # msg = self.active_tool.click(self.snap(event.xdata, event.ydata))
+                    self.active_tool.click_release((self.pos[0], self.pos[1]))
+                    # self.app.inform.emit(msg)
+                    self.replot()
+            elif event.button == 2:  # right click
+                if self.app.ui.popMenu.mouse_is_panning == False:
                     if self.in_action is False:
                         try:
                             QtGui.QGuiApplication.restoreOverrideCursor()
@@ -3661,38 +3754,6 @@ class FlatCAMGeoEditor(QtCore.QObject):
                                 self.on_shape_complete()
                                 self.app.inform.emit(_("[success] Done."))
                                 self.select_tool(self.active_tool.name)
-
-                                # MS: always return to the Select Tool if modifier key is not pressed
-                                # else return to the current tool
-                                # key_modifier = QtWidgets.QApplication.keyboardModifiers()
-                                # if self.app.defaults["global_mselect_key"] == 'Control':
-                                #     modifier_to_use = Qt.ControlModifier
-                                # else:
-                                #     modifier_to_use = Qt.ShiftModifier
-                                #
-                                # if key_modifier == modifier_to_use:
-                                #     self.select_tool(self.active_tool.name)
-                                # else:
-                                #     self.select_tool("select")
-
-        except Exception as e:
-            log.warning("Error: %s" % str(e))
-            return
-
-        # if the released mouse button was LMB then test if we had a right-to-left selection or a left-to-right
-        # selection and then select a type of selection ("enclosing" or "touching")
-        try:
-            if event.button == 1:  # left click
-                if self.app.selection_type is not None:
-                    self.draw_selection_area_handler(self.pos, pos, self.app.selection_type)
-                    self.app.selection_type = None
-                elif isinstance(self.active_tool, FCSelect):
-                    # Dispatch event to active_tool
-                    # msg = self.active_tool.click(self.snap(event.xdata, event.ydata))
-                    self.active_tool.click_release((self.pos[0], self.pos[1]))
-                    # self.app.inform.emit(msg)
-                    self.replot()
-
         except Exception as e:
             log.warning("Error: %s" % str(e))
             return
@@ -3707,19 +3768,34 @@ class FlatCAMGeoEditor(QtCore.QObject):
         """
         poly_selection = Polygon([start_pos, (end_pos[0], start_pos[1]), end_pos, (start_pos[0], end_pos[1])])
 
+        key_modifier = QtWidgets.QApplication.keyboardModifiers()
+
+        if key_modifier == QtCore.Qt.ShiftModifier:
+            mod_key = 'Shift'
+        elif key_modifier == QtCore.Qt.ControlModifier:
+            mod_key = 'Control'
+        else:
+            mod_key = None
+
         self.app.delete_selection_shape()
+
+        sel_objects_list = []
         for obj in self.storage.get_objects():
             if (sel_type is True and poly_selection.contains(obj.geo)) or (sel_type is False and
                                                                            poly_selection.intersects(obj.geo)):
-                    if self.key == self.app.defaults["global_mselect_key"]:
-                        if obj in self.selected:
-                            self.selected.remove(obj)
-                        else:
-                            # add the object to the selected shapes
-                            self.selected.append(obj)
-                    else:
-                        if obj not in self.selected:
-                            self.selected.append(obj)
+                sel_objects_list.append(obj)
+
+        if mod_key == self.app.defaults["global_mselect_key"]:
+            for obj in sel_objects_list:
+                if obj in self.selected:
+                    self.selected.remove(obj)
+                else:
+                    # add the object to the selected shapes
+                    self.selected.append(obj)
+        else:
+            self.selected = []
+            self.selected = sel_objects_list
+
         self.replot()
 
     def draw_utility_geometry(self, geo):
@@ -4107,8 +4183,10 @@ class FlatCAMGeoEditor(QtCore.QObject):
         selected = self.get_selected()
         try:
             tools = selected[1:]
-            toolgeo = unary_union([shp.geo for shp in tools])
-            result = selected[0].geo.difference(toolgeo)
+            toolgeo = unary_union([shp.geo for shp in tools]).buffer(0.0000001)
+            target = selected[0].geo
+            target = target.buffer(0.0000001)
+            result = target.difference(toolgeo)
 
             for_deletion = [s for s in self.get_selected()]
             for shape in for_deletion:
@@ -4169,11 +4247,11 @@ class FlatCAMGeoEditor(QtCore.QObject):
             # deselect everything
             self.selected = []
             self.replot()
-            return
+            return 'fail'
 
         if len(selected) == 0:
             self.app.inform.emit(_("[WARNING_NOTCL] Nothing selected for buffering."))
-            return
+            return 'fail'
 
         if not isinstance(buf_distance, float):
             self.app.inform.emit(_("[WARNING_NOTCL] Invalid distance for buffering."))
@@ -4181,17 +4259,32 @@ class FlatCAMGeoEditor(QtCore.QObject):
             # deselect everything
             self.selected = []
             self.replot()
-            return
+            return 'fail'
+
+        results = []
+        for t in selected:
+            if isinstance(t.geo, Polygon) and not t.geo.is_empty:
+                results.append((t.geo.exterior).buffer(
+                    buf_distance - 1e-10,
+                    resolution=int(int(self.app.defaults["geometry_circle_steps"]) / 4),
+                    join_style=join_style)
+                )
+            else:
+                results.append(t.geo.buffer(
+                    buf_distance - 1e-10,
+                    resolution=int(int(self.app.defaults["geometry_circle_steps"]) / 4),
+                    join_style=join_style)
+                )
 
-        pre_buffer = cascaded_union([t.geo for t in selected])
-        results = pre_buffer.buffer(buf_distance - 1e-10, resolution=32, join_style=join_style)
-        if results.is_empty:
+        if not results:
             self.app.inform.emit(_("[ERROR_NOTCL] Failed, the result is empty. Choose a different buffer value."))
             # deselect everything
             self.selected = []
             self.replot()
-            return
-        self.add_shape(DrawToolShape(results))
+            return 'fail'
+
+        for sha in results:
+            self.add_shape(DrawToolShape(sha))
 
         self.replot()
         self.app.inform.emit(_("[success] Full buffer geometry created."))
@@ -4201,77 +4294,48 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         if buf_distance < 0:
             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.")
             )
             # deselect everything
             self.selected = []
             self.replot()
-            return
+            return 'fail'
 
         if len(selected) == 0:
             self.app.inform.emit(_("[WARNING_NOTCL] Nothing selected for buffering."))
-            return
+            return 'fail'
 
         if not isinstance(buf_distance, float):
             self.app.inform.emit(_("[WARNING_NOTCL] Invalid distance for buffering."))
             # deselect everything
             self.selected = []
             self.replot()
-            return
+            return 'fail'
 
-        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 = []
+        for t in selected:
+            if isinstance(t.geo, LinearRing):
+                t.geo = Polygon(t.geo)
+
+            if isinstance(t.geo, Polygon) and not t.geo.is_empty:
+                results.append((t.geo).buffer(
+                    -buf_distance + 1e-10,
+                    resolution=int(int(self.app.defaults["geometry_circle_steps"]) / 4),
+                    join_style=join_style)
+                )
 
-        if results.is_empty:
+        if not results:
             self.app.inform.emit(_("[ERROR_NOTCL] Failed, the result is empty. Choose a smaller buffer value."))
             # deselect everything
             self.selected = []
             self.replot()
-            return
+            return 'fail'
 
-        if type(results) == MultiPolygon:
-            for poly in results:
-                for interior in poly.interiors:
-                    self.add_shape(DrawToolShape(interior))
-        else:
-            for interior in results:
-                self.add_shape(DrawToolShape(interior))
+        for sha in results:
+            self.add_shape(DrawToolShape(sha))
 
         self.replot()
         self.app.inform.emit(_("[success] Interior buffer geometry created."))
-        # selected = self.get_selected()
-        #
-        # if len(selected) == 0:
-        #     self.app.inform.emit("[WARNING] Nothing selected for buffering.")
-        #     return
-        #
-        # if not isinstance(buf_distance, float):
-        #     self.app.inform.emit("[WARNING] Invalid distance for buffering.")
-        #     return
-        #
-        # pre_buffer = cascaded_union([t.geo for t in selected])
-        # results = pre_buffer.buffer(buf_distance)
-        # if results.is_empty:
-        #     self.app.inform.emit("Failed. Choose a smaller buffer value.")
-        #     return
-        #
-        # int_geo = []
-        # if type(results) == MultiPolygon:
-        #     for poly in results:
-        #         for g in poly.interiors:
-        #             int_geo.append(g)
-        #         res = cascaded_union(int_geo)
-        #         self.add_shape(DrawToolShape(res))
-        # else:
-        #     print(results.interiors)
-        #     for g in results.interiors:
-        #         int_geo.append(g)
-        #     res = cascaded_union(int_geo)
-        #     self.add_shape(DrawToolShape(res))
-        #
-        # self.replot()
-        # self.app.inform.emit("Interior buffer geometry created.")
 
     def buffer_ext(self, buf_distance, join_style):
         selected = self.get_selected()
@@ -4295,19 +4359,27 @@ class FlatCAMGeoEditor(QtCore.QObject):
             self.replot()
             return
 
-        pre_buffer = cascaded_union([t.geo for t in selected])
-        results = pre_buffer.buffer(buf_distance - 1e-10, resolution=32, join_style=join_style)
-        if results.is_empty:
+        results = []
+        for t in selected:
+            if isinstance(t.geo, LinearRing):
+                t.geo = Polygon(t.geo)
+
+            if isinstance(t.geo, Polygon) and not t.geo.is_empty:
+                results.append((t.geo).buffer(
+                    buf_distance,
+                    resolution=int(int(self.app.defaults["geometry_circle_steps"]) / 4),
+                    join_style=join_style)
+                )
+
+        if not results:
             self.app.inform.emit(_("[ERROR_NOTCL] Failed, the result is empty. Choose a different buffer value."))
             # deselect everything
             self.selected = []
             self.replot()
             return
-        if type(results) == MultiPolygon:
-            for poly in results:
-                self.add_shape(DrawToolShape(poly.exterior))
-        else:
-            self.add_shape(DrawToolShape(results.exterior))
+
+        for sha in results:
+            self.add_shape(DrawToolShape(sha))
 
         self.replot()
         self.app.inform.emit(_("[success] Exterior buffer geometry created."))

+ 165 - 79
flatcamEditors/FlatCAMGrbEditor.py

@@ -1,3 +1,11 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Date: 8/17/2019                                          #
+# MIT Licence                                              #
+# ##########################################################
+
 from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5.QtCore import Qt, QSettings
 
@@ -530,7 +538,7 @@ class FCPadArray(FCShapeTool):
                         )
                     if 'follow' in geo_el:
                         new_geo_el['follow'] = affinity.translate(
-                            geo_el['solid'], xoff=(dx - self.last_dx), yoff=(dy - self.last_dy)
+                            geo_el['follow'], xoff=(dx - self.last_dx), yoff=(dy - self.last_dy)
                         )
                     geo_el_list.append(new_geo_el)
 
@@ -839,6 +847,8 @@ class FCRegion(FCShapeTool):
         self.name = 'region'
         self.draw_app = draw_app
 
+        self.steps_per_circle = self.draw_app.app.defaults["gerber_circle_steps"]
+
         size_ap = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['size'])
         self.buf_val = (size_ap / 2) if size_ap > 0 else 0.0000001
 
@@ -885,7 +895,7 @@ class FCRegion(FCShapeTool):
         y = data[1]
 
         if len(self.points) == 0:
-            new_geo_el['solid'] = Point(data).buffer(self.buf_val)
+            new_geo_el['solid'] = Point(data).buffer(self.buf_val, resolution=int(self.steps_per_circle / 4))
             return DrawToolUtilityShape(new_geo_el)
 
         if len(self.points) == 1:
@@ -951,12 +961,15 @@ class FCRegion(FCShapeTool):
 
             if len(self.temp_points) > 1:
                 try:
-                    new_geo_el['solid'] = LineString(self.temp_points).buffer(self.buf_val, join_style=1)
+                    new_geo_el['solid'] = LineString(self.temp_points).buffer(self.buf_val,
+                                                                              resolution=int(self.steps_per_circle / 4),
+                                                                              join_style=1)
                     return DrawToolUtilityShape(new_geo_el)
                 except Exception as e:
                     log.debug("FlatCAMGrbEditor.FCRegion.utility_geometry() --> %s" % str(e))
             else:
-                new_geo_el['solid'] = Point(self.temp_points).buffer(self.buf_val)
+                new_geo_el['solid'] = Point(self.temp_points).buffer(self.buf_val,
+                                                                     resolution=int(self.steps_per_circle / 4))
                 return DrawToolUtilityShape(new_geo_el)
 
         if len(self.points) > 2:
@@ -1012,7 +1025,9 @@ class FCRegion(FCShapeTool):
             self.temp_points.append(data)
             new_geo_el = dict()
 
-            new_geo_el['solid'] = LinearRing(self.temp_points).buffer(self.buf_val, join_style=1)
+            new_geo_el['solid'] = LinearRing(self.temp_points).buffer(self.buf_val,
+                                                                      resolution=int(self.steps_per_circle / 4),
+                                                                      join_style=1)
             new_geo_el['follow'] = LinearRing(self.temp_points)
 
             return DrawToolUtilityShape(new_geo_el)
@@ -1031,7 +1046,9 @@ class FCRegion(FCShapeTool):
 
             new_geo_el = dict()
 
-            new_geo_el['solid'] = Polygon(self.points).buffer(self.buf_val, join_style=2)
+            new_geo_el['solid'] = Polygon(self.points).buffer(self.buf_val,
+                                                              resolution=int(self.steps_per_circle / 4),
+                                                              join_style=2)
             new_geo_el['follow'] = Polygon(self.points).exterior
 
             self.geometry = DrawToolShape(new_geo_el)
@@ -1128,10 +1145,12 @@ class FCTrack(FCRegion):
     def make(self):
         new_geo_el = dict()
         if len(self.temp_points) == 1:
-            new_geo_el['solid'] = Point(self.temp_points).buffer(self.buf_val)
+            new_geo_el['solid'] = Point(self.temp_points).buffer(self.buf_val,
+                                                                 resolution=int(self.steps_per_circle / 4))
             new_geo_el['follow'] = Point(self.temp_points)
         else:
-            new_geo_el['solid'] = (LineString(self.temp_points).buffer(self.buf_val)).buffer(0)
+            new_geo_el['solid'] = (LineString(self.temp_points).buffer(
+                self.buf_val, resolution=int(self.steps_per_circle / 4))).buffer(0)
             new_geo_el['follow'] = LineString(self.temp_points)
 
         self.geometry = DrawToolShape(new_geo_el)
@@ -1156,10 +1175,12 @@ class FCTrack(FCRegion):
         new_geo_el = dict()
 
         if len(self.temp_points) == 1:
-            new_geo_el['solid'] = Point(self.temp_points).buffer(self.buf_val)
+            new_geo_el['solid'] = Point(self.temp_points).buffer(self.buf_val,
+                                                                 resolution=int(self.steps_per_circle / 4))
             new_geo_el['follow'] = Point(self.temp_points)
         else:
-            new_geo_el['solid'] = LineString(self.temp_points).buffer(self.buf_val)
+            new_geo_el['solid'] = LineString(self.temp_points).buffer(self.buf_val,
+                                                                      resolution=int(self.steps_per_circle / 4))
             new_geo_el['follow'] = LineString(self.temp_points)
 
         self.draw_app.add_gerber_shape(DrawToolShape(new_geo_el),
@@ -1177,7 +1198,8 @@ class FCTrack(FCRegion):
         new_geo_el = dict()
 
         if len(self.points) == 0:
-            new_geo_el['solid'] = Point(data).buffer(self.buf_val)
+            new_geo_el['solid'] = Point(data).buffer(self.buf_val,
+                                                     resolution=int(self.steps_per_circle / 4))
 
             return DrawToolUtilityShape(new_geo_el)
         elif len(self.points) > 0:
@@ -1235,10 +1257,12 @@ class FCTrack(FCRegion):
 
             self.temp_points.append(data)
             if len(self.temp_points) == 1:
-                new_geo_el['solid'] = Point(self.temp_points).buffer(self.buf_val)
+                new_geo_el['solid'] = Point(self.temp_points).buffer(self.buf_val,
+                                                                     resolution=int(self.steps_per_circle / 4))
                 return DrawToolUtilityShape(new_geo_el)
 
-            new_geo_el['solid'] = LineString(self.temp_points).buffer(self.buf_val)
+            new_geo_el['solid'] = LineString(self.temp_points).buffer(self.buf_val,
+                                                                      resolution=int(self.steps_per_circle / 4))
             return DrawToolUtilityShape(new_geo_el)
 
     def on_key(self, key):
@@ -1775,6 +1799,9 @@ class FCMarkArea(FCShapeTool):
         self.draw_app.hide_tool('all')
         self.draw_app.ma_tool_frame.show()
 
+        # clear previous marking
+        self.draw_app.ma_annotation.clear(update=True)
+
         try:
             self.draw_app.ma_threshold__button.clicked.disconnect()
         except (TypeError, AttributeError):
@@ -2168,6 +2195,9 @@ class FCApertureSelect(DrawTool):
         # bending modes using in FCRegion and FCTrack
         self.draw_app.bend_mode = 1
 
+        # here store the selected apertures
+        self.sel_aperture = set()
+
         try:
             self.grb_editor_app.apertures_table.clearSelection()
         except Exception as e:
@@ -2175,6 +2205,7 @@ class FCApertureSelect(DrawTool):
 
         self.grb_editor_app.hide_tool('all')
         self.grb_editor_app.hide_tool('select')
+        self.grb_editor_app.array_frame.hide()
 
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
@@ -2186,42 +2217,47 @@ class FCApertureSelect(DrawTool):
 
     def click(self, point):
         key_modifier = QtWidgets.QApplication.keyboardModifiers()
-        if self.grb_editor_app.app.defaults["global_mselect_key"] == 'Control':
-            if key_modifier == Qt.ControlModifier:
-                pass
-            else:
-                self.grb_editor_app.selected = []
+
+        if key_modifier == QtCore.Qt.ShiftModifier:
+            mod_key = 'Shift'
+        elif key_modifier == QtCore.Qt.ControlModifier:
+            mod_key = 'Control'
         else:
-            if key_modifier == Qt.ShiftModifier:
-                pass
-            else:
-                self.grb_editor_app.selected = []
+            mod_key = None
+
+        if mod_key == self.draw_app.app.defaults["global_mselect_key"]:
+            pass
+        else:
+            self.grb_editor_app.selected = []
 
     def click_release(self, point):
         self.grb_editor_app.apertures_table.clearSelection()
-        sel_aperture = set()
         key_modifier = QtWidgets.QApplication.keyboardModifiers()
 
+        if key_modifier == QtCore.Qt.ShiftModifier:
+            mod_key = 'Shift'
+        elif key_modifier == QtCore.Qt.ControlModifier:
+            mod_key = 'Control'
+        else:
+            mod_key = None
+
         for storage in self.grb_editor_app.storage_dict:
             try:
                 for geo_el in self.grb_editor_app.storage_dict[storage]['geometry']:
                     if 'solid' in geo_el.geo:
                         geometric_data = geo_el.geo['solid']
                         if Point(point).within(geometric_data):
-                            if (self.grb_editor_app.app.defaults["global_mselect_key"] == 'Control' and
-                                key_modifier == Qt.ControlModifier) or \
-                                    (self.grb_editor_app.app.defaults["global_mselect_key"] == 'Shift' and
-                                     key_modifier == Qt.ShiftModifier):
-
+                            if mod_key == self.grb_editor_app.app.defaults["global_mselect_key"]:
                                 if geo_el in self.draw_app.selected:
                                     self.draw_app.selected.remove(geo_el)
+                                    self.sel_aperture.remove(storage)
                                 else:
                                     # add the object to the selected shapes
                                     self.draw_app.selected.append(geo_el)
-                                    sel_aperture.add(storage)
+                                    self.sel_aperture.add(storage)
                             else:
                                 self.draw_app.selected.append(geo_el)
-                                sel_aperture.add(storage)
+                                self.sel_aperture.add(storage)
             except KeyError:
                 pass
 
@@ -2232,7 +2268,7 @@ class FCApertureSelect(DrawTool):
             log.debug("FlatCAMGrbEditor.FCApertureSelect.click_release() --> %s" % str(e))
 
         self.grb_editor_app.apertures_table.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
-        for aper in sel_aperture:
+        for aper in self.sel_aperture:
             for row in range(self.grb_editor_app.apertures_table.rowCount()):
                 if str(aper) == self.grb_editor_app.apertures_table.item(row, 1).text():
                     self.grb_editor_app.apertures_table.selectRow(row)
@@ -2318,7 +2354,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # #########################
         # ### Gerber Apertures ####
         # #########################
-        self.apertures_table_label = QtWidgets.QLabel(_('<b>Apertures:</b>'))
+        self.apertures_table_label = QtWidgets.QLabel('<b>%s:</b>' % _('Apertures'))
         self.apertures_table_label.setToolTip(
             _("Apertures Table for the Gerber Object.")
         )
@@ -2364,7 +2400,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         grid1 = QtWidgets.QGridLayout()
         self.apertures_box.addLayout(grid1)
 
-        apcode_lbl = QtWidgets.QLabel(_('Aperture Code:'))
+        apcode_lbl = QtWidgets.QLabel('%s:' % _('Aperture Code'))
         apcode_lbl.setToolTip(
         _("Code for the new aperture")
         )
@@ -2374,7 +2410,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.apcode_entry.setValidator(QtGui.QIntValidator(0, 999))
         grid1.addWidget(self.apcode_entry, 1, 1)
 
-        apsize_lbl = QtWidgets.QLabel(_('Aperture Size:'))
+        apsize_lbl = QtWidgets.QLabel('%s:' % _('Aperture Size'))
         apsize_lbl.setToolTip(
         _("Size for the new aperture.\n"
           "If aperture type is 'R' or 'O' then\n"
@@ -2388,7 +2424,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.apsize_entry.setValidator(QtGui.QDoubleValidator(0.0001, 99.9999, 4))
         grid1.addWidget(self.apsize_entry, 2, 1)
 
-        aptype_lbl = QtWidgets.QLabel(_('Aperture Type:'))
+        aptype_lbl = QtWidgets.QLabel('%s:' % _('Aperture Type'))
         aptype_lbl.setToolTip(
         _("Select the type of new aperture. Can be:\n"
           "C = circular\n"
@@ -2401,7 +2437,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.aptype_cb.addItems(['C', 'R', 'O'])
         grid1.addWidget(self.aptype_cb, 3, 1)
 
-        self.apdim_lbl = QtWidgets.QLabel(_('Aperture Dim:'))
+        self.apdim_lbl = QtWidgets.QLabel('%s:' % _('Aperture Dim'))
         self.apdim_lbl.setToolTip(
         _("Dimensions for the new aperture.\n"
           "Active only for rectangular apertures (type R).\n"
@@ -2457,8 +2493,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
         # Buffer distance
         self.buffer_distance_entry = FCEntry()
-        buf_form_layout.addRow(_("Buffer distance:"), self.buffer_distance_entry)
-        self.buffer_corner_lbl = QtWidgets.QLabel(_("Buffer corner:"))
+        buf_form_layout.addRow('%s:' % _("Buffer distance"), self.buffer_distance_entry)
+        self.buffer_corner_lbl = QtWidgets.QLabel('%s:' % _("Buffer corner"))
         self.buffer_corner_lbl.setToolTip(
             _("There are 3 types of corners:\n"
               " - 'Round': the corner is rounded.\n"
@@ -2490,7 +2526,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.scale_tool_frame.hide()
 
         # Title
-        scale_title_lbl = QtWidgets.QLabel('<b>%s</b>' % _('Scale Aperture:'))
+        scale_title_lbl = QtWidgets.QLabel('<b>%s:</b>' % _('Scale Aperture'))
         scale_title_lbl.setToolTip(
             _("Scale a aperture in the aperture list")
         )
@@ -2500,7 +2536,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         scale_form_layout = QtWidgets.QFormLayout()
         self.scale_tools_box.addLayout(scale_form_layout)
 
-        self.scale_factor_lbl = QtWidgets.QLabel(_("Scale factor:"))
+        self.scale_factor_lbl = QtWidgets.QLabel('%s:' % _("Scale factor"))
         self.scale_factor_lbl.setToolTip(
             _("The factor by which to scale the selected aperture.\n"
               "Values can be between 0.0000 and 999.9999")
@@ -2528,7 +2564,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.ma_tool_frame.hide()
 
         # Title
-        ma_title_lbl = QtWidgets.QLabel('<b>%s</b>' % _('Mark polygon areas:'))
+        ma_title_lbl = QtWidgets.QLabel('<b>%s:</b>' % _('Mark polygon areas'))
         ma_title_lbl.setToolTip(
             _("Mark the polygon areas.")
         )
@@ -2538,7 +2574,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         ma_form_layout = QtWidgets.QFormLayout()
         self.ma_tools_box.addLayout(ma_form_layout)
 
-        self.ma_upper_threshold_lbl = QtWidgets.QLabel(_("Area UPPER threshold:"))
+        self.ma_upper_threshold_lbl = QtWidgets.QLabel('%s:' % _("Area UPPER threshold"))
         self.ma_upper_threshold_lbl.setToolTip(
             _("The threshold value, all areas less than this are marked.\n"
               "Can have a value between 0.0000 and 9999.9999")
@@ -2546,7 +2582,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.ma_upper_threshold_entry = FCEntry()
         self.ma_upper_threshold_entry.setValidator(QtGui.QDoubleValidator(0.0000, 9999.9999, 4))
 
-        self.ma_lower_threshold_lbl = QtWidgets.QLabel(_("Area LOWER threshold:"))
+        self.ma_lower_threshold_lbl = QtWidgets.QLabel('%s:' % _("Area LOWER threshold"))
         self.ma_lower_threshold_lbl.setToolTip(
             _("The threshold value, all areas more than this are marked.\n"
               "Can have a value between 0.0000 and 9999.9999")
@@ -2567,7 +2603,6 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # ######################
         # ### Add Pad Array ####
         # ######################
-
         # add a frame and inside add a vertical box layout. Inside this vbox layout I add
         # all the add Pad array  widgets
         # this way I can hide/show the frame
@@ -2600,7 +2635,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.array_form = QtWidgets.QFormLayout()
         self.array_box.addLayout(self.array_form)
 
-        self.pad_array_size_label = QtWidgets.QLabel(_('Nr of pads:'))
+        self.pad_array_size_label = QtWidgets.QLabel('%s:' % _('Nr of pads'))
         self.pad_array_size_label.setToolTip(
             _("Specify how many pads to be in the array.")
         )
@@ -2619,7 +2654,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.linear_form = QtWidgets.QFormLayout()
         self.linear_box.addLayout(self.linear_form)
 
-        self.pad_axis_label = QtWidgets.QLabel(_('Direction:'))
+        self.pad_axis_label = QtWidgets.QLabel('%s:' % _('Direction'))
         self.pad_axis_label.setToolTip(
             _("Direction on which the linear array is oriented:\n"
               "- 'X' - horizontal axis \n"
@@ -2634,7 +2669,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.pad_axis_radio.set_value('X')
         self.linear_form.addRow(self.pad_axis_label, self.pad_axis_radio)
 
-        self.pad_pitch_label = QtWidgets.QLabel(_('Pitch:'))
+        self.pad_pitch_label = QtWidgets.QLabel('%s:' % _('Pitch'))
         self.pad_pitch_label.setToolTip(
             _("Pitch = Distance between elements of the array.")
         )
@@ -2643,7 +2678,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.pad_pitch_entry = LengthEntry()
         self.linear_form.addRow(self.pad_pitch_label, self.pad_pitch_entry)
 
-        self.linear_angle_label = QtWidgets.QLabel(_('Angle:'))
+        self.linear_angle_label = QtWidgets.QLabel('%s:' % _('Angle'))
         self.linear_angle_label.setToolTip(
            _( "Angle at which the linear array is placed.\n"
               "The precision is of max 2 decimals.\n"
@@ -2664,7 +2699,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.circular_box.setContentsMargins(0, 0, 0, 0)
         self.array_circular_frame.setLayout(self.circular_box)
 
-        self.pad_direction_label = QtWidgets.QLabel(_('Direction:'))
+        self.pad_direction_label = QtWidgets.QLabel('%s:' % _('Direction'))
         self.pad_direction_label.setToolTip(
            _("Direction for circular array."
              "Can be CW = clockwise or CCW = counter clockwise.")
@@ -2679,7 +2714,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.pad_direction_radio.set_value('CW')
         self.circular_form.addRow(self.pad_direction_label, self.pad_direction_radio)
 
-        self.pad_angle_label = QtWidgets.QLabel(_('Angle:'))
+        self.pad_angle_label = QtWidgets.QLabel('%s:' % _('Angle'))
         self.pad_angle_label.setToolTip(
             _("Angle at which each element in circular array is placed.")
         )
@@ -2799,6 +2834,11 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # this will flag if the Editor "tools" are launched from key shortcuts (True) or from menu toolbar (False)
         self.launched_from_shortcuts = False
 
+        if self.units == 'MM':
+            self.tolerance = float(self.app.defaults["global_tolerance"])
+        else:
+            self.tolerance = float(self.app.defaults["global_tolerance"]) / 20
+
         def make_callback(the_tool):
             def f():
                 self.on_tool_select(the_tool)
@@ -2884,6 +2924,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.conversion_factor = 1
 
         self.set_ui()
+        log.debug("Initialization of the FlatCAM Gerber Editor is finished ...")
 
     def pool_recreated(self, pool):
         self.shapes.pool = pool
@@ -2911,23 +2952,24 @@ class FlatCAMGrbEditor(QtCore.QObject):
             self.tool2tooldia[i + 1] = tt_aperture
 
         # Init GUI
-        if self.units == 'MM':
-            self.buffer_distance_entry.set_value(0.01)
-            self.scale_factor_entry.set_value(1.0)
-            self.ma_upper_threshold_entry.set_value(1.0)
-            self.apsize_entry.set_value(1.00)
-        else:
-            self.buffer_distance_entry.set_value(0.0003937)
-            self.scale_factor_entry.set_value(0.03937)
-            self.ma_upper_threshold_entry.set_value(0.00155)
-            self.apsize_entry.set_value(0.039)
-        self.ma_lower_threshold_entry.set_value(0.0)
-
-        self.pad_array_size_entry.set_value(5)
-        self.pad_pitch_entry.set_value(2.54)
-        self.pad_angle_entry.set_value(12)
-        self.pad_direction_radio.set_value('CW')
-        self.pad_axis_radio.set_value('X')
+
+        self.buffer_distance_entry.set_value(self.app.defaults["gerber_editor_buff_f"])
+        self.scale_factor_entry.set_value(self.app.defaults["gerber_editor_scale_f"])
+        self.ma_upper_threshold_entry.set_value(self.app.defaults["gerber_editor_ma_low"])
+        self.ma_lower_threshold_entry.set_value(self.app.defaults["gerber_editor_ma_high"])
+
+        self.apsize_entry.set_value(self.app.defaults["gerber_editor_newsize"])
+        self.aptype_cb.set_value(self.app.defaults["gerber_editor_newtype"])
+        self.apdim_entry.set_value(self.app.defaults["gerber_editor_newdim"])
+
+        self.pad_array_size_entry.set_value(self.app.defaults["gerber_editor_array_size"])
+        # linear array
+        self.pad_axis_radio.set_value(self.app.defaults["gerber_editor_lin_axis"])
+        self.pad_pitch_entry.set_value(self.app.defaults["gerber_editor_lin_pitch"])
+        self.linear_angle_spinner.set_value(self.app.defaults["gerber_editor_lin_angle"])
+        # circular array
+        self.pad_direction_radio.set_value(self.app.defaults["gerber_editor_circ_dir"])
+        self.pad_angle_entry.set_value(self.app.defaults["gerber_editor_circ_angle"])
 
     def build_ui(self, first_run=None):
 
@@ -3080,7 +3122,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             self.apcode_entry.set_value(max(self.tool2tooldia.values()) + 1)
         except ValueError:
             # this means that the edited object has no apertures so we start with 10 (Gerber specifications)
-            self.apcode_entry.set_value(10)
+            self.apcode_entry.set_value(self.app.defaults["gerber_editor_newcode"])
 
     def on_aperture_add(self, apid=None):
         self.is_modified = True
@@ -3471,6 +3513,16 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.app.ui.grb_draw_track.triggered.connect(self.on_track_add)
         self.app.ui.grb_draw_region.triggered.connect(self.on_region_add)
 
+        self.app.ui.grb_draw_poligonize.triggered.connect(self.on_poligonize)
+        self.app.ui.grb_draw_semidisc.triggered.connect(self.on_add_semidisc)
+        self.app.ui.grb_draw_disc.triggered.connect(self.on_disc_add)
+        self.app.ui.grb_draw_buffer.triggered.connect(lambda: self.select_tool("buffer"))
+        self.app.ui.grb_draw_scale.triggered.connect(lambda: self.select_tool("scale"))
+        self.app.ui.grb_draw_markarea.triggered.connect(lambda: self.select_tool("markarea"))
+        self.app.ui.grb_draw_eraser.triggered.connect(self.on_eraser)
+        self.app.ui.grb_draw_transformations.triggered.connect(self.on_transform)
+
+
     def disconnect_canvas_event_handlers(self):
 
         # we restore the key and mouse control to FlatCAMApp method
@@ -3527,6 +3579,39 @@ class FlatCAMGrbEditor(QtCore.QObject):
         except (TypeError, AttributeError):
             pass
 
+        try:
+            self.app.ui.grb_draw_poligonize.triggered.disconnect(self.on_poligonize)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_semidisc.triggered.diconnect(self.on_add_semidisc)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_disc.triggered.disconnect(self.on_disc_add)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_buffer.triggered.disconnect()
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_scale.triggered.disconnect()
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_markarea.triggered.disconnect()
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_eraser.triggered.disconnect(self.on_eraser)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_transformations.triggered.disconnect(self.on_transform)
+        except (TypeError, AttributeError):
+            pass
+
     def clear(self):
         self.active_tool = None
         self.selected = []
@@ -3984,9 +4069,9 @@ class FlatCAMGrbEditor(QtCore.QObject):
         :return: None
         """
 
-        self.pos = self.canvas.vispy_canvas.translate_coords(event.pos)
+        self.pos = self.canvas.translate_coords(event.pos)
 
-        if self.app.grid_status():
+        if self.app.grid_status() == True:
             self.pos = self.app.geo_editor.snap(self.pos[0], self.pos[1])
             self.app.app_cursor.enabled = True
             # Update cursor
@@ -4047,8 +4132,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
     def on_grb_click_release(self, event):
         self.modifiers = QtWidgets.QApplication.keyboardModifiers()
 
-        pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
-        if self.app.grid_status():
+        pos_canvas = self.canvas.translate_coords(event.pos)
+        if self.app.grid_status() == True:
             pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
         else:
             pos = (pos_canvas[0], pos_canvas[1])
@@ -4159,9 +4244,10 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # select the aperture code of the selected geometry, in the tool table
         self.apertures_table.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
         for aper in sel_aperture:
-            for row in range(self.apertures_table.rowCount()):
-                if str(aper) == self.apertures_table.item(row, 1).text():
-                    self.apertures_table.selectRow(row)
+            for row_to_sel in range(self.apertures_table.rowCount()):
+                if str(aper) == self.apertures_table.item(row_to_sel, 1).text():
+                    if row_to_sel not in set(index.row() for index in self.apertures_table.selectedIndexes()):
+                        self.apertures_table.selectRow(row_to_sel)
                     self.last_aperture_selected = aper
         self.apertures_table.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
 
@@ -4178,7 +4264,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         :return: None
         """
 
-        pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
+        pos_canvas = self.canvas.translate_coords(event.pos)
         event.xdata, event.ydata = pos_canvas[0], pos_canvas[1]
 
         self.x = event.xdata
@@ -4201,7 +4287,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             return
 
         # # ## Snap coordinates
-        if self.app.grid_status():
+        if self.app.grid_status() == True:
             x, y = self.app.geo_editor.snap(x, y)
             self.app.app_cursor.enabled = True
             # Update cursor
@@ -4325,11 +4411,11 @@ class FlatCAMGrbEditor(QtCore.QObject):
             geometry = self.active_tool.geometry
 
         try:
-            self.shapes.add(shape=geometry.geo, color=color, face_color=color, layer=0)
+            self.shapes.add(shape=geometry.geo, color=color, face_color=color, layer=0, tolerance=self.tolerance)
         except AttributeError:
             if type(geometry) == Point:
                 return
-            self.shapes.add(shape=geometry, color=color, face_color=color+'AF', layer=0)
+            self.shapes.add(shape=geometry, color=color, face_color=color+'AF', layer=0, tolerance=self.tolerance)
 
     def start_delayed_plot(self, check_period):
         """

File diff suppressed because it is too large
+ 445 - 59
flatcamGUI/FlatCAMGUI.py


+ 127 - 50
flatcamGUI/GUIElements.py

@@ -171,9 +171,11 @@ class LengthEntry(QtWidgets.QLineEdit):
             self.readyToEdit = False
 
     def focusOutEvent(self, e):
-        super(LengthEntry, self).focusOutEvent(e)  # required to remove cursor on focusOut
-        self.deselect()
-        self.readyToEdit = True
+        # don't focus out if the user requests an popup menu
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(LengthEntry, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.deselect()
+            self.readyToEdit = True
 
     def returnPressed(self, *args, **kwargs):
         val = self.get_value()
@@ -225,9 +227,11 @@ class FloatEntry(QtWidgets.QLineEdit):
             self.readyToEdit = False
 
     def focusOutEvent(self, e):
-        super(FloatEntry, self).focusOutEvent(e)  # required to remove cursor on focusOut
-        self.deselect()
-        self.readyToEdit = True
+        # don't focus out if the user requests an popup menu
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(FloatEntry, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.deselect()
+            self.readyToEdit = True
 
     def returnPressed(self, *args, **kwargs):
         val = self.get_value()
@@ -274,9 +278,11 @@ class FloatEntry2(QtWidgets.QLineEdit):
             self.readyToEdit = False
 
     def focusOutEvent(self, e):
-        super(FloatEntry2, self).focusOutEvent(e)  # required to remove cursor on focusOut
-        self.deselect()
-        self.readyToEdit = True
+        # don't focus out if the user requests an popup menu
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(FloatEntry2, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.deselect()
+            self.readyToEdit = True
 
     def get_value(self):
         raw = str(self.text()).strip(' ')
@@ -316,9 +322,11 @@ class IntEntry(QtWidgets.QLineEdit):
             self.readyToEdit = False
 
     def focusOutEvent(self, e):
-        super(IntEntry, self).focusOutEvent(e)  # required to remove cursor on focusOut
-        self.deselect()
-        self.readyToEdit = True
+        # don't focus out if the user requests an popup menu
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(IntEntry, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.deselect()
+            self.readyToEdit = True
 
     def get_value(self):
 
@@ -353,16 +361,17 @@ class FCEntry(QtWidgets.QLineEdit):
     def on_edit_finished(self):
         self.clearFocus()
 
-    def mousePressEvent(self, e, Parent=None):
+    def mousePressEvent(self, e, parent=None):
         super(FCEntry, self).mousePressEvent(e)  # required to deselect on 2e click
         if self.readyToEdit:
             self.selectAll()
             self.readyToEdit = False
 
     def focusOutEvent(self, e):
-        super(FCEntry, self).focusOutEvent(e)  # required to remove cursor on focusOut
-        self.deselect()
-        self.readyToEdit = True
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(FCEntry, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.deselect()
+            self.readyToEdit = True
 
     def get_value(self):
         return str(self.text())
@@ -381,36 +390,24 @@ class FCEntry(QtWidgets.QLineEdit):
 class FCEntry2(FCEntry):
     def __init__(self, parent=None):
         super(FCEntry2, self).__init__(parent)
-        self.readyToEdit = True
-        self.editingFinished.connect(self.on_edit_finished)
-
-    def on_edit_finished(self):
-        self.clearFocus()
 
     def set_value(self, val, decimals=4):
         try:
             fval = float(val)
         except ValueError:
             return
-
         self.setText('%.*f' % (decimals, fval))
 
 
 class FCEntry3(FCEntry):
     def __init__(self, parent=None):
         super(FCEntry3, self).__init__(parent)
-        self.readyToEdit = True
-        self.editingFinished.connect(self.on_edit_finished)
-
-    def on_edit_finished(self):
-        self.clearFocus()
 
     def set_value(self, val, decimals=4):
         try:
             fval = float(val)
         except ValueError:
             return
-
         self.setText('%.*f' % (decimals, fval))
 
     def get_value(self):
@@ -432,16 +429,17 @@ class EvalEntry(QtWidgets.QLineEdit):
     def on_edit_finished(self):
         self.clearFocus()
 
-    def mousePressEvent(self, e, Parent=None):
+    def mousePressEvent(self, e, parent=None):
         super(EvalEntry, self).mousePressEvent(e)  # required to deselect on 2e click
         if self.readyToEdit:
             self.selectAll()
             self.readyToEdit = False
 
     def focusOutEvent(self, e):
-        super(EvalEntry, self).focusOutEvent(e)  # required to remove cursor on focusOut
-        self.deselect()
-        self.readyToEdit = True
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(EvalEntry, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.deselect()
+            self.readyToEdit = True
 
     def returnPressed(self, *args, **kwargs):
         val = self.get_value()
@@ -478,16 +476,17 @@ class EvalEntry2(QtWidgets.QLineEdit):
     def on_edit_finished(self):
         self.clearFocus()
 
-    def mousePressEvent(self, e, Parent=None):
+    def mousePressEvent(self, e, parent=None):
         super(EvalEntry2, self).mousePressEvent(e)  # required to deselect on 2e click
         if self.readyToEdit:
             self.selectAll()
             self.readyToEdit = False
 
     def focusOutEvent(self, e):
-        super(EvalEntry2, self).focusOutEvent(e)  # required to remove cursor on focusOut
-        self.deselect()
-        self.readyToEdit = True
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(EvalEntry2, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.deselect()
+            self.readyToEdit = True
 
     def get_value(self):
         raw = str(self.text()).strip(' ')
@@ -847,9 +846,9 @@ class FCDetachableTab(QtWidgets.QTabWidget):
         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.set_detachable(val=True)
 
         self.setTabBar(self.tabBar)
 
@@ -872,6 +871,48 @@ class FCDetachableTab(QtWidgets.QTabWidget):
         self.setTabsClosable(True)
         self.tabCloseRequested.connect(self.closeTab)
 
+    def set_rmb_callback(self, callback):
+        """
+
+        :param callback: Function to call on right mouse click on tab
+        :type callback: func
+        :return: None
+        """
+
+        self.tabBar.right_click.connect(callback)
+
+    def set_detachable(self, val=True):
+        try:
+            self.tabBar.onDetachTabSignal.disconnect()
+        except TypeError:
+            pass
+
+        if val is True:
+            self.tabBar.onDetachTabSignal.connect(self.detachTab)
+            # the tab can be moved around
+            self.tabBar.can_be_dragged = True
+        else:
+            # the detached tab can't be moved
+            self.tabBar.can_be_dragged = False
+
+        return val
+
+    def setupContextMenu(self):
+        self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
+
+    def addContextMenu(self, entry, call_function, icon=None, initial_checked=False):
+        action_name = str(entry)
+        action = QtWidgets.QAction(self)
+        action.setCheckable(True)
+        action.setText(action_name)
+        if icon:
+            assert isinstance(icon, QtGui.QIcon), \
+                "Expected the argument to be QtGui.QIcon. Instead it is %s" % type(icon)
+            action.setIcon(icon)
+        action.setChecked(initial_checked)
+        self.addAction(action)
+        action.triggered.connect(call_function)
+
     def useOldIndex(self, param):
         if param:
             self.use_old_index = True
@@ -1209,6 +1250,8 @@ class FCDetachableTab(QtWidgets.QTabWidget):
         onMoveTabSignal = pyqtSignal(int, int)
         detachedTabDropSignal = pyqtSignal(str, int, QtCore.QPoint)
 
+        right_click = pyqtSignal(int)
+
         def __init__(self, parent=None):
             QtWidgets.QTabBar.__init__(self, parent)
 
@@ -1216,11 +1259,16 @@ class FCDetachableTab(QtWidgets.QTabWidget):
             self.setElideMode(QtCore.Qt.ElideRight)
             self.setSelectionBehaviorOnRemove(QtWidgets.QTabBar.SelectLeftTab)
 
+            self.prev_index = -1
+
             self.dragStartPos = QtCore.QPoint()
             self.dragDropedPos = QtCore.QPoint()
             self.mouseCursor = QtGui.QCursor()
             self.dragInitiated = False
 
+            # set this to False and the tab will no longer be displayed as detached
+            self.can_be_dragged = True
+
         def mouseDoubleClickEvent(self, event):
             """
             Send the onDetachTabSignal when a tab is double clicked
@@ -1234,21 +1282,37 @@ class FCDetachableTab(QtWidgets.QTabWidget):
 
         def mousePressEvent(self, event):
             """
-            Set the starting position for a drag event when the mouse button is pressed
+            Set the starting position for a drag event when the left mouse button is pressed.
+            Start detection of a right mouse click.
 
             :param event:   a mouse press event
             :return:
             """
             if event.button() == QtCore.Qt.LeftButton:
                 self.dragStartPos = event.pos()
+            elif event.button() == QtCore.Qt.RightButton:
+                self.prev_index = self.tabAt(event.pos())
 
             self.dragDropedPos.setX(0)
             self.dragDropedPos.setY(0)
-
             self.dragInitiated = False
 
             QtWidgets.QTabBar.mousePressEvent(self, event)
 
+        def mouseReleaseEvent(self, event):
+            """
+            Finish the detection of the right mouse click on the tab
+
+
+            :param event:   a mouse press event
+            :return:
+            """
+            if event.button() == QtCore.Qt.RightButton and self.prev_index == self.tabAt(event.pos()):
+                self.right_click.emit(self.prev_index)
+            self.prev_index = -1
+
+            QtWidgets.QTabBar.mouseReleaseEvent(self, event)
+
         def mouseMoveEvent(self, event):
             """
             Determine if the current movement is a drag.  If it is, convert it into a QDrag.  If the
@@ -1264,7 +1328,7 @@ class FCDetachableTab(QtWidgets.QTabWidget):
                 self.dragInitiated = True
 
             # If the current movement is a drag initiated by the left button
-            if (((event.buttons() & QtCore.Qt.LeftButton)) and self.dragInitiated):
+            if (((event.buttons() & QtCore.Qt.LeftButton)) and self.dragInitiated and self.can_be_dragged):
 
                 # Stop the move event
                 finishMoveEvent = QtGui.QMouseEvent(
@@ -1561,9 +1625,11 @@ class FCSpinner(QtWidgets.QSpinBox):
             self.readyToEdit = False
 
     def focusOutEvent(self, e):
-        super(FCSpinner, self).focusOutEvent(e)  # required to remove cursor on focusOut
-        self.lineEdit().deselect()
-        self.readyToEdit = True
+        # don't focus out if the user requests an popup menu
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(FCSpinner, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.lineEdit().deselect()
+            self.readyToEdit = True
 
     def get_value(self):
         return str(self.value())
@@ -1600,16 +1666,18 @@ class FCDoubleSpinner(QtWidgets.QDoubleSpinBox):
             self.readyToEdit = False
 
     def focusOutEvent(self, e):
-        super(FCDoubleSpinner, self).focusOutEvent(e)  # required to remove cursor on focusOut
-        self.lineEdit().deselect()
-        self.readyToEdit = True
+        # don't focus out if the user requests an popup menu
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(FCDoubleSpinner, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.lineEdit().deselect()
+            self.readyToEdit = True
 
     def get_value(self):
         return str(self.value())
 
     def set_value(self, val):
         try:
-            k = int(val)
+            k = float(val)
         except Exception as e:
             log.debug(str(e))
             return
@@ -1647,9 +1715,11 @@ class Dialog_box(QtWidgets.QWidget):
             self.readyToEdit = False
 
     def focusOutEvent(self, e):
-        super(Dialog_box, self).focusOutEvent(e)  # required to remove cursor on focusOut
-        self.lineEdit().deselect()
-        self.readyToEdit = True
+        # don't focus out if the user requests an popup menu
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(Dialog_box, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.lineEdit().deselect()
+            self.readyToEdit = True
 
 
 class _BrowserTextEdit(QTextEdit):
@@ -1811,9 +1881,16 @@ class MyCompleter(QCompleter):
         QCompleter.__init__(self)
         self.setCompletionMode(QCompleter.PopupCompletion)
         self.highlighted.connect(self.setHighlighted)
+        # self.popup().installEventFilter(self)
+
+    # def eventFilter(self, obj, event):
+    #     if event.type() == QtCore.QEvent.Wheel and obj is self.popup():
+    #         pass
+    #     return False
 
     def setHighlighted(self, text):
         self.lastSelected = text
 
     def getSelected(self):
         return self.lastSelected
+

+ 149 - 174
flatcamGUI/ObjectUI.py

@@ -76,7 +76,7 @@ class ObjectUI(QtWidgets.QWidget):
         # ###########################
 
         # ### Scale ####
-        self.scale_label = QtWidgets.QLabel(_('<b>Scale:</b>'))
+        self.scale_label = QtWidgets.QLabel('<b>%s:</b>' % _('Scale'))
         self.scale_label.setToolTip(
             _("Change the size of the object.")
         )
@@ -86,7 +86,7 @@ class ObjectUI(QtWidgets.QWidget):
         layout.addLayout(self.scale_grid)
 
         # Factor
-        faclabel = QtWidgets.QLabel(_('Factor:'))
+        faclabel = QtWidgets.QLabel('%s:' % _('Factor'))
         faclabel.setToolTip(
             _("Factor by which to multiply\n"
               "geometric features of this object.")
@@ -105,7 +105,7 @@ class ObjectUI(QtWidgets.QWidget):
         self.scale_grid.addWidget(self.scale_button, 0, 2)
 
         # ### Offset ####
-        self.offset_label = QtWidgets.QLabel(_('<b>Offset:</b>'))
+        self.offset_label = QtWidgets.QLabel('<b>%s:</b>' % _('Offset'))
         self.offset_label.setToolTip(
             _("Change the position of this object.")
         )
@@ -114,7 +114,7 @@ class ObjectUI(QtWidgets.QWidget):
         self.offset_grid = QtWidgets.QGridLayout()
         layout.addLayout(self.offset_grid)
 
-        self.offset_vectorlabel = QtWidgets.QLabel(_('Vector:'))
+        self.offset_vectorlabel = QtWidgets.QLabel('%s:' % _('Vector'))
         self.offset_vectorlabel.setToolTip(
             _("Amount by which to move the object\n"
               "in the x and y axes in (x, y) format.")
@@ -147,7 +147,7 @@ class GerberObjectUI(ObjectUI):
         grid0.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
         self.custom_box.addLayout(grid0)
 
-        self.plot_options_label = QtWidgets.QLabel(_("<b>Plot Options:</b>"))
+        self.plot_options_label = QtWidgets.QLabel("<b>%s:</b>" % _("Plot Options"))
         self.plot_options_label.setMinimumWidth(90)
 
         grid0.addWidget(self.plot_options_label, 0, 0)
@@ -179,7 +179,7 @@ class GerberObjectUI(ObjectUI):
         # ## Object name
         self.name_hlay = QtWidgets.QHBoxLayout()
         self.custom_box.addLayout(self.name_hlay)
-        name_label = QtWidgets.QLabel(_("<b>Name:</b>"))
+        name_label = QtWidgets.QLabel("<b>%s:</b>" % _("Name"))
         self.name_entry = FCEntry()
         self.name_entry.setFocusPolicy(QtCore.Qt.StrongFocus)
         self.name_hlay.addWidget(name_label)
@@ -189,7 +189,7 @@ class GerberObjectUI(ObjectUI):
         self.custom_box.addLayout(hlay_plot)
 
         # ### Gerber Apertures ####
-        self.apertures_table_label = QtWidgets.QLabel(_('<b>Apertures:</b>'))
+        self.apertures_table_label = QtWidgets.QLabel('<b>%s:</b>' % _('Apertures'))
         self.apertures_table_label.setToolTip(
             _("Apertures Table for the Gerber Object.")
         )
@@ -247,7 +247,7 @@ class GerberObjectUI(ObjectUI):
         self.apertures_table.setVisible(False)
 
         # Isolation Routing
-        self.isolation_routing_label = QtWidgets.QLabel(_("<b>Isolation Routing:</b>"))
+        self.isolation_routing_label = QtWidgets.QLabel("<b>%s:</b>" % _("Isolation Routing"))
         self.isolation_routing_label.setToolTip(
             _("Create a Geometry object with\n"
               "toolpaths to cut outside polygons.")
@@ -256,7 +256,7 @@ class GerberObjectUI(ObjectUI):
 
         grid1 = QtWidgets.QGridLayout()
         self.custom_box.addLayout(grid1)
-        tdlabel = QtWidgets.QLabel(_('Tool dia:'))
+        tdlabel = QtWidgets.QLabel('%s:' % _('Tool dia'))
         tdlabel.setToolTip(
             _("Diameter of the cutting tool.\n"
               "If you want to have an isolation path\n"
@@ -269,17 +269,18 @@ class GerberObjectUI(ObjectUI):
         self.iso_tool_dia_entry = LengthEntry()
         grid1.addWidget(self.iso_tool_dia_entry, 0, 1)
 
-        passlabel = QtWidgets.QLabel(_('Passes:'))
+        passlabel = QtWidgets.QLabel('%s:' % _('# Passes'))
         passlabel.setToolTip(
             _("Width of the isolation gap in\n"
               "number (integer) of tool widths.")
         )
         passlabel.setMinimumWidth(90)
         grid1.addWidget(passlabel, 1, 0)
-        self.iso_width_entry = IntEntry()
+        self.iso_width_entry = FCSpinner()
+        self.iso_width_entry.setRange(1, 999)
         grid1.addWidget(self.iso_width_entry, 1, 1)
 
-        overlabel = QtWidgets.QLabel(_('Pass overlap:'))
+        overlabel = QtWidgets.QLabel('%s:' % _('Pass overlap'))
         overlabel.setToolTip(
             _("How much (fraction) of the tool width to overlap each tool pass.\n"
               "Example:\n"
@@ -291,7 +292,7 @@ class GerberObjectUI(ObjectUI):
         grid1.addWidget(self.iso_overlap_entry, 2, 1)
 
         # Milling Type Radio Button
-        self.milling_type_label = QtWidgets.QLabel(_('Milling Type:'))
+        self.milling_type_label = QtWidgets.QLabel('%s:' % _('Milling Type'))
         self.milling_type_label.setToolTip(
             _("Milling type:\n"
               "- climb / best for precision milling and to reduce tool usage\n"
@@ -303,7 +304,7 @@ class GerberObjectUI(ObjectUI):
         grid1.addWidget(self.milling_type_radio, 3, 1)
 
         # combine all passes CB
-        self.combine_passes_cb = FCCheckBox(label=_('Combine'))
+        self.combine_passes_cb = FCCheckBox(label=_('Combine Passes'))
         self.combine_passes_cb.setToolTip(
             _("Combine all passes into one object")
         )
@@ -319,7 +320,7 @@ class GerberObjectUI(ObjectUI):
         )
         grid1.addWidget(self.follow_cb, 4, 1)
 
-        self.gen_iso_label = QtWidgets.QLabel(_("<b>Generate Isolation Geometry:</b>"))
+        self.gen_iso_label = QtWidgets.QLabel("<b>%s:</b>" % _("Generate Isolation Geometry"))
         self.gen_iso_label.setToolTip(
             _("Create a Geometry object with toolpaths to cut \n"
               "isolation outside, inside or on both sides of the\n"
@@ -378,7 +379,7 @@ class GerberObjectUI(ObjectUI):
         self.custom_box.addLayout(grid2)
 
         # ## Clear non-copper regions
-        self.clearcopper_label = QtWidgets.QLabel(_("<b>Clear N-copper:</b>"))
+        self.clearcopper_label = QtWidgets.QLabel("<b>%s:</b>" % _("Clear N-copper"))
         self.clearcopper_label.setToolTip(
             _("Create a Geometry object with\n"
               "toolpaths to cut all non-copper regions.")
@@ -394,7 +395,7 @@ class GerberObjectUI(ObjectUI):
         grid2.addWidget(self.generate_ncc_button, 0, 1)
 
         # ## Board cutout
-        self.board_cutout_label = QtWidgets.QLabel(_("<b>Board cutout:</b>"))
+        self.board_cutout_label = QtWidgets.QLabel("<b>%s:</b>" % _("Board cutout"))
         self.board_cutout_label.setToolTip(
             _("Create toolpaths to cut around\n"
               "the PCB and separate it from\n"
@@ -410,7 +411,7 @@ class GerberObjectUI(ObjectUI):
         grid2.addWidget(self.generate_cutout_button, 1, 1)
 
         # ## Non-copper regions
-        self.noncopper_label = QtWidgets.QLabel(_("<b>Non-copper regions:</b>"))
+        self.noncopper_label = QtWidgets.QLabel("<b>%s:</b>" % _("Non-copper regions"))
         self.noncopper_label.setToolTip(
             _("Create polygons covering the\n"
               "areas without copper on the PCB.\n"
@@ -424,7 +425,7 @@ class GerberObjectUI(ObjectUI):
         self.custom_box.addLayout(grid4)
 
         # Margin
-        bmlabel = QtWidgets.QLabel(_('Boundary Margin:'))
+        bmlabel = QtWidgets.QLabel('%s:' % _('Boundary Margin'))
         bmlabel.setToolTip(
             _("Specify the edge of the PCB\n"
               "by drawing a box around all\n"
@@ -448,7 +449,7 @@ class GerberObjectUI(ObjectUI):
         grid4.addWidget(self.generate_noncopper_button, 1, 1)
 
         # ## Bounding box
-        self.boundingbox_label = QtWidgets.QLabel(_('<b>Bounding Box:</b>'))
+        self.boundingbox_label = QtWidgets.QLabel('<b>%s:</b>' % _('Bounding Box'))
         self.boundingbox_label.setToolTip(
             _("Create a geometry surrounding the Gerber object.\n"
               "Square shape.")
@@ -458,7 +459,7 @@ class GerberObjectUI(ObjectUI):
         grid5 = QtWidgets.QGridLayout()
         self.custom_box.addLayout(grid5)
 
-        bbmargin = QtWidgets.QLabel(_('Boundary Margin:'))
+        bbmargin = QtWidgets.QLabel('%s:' % _('Boundary Margin'))
         bbmargin.setToolTip(
             _("Distance of the edges of the box\n"
               "to the nearest polygon.")
@@ -499,7 +500,7 @@ class ExcellonObjectUI(ObjectUI):
         hlay_plot = QtWidgets.QHBoxLayout()
         self.custom_box.addLayout(hlay_plot)
 
-        self.plot_options_label = QtWidgets.QLabel(_("<b>Plot Options:</b>"))
+        self.plot_options_label = QtWidgets.QLabel("<b>%s:</b>" % _("Plot Options"))
         self.solid_cb = FCCheckBox(label=_('Solid'))
         self.solid_cb.setToolTip(
             _("Solid circles.")
@@ -511,7 +512,7 @@ class ExcellonObjectUI(ObjectUI):
         # ## Object name
         self.name_hlay = QtWidgets.QHBoxLayout()
         self.custom_box.addLayout(self.name_hlay)
-        name_label = QtWidgets.QLabel(_("<b>Name:</b>"))
+        name_label = QtWidgets.QLabel("<b>%s:</b>" % _("Name"))
         self.name_entry = FCEntry()
         self.name_entry.setFocusPolicy(QtCore.Qt.StrongFocus)
         self.name_hlay.addWidget(name_label)
@@ -530,7 +531,7 @@ class ExcellonObjectUI(ObjectUI):
         self.tools_box.addLayout(hlay_plot)
 
         # ### Tools Drills ####
-        self.tools_table_label = QtWidgets.QLabel(_('<b>Tools Table</b>'))
+        self.tools_table_label = QtWidgets.QLabel('<b>%s:</b>' % _('Tools Table'))
         self.tools_table_label.setToolTip(
             _("Tools in this Excellon object\n"
               "when are used for drilling.")
@@ -538,7 +539,7 @@ class ExcellonObjectUI(ObjectUI):
         hlay_plot.addWidget(self.tools_table_label)
 
         # Plot CB
-        self.plot_cb = FCCheckBox(_('Plot Object'))
+        self.plot_cb = FCCheckBox(_('Plot'))
         self.plot_cb.setToolTip(
             _("Plot (show) this object.")
         )
@@ -578,7 +579,7 @@ class ExcellonObjectUI(ObjectUI):
         self.tools_box.addWidget(self.empty_label)
 
         # ### Create CNC Job ####
-        self.cncjob_label = QtWidgets.QLabel(_('<b>Create CNC Job</b>'))
+        self.cncjob_label = QtWidgets.QLabel('<b>%s</b>' % _('Create CNC Job'))
         self.cncjob_label.setToolTip(
             _("Create a CNC Job object\n"
               "for this drill object.")
@@ -589,7 +590,7 @@ class ExcellonObjectUI(ObjectUI):
         self.tools_box.addLayout(grid1)
 
         # Cut Z
-        cutzlabel = QtWidgets.QLabel(_('Cut Z:'))
+        cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
         cutzlabel.setToolTip(
             _("Drill depth (negative)\n"
               "below the copper surface.")
@@ -599,7 +600,7 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(self.cutz_entry, 0, 1)
 
         # Travel Z (z_move)
-        travelzlabel = QtWidgets.QLabel(_('Travel Z:'))
+        travelzlabel = QtWidgets.QLabel('%s:' % _('Travel Z'))
         travelzlabel.setToolTip(
             _("Tool height when travelling\n"
               "across the XY plane.")
@@ -609,7 +610,7 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(self.travelz_entry, 1, 1)
 
         # Tool change:
-        self.toolchange_cb = FCCheckBox(_("Tool change"))
+        self.toolchange_cb = FCCheckBox('%s:' % _("Tool change"))
         self.toolchange_cb.setToolTip(
             _("Include tool-change sequence\n"
               "in G-Code (Pause for tool change).")
@@ -617,7 +618,7 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(self.toolchange_cb, 2, 0)
 
         # Tool change Z:
-        toolchzlabel = QtWidgets.QLabel(_("Tool change Z:"))
+        toolchzlabel = QtWidgets.QLabel('%s:' % _("Tool change Z"))
         toolchzlabel.setToolTip(
             _("Z-axis position (height) for\n"
               "tool change.")
@@ -628,9 +629,9 @@ class ExcellonObjectUI(ObjectUI):
         self.ois_tcz_e = OptionalInputSection(self.toolchange_cb, [self.toolchangez_entry])
 
         # Start move Z:
-        self.estartz_label = QtWidgets.QLabel(_("Start move Z:"))
+        self.estartz_label = QtWidgets.QLabel('%s:' % _("Start move Z"))
         self.estartz_label.setToolTip(
-            _("Tool height just before starting the work.\n"
+            _("Height of the tool just after start.\n"
               "Delete the value if you don't need this feature.")
         )
         grid1.addWidget(self.estartz_label, 4, 0)
@@ -638,17 +639,17 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(self.estartz_entry, 4, 1)
 
         # End move Z:
-        self.eendz_label = QtWidgets.QLabel(_("End move Z:"))
+        self.eendz_label = QtWidgets.QLabel('%s:' % _("End move Z"))
         self.eendz_label.setToolTip(
-            _("Z-axis position (height) for\n"
-              "the last move.")
+            _("Height of the tool after\n"
+              "the last move at the end of the job.")
         )
         grid1.addWidget(self.eendz_label, 5, 0)
         self.eendz_entry = LengthEntry()
         grid1.addWidget(self.eendz_entry, 5, 1)
 
         # Excellon Feedrate
-        frlabel = QtWidgets.QLabel(_('Feedrate (Plunge):'))
+        frlabel = QtWidgets.QLabel('%s:' % _('Feedrate (Plunge):'))
         frlabel.setToolTip(
             _("Tool speed while drilling\n"
               "(in units per minute).\n"
@@ -659,14 +660,13 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(self.feedrate_entry, 6, 1)
 
         # Excellon Rapid Feedrate
-        self.feedrate_rapid_label = QtWidgets.QLabel(_('Feedrate Rapids:'))
+        self.feedrate_rapid_label = QtWidgets.QLabel('%s:' % _('Feedrate Rapids'))
         self.feedrate_rapid_label.setToolTip(
             _("Tool speed while drilling\n"
               "(in units per minute).\n"
               "This is for the rapid move G00.\n"
               "It is useful only for Marlin,\n"
-              "ignore for any other cases."
-              )
+              "ignore for any other cases.")
         )
         grid1.addWidget(self.feedrate_rapid_label, 7, 0)
         self.feedrate_rapid_entry = LengthEntry()
@@ -676,7 +676,7 @@ class ExcellonObjectUI(ObjectUI):
         self.feedrate_rapid_entry.hide()
 
         # Spindlespeed
-        spdlabel = QtWidgets.QLabel(_('Spindle speed:'))
+        spdlabel = QtWidgets.QLabel('%s:' % _('Spindle speed'))
         spdlabel.setToolTip(
             _("Speed of the spindle\n"
               "in RPM (optional)")
@@ -686,14 +686,14 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(self.spindlespeed_entry, 8, 1)
 
         # Dwell
-        self.dwell_cb = FCCheckBox(_('Dwell:'))
+        self.dwell_cb = FCCheckBox('%s:' % _('Dwell'))
         self.dwell_cb.setToolTip(
             _("Pause to allow the spindle to reach its\n"
               "speed before cutting.")
         )
         self.dwelltime_entry = FCEntry()
         self.dwelltime_entry.setToolTip(
-            _("Number of milliseconds for spindle to dwell.")
+            _("Number of time units for spindle to dwell.")
         )
         grid1.addWidget(self.dwell_cb, 9, 0)
         grid1.addWidget(self.dwelltime_entry, 9, 1)
@@ -701,10 +701,10 @@ class ExcellonObjectUI(ObjectUI):
         self.ois_dwell = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
 
         # postprocessor selection
-        pp_excellon_label = QtWidgets.QLabel(_("Postprocessor:"))
+        pp_excellon_label = QtWidgets.QLabel('%s:' % _("Postprocessor"))
         pp_excellon_label.setToolTip(
-            _("The json file that dictates\n"
-              "gcode output.")
+            _("The postprocessor JSON file that dictates\n"
+              "Gcode output.")
         )
         self.pp_excellon_name_cb = FCComboBox()
         self.pp_excellon_name_cb.setFocusPolicy(QtCore.Qt.StrongFocus)
@@ -712,7 +712,7 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(self.pp_excellon_name_cb, 10, 1)
 
         # Probe depth
-        self.pdepth_label = QtWidgets.QLabel(_("Probe Z depth:"))
+        self.pdepth_label = QtWidgets.QLabel('%s:' % _("Probe Z depth"))
         self.pdepth_label.setToolTip(
             _("The maximum depth that the probe is allowed\n"
               "to probe. Negative value, in current units.")
@@ -724,7 +724,7 @@ class ExcellonObjectUI(ObjectUI):
         self.pdepth_entry.setVisible(False)
 
         # Probe feedrate
-        self.feedrate_probe_label = QtWidgets.QLabel(_("Feedrate Probe:"))
+        self.feedrate_probe_label = QtWidgets.QLabel('%s:' % _("Feedrate Probe"))
         self.feedrate_probe_label.setToolTip(
             _("The feedrate used while the probe is probing.")
         )
@@ -742,7 +742,7 @@ class ExcellonObjectUI(ObjectUI):
 
         # ### Choose what to use for Gcode creation: Drills, Slots or Both
         gcode_box = QtWidgets.QFormLayout()
-        gcode_type_label = QtWidgets.QLabel(_('<b>Type:    </b>'))
+        gcode_type_label = QtWidgets.QLabel('<b>%s</b>' % _('Gcode'))
         gcode_type_label.setToolTip(
             _("Choose what to use for GCode generation:\n"
               "'Drills', 'Slots' or 'Both'.\n"
@@ -766,7 +766,7 @@ class ExcellonObjectUI(ObjectUI):
         self.tools_box.addWidget(self.generate_cnc_button)
 
         # ### Milling Holes Drills ####
-        self.mill_hole_label = QtWidgets.QLabel(_('<b>Mill Holes</b>'))
+        self.mill_hole_label = QtWidgets.QLabel('<b>%s</b>' % _('Mill Holes'))
         self.mill_hole_label.setToolTip(
             _("Create Geometry for milling holes.")
         )
@@ -780,7 +780,7 @@ class ExcellonObjectUI(ObjectUI):
 
         grid2 = QtWidgets.QGridLayout()
         self.tools_box.addLayout(grid2)
-        self.tdlabel = QtWidgets.QLabel(_('Drills Tool dia:'))
+        self.tdlabel = QtWidgets.QLabel('%s:' % _('Drill Tool dia'))
         self.tdlabel.setToolTip(
             _("Diameter of the cutting tool.")
         )
@@ -796,9 +796,10 @@ class ExcellonObjectUI(ObjectUI):
 
         grid3 = QtWidgets.QGridLayout()
         self.custom_box.addLayout(grid3)
-        self.stdlabel = QtWidgets.QLabel(_('Slots Tool dia:'))
+        self.stdlabel = QtWidgets.QLabel('%s:' % _('Slot Tool dia'))
         self.stdlabel.setToolTip(
-            _("Diameter of the cutting tool.")
+            _("Diameter of the cutting tool\n"
+              "when milling slots.")
         )
         grid3.addWidget(self.stdlabel, 0, 0)
         self.slot_tooldia_entry = LengthEntry()
@@ -827,13 +828,13 @@ class GeometryObjectUI(ObjectUI):
                                                icon_file='share/geometry32.png', parent=parent)
 
         # Plot options
-        self.plot_options_label = QtWidgets.QLabel(_("<b>Plot Options:</b>"))
+        self.plot_options_label = QtWidgets.QLabel("<b>%s:</b>" % _("Plot Options"))
         self.custom_box.addWidget(self.plot_options_label)
 
         # ## Object name
         self.name_hlay = QtWidgets.QHBoxLayout()
         self.custom_box.addLayout(self.name_hlay)
-        name_label = QtWidgets.QLabel(_("<b>Name:</b>"))
+        name_label = QtWidgets.QLabel("<b>%s:</b>" % _("Name"))
         self.name_entry = FCEntry()
         self.name_entry.setFocusPolicy(QtCore.Qt.StrongFocus)
         self.name_hlay.addWidget(name_label)
@@ -852,7 +853,7 @@ class GeometryObjectUI(ObjectUI):
         self.geo_tools_box.addLayout(hlay_plot)
 
         # ### Tools ####
-        self.tools_table_label = QtWidgets.QLabel(_('<b>Tools Table</b>'))
+        self.tools_table_label = QtWidgets.QLabel('<b>%s:</b>' % _('Tools Table'))
         self.tools_table_label.setToolTip(
             _("Tools in this Geometry object used for cutting.\n"
               "The 'Offset' entry will set an offset for the cut.\n"
@@ -944,7 +945,7 @@ class GeometryObjectUI(ObjectUI):
         self.grid1 = QtWidgets.QGridLayout()
         self.geo_tools_box.addLayout(self.grid1)
 
-        self.tool_offset_lbl = QtWidgets.QLabel(_('Tool Offset:'))
+        self.tool_offset_lbl = QtWidgets.QLabel('%s:' % _('Tool Offset'))
         self.tool_offset_lbl.setToolTip(
             _(
                 "The value to offset the cut when \n"
@@ -970,7 +971,7 @@ class GeometryObjectUI(ObjectUI):
         # self.addtool_label.setToolTip(
         #     "Add/Copy/Delete a tool to the tool list."
         # )
-        self.addtool_entry_lbl = QtWidgets.QLabel(_('<b>Tool Dia:</b>'))
+        self.addtool_entry_lbl = QtWidgets.QLabel('<b>%s:</b>' % _('Tool Dia'))
         self.addtool_entry_lbl.setToolTip(
             _(
                 "Diameter for the new tool"
@@ -1021,7 +1022,7 @@ class GeometryObjectUI(ObjectUI):
         # Create CNC Job ###
         # ##################
         # ### Tools Data ## ##
-        self.tool_data_label = QtWidgets.QLabel(_('<b>Tool Data</b>'))
+        self.tool_data_label = QtWidgets.QLabel('<b>%s</b>' % _('Tool Data'))
         self.tool_data_label.setToolTip(
             _(
                 "The data used for creating GCode.\n"
@@ -1042,7 +1043,7 @@ class GeometryObjectUI(ObjectUI):
         self.geo_param_box.addLayout(self.grid3)
 
         # Tip Dia
-        self.tipdialabel = QtWidgets.QLabel(_('V-Tip Dia:'))
+        self.tipdialabel = QtWidgets.QLabel('%s:' % _('V-Tip Dia'))
         self.tipdialabel.setToolTip(
             _(
                 "The tip diameter for V-Shape Tool"
@@ -1053,7 +1054,7 @@ class GeometryObjectUI(ObjectUI):
         self.grid3.addWidget(self.tipdia_entry, 1, 1)
 
         # Tip Angle
-        self.tipanglelabel = QtWidgets.QLabel(_('V-Tip Angle:'))
+        self.tipanglelabel = QtWidgets.QLabel('%s:' % _('V-Tip Angle'))
         self.tipanglelabel.setToolTip(
             _(
                 "The tip angle for V-Shape Tool.\n"
@@ -1065,7 +1066,7 @@ class GeometryObjectUI(ObjectUI):
         self.grid3.addWidget(self.tipangle_entry, 2, 1)
 
         # Cut Z
-        cutzlabel = QtWidgets.QLabel(_('Cut Z:'))
+        cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
         cutzlabel.setToolTip(
             _(
                 "Cutting depth (negative)\n"
@@ -1077,15 +1078,13 @@ class GeometryObjectUI(ObjectUI):
         self.grid3.addWidget(self.cutz_entry, 3, 1)
 
         # Multi-pass
-        self.mpass_cb = FCCheckBox(_("Multi-Depth:"))
+        self.mpass_cb = FCCheckBox('%s:' % _("Multi-Depth"))
         self.mpass_cb.setToolTip(
             _(
                 "Use multiple passes to limit\n"
                 "the cut depth in each pass. Will\n"
                 "cut multiple times until Cut Z is\n"
-                "reached.\n"
-                "To the right, input the depth of \n"
-                "each pass (positive value)."
+                "reached."
             )
         )
         self.grid3.addWidget(self.mpass_cb, 4, 0)
@@ -1101,12 +1100,10 @@ class GeometryObjectUI(ObjectUI):
         self.ois_mpass_geo = OptionalInputSection(self.mpass_cb, [self.maxdepth_entry])
 
         # Travel Z
-        travelzlabel = QtWidgets.QLabel(_('Travel Z:'))
+        travelzlabel = QtWidgets.QLabel('%s:' % _('Travel Z'))
         travelzlabel.setToolTip(
-            _(
-                "Height of the tool when\n"
-                "moving without cutting."
-            )
+            _("Height of the tool when\n"
+              "moving without cutting.")
         )
         self.grid3.addWidget(travelzlabel, 5, 0)
         self.travelz_entry = FloatEntry()
@@ -1114,14 +1111,14 @@ class GeometryObjectUI(ObjectUI):
 
         # Tool change:
 
-        self.toolchzlabel = QtWidgets.QLabel(_("Tool change Z:"))
+        self.toolchzlabel = QtWidgets.QLabel('%s:' %_("Tool change Z"))
         self.toolchzlabel.setToolTip(
             _(
                 "Z-axis position (height) for\n"
                 "tool change."
             )
         )
-        self.toolchangeg_cb = FCCheckBox(_("Tool change"))
+        self.toolchangeg_cb = FCCheckBox('%s:' % _("Tool change"))
         self.toolchangeg_cb.setToolTip(
             _(
                 "Include tool-change sequence\n"
@@ -1147,52 +1144,44 @@ class GeometryObjectUI(ObjectUI):
         # self.grid3.addWidget(self.gstartz_entry, 8, 1)
 
         # The Z value for the end move
-        self.endzlabel = QtWidgets.QLabel(_('End move Z:'))
+        self.endzlabel = QtWidgets.QLabel('%s:' % _('End move Z'))
         self.endzlabel.setToolTip(
-            _(
-                "This is the height (Z) at which the CNC\n"
-                "will go as the last move."
-            )
+            _("Height of the tool after\n"
+              "the last move at the end of the job.")
         )
         self.grid3.addWidget(self.endzlabel, 9, 0)
         self.gendz_entry = FloatEntry()
         self.grid3.addWidget(self.gendz_entry, 9, 1)
 
         # Feedrate X-Y
-        frlabel = QtWidgets.QLabel(_('Feed Rate X-Y:'))
+        frlabel = QtWidgets.QLabel('%s:' % _('Feed Rate X-Y'))
         frlabel.setToolTip(
-            _(
-                "Cutting speed in the XY\n"
-                "plane in units per minute"
-            )
+            _("Cutting speed in the XY\n"
+              "plane in units per minute")
         )
         self.grid3.addWidget(frlabel, 10, 0)
         self.cncfeedrate_entry = FloatEntry()
         self.grid3.addWidget(self.cncfeedrate_entry, 10, 1)
 
         # Feedrate Z (Plunge)
-        frzlabel = QtWidgets.QLabel(_('Feed Rate Z (Plunge):'))
+        frzlabel = QtWidgets.QLabel('%s:' % _('Feed Rate Z'))
         frzlabel.setToolTip(
-            _(
-                "Cutting speed in the Z\n"
-                "plane in units per minute"
-            )
+            _("Cutting speed in the XY\n"
+              "plane in units per minute.\n"
+              "It is called also Plunge.")
         )
         self.grid3.addWidget(frzlabel, 11, 0)
         self.cncplunge_entry = FloatEntry()
         self.grid3.addWidget(self.cncplunge_entry, 11, 1)
 
         # Feedrate rapids
-        self.fr_rapidlabel = QtWidgets.QLabel(_('Feed Rate Rapids:'))
+        self.fr_rapidlabel = QtWidgets.QLabel('%s:' % _('Feed Rate Rapids'))
         self.fr_rapidlabel.setToolTip(
-            _(
-              "Cutting speed in the XY\n"
-              "plane in units per minute\n"
+            _("Cutting speed in the XY plane\n"
               "(in units per minute).\n"
               "This is for the rapid move G00.\n"
               "It is useful only for Marlin,\n"
-              "ignore for any other cases."
-            )
+              "ignore for any other cases.")
         )
         self.grid3.addWidget(self.fr_rapidlabel, 12, 0)
         self.cncfeedrate_rapid_entry = FloatEntry()
@@ -1202,19 +1191,17 @@ class GeometryObjectUI(ObjectUI):
         self.cncfeedrate_rapid_entry.hide()
 
         # Cut over 1st point in path
-        self.extracut_cb = FCCheckBox(_('Cut over 1st pt'))
+        self.extracut_cb = FCCheckBox('%s' % _('Re-cut 1st pt.'))
         self.extracut_cb.setToolTip(
-            _(
-                "In order to remove possible\n"
-                "copper leftovers where first cut\n"
-                "meet with last cut, we generate an\n"
-                "extended cut over the first cut section."
-            )
+            _("In order to remove possible\n"
+              "copper leftovers where first cut\n"
+              "meet with last cut, we generate an\n"
+              "extended cut over the first cut section.")
         )
         self.grid3.addWidget(self.extracut_cb, 13, 0)
 
         # Spindlespeed
-        spdlabel = QtWidgets.QLabel(_('Spindle speed:'))
+        spdlabel = QtWidgets.QLabel('%s:' % _('Spindle speed'))
         spdlabel.setToolTip(
             _(
                 "Speed of the spindle in RPM (optional).\n"
@@ -1227,7 +1214,7 @@ class GeometryObjectUI(ObjectUI):
         self.grid3.addWidget(self.cncspindlespeed_entry, 14, 1)
 
         # Dwell
-        self.dwell_cb = FCCheckBox(_('Dwell:'))
+        self.dwell_cb = FCCheckBox('%s:' % _('Dwell'))
         self.dwell_cb.setToolTip(
             _(
                 "Pause to allow the spindle to reach its\n"
@@ -1236,9 +1223,7 @@ class GeometryObjectUI(ObjectUI):
         )
         self.dwelltime_entry = FloatEntry()
         self.dwelltime_entry.setToolTip(
-            _(
-                "Number of milliseconds for spindle to dwell."
-            )
+            _("Number of time units for spindle to dwell.")
         )
         self.grid3.addWidget(self.dwell_cb, 15, 0)
         self.grid3.addWidget(self.dwelltime_entry, 15, 1)
@@ -1246,12 +1231,10 @@ class GeometryObjectUI(ObjectUI):
         self.ois_dwell_geo = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
 
         # postprocessor selection
-        pp_label = QtWidgets.QLabel(_("PostProcessor:"))
+        pp_label = QtWidgets.QLabel('%s:' % _("PostProcessor"))
         pp_label.setToolTip(
-            _(
-                "The Postprocessor file that dictates\n"
-                "the Machine Code (like GCode, RML, HPGL) output."
-            )
+            _("The Postprocessor file that dictates\n"
+              "the Machine Code (like GCode, RML, HPGL) output.")
         )
         self.grid3.addWidget(pp_label, 16, 0)
         self.pp_geometry_name_cb = FCComboBox()
@@ -1259,12 +1242,10 @@ class GeometryObjectUI(ObjectUI):
         self.grid3.addWidget(self.pp_geometry_name_cb, 16, 1)
 
         # Probe depth
-        self.pdepth_label = QtWidgets.QLabel(_("Probe Z depth:"))
+        self.pdepth_label = QtWidgets.QLabel('%s:' % _("Probe Z depth"))
         self.pdepth_label.setToolTip(
-            _(
-                "The maximum depth that the probe is allowed\n"
-                "to probe. Negative value, in current units."
-            )
+            _("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()
@@ -1273,11 +1254,9 @@ class GeometryObjectUI(ObjectUI):
         self.pdepth_entry.setVisible(False)
 
         # Probe feedrate
-        self.feedrate_probe_label = QtWidgets.QLabel(_("Feedrate Probe:"))
+        self.feedrate_probe_label = QtWidgets.QLabel('%s:' % _("Feedrate Probe"))
         self.feedrate_probe_label.setToolTip(
-            _(
-                "The feedrate used while the probe is probing."
-            )
+            _("The feedrate used while the probe is probing.")
         )
         self.grid3.addWidget(self.feedrate_probe_label, 18, 0)
         self.feedrate_probe_entry = FCEntry()
@@ -1296,16 +1275,14 @@ class GeometryObjectUI(ObjectUI):
         # Button
         self.generate_cnc_button = QtWidgets.QPushButton(_('Generate'))
         self.generate_cnc_button.setToolTip(
-            _(
-                "Generate the CNC Job object."
-            )
+            _("Generate the CNC Job object.")
         )
         self.geo_param_box.addWidget(self.generate_cnc_button)
 
         # ##############
         # Paint area ##
         # ##############
-        self.paint_label = QtWidgets.QLabel(_('<b>Paint Area:</b>'))
+        self.paint_label = QtWidgets.QLabel('<b>%s</b>' % _('Paint Area'))
         self.paint_label.setToolTip(
             _(
                 "Creates tool paths to cover the\n"
@@ -1319,9 +1296,7 @@ class GeometryObjectUI(ObjectUI):
         # GO Button
         self.paint_tool_button = QtWidgets.QPushButton(_('Paint Tool'))
         self.paint_tool_button.setToolTip(
-            _(
-                "Launch Paint Tool in Tools Tab."
-            )
+            _("Launch Paint Tool in Tools Tab.")
         )
         self.geo_tools_box.addWidget(self.paint_tool_button)
 
@@ -1352,10 +1327,10 @@ class CNCObjectUI(ObjectUI):
         self.offset_button.hide()
 
         # ## Plot options
-        self.plot_options_label = QtWidgets.QLabel(_("<b>Plot Options:</b>"))
+        self.plot_options_label = QtWidgets.QLabel("<b>%s:</b>" % _("Plot Options"))
         self.custom_box.addWidget(self.plot_options_label)
 
-        self.cncplot_method_label = QtWidgets.QLabel(_("<b>Plot kind:</b>"))
+        self.cncplot_method_label = QtWidgets.QLabel("<b>%s:</b>" % _("Plot kind"))
         self.cncplot_method_label.setToolTip(
             _(
                 "This selects the kind of geometries on the canvas to plot.\n"
@@ -1371,12 +1346,11 @@ class CNCObjectUI(ObjectUI):
             {"label": _("Cut"), "value": "cut"}
         ], stretch=False)
 
-        self.annotation_label = QtWidgets.QLabel(_("<b>Display Annotation:</b>"))
+        self.annotation_label = QtWidgets.QLabel("<b>%s:</b>" % _("Display Annotation"))
         self.annotation_label.setToolTip(
-            _(
-                "This selects if to display text annotation on the plot.\n"
-                "When checked it will display numbers in order for each end\n"
-                "of a travel line."
+            _("This selects if to display text annotation on the plot.\n"
+              "When checked it will display numbers in order for each end\n"
+              "of a travel line."
             )
         )
         self.annotation_cb = FCCheckBox()
@@ -1384,28 +1358,36 @@ class CNCObjectUI(ObjectUI):
         # ## Object name
         self.name_hlay = QtWidgets.QHBoxLayout()
         self.custom_box.addLayout(self.name_hlay)
-        name_label = QtWidgets.QLabel(_("<b>Name:</b>"))
+        name_label = QtWidgets.QLabel("<b>%s:</b>" % _("Name"))
         self.name_entry = FCEntry()
         self.name_entry.setFocusPolicy(QtCore.Qt.StrongFocus)
         self.name_hlay.addWidget(name_label)
         self.name_hlay.addWidget(self.name_entry)
 
-        self.t_distance_label = QtWidgets.QLabel(_("<b>Travelled dist.:</b>"))
+        self.t_distance_label = QtWidgets.QLabel("<b>%s:</b>" % _("Travelled dist."))
         self.t_distance_label.setToolTip(
-            _(
-                "This is the total travelled distance on X-Y plane.\n"
-                "In current units."
-            )
+            _("This is the total travelled distance on X-Y plane.\n"
+              "In current units.")
         )
         self.t_distance_entry = FCEntry()
         self.t_distance_entry.setToolTip(
-            _(
-                "This is the total travelled distance on X-Y plane.\n"
-                "In current units."
-            )
+            _("This is the total travelled distance on X-Y plane.\n"
+              "In current units.")
         )
         self.units_label = QtWidgets.QLabel()
 
+        self.t_time_label = QtWidgets.QLabel("<b>%s:</b>" % _("Estimated time"))
+        self.t_time_label.setToolTip(
+            _("This is the estimated time to do the routing/drilling,\n"
+              "without the time spent in ToolChange events.")
+        )
+        self.t_time_entry = FCEntry()
+        self.t_time_entry.setToolTip(
+            _("This is the estimated time to do the routing/drilling,\n"
+              "without the time spent in ToolChange events.")
+        )
+        self.units_time_label = QtWidgets.QLabel()
+
         f_lay = QtWidgets.QGridLayout()
         f_lay.setColumnStretch(1, 1)
         f_lay.setColumnStretch(2, 1)
@@ -1420,9 +1402,14 @@ class CNCObjectUI(ObjectUI):
         f_lay.addWidget(self.t_distance_label, 2, 0)
         f_lay.addWidget(self.t_distance_entry, 2, 1)
         f_lay.addWidget(self.units_label, 2, 2)
+        f_lay.addWidget(self.t_time_label, 3, 0)
+        f_lay.addWidget(self.t_time_entry, 3, 1)
+        f_lay.addWidget(self.units_time_label, 3, 2)
 
         self.t_distance_label.hide()
         self.t_distance_entry.setVisible(False)
+        self.t_time_label.hide()
+        self.t_time_entry.setVisible(False)
 
         e1_lbl = QtWidgets.QLabel('')
         self.custom_box.addWidget(e1_lbl)
@@ -1431,7 +1418,7 @@ class CNCObjectUI(ObjectUI):
         self.custom_box.addLayout(hlay)
 
         # CNC Tools Table for plot
-        self.cnc_tools_table_label = QtWidgets.QLabel(_('<b>CNC Tools Table</b>'))
+        self.cnc_tools_table_label = QtWidgets.QLabel('<b>%s</b>' % _('CNC Tools Table'))
         self.cnc_tools_table_label.setToolTip(
             _(
                 "Tools in this CNCJob object used for cutting.\n"
@@ -1451,9 +1438,7 @@ class CNCObjectUI(ObjectUI):
         # self.plot_cb = QtWidgets.QCheckBox('Plot')
         self.plot_cb = FCCheckBox(_('Plot Object'))
         self.plot_cb.setToolTip(
-            _(
-                "Plot (show) this object."
-            )
+            _("Plot (show) this object.")
         )
         self.plot_cb.setLayoutDirection(QtCore.Qt.RightToLeft)
         hlay.addStretch()
@@ -1483,20 +1468,18 @@ class CNCObjectUI(ObjectUI):
         # ####################
         # ## Export G-Code ##
         # ####################
-        self.export_gcode_label = QtWidgets.QLabel(_("<b>Export CNC Code:</b>"))
+        self.export_gcode_label = QtWidgets.QLabel("<b>%s:</b>" % _("Export CNC Code"))
         self.export_gcode_label.setToolTip(
             _("Export and save G-Code to\n"
-            "make this object to a file.")
+              "make this object to a file.")
         )
         self.custom_box.addWidget(self.export_gcode_label)
 
         # Prepend text to GCode
-        prependlabel = QtWidgets.QLabel(_('Prepend to CNC Code:'))
+        prependlabel = QtWidgets.QLabel('%s:' % _('Prepend to CNC Code'))
         prependlabel.setToolTip(
-            _(
-                "Type here any G-Code commands you would\n"
-                "like to add to the beginning of the generated file."
-            )
+            _("Type here any G-Code commands you would\n"
+              "like to add at the beginning of the G-Code file.")
         )
         self.custom_box.addWidget(prependlabel)
 
@@ -1504,13 +1487,11 @@ class CNCObjectUI(ObjectUI):
         self.custom_box.addWidget(self.prepend_text)
 
         # Append text to GCode
-        appendlabel = QtWidgets.QLabel(_('Append to CNC Code:'))
+        appendlabel = QtWidgets.QLabel('%s:' % _('Append to CNC Code'))
         appendlabel.setToolTip(
-            _(
-                "Type here any G-Code commands you would\n"
-                "like to append to the generated file.\n"
-                "I.e.: M2 (End of program)"
-            )
+            _("Type here any G-Code commands you would\n"
+              "like to append to the generated file.\n"
+              "I.e.: M2 (End of program)")
         )
         self.custom_box.addWidget(appendlabel)
 
@@ -1525,7 +1506,7 @@ class CNCObjectUI(ObjectUI):
         self.cnc_frame.setLayout(self.cnc_box)
 
         # Toolchange Custom G-Code
-        self.toolchangelabel = QtWidgets.QLabel(_('Toolchange G-Code:'))
+        self.toolchangelabel = QtWidgets.QLabel('%s:' % _('Toolchange G-Code'))
         self.toolchangelabel.setToolTip(
             _(
                 "Type here any G-Code commands you would\n"
@@ -1547,12 +1528,10 @@ class CNCObjectUI(ObjectUI):
         self.cnc_box.addLayout(cnclay)
 
         # Toolchange Replacement Enable
-        self.toolchange_cb = FCCheckBox(label=_('Use Toolchange Macro'))
+        self.toolchange_cb = FCCheckBox(label='%s' % _('Use Toolchange Macro'))
         self.toolchange_cb.setToolTip(
-            _(
-                "Check this box if you want to use\n"
-                "a Custom Toolchange GCode (macro)."
-            )
+            _("Check this box if you want to use\n"
+              "a Custom Toolchange GCode (macro).")
         )
 
         # Variable list
@@ -1598,19 +1577,15 @@ class CNCObjectUI(ObjectUI):
         # Edit GCode Button
         self.modify_gcode_button = QtWidgets.QPushButton(_('View CNC Code'))
         self.modify_gcode_button.setToolTip(
-            _(
-                "Opens TAB to view/modify/print G-Code\n"
-                "file."
-            )
+            _("Opens TAB to view/modify/print G-Code\n"
+              "file.")
         )
 
         # GO Button
         self.export_gcode_button = QtWidgets.QPushButton(_('Save CNC Code'))
         self.export_gcode_button.setToolTip(
-            _(
-                "Opens dialog to save G-Code\n"
-                "file."
-            )
+            _("Opens dialog to save G-Code\n"
+              "file.")
         )
 
         h_lay.addWidget(self.modify_gcode_button)

+ 51 - 41
flatcamGUI/PlotCanvas.py

@@ -18,12 +18,12 @@ from vispy.geometry import Rect
 log = logging.getLogger('base')
 
 
-class PlotCanvas(QtCore.QObject):
+class PlotCanvas(QtCore.QObject, VisPyCanvas):
     """
     Class handling the plotting area in the application.
     """
 
-    def __init__(self, container, app):
+    def __init__(self, container, fcapp):
         """
         The constructor configures the VisPy figure that
         will contain all plots, creates the base axes and connects
@@ -34,8 +34,12 @@ class PlotCanvas(QtCore.QObject):
         """
 
         super(PlotCanvas, self).__init__()
+        VisPyCanvas.__init__(self)
 
-        self.app = app
+        # VisPyCanvas does not allow new attributes. Override.
+        self.unfreeze()
+
+        self.fcapp = fcapp
 
         # Parent container
         self.container = container
@@ -44,19 +48,19 @@ class PlotCanvas(QtCore.QObject):
         # which might decrease performance
         self.b_line, self.r_line, self.t_line, self.l_line = None, None, None, None
 
-        # Attach to parent
-        self.vispy_canvas = VisPyCanvas()
+        # <VisPyCanvas>
+        self.create_native()
+        self.native.setParent(self.fcapp.ui)
 
-        self.vispy_canvas.create_native()
-        self.vispy_canvas.native.setParent(self.app.ui)
-        self.container.addWidget(self.vispy_canvas.native)
+        # <QtCore.QObject>
+        self.container.addWidget(self.native)
 
         # ## AXIS # ##
         self.v_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 1.0), vertical=True,
-                                   parent=self.vispy_canvas.view.scene)
+                                   parent=self.view.scene)
 
         self.h_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 1.0), vertical=False,
-                                   parent=self.vispy_canvas.view.scene)
+                                   parent=self.view.scene)
 
         # draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
         # all CNC have a limited workspace
@@ -70,12 +74,15 @@ class PlotCanvas(QtCore.QObject):
         self.shape_collections = []
 
         self.shape_collection = self.new_shape_collection()
-        self.app.pool_recreated.connect(self.on_pool_recreated)
+        self.fcapp.pool_recreated.connect(self.on_pool_recreated)
         self.text_collection = self.new_text_collection()
 
         # TODO: Should be setting to show/hide CNC job annotations (global or per object)
         self.text_collection.enabled = True
 
+        # Keep VisPy canvas happy by letting it be "frozen" again.
+        self.freeze()
+
     # draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
     # all CNC have a limited workspace
     def draw_workspace(self):
@@ -91,38 +98,38 @@ class PlotCanvas(QtCore.QObject):
         a3p_mm = np.array([(0, 0), (297, 0), (297, 420), (0, 420)])
         a3l_mm = np.array([(0, 0), (420, 0), (420, 297), (0, 297)])
 
-        if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
-            if self.app.defaults['global_workspaceT'] == 'A4P':
+        if self.fcapp.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
+            if self.fcapp.defaults['global_workspaceT'] == 'A4P':
                 a = a4p_mm
-            elif self.app.defaults['global_workspaceT'] == 'A4L':
+            elif self.fcapp.defaults['global_workspaceT'] == 'A4L':
                 a = a4l_mm
-            elif self.app.defaults['global_workspaceT'] == 'A3P':
+            elif self.fcapp.defaults['global_workspaceT'] == 'A3P':
                 a = a3p_mm
-            elif self.app.defaults['global_workspaceT'] == 'A3L':
+            elif self.fcapp.defaults['global_workspaceT'] == 'A3L':
                 a = a3l_mm
         else:
-            if self.app.defaults['global_workspaceT'] == 'A4P':
+            if self.fcapp.defaults['global_workspaceT'] == 'A4P':
                 a = a4p_in
-            elif self.app.defaults['global_workspaceT'] == 'A4L':
+            elif self.fcapp.defaults['global_workspaceT'] == 'A4L':
                 a = a4l_in
-            elif self.app.defaults['global_workspaceT'] == 'A3P':
+            elif self.fcapp.defaults['global_workspaceT'] == 'A3P':
                 a = a3p_in
-            elif self.app.defaults['global_workspaceT'] == 'A3L':
+            elif self.fcapp.defaults['global_workspaceT'] == 'A3L':
                 a = a3l_in
 
         self.delete_workspace()
 
         self.b_line = Line(pos=a[0:2], color=(0.70, 0.3, 0.3, 1.0),
-                           antialias= True, method='agg', parent=self.vispy_canvas.view.scene)
+                           antialias= True, method='agg', parent=self.view.scene)
         self.r_line = Line(pos=a[1:3], color=(0.70, 0.3, 0.3, 1.0),
-                           antialias= True, method='agg', parent=self.vispy_canvas.view.scene)
+                           antialias= True, method='agg', parent=self.view.scene)
 
         self.t_line = Line(pos=a[2:4], color=(0.70, 0.3, 0.3, 1.0),
-                           antialias= True, method='agg', parent=self.vispy_canvas.view.scene)
+                           antialias= True, method='agg', parent=self.view.scene)
         self.l_line = Line(pos=np.array((a[0], a[3])), color=(0.70, 0.3, 0.3, 1.0),
-                           antialias= True, method='agg', parent=self.vispy_canvas.view.scene)
+                           antialias= True, method='agg', parent=self.view.scene)
 
-        if self.app.defaults['global_workspace'] is False:
+        if self.fcapp.defaults['global_workspace'] is False:
             self.delete_workspace()
 
     # delete the workspace lines from the plot by removing the parent
@@ -138,21 +145,21 @@ class PlotCanvas(QtCore.QObject):
     # redraw the workspace lines on the plot by readding them to the parent view.scene
     def restore_workspace(self):
         try:
-            self.b_line.parent = self.vispy_canvas.view.scene
-            self.r_line.parent = self.vispy_canvas.view.scene
-            self.t_line.parent = self.vispy_canvas.view.scene
-            self.l_line.parent = self.vispy_canvas.view.scene
+            self.b_line.parent = self.view.scene
+            self.r_line.parent = self.view.scene
+            self.t_line.parent = self.view.scene
+            self.l_line.parent = self.view.scene
         except Exception as e:
             pass
 
     def vis_connect(self, event_name, callback):
-        return getattr(self.vispy_canvas.events, event_name).connect(callback)
+        return getattr(self.events, event_name).connect(callback)
 
     def vis_disconnect(self, event_name, callback=None):
         if callback is None:
-            getattr(self.vispy_canvas.events, event_name).disconnect()
+            getattr(self.events, event_name).disconnect()
         else:
-            getattr(self.vispy_canvas.events, event_name).disconnect(callback)
+            getattr(self.events, event_name).disconnect(callback)
 
     def zoom(self, factor, center=None):
         """
@@ -165,7 +172,7 @@ class PlotCanvas(QtCore.QObject):
         :type center: list
         :return: None
         """
-        self.vispy_canvas.view.camera.zoom(factor, center)
+        self.view.camera.zoom(factor, center)
 
     def new_shape_group(self, shape_collection=None):
         if shape_collection:
@@ -173,21 +180,24 @@ class PlotCanvas(QtCore.QObject):
         return ShapeGroup(self.shape_collection)
 
     def new_shape_collection(self, **kwargs):
-        # sc = ShapeCollection(parent=self.vispy_canvas.view.scene, pool=self.app.pool, **kwargs)
+        # sc = ShapeCollection(parent=self.view.scene, pool=self.app.pool, **kwargs)
         # self.shape_collections.append(sc)
         # return sc
-        return ShapeCollection(parent=self.vispy_canvas.view.scene, pool=self.app.pool, **kwargs)
+        return ShapeCollection(parent=self.view.scene, pool=self.fcapp.pool, **kwargs)
 
     def new_cursor(self):
-        c = Cursor(pos=np.empty((0, 2)), parent=self.vispy_canvas.view.scene)
+        c = Cursor(pos=np.empty((0, 2)), parent=self.view.scene)
         c.antialias = 0
         return c
 
-    def new_text_group(self):
-        return TextGroup(self.text_collection)
+    def new_text_group(self, collection=None):
+        if collection:
+            return TextGroup(collection)
+        else:
+            return TextGroup(self.text_collection)
 
     def new_text_collection(self, **kwargs):
-        return TextCollection(parent=self.vispy_canvas.view.scene, **kwargs)
+        return TextCollection(parent=self.view.scene, **kwargs)
 
     def fit_view(self, rect=None):
 
@@ -209,7 +219,7 @@ class PlotCanvas(QtCore.QObject):
         rect.right *= 1.01
         rect.top *= 1.01
 
-        self.vispy_canvas.view.camera.rect = rect
+        self.view.camera.rect = rect
 
         self.shape_collection.unlock_updates()
 
@@ -224,7 +234,7 @@ class PlotCanvas(QtCore.QObject):
             except TypeError:
                 pass
 
-        self.vispy_canvas.view.camera.rect = rect
+        self.view.camera.rect = rect
 
         self.shape_collection.unlock_updates()
 

+ 13 - 5
flatcamGUI/VisPyCanvas.py

@@ -8,12 +8,13 @@
 
 import numpy as np
 from PyQt5.QtGui import QPalette
+from PyQt5.QtCore import QSettings
 import vispy.scene as scene
 from vispy.scene.cameras.base_camera import BaseCamera
 from vispy.color import Color
 import time
 
-white = Color("#ffffff" )
+white = Color("#ffffff")
 black = Color("#000000")
 
 
@@ -35,12 +36,19 @@ class VisPyCanvas(scene.SceneCanvas):
         top_padding = self.grid_widget.add_widget(row=0, col=0, col_span=2)
         top_padding.height_max = 0
 
-        self.yaxis = scene.AxisWidget(orientation='left', axis_color='black', text_color='black', font_size=8)
+        settings = QSettings("Open Source", "FlatCAM")
+        if settings.contains("axis_font_size"):
+            a_fsize = settings.value('axis_font_size', type=int)
+        else:
+            a_fsize = 8
+
+        self.yaxis = scene.AxisWidget(orientation='left', axis_color='black', text_color='black', font_size=a_fsize)
         self.yaxis.width_max = 55
         self.grid_widget.add_widget(self.yaxis, row=1, col=0)
 
-        self.xaxis = scene.AxisWidget(orientation='bottom', axis_color='black', text_color='black', font_size=8)
-        self.xaxis.height_max = 25
+        self.xaxis = scene.AxisWidget(orientation='bottom', axis_color='black', text_color='black', font_size=a_fsize,
+                                      anchors=['center', 'bottom'])
+        self.xaxis.height_max = 30
         self.grid_widget.add_widget(self.xaxis, row=2, col=1)
 
         right_padding = self.grid_widget.add_widget(row=0, col=2, row_span=2)
@@ -48,7 +56,7 @@ class VisPyCanvas(scene.SceneCanvas):
         right_padding.width_max = 0
 
         view = self.grid_widget.add_view(row=1, col=1, border_color='black', bgcolor='white')
-        view.camera = Camera(aspect=1, rect=(-25,-25,150,150))
+        view.camera = Camera(aspect=1, rect=(-25, -25, 150, 150))
 
         # Following function was removed from 'prepare_draw()' of 'Grid' class by patch,
         # it is necessary to call manually

BIN
flatcamGUI/VisPyData/data/fonts/opensans-regular.ttf


BIN
flatcamGUI/VisPyData/data/freetype/freetype253.dll


BIN
flatcamGUI/VisPyData/data/freetype/freetype253_x64.dll


+ 2 - 2
flatcamParsers/ParseDXF.py

@@ -1,10 +1,10 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 from shapely.geometry import LineString
 import logging

+ 14 - 2
flatcamParsers/ParseSVG.py

@@ -284,7 +284,7 @@ def svgpolygon2shapely(polygon):
     # return LinearRing(points)
 
 
-def getsvggeo(node, object_type):
+def getsvggeo(node, object_type, root = None):
     """
     Extracts and flattens all geometry from an SVG node
     into a list of Shapely geometry.
@@ -293,13 +293,16 @@ def getsvggeo(node, object_type):
     :return: List of Shapely geometry
     :rtype: list
     """
+    if root is None:
+        root = node
+
     kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
     geo = []
 
     # Recurse
     if len(node) > 0:
         for child in node:
-            subgeo = getsvggeo(child, object_type)
+            subgeo = getsvggeo(child, object_type, root)
             if subgeo is not None:
                 geo += subgeo
 
@@ -341,6 +344,15 @@ def getsvggeo(node, object_type):
         pline = svgpolyline2shapely(node)
         geo = [pline]
 
+    elif kind == 'use':
+        log.debug('***USE***')
+        # href= is the preferred name for this[1], but inkscape still generates xlink:href=.
+        # [1] https://developer.mozilla.org/en-US/docs/Web/SVG/Element/use#Attributes
+        href = node.attrib['href'] if 'href' in node.attrib else node.attrib['{http://www.w3.org/1999/xlink}href']
+        ref = root.find(".//*[@id='%s']" % href.replace('#', ''))
+        if ref is not None:
+            geo = getsvggeo(ref, object_type, root)
+
     else:
         log.warning("Unknown kind: " + kind)
         geo = None

+ 20 - 14
flatcamTools/ToolCalculators.py

@@ -89,28 +89,29 @@ class ToolCalculator(FlatCAMTool):
         form_layout = QtWidgets.QFormLayout()
         self.layout.addLayout(form_layout)
 
-        self.tipDia_label = QtWidgets.QLabel(_("Tip Diameter:"))
+        self.tipDia_label = QtWidgets.QLabel('%s:' % _("Tip Diameter"))
         self.tipDia_entry = FCEntry()
         # self.tipDia_entry.setFixedWidth(70)
         self.tipDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.tipDia_label.setToolTip(_('This is the diameter of the tool tip.\n'
-                                       'The manufacturer specifies it.'))
-
-        self.tipAngle_label = QtWidgets.QLabel(_("Tip Angle:"))
+        self.tipDia_label.setToolTip(
+            _("This is the tool tip diameter.\n"
+              "It is specified by manufacturer.")
+        )
+        self.tipAngle_label = QtWidgets.QLabel('%s:' % _("Tip Angle"))
         self.tipAngle_entry = FCEntry()
         # self.tipAngle_entry.setFixedWidth(70)
         self.tipAngle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.tipAngle_label.setToolTip(_("This is the angle of the tip of the tool.\n"
                                          "It is specified by manufacturer."))
 
-        self.cutDepth_label = QtWidgets.QLabel(_("Cut Z:"))
+        self.cutDepth_label = QtWidgets.QLabel('%s:' % _("Cut Z"))
         self.cutDepth_entry = FCEntry()
         # self.cutDepth_entry.setFixedWidth(70)
         self.cutDepth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.cutDepth_label.setToolTip(_("This is the depth to cut into the material.\n"
                                          "In the CNCJob is the CutZ parameter."))
 
-        self.effectiveToolDia_label = QtWidgets.QLabel(_("Tool Diameter:"))
+        self.effectiveToolDia_label = QtWidgets.QLabel('%s:' % _("Tool Diameter"))
         self.effectiveToolDia_entry = FCEntry()
         # self.effectiveToolDia_entry.setFixedWidth(70)
         self.effectiveToolDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
@@ -154,26 +155,26 @@ class ToolCalculator(FlatCAMTool):
         plate_form_layout = QtWidgets.QFormLayout()
         self.layout.addLayout(plate_form_layout)
 
-        self.pcblengthlabel = QtWidgets.QLabel(_("Board Length:"))
+        self.pcblengthlabel = QtWidgets.QLabel('%s:' % _("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.pcbwidthlabel = QtWidgets.QLabel('%s:' % _("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_label = QtWidgets.QLabel('%s:' % _("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_label = QtWidgets.QLabel('%s:' % _("Copper Growth"))
         self.growth_entry = FCEntry()
         # self.growth_entry.setFixedWidth(70)
         self.growth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
@@ -182,7 +183,7 @@ class ToolCalculator(FlatCAMTool):
 
         # self.growth_entry.setEnabled(False)
 
-        self.cvaluelabel = QtWidgets.QLabel(_("Current Value:"))
+        self.cvaluelabel = QtWidgets.QLabel('%s:' % _("Current Value"))
         self.cvalue_entry = FCEntry()
         # self.cvaluelabel.setFixedWidth(70)
         self.cvalue_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
@@ -190,7 +191,7 @@ class ToolCalculator(FlatCAMTool):
                                       'to be set on the Power Supply. In Amps.'))
         self.cvalue_entry.setDisabled(True)
 
-        self.timelabel = QtWidgets.QLabel(_("Time:"))
+        self.timelabel = QtWidgets.QLabel('%s:' % _("Time"))
         self.time_entry = FCEntry()
         # self.timelabel.setFixedWidth(70)
         self.time_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
@@ -242,7 +243,12 @@ class ToolCalculator(FlatCAMTool):
             else:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        self.app.ui.splitter.setSizes([0, 1])
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
                 except AttributeError:
                     pass
         else:

+ 261 - 137
flatcamTools/ToolCutOut.py

@@ -16,7 +16,6 @@ if '_' not in builtins.__dict__:
 class CutOut(FlatCAMTool):
 
     toolName = _("Cutout PCB")
-    gapFinished = pyqtSignal()
 
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
@@ -51,7 +50,7 @@ class CutOut(FlatCAMTool):
         # self.type_obj_combo.setItemIcon(1, QtGui.QIcon("share/drill16.png"))
         self.type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
 
-        self.type_obj_combo_label = QtWidgets.QLabel(_("Obj Type:"))
+        self.type_obj_combo_label = QtWidgets.QLabel('%s:' % _("Obj Type"))
         self.type_obj_combo_label.setToolTip(
             _("Specify the type of object to be cutout.\n"
               "It can be of type: Gerber or Geometry.\n"
@@ -67,14 +66,14 @@ class CutOut(FlatCAMTool):
         self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.obj_combo.setCurrentIndex(1)
 
-        self.object_label = QtWidgets.QLabel(_("Object:"))
+        self.object_label = QtWidgets.QLabel('%s:' % _("Object"))
         self.object_label.setToolTip(
             _("Object to be cutout.                        ")
         )
         form_layout.addRow(self.object_label, self.obj_combo)
 
         # Object kind
-        self.kindlabel = QtWidgets.QLabel(_('Obj kind:'))
+        self.kindlabel = QtWidgets.QLabel('%s:' % _('Obj kind'))
         self.kindlabel.setToolTip(
             _("Choice of what kind the object we want to cutout is.<BR>"
               "- <B>Single</B>: contain a single PCB Gerber outline object.<BR>"
@@ -89,7 +88,7 @@ class CutOut(FlatCAMTool):
 
         # Tool Diameter
         self.dia = FCEntry()
-        self.dia_label = QtWidgets.QLabel(_("Tool Dia:"))
+        self.dia_label = QtWidgets.QLabel('%s:' % _("Tool dia"))
         self.dia_label.setToolTip(
            _("Diameter of the tool used to cutout\n"
              "the PCB shape out of the surrounding material.")
@@ -98,7 +97,7 @@ class CutOut(FlatCAMTool):
 
         # Margin
         self.margin = FCEntry()
-        self.margin_label = QtWidgets.QLabel(_("Margin:"))
+        self.margin_label = QtWidgets.QLabel('%s:' % _("Margin:"))
         self.margin_label.setToolTip(
            _("Margin over bounds. A positive value here\n"
              "will make the cutout of the PCB further from\n"
@@ -108,7 +107,7 @@ class CutOut(FlatCAMTool):
 
         # Gapsize
         self.gapsize = FCEntry()
-        self.gapsize_label = QtWidgets.QLabel(_("Gap size:"))
+        self.gapsize_label = QtWidgets.QLabel('%s:' % _("Gap size:"))
         self.gapsize_label.setToolTip(
            _("The size of the bridge gaps in the cutout\n"
              "used to keep the board connected to\n"
@@ -127,7 +126,7 @@ class CutOut(FlatCAMTool):
 
         # Surrounding convex box shape
         self.convex_box = FCCheckBox()
-        self.convex_box_label = QtWidgets.QLabel(_("Convex Sh.:"))
+        self.convex_box_label = QtWidgets.QLabel('%s:' % _("Convex Sh."))
         self.convex_box_label.setToolTip(
             _("Create a convex shape surrounding the entire PCB.\n"
               "Used only if the source object type is Gerber.")
@@ -146,11 +145,12 @@ class CutOut(FlatCAMTool):
         self.layout.addLayout(form_layout_2)
 
         # Gaps
-        gaps_label = QtWidgets.QLabel(_('Gaps:'))
+        gaps_label = QtWidgets.QLabel('%s:' % _('Gaps'))
         gaps_label.setToolTip(
             _("Number of gaps used for the Automatic cutout.\n"
               "There can be maximum 8 bridges/gaps.\n"
               "The choices are:\n"
+              "- None  - no gaps\n"
               "- lr    - left + right\n"
               "- tb    - top + bottom\n"
               "- 4     - left + right +top + bottom\n"
@@ -161,7 +161,7 @@ class CutOut(FlatCAMTool):
         gaps_label.setMinimumWidth(60)
 
         self.gaps = FCComboBox()
-        gaps_items = ['LR', 'TB', '4', '2LR', '2TB', '8']
+        gaps_items = ['None', 'LR', 'TB', '4', '2LR', '2TB', '8']
         for it in gaps_items:
             self.gaps.addItem(it)
             self.gaps.setStyleSheet('background-color: rgb(255,255,255)')
@@ -171,7 +171,7 @@ class CutOut(FlatCAMTool):
         hlay = QtWidgets.QHBoxLayout()
         self.layout.addLayout(hlay)
 
-        title_ff_label = QtWidgets.QLabel("<b>%s</b>" % _('FreeForm:'))
+        title_ff_label = QtWidgets.QLabel("<b>%s:</b>" % _('FreeForm'))
         title_ff_label.setToolTip(
             _("The cutout shape can be of ny shape.\n"
               "Useful when the PCB has a non-rectangular shape.")
@@ -191,7 +191,7 @@ class CutOut(FlatCAMTool):
         hlay2 = QtWidgets.QHBoxLayout()
         self.layout.addLayout(hlay2)
 
-        title_rct_label = QtWidgets.QLabel("<b>%s</b>" % _('Rectangular:'))
+        title_rct_label = QtWidgets.QLabel("<b>%s:</b>" % _('Rectangular'))
         title_rct_label.setToolTip(
             _("The resulting cutout shape is\n"
               "always a rectangle shape and it will be\n"
@@ -228,7 +228,7 @@ class CutOut(FlatCAMTool):
         self.man_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
         self.man_object_combo.setCurrentIndex(1)
 
-        self.man_object_label = QtWidgets.QLabel(_("Geo Obj:"))
+        self.man_object_label = QtWidgets.QLabel('%s:' % _("Geo Obj"))
         self.man_object_label.setToolTip(
             _("Geometry object used to create the manual cutout.")
         )
@@ -241,7 +241,7 @@ class CutOut(FlatCAMTool):
         hlay3 = QtWidgets.QHBoxLayout()
         self.layout.addLayout(hlay3)
 
-        self.man_geo_label = QtWidgets.QLabel(_("Manual Geo:"))
+        self.man_geo_label = QtWidgets.QLabel('%s:' % _("Manual Geo"))
         self.man_geo_label.setToolTip(
             _("If the object to be cutout is a Gerber\n"
               "first create a Geometry that surrounds it,\n"
@@ -263,7 +263,7 @@ class CutOut(FlatCAMTool):
         hlay4 = QtWidgets.QHBoxLayout()
         self.layout.addLayout(hlay4)
 
-        self.man_bridge_gaps_label = QtWidgets.QLabel(_("Manual Add Bridge Gaps:"))
+        self.man_bridge_gaps_label = QtWidgets.QLabel('%s:' % _("Manual Add Bridge Gaps"))
         self.man_bridge_gaps_label.setToolTip(
             _("Use the left mouse button (LMB) click\n"
               "to create a bridge gap to separate the PCB from\n"
@@ -292,6 +292,16 @@ class CutOut(FlatCAMTool):
 
         self.flat_geometry = []
 
+        # this is the Geometry object generated in this class to be used for adding manual gaps
+        self.man_cutout_obj = None
+
+        # if mouse is dragging set the object True
+        self.mouse_is_dragging = False
+
+        # hold the mouse position here
+        self.x_pos = None
+        self.y_pos = None
+
         # Signals
         self.ff_cutout_object_btn.clicked.connect(self.on_freeform_cutout)
         self.rect_cutout_object_btn.clicked.connect(self.on_rectangular_cutout)
@@ -315,7 +325,12 @@ class CutOut(FlatCAMTool):
             else:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        self.app.ui.splitter.setSizes([0, 1])
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
                 except AttributeError:
                     pass
         else:
@@ -340,8 +355,6 @@ class CutOut(FlatCAMTool):
         self.gaps.set_value(self.app.defaults["tools_gaps_ff"])
         self.convex_box.set_value(self.app.defaults['tools_cutout_convexshape'])
 
-        self.gapFinished.connect(self.on_gap_finished)
-
     def on_freeform_cutout(self):
 
         # def subtract_rectangle(obj_, x0, y0, x1, y1):
@@ -410,8 +423,9 @@ class CutOut(FlatCAMTool):
             self.app.inform.emit(_("[WARNING_NOTCL] Number of gaps value is missing. Add it and retry."))
             return
 
-        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. "
+        if gaps not in ['None', 'LR', 'TB', '2LR', '2TB', '4', '8']:
+            self.app.inform.emit(_("[WARNING_NOTCL] Gaps value can be only one of: "
+                                   "'None', 'lr', 'tb', '2lr', '2tb', 4 or 8. "
                                    "Fill in a correct value and retry. "))
             return
 
@@ -446,44 +460,46 @@ class CutOut(FlatCAMTool):
                 leny = (ymax - ymin) + (margin * 2)
 
                 proc_geometry = []
-
-                if gaps == '8' or gaps == '2LR':
-                    geom = self.subtract_poly_from_geo(geom,
-                                                       xmin - gapsize,  # botleft_x
-                                                       py - gapsize + leny / 4,  # botleft_y
-                                                       xmax + gapsize,  # topright_x
-                                                       py + gapsize + leny / 4)  # topright_y
-                    geom = self.subtract_poly_from_geo(geom,
-                                                       xmin - gapsize,
-                                                       py - gapsize - leny / 4,
-                                                       xmax + gapsize,
-                                                       py + gapsize - leny / 4)
-
-                if gaps == '8' or gaps == '2TB':
-                    geom = self.subtract_poly_from_geo(geom,
-                                                       px - gapsize + lenx / 4,
-                                                       ymin - gapsize,
-                                                       px + gapsize + lenx / 4,
-                                                       ymax + gapsize)
-                    geom = self.subtract_poly_from_geo(geom,
-                                                       px - gapsize - lenx / 4,
-                                                       ymin - gapsize,
-                                                       px + gapsize - lenx / 4,
-                                                       ymax + gapsize)
-
-                if gaps == '4' or gaps == 'LR':
-                    geom = self.subtract_poly_from_geo(geom,
-                                                       xmin - gapsize,
-                                                       py - gapsize,
-                                                       xmax + gapsize,
-                                                       py + gapsize)
-
-                if gaps == '4' or gaps == 'TB':
-                    geom = self.subtract_poly_from_geo(geom,
-                                                       px - gapsize,
-                                                       ymin - gapsize,
-                                                       px + gapsize,
-                                                       ymax + gapsize)
+                if gaps == 'None':
+                    pass
+                else:
+                    if gaps == '8' or gaps == '2LR':
+                        geom = self.subtract_poly_from_geo(geom,
+                                                           xmin - gapsize,  # botleft_x
+                                                           py - gapsize + leny / 4,  # botleft_y
+                                                           xmax + gapsize,  # topright_x
+                                                           py + gapsize + leny / 4)  # topright_y
+                        geom = self.subtract_poly_from_geo(geom,
+                                                           xmin - gapsize,
+                                                           py - gapsize - leny / 4,
+                                                           xmax + gapsize,
+                                                           py + gapsize - leny / 4)
+
+                    if gaps == '8' or gaps == '2TB':
+                        geom = self.subtract_poly_from_geo(geom,
+                                                           px - gapsize + lenx / 4,
+                                                           ymin - gapsize,
+                                                           px + gapsize + lenx / 4,
+                                                           ymax + gapsize)
+                        geom = self.subtract_poly_from_geo(geom,
+                                                           px - gapsize - lenx / 4,
+                                                           ymin - gapsize,
+                                                           px + gapsize - lenx / 4,
+                                                           ymax + gapsize)
+
+                    if gaps == '4' or gaps == 'LR':
+                        geom = self.subtract_poly_from_geo(geom,
+                                                           xmin - gapsize,
+                                                           py - gapsize,
+                                                           xmax + gapsize,
+                                                           py + gapsize)
+
+                    if gaps == '4' or gaps == 'TB':
+                        geom = self.subtract_poly_from_geo(geom,
+                                                           px - gapsize,
+                                                           ymin - gapsize,
+                                                           px + gapsize,
+                                                           ymax + gapsize)
 
                 try:
                     for g in geom:
@@ -603,8 +619,9 @@ class CutOut(FlatCAMTool):
             self.app.inform.emit(_("[WARNING_NOTCL] Number of gaps value is missing. Add it and retry."))
             return
 
-        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. "
+        if gaps not in ['None', 'LR', 'TB', '2LR', '2TB', '4', '8']:
+            self.app.inform.emit(_("[WARNING_NOTCL] Gaps value can be only one of: "
+                                   "'None', 'lr', 'tb', '2lr', '2tb', 4 or 8. "
                                    "Fill in a correct value and retry. "))
             return
 
@@ -630,43 +647,46 @@ class CutOut(FlatCAMTool):
                 lenx = (xmax - xmin) + (margin * 2)
                 leny = (ymax - ymin) + (margin * 2)
 
-                if gaps == '8' or gaps == '2LR':
-                    geom = self.subtract_poly_from_geo(geom,
-                                                       xmin - gapsize,  # botleft_x
-                                                       py - gapsize + leny / 4,  # botleft_y
-                                                       xmax + gapsize,  # topright_x
-                                                       py + gapsize + leny / 4)  # topright_y
-                    geom = self.subtract_poly_from_geo(geom,
-                                                       xmin - gapsize,
-                                                       py - gapsize - leny / 4,
-                                                       xmax + gapsize,
-                                                       py + gapsize - leny / 4)
-
-                if gaps == '8' or gaps == '2TB':
-                    geom = self.subtract_poly_from_geo(geom,
-                                                       px - gapsize + lenx / 4,
-                                                       ymin - gapsize,
-                                                       px + gapsize + lenx / 4,
-                                                       ymax + gapsize)
-                    geom = self.subtract_poly_from_geo(geom,
-                                                       px - gapsize - lenx / 4,
-                                                       ymin - gapsize,
-                                                       px + gapsize - lenx / 4,
-                                                       ymax + gapsize)
-
-                if gaps == '4' or gaps == 'LR':
-                    geom = self.subtract_poly_from_geo(geom,
-                                                       xmin - gapsize,
-                                                       py - gapsize,
-                                                       xmax + gapsize,
-                                                       py + gapsize)
-
-                if gaps == '4' or gaps == 'TB':
-                    geom = self.subtract_poly_from_geo(geom,
-                                                       px - gapsize,
-                                                       ymin - gapsize,
-                                                       px + gapsize,
-                                                       ymax + gapsize)
+                if gaps == 'None':
+                    pass
+                else:
+                    if gaps == '8' or gaps == '2LR':
+                        geom = self.subtract_poly_from_geo(geom,
+                                                           xmin - gapsize,  # botleft_x
+                                                           py - gapsize + leny / 4,  # botleft_y
+                                                           xmax + gapsize,  # topright_x
+                                                           py + gapsize + leny / 4)  # topright_y
+                        geom = self.subtract_poly_from_geo(geom,
+                                                           xmin - gapsize,
+                                                           py - gapsize - leny / 4,
+                                                           xmax + gapsize,
+                                                           py + gapsize - leny / 4)
+
+                    if gaps == '8' or gaps == '2TB':
+                        geom = self.subtract_poly_from_geo(geom,
+                                                           px - gapsize + lenx / 4,
+                                                           ymin - gapsize,
+                                                           px + gapsize + lenx / 4,
+                                                           ymax + gapsize)
+                        geom = self.subtract_poly_from_geo(geom,
+                                                           px - gapsize - lenx / 4,
+                                                           ymin - gapsize,
+                                                           px + gapsize - lenx / 4,
+                                                           ymax + gapsize)
+
+                    if gaps == '4' or gaps == 'LR':
+                        geom = self.subtract_poly_from_geo(geom,
+                                                           xmin - gapsize,
+                                                           py - gapsize,
+                                                           xmax + gapsize,
+                                                           py + gapsize)
+
+                    if gaps == '4' or gaps == 'TB':
+                        geom = self.subtract_poly_from_geo(geom,
+                                                           px - gapsize,
+                                                           ymin - gapsize,
+                                                           px + gapsize,
+                                                           ymax + gapsize)
                 try:
                     for g in geom:
                         proc_geometry.append(g)
@@ -743,66 +763,50 @@ class CutOut(FlatCAMTool):
                                      "Add it and retry."))
                 return
 
+        name = self.man_object_combo.currentText()
+        # Get Geometry source object to be used as target for Manual adding Gaps
+        try:
+            self.man_cutout_obj = self.app.collection.get_by_name(str(name))
+        except Exception as e:
+            log.debug("CutOut.on_manual_cutout() --> %s" % str(e))
+            self.app.inform.emit(_("[ERROR_NOTCL] Could not retrieve Geometry object: %s") % name)
+            return "Could not retrieve object: %s" % name
+
         self.app.plotcanvas.vis_disconnect('key_press', self.app.ui.keyPressEvent)
         self.app.plotcanvas.vis_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
         self.app.plotcanvas.vis_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
         self.app.plotcanvas.vis_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
         self.app.plotcanvas.vis_connect('key_press', self.on_key_press)
         self.app.plotcanvas.vis_connect('mouse_move', self.on_mouse_move)
-        self.app.plotcanvas.vis_connect('mouse_release', self.doit)
-
-    # To be called after clicking on the plot.
-    def doit(self, event):
-        # do paint single only for left mouse clicks
-        if event.button == 1:
-            self.app.inform.emit(_("Making manual bridge gap..."))
-            pos = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
-            self.on_manual_cutout(click_pos=pos)
-
-            self.app.plotcanvas.vis_disconnect('key_press', self.on_key_press)
-            self.app.plotcanvas.vis_disconnect('mouse_move', self.on_mouse_move)
-            self.app.plotcanvas.vis_disconnect('mouse_release', self.doit)
-            self.app.plotcanvas.vis_connect('key_press', self.app.ui.keyPressEvent)
-            self.app.plotcanvas.vis_connect('mouse_press', self.app.on_mouse_click_over_plot)
-            self.app.plotcanvas.vis_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
-            self.app.plotcanvas.vis_connect('mouse_move', self.app.on_mouse_move_over_plot)
-
-            self.app.geo_editor.tool_shape.clear(update=True)
-            self.app.geo_editor.tool_shape.enabled = False
-            self.gapFinished.emit()
+        self.app.plotcanvas.vis_connect('mouse_release', self.on_mouse_click_release)
 
     def on_manual_cutout(self, click_pos):
         name = self.man_object_combo.currentText()
 
         # Get source object.
         try:
-            cutout_obj = self.app.collection.get_by_name(str(name))
+            self.man_cutout_obj = self.app.collection.get_by_name(str(name))
         except Exception as e:
             log.debug("CutOut.on_manual_cutout() --> %s" % str(e))
             self.app.inform.emit(_("[ERROR_NOTCL] Could not retrieve Geometry object: %s") % name)
             return "Could not retrieve object: %s" % name
 
-        if cutout_obj is None:
-            self.app.inform.emit(_("[ERROR_NOTCL] Geometry object for manual cutout not found: %s") % cutout_obj)
+        if self.man_cutout_obj is None:
+            self.app.inform.emit(
+                _("[ERROR_NOTCL] Geometry object for manual cutout not found: %s") % self.man_cutout_obj)
             return
 
         # use the snapped position as reference
         snapped_pos = self.app.geo_editor.snap(click_pos[0], click_pos[1])
 
         cut_poly = self.cutting_geo(pos=(snapped_pos[0], snapped_pos[1]))
-        cutout_obj.subtract_polygon(cut_poly)
+        self.man_cutout_obj.subtract_polygon(cut_poly)
 
-        cutout_obj.plot()
+        self.man_cutout_obj.plot()
         self.app.inform.emit(_("[success] Added manual Bridge Gap."))
 
         self.app.should_we_save = True
 
-    def on_gap_finished(self):
-        # if CTRL key modifier is pressed then repeat the bridge gap cut
-        key_modifier = QtWidgets.QApplication.keyboardModifiers()
-        if key_modifier == Qt.ControlModifier:
-            self.on_manual_gap_click()
-
     def on_manual_geo(self):
         name = self.obj_combo.currentText()
 
@@ -864,9 +868,17 @@ class CutOut(FlatCAMTool):
                 geo = geo_union.convex_hull
                 geo_obj.solid_geometry = geo.buffer(margin + abs(dia / 2))
             elif kind == 'single':
-                x0, y0, x1, y1 = geo_union.bounds
-                geo = box(x0, y0, x1, y1)
-                geo_obj.solid_geometry = geo.buffer(margin + abs(dia / 2))
+                if isinstance(geo_union, Polygon) or \
+                        (isinstance(geo_union, list) and len(geo_union) == 1) or \
+                        (isinstance(geo_union, MultiPolygon) and len(geo_union) == 1):
+                    geo_obj.solid_geometry = geo_union.buffer(margin + abs(dia / 2)).exterior
+                elif isinstance(geo_union, MultiPolygon):
+                    x0, y0, x1, y1 = geo_union.bounds
+                    geo = box(x0, y0, x1, y1)
+                    geo_obj.solid_geometry = geo.buffer(margin + abs(dia / 2))
+                else:
+                    self.app.inform.emit(_("[ERROR_NOTCL] Geometry not supported for cutout: %s") % type(geo_union))
+                    return 'fail'
             else:
                 geo = geo_union
                 geo = geo.buffer(margin + abs(dia / 2))
@@ -896,26 +908,127 @@ class CutOut(FlatCAMTool):
         cut_poly = box(xmin, ymin, xmax, ymax)
         return cut_poly
 
+    # To be called after clicking on the plot.
+    def on_mouse_click_release(self, event):
+
+        # do paint single only for left mouse clicks
+        if event.button == 1:
+            self.app.inform.emit(_("Making manual bridge gap..."))
+            pos = self.app.plotcanvas.translate_coords(event.pos)
+            self.on_manual_cutout(click_pos=pos)
+
+            # self.app.plotcanvas.vis_disconnect('key_press', self.on_key_press)
+            # self.app.plotcanvas.vis_disconnect('mouse_move', self.on_mouse_move)
+            # self.app.plotcanvas.vis_disconnect('mouse_release', self.on_mouse_click_release)
+            # self.app.plotcanvas.vis_connect('key_press', self.app.ui.keyPressEvent)
+            # self.app.plotcanvas.vis_connect('mouse_press', self.app.on_mouse_click_over_plot)
+            # self.app.plotcanvas.vis_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
+            # self.app.plotcanvas.vis_connect('mouse_move', self.app.on_mouse_move_over_plot)
+
+            # self.app.geo_editor.tool_shape.clear(update=True)
+            # self.app.geo_editor.tool_shape.enabled = False
+            # self.gapFinished.emit()
+
+        # if RMB then we exit
+        elif event.button == 2 and self.mouse_is_dragging is False:
+            self.app.plotcanvas.vis_disconnect('key_press', self.on_key_press)
+            self.app.plotcanvas.vis_disconnect('mouse_move', self.on_mouse_move)
+            self.app.plotcanvas.vis_disconnect('mouse_release', self.on_mouse_click_release)
+            self.app.plotcanvas.vis_connect('key_press', self.app.ui.keyPressEvent)
+            self.app.plotcanvas.vis_connect('mouse_press', self.app.on_mouse_click_over_plot)
+            self.app.plotcanvas.vis_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
+            self.app.plotcanvas.vis_connect('mouse_move', self.app.on_mouse_move_over_plot)
+
+            # Remove any previous utility shape
+            self.app.geo_editor.tool_shape.clear(update=True)
+            self.app.geo_editor.tool_shape.enabled = False
+
     def on_mouse_move(self, event):
 
         self.app.on_mouse_move_over_plot(event=event)
 
-        pos = self.canvas.vispy_canvas.translate_coords(event.pos)
+        pos = self.canvas.translate_coords(event.pos)
         event.xdata, event.ydata = pos[0], pos[1]
 
+        if event.is_dragging is True:
+            self.mouse_is_dragging = True
+        else:
+            self.mouse_is_dragging = False
+
         try:
             x = float(event.xdata)
             y = float(event.ydata)
         except TypeError:
             return
 
-        snap_x, snap_y = self.app.geo_editor.snap(x, y)
+        if self.app.grid_status() == True:
+            snap_x, snap_y = self.app.geo_editor.snap(x, y)
+        else:
+            snap_x, snap_y = x, y
+
+        self.x_pos, self.y_pos = snap_x, snap_y
 
-        geo = self.cutting_geo(pos=(snap_x, snap_y))
+        # #################################################
+        # ### This section makes the cutting geo to #######
+        # ### rotate if it intersects the target geo ######
+        # #################################################
+        cut_geo = self.cutting_geo(pos=(snap_x, snap_y))
+        man_geo = self.man_cutout_obj.solid_geometry
+
+        def get_angle(geo):
+            line = cut_geo.intersection(geo)
+
+            try:
+                pt1_x = line.coords[0][0]
+                pt1_y = line.coords[0][1]
+                pt2_x = line.coords[1][0]
+                pt2_y = line.coords[1][1]
+                dx = pt1_x - pt2_x
+                dy = pt1_y - pt2_y
+
+                if dx == 0 or dy == 0:
+                    angle = 0
+                else:
+                    radian = math.atan(dx / dy)
+                    angle = radian * 180 / math.pi
+            except Exception as e:
+                angle = 0
+            return angle
+
+        try:
+            rot_angle = 0
+            for geo_el in man_geo:
+                if isinstance(geo_el, Polygon):
+                    work_geo = geo_el.exterior
+                    if cut_geo.intersects(work_geo):
+                        rot_angle = get_angle(geo=work_geo)
+                    else:
+                        rot_angle = 0
+                else:
+                    rot_angle = 0
+                    if cut_geo.intersects(geo_el):
+                        rot_angle = get_angle(geo=geo_el)
+                if rot_angle != 0:
+                    break
+        except TypeError:
+            if isinstance(man_geo, Polygon):
+                work_geo = man_geo.exterior
+                if cut_geo.intersects(work_geo):
+                    rot_angle = get_angle(geo=work_geo)
+                else:
+                    rot_angle = 0
+            else:
+                rot_angle = 0
+                if cut_geo.intersects(man_geo):
+                    rot_angle = get_angle(geo=man_geo)
+
+        # rotate only if there is an angle to rotate to
+        if rot_angle != 0:
+            cut_geo = affinity.rotate(cut_geo, -rot_angle)
 
         # Remove any previous utility shape
         self.app.geo_editor.tool_shape.clear(update=True)
-        self.draw_utility_geometry(geo=geo)
+        self.draw_utility_geometry(geo=cut_geo)
 
     def draw_utility_geometry(self, geo):
         self.app.geo_editor.tool_shape.add(
@@ -941,7 +1054,7 @@ class CutOut(FlatCAMTool):
         if key == QtCore.Qt.Key_Escape or key == 'Escape':
             self.app.plotcanvas.vis_disconnect('key_press', self.on_key_press)
             self.app.plotcanvas.vis_disconnect('mouse_move', self.on_mouse_move)
-            self.app.plotcanvas.vis_disconnect('mouse_release', self.doit)
+            self.app.plotcanvas.vis_disconnect('mouse_release', self.on_mouse_click_release)
             self.app.plotcanvas.vis_connect('key_press', self.app.ui.keyPressEvent)
             self.app.plotcanvas.vis_connect('mouse_press', self.app.on_mouse_click_over_plot)
             self.app.plotcanvas.vis_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
@@ -951,6 +1064,17 @@ class CutOut(FlatCAMTool):
             self.app.geo_editor.tool_shape.clear(update=True)
             self.app.geo_editor.tool_shape.enabled = False
 
+        # Grid toggle
+        if key == QtCore.Qt.Key_G or key == 'G':
+            self.app.ui.grid_snap_btn.trigger()
+
+        # Jump to coords
+        if key == QtCore.Qt.Key_J or key == 'J':
+            l_x, l_y = self.app.on_jump_to()
+            self.app.geo_editor.tool_shape.clear(update=True)
+            geo = self.cutting_geo(pos=(l_x, l_y))
+            self.draw_utility_geometry(geo=geo)
+
     def subtract_poly_from_geo(self, solid_geo, x0, y0, x1, y1):
         """
         Subtract polygon made from points from the given object.

+ 13 - 8
flatcamTools/ToolDblSided.py

@@ -44,7 +44,7 @@ class DblSidedTool(FlatCAMTool):
         self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.gerber_object_combo.setCurrentIndex(1)
 
-        self.botlay_label = QtWidgets.QLabel(_("<b>GERBER:</b>"))
+        self.botlay_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
         self.botlay_label.setToolTip(
             "Gerber  to be mirrored."
         )
@@ -68,7 +68,7 @@ class DblSidedTool(FlatCAMTool):
         self.exc_object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
         self.exc_object_combo.setCurrentIndex(1)
 
-        self.excobj_label = QtWidgets.QLabel(_("<b>EXCELLON:</b>"))
+        self.excobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("EXCELLON"))
         self.excobj_label.setToolTip(
             _("Excellon Object to be mirrored.")
         )
@@ -92,7 +92,7 @@ class DblSidedTool(FlatCAMTool):
         self.geo_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
         self.geo_object_combo.setCurrentIndex(1)
 
-        self.geoobj_label = QtWidgets.QLabel(_("<b>GEOMETRY</b>:"))
+        self.geoobj_label = QtWidgets.QLabel("<b>%s</b>:" % _("GEOMETRY"))
         self.geoobj_label.setToolTip(
             _("Geometry Obj to be mirrored.")
         )
@@ -149,7 +149,7 @@ class DblSidedTool(FlatCAMTool):
 
         # ## Point/Box
         self.point_box_container = QtWidgets.QVBoxLayout()
-        self.pb_label = QtWidgets.QLabel("<b>%s</b>" % _('Point/Box Reference:'))
+        self.pb_label = QtWidgets.QLabel("<b>%s:</b>" % _('Point/Box Reference'))
         self.pb_label.setToolTip(
             _("If 'Point' is selected above it store the coordinates (x, y) through which\n"
               "the mirroring axis passes.\n"
@@ -189,7 +189,7 @@ class DblSidedTool(FlatCAMTool):
         self.box_combo_type.hide()
 
         # ## Alignment holes
-        self.ah_label = QtWidgets.QLabel("<b>%s</b>" % _('Alignment Drill Coordinates:'))
+        self.ah_label = QtWidgets.QLabel("<b>%s:</b>" % _('Alignment Drill Coordinates'))
         self.ah_label.setToolTip(
            _("Alignment holes (x1, y1), (x2, y2), ... "
              "on one side of the mirror axis. For each set of (x, y) coordinates\n"
@@ -220,7 +220,7 @@ class DblSidedTool(FlatCAMTool):
         grid_lay3.addWidget(self.add_drill_point_button, 0, 1)
 
         # ## Drill diameter for alignment holes
-        self.dt_label = QtWidgets.QLabel("<b>%s</b>:" % _('Alignment Drill Diameter'))
+        self.dt_label = QtWidgets.QLabel("<b>%s:</b>" % _('Alignment Drill Diameter'))
         self.dt_label.setToolTip(
             _("Diameter of the drill for the "
               "alignment holes.")
@@ -231,7 +231,7 @@ class DblSidedTool(FlatCAMTool):
         self.layout.addLayout(hlay)
 
         self.drill_dia = FCEntry()
-        self.dd_label = QtWidgets.QLabel(_("Drill diam.:"))
+        self.dd_label = QtWidgets.QLabel('%s:' % _("Drill dia"))
         self.dd_label.setToolTip(
             _("Diameter of the drill for the "
               "alignment holes.")
@@ -288,7 +288,12 @@ class DblSidedTool(FlatCAMTool):
             else:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        self.app.ui.splitter.setSizes([0, 1])
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
                 except AttributeError:
                     pass
         else:

+ 11 - 6
flatcamTools/ToolFilm.py

@@ -53,7 +53,7 @@ class Film(FlatCAMTool):
         self.tf_type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
         self.tf_type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
 
-        self.tf_type_obj_combo_label = QtWidgets.QLabel(_("Object Type:"))
+        self.tf_type_obj_combo_label = QtWidgets.QLabel('%s:' % _("Object Type"))
         self.tf_type_obj_combo_label.setToolTip(
             _("Specify the type of object for which to create the film.\n"
               "The object can be of type: Gerber or Geometry.\n"
@@ -68,7 +68,7 @@ class Film(FlatCAMTool):
         self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.tf_object_combo.setCurrentIndex(1)
 
-        self.tf_object_label = QtWidgets.QLabel(_("Film Object:"))
+        self.tf_object_label = QtWidgets.QLabel('%s:' % _("Film Object"))
         self.tf_object_label.setToolTip(
             _("Object for which to create the film.")
         )
@@ -101,7 +101,7 @@ class Film(FlatCAMTool):
         self.tf_box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.tf_box_combo.setCurrentIndex(1)
 
-        self.tf_box_combo_label = QtWidgets.QLabel(_("Box Object:"))
+        self.tf_box_combo_label = QtWidgets.QLabel('%s:' % _("Box Object"))
         self.tf_box_combo_label.setToolTip(
             _("The actual object that is used a container for the\n "
               "selected object for which we create the film.\n"
@@ -127,7 +127,7 @@ class Film(FlatCAMTool):
         # Boundary for negative film generation
 
         self.boundary_entry = FCEntry()
-        self.boundary_label = QtWidgets.QLabel(_("Border:"))
+        self.boundary_label = QtWidgets.QLabel('%s:' % _("Border"))
         self.boundary_label.setToolTip(
             _("Specify a border around the object.\n"
               "Only for negative film.\n"
@@ -141,7 +141,7 @@ class Film(FlatCAMTool):
         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 = QtWidgets.QLabel('%s:' % _("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"
@@ -190,7 +190,12 @@ class Film(FlatCAMTool):
             else:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        self.app.ui.splitter.setSizes([0, 1])
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
                 except AttributeError:
                     pass
         else:

+ 9 - 4
flatcamTools/ToolImage.py

@@ -50,7 +50,7 @@ class ToolImage(FlatCAMTool):
         self.tf_type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
         self.tf_type_obj_combo.setItemIcon(1, QtGui.QIcon("share/geometry16.png"))
 
-        self.tf_type_obj_combo_label = QtWidgets.QLabel(_("Object Type:"))
+        self.tf_type_obj_combo_label = QtWidgets.QLabel('%s:' % _("Object Type"))
         self.tf_type_obj_combo_label.setToolTip(
            _("Specify the type of object to create from the image.\n"
              "It can be of type: Gerber or Geometry.")
@@ -60,7 +60,7 @@ class ToolImage(FlatCAMTool):
 
         # DPI value of the imported image
         self.dpi_entry = IntEntry()
-        self.dpi_label = QtWidgets.QLabel(_("DPI value:"))
+        self.dpi_label = QtWidgets.QLabel('%s:' % _("DPI value"))
         self.dpi_label.setToolTip(
            _("Specify a DPI value for the image.")
         )
@@ -69,7 +69,7 @@ class ToolImage(FlatCAMTool):
         self.emty_lbl = QtWidgets.QLabel("")
         self.layout.addWidget(self.emty_lbl)
 
-        self.detail_label = QtWidgets.QLabel("<font size=4><b>%s:</b>" % _('Level of detail'))
+        self.detail_label = QtWidgets.QLabel("<font size=4><b>%s:</b></font>" % _('Level of detail'))
         self.layout.addWidget(self.detail_label)
 
         ti2_form_layout = QtWidgets.QFormLayout()
@@ -157,7 +157,12 @@ class ToolImage(FlatCAMTool):
             else:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        self.app.ui.splitter.setSizes([0, 1])
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
                 except AttributeError:
                     pass
         else:

+ 8 - 8
flatcamTools/ToolMeasurement.py

@@ -40,7 +40,7 @@ class Measurement(FlatCAMTool):
         form_layout = QtWidgets.QFormLayout()
         self.layout.addLayout(form_layout)
 
-        self.units_label = QtWidgets.QLabel(_("Units:"))
+        self.units_label = QtWidgets.QLabel('%s:' % _("Units"))
         self.units_label.setToolTip(_("Those are the units in which the distance is measured."))
         self.units_value = QtWidgets.QLabel("%s" % str({'mm': _("METRIC (mm)"), 'in': _("INCH (in)")}[self.units]))
         self.units_value.setDisabled(True)
@@ -51,10 +51,10 @@ class Measurement(FlatCAMTool):
         self.stop_label = QtWidgets.QLabel("<b>%s</b> %s:" % (_('Stop'), _('Coords')))
         self.stop_label.setToolTip(_("This is the measuring Stop point coordinates."))
 
-        self.distance_x_label = QtWidgets.QLabel(_("Dx:"))
+        self.distance_x_label = QtWidgets.QLabel('%s:' % _("Dx"))
         self.distance_x_label.setToolTip(_("This is the distance measured over the X axis."))
 
-        self.distance_y_label = QtWidgets.QLabel(_("Dy:"))
+        self.distance_y_label = QtWidgets.QLabel('%s:' % _("Dy"))
         self.distance_y_label.setToolTip(_("This is the distance measured over the Y axis."))
 
         self.total_distance_label = QtWidgets.QLabel("<b>%s:</b>" % _('DISTANCE'))
@@ -113,7 +113,7 @@ class Measurement(FlatCAMTool):
         self.original_call_source = 'app'
 
         # VisPy visuals
-        self.sel_shapes = ShapeCollection(parent=self.app.plotcanvas.vispy_canvas.view.scene, layers=1)
+        self.sel_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1)
 
         self.measure_btn.clicked.connect(self.activate_measure_tool)
 
@@ -247,9 +247,9 @@ class Measurement(FlatCAMTool):
         log.debug("Measuring Tool --> mouse click release")
 
         if event.button == 1:
-            pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
+            pos_canvas = self.canvas.translate_coords(event.pos)
             # if GRID is active we need to get the snapped positions
-            if self.app.grid_status():
+            if self.app.grid_status() == True:
                 pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
             else:
                 pos = pos_canvas[0], pos_canvas[1]
@@ -286,8 +286,8 @@ class Measurement(FlatCAMTool):
 
     def on_mouse_move_meas(self, event):
         try:  # May fail in case mouse not within axes
-            pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
-            if self.app.grid_status():
+            pos_canvas = self.app.plotcanvas.translate_coords(event.pos)
+            if self.app.grid_status() == True:
                 pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
                 self.app.app_cursor.enabled = True
                 # Update cursor

+ 4 - 4
flatcamTools/ToolMove.py

@@ -43,7 +43,7 @@ class ToolMove(FlatCAMTool):
         self.old_coords = []
 
         # VisPy visuals
-        self.sel_shapes = ShapeCollection(parent=self.app.plotcanvas.vispy_canvas.view.scene, layers=1)
+        self.sel_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1)
 
     def install(self, icon=None, separator=None, **kwargs):
         FlatCAMTool.install(self, icon, separator, shortcut='M', **kwargs)
@@ -94,7 +94,7 @@ class ToolMove(FlatCAMTool):
 
         if event.button == 1:
             if self.clicked_move == 0:
-                pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
+                pos_canvas = self.app.plotcanvas.translate_coords(event.pos)
 
                 # if GRID is active we need to get the snapped positions
                 if self.app.grid_status() == True:
@@ -111,7 +111,7 @@ class ToolMove(FlatCAMTool):
 
             if self.clicked_move == 1:
                 try:
-                    pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
+                    pos_canvas = self.app.plotcanvas.translate_coords(event.pos)
 
                     # delete the selection bounding box
                     self.delete_shape()
@@ -178,7 +178,7 @@ class ToolMove(FlatCAMTool):
             self.clicked_move = 1
 
     def on_move(self, event):
-        pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
+        pos_canvas = self.app.plotcanvas.translate_coords(event.pos)
 
         # if GRID is active we need to get the snapped positions
         if self.app.grid_status() == True:

File diff suppressed because it is too large
+ 606 - 172
flatcamTools/ToolNonCopperClear.py


File diff suppressed because it is too large
+ 502 - 146
flatcamTools/ToolPaint.py


+ 20 - 15
flatcamTools/ToolPanelize.py

@@ -53,7 +53,7 @@ class Panelize(FlatCAMTool):
         self.type_obj_combo.setItemIcon(1, QtGui.QIcon("share/drill16.png"))
         self.type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
 
-        self.type_obj_combo_label = QtWidgets.QLabel(_("Object Type:"))
+        self.type_obj_combo_label = QtWidgets.QLabel('%s:' % _("Object Type"))
         self.type_obj_combo_label.setToolTip(
             _("Specify the type of object to be panelized\n"
               "It can be of type: Gerber, Excellon or Geometry.\n"
@@ -68,7 +68,7 @@ class Panelize(FlatCAMTool):
         self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.object_combo.setCurrentIndex(1)
 
-        self.object_label = QtWidgets.QLabel(_("Object:"))
+        self.object_label = QtWidgets.QLabel('%s:' % _("Object"))
         self.object_label.setToolTip(
             _("Object to be panelized. This means that it will\n"
               "be duplicated in an array of rows and columns.")
@@ -83,7 +83,7 @@ class Panelize(FlatCAMTool):
         # Type of box Panel object
         self.reference_radio = RadioSet([{'label': _('Object'), 'value': 'object'},
                                          {'label': _('Bounding Box'), 'value': 'bbox'}])
-        self.box_label = QtWidgets.QLabel(_("<b>Penelization Reference:</b>"))
+        self.box_label = QtWidgets.QLabel("<b>%s:</b>" % _("Penelization Reference"))
         self.box_label.setToolTip(
             _("Choose the reference for panelization:\n"
               "- Object = the bounding box of a different object\n"
@@ -108,7 +108,7 @@ class Panelize(FlatCAMTool):
         self.type_box_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
         self.type_box_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
 
-        self.type_box_combo_label = QtWidgets.QLabel(_("Box Type:"))
+        self.type_box_combo_label = QtWidgets.QLabel('%s:' % _("Box Type"))
         self.type_box_combo_label.setToolTip(
             _("Specify the type of object to be used as an container for\n"
               "panelization. It can be: Gerber or Geometry type.\n"
@@ -123,7 +123,7 @@ class Panelize(FlatCAMTool):
         self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.box_combo.setCurrentIndex(1)
 
-        self.box_combo_label = QtWidgets.QLabel(_("Box Object:"))
+        self.box_combo_label = QtWidgets.QLabel('%s:' % _("Box Object"))
         self.box_combo_label.setToolTip(
             _("The actual object that is used a container for the\n "
               "selected object that is to be panelized.")
@@ -131,7 +131,7 @@ class Panelize(FlatCAMTool):
         form_layout.addRow(self.box_combo_label, self.box_combo)
         form_layout.addRow(QtWidgets.QLabel(""))
 
-        panel_data_label = QtWidgets.QLabel(_("<b>Panel Data:</b>"))
+        panel_data_label = QtWidgets.QLabel("<b>%s:</b>" % _("Panel Data"))
         panel_data_label.setToolTip(
             _("This informations will shape the resulting panel.\n"
               "The number of rows and columns will set how many\n"
@@ -144,7 +144,7 @@ class Panelize(FlatCAMTool):
 
         # Spacing Columns
         self.spacing_columns = FCEntry()
-        self.spacing_columns_label = QtWidgets.QLabel(_("Spacing cols:"))
+        self.spacing_columns_label = QtWidgets.QLabel('%s:' % _("Spacing cols"))
         self.spacing_columns_label.setToolTip(
             _("Spacing between columns of the desired panel.\n"
               "In current units.")
@@ -153,7 +153,7 @@ class Panelize(FlatCAMTool):
 
         # Spacing Rows
         self.spacing_rows = FCEntry()
-        self.spacing_rows_label = QtWidgets.QLabel(_("Spacing rows:"))
+        self.spacing_rows_label = QtWidgets.QLabel('%s:' % _("Spacing rows"))
         self.spacing_rows_label.setToolTip(
             _("Spacing between rows of the desired panel.\n"
               "In current units.")
@@ -162,7 +162,7 @@ class Panelize(FlatCAMTool):
 
         # Columns
         self.columns = FCEntry()
-        self.columns_label = QtWidgets.QLabel(_("Columns:"))
+        self.columns_label = QtWidgets.QLabel('%s:' % _("Columns"))
         self.columns_label.setToolTip(
             _("Number of columns of the desired panel")
         )
@@ -170,7 +170,7 @@ class Panelize(FlatCAMTool):
 
         # Rows
         self.rows = FCEntry()
-        self.rows_label = QtWidgets.QLabel(_("Rows:"))
+        self.rows_label = QtWidgets.QLabel('%s:' % _("Rows"))
         self.rows_label.setToolTip(
             _("Number of rows of the desired panel")
         )
@@ -180,7 +180,7 @@ class Panelize(FlatCAMTool):
         # Type of resulting Panel object
         self.panel_type_radio = RadioSet([{'label': _('Gerber'), 'value': 'gerber'},
                                           {'label': _('Geo'), 'value': 'geometry'}])
-        self.panel_type_label = QtWidgets.QLabel(_("<b>Panel Type:</b>"))
+        self.panel_type_label = QtWidgets.QLabel("<b>%s:</b>" % _("Panel Type"))
         self.panel_type_label.setToolTip(
             _("Choose the type of object for the panel object:\n"
               "- Geometry\n"
@@ -190,7 +190,7 @@ class Panelize(FlatCAMTool):
         form_layout.addRow(self.panel_type_radio)
 
         # Constrains
-        self.constrain_cb = FCCheckBox(_("Constrain panel within:"))
+        self.constrain_cb = FCCheckBox('%s:' % _("Constrain panel within"))
         self.constrain_cb.setToolTip(
             _("Area define by DX and DY within to constrain the panel.\n"
               "DX and DY values are in current units.\n"
@@ -201,7 +201,7 @@ class Panelize(FlatCAMTool):
         form_layout.addRow(self.constrain_cb)
 
         self.x_width_entry = FCEntry()
-        self.x_width_lbl = QtWidgets.QLabel(_("Width (DX):"))
+        self.x_width_lbl = QtWidgets.QLabel('%s:' % _("Width (DX)"))
         self.x_width_lbl.setToolTip(
             _("The width (DX) within which the panel must fit.\n"
               "In current units.")
@@ -209,7 +209,7 @@ class Panelize(FlatCAMTool):
         form_layout.addRow(self.x_width_lbl, self.x_width_entry)
 
         self.y_height_entry = FCEntry()
-        self.y_height_lbl = QtWidgets.QLabel(_("Height (DY):"))
+        self.y_height_lbl = QtWidgets.QLabel('%s:' % _("Height (DY)"))
         self.y_height_lbl.setToolTip(
             _("The height (DY)within which the panel must fit.\n"
               "In current units.")
@@ -259,7 +259,12 @@ class Panelize(FlatCAMTool):
             else:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        self.app.ui.splitter.setSizes([0, 1])
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
                 except AttributeError:
                     pass
         else:

+ 13 - 8
flatcamTools/ToolPcbWizard.py

@@ -48,13 +48,13 @@ class PcbWizard(FlatCAMTool):
         self.layout.addWidget(title_label)
 
         self.layout.addWidget(QtWidgets.QLabel(""))
-        self.layout.addWidget(QtWidgets.QLabel(_("<b>Load files:</b>")))
+        self.layout.addWidget(QtWidgets.QLabel("<b>%s:</b>" % _("Load files")))
 
         # Form Layout
         form_layout = QtWidgets.QFormLayout()
         self.layout.addLayout(form_layout)
 
-        self.excellon_label = QtWidgets.QLabel(_("Excellon file:"))
+        self.excellon_label = QtWidgets.QLabel('%s:' % _("Excellon file"))
         self.excellon_label.setToolTip(
            _("Load the Excellon file.\n"
              "Usually it has a .DRL extension")
@@ -62,7 +62,7 @@ class PcbWizard(FlatCAMTool):
         self.excellon_brn = FCButton(_("Open"))
         form_layout.addRow(self.excellon_label, self.excellon_brn)
 
-        self.inf_label = QtWidgets.QLabel(_("INF file:"))
+        self.inf_label = QtWidgets.QLabel('%s:' % _("INF file"))
         self.inf_label.setToolTip(
             _("Load the INF file.")
         )
@@ -84,7 +84,7 @@ class PcbWizard(FlatCAMTool):
         self.tools_table.setVisible(False)
 
         self.layout.addWidget(QtWidgets.QLabel(""))
-        self.layout.addWidget(QtWidgets.QLabel(_("<b>Excellon format:</b>")))
+        self.layout.addWidget(QtWidgets.QLabel("<b>%s:</b>" % _("Excellon format")))
         # Form Layout
         form_layout1 = QtWidgets.QFormLayout()
         self.layout.addLayout(form_layout1)
@@ -92,7 +92,7 @@ class PcbWizard(FlatCAMTool):
         # Integral part of the coordinates
         self.int_entry = FCSpinner()
         self.int_entry.set_range(1, 10)
-        self.int_label = QtWidgets.QLabel(_("Int. digits:"))
+        self.int_label = QtWidgets.QLabel('%s:' % _("Int. digits"))
         self.int_label.setToolTip(
            _("The number of digits for the integral part of the coordinates.")
         )
@@ -101,7 +101,7 @@ class PcbWizard(FlatCAMTool):
         # Fractional part of the coordinates
         self.frac_entry = FCSpinner()
         self.frac_entry.set_range(1, 10)
-        self.frac_label = QtWidgets.QLabel(_("Frac. digits:"))
+        self.frac_label = QtWidgets.QLabel('%s:' % _("Frac. digits"))
         self.frac_label.setToolTip(
             _("The number of digits for the fractional part of the coordinates.")
         )
@@ -111,7 +111,7 @@ class PcbWizard(FlatCAMTool):
         self.zeros_radio = RadioSet([{'label': _('LZ'), 'value': 'LZ'},
                                      {'label': _('TZ'), 'value': 'TZ'},
                                      {'label': _('No Suppression'), 'value': 'D'}])
-        self.zeros_label = QtWidgets.QLabel(_("Zeros supp.:"))
+        self.zeros_label = QtWidgets.QLabel('%s:' % _("Zeros supp."))
         self.zeros_label.setToolTip(
             _("The type of zeros suppression used.\n"
               "Can be of type:\n"
@@ -179,7 +179,12 @@ class PcbWizard(FlatCAMTool):
             else:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        self.app.ui.splitter.setSizes([0, 1])
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
                 except AttributeError:
                     pass
         else:

+ 49 - 20
flatcamTools/ToolProperties.py

@@ -77,7 +77,12 @@ class Properties(FlatCAMTool):
             else:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        self.app.ui.splitter.setSizes([0, 1])
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
                 except AttributeError:
                     pass
         else:
@@ -117,23 +122,24 @@ class Properties(FlatCAMTool):
 
         font = QtGui.QFont()
         font.setBold(True)
-        obj_type = self.addParent(parent, 'TYPE', expanded=True, color=QtGui.QColor("#000000"), font=font)
-        obj_name = self.addParent(parent, 'NAME', expanded=True, color=QtGui.QColor("#000000"), font=font)
-        dims = self.addParent(parent, 'Dimensions', expanded=True, color=QtGui.QColor("#000000"), font=font)
-        units = self.addParent(parent, 'Units', expanded=True, color=QtGui.QColor("#000000"), font=font)
+        obj_type = self.addParent(parent, _('TYPE'), expanded=True, color=QtGui.QColor("#000000"), font=font)
+        obj_name = self.addParent(parent, _('NAME'), expanded=True, color=QtGui.QColor("#000000"), font=font)
+        dims = self.addParent(parent, _('Dimensions'), expanded=True, color=QtGui.QColor("#000000"), font=font)
+        units = self.addParent(parent, _('Units'), expanded=True, color=QtGui.QColor("#000000"), font=font)
 
-        options = self.addParent(parent, 'Options', color=QtGui.QColor("#000000"), font=font)
+        options = self.addParent(parent, _('Options'), color=QtGui.QColor("#000000"), font=font)
         if obj.kind.lower() == 'gerber':
-            apertures = self.addParent(parent, 'Apertures', expanded=True, color=QtGui.QColor("#000000"), font=font)
+            apertures = self.addParent(parent, _('Apertures'), expanded=True, color=QtGui.QColor("#000000"), font=font)
         else:
-            tools = self.addParent(parent, 'Tools', expanded=True, color=QtGui.QColor("#000000"), font=font)
+            tools = self.addParent(parent, _('Tools'), expanded=True, color=QtGui.QColor("#000000"), font=font)
 
         separator = self.addParent(parent, '')
 
-        self.addChild(obj_type, ['Object Type:', ('%s' % (obj.kind.capitalize()))], True)
+        self.addChild(obj_type, ['%s:' % _('Object Type'), ('%s' % (obj.kind.capitalize()))], True)
         try:
             self.addChild(obj_type,
-                          ['Geo Type:', ('%s' % ({False: "Single-Geo", True: "Multi-Geo"}[obj.multigeo]))],
+                          ['%s:' % _('Geo Type'),
+                           ('%s' % ({False: _("Single-Geo"), True: _("Multi-Geo")}[obj.multigeo]))],
                           True)
         except Exception as e:
             log.debug("Properties.addItems() --> %s" % str(e))
@@ -150,22 +156,45 @@ class Properties(FlatCAMTool):
         length = abs(xmax - xmin)
         width = abs(ymax - ymin)
 
-        self.addChild(dims, ['Length:', '%.4f %s' % (
+        self.addChild(dims, ['%s:' % _('Length'), '%.4f %s' % (
             length, self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower())], True)
-        self.addChild(dims, ['Width:', '%.4f %s' % (
+        self.addChild(dims, ['%s:' % _('Width'), '%.4f %s' % (
             width, self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower())], True)
+
+        # calculate and add box area
         if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower() == 'mm':
             area = (length * width) / 100
-            self.addChild(dims, ['Box Area:', '%.4f %s' % (area, 'cm2')], True)
+            self.addChild(dims, ['%s:' % _('Box Area'), '%.4f %s' % (area, 'cm2')], True)
         else:
             area = length * width
-            self.addChild(dims, ['Box Area:', '%.4f %s' % (area, 'in2')], True)
+            self.addChild(dims, ['%s:' % _('Box Area'), '%.4f %s' % (area, 'in2')], True)
+
+        if not isinstance(obj, FlatCAMCNCjob):
+            # calculate and add convex hull area
+            geo = obj.solid_geometry
+            if isinstance(geo, MultiPolygon):
+                env_obj = geo.convex_hull
+            elif (isinstance(geo, MultiPolygon) and len(geo) == 1) or \
+                    (isinstance(geo, list) and len(geo) == 1) and isinstance(geo[0], Polygon):
+                env_obj = cascaded_union(obj.solid_geometry)
+                env_obj = env_obj.convex_hull
+            else:
+                env_obj = cascaded_union(obj.solid_geometry)
+                env_obj = env_obj.convex_hull
+
+            area_chull = env_obj.area
+            if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower() == 'mm':
+                area_chull = area_chull / 100
+                self.addChild(dims, ['%s:' % _('Convex_Hull Area'), '%.4f %s' % (area_chull, 'cm2')], True)
+            else:
+                area_chull = area_chull
+                self.addChild(dims, ['%s:' % _('Convex_Hull Area'), '%.4f %s' % (area_chull, 'in2')], True)
 
         self.addChild(units,
                       ['FlatCAM units:',
                        {
-                           'in': 'Inch',
-                           'mm': 'Metric'
+                           'in': _('Inch'),
+                           'mm': _('Metric')
                        }
                        [str(self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower())]
                        ],
@@ -216,7 +245,7 @@ class Properties(FlatCAMTool):
                 geo_tool = self.addParent(tools, str(tool), expanded=True, color=QtGui.QColor("#000000"), font=font)
                 for k, v in value.items():
                     if k == 'solid_geometry':
-                        printed_value = 'Present' if v else 'None'
+                        printed_value = _('Present') if v else _('None')
                         self.addChild(geo_tool, [str(k), printed_value], True)
                     elif k == 'data':
                         tool_data = self.addParent(geo_tool, str(k).capitalize(),
@@ -230,13 +259,13 @@ class Properties(FlatCAMTool):
                 geo_tool = self.addParent(tools, str(tool), expanded=True, color=QtGui.QColor("#000000"), font=font)
                 for k, v in value.items():
                     if k == 'solid_geometry':
-                        printed_value = 'Present' if v else 'None'
+                        printed_value = _('Present') if v else _('None')
                         self.addChild(geo_tool, [str(k), printed_value], True)
                     elif k == 'gcode':
-                        printed_value = 'Present' if v != '' else 'None'
+                        printed_value = _('Present') if v != '' else _('None')
                         self.addChild(geo_tool, [str(k), printed_value], True)
                     elif k == 'gcode_parsed':
-                        printed_value = 'Present' if v else 'None'
+                        printed_value = _('Present') if v else _('None')
                         self.addChild(geo_tool, [str(k), printed_value], True)
                     elif k == 'data':
                         tool_data = self.addParent(geo_tool, str(k).capitalize(),

+ 26 - 21
flatcamTools/ToolSolderPaste.py

@@ -139,7 +139,7 @@ class SolderPaste(FlatCAMTool):
         grid0_1 = QtWidgets.QGridLayout()
         self.layout.addLayout(grid0_1)
 
-        step1_lbl = QtWidgets.QLabel("<b>%s:</b>" % _('STEP 1:'))
+        step1_lbl = QtWidgets.QLabel("<b>%s:</b>" % _('STEP 1'))
         step1_lbl.setToolTip(
             _("First step is to select a number of nozzle tools for usage\n"
               "and then optionally modify the GCode parameters bellow.")
@@ -163,7 +163,7 @@ class SolderPaste(FlatCAMTool):
 
         # Z dispense start
         self.z_start_entry = FCEntry()
-        self.z_start_label = QtWidgets.QLabel(_("Z Dispense Start:"))
+        self.z_start_label = QtWidgets.QLabel('%s:' % _("Z Dispense Start"))
         self.z_start_label.setToolTip(
             _("The height (Z) when solder paste dispensing starts.")
         )
@@ -171,7 +171,7 @@ class SolderPaste(FlatCAMTool):
 
         # Z dispense
         self.z_dispense_entry = FCEntry()
-        self.z_dispense_label = QtWidgets.QLabel(_("Z Dispense:"))
+        self.z_dispense_label = QtWidgets.QLabel('%s:' % _("Z Dispense"))
         self.z_dispense_label.setToolTip(
             _("The height (Z) when doing solder paste dispensing.")
         )
@@ -179,7 +179,7 @@ class SolderPaste(FlatCAMTool):
 
         # Z dispense stop
         self.z_stop_entry = FCEntry()
-        self.z_stop_label = QtWidgets.QLabel(_("Z Dispense Stop:"))
+        self.z_stop_label = QtWidgets.QLabel('%s:' % _("Z Dispense Stop"))
         self.z_stop_label.setToolTip(
             _("The height (Z) when solder paste dispensing stops.")
         )
@@ -187,7 +187,7 @@ class SolderPaste(FlatCAMTool):
 
         # Z travel
         self.z_travel_entry = FCEntry()
-        self.z_travel_label = QtWidgets.QLabel(_("Z Travel:"))
+        self.z_travel_label = QtWidgets.QLabel('%s:' % _("Z Travel"))
         self.z_travel_label.setToolTip(
            _("The height (Z) for travel between pads\n"
              "(without dispensing solder paste).")
@@ -196,7 +196,7 @@ class SolderPaste(FlatCAMTool):
 
         # Z toolchange location
         self.z_toolchange_entry = FCEntry()
-        self.z_toolchange_label = QtWidgets.QLabel(_("Z Toolchange:"))
+        self.z_toolchange_label = QtWidgets.QLabel('%s:' % _("Z Toolchange"))
         self.z_toolchange_label.setToolTip(
            _("The height (Z) for tool (nozzle) change.")
         )
@@ -204,7 +204,7 @@ class SolderPaste(FlatCAMTool):
 
         # X,Y Toolchange location
         self.xy_toolchange_entry = FCEntry()
-        self.xy_toolchange_label = QtWidgets.QLabel(_("XY Toolchange:"))
+        self.xy_toolchange_label = QtWidgets.QLabel('%s:' % _("Toolchange X-Y"))
         self.xy_toolchange_label.setToolTip(
             _("The X,Y location for tool (nozzle) change.\n"
               "The format is (x, y) where x and y are real numbers.")
@@ -213,7 +213,7 @@ class SolderPaste(FlatCAMTool):
 
         # Feedrate X-Y
         self.frxy_entry = FCEntry()
-        self.frxy_label = QtWidgets.QLabel(_("Feedrate X-Y:"))
+        self.frxy_label = QtWidgets.QLabel('%s:' % _("Feedrate X-Y"))
         self.frxy_label.setToolTip(
            _("Feedrate (speed) while moving on the X-Y plane.")
         )
@@ -221,7 +221,7 @@ class SolderPaste(FlatCAMTool):
 
         # Feedrate Z
         self.frz_entry = FCEntry()
-        self.frz_label = QtWidgets.QLabel(_("Feedrate Z:"))
+        self.frz_label = QtWidgets.QLabel('%s:' % _("Feedrate Z"))
         self.frz_label.setToolTip(
             _("Feedrate (speed) while moving vertically\n"
               "(on Z plane).")
@@ -230,7 +230,7 @@ class SolderPaste(FlatCAMTool):
 
         # Feedrate Z Dispense
         self.frz_dispense_entry = FCEntry()
-        self.frz_dispense_label = QtWidgets.QLabel(_("Feedrate Z Dispense:"))
+        self.frz_dispense_label = QtWidgets.QLabel('%s:' % _("Feedrate Z Dispense"))
         self.frz_dispense_label.setToolTip(
            _("Feedrate (speed) while moving up vertically\n"
              " to Dispense position (on Z plane).")
@@ -239,7 +239,7 @@ class SolderPaste(FlatCAMTool):
 
         # Spindle Speed Forward
         self.speedfwd_entry = FCEntry()
-        self.speedfwd_label = QtWidgets.QLabel(_("Spindle Speed FWD:"))
+        self.speedfwd_label = QtWidgets.QLabel('%s:' % _("Spindle Speed FWD"))
         self.speedfwd_label.setToolTip(
            _("The dispenser speed while pushing solder paste\n"
              "through the dispenser nozzle.")
@@ -248,7 +248,7 @@ class SolderPaste(FlatCAMTool):
 
         # Dwell Forward
         self.dwellfwd_entry = FCEntry()
-        self.dwellfwd_label = QtWidgets.QLabel(_("Dwell FWD:"))
+        self.dwellfwd_label = QtWidgets.QLabel('%s:' % _("Dwell FWD"))
         self.dwellfwd_label.setToolTip(
             _("Pause after solder dispensing.")
         )
@@ -256,7 +256,7 @@ class SolderPaste(FlatCAMTool):
 
         # Spindle Speed Reverse
         self.speedrev_entry = FCEntry()
-        self.speedrev_label = QtWidgets.QLabel(_("Spindle Speed REV:"))
+        self.speedrev_label = QtWidgets.QLabel('%s:' % _("Spindle Speed REV"))
         self.speedrev_label.setToolTip(
            _("The dispenser speed while retracting solder paste\n"
              "through the dispenser nozzle.")
@@ -265,7 +265,7 @@ class SolderPaste(FlatCAMTool):
 
         # Dwell Reverse
         self.dwellrev_entry = FCEntry()
-        self.dwellrev_label = QtWidgets.QLabel(_("Dwell REV:"))
+        self.dwellrev_label = QtWidgets.QLabel('%s:' % _("Dwell REV"))
         self.dwellrev_label.setToolTip(
             _("Pause after solder paste dispenser retracted,\n"
               "to allow pressure equilibrium.")
@@ -273,7 +273,7 @@ class SolderPaste(FlatCAMTool):
         self.gcode_form_layout.addRow(self.dwellrev_label, self.dwellrev_entry)
 
         # Postprocessors
-        pp_label = QtWidgets.QLabel(_('PostProcessors:'))
+        pp_label = QtWidgets.QLabel('%s:' % _('PostProcessor'))
         pp_label.setToolTip(
             _("Files that control the GCode generation.")
         )
@@ -303,7 +303,7 @@ class SolderPaste(FlatCAMTool):
         grid2 = QtWidgets.QGridLayout()
         self.generation_box.addLayout(grid2)
 
-        step2_lbl = QtWidgets.QLabel("<b>%s</b>" % _('STEP 2:'))
+        step2_lbl = QtWidgets.QLabel("<b>%s:</b>" % _('STEP 2'))
         step2_lbl.setToolTip(
             _("Second step is to create a solder paste dispensing\n"
               "geometry out of an Solder Paste Mask Gerber file.")
@@ -321,7 +321,7 @@ class SolderPaste(FlatCAMTool):
         self.geo_obj_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
         self.geo_obj_combo.setCurrentIndex(1)
 
-        self.geo_object_label = QtWidgets.QLabel(_("Geo Result:"))
+        self.geo_object_label = QtWidgets.QLabel('%s:' % _("Geo Result"))
         self.geo_object_label.setToolTip(
            _("Geometry Solder Paste object.\n"
              "The name of the object has to end in:\n"
@@ -332,7 +332,7 @@ class SolderPaste(FlatCAMTool):
         grid3 = QtWidgets.QGridLayout()
         self.generation_box.addLayout(grid3)
 
-        step3_lbl = QtWidgets.QLabel("<b>%s</b>" % _('STEP 3:'))
+        step3_lbl = QtWidgets.QLabel("<b>%s:</b>" % _('STEP 3'))
         step3_lbl.setToolTip(
            _("Third step is to select a solder paste dispensing geometry,\n"
              "and then generate a CNCJob object.\n\n"
@@ -354,7 +354,7 @@ class SolderPaste(FlatCAMTool):
         self.cnc_obj_combo.setRootModelIndex(self.app.collection.index(3, 0, QtCore.QModelIndex()))
         self.cnc_obj_combo.setCurrentIndex(1)
 
-        self.cnc_object_label = QtWidgets.QLabel(_("CNC Result:"))
+        self.cnc_object_label = QtWidgets.QLabel('%s:' % _("CNC Result"))
         self.cnc_object_label.setToolTip(
            _("CNCJob Solder paste object.\n"
              "In order to enable the GCode save section,\n"
@@ -378,7 +378,7 @@ class SolderPaste(FlatCAMTool):
              "on PCB pads, to a file.")
         )
 
-        step4_lbl = QtWidgets.QLabel("<b>%s</b>" % _('STEP 4:'))
+        step4_lbl = QtWidgets.QLabel("<b>%s:</b>" % _('STEP 4'))
         step4_lbl.setToolTip(
            _("Fourth step (and last) is to select a CNCJob made from \n"
              "a solder paste dispensing geometry, and then view/save it's GCode.")
@@ -436,7 +436,12 @@ class SolderPaste(FlatCAMTool):
             else:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        self.app.ui.splitter.setSizes([0, 1])
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
                 except AttributeError:
                     pass
         else:

+ 116 - 37
flatcamTools/ToolSub.py

@@ -23,6 +23,8 @@ if '_' not in builtins.__dict__:
 
 class ToolSub(FlatCAMTool):
 
+    job_finished = QtCore.pyqtSignal(bool)
+
     toolName = _("Substract Tool")
 
     def __init__(self, app):
@@ -52,7 +54,7 @@ class ToolSub(FlatCAMTool):
         form_layout = QtWidgets.QFormLayout()
         self.tools_box.addLayout(form_layout)
 
-        self.gerber_title = QtWidgets.QLabel(_("<b>Gerber Objects</b>"))
+        self.gerber_title = QtWidgets.QLabel("<b>%s</b>" % _("Gerber Objects"))
         form_layout.addRow(self.gerber_title)
 
         # Target Gerber Object
@@ -61,7 +63,7 @@ class ToolSub(FlatCAMTool):
         self.target_gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.target_gerber_combo.setCurrentIndex(1)
 
-        self.target_gerber_label = QtWidgets.QLabel(_("Target:"))
+        self.target_gerber_label = QtWidgets.QLabel('%s:' % _("Target"))
         self.target_gerber_label.setToolTip(
             _("Gerber object from which to substract\n"
               "the substractor Gerber object.")
@@ -75,7 +77,7 @@ class ToolSub(FlatCAMTool):
         self.sub_gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.sub_gerber_combo.setCurrentIndex(1)
 
-        self.sub_gerber_label = QtWidgets.QLabel(_("Substractor:"))
+        self.sub_gerber_label = QtWidgets.QLabel('%s:' % _("Substractor"))
         self.sub_gerber_label.setToolTip(
             _("Gerber object that will be substracted\n"
               "from the target Gerber object.")
@@ -98,7 +100,7 @@ class ToolSub(FlatCAMTool):
         form_geo_layout = QtWidgets.QFormLayout()
         self.tools_box.addLayout(form_geo_layout)
 
-        self.geo_title = QtWidgets.QLabel(_("<b>Geometry Objects</b>"))
+        self.geo_title = QtWidgets.QLabel("<b>%s</b>" % _("Geometry Objects"))
         form_geo_layout.addRow(self.geo_title)
 
         # Target Geometry Object
@@ -107,7 +109,7 @@ class ToolSub(FlatCAMTool):
         self.target_geo_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
         self.target_geo_combo.setCurrentIndex(1)
 
-        self.target_geo_label = QtWidgets.QLabel(_("Target:"))
+        self.target_geo_label = QtWidgets.QLabel('%s:' % _("Target"))
         self.target_geo_label.setToolTip(
             _("Geometry object from which to substract\n"
               "the substractor Geometry object.")
@@ -121,7 +123,7 @@ class ToolSub(FlatCAMTool):
         self.sub_geo_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
         self.sub_geo_combo.setCurrentIndex(1)
 
-        self.sub_geo_label = QtWidgets.QLabel(_("Substractor:"))
+        self.sub_geo_label = QtWidgets.QLabel('%s:' % _("Substractor"))
         self.sub_geo_label.setToolTip(
             _("Geometry object that will be substracted\n"
               "from the target Geometry object.")
@@ -130,6 +132,10 @@ class ToolSub(FlatCAMTool):
 
         form_geo_layout.addRow(self.sub_geo_label, self.sub_geo_combo)
 
+        self.close_paths_cb = FCCheckBox(_("Close paths"))
+        self.close_paths_cb.setToolTip(_("Checking this will close the paths cut by the Geometry substractor object."))
+        self.tools_box.addWidget(self.close_paths_cb)
+
         self.intersect_geo_btn = FCButton(_('Substract Geometry'))
         self.intersect_geo_btn.setToolTip(
             _("Will remove the area occupied by the substractor\n"
@@ -184,6 +190,7 @@ class ToolSub(FlatCAMTool):
         except (TypeError, AttributeError):
             pass
         self.intersect_geo_btn.clicked.connect(self.on_geo_intersection_click)
+        self.job_finished.connect(self.on_job_finished)
 
     def install(self, icon=None, separator=None, **kwargs):
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+W', **kwargs)
@@ -198,7 +205,12 @@ class ToolSub(FlatCAMTool):
             else:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        self.app.ui.splitter.setSizes([0, 1])
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
                 except AttributeError:
                     pass
         else:
@@ -217,6 +229,7 @@ class ToolSub(FlatCAMTool):
 
     def set_tool_ui(self):
         self.tools_frame.show()
+        self.close_paths_cb.setChecked(self.app.defaults["tools_sub_close_paths"])
 
     def on_grb_intersection_click(self):
         # reset previous values
@@ -231,7 +244,7 @@ class ToolSub(FlatCAMTool):
             self.app.inform.emit(_("[ERROR_NOTCL] No Target object loaded."))
             return
 
-        # Get source object.
+        # Get target object.
         try:
             self.target_grb_obj = self.app.collection.get_by_name(self.target_grb_obj_name)
         except Exception as e:
@@ -244,7 +257,7 @@ class ToolSub(FlatCAMTool):
             self.app.inform.emit(_("[ERROR_NOTCL] No Substractor object loaded."))
             return
 
-        # Get source object.
+        # Get substractor object.
         try:
             self.sub_grb_obj = self.app.collection.get_by_name(self.sub_grb_obj_name)
         except Exception as e:
@@ -281,7 +294,7 @@ class ToolSub(FlatCAMTool):
         for apid in self.target_grb_obj.apertures:
             self.promises.append(apid)
 
-        # start the QTimer to check for promises with 1 second period check
+        # start the QTimer to check for promises with 0.5 second period check
         self.periodic_check(500, reset=True)
 
         for apid in self.target_grb_obj.apertures:
@@ -408,7 +421,10 @@ class ToolSub(FlatCAMTool):
             # cleanup
             self.new_apertures.clear()
             self.new_solid_geometry[:] = []
-            self.sub_union[:] = []
+            try:
+                self.sub_union[:] = []
+            except TypeError:
+                self.sub_union = []
 
     def on_geo_intersection_click(self):
         # reset previous values
@@ -424,7 +440,7 @@ class ToolSub(FlatCAMTool):
             self.app.inform.emit(_("[ERROR_NOTCL] No Target object loaded."))
             return
 
-        # Get source object.
+        # Get target object.
         try:
             self.target_geo_obj = self.app.collection.get_by_name(self.target_geo_obj_name)
         except Exception as e:
@@ -437,7 +453,7 @@ class ToolSub(FlatCAMTool):
             self.app.inform.emit(_("[ERROR_NOTCL] No Substractor object loaded."))
             return
 
-        # Get source object.
+        # Get substractor object.
         try:
             self.sub_geo_obj = self.app.collection.get_by_name(self.sub_geo_obj_name)
         except Exception as e:
@@ -450,14 +466,14 @@ class ToolSub(FlatCAMTool):
             return
 
         # create the target_options obj
-        self.target_options = {}
-        for opt in self.target_geo_obj.options:
-            if opt != 'name':
-                self.target_options[opt] = deepcopy(self.target_geo_obj.options[opt])
+        # self.target_options = dict()
+        # for k, v in self.target_geo_obj.options.items():
+        #     if k != 'name':
+        #         self.target_options[k] = v
 
         # crate the new_tools dict structure
         for tool in self.target_geo_obj.tools:
-            self.new_tools[tool] = {}
+            self.new_tools[tool] = dict()
             for key in self.target_geo_obj.tools[tool]:
                 if key == 'solid_geometry':
                     self.new_tools[tool][key] = []
@@ -496,10 +512,53 @@ class ToolSub(FlatCAMTool):
             text = _("Parsing tool %s geometry ...") % str(tool)
 
         with self.app.proc_container.new(text):
-            new_geo = (cascaded_union(geo)).difference(self.sub_union)
-            if new_geo:
-                if not new_geo.is_empty:
-                    new_geometry.append(new_geo)
+            # resulting paths are closed resulting into Polygons
+            if self.close_paths_cb.isChecked():
+                new_geo = (cascaded_union(geo)).difference(self.sub_union)
+                if new_geo:
+                    if not new_geo.is_empty:
+                        new_geometry.append(new_geo)
+            # resulting paths are unclosed resulting in a multitude of rings
+            else:
+                try:
+                    for geo_elem in geo:
+                        if isinstance(geo_elem, Polygon):
+                            for ring in self.poly2rings(geo_elem):
+                                new_geo = ring.difference(self.sub_union)
+                                if new_geo and not new_geo.is_empty:
+                                    new_geometry.append(new_geo)
+                        elif isinstance(geo_elem, MultiPolygon):
+                            for poly in geo_elem:
+                                for ring in self.poly2rings(poly):
+                                    new_geo = ring.difference(self.sub_union)
+                                    if new_geo and not new_geo.is_empty:
+                                        new_geometry.append(new_geo)
+                        elif isinstance(geo_elem, LineString):
+                            new_geo = geo_elem.difference(self.sub_union)
+                            if new_geo:
+                                if not new_geo.is_empty:
+                                    new_geometry.append(new_geo)
+                        elif isinstance(geo_elem, MultiLineString):
+                            for line_elem in geo_elem:
+                                new_geo = line_elem.difference(self.sub_union)
+                                if new_geo and not new_geo.is_empty:
+                                    new_geometry.append(new_geo)
+                except TypeError:
+                    if isinstance(geo, Polygon):
+                        for ring in self.poly2rings(geo):
+                            new_geo = ring.difference(self.sub_union)
+                            if new_geo:
+                                if not new_geo.is_empty:
+                                    new_geometry.append(new_geo)
+                    elif isinstance(geo, LineString):
+                        new_geo = geo.difference(self.sub_union)
+                        if new_geo and not new_geo.is_empty:
+                            new_geometry.append(new_geo)
+                    elif isinstance(geo, MultiLineString):
+                        for line_elem in geo:
+                            new_geo = line_elem.difference(self.sub_union)
+                            if new_geo and not new_geo.is_empty:
+                                new_geometry.append(new_geo)
 
         if new_geometry:
             if tool == "single":
@@ -522,10 +581,14 @@ class ToolSub(FlatCAMTool):
         log.debug("Promise fulfilled: %s" % str(tool))
 
     def new_geo_object(self, outname):
+        geo_name = outname
         def obj_init(geo_obj, app_obj):
 
-            geo_obj.options = deepcopy(self.target_options)
-            geo_obj.options['name'] = outname
+            # geo_obj.options = self.target_options
+            # create the target_options obj
+            for k, v in self.target_geo_obj.options.items():
+                geo_obj.options[k] = v
+            geo_obj.options['name'] = geo_name
 
             if self.target_geo_obj.multigeo:
                 geo_obj.tools = deepcopy(self.new_tools)
@@ -540,6 +603,7 @@ class ToolSub(FlatCAMTool):
                         geo_obj.tools[tool]['solid_geometry'] = deepcopy(self.new_solid_geometry)
                 except Exception as e:
                     log.debug("ToolSub.new_geo_object() --> %s" % str(e))
+                geo_obj.multigeo = False
 
         with self.app.proc_container.new(_("Generating new object ...")):
             ret = self.app.new_object('geometry', outname, obj_init, autoselected=False)
@@ -554,7 +618,7 @@ class ToolSub(FlatCAMTool):
             # cleanup
             self.new_tools.clear()
             self.new_solid_geometry[:] = []
-            self.sub_union[:] = []
+            self.sub_union = []
 
     def periodic_check(self, check_period, reset=False):
         """
@@ -592,27 +656,39 @@ class ToolSub(FlatCAMTool):
         try:
             if not self.promises:
                 self.check_thread.stop()
-                if self.sub_type == "gerber":
-                    outname = self.target_gerber_combo.currentText() + '_sub'
-
-                    # intersection jobs finished, start the creation of solid_geometry
-                    self.app.worker_task.emit({'fcn': self.new_gerber_object,
-                                               'params': [outname]})
-                else:
-                    outname = self.target_geo_combo.currentText() + '_sub'
-
-                    # intersection jobs finished, start the creation of solid_geometry
-                    self.app.worker_task.emit({'fcn': self.new_geo_object,
-                                               'params': [outname]})
+                self.job_finished.emit(True)
 
                 # reset the type of substraction for next time
                 self.sub_type = None
 
                 log.debug("ToolSub --> Periodic check finished.")
         except Exception as e:
+            self.job_finished.emit(False)
             log.debug("ToolSub().periodic_check_handler() --> %s" % str(e))
             traceback.print_exc()
 
+    def on_job_finished(self, succcess):
+        """
+
+        :param succcess: boolean, this parameter signal if all the apertures were processed
+        :return: None
+        """
+        if succcess is True:
+            if self.sub_type == "gerber":
+                outname = self.target_gerber_combo.currentText() + '_sub'
+
+                # intersection jobs finished, start the creation of solid_geometry
+                self.app.worker_task.emit({'fcn': self.new_gerber_object,
+                                           'params': [outname]})
+            else:
+                outname = self.target_geo_combo.currentText() + '_sub'
+
+                # intersection jobs finished, start the creation of solid_geometry
+                self.app.worker_task.emit({'fcn': self.new_geo_object,
+                                           'params': [outname]})
+        else:
+            self.app.inform.emit(_('[ERROR_NOTCL] Generating new object failed.'))
+
     def reset_fields(self):
         self.target_gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.sub_gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
@@ -620,4 +696,7 @@ class ToolSub(FlatCAMTool):
         self.target_geo_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
         self.sub_geo_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
 
+    @staticmethod
+    def poly2rings(poly):
+        return [poly.exterior] + [interior for interior in poly.interiors]
 # end of file

+ 22 - 16
flatcamTools/ToolTransform.py

@@ -65,7 +65,7 @@ class ToolTransform(FlatCAMTool):
         self.transform_lay.addLayout(form_layout)
         form_child = QtWidgets.QHBoxLayout()
 
-        self.rotate_label = QtWidgets.QLabel(_("Angle:"))
+        self.rotate_label = QtWidgets.QLabel('%s:' % _("Angle"))
         self.rotate_label.setToolTip(
             _("Angle for Rotation action, in degrees.\n"
               "Float number between -360 and 359.\n"
@@ -104,7 +104,7 @@ class ToolTransform(FlatCAMTool):
         form1_child_1 = QtWidgets.QHBoxLayout()
         form1_child_2 = QtWidgets.QHBoxLayout()
 
-        self.skewx_label = QtWidgets.QLabel(_("Angle X:"))
+        self.skewx_label = QtWidgets.QLabel('%s:' % _("Skew_X angle"))
         self.skewx_label.setToolTip(
             _("Angle for Skew action, in degrees.\n"
               "Float number between -360 and 359.")
@@ -122,7 +122,7 @@ class ToolTransform(FlatCAMTool):
               "the bounding box for all selected objects."))
         self.skewx_button.setMinimumWidth(90)
 
-        self.skewy_label = QtWidgets.QLabel(_("Angle Y:"))
+        self.skewy_label = QtWidgets.QLabel('%s:' % _("Skew_Y angle"))
         self.skewy_label.setToolTip(
             _("Angle for Skew action, in degrees.\n"
               "Float number between -360 and 359.")
@@ -161,9 +161,9 @@ class ToolTransform(FlatCAMTool):
         form2_child_1 = QtWidgets.QHBoxLayout()
         form2_child_2 = QtWidgets.QHBoxLayout()
 
-        self.scalex_label = QtWidgets.QLabel(_("Factor X:"))
+        self.scalex_label = QtWidgets.QLabel('%s:' % _("Scale_X factor"))
         self.scalex_label.setToolTip(
-            _("Factor for Scale action over X axis.")
+            _("Factor for scaling on X axis.")
         )
         self.scalex_label.setMinimumWidth(70)
         self.scalex_entry = FCEntry()
@@ -178,9 +178,9 @@ class ToolTransform(FlatCAMTool):
               "the Scale reference checkbox state."))
         self.scalex_button.setMinimumWidth(90)
 
-        self.scaley_label = QtWidgets.QLabel(_("Factor Y:"))
+        self.scaley_label = QtWidgets.QLabel('%s:' % _("Scale_Y factor"))
         self.scaley_label.setToolTip(
-            _("Factor for Scale action over Y axis.")
+            _("Factor for scaling on Y axis.")
         )
         self.scaley_label.setMinimumWidth(70)
         self.scaley_entry = FCEntry()
@@ -200,12 +200,13 @@ class ToolTransform(FlatCAMTool):
         self.scale_link_cb.setText(_("Link"))
         self.scale_link_cb.setToolTip(
             _("Scale the selected object(s)\n"
-              "using the Scale Factor X for both axis."))
+              "using the Scale_X factor for both axis.")
+        )
         self.scale_link_cb.setMinimumWidth(70)
 
         self.scale_zero_ref_cb = FCCheckBox()
         self.scale_zero_ref_cb.set_value(True)
-        self.scale_zero_ref_cb.setText(_("Scale Reference"))
+        self.scale_zero_ref_cb.setText('%s' % _("Scale Reference"))
         self.scale_zero_ref_cb.setToolTip(
             _("Scale the selected object(s)\n"
               "using the origin reference when checked,\n"
@@ -235,9 +236,9 @@ class ToolTransform(FlatCAMTool):
         form3_child_1 = QtWidgets.QHBoxLayout()
         form3_child_2 = QtWidgets.QHBoxLayout()
 
-        self.offx_label = QtWidgets.QLabel(_("Value X:"))
+        self.offx_label = QtWidgets.QLabel('%s:' % _("Offset_X val"))
         self.offx_label.setToolTip(
-            _("Value for Offset action on X axis.")
+            _("Distance to offset on X axis. In current units.")
         )
         self.offx_label.setMinimumWidth(70)
         self.offx_entry = FCEntry()
@@ -252,9 +253,9 @@ class ToolTransform(FlatCAMTool):
               "the bounding box for all selected objects.\n"))
         self.offx_button.setMinimumWidth(90)
 
-        self.offy_label = QtWidgets.QLabel(_("Value Y:"))
+        self.offy_label = QtWidgets.QLabel('%s:' % _("Offset_Y val"))
         self.offy_label.setToolTip(
-            _("Value for Offset action on Y axis.")
+            _("Distance to offset on Y axis. In current units.")
         )
         self.offy_label.setMinimumWidth(70)
         self.offy_entry = FCEntry()
@@ -309,7 +310,7 @@ class ToolTransform(FlatCAMTool):
 
         self.flip_ref_cb = FCCheckBox()
         self.flip_ref_cb.set_value(True)
-        self.flip_ref_cb.setText(_("Ref Pt"))
+        self.flip_ref_cb.setText('%s' % _("Mirror Reference"))
         self.flip_ref_cb.setToolTip(
             _("Flip the selected object(s)\n"
               "around the point in Point Entry Field.\n"
@@ -322,7 +323,7 @@ class ToolTransform(FlatCAMTool):
               "Point Entry field and click Flip on X(Y)"))
         self.flip_ref_cb.setMinimumWidth(70)
 
-        self.flip_ref_label = QtWidgets.QLabel(_("Point:"))
+        self.flip_ref_label = QtWidgets.QLabel('%s:' % _(" Mirror Ref. Point"))
         self.flip_ref_label.setToolTip(
             _("Coordinates in format (x, y) used as reference for mirroring.\n"
               "The 'x' in (x, y) will be used when using Flip on X and\n"
@@ -384,7 +385,12 @@ class ToolTransform(FlatCAMTool):
             else:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
-                        self.app.ui.splitter.setSizes([0, 1])
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
                 except AttributeError:
                     pass
         else:

BIN
locale/de/LC_MESSAGES/strings.mo


File diff suppressed because it is too large
+ 222 - 195
locale/de/LC_MESSAGES/strings.po


BIN
locale/en/LC_MESSAGES/strings.mo


File diff suppressed because it is too large
+ 228 - 201
locale/en/LC_MESSAGES/strings.po


BIN
locale/es/LC_MESSAGES/strings.mo


File diff suppressed because it is too large
+ 222 - 195
locale/es/LC_MESSAGES/strings.po


BIN
locale/pt_BR/LC_MESSAGES/strings.mo


File diff suppressed because it is too large
+ 228 - 201
locale/pt_BR/LC_MESSAGES/strings.po


BIN
locale/ro/LC_MESSAGES/strings.mo


File diff suppressed because it is too large
+ 228 - 201
locale/ro/LC_MESSAGES/strings.po


BIN
locale/ru/LC_MESSAGES/strings.mo


File diff suppressed because it is too large
+ 227 - 200
locale/ru/LC_MESSAGES/strings.po


File diff suppressed because it is too large
+ 282 - 249
locale_template/strings.pot


+ 2 - 0
make_win.py

@@ -55,6 +55,8 @@ if platform.architecture()[0] == '64bit':
 include_files.append(("locale", "lib/locale"))
 include_files.append(("postprocessors", "lib/postprocessors"))
 include_files.append(("share", "lib/share"))
+include_files.append(("flatcamGUI/VisPyData", "lib/vispy"))
+include_files.append(("config", "lib/config"))
 
 include_files.append(("README.md", "README.md"))
 include_files.append(("LICENSE", "LICENSE"))

BIN
share/aero_arc.png


BIN
share/aero_array.png


BIN
share/aero_buffer.png


BIN
share/aero_circle.png


BIN
share/aero_circle_geo.png


BIN
share/aero_disc.png


BIN
share/aero_drill.png


BIN
share/aero_drill_array.png


BIN
share/aero_path1.png


BIN
share/aero_path2.png


BIN
share/aero_path3.png


BIN
share/aero_path4.png


BIN
share/aero_path5.png


BIN
share/aero_semidisc.png


BIN
share/aero_slot.png


BIN
share/aero_text.png


BIN
share/backup24.png


BIN
share/backup_export24.png


BIN
share/backup_import24.png


BIN
share/slot26.png


BIN
share/slot_array26.png


+ 2 - 3
tclCommands/TclCommand.py

@@ -202,7 +202,6 @@ class TclCommand(object):
         """
 
         arguments, options = self.parse_arguments(args)
-
         named_args = {}
         unnamed_args = []
 
@@ -274,7 +273,7 @@ class TclCommand(object):
         :return: None, output text or exception
         """
 
-        #self.worker_task.emit({'fcn': self.exec_command_test, 'params': [text, False]})
+        # self.worker_task.emit({'fcn': self.exec_command_test, 'params': [text, False]})
 
         try:
             self.log.debug("TCL command '%s' executed." % str(self.__class__))
@@ -283,7 +282,7 @@ class TclCommand(object):
             return self.execute(args, unnamed_args)
         except Exception as unknown:
             error_info = sys.exc_info()
-            self.log.error("TCL command '%s' failed." % str(self))
+            self.log.error("TCL command '%s' failed. Error text: %s" % (str(self), str(unknown)))
             self.app.display_tcl_error(unknown, error_info)
             self.raise_tcl_unknown_error(unknown)
 

+ 2 - 1
tclCommands/TclCommandAddPolygon.py

@@ -55,7 +55,8 @@ class TclCommandAddPolygon(TclCommandSignaled):
         if len(unnamed_args) % 2 != 0:
             self.raise_tcl_error("Incomplete coordinates.")
 
-        points = [[float(unnamed_args[2*i]), float(unnamed_args[2*i+1])] for i in range(len(unnamed_args)/2)]
+        nr_points = int(len(unnamed_args) / 2)
+        points = [[float(unnamed_args[2*i]), float(unnamed_args[2*i+1])] for i in range(nr_points)]
 
         obj.add_polygon(points)
         obj.plot()

+ 2 - 1
tclCommands/TclCommandAddPolyline.py

@@ -55,7 +55,8 @@ class TclCommandAddPolyline(TclCommandSignaled):
         if len(unnamed_args) % 2 != 0:
             self.raise_tcl_error("Incomplete coordinates.")
 
-        points = [[float(unnamed_args[2*i]), float(unnamed_args[2*i+1])] for i in range(len(unnamed_args)/2)]
+        nr_points = int(len(unnamed_args) / 2)
+        points = [[float(unnamed_args[2*i]), float(unnamed_args[2*i+1])] for i in range(nr_points)]
 
         obj.add_polyline(points)
         obj.plot()

+ 95 - 0
tclCommands/TclCommandBbox.py

@@ -0,0 +1,95 @@
+from ObjectCollection import *
+from tclCommands.TclCommand import TclCommand
+import gettext
+import FlatCAMTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class TclCommandBbox(TclCommand):
+    """
+    Tcl shell command to follow a Gerber file
+    """
+
+    # array of all command aliases, to be able use  old names for backward compatibility (add_poly, add_polygon)
+    aliases = ['bounding_box', 'bbox']
+
+    # dictionary of types from Tcl command, needs to be ordered
+    arg_names = collections.OrderedDict([
+        ('name', str)
+    ])
+
+    # dictionary of types from Tcl command, needs to be ordered , this  is  for options  like -optionname value
+    option_types = collections.OrderedDict([
+        ('outname', str),
+        ('margin', float),
+        ('rounded', bool)
+    ])
+
+    # array of mandatory options for current Tcl command: required = {'name','outname'}
+    required = ['name']
+
+    # structured help for current command, args needs to be ordered
+    help = {
+        'main': "Creates a Geometry object that surrounds the object.",
+        'args': collections.OrderedDict([
+            ('name', 'Object name for which to create bounding box. String'),
+            ('outname', 'Name of the resulting Geometry object. String.'),
+            ('margin', "Distance of the edges of the box to the nearest polygon."
+                       "Float number."),
+            ('rounded', "If the bounding box is to have rounded corners their radius is equal to the margin. "
+                        "True or False.")
+        ]),
+        'examples': ['bbox name -outname name_bbox']
+    }
+
+    def execute(self, args, unnamed_args):
+        """
+        execute current TCL shell command
+
+        :param args: array of known named arguments and options
+        :param unnamed_args: array of other values which were passed into command
+            without -somename and  we do not have them in known arg_names
+        :return: None or exception
+        """
+
+        name = args['name']
+
+        if 'outname' not in args:
+            args['outname'] = name + "_bbox"
+
+        obj = self.app.collection.get_by_name(name)
+        if obj is None:
+            self.raise_tcl_error("%s: %s" % (_("Object not found"), name))
+
+        if not isinstance(obj, FlatCAMGerber) and not isinstance(obj, FlatCAMGeometry):
+            self.raise_tcl_error('%s %s: %s.' % (
+                _("Expected FlatCAMGerber or FlatCAMGeometry, got"), name, type(obj)))
+
+        if 'margin' not in args:
+            args['margin'] = float(self.app.defaults["gerber_bboxmargin"])
+        margin = args['margin']
+
+        if 'rounded' not in args:
+            args['rounded'] = self.app.defaults["gerber_bboxrounded"]
+        rounded = args['rounded']
+
+        del args['name']
+
+        try:
+            def geo_init(geo_obj, app_obj):
+                assert isinstance(geo_obj, FlatCAMGeometry)
+
+                # Bounding box with rounded corners
+                geo = cascaded_union(obj.solid_geometry)
+                bounding_box = geo.envelope.buffer(float(margin))
+                if not rounded:  # Remove rounded corners
+                    bounding_box = bounding_box.envelope
+                geo_obj.solid_geometry = bounding_box
+
+            self.app.new_object("geometry", args['outname'], geo_init)
+        except Exception as e:
+            return "Operation failed: %s" % str(e)

+ 1 - 1
tclCommands/TclCommandClearShell.py

@@ -4,7 +4,7 @@ from ObjectCollection import *
 
 class TclCommandClearShell(TclCommand):
     """
-    Tcl shell command to creates a circle in the given Geometry object.
+    Tcl shell command to clear the text in the Tcl Shell browser.
 
     example:
 

+ 5 - 2
tclCommands/TclCommandCncjob.py

@@ -24,15 +24,18 @@ class TclCommandCncjob(TclCommandSignaled):
 
     # dictionary of types from Tcl command, needs to be ordered , this  is  for options  like -optionname value
     option_types = collections.OrderedDict([
+        ('tooldia', float),
         ('z_cut', float),
         ('z_move', float),
         ('feedrate', float),
         ('feedrate_rapid', float),
-        ('tooldia', float),
         ('spindlespeed', int),
         ('multidepth', bool),
         ('extracut', bool),
         ('depthperpass', float),
+        ('toolchange', int),
+        ('toolchangez', float),
+        ('toolchangexy', tuple),
         ('endz', float),
         ('ppname_g', str),
         ('outname', str)
@@ -62,7 +65,7 @@ class TclCommandCncjob(TclCommandSignaled):
             ('outname', 'Name of the resulting Geometry object.'),
             ('ppname_g', 'Name of the Geometry postprocessor. No quotes, case sensitive')
         ]),
-        'examples': []
+        'examples': ['cncjob geo_name -tooldia 0.5 -z_cut -1.7 -z_move 2 -feedrate 120 -ppname_g default']
     }
 
     def execute(self, args, unnamed_args):

+ 266 - 0
tclCommands/TclCommandCopperClear.py

@@ -0,0 +1,266 @@
+from ObjectCollection import *
+from tclCommands.TclCommand import TclCommand
+
+import gettext
+import FlatCAMTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class TclCommandCopperClear(TclCommand):
+    """
+    Clear the non-copper areas.
+    """
+
+    # Array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
+    aliases = ['ncc_clear', 'ncc']
+
+    # dictionary of types from Tcl command, needs to be ordered
+    arg_names = collections.OrderedDict([
+        ('name', str),
+    ])
+
+    # dictionary of types from Tcl command, needs to be ordered , this  is  for options  like -optionname value
+    option_types = collections.OrderedDict([
+        ('tooldia', str),
+        ('overlap', float),
+        ('order', str),
+        ('margin', float),
+        ('method', str),
+        ('connect', bool),
+        ('contour', bool),
+        ('has_offset', bool),
+        ('offset', float),
+        ('rest', bool),
+        ('all', int),
+        ('ref', int),
+        ('box', str),
+        ('outname', str),
+    ])
+
+    # array of mandatory options for current Tcl command: required = {'name','outname'}
+    required = ['name']
+
+    # structured help for current command, args needs to be ordered
+    help = {
+        'main': "Clear excess copper in polygons. Basically it's a negative Paint.",
+        'args': collections.OrderedDict([
+            ('name', 'Name of the source Geometry object. String.'),
+            ('tooldia', 'Diameter of the tool to be used. Can be a comma separated list of diameters. No space is '
+                        'allowed between tool diameters. E.g: correct: 0.5,1 / incorrect: 0.5, 1'),
+            ('overlap', 'Fraction of the tool diameter to overlap cuts. Float number.'),
+            ('margin', 'Bounding box margin. Float number.'),
+            ('order', 'Can have the values: "no", "fwd" and "rev". String.'
+                      'It is useful when there are multiple tools in tooldia parameter.'
+                      '"no" -> the order used is the one provided.'
+                      '"fwd" -> tools are ordered from smallest to biggest.'
+                      '"rev" -> tools are ordered from biggest to smallest.'),
+            ('method', 'Algorithm for copper clearing. Can be: "standard", "seed" or "lines".'),
+            ('connect', 'Draw lines to minimize tool lifts. True or False'),
+            ('contour', 'Cut around the perimeter of the painting. True or False'),
+            ('rest', 'Use rest-machining. True or False'),
+            ('has_offset', 'The offset will used only if this is set True or present in args. True or False.'),
+            ('offset', 'The copper clearing will finish to a distance from copper features. Float number.'),
+            ('all', 'Will copper clear the whole object. 1 = enabled, anything else = disabled'),
+            ('ref', 'Will clear of extra copper all polygons within a specified object with the name in "box" '
+                    'parameter. 1 = enabled, anything else = disabled'),
+            ('box', 'Name of the object to be used as reference. Required when selecting "ref" = 1. String.'),
+            ('outname', 'Name of the resulting Geometry object. String.'),
+        ]),
+        'examples': []
+    }
+
+    def execute(self, args, unnamed_args):
+        """
+        execute current TCL shell command
+
+        :param args: array of known named arguments and options
+        :param unnamed_args: array of other values which were passed into command
+            without -somename and  we do not have them in known arg_names
+        :return: None or exception
+        """
+
+        name = args['name']
+
+        if 'tooldia' in args:
+            tooldia = str(args['tooldia'])
+        else:
+            tooldia = self.app.defaults["tools_ncctools"]
+
+        if 'overlap' in args:
+            overlap = float(args['overlap'])
+        else:
+            overlap = float(self.app.defaults["tools_nccoverlap"])
+
+        if 'order' in args:
+            order = args['order']
+        else:
+            order = str(self.app.defaults["tools_nccorder"])
+
+        if 'margin' in args:
+            margin = float(args['margin'])
+        else:
+            margin = float(self.app.defaults["tools_nccmargin"])
+
+        if 'method' in args:
+            method = args['method']
+        else:
+            method = str(self.app.defaults["tools_nccmethod"])
+
+        if 'connect' in args:
+            connect = eval(str(args['connect']).capitalize())
+        else:
+            connect = eval(str(self.app.defaults["tools_nccconnect"]))
+
+        if 'contour' in args:
+            contour = eval(str(args['contour']).capitalize())
+        else:
+            contour = eval(str(self.app.defaults["tools_ncccontour"]))
+
+        offset = 0.0
+        if 'has_offset' in args:
+            has_offset = args['has_offset']
+            if args['has_offset'] is True:
+                if 'offset' in args:
+                    offset = float(args['margin'])
+                else:
+                    # 'offset' has to be in args if 'has_offset' is and it is set True
+                    self.raise_tcl_error("%s: %s" % (_("Could not retrieve object"), name))
+        else:
+            has_offset = False
+
+        try:
+            tools = [float(eval(dia)) for dia in tooldia.split(",") if dia != '']
+        except AttributeError:
+            tools = [float(tooldia)]
+        # store here the default data for Geometry Data
+        default_data = {}
+        default_data.update({
+            "name": '_paint',
+            "plot": self.app.defaults["geometry_plot"],
+            "cutz": self.app.defaults["geometry_cutz"],
+            "vtipdia": 0.1,
+            "vtipangle": 30,
+            "travelz": self.app.defaults["geometry_travelz"],
+            "feedrate": self.app.defaults["geometry_feedrate"],
+            "feedrate_z": self.app.defaults["geometry_feedrate_z"],
+            "feedrate_rapid": self.app.defaults["geometry_feedrate_rapid"],
+            "dwell": self.app.defaults["geometry_dwell"],
+            "dwelltime": self.app.defaults["geometry_dwelltime"],
+            "multidepth": self.app.defaults["geometry_multidepth"],
+            "ppname_g": self.app.defaults["geometry_ppname_g"],
+            "depthperpass": self.app.defaults["geometry_depthperpass"],
+            "extracut": self.app.defaults["geometry_extracut"],
+            "toolchange": self.app.defaults["geometry_toolchange"],
+            "toolchangez": self.app.defaults["geometry_toolchangez"],
+            "endz": self.app.defaults["geometry_endz"],
+            "spindlespeed": self.app.defaults["geometry_spindlespeed"],
+            "toolchangexy": self.app.defaults["geometry_toolchangexy"],
+            "startz": self.app.defaults["geometry_startz"],
+
+            "tooldia": self.app.defaults["tools_painttooldia"],
+            "paintmargin": self.app.defaults["tools_paintmargin"],
+            "paintmethod": self.app.defaults["tools_paintmethod"],
+            "selectmethod": self.app.defaults["tools_selectmethod"],
+            "pathconnect": self.app.defaults["tools_pathconnect"],
+            "paintcontour": self.app.defaults["tools_paintcontour"],
+            "paintoverlap": self.app.defaults["tools_paintoverlap"]
+        })
+        ncc_tools = dict()
+
+        tooluid = 0
+        for tool in tools:
+            tooluid += 1
+            ncc_tools.update({
+                int(tooluid): {
+                    'tooldia': float('%.4f' % tool),
+                    'offset': 'Path',
+                    'offset_value': 0.0,
+                    'type': 'Iso',
+                    'tool_type': 'C1',
+                    'data': dict(default_data),
+                    'solid_geometry': []
+                }
+            })
+
+        if 'rest' in args:
+            rest = eval(str(args['rest']).capitalize())
+        else:
+            rest = eval(str(self.app.defaults["tools_nccrest"]))
+
+        if 'outname' in args:
+            outname = args['outname']
+        else:
+            if rest is True:
+                outname = name + "_ncc"
+            else:
+                outname = name + "_ncc_rm"
+
+        # Get source object.
+        try:
+            obj = self.app.collection.get_by_name(str(name))
+        except Exception as e:
+            log.debug("TclCommandCopperClear.execute() --> %s" % str(e))
+            self.raise_tcl_error("%s: %s" % (_("Could not retrieve object"), name))
+            return "Could not retrieve object: %s" % name
+
+        if obj is None:
+            return "Object not found: %s" % name
+
+        # Non-Copper clear all polygons in the non-copper clear object
+        if 'all' in args and args['all'] == 1:
+            self.app.ncclear_tool.clear_copper(ncc_obj=obj,
+                                               select_method='itself',
+                                               tooldia=tooldia,
+                                               overlap=overlap,
+                                               order=order,
+                                               margin=margin,
+                                               has_offset=has_offset,
+                                               offset=offset,
+                                               method=method,
+                                               outname=outname,
+                                               connect=connect,
+                                               contour=contour,
+                                               rest=rest,
+                                               tools_storage=ncc_tools)
+            return
+
+        # Non-Copper clear all polygons found within the box object from the the non_copper cleared object
+        elif 'ref' in args and args['ref'] == 1:
+            if 'box' not in args:
+                self.raise_tcl_error('%s' % _("Expected -box <value>."))
+            else:
+                box_name = args['box']
+
+                # Get box source object.
+                try:
+                    box_obj = self.app.collection.get_by_name(str(box_name))
+                except Exception as e:
+                    log.debug("TclCommandCopperClear.execute() --> %s" % str(e))
+                    self.raise_tcl_error("%s: %s" % (_("Could not retrieve box object"), name))
+                    return "Could not retrieve object: %s" % name
+
+                self.app.ncclear_tool.clear_copper(ncc_obj=obj,
+                                                   sel_obj=box_obj,
+                                                   select_method='box',
+                                                   tooldia=tooldia,
+                                                   overlap=overlap,
+                                                   order=order,
+                                                   margin=margin,
+                                                   has_offset=has_offset,
+                                                   offset=offset,
+                                                   method=method,
+                                                   outname=outname,
+                                                   connect=connect,
+                                                   contour=contour,
+                                                   rest=rest,
+                                                   tools_storage=ncc_tools)
+            return
+        else:
+            self.raise_tcl_error("%s:" % _("None of the following args: 'ref', 'all' were found or none was set to 1.\n"
+                                           "Copper clearing failed."))
+            return "None of the following args: 'ref', 'all' were found or none was set to 1.\n" \
+                   "Copper clearing failed."

+ 1 - 1
tclCommands/TclCommandFollow.py

@@ -57,7 +57,7 @@ class TclCommandFollow(TclCommandSignaled):
 
         del args['name']
         try:
-            obj.follow(**args)
+            obj.follow_geo(**args)
         except Exception as e:
             return "Operation failed: %s" % str(e)
 

+ 97 - 0
tclCommands/TclCommandNregions.py

@@ -0,0 +1,97 @@
+from ObjectCollection import *
+from tclCommands.TclCommand import TclCommand
+import gettext
+import FlatCAMTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class TclCommandNregions(TclCommand):
+    """
+    Tcl shell command to follow a Gerber file
+    """
+
+    # array of all command aliases, to be able use  old names for backward compatibility (add_poly, add_polygon)
+    aliases = ['non_copper_regions', 'ncr']
+
+    # dictionary of types from Tcl command, needs to be ordered
+    arg_names = collections.OrderedDict([
+        ('name', str)
+    ])
+
+    # dictionary of types from Tcl command, needs to be ordered , this  is  for options  like -optionname value
+    option_types = collections.OrderedDict([
+        ('outname', str),
+        ('margin', float),
+        ('rounded', bool)
+    ])
+
+    # array of mandatory options for current Tcl command: required = {'name','outname'}
+    required = ['name']
+
+    # structured help for current command, args needs to be ordered
+    help = {
+        'main': "Creates a geometry object with the non-copper regions.",
+        'args': collections.OrderedDict([
+            ('name', 'Object name for which to create non-copper regions. String'),
+            ('outname', 'Name of the resulting Geometry object. String.'),
+            ('margin', "Specify the edge of the PCB by drawing a box around all objects with this minimum distance. "
+                       "Float number."),
+            ('rounded', "Resulting geometry will have rounded corners. True or False.")
+        ]),
+        'examples': ['ncr name -outname name_ncr']
+    }
+
+    def execute(self, args, unnamed_args):
+        """
+        execute current TCL shell command
+
+        :param args: array of known named arguments and options
+        :param unnamed_args: array of other values which were passed into command
+            without -somename and  we do not have them in known arg_names
+        :return: None or exception
+        """
+
+        name = args['name']
+
+        if 'outname' not in args:
+            args['outname'] = name + "_noncopper"
+
+        obj = self.app.collection.get_by_name(name)
+        if obj is None:
+            self.raise_tcl_error("%s: %s" % (_("Object not found"), name))
+
+        if not isinstance(obj, FlatCAMGerber) and not isinstance(obj, FlatCAMGeometry):
+            self.raise_tcl_error('%s %s: %s.' % (_("Expected FlatCAMGerber or FlatCAMGeometry, got"), name, type(obj)))
+
+        if 'margin' not in args:
+            args['margin'] = float(self.app.defaults["gerber_noncoppermargin"])
+        margin = args['margin']
+
+        if 'rounded' not in args:
+            args['rounded'] = self.app.defaults["gerber_noncopperrounded"]
+        rounded = args['rounded']
+
+        del args['name']
+
+        try:
+            def geo_init(geo_obj, app_obj):
+                assert isinstance(geo_obj, FlatCAMGeometry)
+
+                geo = cascaded_union(obj.solid_geometry)
+                bounding_box = geo.envelope.buffer(float(margin))
+                if not rounded:
+                    bounding_box = bounding_box.envelope
+
+                non_copper = bounding_box.difference(geo)
+                geo_obj.solid_geometry = non_copper
+
+            self.app.new_object("geometry", args['outname'], geo_init)
+        except Exception as e:
+            return "Operation failed: %s" % str(e)
+
+        # in the end toggle the visibility of the origin object so we can see the generated Geometry
+        self.app.collection.get_by_name(name).ui.plot_cb.toggle()

+ 204 - 28
tclCommands/TclCommandPaint.py

@@ -1,8 +1,16 @@
 from ObjectCollection import *
-from tclCommands.TclCommand import TclCommandSignaled
+from tclCommands.TclCommand import TclCommand
 
+import gettext
+import FlatCAMTranslation as fcTranslate
+import builtins
 
-class TclCommandPaint(TclCommandSignaled):
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class TclCommandPaint(TclCommand):
     """
     Paint the interior of polygons
     """
@@ -13,32 +21,54 @@ class TclCommandPaint(TclCommandSignaled):
     # dictionary of types from Tcl command, needs to be ordered
     arg_names = collections.OrderedDict([
         ('name', str),
-        ('tooldia', float),
-        ('overlap', float)
     ])
 
     # dictionary of types from Tcl command, needs to be ordered , this  is  for options  like -optionname value
     option_types = collections.OrderedDict([
-        ('outname', str),
+        ('tooldia', str),
+        ('overlap', float),
+        ('order', str),
+        ('margin', float),
+        ('method', str),
+        ('connect', bool),
+        ('contour', bool),
+
         ('all', bool),
+        ('single', bool),
+        ('ref', bool),
+        ('box', str),
         ('x', float),
-        ('y', float)
+        ('y', float),
+        ('outname', str),
     ])
 
     # array of mandatory options for current Tcl command: required = {'name','outname'}
-    required = ['name', 'tooldia', 'overlap']
+    required = ['name']
 
     # structured help for current command, args needs to be ordered
     help = {
         'main': "Paint polygons",
         'args': collections.OrderedDict([
-            ('name', 'Name of the source Geometry object.'),
-            ('tooldia', 'Diameter of the tool to be used.'),
-            ('overlap', 'Fraction of the tool diameter to overlap cuts.'),
-            ('outname', 'Name of the resulting Geometry object.'),
-            ('all', 'Paint all polygons in the object.'),
-            ('x', 'X value of coordinate for the selection of a single polygon.'),
-            ('y', 'Y value of coordinate for the selection of a single polygon.')
+            ('name', 'Name of the source Geometry object. String.'),
+            ('tooldia', 'Diameter of the tool to be used. Can be a comma separated list of diameters. No space is '
+                        'allowed between tool diameters. E.g: correct: 0.5,1 / incorrect: 0.5, 1'),
+            ('overlap', 'Fraction of the tool diameter to overlap cuts. Float number.'),
+            ('margin', 'Bounding box margin. Float number.'),
+            ('order', 'Can have the values: "no", "fwd" and "rev". String.'
+                      'It is useful when there are multiple tools in tooldia parameter.'
+                      '"no" -> the order used is the one provided.'
+                      '"fwd" -> tools are ordered from smallest to biggest.'
+                      '"rev" -> tools are ordered from biggest to smallest.'),
+            ('method', 'Algorithm for painting. Can be: "standard", "seed" or "lines".'),
+            ('connect', 'Draw lines to minimize tool lifts. True or False'),
+            ('contour', 'Cut around the perimeter of the painting. True or False'),
+            ('all', 'Paint all polygons in the object. True or False'),
+            ('single', 'Paint a single polygon specified by "x" and "y" parameters. True or False'),
+            ('ref', 'Paint all polygons within a specified object with the name in "box" parameter. True or False'),
+            ('box', 'name of the object to be used as paint reference when selecting "ref"" True. String.'),
+            ('x', 'X value of coordinate for the selection of a single polygon. Float number.'),
+            ('y', 'Y value of coordinate for the selection of a single polygon. Float number.'),
+            ('outname', 'Name of the resulting Geometry object. String.'),
         ]),
         'examples': []
     }
@@ -54,31 +84,177 @@ class TclCommandPaint(TclCommandSignaled):
         """
 
         name = args['name']
-        tooldia = args['tooldia']
-        overlap = args['overlap']
+
+        if 'tooldia' in args:
+            tooldia = str(args['tooldia'])
+        else:
+            tooldia = float(self.app.defaults["tools_paintoverlap"])
+
+        if 'overlap' in args:
+            overlap = float(args['overlap'])
+        else:
+            overlap = float(self.app.defaults["tools_paintoverlap"])
+
+        if 'order' in args:
+            order = args['order']
+        else:
+            order = str(self.app.defaults["tools_paintorder"])
+
+        if 'margin' in args:
+            margin = float(args['margin'])
+        else:
+            margin = float(self.app.defaults["tools_paintmargin"])
+
+        if 'method' in args:
+            method = args['method']
+        else:
+            method = str(self.app.defaults["tools_paintmethod"])
+
+        if 'connect' in args:
+            connect = eval(str(args['connect']).capitalize())
+        else:
+            connect = eval(str(self.app.defaults["tools_pathconnect"]))
+
+        if 'contour' in args:
+            contour = eval(str(args['contour']).capitalize())
+        else:
+            contour = eval(str(self.app.defaults["tools_paintcontour"]))
 
         if 'outname' in args:
             outname = args['outname']
         else:
             outname = name + "_paint"
 
-        obj = self.app.collection.get_by_name(name)
-        if obj is None:
-            self.raise_tcl_error("Object not found: %s" % name)
+        # Get source object.
+        try:
+            obj = self.app.collection.get_by_name(str(name))
+        except Exception as e:
+            log.debug("TclCommandPaint.execute() --> %s" % str(e))
+            self.raise_tcl_error("%s: %s" % (_("Could not retrieve object"), name))
+            return "Could not retrieve object: %s" % name
+
+        try:
+            tools = [float(eval(dia)) for dia in tooldia.split(",") if dia != '']
+        except AttributeError:
+            tools = [float(tooldia)]
+        # store here the default data for Geometry Data
+        default_data = {}
+        default_data.update({
+            "name": '_paint',
+            "plot": self.app.defaults["geometry_plot"],
+            "cutz": self.app.defaults["geometry_cutz"],
+            "vtipdia": 0.1,
+            "vtipangle": 30,
+            "travelz": self.app.defaults["geometry_travelz"],
+            "feedrate": self.app.defaults["geometry_feedrate"],
+            "feedrate_z": self.app.defaults["geometry_feedrate_z"],
+            "feedrate_rapid": self.app.defaults["geometry_feedrate_rapid"],
+            "dwell": self.app.defaults["geometry_dwell"],
+            "dwelltime": self.app.defaults["geometry_dwelltime"],
+            "multidepth": self.app.defaults["geometry_multidepth"],
+            "ppname_g": self.app.defaults["geometry_ppname_g"],
+            "depthperpass": self.app.defaults["geometry_depthperpass"],
+            "extracut": self.app.defaults["geometry_extracut"],
+            "toolchange": self.app.defaults["geometry_toolchange"],
+            "toolchangez": self.app.defaults["geometry_toolchangez"],
+            "endz": self.app.defaults["geometry_endz"],
+            "spindlespeed": self.app.defaults["geometry_spindlespeed"],
+            "toolchangexy": self.app.defaults["geometry_toolchangexy"],
+            "startz": self.app.defaults["geometry_startz"],
 
-        if not isinstance(obj, Geometry):
-            self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj)))
+            "tooldia": self.app.defaults["tools_painttooldia"],
+            "paintmargin": self.app.defaults["tools_paintmargin"],
+            "paintmethod": self.app.defaults["tools_paintmethod"],
+            "selectmethod": self.app.defaults["tools_selectmethod"],
+            "pathconnect": self.app.defaults["tools_pathconnect"],
+            "paintcontour": self.app.defaults["tools_paintcontour"],
+            "paintoverlap": self.app.defaults["tools_paintoverlap"]
+        })
+        paint_tools = dict()
 
-        if 'all' in args and args['all']:
-            obj.paint_poly_all(tooldia, overlap, outname)
+        tooluid = 0
+        for tool in tools:
+            tooluid += 1
+            paint_tools.update({
+                int(tooluid): {
+                    'tooldia': float('%.4f' % tool),
+                    'offset': 'Path',
+                    'offset_value': 0.0,
+                    'type': 'Iso',
+                    'tool_type': 'C1',
+                    'data': dict(default_data),
+                    'solid_geometry': []
+                }
+            })
+
+        if obj is None:
+            return "Object not found: %s" % name
+
+        # Paint all polygons in the painted object
+        if 'all' in args and args['all'] is True:
+            self.app.paint_tool.paint_poly_all(obj=obj,
+                                               tooldia=tooldia,
+                                               overlap=overlap,
+                                               order=order,
+                                               margin=margin,
+                                               method=method,
+                                               outname=outname,
+                                               connect=connect,
+                                               contour=contour,
+                                               tools_storage=paint_tools)
             return
 
-        if 'x' not in args or 'y' not in args:
-            self.raise_tcl_error('Expected -all 1 or -x <value> and -y <value>.')
+        # Paint single polygon in the painted object
+        elif 'single' in args and args['single'] is True:
+            if 'x' not in args or 'y' not in args:
+                self.raise_tcl_error('%s' % _("Expected -x <value> and -y <value>."))
+            else:
+                x = args['x']
+                y = args['y']
 
-        x = args['x']
-        y = args['y']
+                self.app.paint_tool.paint_poly(obj=obj,
+                                               inside_pt=[x, y],
+                                               tooldia=tooldia,
+                                               overlap=overlap,
+                                               order=order,
+                                               margin=margin,
+                                               method=method,
+                                               outname=outname,
+                                               connect=connect,
+                                               contour=contour,
+                                               tools_storage=paint_tools)
+            return
 
-        obj.paint_poly_single_click([x, y], tooldia, overlap, outname)
+        # Paint all polygons found within the box object from the the painted object
+        elif 'ref' in args and args['ref'] is True:
+            if 'box' not in args:
+                self.raise_tcl_error('%s' % _("Expected -box <value>."))
+            else:
+                box_name = args['box']
 
+                # Get box source object.
+                try:
+                    box_obj = self.app.collection.get_by_name(str(box_name))
+                except Exception as e:
+                    log.debug("TclCommandPaint.execute() --> %s" % str(e))
+                    self.raise_tcl_error("%s: %s" % (_("Could not retrieve box object"), name))
+                    return "Could not retrieve object: %s" % name
 
+                self.app.paint_tool.paint_poly_ref(obj=obj,
+                                                   sel_obj=box_obj,
+                                                   tooldia=tooldia,
+                                                   overlap=overlap,
+                                                   order=order,
+                                                   margin=margin,
+                                                   method=method,
+                                                   outname=outname,
+                                                   connect=connect,
+                                                   contour=contour,
+                                                   tools_storage=paint_tools)
+            return
+
+        else:
+            self.raise_tcl_error("%s:" % _("There was none of the following args: 'ref', 'single', 'all'.\n"
+                                           "Paint failed."))
+            return "There was none of the following args: 'ref', 'single', 'all'.\n" \
+                   "Paint failed."

+ 60 - 7
tclCommands/TclCommandScale.py

@@ -21,20 +21,30 @@ class TclCommandScale(TclCommand):
 
     # Dictionary of types from Tcl command, needs to be ordered , this  is  for options  like -optionname value
     option_types = collections.OrderedDict([
-
+        ('x', float),
+        ('y', float),
+        ('origin', str)
     ])
 
     # array of mandatory options for current Tcl command: required = {'name','outname'}
-    required = ['name', 'factor']
+    required = ['name']
 
     # structured help for current command, args needs to be ordered
     help = {
-        'main': "Resizes the object by a factor.",
+        'main': "Resizes the object by a factor on X axis and a factor on Y axis, having as scale origin the point ",
         'args': collections.OrderedDict([
             ('name', 'Name of the object to resize.'),
-            ('factor', 'Fraction by which to scale.')
+            ('factor', 'Fraction by which to scale on both axis. '),
+            ('x', 'Fraction by which to scale on X axis. If "factor" is used then this parameter is ignored'),
+            ('y', 'Fraction by which to scale on Y axis. If "factor" is used then this parameter is ignored'),
+            ('origin', 'Reference used for scale. It can be: "origin" which means point (0, 0) or "min_bounds" which '
+                       'means the lower left point of the bounding box or it can be "center" which means the center '
+                       'of the bounding box.')
+
         ]),
-        'examples': ['scale my_geometry 4.2']
+        'examples': ['scale my_geometry 4.2',
+                     'scale my_geo -x 3.1 -y 2.8',
+                     'scale my_geo 1.2 -origin min_bounds']
     }
 
     def execute(self, args, unnamed_args):
@@ -46,6 +56,49 @@ class TclCommandScale(TclCommand):
         """
 
         name = args['name']
-        factor = args['factor']
+        try:
+            obj_to_scale = self.app.collection.get_by_name(name)
+        except Exception as e:
+            log.debug("TclCommandCopperClear.execute() --> %s" % str(e))
+            self.raise_tcl_error("%s: %s" % (_("Could not retrieve box object"), name))
+            return "Could not retrieve object: %s" % name
+
+        if 'origin' not in args:
+            xmin, ymin, xmax, ymax = obj_to_scale.bounds()
+            c_x = xmin + (xmax - xmin) / 2
+            c_y = ymin + (ymax - ymin) / 2
+            point = (c_x, c_y)
+        else:
+            if args['origin'] == 'origin':
+                point = (0, 0)
+            elif args['origin'] == 'min_bounds':
+                xmin, ymin, xmax, ymax = obj_to_scale.bounds()
+                point = (xmin, ymin)
+            elif args['origin'] == 'center':
+                xmin, ymin, xmax, ymax = obj_to_scale.bounds()
+                c_x = xmin + (xmax - xmin) / 2
+                c_y = ymin + (ymax - ymin) / 2
+                point = (c_x, c_y)
+            else:
+                self.raise_tcl_error('%s' % _("Expected -origin <origin> or -origin <min_bounds> or -origin <center>."))
+                return 'fail'
+
+        if 'factor' in args:
+            factor = float(args['factor'])
+            obj_to_scale.scale(factor, point=point)
+            return
+
+        if 'x' not in args and 'y' not in args:
+            self.raise_tcl_error('%s' % _("Expected -x <value> -y <value>."))
+            return 'fail'
 
-        self.app.collection.get_by_name(name).scale(factor)
+        if 'x' in args and 'y' not in args:
+            f_x = float(args['x'])
+            obj_to_scale.scale(f_x, 0, point=point)
+        elif 'x' not in args and 'y' in args:
+            f_y = float(args['y'])
+            obj_to_scale.scale(0, f_y, point=point)
+        elif 'x' in args and 'y' in args:
+            f_x = float(args['x'])
+            f_y = float(args['y'])
+            obj_to_scale.scale(f_x, f_y, point=point)

+ 7 - 4
tclCommands/TclCommandSkew.py

@@ -30,7 +30,8 @@ class TclCommandSkew(TclCommand):
 
     # structured help for current command, args needs to be ordered
     help = {
-        'main': "Shear/Skew an object by angles along x and y dimensions.",
+        'main': "Shear/Skew an object by angles along x and y dimensions. The reference point is the left corner of "
+                "the bounding box of the object.",
         'args': collections.OrderedDict([
             ('name', 'Name of the object to skew.'),
             ('angle_x', 'Angle in degrees by which to skew on the X axis.'),
@@ -48,7 +49,9 @@ class TclCommandSkew(TclCommand):
         """
 
         name = args['name']
-        angle_x = args['angle_x']
-        angle_y = args['angle_y']
+        angle_x = float(args['angle_x'])
+        angle_y = float(args['angle_y'])
 
-        self.app.collection.get_by_name(name).skew(angle_x, angle_y)
+        obj_to_skew = self.app.collection.get_by_name(name)
+        xmin, ymin, xmax, ymax = obj_to_skew.bounds()
+        obj_to_skew.skew(angle_x, angle_y, point=(xmin, ymin))

+ 16 - 13
tclCommands/TclCommandSubtractRectangle.py

@@ -4,7 +4,7 @@ from tclCommands.TclCommand import TclCommandSignaled
 
 class TclCommandSubtractRectangle(TclCommandSignaled):
     """
-    Tcl shell command to subtract a rectange from the given Geometry object.
+    Tcl shell command to subtract a rectangle from the given Geometry object.
     """
 
     # array of all command aliases, to be able use  old names for backward compatibility (add_poly, add_polygon)
@@ -13,11 +13,7 @@ class TclCommandSubtractRectangle(TclCommandSignaled):
     # Dictionary of types from Tcl command, needs to be ordered.
     # For positional arguments
     arg_names = collections.OrderedDict([
-        ('name', str),
-        ('x0', float),
-        ('y0', float),
-        ('x1', float),
-        ('y1', float)
+        ('name', str)
     ])
 
     # Dictionary of types from Tcl command, needs to be ordered.
@@ -27,7 +23,7 @@ class TclCommandSubtractRectangle(TclCommandSignaled):
     ])
 
     # array of mandatory options for current Tcl command: required = {'name','outname'}
-    required = ['name', 'x0', 'y0', 'x1', 'y1']
+    required = ['name']
 
     # structured help for current command, args needs to be ordered
     help = {
@@ -37,7 +33,7 @@ class TclCommandSubtractRectangle(TclCommandSignaled):
             ('x0 y0', 'Bottom left corner coordinates.'),
             ('x1 y1', 'Top right corner coordinates.')
         ]),
-        'examples': []
+        'examples': ['subtract_rectangle geo_obj 8 8 15 15']
     }
 
     def execute(self, args, unnamed_args):
@@ -49,12 +45,19 @@ class TclCommandSubtractRectangle(TclCommandSignaled):
             without -somename and  we do not have them in known arg_names
         :return: None or exception
         """
-
+        if 'name' not in args:
+            self.raise_tcl_error("%s:" % _("No Geometry name in args. Provide a name and try again."))
+            return 'fail'
         obj_name = args['name']
-        x0 = args['x0']
-        y0 = args['y0']
-        x1 = args['x1']
-        y1 = args['y1']
+
+        if len(unnamed_args) != 4:
+            self.raise_tcl_error("Incomplete coordinates. There are 4 required: x0 y0 x1 y1.")
+            return 'fail'
+
+        x0 = float(unnamed_args[0])
+        y0 = float(unnamed_args[1])
+        x1 = float(unnamed_args[2])
+        y1 = float(unnamed_args[3])
 
         try:
             obj = self.app.collection.get_by_name(str(obj_name))

+ 1 - 1
tclCommands/TclCommandVersion.py

@@ -32,7 +32,7 @@ class TclCommandVersion(TclCommand):
         'args': collections.OrderedDict([
 
         ]),
-        'examples': []
+        'examples': ['version']
     }
 
     def execute(self, args, unnamed_args):

+ 1 - 1
tclCommands/TclCommandWriteGCode.py

@@ -37,7 +37,7 @@ class TclCommandWriteGCode(TclCommandSignaled):
             ('preamble', 'Text to append at the beginning.'),
             ('postamble', 'Text to append at the end.')
         ]),
-        'examples': []
+        'examples': ["write_gcode name c:\\\\gcode_repo"]
     }
 
     def execute(self, args, unnamed_args):

+ 3 - 0
tclCommands/__init__.py

@@ -9,8 +9,10 @@ import tclCommands.TclCommandAddPolyline
 import tclCommands.TclCommandAddRectangle
 import tclCommands.TclCommandAlignDrill
 import tclCommands.TclCommandAlignDrillGrid
+import tclCommands.TclCommandBbox
 import tclCommands.TclCommandClearShell
 import tclCommands.TclCommandCncjob
+import tclCommands.TclCommandCopperClear
 import tclCommands.TclCommandCutout
 import tclCommands.TclCommandDelete
 import tclCommands.TclCommandDrillcncjob
@@ -31,6 +33,7 @@ import tclCommands.TclCommandListSys
 import tclCommands.TclCommandMillHoles
 import tclCommands.TclCommandMirror
 import tclCommands.TclCommandNew
+import tclCommands.TclCommandNregions
 import tclCommands.TclCommandNewGeometry
 import tclCommands.TclCommandOffset
 import tclCommands.TclCommandOpenExcellon

+ 101 - 0
tests/svg/use.svg

@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="50mm"
+   height="50mm"
+   viewBox="0 0 50 50"
+   version="1.1"
+   id="svg8"
+   inkscape:version="0.92.4 5da689c313, 2019-01-14"
+   sodipodi:docname="use.svg">
+  <defs
+     id="defs2" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="5.4679048"
+     inkscape:cx="113.26276"
+     inkscape:cy="109.86475"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="3834"
+     inkscape:window-height="2095"
+     inkscape:window-x="0"
+     inkscape:window-y="28"
+     inkscape:window-maximized="0" />
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-247)">
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 6.2421073,283.98351 c -2.4960796,-5.97466 14.6658087,-0.0283 2.7443869,-28.40936 -2.6185936,-6.23403 13.3146118,1.40862 15.4980508,1.40862"
+       id="path815"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="csc" />
+    <use
+       x="0"
+       y="0"
+       xlink:href="#path815"
+       id="use817"
+       transform="matrix(-1,0,0,1,48.96909,0)"
+       width="100%"
+       height="100%" />
+    <rect
+       style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal"
+       id="rect942"
+       width="4.3065705"
+       height="4.7904549"
+       x="13.742314"
+       y="288.14493" />
+    <use
+       x="0"
+       y="0"
+       xlink:href="#rect942"
+       id="use944"
+       transform="matrix(2.4752181,0,0,1.0534789,-4.8199296,-20.27984)"
+       width="100%"
+       height="100%" />
+    <circle
+       style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal"
+       id="path948"
+       cx="18.823099"
+       cy="275.70914"
+       r="4.5485125" />
+    <use
+       x="0"
+       y="0"
+       xlink:href="#path948"
+       id="use950"
+       transform="translate(10.50029,-10.984174)"
+       width="100%"
+       height="100%" />
+  </g>
+</svg>

Some files were not shown because too many files changed in this diff