فهرست منبع

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

Camellan 6 سال پیش
والد
کامیت
d37875898e
87فایلهای تغییر یافته به همراه7144 افزوده شده و 3183 حذف شده
  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

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 415 - 287
FlatCAMApp.py


+ 148 - 55
FlatCAMObj.py

@@ -73,7 +73,7 @@ class FlatCAMObj(QtCore.QObject):
 
 
         self.kind = None  # Override with proper name
         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.shapes = self.app.plotcanvas.new_shape_group()
 
 
         # self.mark_shapes = self.app.plotcanvas.new_shape_collection(layers=2)
         # 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):
     def on_options_change(self, key):
         # Update form on programmatically options change
         # Update form on programmatically options change
         self.set_form_item(key)
         self.set_form_item(key)
-
         # Set object visibility
         # Set object visibility
         if key == 'plot':
         if key == 'plot':
             self.visible = self.options['plot']
             self.visible = self.options['plot']
@@ -358,7 +357,7 @@ class FlatCAMObj(QtCore.QObject):
                 pass
                 pass
 
 
         if threaded is False:
         if threaded is False:
-            worker_task(self)
+            worker_task(app_obj=self)
         else:
         else:
             self.app.worker_task.emit({'fcn': worker_task, 'params': [self]})
             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:
         if self.ui.follow_cb.get_value() is True:
             obj = self.app.collection.get_active()
             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
             # in the end toggle the visibility of the origin object so we can see the generated Geometry
             obj.ui.plot_cb.toggle()
             obj.ui.plot_cb.toggle()
         else:
         else:
@@ -786,7 +785,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
 
         if self.ui.follow_cb.get_value() is True:
         if self.ui.follow_cb.get_value() is True:
             obj = self.app.collection.get_active()
             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
             # in the end toggle the visibility of the origin object so we can see the generated Geometry
             obj.ui.plot_cb.toggle()
             obj.ui.plot_cb.toggle()
         else:
         else:
@@ -879,23 +878,48 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
 
             if invert:
             if invert:
                 try:
                 try:
-                    if type(geom) is MultiPolygon:
+                    try:
                         pl = []
                         pl = []
                         for p in geom:
                         for p in geom:
                             if p is not None:
                             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)
                         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:
                 except Exception as e:
                     log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> %s" % str(e))
                     log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> %s" % str(e))
                     return 'fail'
                     return 'fail'
             return geom
             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 combine:
             if self.iso_type == 0:
             if self.iso_type == 0:
@@ -983,7 +1007,12 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
 
                 if empty_cnt == len(geo_obj.solid_geometry):
                 if empty_cnt == len(geo_obj.solid_geometry):
                     raise ValidationError("Empty Geometry", None)
                     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?
             # TODO: Do something if this is None. Offer changing name?
             self.app.new_object("geometry", iso_name, iso_init)
             self.app.new_object("geometry", iso_name, iso_init)
@@ -1100,6 +1129,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         :return: None
         :return: None
         :rtype: None
         :rtype: None
         """
         """
+        log.debug("FlatCAMObj.FlatCAMGerber.convert_units()")
 
 
         factor = Gerber.convert_units(self, units)
         factor = Gerber.convert_units(self, units)
 
 
@@ -2082,6 +2112,11 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         # Fill form fields
         # Fill form fields
         self.to_form()
         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
         # initialize the dict that holds the tools offset
         t_default_offset = self.app.defaults["excellon_offset"]
         t_default_offset = self.app.defaults["excellon_offset"]
         if not self.tool_offset:
         if not self.tool_offset:
@@ -2207,7 +2242,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
             item[0] = str(item[0])
             item[0] = str(item[0])
         return table_tools_items
         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
         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
         :return: has_slots and Excellon_code
@@ -2273,6 +2308,8 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
             if self.slots:
             if self.slots:
                 has_slots = 1
                 has_slots = 1
                 for tool in self.tools:
                 for tool in self.tools:
+                    excellon_code += 'G05\n'
+
                     if int(tool) < 10:
                     if int(tool) < 10:
                         excellon_code += 'T0' + str(tool) + '\n'
                         excellon_code += 'T0' + str(tool) + '\n'
                     else:
                     else:
@@ -2284,13 +2321,17 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
                             start_slot_y = slot['start'].y * factor
                             start_slot_y = slot['start'].y * factor
                             stop_slot_x = slot['stop'].x * factor
                             stop_slot_x = slot['stop'].x * factor
                             stop_slot_y = slot['stop'].y * 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']:
                         elif e_zeros == 'LZ' and tool == slot['tool']:
                             start_slot_x = slot['start'].x * factor
                             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_x_formatted = stop_x_whole + stop_slot_x_formatted[2]
                             stop_slot_y_formatted = stop_y_whole + stop_slot_y_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']:
                         elif tool == slot['tool']:
                             start_slot_x = slot['start'].x * factor
                             start_slot_x = slot['start'].x * factor
                             start_slot_y = slot['start'].y * 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_x_formatted.ljust(length, '0')
                             stop_slot_y_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:
         except Exception as e:
             log.debug(str(e))
             log.debug(str(e))
 
 
@@ -2721,8 +2774,9 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
         self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
 
 
     def convert_units(self, units):
     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['drillz'] = float(self.options['drillz']) * factor
         self.options['travelz'] = float(self.options['travelz']) * factor
         self.options['travelz'] = float(self.options['travelz']) * factor
         self.options['feedrate'] = float(self.options['feedrate']) * 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.
         # engine of FlatCAM. Most likely are generated by some of tools and are special cases of geometries.
         self. special_group = None
         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
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # Always append to it because it carries contents
@@ -3251,6 +3305,11 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         # Fill form fields only on object create
         # Fill form fields only on object create
         self.to_form()
         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.tipdialabel.hide()
         self.ui.tipdia_entry.hide()
         self.ui.tipdia_entry.hide()
         self.ui.tipanglelabel.hide()
         self.ui.tipanglelabel.hide()
@@ -3375,7 +3434,6 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             self.ui.level.setText(_(
             self.ui.level.setText(_(
                 '<span style="color:red;"><b>Advanced</b></span>'
                 '<span style="color:red;"><b>Advanced</b></span>'
             ))
             ))
-
         self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
         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.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click)
         self.ui.paint_tool_button.clicked.connect(lambda: self.app.paint_tool.run(toggle=False))
         self.ui.paint_tool_button.clicked.connect(lambda: self.app.paint_tool.run(toggle=False))
@@ -4910,6 +4968,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         :return: None
         :return: None
         :rtype: None
         :rtype: None
         """
         """
+        log.debug("FlatCAMObj.FlatCAMGeometry.scale()")
 
 
         try:
         try:
             xfactor = float(xfactor)
             xfactor = float(xfactor)
@@ -4952,14 +5011,20 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                     geoms.append(scale_recursion(local_geom))
                     geoms.append(scale_recursion(local_geom))
                 return geoms
                 return geoms
             else:
             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:
         if self.multigeo is True:
             for tool in self.tools:
             for tool in self.tools:
                 self.tools[tool]['solid_geometry'] = scale_recursion(self.tools[tool]['solid_geometry'])
                 self.tools[tool]['solid_geometry'] = scale_recursion(self.tools[tool]['solid_geometry'])
         else:
         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(_(
         self.app.inform.emit(_(
             "[success] Geometry Scale done."
             "[success] Geometry Scale done."
         ))
         ))
@@ -4973,6 +5038,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         :return: None
         :return: None
         :rtype: None
         :rtype: None
         """
         """
+        log.debug("FlatCAMObj.FlatCAMGeometry.offset()")
 
 
         try:
         try:
             dx, dy = vect
             dx, dy = vect
@@ -4990,7 +5056,10 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                     geoms.append(translate_recursion(local_geom))
                     geoms.append(translate_recursion(local_geom))
                 return geoms
                 return geoms
             else:
             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:
         if self.multigeo is True:
             for tool in self.tools:
             for tool in self.tools:
@@ -5000,6 +5069,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         self.app.inform.emit(_("[success] Geometry Offset done."))
         self.app.inform.emit(_("[success] Geometry Offset done."))
 
 
     def convert_units(self, units):
     def convert_units(self, units):
+        log.debug("FlatCAMObj.FlatCAMGeometry.convert_units()")
+
         self.ui_disconnect()
         self.ui_disconnect()
 
 
         factor = Geometry.convert_units(self, units)
         factor = Geometry.convert_units(self, units)
@@ -5146,11 +5217,11 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 for tooluid_key in self.tools:
                 for tooluid_key in self.tools:
                     solid_geometry = self.tools[tooluid_key]['solid_geometry']
                     solid_geometry = self.tools[tooluid_key]['solid_geometry']
                     self.plot_element(solid_geometry, visible=visible)
                     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.plot_element(self.solid_geometry, visible=self.options['plot'])
             self.shapes.redraw()
             self.shapes.redraw()
@@ -5160,8 +5231,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
     def on_plot_cb_click(self, *args):
     def on_plot_cb_click(self, *args):
         if self.muted_ui:
         if self.muted_ui:
             return
             return
-        self.plot()
         self.read_form_item('plot')
         self.read_form_item('plot')
+        self.plot()
 
 
         self.ui_disconnect()
         self.ui_disconnect()
         cb_flag = self.ui.plot_cb.isChecked()
         cb_flag = self.ui.plot_cb.isChecked()
@@ -5315,7 +5386,9 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         # from predecessors.
         # from predecessors.
         self.ser_attrs += ['options', 'kind', 'cnc_tools', 'multitool']
         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):
     def build_ui(self):
         self.ui_disconnect()
         self.ui_disconnect()
@@ -5456,7 +5529,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         # Fill form fields only on object create
         # Fill form fields only on object create
         self.to_form()
         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:
         try:
             if self.travel_distance:
             if self.travel_distance:
                 self.ui.t_distance_label.show()
                 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.t_distance_entry.set_value('%.4f' % float(self.travel_distance))
                 self.ui.units_label.setText(str(self.units).lower())
                 self.ui.units_label.setText(str(self.units).lower())
                 self.ui.units_label.setDisabled(True)
                 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:
         except AttributeError:
             pass
             pass
 
 
@@ -5922,9 +6008,20 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
 
 
         visible = visible if visible else self.options['plot']
         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:
         try:
             if self.multitool is False:  # single tool usage
             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:
             else:
                 # multiple tools usage
                 # multiple tools usage
                 for tooluid_key in self.cnc_tools:
                 for tooluid_key in self.cnc_tools:
@@ -5936,23 +6033,19 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
             self.shapes.clear(update=True)
             self.shapes.clear(update=True)
             self.annotation.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):
     def on_annotation_change(self):
         if self.ui.annotation_cb.get_value():
         if self.ui.annotation_cb.get_value():
-            self.app.plotcanvas.text_collection.enabled = True
+            self.text_col.enabled = True
         else:
         else:
-            self.app.plotcanvas.text_collection.enabled = False
+            self.text_col.enabled = False
         # kind = self.ui.cncplot_method_combo.get_value()
         # kind = self.ui.cncplot_method_combo.get_value()
         # self.plot(kind=kind)
         # self.plot(kind=kind)
         self.annotation.redraw()
         self.annotation.redraw()
 
 
     def convert_units(self, units):
     def convert_units(self, units):
+        log.debug("FlatCAMObj.FlatCAMECNCjob.convert_units()")
+
         factor = CNCjob.convert_units(self, units)
         factor = CNCjob.convert_units(self, units)
-        FlatCAMApp.App.log.debug("FlatCAMCNCjob.convert_units()")
         self.options["tooldia"] = float(self.options["tooldia"]) * factor
         self.options["tooldia"] = float(self.options["tooldia"]) * factor
 
 
         param_list = ['cutz', 'depthperpass', 'travelz', 'feedrate', 'feedrate_z', 'feedrate_rapid',
         param_list = ['cutz', 'depthperpass', 'travelz', 'feedrate', 'feedrate_z', 'feedrate_rapid',

+ 2 - 0
FlatCAMWorker.py

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

+ 10 - 4
ObjectCollection.py

@@ -15,7 +15,7 @@ from FlatCAMObj import *
 import inspect  # TODO: Remove
 import inspect  # TODO: Remove
 import FlatCAMApp
 import FlatCAMApp
 from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5 import QtGui, QtCore, QtWidgets
-from PyQt5.QtCore import Qt
+from PyQt5.QtCore import Qt, QSettings
 # import webbrowser
 # import webbrowser
 
 
 import gettext
 import gettext
@@ -256,8 +256,14 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         # self.view.setAcceptDrops(True)
         # self.view.setAcceptDrops(True)
         # self.view.setDropIndicatorShown(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 = QtGui.QFont()
-        font.setPixelSize(12)
+        font.setPixelSize(fsize)
         font.setFamily("Seagoe UI")
         font.setFamily("Seagoe UI")
         self.view.setFont(font)
         self.view.setFont(font)
 
 
@@ -312,7 +318,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
             for obj in self.get_selected():
             for obj in self.get_selected():
                 if type(obj) != FlatCAMGeometry:
                 if type(obj) != FlatCAMGeometry:
                     self.app.ui.menuprojectgeneratecnc.setVisible(False)
                     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)
                     self.app.ui.menuprojectedit.setVisible(False)
                 if type(obj) != FlatCAMGerber and type(obj) != FlatCAMExcellon:
                 if type(obj) != FlatCAMGerber and type(obj) != FlatCAMExcellon:
                     self.app.ui.menuprojectviewsource.setVisible(False)
                     self.app.ui.menuprojectviewsource.setVisible(False)
@@ -687,7 +693,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         :param name: Name of the FlatCAM Object
         :param name: Name of the FlatCAM Object
         :return: None
         :return: None
         """
         """
-        log.debug("ObjectCollection.set_inactive()")
+        # log.debug("ObjectCollection.set_inactive()")
 
 
         obj = self.get_by_name(name)
         obj = self.get_by_name(name)
         item = obj.item
         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
 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
 - 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
         # fixed issue of getting bounds only for one level lists of objects
         # now it can get bounds for nested 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:
         if self.solid_geometry is None:
             log.debug("solid_geometry is None")
             log.debug("solid_geometry is None")
             return 0, 0, 0, 0
             return 0, 0, 0, 0
@@ -554,22 +555,18 @@ class Geometry(object):
             if follow:
             if follow:
                 geo_iso = self.follow_geometry
                 geo_iso = self.follow_geometry
             else:
             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:
                 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:
                 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
         # end of replaced block
         if follow:
         if follow:
@@ -1282,7 +1279,7 @@ class Geometry(object):
         :return: Scaling factor resulting from unit change.
         :return: Scaling factor resulting from unit change.
         :rtype: float
         :rtype: float
         """
         """
-        log.debug("Geometry.convert_units()")
+        log.debug("camlib.Geometry.convert_units()")
 
 
         if units.upper() == self.units.upper():
         if units.upper() == self.units.upper():
             return 1.0
             return 1.0
@@ -1382,6 +1379,7 @@ class Geometry(object):
         :type point: list
         :type point: list
         :return: None
         :return: None
         """
         """
+        log.debug("camlib.Geometry.mirror()")
 
 
         px, py = point
         px, py = point
         xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
         xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
@@ -1393,7 +1391,10 @@ class Geometry(object):
                     new_obj.append(mirror_geom(g))
                     new_obj.append(mirror_geom(g))
                 return new_obj
                 return new_obj
             else:
             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:
         try:
             if self.multigeo is True:
             if self.multigeo is True:
@@ -1421,6 +1422,7 @@ class Geometry(object):
         See shapely manual for more information:
         See shapely manual for more information:
         http://toblerity.org/shapely/manual.html#affine-transformations
         http://toblerity.org/shapely/manual.html#affine-transformations
         """
         """
+        log.debug("camlib.Geometry.rotate()")
 
 
         px, py = point
         px, py = point
 
 
@@ -1431,7 +1433,10 @@ class Geometry(object):
                     new_obj.append(rotate_geom(g))
                     new_obj.append(rotate_geom(g))
                 return new_obj
                 return new_obj
             else:
             else:
-                return affinity.rotate(obj, angle, origin=(px, py))
+                try:
+                    return affinity.rotate(obj, angle, origin=(px, py))
+                except AttributeError:
+                    return obj
 
 
         try:
         try:
             if self.multigeo is True:
             if self.multigeo is True:
@@ -1458,6 +1463,8 @@ class Geometry(object):
         See shapely manual for more information:
         See shapely manual for more information:
         http://toblerity.org/shapely/manual.html#affine-transformations
         http://toblerity.org/shapely/manual.html#affine-transformations
         """
         """
+        log.debug("camlib.Geometry.skew()")
+
         px, py = point
         px, py = point
 
 
         def skew_geom(obj):
         def skew_geom(obj):
@@ -1467,7 +1474,10 @@ class Geometry(object):
                     new_obj.append(skew_geom(g))
                     new_obj.append(skew_geom(g))
                 return new_obj
                 return new_obj
             else:
             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:
         try:
             if self.multigeo is True:
             if self.multigeo is True:
@@ -2362,7 +2372,7 @@ class Gerber (Geometry):
                         "D-no zero suppression)" % self.gerber_zeros)
                         "D-no zero suppression)" % self.gerber_zeros)
                     log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute)
                     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)
                     log.debug("Gerber units found = %s" % self.gerber_units)
                     # Changed for issue #80
                     # Changed for issue #80
                     self.convert_units(match.group(5))
                     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
         # fixed issue of getting bounds only for one level lists of objects
         # now it can get bounds for nested 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:
         if self.solid_geometry is None:
             log.debug("solid_geometry is None")
             log.debug("solid_geometry is None")
             return 0, 0, 0, 0
             return 0, 0, 0, 0
@@ -3384,7 +3395,10 @@ class Gerber (Geometry):
                     new_obj.append(scale_geom(g))
                     new_obj.append(scale_geom(g))
                 return new_obj
                 return new_obj
             else:
             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.solid_geometry = scale_geom(self.solid_geometry)
         self.follow_geometry = scale_geom(self.follow_geometry)
         self.follow_geometry = scale_geom(self.follow_geometry)
@@ -3434,6 +3448,8 @@ class Gerber (Geometry):
         :type vect: tuple
         :type vect: tuple
         :return: None
         :return: None
         """
         """
+        log.debug("camlib.Gerber.offset()")
+
         try:
         try:
             dx, dy = vect
             dx, dy = vect
         except TypeError:
         except TypeError:
@@ -3448,7 +3464,10 @@ class Gerber (Geometry):
                     new_obj.append(offset_geom(g))
                     new_obj.append(offset_geom(g))
                 return new_obj
                 return new_obj
             else:
             else:
-                return affinity.translate(obj, xoff=dx, yoff=dy)
+                try:
+                    return affinity.translate(obj, xoff=dx, yoff=dy)
+                except AttributeError:
+                    return obj
 
 
         # ## Solid geometry
         # ## Solid geometry
         self.solid_geometry = offset_geom(self.solid_geometry)
         self.solid_geometry = offset_geom(self.solid_geometry)
@@ -3493,6 +3512,7 @@ class Gerber (Geometry):
         :type point: list
         :type point: list
         :return: None
         :return: None
         """
         """
+        log.debug("camlib.Gerber.mirror()")
 
 
         px, py = point
         px, py = point
         xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
         xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
@@ -3504,7 +3524,10 @@ class Gerber (Geometry):
                     new_obj.append(mirror_geom(g))
                     new_obj.append(mirror_geom(g))
                 return new_obj
                 return new_obj
             else:
             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.solid_geometry = mirror_geom(self.solid_geometry)
         self.follow_geometry = mirror_geom(self.follow_geometry)
         self.follow_geometry = mirror_geom(self.follow_geometry)
@@ -3540,6 +3563,7 @@ class Gerber (Geometry):
         See shapely manual for more information:
         See shapely manual for more information:
         http://toblerity.org/shapely/manual.html#affine-transformations
         http://toblerity.org/shapely/manual.html#affine-transformations
         """
         """
+        log.debug("camlib.Gerber.skew()")
 
 
         px, py = point
         px, py = point
 
 
@@ -3550,7 +3574,10 @@ class Gerber (Geometry):
                     new_obj.append(skew_geom(g))
                     new_obj.append(skew_geom(g))
                 return new_obj
                 return new_obj
             else:
             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.solid_geometry = skew_geom(self.solid_geometry)
         self.follow_geometry = skew_geom(self.follow_geometry)
         self.follow_geometry = skew_geom(self.follow_geometry)
@@ -3579,6 +3606,7 @@ class Gerber (Geometry):
         :param point:
         :param point:
         :return:
         :return:
         """
         """
+        log.debug("camlib.Gerber.rotate()")
 
 
         px, py = point
         px, py = point
 
 
@@ -3589,7 +3617,10 @@ class Gerber (Geometry):
                     new_obj.append(rotate_geom(g))
                     new_obj.append(rotate_geom(g))
                 return new_obj
                 return new_obj
             else:
             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.solid_geometry = rotate_geom(self.solid_geometry)
         self.follow_geometry = rotate_geom(self.follow_geometry)
         self.follow_geometry = rotate_geom(self.follow_geometry)
@@ -3983,10 +4014,10 @@ class Excellon(Geometry):
                         ':' + str(self.excellon_format_lower_in))
                         ':' + str(self.excellon_format_lower_in))
                     continue
                     continue
 
 
-                #### Body ## ##
+                # ### Body ####
                 if not in_header:
                 if not in_header:
 
 
-                    # ## Tool change # ##
+                    # ## Tool change ###
                     match = self.toolsel_re.search(eline)
                     match = self.toolsel_re.search(eline)
                     if match:
                     if match:
                         current_tool = str(int(match.group(1)))
                         current_tool = str(int(match.group(1)))
@@ -4026,7 +4057,7 @@ class Excellon(Geometry):
 
 
                         continue
                         continue
 
 
-                    # ## Allegro Type Tool change # ##
+                    # ## Allegro Type Tool change ###
                     if allegro_warning is True:
                     if allegro_warning is True:
                         match = self.absinc_re.search(eline)
                         match = self.absinc_re.search(eline)
                         match1 = self.stop_re.search(eline)
                         match1 = self.stop_re.search(eline)
@@ -4118,7 +4149,7 @@ class Excellon(Geometry):
                             )
                             )
                             continue
                             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
                         # get the coordinates for slot start and for slot stop into variables
                         start_coords_period = self.coordsperiod_re.search(start_coords_match)
                         start_coords_period = self.coordsperiod_re.search(start_coords_match)
                         stop_coords_period = self.coordsperiod_re.search(stop_coords_match)
                         stop_coords_period = self.coordsperiod_re.search(stop_coords_match)
@@ -4278,7 +4309,6 @@ class Excellon(Geometry):
                     if match:
                     if match:
                         # signal that there are drill operations
                         # signal that there are drill operations
                         self.defaults['excellon_drills'] = True
                         self.defaults['excellon_drills'] = True
-
                         try:
                         try:
                             x = float(match.group(1))
                             x = float(match.group(1))
                             repeating_x = current_x
                             repeating_x = current_x
@@ -4350,7 +4380,7 @@ class Excellon(Geometry):
                             # log.debug("{:15} {:8} {:8}".format(eline, x, y))
                             # log.debug("{:15} {:8} {:8}".format(eline, x, y))
                             continue
                             continue
 
 
-                #### Header ## ##
+                # ### Header ####
                 if in_header:
                 if in_header:
 
 
                     # ## Tool definitions # ##
                     # ## Tool definitions # ##
@@ -4488,7 +4518,7 @@ class Excellon(Geometry):
         match = self.leadingzeros_re.search(number_str)
         match = self.leadingzeros_re.search(number_str)
         nr_length = len(match.group(1)) + len(match.group(2))
         nr_length = len(match.group(1)) + len(match.group(2))
         try:
         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,
                 # With leading zeros, when you type in a coordinate,
                 # the leading zeros must always be included.  Trailing zeros
                 # the leading zeros must always be included.  Trailing zeros
                 # are unneeded and may be left off. The CNC-7 will automatically add them.
                 # 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
         # fixed issue of getting bounds only for one level lists of objects
         # now it can get bounds for nested 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):
         def bounds_rec(obj):
             if type(obj) is list:
             if type(obj) is list:
@@ -4669,6 +4699,8 @@ class Excellon(Geometry):
         :type str: IN or MM
         :type str: IN or MM
         :return:
         :return:
         """
         """
+        log.debug("camlib.Excellon.convert_units()")
+
         factor = Geometry.convert_units(self, units)
         factor = Geometry.convert_units(self, units)
 
 
         # Tools
         # Tools
@@ -4689,6 +4721,8 @@ class Excellon(Geometry):
         :return: None
         :return: None
         :rtype: NOne
         :rtype: NOne
         """
         """
+        log.debug("camlib.Excellon.scale()")
+
         if yfactor is None:
         if yfactor is None:
             yfactor = xfactor
             yfactor = xfactor
 
 
@@ -4705,8 +4739,10 @@ class Excellon(Geometry):
                     new_obj.append(scale_geom(g))
                     new_obj.append(scale_geom(g))
                 return new_obj
                 return new_obj
             else:
             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
         # Drills
         for drill in self.drills:
         for drill in self.drills:
@@ -4731,6 +4767,7 @@ class Excellon(Geometry):
         :type vect: tuple
         :type vect: tuple
         :return: None
         :return: None
         """
         """
+        log.debug("camlib.Excellon.offset()")
 
 
         dx, dy = vect
         dx, dy = vect
 
 
@@ -4741,7 +4778,10 @@ class Excellon(Geometry):
                     new_obj.append(offset_geom(g))
                     new_obj.append(offset_geom(g))
                 return new_obj
                 return new_obj
             else:
             else:
-                return affinity.translate(obj, xoff=dx, yoff=dy)
+                try:
+                    return affinity.translate(obj, xoff=dx, yoff=dy)
+                except AttributeError:
+                    return obj
 
 
         # Drills
         # Drills
         for drill in self.drills:
         for drill in self.drills:
@@ -4768,6 +4808,8 @@ class Excellon(Geometry):
         :type point: list
         :type point: list
         :return: None
         :return: None
         """
         """
+        log.debug("camlib.Excellon.mirror()")
+
         px, py = point
         px, py = point
         xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
         xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
 
 
@@ -4778,7 +4820,10 @@ class Excellon(Geometry):
                     new_obj.append(mirror_geom(g))
                     new_obj.append(mirror_geom(g))
                 return new_obj
                 return new_obj
             else:
             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
         # Modify data
         # Drills
         # Drills
@@ -4812,6 +4857,8 @@ class Excellon(Geometry):
         See shapely manual for more information:
         See shapely manual for more information:
         http://toblerity.org/shapely/manual.html#affine-transformations
         http://toblerity.org/shapely/manual.html#affine-transformations
         """
         """
+        log.debug("camlib.Excellon.skew()")
+
         if angle_x is None:
         if angle_x is None:
             angle_x = 0.0
             angle_x = 0.0
 
 
@@ -4825,7 +4872,10 @@ class Excellon(Geometry):
                     new_obj.append(skew_geom(g))
                     new_obj.append(skew_geom(g))
                 return new_obj
                 return new_obj
             else:
             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:
         if point is None:
             px, py = 0, 0
             px, py = 0, 0
@@ -4867,6 +4917,7 @@ class Excellon(Geometry):
         :param point: tuple of coordinates (x, y)
         :param point: tuple of coordinates (x, y)
         :return:
         :return:
         """
         """
+        log.debug("camlib.Excellon.rotate()")
 
 
         def rotate_geom(obj, origin=None):
         def rotate_geom(obj, origin=None):
             if type(obj) is list:
             if type(obj) is list:
@@ -4876,9 +4927,15 @@ class Excellon(Geometry):
                 return new_obj
                 return new_obj
             else:
             else:
                 if origin:
                 if origin:
-                    return affinity.rotate(obj, angle, origin=origin)
+                    try:
+                        return affinity.rotate(obj, angle, origin=origin)
+                    except AttributeError:
+                        return obj
                 else:
                 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:
         if point is None:
             # Drills
             # Drills
@@ -5026,6 +5083,11 @@ class CNCjob(Geometry):
 
 
         self.tool = 0.0
         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()
         # used for creating drill CCode geometry; will be updated in the generate_from_excellon_by_tool()
         self.exc_drills = None
         self.exc_drills = None
         self.exc_tools = None
         self.exc_tools = None
@@ -5048,8 +5110,9 @@ class CNCjob(Geometry):
         return self.__dict__
         return self.__dict__
 
 
     def convert_units(self, units):
     def convert_units(self, units):
+        log.debug("camlib.CNCJob.convert_units()")
+
         factor = Geometry.convert_units(self, units)
         factor = Geometry.convert_units(self, units)
-        log.debug("CNCjob.convert_units()")
 
 
         self.z_cut = float(self.z_cut) * factor
         self.z_cut = float(self.z_cut) * factor
         self.z_move *= factor
         self.z_move *= factor
@@ -5287,7 +5350,10 @@ class CNCjob(Geometry):
             self.oldx = 0.0
             self.oldx = 0.0
             self.oldy = 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]
         current_platform = platform.architecture()[0]
         if current_platform == '64bit':
         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.rapid_code, x=locx, y=locy)
                                 gcode += self.doformat(p.down_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:
                                 if self.f_retract is False:
                                     gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
                                     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)
                                 gcode += self.doformat(p.lift_code, x=locx, y=locy)
                                 measured_distance += abs(distance_euclidian(locx, locy, self.oldx, self.oldy))
                                 measured_distance += abs(distance_euclidian(locx, locy, self.oldx, self.oldy))
                                 self.oldx = locx
                                 self.oldx = locx
@@ -5479,10 +5553,19 @@ class CNCjob(Geometry):
                             for k in node_list:
                             for k in node_list:
                                 locx = locations[k][0]
                                 locx = locations[k][0]
                                 locy = locations[k][1]
                                 locy = locations[k][1]
+
                                 gcode += self.doformat(p.rapid_code, x=locx, y=locy)
                                 gcode += self.doformat(p.rapid_code, x=locx, y=locy)
                                 gcode += self.doformat(p.down_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:
                                 if self.f_retract is False:
                                     gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
                                     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)
                                 gcode += self.doformat(p.lift_code, x=locx, y=locy)
                                 measured_distance += abs(distance_euclidian(locx, locy, self.oldx, self.oldy))
                                 measured_distance += abs(distance_euclidian(locx, locy, self.oldx, self.oldy))
                                 self.oldx = locx
                                 self.oldx = locx
@@ -5539,8 +5622,16 @@ class CNCjob(Geometry):
                         for point in self.optimized_travelling_salesman(altPoints):
                         for point in self.optimized_travelling_salesman(altPoints):
                             gcode += self.doformat(p.rapid_code, x=point[0], y=point[1])
                             gcode += self.doformat(p.rapid_code, x=point[0], y=point[1])
                             gcode += self.doformat(p.down_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:
                             if self.f_retract is False:
                                 gcode += self.doformat(p.up_to_zero_code, x=point[0], y=point[1])
                                 gcode += self.doformat(p.up_to_zero_code, x=point[0], y=point[1])
+                                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])
                             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))
                             measured_distance += abs(distance_euclidian(point[0], point[1], self.oldx, self.oldy))
                             self.oldx = point[0]
                             self.oldx = point[0]
@@ -5560,6 +5651,15 @@ class CNCjob(Geometry):
                   str(measured_distance) + '\n')
                   str(measured_distance) + '\n')
         self.travel_distance = measured_distance
         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
         self.gcode = gcode
         return 'OK'
         return 'OK'
 
 
@@ -5664,6 +5764,10 @@ class CNCjob(Geometry):
                                  "There will be no cut, skipping %s file") % self.options['name'])
                                  "There will be no cut, skipping %s file") % self.options['name'])
             return 'fail'
             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:
         if self.z_move is None:
             self.app.inform.emit(_("[ERROR_NOTCL] Travel Z parameter is None or zero."))
             self.app.inform.emit(_("[ERROR_NOTCL] Travel Z parameter is None or zero."))
             return 'fail'
             return 'fail'
@@ -5734,6 +5838,9 @@ class CNCjob(Geometry):
             if self.dwell is True:
             if self.dwell is True:
                 self.gcode += self.doformat(p.dwell_code)   # Dwell time
                 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.
         # ## Iterate over geometry paths getting the nearest each time.
         log.debug("Starting G-Code...")
         log.debug("Starting G-Code...")
         path_count = 0
         path_count = 0
@@ -5755,21 +5862,41 @@ class CNCjob(Geometry):
 
 
                 # ---------- Single depth/pass --------
                 # ---------- Single depth/pass --------
                 if not multidepth:
                 if not multidepth:
+                    # calculate the cut distance
+                    total_cut = total_cut + geo.length
+
                     self.gcode += self.create_gcode_single_pass(geo, extracut, tolerance)
                     self.gcode += self.create_gcode_single_pass(geo, extracut, tolerance)
 
 
                 # --------- Multi-pass ---------
                 # --------- Multi-pass ---------
                 else:
                 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,
                     self.gcode += self.create_gcode_multi_pass(geo, extracut, tolerance,
                                                                postproc=p, current_point=current_pt)
                                                                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]
                 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.
         except StopIteration:  # Nothing found in storage.
             pass
             pass
 
 
         log.debug("Finishing G-Code... %s paths traced." % path_count)
         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
         # Finish
         self.gcode += self.doformat(p.spindle_stop_code)
         self.gcode += self.doformat(p.spindle_stop_code)
         self.gcode += self.doformat(p.lift_code, x=current_pt[0], y=current_pt[1])
         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)
         flat_geometry = self.flatten(temp_solid_geometry, pathonly=True)
         log.debug("%d paths" % len(flat_geometry))
         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_cut = float(z_cut) if z_cut else None
         self.z_move = float(z_move) if z_move 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:
             if self.dwell is True:
                 self.gcode += self.doformat(p.dwell_code)   # Dwell time
                 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.
         # Iterate over geometry paths getting the nearest each time.
         log.debug("Starting G-Code...")
         log.debug("Starting G-Code...")
         path_count = 0
         path_count = 0
@@ -6019,21 +6152,40 @@ class CNCjob(Geometry):
 
 
                 # ---------- Single depth/pass --------
                 # ---------- Single depth/pass --------
                 if not multidepth:
                 if not multidepth:
+                    # calculate the cut distance
+                    total_cut += geo.length
                     self.gcode += self.create_gcode_single_pass(geo, extracut, tolerance)
                     self.gcode += self.create_gcode_single_pass(geo, extracut, tolerance)
 
 
                 # --------- Multi-pass ---------
                 # --------- Multi-pass ---------
                 else:
                 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,
                     self.gcode += self.create_gcode_multi_pass(geo, extracut, tolerance,
                                                                postproc=p, current_point=current_pt)
                                                                postproc=p, current_point=current_pt)
 
 
+                # calculate the travel distance
+                total_travel += abs(distance(pt1=current_pt, pt2=pt))
                 current_pt = geo.coords[-1]
                 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.
         except StopIteration:  # Nothing found in storage.
             pass
             pass
 
 
         log.debug("Finishing G-Code... %s paths traced." % path_count)
         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
         # Finish
         self.gcode += self.doformat(p.spindle_stop_code)
         self.gcode += self.doformat(p.spindle_stop_code)
         self.gcode += self.doformat(p.lift_code, x=current_pt[0], y=current_pt[1])
         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:
         if tooldia is None:
             tooldia = self.tooldia
             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:
         if tooldia == 0:
             for geo in gcode_parsed:
             for geo in gcode_parsed:
                 if kind == 'all':
                 if kind == 'all':
@@ -6570,10 +6726,12 @@ class CNCjob(Geometry):
                     if geo['kind'][0] == 'C':
                     if geo['kind'][0] == 'C':
                         obj.add_shape(shape=poly, color=color['C'][1], face_color=color['C'][0],
                         obj.add_shape(shape=poly, color=color['C'][1], face_color=color['C'][0],
                                       visible=visible, layer=1)
                                       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):
     def create_geometry(self):
         # TODO: This takes forever. Too much data?
         # 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
         # fixed issue of getting bounds only for one level lists of objects
         # now it can get bounds for nested lists of objects
         # now it can get bounds for nested lists of objects
 
 
+        log.debug("camlib.CNCJob.bounds()")
+
         def bounds_rec(obj):
         def bounds_rec(obj):
             if type(obj) is list:
             if type(obj) is list:
                 minx = Inf
                 minx = Inf
@@ -6889,7 +7049,10 @@ class CNCjob(Geometry):
 
 
             bounds_coords = bounds_rec(self.solid_geometry)
             bounds_coords = bounds_rec(self.solid_geometry)
         else:
         else:
-
+            minx = Inf
+            miny = Inf
+            maxx = -Inf
+            maxy = -Inf
             for k, v in self.cnc_tools.items():
             for k, v in self.cnc_tools.items():
                 minx = Inf
                 minx = Inf
                 miny = Inf
                 miny = Inf
@@ -6926,6 +7089,7 @@ class CNCjob(Geometry):
         :return: None
         :return: None
         :rtype: None
         :rtype: None
         """
         """
+        log.debug("camlib.CNCJob.scale()")
 
 
         if yfactor is None:
         if yfactor is None:
             yfactor = xfactor
             yfactor = xfactor
@@ -7044,7 +7208,10 @@ class CNCjob(Geometry):
             self.gcode = scale_g(self.gcode)
             self.gcode = scale_g(self.gcode)
             # offset geometry
             # offset geometry
             for g in self.gcode_parsed:
             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()
             self.create_geometry()
         else:
         else:
             for k, v in self.cnc_tools.items():
             for k, v in self.cnc_tools.items():
@@ -7052,9 +7219,11 @@ class CNCjob(Geometry):
                 v['gcode'] = scale_g(v['gcode'])
                 v['gcode'] = scale_g(v['gcode'])
                 # scale gcode_parsed
                 # scale gcode_parsed
                 for g in v['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']])
                 v['solid_geometry'] = cascaded_union([geo['geom'] for geo in v['gcode_parsed']])
-
         self.create_geometry()
         self.create_geometry()
 
 
     def offset(self, vect):
     def offset(self, vect):
@@ -7070,6 +7239,8 @@ class CNCjob(Geometry):
         :type vect: tuple
         :type vect: tuple
         :return: None
         :return: None
         """
         """
+        log.debug("camlib.CNCJob.offset()")
+
         dx, dy = vect
         dx, dy = vect
 
 
         def offset_g(g):
         def offset_g(g):
@@ -7110,7 +7281,10 @@ class CNCjob(Geometry):
             self.gcode = offset_g(self.gcode)
             self.gcode = offset_g(self.gcode)
             # offset geometry
             # offset geometry
             for g in self.gcode_parsed:
             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()
             self.create_geometry()
         else:
         else:
             for k, v in self.cnc_tools.items():
             for k, v in self.cnc_tools.items():
@@ -7118,7 +7292,10 @@ class CNCjob(Geometry):
                 v['gcode'] = offset_g(v['gcode'])
                 v['gcode'] = offset_g(v['gcode'])
                 # offset gcode_parsed
                 # offset gcode_parsed
                 for g in v['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']])
                 v['solid_geometry'] = cascaded_union([geo['geom'] for geo in v['gcode_parsed']])
 
 
     def mirror(self, axis, point):
     def mirror(self, axis, point):
@@ -7128,12 +7305,16 @@ class CNCjob(Geometry):
         :param point: tupple of coordinates (x,y)
         :param point: tupple of coordinates (x,y)
         :return:
         :return:
         """
         """
+        log.debug("camlib.CNCJob.mirror()")
+
         px, py = point
         px, py = point
         xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
         xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
 
 
         for g in self.gcode_parsed:
         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()
         self.create_geometry()
 
 
     def skew(self, angle_x, angle_y, point):
     def skew(self, angle_x, angle_y, point):
@@ -7151,12 +7332,15 @@ class CNCjob(Geometry):
         See shapely manual for more information:
         See shapely manual for more information:
         http://toblerity.org/shapely/manual.html#affine-transformations
         http://toblerity.org/shapely/manual.html#affine-transformations
         """
         """
+        log.debug("camlib.CNCJob.skew()")
+
         px, py = point
         px, py = point
 
 
         for g in self.gcode_parsed:
         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()
         self.create_geometry()
 
 
     def rotate(self, angle, point):
     def rotate(self, angle, point):
@@ -7166,12 +7350,15 @@ class CNCjob(Geometry):
         :param point: tupple of coordinates (x,y)
         :param point: tupple of coordinates (x,y)
         :return:
         :return:
         """
         """
+        log.debug("camlib.CNCJob.rotate()")
 
 
         px, py = point
         px, py = point
 
 
         for g in self.gcode_parsed:
         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()
         self.create_geometry()
 
 
 
 

+ 1 - 0
config/configuration.txt

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

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 733 - 70
flatcamEditors/FlatCAMExcEditor.py


+ 240 - 168
flatcamEditors/FlatCAMGeoEditor.py

@@ -76,7 +76,9 @@ class BufferSelectionTool(FlatCAMTool):
         self.buffer_tools_box.addLayout(form_layout)
         self.buffer_tools_box.addLayout(form_layout)
 
 
         # Buffer distance
         # 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)
         form_layout.addRow(_("Buffer distance:"), self.buffer_distance_entry)
         self.buffer_corner_lbl = QtWidgets.QLabel(_("Buffer corner:"))
         self.buffer_corner_lbl = QtWidgets.QLabel(_("Buffer corner:"))
         self.buffer_corner_lbl.setToolTip(
         self.buffer_corner_lbl.setToolTip(
@@ -2358,10 +2360,6 @@ class FCSelect(DrawTool):
 
 
     def click_release(self, point):
     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
         # list where we store the overlapped shapes under our mouse left click position
         over_shape_list = []
         over_shape_list = []
 
 
@@ -2381,7 +2379,7 @@ class FCSelect(DrawTool):
 
 
             # 3rd method of click selection -> inconvenient
             # 3rd method of click selection -> inconvenient
             try:
             try:
-                _, closest_shape = self.storage.nearest(pos)
+                _, closest_shape = self.storage.nearest(point)
             except StopIteration:
             except StopIteration:
                 return ""
                 return ""
 
 
@@ -2400,30 +2398,28 @@ class FCSelect(DrawTool):
                 obj_to_add = over_shape_list[int(FlatCAMGeoEditor.draw_shape_idx)]
                 obj_to_add = over_shape_list[int(FlatCAMGeoEditor.draw_shape_idx)]
 
 
                 key_modifier = QtWidgets.QApplication.keyboardModifiers()
                 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.
                     # 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:
                     else:
-                        self.draw_app.selected = []
                         self.draw_app.selected.append(obj_to_add)
                         self.draw_app.selected.append(obj_to_add)
                 else:
                 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:
         except Exception as e:
             log.error("[ERROR] Something went bad. %s" % str(e))
             log.error("[ERROR] Something went bad. %s" % str(e))
             raise
             raise
+        return ""
 
 
 
 
 class FCMove(FCShapeTool):
 class FCMove(FCShapeTool):
@@ -2633,15 +2629,20 @@ class FCText(FCShapeTool):
         # Create new geometry
         # Create new geometry
         dx = point[0]
         dx = point[0]
         dy = point[1]
         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
             return
 
 
         self.text_gui.text_path = []
         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
         # 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)
         # 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
         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.app.ui.notebook.setTabText(2, _("Tools"))
         self.draw_app.app.ui.splitter.setSizes([0, 1])
         self.draw_app.app.ui.splitter.setSizes([0, 1])
 
 
         self.disactivate()
         self.disactivate()
+        if ret_val == 'fail':
+            return
         self.draw_app.app.inform.emit(_("[success] Done. Buffer Tool completed."))
         self.draw_app.app.inform.emit(_("[success] Done. Buffer Tool completed."))
 
 
     def on_buffer_int(self):
     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
         # 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)
         # 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
         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.app.ui.notebook.setTabText(2, _("Tools"))
         self.draw_app.app.ui.splitter.setSizes([0, 1])
         self.draw_app.app.ui.splitter.setSizes([0, 1])
 
 
         self.disactivate()
         self.disactivate()
+        if ret_val == 'fail':
+            return
         self.draw_app.app.inform.emit(_("[success] Done. Buffer Int Tool completed."))
         self.draw_app.app.inform.emit(_("[success] Done. Buffer Int Tool completed."))
 
 
     def on_buffer_ext(self):
     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
         # 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)
         # 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
         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.app.ui.notebook.setTabText(2, _("Tools"))
         self.draw_app.app.ui.splitter.setSizes([0, 1])
         self.draw_app.app.ui.splitter.setSizes([0, 1])
 
 
         self.disactivate()
         self.disactivate()
+        if ret_val == 'fail':
+            return
         self.draw_app.app.inform.emit(_("[success] Done. Buffer Ext Tool completed."))
         self.draw_app.app.inform.emit(_("[success] Done. Buffer Ext Tool completed."))
 
 
     def activate(self):
     def activate(self):
@@ -2912,14 +2919,14 @@ class FCTransform(FCShapeTool):
         self.draw_app = draw_app
         self.draw_app = draw_app
         self.app = draw_app.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.origin = (0, 0)
         self.draw_app.transform_tool.run()
         self.draw_app.transform_tool.run()
 
 
 
 
-# ##################### ##
-# # ## Main Application # ##
-# ##################### ##
+# ###############################################
+# ################ Main Application #############
+# ###############################################
 class FlatCAMGeoEditor(QtCore.QObject):
 class FlatCAMGeoEditor(QtCore.QObject):
 
 
     transform_complete = QtCore.pyqtSignal()
     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
         # store the status of the editor so the Delete at object level will not work until the edit is finished
         self.editor_active = False
         self.editor_active = False
+        log.debug("Initialization of the FlatCAM Geometry Editor is finished ...")
 
 
     def pool_recreated(self, pool):
     def pool_recreated(self, pool):
         self.shapes.pool = pool
         self.shapes.pool = pool
@@ -3169,6 +3177,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
 
         # Tell the App that the editor is active
         # Tell the App that the editor is active
         self.editor_active = True
         self.editor_active = True
+        log.debug("Finished activating the Geometry Editor...")
 
 
     def deactivate(self):
     def deactivate(self):
         try:
         try:
@@ -3248,6 +3257,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # Show original geometry
         # Show original geometry
         if self.fcgeometry:
         if self.fcgeometry:
             self.fcgeometry.visible = True
             self.fcgeometry.visible = True
+        log.debug("Finished deactivating the Geometry Editor...")
 
 
     def connect_canvas_event_handlers(self):
     def connect_canvas_event_handlers(self):
         # Canvas events
         # Canvas events
@@ -3277,7 +3287,22 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # Geometry Editor
         # Geometry Editor
         self.app.ui.draw_line.triggered.connect(self.draw_tool_path)
         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_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_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)
         self.app.ui.draw_move.triggered.connect(self.on_move)
 
 
     def disconnect_canvas_event_handlers(self):
     def disconnect_canvas_event_handlers(self):
@@ -3332,6 +3357,62 @@ class FlatCAMGeoEditor(QtCore.QObject):
         except (TypeError, AttributeError):
         except (TypeError, AttributeError):
             pass
             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):
     def add_shape(self, shape):
         """
         """
         Adds a shape to the shape storage.
         Adds a shape to the shape storage.
@@ -3495,9 +3576,9 @@ class FlatCAMGeoEditor(QtCore.QObject):
         :return: None
         :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.pos = self.app.geo_editor.snap(self.pos[0], self.pos[1])
             self.app.app_cursor.enabled = True
             self.app.app_cursor.enabled = True
             # Update cursor
             # Update cursor
@@ -3507,7 +3588,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
             self.pos = (self.pos[0], self.pos[1])
             self.pos = (self.pos[0], self.pos[1])
             self.app.app_cursor.enabled = False
             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>: "
             self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
                                                    "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
                                                    "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
 
 
@@ -3536,21 +3617,18 @@ class FlatCAMGeoEditor(QtCore.QObject):
                 if isinstance(self.active_tool, FCSelect):
                 if isinstance(self.active_tool, FCSelect):
                     # self.app.log.debug("Replotting after click.")
                     # self.app.log.debug("Replotting after click.")
                     self.replot()
                     self.replot()
-
             else:
             else:
                 self.app.log.debug("No active tool to respond to click!")
                 self.app.log.debug("No active tool to respond to click!")
 
 
     def on_canvas_move(self, event):
     def on_canvas_move(self, event):
         """
         """
         Called on 'mouse_move' event
         Called on 'mouse_move' event
-
         event.pos have canvas screen coordinates
         event.pos have canvas screen coordinates
 
 
         :param event: Event object dispatched by VisPy SceneCavas
         :param event: Event object dispatched by VisPy SceneCavas
         :return: None
         :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]
         event.xdata, event.ydata = pos[0], pos[1]
 
 
         self.x = event.xdata
         self.x = event.xdata
@@ -3559,8 +3637,14 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.app.ui.popMenu.mouse_is_panning = False
         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 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
             return
 
 
         try:
         try:
@@ -3569,11 +3653,8 @@ class FlatCAMGeoEditor(QtCore.QObject):
         except TypeError:
         except TypeError:
             return
             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)
             x, y = self.snap(x, y)
             self.app.app_cursor.enabled = True
             self.app.app_cursor.enabled = True
             # Update cursor
             # 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>: "
         self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
                                            "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (dx, dy))
                                            "%.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
             pass
         else:
         else:
-            # # ## Utility geometry (animated)
+            # ### Utility geometry (animated) ###
             geo = self.active_tool.utility_geometry(data=(x, y))
             geo = self.active_tool.utility_geometry(data=(x, y))
             if isinstance(geo, DrawToolShape) and geo.geo is not None:
             if isinstance(geo, DrawToolShape) and geo.geo is not None:
                 # Remove any previous utility shape
                 # Remove any previous utility shape
                 self.tool_shape.clear(update=True)
                 self.tool_shape.clear(update=True)
                 self.draw_utility_geometry(geo=geo)
                 self.draw_utility_geometry(geo=geo)
 
 
-        # # ## Selection area on canvas section # ##
+        # ### Selection area on canvas section ###
         dx = pos[0] - self.pos[0]
         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()
             self.app.delete_selection_shape()
             if dx < 0:
             if dx < 0:
                 self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x, y),
                 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
             self.app.selection_type = None
 
 
     def on_geo_click_release(self, event):
     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])
             pos = self.snap(pos_canvas[0], pos_canvas[1])
         else:
         else:
             pos = (pos_canvas[0], pos_canvas[1])
             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
         # 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
         # canvas menu
         try:
         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:
                     if self.in_action is False:
                         try:
                         try:
                             QtGui.QGuiApplication.restoreOverrideCursor()
                             QtGui.QGuiApplication.restoreOverrideCursor()
@@ -3661,38 +3754,6 @@ class FlatCAMGeoEditor(QtCore.QObject):
                                 self.on_shape_complete()
                                 self.on_shape_complete()
                                 self.app.inform.emit(_("[success] Done."))
                                 self.app.inform.emit(_("[success] Done."))
                                 self.select_tool(self.active_tool.name)
                                 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:
         except Exception as e:
             log.warning("Error: %s" % str(e))
             log.warning("Error: %s" % str(e))
             return
             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])])
         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()
         self.app.delete_selection_shape()
+
+        sel_objects_list = []
         for obj in self.storage.get_objects():
         for obj in self.storage.get_objects():
             if (sel_type is True and poly_selection.contains(obj.geo)) or (sel_type is False and
             if (sel_type is True and poly_selection.contains(obj.geo)) or (sel_type is False and
                                                                            poly_selection.intersects(obj.geo)):
                                                                            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()
         self.replot()
 
 
     def draw_utility_geometry(self, geo):
     def draw_utility_geometry(self, geo):
@@ -4107,8 +4183,10 @@ class FlatCAMGeoEditor(QtCore.QObject):
         selected = self.get_selected()
         selected = self.get_selected()
         try:
         try:
             tools = selected[1:]
             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_deletion = [s for s in self.get_selected()]
             for shape in for_deletion:
             for shape in for_deletion:
@@ -4169,11 +4247,11 @@ class FlatCAMGeoEditor(QtCore.QObject):
             # deselect everything
             # deselect everything
             self.selected = []
             self.selected = []
             self.replot()
             self.replot()
-            return
+            return 'fail'
 
 
         if len(selected) == 0:
         if len(selected) == 0:
             self.app.inform.emit(_("[WARNING_NOTCL] Nothing selected for buffering."))
             self.app.inform.emit(_("[WARNING_NOTCL] Nothing selected for buffering."))
-            return
+            return 'fail'
 
 
         if not isinstance(buf_distance, float):
         if not isinstance(buf_distance, float):
             self.app.inform.emit(_("[WARNING_NOTCL] Invalid distance for buffering."))
             self.app.inform.emit(_("[WARNING_NOTCL] Invalid distance for buffering."))
@@ -4181,17 +4259,32 @@ class FlatCAMGeoEditor(QtCore.QObject):
             # deselect everything
             # deselect everything
             self.selected = []
             self.selected = []
             self.replot()
             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."))
             self.app.inform.emit(_("[ERROR_NOTCL] Failed, the result is empty. Choose a different buffer value."))
             # deselect everything
             # deselect everything
             self.selected = []
             self.selected = []
             self.replot()
             self.replot()
-            return
-        self.add_shape(DrawToolShape(results))
+            return 'fail'
+
+        for sha in results:
+            self.add_shape(DrawToolShape(sha))
 
 
         self.replot()
         self.replot()
         self.app.inform.emit(_("[success] Full buffer geometry created."))
         self.app.inform.emit(_("[success] Full buffer geometry created."))
@@ -4201,77 +4294,48 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
 
         if buf_distance < 0:
         if buf_distance < 0:
             self.app.inform.emit(
             self.app.inform.emit(
-                _("[ERROR_NOTCL] Negative buffer value is not accepted. "
-                  "Use Buffer interior to generate an 'inside' shape")
+                _("[ERROR_NOTCL] Negative buffer value is not accepted.")
             )
             )
             # deselect everything
             # deselect everything
             self.selected = []
             self.selected = []
             self.replot()
             self.replot()
-            return
+            return 'fail'
 
 
         if len(selected) == 0:
         if len(selected) == 0:
             self.app.inform.emit(_("[WARNING_NOTCL] Nothing selected for buffering."))
             self.app.inform.emit(_("[WARNING_NOTCL] Nothing selected for buffering."))
-            return
+            return 'fail'
 
 
         if not isinstance(buf_distance, float):
         if not isinstance(buf_distance, float):
             self.app.inform.emit(_("[WARNING_NOTCL] Invalid distance for buffering."))
             self.app.inform.emit(_("[WARNING_NOTCL] Invalid distance for buffering."))
             # deselect everything
             # deselect everything
             self.selected = []
             self.selected = []
             self.replot()
             self.replot()
-            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."))
             self.app.inform.emit(_("[ERROR_NOTCL] Failed, the result is empty. Choose a smaller buffer value."))
             # deselect everything
             # deselect everything
             self.selected = []
             self.selected = []
             self.replot()
             self.replot()
-            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.replot()
         self.app.inform.emit(_("[success] Interior buffer geometry created."))
         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):
     def buffer_ext(self, buf_distance, join_style):
         selected = self.get_selected()
         selected = self.get_selected()
@@ -4295,19 +4359,27 @@ class FlatCAMGeoEditor(QtCore.QObject):
             self.replot()
             self.replot()
             return
             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."))
             self.app.inform.emit(_("[ERROR_NOTCL] Failed, the result is empty. Choose a different buffer value."))
             # deselect everything
             # deselect everything
             self.selected = []
             self.selected = []
             self.replot()
             self.replot()
             return
             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.replot()
         self.app.inform.emit(_("[success] Exterior buffer geometry created."))
         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 import QtGui, QtCore, QtWidgets
 from PyQt5.QtCore import Qt, QSettings
 from PyQt5.QtCore import Qt, QSettings
 
 
@@ -530,7 +538,7 @@ class FCPadArray(FCShapeTool):
                         )
                         )
                     if 'follow' in geo_el:
                     if 'follow' in geo_el:
                         new_geo_el['follow'] = affinity.translate(
                         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)
                     geo_el_list.append(new_geo_el)
 
 
@@ -839,6 +847,8 @@ class FCRegion(FCShapeTool):
         self.name = 'region'
         self.name = 'region'
         self.draw_app = draw_app
         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'])
         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
         self.buf_val = (size_ap / 2) if size_ap > 0 else 0.0000001
 
 
@@ -885,7 +895,7 @@ class FCRegion(FCShapeTool):
         y = data[1]
         y = data[1]
 
 
         if len(self.points) == 0:
         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)
             return DrawToolUtilityShape(new_geo_el)
 
 
         if len(self.points) == 1:
         if len(self.points) == 1:
@@ -951,12 +961,15 @@ class FCRegion(FCShapeTool):
 
 
             if len(self.temp_points) > 1:
             if len(self.temp_points) > 1:
                 try:
                 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)
                     return DrawToolUtilityShape(new_geo_el)
                 except Exception as e:
                 except Exception as e:
                     log.debug("FlatCAMGrbEditor.FCRegion.utility_geometry() --> %s" % str(e))
                     log.debug("FlatCAMGrbEditor.FCRegion.utility_geometry() --> %s" % str(e))
             else:
             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)
                 return DrawToolUtilityShape(new_geo_el)
 
 
         if len(self.points) > 2:
         if len(self.points) > 2:
@@ -1012,7 +1025,9 @@ class FCRegion(FCShapeTool):
             self.temp_points.append(data)
             self.temp_points.append(data)
             new_geo_el = dict()
             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)
             new_geo_el['follow'] = LinearRing(self.temp_points)
 
 
             return DrawToolUtilityShape(new_geo_el)
             return DrawToolUtilityShape(new_geo_el)
@@ -1031,7 +1046,9 @@ class FCRegion(FCShapeTool):
 
 
             new_geo_el = dict()
             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
             new_geo_el['follow'] = Polygon(self.points).exterior
 
 
             self.geometry = DrawToolShape(new_geo_el)
             self.geometry = DrawToolShape(new_geo_el)
@@ -1128,10 +1145,12 @@ class FCTrack(FCRegion):
     def make(self):
     def make(self):
         new_geo_el = dict()
         new_geo_el = dict()
         if len(self.temp_points) == 1:
         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)
             new_geo_el['follow'] = Point(self.temp_points)
         else:
         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)
             new_geo_el['follow'] = LineString(self.temp_points)
 
 
         self.geometry = DrawToolShape(new_geo_el)
         self.geometry = DrawToolShape(new_geo_el)
@@ -1156,10 +1175,12 @@ class FCTrack(FCRegion):
         new_geo_el = dict()
         new_geo_el = dict()
 
 
         if len(self.temp_points) == 1:
         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)
             new_geo_el['follow'] = Point(self.temp_points)
         else:
         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)
             new_geo_el['follow'] = LineString(self.temp_points)
 
 
         self.draw_app.add_gerber_shape(DrawToolShape(new_geo_el),
         self.draw_app.add_gerber_shape(DrawToolShape(new_geo_el),
@@ -1177,7 +1198,8 @@ class FCTrack(FCRegion):
         new_geo_el = dict()
         new_geo_el = dict()
 
 
         if len(self.points) == 0:
         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)
             return DrawToolUtilityShape(new_geo_el)
         elif len(self.points) > 0:
         elif len(self.points) > 0:
@@ -1235,10 +1257,12 @@ class FCTrack(FCRegion):
 
 
             self.temp_points.append(data)
             self.temp_points.append(data)
             if len(self.temp_points) == 1:
             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)
                 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)
             return DrawToolUtilityShape(new_geo_el)
 
 
     def on_key(self, key):
     def on_key(self, key):
@@ -1775,6 +1799,9 @@ class FCMarkArea(FCShapeTool):
         self.draw_app.hide_tool('all')
         self.draw_app.hide_tool('all')
         self.draw_app.ma_tool_frame.show()
         self.draw_app.ma_tool_frame.show()
 
 
+        # clear previous marking
+        self.draw_app.ma_annotation.clear(update=True)
+
         try:
         try:
             self.draw_app.ma_threshold__button.clicked.disconnect()
             self.draw_app.ma_threshold__button.clicked.disconnect()
         except (TypeError, AttributeError):
         except (TypeError, AttributeError):
@@ -2168,6 +2195,9 @@ class FCApertureSelect(DrawTool):
         # bending modes using in FCRegion and FCTrack
         # bending modes using in FCRegion and FCTrack
         self.draw_app.bend_mode = 1
         self.draw_app.bend_mode = 1
 
 
+        # here store the selected apertures
+        self.sel_aperture = set()
+
         try:
         try:
             self.grb_editor_app.apertures_table.clearSelection()
             self.grb_editor_app.apertures_table.clearSelection()
         except Exception as e:
         except Exception as e:
@@ -2175,6 +2205,7 @@ class FCApertureSelect(DrawTool):
 
 
         self.grb_editor_app.hide_tool('all')
         self.grb_editor_app.hide_tool('all')
         self.grb_editor_app.hide_tool('select')
         self.grb_editor_app.hide_tool('select')
+        self.grb_editor_app.array_frame.hide()
 
 
         try:
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
             QtGui.QGuiApplication.restoreOverrideCursor()
@@ -2186,42 +2217,47 @@ class FCApertureSelect(DrawTool):
 
 
     def click(self, point):
     def click(self, point):
         key_modifier = QtWidgets.QApplication.keyboardModifiers()
         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:
         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):
     def click_release(self, point):
         self.grb_editor_app.apertures_table.clearSelection()
         self.grb_editor_app.apertures_table.clearSelection()
-        sel_aperture = set()
         key_modifier = QtWidgets.QApplication.keyboardModifiers()
         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:
         for storage in self.grb_editor_app.storage_dict:
             try:
             try:
                 for geo_el in self.grb_editor_app.storage_dict[storage]['geometry']:
                 for geo_el in self.grb_editor_app.storage_dict[storage]['geometry']:
                     if 'solid' in geo_el.geo:
                     if 'solid' in geo_el.geo:
                         geometric_data = geo_el.geo['solid']
                         geometric_data = geo_el.geo['solid']
                         if Point(point).within(geometric_data):
                         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:
                                 if geo_el in self.draw_app.selected:
                                     self.draw_app.selected.remove(geo_el)
                                     self.draw_app.selected.remove(geo_el)
+                                    self.sel_aperture.remove(storage)
                                 else:
                                 else:
                                     # add the object to the selected shapes
                                     # add the object to the selected shapes
                                     self.draw_app.selected.append(geo_el)
                                     self.draw_app.selected.append(geo_el)
-                                    sel_aperture.add(storage)
+                                    self.sel_aperture.add(storage)
                             else:
                             else:
                                 self.draw_app.selected.append(geo_el)
                                 self.draw_app.selected.append(geo_el)
-                                sel_aperture.add(storage)
+                                self.sel_aperture.add(storage)
             except KeyError:
             except KeyError:
                 pass
                 pass
 
 
@@ -2232,7 +2268,7 @@ class FCApertureSelect(DrawTool):
             log.debug("FlatCAMGrbEditor.FCApertureSelect.click_release() --> %s" % str(e))
             log.debug("FlatCAMGrbEditor.FCApertureSelect.click_release() --> %s" % str(e))
 
 
         self.grb_editor_app.apertures_table.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
         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()):
             for row in range(self.grb_editor_app.apertures_table.rowCount()):
                 if str(aper) == self.grb_editor_app.apertures_table.item(row, 1).text():
                 if str(aper) == self.grb_editor_app.apertures_table.item(row, 1).text():
                     self.grb_editor_app.apertures_table.selectRow(row)
                     self.grb_editor_app.apertures_table.selectRow(row)
@@ -2318,7 +2354,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # #########################
         # #########################
         # ### Gerber Apertures ####
         # ### 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(
         self.apertures_table_label.setToolTip(
             _("Apertures Table for the Gerber Object.")
             _("Apertures Table for the Gerber Object.")
         )
         )
@@ -2364,7 +2400,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         grid1 = QtWidgets.QGridLayout()
         grid1 = QtWidgets.QGridLayout()
         self.apertures_box.addLayout(grid1)
         self.apertures_box.addLayout(grid1)
 
 
-        apcode_lbl = QtWidgets.QLabel(_('Aperture Code:'))
+        apcode_lbl = QtWidgets.QLabel('%s:' % _('Aperture Code'))
         apcode_lbl.setToolTip(
         apcode_lbl.setToolTip(
         _("Code for the new aperture")
         _("Code for the new aperture")
         )
         )
@@ -2374,7 +2410,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.apcode_entry.setValidator(QtGui.QIntValidator(0, 999))
         self.apcode_entry.setValidator(QtGui.QIntValidator(0, 999))
         grid1.addWidget(self.apcode_entry, 1, 1)
         grid1.addWidget(self.apcode_entry, 1, 1)
 
 
-        apsize_lbl = QtWidgets.QLabel(_('Aperture Size:'))
+        apsize_lbl = QtWidgets.QLabel('%s:' % _('Aperture Size'))
         apsize_lbl.setToolTip(
         apsize_lbl.setToolTip(
         _("Size for the new aperture.\n"
         _("Size for the new aperture.\n"
           "If aperture type is 'R' or 'O' then\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))
         self.apsize_entry.setValidator(QtGui.QDoubleValidator(0.0001, 99.9999, 4))
         grid1.addWidget(self.apsize_entry, 2, 1)
         grid1.addWidget(self.apsize_entry, 2, 1)
 
 
-        aptype_lbl = QtWidgets.QLabel(_('Aperture Type:'))
+        aptype_lbl = QtWidgets.QLabel('%s:' % _('Aperture Type'))
         aptype_lbl.setToolTip(
         aptype_lbl.setToolTip(
         _("Select the type of new aperture. Can be:\n"
         _("Select the type of new aperture. Can be:\n"
           "C = circular\n"
           "C = circular\n"
@@ -2401,7 +2437,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.aptype_cb.addItems(['C', 'R', 'O'])
         self.aptype_cb.addItems(['C', 'R', 'O'])
         grid1.addWidget(self.aptype_cb, 3, 1)
         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(
         self.apdim_lbl.setToolTip(
         _("Dimensions for the new aperture.\n"
         _("Dimensions for the new aperture.\n"
           "Active only for rectangular apertures (type R).\n"
           "Active only for rectangular apertures (type R).\n"
@@ -2457,8 +2493,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
 
         # Buffer distance
         # Buffer distance
         self.buffer_distance_entry = FCEntry()
         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(
         self.buffer_corner_lbl.setToolTip(
             _("There are 3 types of corners:\n"
             _("There are 3 types of corners:\n"
               " - 'Round': the corner is rounded.\n"
               " - 'Round': the corner is rounded.\n"
@@ -2490,7 +2526,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.scale_tool_frame.hide()
         self.scale_tool_frame.hide()
 
 
         # Title
         # 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_title_lbl.setToolTip(
             _("Scale a aperture in the aperture list")
             _("Scale a aperture in the aperture list")
         )
         )
@@ -2500,7 +2536,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         scale_form_layout = QtWidgets.QFormLayout()
         scale_form_layout = QtWidgets.QFormLayout()
         self.scale_tools_box.addLayout(scale_form_layout)
         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(
         self.scale_factor_lbl.setToolTip(
             _("The factor by which to scale the selected aperture.\n"
             _("The factor by which to scale the selected aperture.\n"
               "Values can be between 0.0000 and 999.9999")
               "Values can be between 0.0000 and 999.9999")
@@ -2528,7 +2564,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.ma_tool_frame.hide()
         self.ma_tool_frame.hide()
 
 
         # Title
         # 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(
         ma_title_lbl.setToolTip(
             _("Mark the polygon areas.")
             _("Mark the polygon areas.")
         )
         )
@@ -2538,7 +2574,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         ma_form_layout = QtWidgets.QFormLayout()
         ma_form_layout = QtWidgets.QFormLayout()
         self.ma_tools_box.addLayout(ma_form_layout)
         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(
         self.ma_upper_threshold_lbl.setToolTip(
             _("The threshold value, all areas less than this are marked.\n"
             _("The threshold value, all areas less than this are marked.\n"
               "Can have a value between 0.0000 and 9999.9999")
               "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 = FCEntry()
         self.ma_upper_threshold_entry.setValidator(QtGui.QDoubleValidator(0.0000, 9999.9999, 4))
         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(
         self.ma_lower_threshold_lbl.setToolTip(
             _("The threshold value, all areas more than this are marked.\n"
             _("The threshold value, all areas more than this are marked.\n"
               "Can have a value between 0.0000 and 9999.9999")
               "Can have a value between 0.0000 and 9999.9999")
@@ -2567,7 +2603,6 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # ######################
         # ######################
         # ### Add Pad Array ####
         # ### Add Pad Array ####
         # ######################
         # ######################
-
         # add a frame and inside add a vertical box layout. Inside this vbox layout I add
         # add a frame and inside add a vertical box layout. Inside this vbox layout I add
         # all the add Pad array  widgets
         # all the add Pad array  widgets
         # this way I can hide/show the frame
         # this way I can hide/show the frame
@@ -2600,7 +2635,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.array_form = QtWidgets.QFormLayout()
         self.array_form = QtWidgets.QFormLayout()
         self.array_box.addLayout(self.array_form)
         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(
         self.pad_array_size_label.setToolTip(
             _("Specify how many pads to be in the array.")
             _("Specify how many pads to be in the array.")
         )
         )
@@ -2619,7 +2654,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.linear_form = QtWidgets.QFormLayout()
         self.linear_form = QtWidgets.QFormLayout()
         self.linear_box.addLayout(self.linear_form)
         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(
         self.pad_axis_label.setToolTip(
             _("Direction on which the linear array is oriented:\n"
             _("Direction on which the linear array is oriented:\n"
               "- 'X' - horizontal axis \n"
               "- 'X' - horizontal axis \n"
@@ -2634,7 +2669,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.pad_axis_radio.set_value('X')
         self.pad_axis_radio.set_value('X')
         self.linear_form.addRow(self.pad_axis_label, self.pad_axis_radio)
         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(
         self.pad_pitch_label.setToolTip(
             _("Pitch = Distance between elements of the array.")
             _("Pitch = Distance between elements of the array.")
         )
         )
@@ -2643,7 +2678,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.pad_pitch_entry = LengthEntry()
         self.pad_pitch_entry = LengthEntry()
         self.linear_form.addRow(self.pad_pitch_label, self.pad_pitch_entry)
         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(
         self.linear_angle_label.setToolTip(
            _( "Angle at which the linear array is placed.\n"
            _( "Angle at which the linear array is placed.\n"
               "The precision is of max 2 decimals.\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.circular_box.setContentsMargins(0, 0, 0, 0)
         self.array_circular_frame.setLayout(self.circular_box)
         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(
         self.pad_direction_label.setToolTip(
            _("Direction for circular array."
            _("Direction for circular array."
              "Can be CW = clockwise or CCW = counter clockwise.")
              "Can be CW = clockwise or CCW = counter clockwise.")
@@ -2679,7 +2714,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.pad_direction_radio.set_value('CW')
         self.pad_direction_radio.set_value('CW')
         self.circular_form.addRow(self.pad_direction_label, self.pad_direction_radio)
         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(
         self.pad_angle_label.setToolTip(
             _("Angle at which each element in circular array is placed.")
             _("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)
         # this will flag if the Editor "tools" are launched from key shortcuts (True) or from menu toolbar (False)
         self.launched_from_shortcuts = False
         self.launched_from_shortcuts = False
 
 
+        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 make_callback(the_tool):
             def f():
             def f():
                 self.on_tool_select(the_tool)
                 self.on_tool_select(the_tool)
@@ -2884,6 +2924,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.conversion_factor = 1
         self.conversion_factor = 1
 
 
         self.set_ui()
         self.set_ui()
+        log.debug("Initialization of the FlatCAM Gerber Editor is finished ...")
 
 
     def pool_recreated(self, pool):
     def pool_recreated(self, pool):
         self.shapes.pool = pool
         self.shapes.pool = pool
@@ -2911,23 +2952,24 @@ class FlatCAMGrbEditor(QtCore.QObject):
             self.tool2tooldia[i + 1] = tt_aperture
             self.tool2tooldia[i + 1] = tt_aperture
 
 
         # Init GUI
         # 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):
     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)
             self.apcode_entry.set_value(max(self.tool2tooldia.values()) + 1)
         except ValueError:
         except ValueError:
             # this means that the edited object has no apertures so we start with 10 (Gerber specifications)
             # 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):
     def on_aperture_add(self, apid=None):
         self.is_modified = True
         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_track.triggered.connect(self.on_track_add)
         self.app.ui.grb_draw_region.triggered.connect(self.on_region_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):
     def disconnect_canvas_event_handlers(self):
 
 
         # we restore the key and mouse control to FlatCAMApp method
         # we restore the key and mouse control to FlatCAMApp method
@@ -3527,6 +3579,39 @@ class FlatCAMGrbEditor(QtCore.QObject):
         except (TypeError, AttributeError):
         except (TypeError, AttributeError):
             pass
             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):
     def clear(self):
         self.active_tool = None
         self.active_tool = None
         self.selected = []
         self.selected = []
@@ -3984,9 +4069,9 @@ class FlatCAMGrbEditor(QtCore.QObject):
         :return: None
         :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.pos = self.app.geo_editor.snap(self.pos[0], self.pos[1])
             self.app.app_cursor.enabled = True
             self.app.app_cursor.enabled = True
             # Update cursor
             # Update cursor
@@ -4047,8 +4132,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
     def on_grb_click_release(self, event):
     def on_grb_click_release(self, event):
         self.modifiers = QtWidgets.QApplication.keyboardModifiers()
         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])
             pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
         else:
         else:
             pos = (pos_canvas[0], pos_canvas[1])
             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
         # select the aperture code of the selected geometry, in the tool table
         self.apertures_table.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
         self.apertures_table.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
         for aper in sel_aperture:
         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.last_aperture_selected = aper
         self.apertures_table.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
         self.apertures_table.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
 
 
@@ -4178,7 +4264,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         :return: None
         :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]
         event.xdata, event.ydata = pos_canvas[0], pos_canvas[1]
 
 
         self.x = event.xdata
         self.x = event.xdata
@@ -4201,7 +4287,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             return
             return
 
 
         # # ## Snap coordinates
         # # ## Snap coordinates
-        if self.app.grid_status():
+        if self.app.grid_status() == True:
             x, y = self.app.geo_editor.snap(x, y)
             x, y = self.app.geo_editor.snap(x, y)
             self.app.app_cursor.enabled = True
             self.app.app_cursor.enabled = True
             # Update cursor
             # Update cursor
@@ -4325,11 +4411,11 @@ class FlatCAMGrbEditor(QtCore.QObject):
             geometry = self.active_tool.geometry
             geometry = self.active_tool.geometry
 
 
         try:
         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:
         except AttributeError:
             if type(geometry) == Point:
             if type(geometry) == Point:
                 return
                 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):
     def start_delayed_plot(self, check_period):
         """
         """

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 445 - 59
flatcamGUI/FlatCAMGUI.py


+ 127 - 50
flatcamGUI/GUIElements.py

@@ -171,9 +171,11 @@ class LengthEntry(QtWidgets.QLineEdit):
             self.readyToEdit = False
             self.readyToEdit = False
 
 
     def focusOutEvent(self, e):
     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):
     def returnPressed(self, *args, **kwargs):
         val = self.get_value()
         val = self.get_value()
@@ -225,9 +227,11 @@ class FloatEntry(QtWidgets.QLineEdit):
             self.readyToEdit = False
             self.readyToEdit = False
 
 
     def focusOutEvent(self, e):
     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):
     def returnPressed(self, *args, **kwargs):
         val = self.get_value()
         val = self.get_value()
@@ -274,9 +278,11 @@ class FloatEntry2(QtWidgets.QLineEdit):
             self.readyToEdit = False
             self.readyToEdit = False
 
 
     def focusOutEvent(self, e):
     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):
     def get_value(self):
         raw = str(self.text()).strip(' ')
         raw = str(self.text()).strip(' ')
@@ -316,9 +322,11 @@ class IntEntry(QtWidgets.QLineEdit):
             self.readyToEdit = False
             self.readyToEdit = False
 
 
     def focusOutEvent(self, e):
     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):
     def get_value(self):
 
 
@@ -353,16 +361,17 @@ class FCEntry(QtWidgets.QLineEdit):
     def on_edit_finished(self):
     def on_edit_finished(self):
         self.clearFocus()
         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
         super(FCEntry, self).mousePressEvent(e)  # required to deselect on 2e click
         if self.readyToEdit:
         if self.readyToEdit:
             self.selectAll()
             self.selectAll()
             self.readyToEdit = False
             self.readyToEdit = False
 
 
     def focusOutEvent(self, e):
     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):
     def get_value(self):
         return str(self.text())
         return str(self.text())
@@ -381,36 +390,24 @@ class FCEntry(QtWidgets.QLineEdit):
 class FCEntry2(FCEntry):
 class FCEntry2(FCEntry):
     def __init__(self, parent=None):
     def __init__(self, parent=None):
         super(FCEntry2, self).__init__(parent)
         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):
     def set_value(self, val, decimals=4):
         try:
         try:
             fval = float(val)
             fval = float(val)
         except ValueError:
         except ValueError:
             return
             return
-
         self.setText('%.*f' % (decimals, fval))
         self.setText('%.*f' % (decimals, fval))
 
 
 
 
 class FCEntry3(FCEntry):
 class FCEntry3(FCEntry):
     def __init__(self, parent=None):
     def __init__(self, parent=None):
         super(FCEntry3, self).__init__(parent)
         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):
     def set_value(self, val, decimals=4):
         try:
         try:
             fval = float(val)
             fval = float(val)
         except ValueError:
         except ValueError:
             return
             return
-
         self.setText('%.*f' % (decimals, fval))
         self.setText('%.*f' % (decimals, fval))
 
 
     def get_value(self):
     def get_value(self):
@@ -432,16 +429,17 @@ class EvalEntry(QtWidgets.QLineEdit):
     def on_edit_finished(self):
     def on_edit_finished(self):
         self.clearFocus()
         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
         super(EvalEntry, self).mousePressEvent(e)  # required to deselect on 2e click
         if self.readyToEdit:
         if self.readyToEdit:
             self.selectAll()
             self.selectAll()
             self.readyToEdit = False
             self.readyToEdit = False
 
 
     def focusOutEvent(self, e):
     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):
     def returnPressed(self, *args, **kwargs):
         val = self.get_value()
         val = self.get_value()
@@ -478,16 +476,17 @@ class EvalEntry2(QtWidgets.QLineEdit):
     def on_edit_finished(self):
     def on_edit_finished(self):
         self.clearFocus()
         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
         super(EvalEntry2, self).mousePressEvent(e)  # required to deselect on 2e click
         if self.readyToEdit:
         if self.readyToEdit:
             self.selectAll()
             self.selectAll()
             self.readyToEdit = False
             self.readyToEdit = False
 
 
     def focusOutEvent(self, e):
     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):
     def get_value(self):
         raw = str(self.text()).strip(' ')
         raw = str(self.text()).strip(' ')
@@ -847,9 +846,9 @@ class FCDetachableTab(QtWidgets.QTabWidget):
         super().__init__()
         super().__init__()
 
 
         self.tabBar = self.FCTabBar(self)
         self.tabBar = self.FCTabBar(self)
-        self.tabBar.onDetachTabSignal.connect(self.detachTab)
         self.tabBar.onMoveTabSignal.connect(self.moveTab)
         self.tabBar.onMoveTabSignal.connect(self.moveTab)
         self.tabBar.detachedTabDropSignal.connect(self.detachedTabDrop)
         self.tabBar.detachedTabDropSignal.connect(self.detachedTabDrop)
+        self.set_detachable(val=True)
 
 
         self.setTabBar(self.tabBar)
         self.setTabBar(self.tabBar)
 
 
@@ -872,6 +871,48 @@ class FCDetachableTab(QtWidgets.QTabWidget):
         self.setTabsClosable(True)
         self.setTabsClosable(True)
         self.tabCloseRequested.connect(self.closeTab)
         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):
     def useOldIndex(self, param):
         if param:
         if param:
             self.use_old_index = True
             self.use_old_index = True
@@ -1209,6 +1250,8 @@ class FCDetachableTab(QtWidgets.QTabWidget):
         onMoveTabSignal = pyqtSignal(int, int)
         onMoveTabSignal = pyqtSignal(int, int)
         detachedTabDropSignal = pyqtSignal(str, int, QtCore.QPoint)
         detachedTabDropSignal = pyqtSignal(str, int, QtCore.QPoint)
 
 
+        right_click = pyqtSignal(int)
+
         def __init__(self, parent=None):
         def __init__(self, parent=None):
             QtWidgets.QTabBar.__init__(self, parent)
             QtWidgets.QTabBar.__init__(self, parent)
 
 
@@ -1216,11 +1259,16 @@ class FCDetachableTab(QtWidgets.QTabWidget):
             self.setElideMode(QtCore.Qt.ElideRight)
             self.setElideMode(QtCore.Qt.ElideRight)
             self.setSelectionBehaviorOnRemove(QtWidgets.QTabBar.SelectLeftTab)
             self.setSelectionBehaviorOnRemove(QtWidgets.QTabBar.SelectLeftTab)
 
 
+            self.prev_index = -1
+
             self.dragStartPos = QtCore.QPoint()
             self.dragStartPos = QtCore.QPoint()
             self.dragDropedPos = QtCore.QPoint()
             self.dragDropedPos = QtCore.QPoint()
             self.mouseCursor = QtGui.QCursor()
             self.mouseCursor = QtGui.QCursor()
             self.dragInitiated = False
             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):
         def mouseDoubleClickEvent(self, event):
             """
             """
             Send the onDetachTabSignal when a tab is double clicked
             Send the onDetachTabSignal when a tab is double clicked
@@ -1234,21 +1282,37 @@ class FCDetachableTab(QtWidgets.QTabWidget):
 
 
         def mousePressEvent(self, event):
         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
             :param event:   a mouse press event
             :return:
             :return:
             """
             """
             if event.button() == QtCore.Qt.LeftButton:
             if event.button() == QtCore.Qt.LeftButton:
                 self.dragStartPos = event.pos()
                 self.dragStartPos = event.pos()
+            elif event.button() == QtCore.Qt.RightButton:
+                self.prev_index = self.tabAt(event.pos())
 
 
             self.dragDropedPos.setX(0)
             self.dragDropedPos.setX(0)
             self.dragDropedPos.setY(0)
             self.dragDropedPos.setY(0)
-
             self.dragInitiated = False
             self.dragInitiated = False
 
 
             QtWidgets.QTabBar.mousePressEvent(self, event)
             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):
         def mouseMoveEvent(self, event):
             """
             """
             Determine if the current movement is a drag.  If it is, convert it into a QDrag.  If the
             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
                 self.dragInitiated = True
 
 
             # If the current movement is a drag initiated by the left button
             # 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
                 # Stop the move event
                 finishMoveEvent = QtGui.QMouseEvent(
                 finishMoveEvent = QtGui.QMouseEvent(
@@ -1561,9 +1625,11 @@ class FCSpinner(QtWidgets.QSpinBox):
             self.readyToEdit = False
             self.readyToEdit = False
 
 
     def focusOutEvent(self, e):
     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):
     def get_value(self):
         return str(self.value())
         return str(self.value())
@@ -1600,16 +1666,18 @@ class FCDoubleSpinner(QtWidgets.QDoubleSpinBox):
             self.readyToEdit = False
             self.readyToEdit = False
 
 
     def focusOutEvent(self, e):
     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):
     def get_value(self):
         return str(self.value())
         return str(self.value())
 
 
     def set_value(self, val):
     def set_value(self, val):
         try:
         try:
-            k = int(val)
+            k = float(val)
         except Exception as e:
         except Exception as e:
             log.debug(str(e))
             log.debug(str(e))
             return
             return
@@ -1647,9 +1715,11 @@ class Dialog_box(QtWidgets.QWidget):
             self.readyToEdit = False
             self.readyToEdit = False
 
 
     def focusOutEvent(self, e):
     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):
 class _BrowserTextEdit(QTextEdit):
@@ -1811,9 +1881,16 @@ class MyCompleter(QCompleter):
         QCompleter.__init__(self)
         QCompleter.__init__(self)
         self.setCompletionMode(QCompleter.PopupCompletion)
         self.setCompletionMode(QCompleter.PopupCompletion)
         self.highlighted.connect(self.setHighlighted)
         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):
     def setHighlighted(self, text):
         self.lastSelected = text
         self.lastSelected = text
 
 
     def getSelected(self):
     def getSelected(self):
         return self.lastSelected
         return self.lastSelected
+

+ 149 - 174
flatcamGUI/ObjectUI.py

@@ -76,7 +76,7 @@ class ObjectUI(QtWidgets.QWidget):
         # ###########################
         # ###########################
 
 
         # ### Scale ####
         # ### Scale ####
-        self.scale_label = QtWidgets.QLabel(_('<b>Scale:</b>'))
+        self.scale_label = QtWidgets.QLabel('<b>%s:</b>' % _('Scale'))
         self.scale_label.setToolTip(
         self.scale_label.setToolTip(
             _("Change the size of the object.")
             _("Change the size of the object.")
         )
         )
@@ -86,7 +86,7 @@ class ObjectUI(QtWidgets.QWidget):
         layout.addLayout(self.scale_grid)
         layout.addLayout(self.scale_grid)
 
 
         # Factor
         # Factor
-        faclabel = QtWidgets.QLabel(_('Factor:'))
+        faclabel = QtWidgets.QLabel('%s:' % _('Factor'))
         faclabel.setToolTip(
         faclabel.setToolTip(
             _("Factor by which to multiply\n"
             _("Factor by which to multiply\n"
               "geometric features of this object.")
               "geometric features of this object.")
@@ -105,7 +105,7 @@ class ObjectUI(QtWidgets.QWidget):
         self.scale_grid.addWidget(self.scale_button, 0, 2)
         self.scale_grid.addWidget(self.scale_button, 0, 2)
 
 
         # ### Offset ####
         # ### Offset ####
-        self.offset_label = QtWidgets.QLabel(_('<b>Offset:</b>'))
+        self.offset_label = QtWidgets.QLabel('<b>%s:</b>' % _('Offset'))
         self.offset_label.setToolTip(
         self.offset_label.setToolTip(
             _("Change the position of this object.")
             _("Change the position of this object.")
         )
         )
@@ -114,7 +114,7 @@ class ObjectUI(QtWidgets.QWidget):
         self.offset_grid = QtWidgets.QGridLayout()
         self.offset_grid = QtWidgets.QGridLayout()
         layout.addLayout(self.offset_grid)
         layout.addLayout(self.offset_grid)
 
 
-        self.offset_vectorlabel = QtWidgets.QLabel(_('Vector:'))
+        self.offset_vectorlabel = QtWidgets.QLabel('%s:' % _('Vector'))
         self.offset_vectorlabel.setToolTip(
         self.offset_vectorlabel.setToolTip(
             _("Amount by which to move the object\n"
             _("Amount by which to move the object\n"
               "in the x and y axes in (x, y) format.")
               "in the x and y axes in (x, y) format.")
@@ -147,7 +147,7 @@ class GerberObjectUI(ObjectUI):
         grid0.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
         grid0.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
         self.custom_box.addLayout(grid0)
         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)
         self.plot_options_label.setMinimumWidth(90)
 
 
         grid0.addWidget(self.plot_options_label, 0, 0)
         grid0.addWidget(self.plot_options_label, 0, 0)
@@ -179,7 +179,7 @@ class GerberObjectUI(ObjectUI):
         # ## Object name
         # ## Object name
         self.name_hlay = QtWidgets.QHBoxLayout()
         self.name_hlay = QtWidgets.QHBoxLayout()
         self.custom_box.addLayout(self.name_hlay)
         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 = FCEntry()
         self.name_entry.setFocusPolicy(QtCore.Qt.StrongFocus)
         self.name_entry.setFocusPolicy(QtCore.Qt.StrongFocus)
         self.name_hlay.addWidget(name_label)
         self.name_hlay.addWidget(name_label)
@@ -189,7 +189,7 @@ class GerberObjectUI(ObjectUI):
         self.custom_box.addLayout(hlay_plot)
         self.custom_box.addLayout(hlay_plot)
 
 
         # ### Gerber Apertures ####
         # ### 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(
         self.apertures_table_label.setToolTip(
             _("Apertures Table for the Gerber Object.")
             _("Apertures Table for the Gerber Object.")
         )
         )
@@ -247,7 +247,7 @@ class GerberObjectUI(ObjectUI):
         self.apertures_table.setVisible(False)
         self.apertures_table.setVisible(False)
 
 
         # Isolation Routing
         # 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(
         self.isolation_routing_label.setToolTip(
             _("Create a Geometry object with\n"
             _("Create a Geometry object with\n"
               "toolpaths to cut outside polygons.")
               "toolpaths to cut outside polygons.")
@@ -256,7 +256,7 @@ class GerberObjectUI(ObjectUI):
 
 
         grid1 = QtWidgets.QGridLayout()
         grid1 = QtWidgets.QGridLayout()
         self.custom_box.addLayout(grid1)
         self.custom_box.addLayout(grid1)
-        tdlabel = QtWidgets.QLabel(_('Tool dia:'))
+        tdlabel = QtWidgets.QLabel('%s:' % _('Tool dia'))
         tdlabel.setToolTip(
         tdlabel.setToolTip(
             _("Diameter of the cutting tool.\n"
             _("Diameter of the cutting tool.\n"
               "If you want to have an isolation path\n"
               "If you want to have an isolation path\n"
@@ -269,17 +269,18 @@ class GerberObjectUI(ObjectUI):
         self.iso_tool_dia_entry = LengthEntry()
         self.iso_tool_dia_entry = LengthEntry()
         grid1.addWidget(self.iso_tool_dia_entry, 0, 1)
         grid1.addWidget(self.iso_tool_dia_entry, 0, 1)
 
 
-        passlabel = QtWidgets.QLabel(_('Passes:'))
+        passlabel = QtWidgets.QLabel('%s:' % _('# Passes'))
         passlabel.setToolTip(
         passlabel.setToolTip(
             _("Width of the isolation gap in\n"
             _("Width of the isolation gap in\n"
               "number (integer) of tool widths.")
               "number (integer) of tool widths.")
         )
         )
         passlabel.setMinimumWidth(90)
         passlabel.setMinimumWidth(90)
         grid1.addWidget(passlabel, 1, 0)
         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)
         grid1.addWidget(self.iso_width_entry, 1, 1)
 
 
-        overlabel = QtWidgets.QLabel(_('Pass overlap:'))
+        overlabel = QtWidgets.QLabel('%s:' % _('Pass overlap'))
         overlabel.setToolTip(
         overlabel.setToolTip(
             _("How much (fraction) of the tool width to overlap each tool pass.\n"
             _("How much (fraction) of the tool width to overlap each tool pass.\n"
               "Example:\n"
               "Example:\n"
@@ -291,7 +292,7 @@ class GerberObjectUI(ObjectUI):
         grid1.addWidget(self.iso_overlap_entry, 2, 1)
         grid1.addWidget(self.iso_overlap_entry, 2, 1)
 
 
         # Milling Type Radio Button
         # 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(
         self.milling_type_label.setToolTip(
             _("Milling type:\n"
             _("Milling type:\n"
               "- climb / best for precision milling and to reduce tool usage\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)
         grid1.addWidget(self.milling_type_radio, 3, 1)
 
 
         # combine all passes CB
         # combine all passes CB
-        self.combine_passes_cb = FCCheckBox(label=_('Combine'))
+        self.combine_passes_cb = FCCheckBox(label=_('Combine Passes'))
         self.combine_passes_cb.setToolTip(
         self.combine_passes_cb.setToolTip(
             _("Combine all passes into one object")
             _("Combine all passes into one object")
         )
         )
@@ -319,7 +320,7 @@ class GerberObjectUI(ObjectUI):
         )
         )
         grid1.addWidget(self.follow_cb, 4, 1)
         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(
         self.gen_iso_label.setToolTip(
             _("Create a Geometry object with toolpaths to cut \n"
             _("Create a Geometry object with toolpaths to cut \n"
               "isolation outside, inside or on both sides of the\n"
               "isolation outside, inside or on both sides of the\n"
@@ -378,7 +379,7 @@ class GerberObjectUI(ObjectUI):
         self.custom_box.addLayout(grid2)
         self.custom_box.addLayout(grid2)
 
 
         # ## Clear non-copper regions
         # ## 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(
         self.clearcopper_label.setToolTip(
             _("Create a Geometry object with\n"
             _("Create a Geometry object with\n"
               "toolpaths to cut all non-copper regions.")
               "toolpaths to cut all non-copper regions.")
@@ -394,7 +395,7 @@ class GerberObjectUI(ObjectUI):
         grid2.addWidget(self.generate_ncc_button, 0, 1)
         grid2.addWidget(self.generate_ncc_button, 0, 1)
 
 
         # ## Board cutout
         # ## 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(
         self.board_cutout_label.setToolTip(
             _("Create toolpaths to cut around\n"
             _("Create toolpaths to cut around\n"
               "the PCB and separate it from\n"
               "the PCB and separate it from\n"
@@ -410,7 +411,7 @@ class GerberObjectUI(ObjectUI):
         grid2.addWidget(self.generate_cutout_button, 1, 1)
         grid2.addWidget(self.generate_cutout_button, 1, 1)
 
 
         # ## Non-copper regions
         # ## 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(
         self.noncopper_label.setToolTip(
             _("Create polygons covering the\n"
             _("Create polygons covering the\n"
               "areas without copper on the PCB.\n"
               "areas without copper on the PCB.\n"
@@ -424,7 +425,7 @@ class GerberObjectUI(ObjectUI):
         self.custom_box.addLayout(grid4)
         self.custom_box.addLayout(grid4)
 
 
         # Margin
         # Margin
-        bmlabel = QtWidgets.QLabel(_('Boundary Margin:'))
+        bmlabel = QtWidgets.QLabel('%s:' % _('Boundary Margin'))
         bmlabel.setToolTip(
         bmlabel.setToolTip(
             _("Specify the edge of the PCB\n"
             _("Specify the edge of the PCB\n"
               "by drawing a box around all\n"
               "by drawing a box around all\n"
@@ -448,7 +449,7 @@ class GerberObjectUI(ObjectUI):
         grid4.addWidget(self.generate_noncopper_button, 1, 1)
         grid4.addWidget(self.generate_noncopper_button, 1, 1)
 
 
         # ## Bounding box
         # ## 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(
         self.boundingbox_label.setToolTip(
             _("Create a geometry surrounding the Gerber object.\n"
             _("Create a geometry surrounding the Gerber object.\n"
               "Square shape.")
               "Square shape.")
@@ -458,7 +459,7 @@ class GerberObjectUI(ObjectUI):
         grid5 = QtWidgets.QGridLayout()
         grid5 = QtWidgets.QGridLayout()
         self.custom_box.addLayout(grid5)
         self.custom_box.addLayout(grid5)
 
 
-        bbmargin = QtWidgets.QLabel(_('Boundary Margin:'))
+        bbmargin = QtWidgets.QLabel('%s:' % _('Boundary Margin'))
         bbmargin.setToolTip(
         bbmargin.setToolTip(
             _("Distance of the edges of the box\n"
             _("Distance of the edges of the box\n"
               "to the nearest polygon.")
               "to the nearest polygon.")
@@ -499,7 +500,7 @@ class ExcellonObjectUI(ObjectUI):
         hlay_plot = QtWidgets.QHBoxLayout()
         hlay_plot = QtWidgets.QHBoxLayout()
         self.custom_box.addLayout(hlay_plot)
         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 = FCCheckBox(label=_('Solid'))
         self.solid_cb.setToolTip(
         self.solid_cb.setToolTip(
             _("Solid circles.")
             _("Solid circles.")
@@ -511,7 +512,7 @@ class ExcellonObjectUI(ObjectUI):
         # ## Object name
         # ## Object name
         self.name_hlay = QtWidgets.QHBoxLayout()
         self.name_hlay = QtWidgets.QHBoxLayout()
         self.custom_box.addLayout(self.name_hlay)
         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 = FCEntry()
         self.name_entry.setFocusPolicy(QtCore.Qt.StrongFocus)
         self.name_entry.setFocusPolicy(QtCore.Qt.StrongFocus)
         self.name_hlay.addWidget(name_label)
         self.name_hlay.addWidget(name_label)
@@ -530,7 +531,7 @@ class ExcellonObjectUI(ObjectUI):
         self.tools_box.addLayout(hlay_plot)
         self.tools_box.addLayout(hlay_plot)
 
 
         # ### Tools Drills ####
         # ### 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(
         self.tools_table_label.setToolTip(
             _("Tools in this Excellon object\n"
             _("Tools in this Excellon object\n"
               "when are used for drilling.")
               "when are used for drilling.")
@@ -538,7 +539,7 @@ class ExcellonObjectUI(ObjectUI):
         hlay_plot.addWidget(self.tools_table_label)
         hlay_plot.addWidget(self.tools_table_label)
 
 
         # Plot CB
         # Plot CB
-        self.plot_cb = FCCheckBox(_('Plot Object'))
+        self.plot_cb = FCCheckBox(_('Plot'))
         self.plot_cb.setToolTip(
         self.plot_cb.setToolTip(
             _("Plot (show) this object.")
             _("Plot (show) this object.")
         )
         )
@@ -578,7 +579,7 @@ class ExcellonObjectUI(ObjectUI):
         self.tools_box.addWidget(self.empty_label)
         self.tools_box.addWidget(self.empty_label)
 
 
         # ### Create CNC Job ####
         # ### 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(
         self.cncjob_label.setToolTip(
             _("Create a CNC Job object\n"
             _("Create a CNC Job object\n"
               "for this drill object.")
               "for this drill object.")
@@ -589,7 +590,7 @@ class ExcellonObjectUI(ObjectUI):
         self.tools_box.addLayout(grid1)
         self.tools_box.addLayout(grid1)
 
 
         # Cut Z
         # Cut Z
-        cutzlabel = QtWidgets.QLabel(_('Cut Z:'))
+        cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
         cutzlabel.setToolTip(
         cutzlabel.setToolTip(
             _("Drill depth (negative)\n"
             _("Drill depth (negative)\n"
               "below the copper surface.")
               "below the copper surface.")
@@ -599,7 +600,7 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(self.cutz_entry, 0, 1)
         grid1.addWidget(self.cutz_entry, 0, 1)
 
 
         # Travel Z (z_move)
         # Travel Z (z_move)
-        travelzlabel = QtWidgets.QLabel(_('Travel Z:'))
+        travelzlabel = QtWidgets.QLabel('%s:' % _('Travel Z'))
         travelzlabel.setToolTip(
         travelzlabel.setToolTip(
             _("Tool height when travelling\n"
             _("Tool height when travelling\n"
               "across the XY plane.")
               "across the XY plane.")
@@ -609,7 +610,7 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(self.travelz_entry, 1, 1)
         grid1.addWidget(self.travelz_entry, 1, 1)
 
 
         # Tool change:
         # Tool change:
-        self.toolchange_cb = FCCheckBox(_("Tool change"))
+        self.toolchange_cb = FCCheckBox('%s:' % _("Tool change"))
         self.toolchange_cb.setToolTip(
         self.toolchange_cb.setToolTip(
             _("Include tool-change sequence\n"
             _("Include tool-change sequence\n"
               "in G-Code (Pause for tool change).")
               "in G-Code (Pause for tool change).")
@@ -617,7 +618,7 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(self.toolchange_cb, 2, 0)
         grid1.addWidget(self.toolchange_cb, 2, 0)
 
 
         # Tool change Z:
         # Tool change Z:
-        toolchzlabel = QtWidgets.QLabel(_("Tool change Z:"))
+        toolchzlabel = QtWidgets.QLabel('%s:' % _("Tool change Z"))
         toolchzlabel.setToolTip(
         toolchzlabel.setToolTip(
             _("Z-axis position (height) for\n"
             _("Z-axis position (height) for\n"
               "tool change.")
               "tool change.")
@@ -628,9 +629,9 @@ class ExcellonObjectUI(ObjectUI):
         self.ois_tcz_e = OptionalInputSection(self.toolchange_cb, [self.toolchangez_entry])
         self.ois_tcz_e = OptionalInputSection(self.toolchange_cb, [self.toolchangez_entry])
 
 
         # Start move Z:
         # Start move Z:
-        self.estartz_label = QtWidgets.QLabel(_("Start move Z:"))
+        self.estartz_label = QtWidgets.QLabel('%s:' % _("Start move Z"))
         self.estartz_label.setToolTip(
         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.")
               "Delete the value if you don't need this feature.")
         )
         )
         grid1.addWidget(self.estartz_label, 4, 0)
         grid1.addWidget(self.estartz_label, 4, 0)
@@ -638,17 +639,17 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(self.estartz_entry, 4, 1)
         grid1.addWidget(self.estartz_entry, 4, 1)
 
 
         # End move Z:
         # End move Z:
-        self.eendz_label = QtWidgets.QLabel(_("End move Z:"))
+        self.eendz_label = QtWidgets.QLabel('%s:' % _("End move Z"))
         self.eendz_label.setToolTip(
         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)
         grid1.addWidget(self.eendz_label, 5, 0)
         self.eendz_entry = LengthEntry()
         self.eendz_entry = LengthEntry()
         grid1.addWidget(self.eendz_entry, 5, 1)
         grid1.addWidget(self.eendz_entry, 5, 1)
 
 
         # Excellon Feedrate
         # Excellon Feedrate
-        frlabel = QtWidgets.QLabel(_('Feedrate (Plunge):'))
+        frlabel = QtWidgets.QLabel('%s:' % _('Feedrate (Plunge):'))
         frlabel.setToolTip(
         frlabel.setToolTip(
             _("Tool speed while drilling\n"
             _("Tool speed while drilling\n"
               "(in units per minute).\n"
               "(in units per minute).\n"
@@ -659,14 +660,13 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(self.feedrate_entry, 6, 1)
         grid1.addWidget(self.feedrate_entry, 6, 1)
 
 
         # Excellon Rapid Feedrate
         # 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(
         self.feedrate_rapid_label.setToolTip(
             _("Tool speed while drilling\n"
             _("Tool speed while drilling\n"
               "(in units per minute).\n"
               "(in units per minute).\n"
               "This is for the rapid move G00.\n"
               "This is for the rapid move G00.\n"
               "It is useful only for Marlin,\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)
         grid1.addWidget(self.feedrate_rapid_label, 7, 0)
         self.feedrate_rapid_entry = LengthEntry()
         self.feedrate_rapid_entry = LengthEntry()
@@ -676,7 +676,7 @@ class ExcellonObjectUI(ObjectUI):
         self.feedrate_rapid_entry.hide()
         self.feedrate_rapid_entry.hide()
 
 
         # Spindlespeed
         # Spindlespeed
-        spdlabel = QtWidgets.QLabel(_('Spindle speed:'))
+        spdlabel = QtWidgets.QLabel('%s:' % _('Spindle speed'))
         spdlabel.setToolTip(
         spdlabel.setToolTip(
             _("Speed of the spindle\n"
             _("Speed of the spindle\n"
               "in RPM (optional)")
               "in RPM (optional)")
@@ -686,14 +686,14 @@ class ExcellonObjectUI(ObjectUI):
         grid1.addWidget(self.spindlespeed_entry, 8, 1)
         grid1.addWidget(self.spindlespeed_entry, 8, 1)
 
 
         # Dwell
         # Dwell
-        self.dwell_cb = FCCheckBox(_('Dwell:'))
+        self.dwell_cb = FCCheckBox('%s:' % _('Dwell'))
         self.dwell_cb.setToolTip(
         self.dwell_cb.setToolTip(
             _("Pause to allow the spindle to reach its\n"
             _("Pause to allow the spindle to reach its\n"
               "speed before cutting.")
               "speed before cutting.")
         )
         )
         self.dwelltime_entry = FCEntry()
         self.dwelltime_entry = FCEntry()
         self.dwelltime_entry.setToolTip(
         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.dwell_cb, 9, 0)
         grid1.addWidget(self.dwelltime_entry, 9, 1)
         grid1.addWidget(self.dwelltime_entry, 9, 1)
@@ -701,10 +701,10 @@ class ExcellonObjectUI(ObjectUI):
         self.ois_dwell = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
         self.ois_dwell = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
 
 
         # postprocessor selection
         # postprocessor selection
-        pp_excellon_label = QtWidgets.QLabel(_("Postprocessor:"))
+        pp_excellon_label = QtWidgets.QLabel('%s:' % _("Postprocessor"))
         pp_excellon_label.setToolTip(
         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 = FCComboBox()
         self.pp_excellon_name_cb.setFocusPolicy(QtCore.Qt.StrongFocus)
         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)
         grid1.addWidget(self.pp_excellon_name_cb, 10, 1)
 
 
         # Probe depth
         # Probe depth
-        self.pdepth_label = QtWidgets.QLabel(_("Probe Z depth:"))
+        self.pdepth_label = QtWidgets.QLabel('%s:' % _("Probe Z depth"))
         self.pdepth_label.setToolTip(
         self.pdepth_label.setToolTip(
             _("The maximum depth that the probe is allowed\n"
             _("The maximum depth that the probe is allowed\n"
               "to probe. Negative value, in current units.")
               "to probe. Negative value, in current units.")
@@ -724,7 +724,7 @@ class ExcellonObjectUI(ObjectUI):
         self.pdepth_entry.setVisible(False)
         self.pdepth_entry.setVisible(False)
 
 
         # Probe feedrate
         # Probe feedrate
-        self.feedrate_probe_label = QtWidgets.QLabel(_("Feedrate Probe:"))
+        self.feedrate_probe_label = QtWidgets.QLabel('%s:' % _("Feedrate Probe"))
         self.feedrate_probe_label.setToolTip(
         self.feedrate_probe_label.setToolTip(
             _("The feedrate used while the probe is probing.")
             _("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
         # ### Choose what to use for Gcode creation: Drills, Slots or Both
         gcode_box = QtWidgets.QFormLayout()
         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(
         gcode_type_label.setToolTip(
             _("Choose what to use for GCode generation:\n"
             _("Choose what to use for GCode generation:\n"
               "'Drills', 'Slots' or 'Both'.\n"
               "'Drills', 'Slots' or 'Both'.\n"
@@ -766,7 +766,7 @@ class ExcellonObjectUI(ObjectUI):
         self.tools_box.addWidget(self.generate_cnc_button)
         self.tools_box.addWidget(self.generate_cnc_button)
 
 
         # ### Milling Holes Drills ####
         # ### 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(
         self.mill_hole_label.setToolTip(
             _("Create Geometry for milling holes.")
             _("Create Geometry for milling holes.")
         )
         )
@@ -780,7 +780,7 @@ class ExcellonObjectUI(ObjectUI):
 
 
         grid2 = QtWidgets.QGridLayout()
         grid2 = QtWidgets.QGridLayout()
         self.tools_box.addLayout(grid2)
         self.tools_box.addLayout(grid2)
-        self.tdlabel = QtWidgets.QLabel(_('Drills Tool dia:'))
+        self.tdlabel = QtWidgets.QLabel('%s:' % _('Drill Tool dia'))
         self.tdlabel.setToolTip(
         self.tdlabel.setToolTip(
             _("Diameter of the cutting tool.")
             _("Diameter of the cutting tool.")
         )
         )
@@ -796,9 +796,10 @@ class ExcellonObjectUI(ObjectUI):
 
 
         grid3 = QtWidgets.QGridLayout()
         grid3 = QtWidgets.QGridLayout()
         self.custom_box.addLayout(grid3)
         self.custom_box.addLayout(grid3)
-        self.stdlabel = QtWidgets.QLabel(_('Slots Tool dia:'))
+        self.stdlabel = QtWidgets.QLabel('%s:' % _('Slot Tool dia'))
         self.stdlabel.setToolTip(
         self.stdlabel.setToolTip(
-            _("Diameter of the cutting tool.")
+            _("Diameter of the cutting tool\n"
+              "when milling slots.")
         )
         )
         grid3.addWidget(self.stdlabel, 0, 0)
         grid3.addWidget(self.stdlabel, 0, 0)
         self.slot_tooldia_entry = LengthEntry()
         self.slot_tooldia_entry = LengthEntry()
@@ -827,13 +828,13 @@ class GeometryObjectUI(ObjectUI):
                                                icon_file='share/geometry32.png', parent=parent)
                                                icon_file='share/geometry32.png', parent=parent)
 
 
         # Plot options
         # 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.custom_box.addWidget(self.plot_options_label)
 
 
         # ## Object name
         # ## Object name
         self.name_hlay = QtWidgets.QHBoxLayout()
         self.name_hlay = QtWidgets.QHBoxLayout()
         self.custom_box.addLayout(self.name_hlay)
         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 = FCEntry()
         self.name_entry.setFocusPolicy(QtCore.Qt.StrongFocus)
         self.name_entry.setFocusPolicy(QtCore.Qt.StrongFocus)
         self.name_hlay.addWidget(name_label)
         self.name_hlay.addWidget(name_label)
@@ -852,7 +853,7 @@ class GeometryObjectUI(ObjectUI):
         self.geo_tools_box.addLayout(hlay_plot)
         self.geo_tools_box.addLayout(hlay_plot)
 
 
         # ### Tools ####
         # ### 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(
         self.tools_table_label.setToolTip(
             _("Tools in this Geometry object used for cutting.\n"
             _("Tools in this Geometry object used for cutting.\n"
               "The 'Offset' entry will set an offset for the cut.\n"
               "The 'Offset' entry will set an offset for the cut.\n"
@@ -944,7 +945,7 @@ class GeometryObjectUI(ObjectUI):
         self.grid1 = QtWidgets.QGridLayout()
         self.grid1 = QtWidgets.QGridLayout()
         self.geo_tools_box.addLayout(self.grid1)
         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(
         self.tool_offset_lbl.setToolTip(
             _(
             _(
                 "The value to offset the cut when \n"
                 "The value to offset the cut when \n"
@@ -970,7 +971,7 @@ class GeometryObjectUI(ObjectUI):
         # self.addtool_label.setToolTip(
         # self.addtool_label.setToolTip(
         #     "Add/Copy/Delete a tool to the tool list."
         #     "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(
         self.addtool_entry_lbl.setToolTip(
             _(
             _(
                 "Diameter for the new tool"
                 "Diameter for the new tool"
@@ -1021,7 +1022,7 @@ class GeometryObjectUI(ObjectUI):
         # Create CNC Job ###
         # Create CNC Job ###
         # ##################
         # ##################
         # ### Tools Data ## ##
         # ### 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(
         self.tool_data_label.setToolTip(
             _(
             _(
                 "The data used for creating GCode.\n"
                 "The data used for creating GCode.\n"
@@ -1042,7 +1043,7 @@ class GeometryObjectUI(ObjectUI):
         self.geo_param_box.addLayout(self.grid3)
         self.geo_param_box.addLayout(self.grid3)
 
 
         # Tip Dia
         # Tip Dia
-        self.tipdialabel = QtWidgets.QLabel(_('V-Tip Dia:'))
+        self.tipdialabel = QtWidgets.QLabel('%s:' % _('V-Tip Dia'))
         self.tipdialabel.setToolTip(
         self.tipdialabel.setToolTip(
             _(
             _(
                 "The tip diameter for V-Shape Tool"
                 "The tip diameter for V-Shape Tool"
@@ -1053,7 +1054,7 @@ class GeometryObjectUI(ObjectUI):
         self.grid3.addWidget(self.tipdia_entry, 1, 1)
         self.grid3.addWidget(self.tipdia_entry, 1, 1)
 
 
         # Tip Angle
         # Tip Angle
-        self.tipanglelabel = QtWidgets.QLabel(_('V-Tip Angle:'))
+        self.tipanglelabel = QtWidgets.QLabel('%s:' % _('V-Tip Angle'))
         self.tipanglelabel.setToolTip(
         self.tipanglelabel.setToolTip(
             _(
             _(
                 "The tip angle for V-Shape Tool.\n"
                 "The tip angle for V-Shape Tool.\n"
@@ -1065,7 +1066,7 @@ class GeometryObjectUI(ObjectUI):
         self.grid3.addWidget(self.tipangle_entry, 2, 1)
         self.grid3.addWidget(self.tipangle_entry, 2, 1)
 
 
         # Cut Z
         # Cut Z
-        cutzlabel = QtWidgets.QLabel(_('Cut Z:'))
+        cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
         cutzlabel.setToolTip(
         cutzlabel.setToolTip(
             _(
             _(
                 "Cutting depth (negative)\n"
                 "Cutting depth (negative)\n"
@@ -1077,15 +1078,13 @@ class GeometryObjectUI(ObjectUI):
         self.grid3.addWidget(self.cutz_entry, 3, 1)
         self.grid3.addWidget(self.cutz_entry, 3, 1)
 
 
         # Multi-pass
         # Multi-pass
-        self.mpass_cb = FCCheckBox(_("Multi-Depth:"))
+        self.mpass_cb = FCCheckBox('%s:' % _("Multi-Depth"))
         self.mpass_cb.setToolTip(
         self.mpass_cb.setToolTip(
             _(
             _(
                 "Use multiple passes to limit\n"
                 "Use multiple passes to limit\n"
                 "the cut depth in each pass. Will\n"
                 "the cut depth in each pass. Will\n"
                 "cut multiple times until Cut Z is\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)
         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])
         self.ois_mpass_geo = OptionalInputSection(self.mpass_cb, [self.maxdepth_entry])
 
 
         # Travel Z
         # Travel Z
-        travelzlabel = QtWidgets.QLabel(_('Travel Z:'))
+        travelzlabel = QtWidgets.QLabel('%s:' % _('Travel Z'))
         travelzlabel.setToolTip(
         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.grid3.addWidget(travelzlabel, 5, 0)
         self.travelz_entry = FloatEntry()
         self.travelz_entry = FloatEntry()
@@ -1114,14 +1111,14 @@ class GeometryObjectUI(ObjectUI):
 
 
         # Tool change:
         # Tool change:
 
 
-        self.toolchzlabel = QtWidgets.QLabel(_("Tool change Z:"))
+        self.toolchzlabel = QtWidgets.QLabel('%s:' %_("Tool change Z"))
         self.toolchzlabel.setToolTip(
         self.toolchzlabel.setToolTip(
             _(
             _(
                 "Z-axis position (height) for\n"
                 "Z-axis position (height) for\n"
                 "tool change."
                 "tool change."
             )
             )
         )
         )
-        self.toolchangeg_cb = FCCheckBox(_("Tool change"))
+        self.toolchangeg_cb = FCCheckBox('%s:' % _("Tool change"))
         self.toolchangeg_cb.setToolTip(
         self.toolchangeg_cb.setToolTip(
             _(
             _(
                 "Include tool-change sequence\n"
                 "Include tool-change sequence\n"
@@ -1147,52 +1144,44 @@ class GeometryObjectUI(ObjectUI):
         # self.grid3.addWidget(self.gstartz_entry, 8, 1)
         # self.grid3.addWidget(self.gstartz_entry, 8, 1)
 
 
         # The Z value for the end move
         # 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(
         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.grid3.addWidget(self.endzlabel, 9, 0)
         self.gendz_entry = FloatEntry()
         self.gendz_entry = FloatEntry()
         self.grid3.addWidget(self.gendz_entry, 9, 1)
         self.grid3.addWidget(self.gendz_entry, 9, 1)
 
 
         # Feedrate X-Y
         # Feedrate X-Y
-        frlabel = QtWidgets.QLabel(_('Feed Rate X-Y:'))
+        frlabel = QtWidgets.QLabel('%s:' % _('Feed Rate X-Y'))
         frlabel.setToolTip(
         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.grid3.addWidget(frlabel, 10, 0)
         self.cncfeedrate_entry = FloatEntry()
         self.cncfeedrate_entry = FloatEntry()
         self.grid3.addWidget(self.cncfeedrate_entry, 10, 1)
         self.grid3.addWidget(self.cncfeedrate_entry, 10, 1)
 
 
         # Feedrate Z (Plunge)
         # Feedrate Z (Plunge)
-        frzlabel = QtWidgets.QLabel(_('Feed Rate Z (Plunge):'))
+        frzlabel = QtWidgets.QLabel('%s:' % _('Feed Rate Z'))
         frzlabel.setToolTip(
         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.grid3.addWidget(frzlabel, 11, 0)
         self.cncplunge_entry = FloatEntry()
         self.cncplunge_entry = FloatEntry()
         self.grid3.addWidget(self.cncplunge_entry, 11, 1)
         self.grid3.addWidget(self.cncplunge_entry, 11, 1)
 
 
         # Feedrate rapids
         # Feedrate rapids
-        self.fr_rapidlabel = QtWidgets.QLabel(_('Feed Rate Rapids:'))
+        self.fr_rapidlabel = QtWidgets.QLabel('%s:' % _('Feed Rate Rapids'))
         self.fr_rapidlabel.setToolTip(
         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"
               "(in units per minute).\n"
               "This is for the rapid move G00.\n"
               "This is for the rapid move G00.\n"
               "It is useful only for Marlin,\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.grid3.addWidget(self.fr_rapidlabel, 12, 0)
         self.cncfeedrate_rapid_entry = FloatEntry()
         self.cncfeedrate_rapid_entry = FloatEntry()
@@ -1202,19 +1191,17 @@ class GeometryObjectUI(ObjectUI):
         self.cncfeedrate_rapid_entry.hide()
         self.cncfeedrate_rapid_entry.hide()
 
 
         # Cut over 1st point in path
         # 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(
         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)
         self.grid3.addWidget(self.extracut_cb, 13, 0)
 
 
         # Spindlespeed
         # Spindlespeed
-        spdlabel = QtWidgets.QLabel(_('Spindle speed:'))
+        spdlabel = QtWidgets.QLabel('%s:' % _('Spindle speed'))
         spdlabel.setToolTip(
         spdlabel.setToolTip(
             _(
             _(
                 "Speed of the spindle in RPM (optional).\n"
                 "Speed of the spindle in RPM (optional).\n"
@@ -1227,7 +1214,7 @@ class GeometryObjectUI(ObjectUI):
         self.grid3.addWidget(self.cncspindlespeed_entry, 14, 1)
         self.grid3.addWidget(self.cncspindlespeed_entry, 14, 1)
 
 
         # Dwell
         # Dwell
-        self.dwell_cb = FCCheckBox(_('Dwell:'))
+        self.dwell_cb = FCCheckBox('%s:' % _('Dwell'))
         self.dwell_cb.setToolTip(
         self.dwell_cb.setToolTip(
             _(
             _(
                 "Pause to allow the spindle to reach its\n"
                 "Pause to allow the spindle to reach its\n"
@@ -1236,9 +1223,7 @@ class GeometryObjectUI(ObjectUI):
         )
         )
         self.dwelltime_entry = FloatEntry()
         self.dwelltime_entry = FloatEntry()
         self.dwelltime_entry.setToolTip(
         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.dwell_cb, 15, 0)
         self.grid3.addWidget(self.dwelltime_entry, 15, 1)
         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])
         self.ois_dwell_geo = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
 
 
         # postprocessor selection
         # postprocessor selection
-        pp_label = QtWidgets.QLabel(_("PostProcessor:"))
+        pp_label = QtWidgets.QLabel('%s:' % _("PostProcessor"))
         pp_label.setToolTip(
         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.grid3.addWidget(pp_label, 16, 0)
         self.pp_geometry_name_cb = FCComboBox()
         self.pp_geometry_name_cb = FCComboBox()
@@ -1259,12 +1242,10 @@ class GeometryObjectUI(ObjectUI):
         self.grid3.addWidget(self.pp_geometry_name_cb, 16, 1)
         self.grid3.addWidget(self.pp_geometry_name_cb, 16, 1)
 
 
         # Probe depth
         # Probe depth
-        self.pdepth_label = QtWidgets.QLabel(_("Probe Z depth:"))
+        self.pdepth_label = QtWidgets.QLabel('%s:' % _("Probe Z depth"))
         self.pdepth_label.setToolTip(
         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.grid3.addWidget(self.pdepth_label, 17, 0)
         self.pdepth_entry = FCEntry()
         self.pdepth_entry = FCEntry()
@@ -1273,11 +1254,9 @@ class GeometryObjectUI(ObjectUI):
         self.pdepth_entry.setVisible(False)
         self.pdepth_entry.setVisible(False)
 
 
         # Probe feedrate
         # Probe feedrate
-        self.feedrate_probe_label = QtWidgets.QLabel(_("Feedrate Probe:"))
+        self.feedrate_probe_label = QtWidgets.QLabel('%s:' % _("Feedrate Probe"))
         self.feedrate_probe_label.setToolTip(
         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.grid3.addWidget(self.feedrate_probe_label, 18, 0)
         self.feedrate_probe_entry = FCEntry()
         self.feedrate_probe_entry = FCEntry()
@@ -1296,16 +1275,14 @@ class GeometryObjectUI(ObjectUI):
         # Button
         # Button
         self.generate_cnc_button = QtWidgets.QPushButton(_('Generate'))
         self.generate_cnc_button = QtWidgets.QPushButton(_('Generate'))
         self.generate_cnc_button.setToolTip(
         self.generate_cnc_button.setToolTip(
-            _(
-                "Generate the CNC Job object."
-            )
+            _("Generate the CNC Job object.")
         )
         )
         self.geo_param_box.addWidget(self.generate_cnc_button)
         self.geo_param_box.addWidget(self.generate_cnc_button)
 
 
         # ##############
         # ##############
         # Paint area ##
         # 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(
         self.paint_label.setToolTip(
             _(
             _(
                 "Creates tool paths to cover the\n"
                 "Creates tool paths to cover the\n"
@@ -1319,9 +1296,7 @@ class GeometryObjectUI(ObjectUI):
         # GO Button
         # GO Button
         self.paint_tool_button = QtWidgets.QPushButton(_('Paint Tool'))
         self.paint_tool_button = QtWidgets.QPushButton(_('Paint Tool'))
         self.paint_tool_button.setToolTip(
         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)
         self.geo_tools_box.addWidget(self.paint_tool_button)
 
 
@@ -1352,10 +1327,10 @@ class CNCObjectUI(ObjectUI):
         self.offset_button.hide()
         self.offset_button.hide()
 
 
         # ## Plot options
         # ## 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.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(
         self.cncplot_method_label.setToolTip(
             _(
             _(
                 "This selects the kind of geometries on the canvas to plot.\n"
                 "This selects the kind of geometries on the canvas to plot.\n"
@@ -1371,12 +1346,11 @@ class CNCObjectUI(ObjectUI):
             {"label": _("Cut"), "value": "cut"}
             {"label": _("Cut"), "value": "cut"}
         ], stretch=False)
         ], 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(
         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()
         self.annotation_cb = FCCheckBox()
@@ -1384,28 +1358,36 @@ class CNCObjectUI(ObjectUI):
         # ## Object name
         # ## Object name
         self.name_hlay = QtWidgets.QHBoxLayout()
         self.name_hlay = QtWidgets.QHBoxLayout()
         self.custom_box.addLayout(self.name_hlay)
         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 = FCEntry()
         self.name_entry.setFocusPolicy(QtCore.Qt.StrongFocus)
         self.name_entry.setFocusPolicy(QtCore.Qt.StrongFocus)
         self.name_hlay.addWidget(name_label)
         self.name_hlay.addWidget(name_label)
         self.name_hlay.addWidget(self.name_entry)
         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(
         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 = FCEntry()
         self.t_distance_entry.setToolTip(
         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.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 = QtWidgets.QGridLayout()
         f_lay.setColumnStretch(1, 1)
         f_lay.setColumnStretch(1, 1)
         f_lay.setColumnStretch(2, 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_label, 2, 0)
         f_lay.addWidget(self.t_distance_entry, 2, 1)
         f_lay.addWidget(self.t_distance_entry, 2, 1)
         f_lay.addWidget(self.units_label, 2, 2)
         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_label.hide()
         self.t_distance_entry.setVisible(False)
         self.t_distance_entry.setVisible(False)
+        self.t_time_label.hide()
+        self.t_time_entry.setVisible(False)
 
 
         e1_lbl = QtWidgets.QLabel('')
         e1_lbl = QtWidgets.QLabel('')
         self.custom_box.addWidget(e1_lbl)
         self.custom_box.addWidget(e1_lbl)
@@ -1431,7 +1418,7 @@ class CNCObjectUI(ObjectUI):
         self.custom_box.addLayout(hlay)
         self.custom_box.addLayout(hlay)
 
 
         # CNC Tools Table for plot
         # 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(
         self.cnc_tools_table_label.setToolTip(
             _(
             _(
                 "Tools in this CNCJob object used for cutting.\n"
                 "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 = QtWidgets.QCheckBox('Plot')
         self.plot_cb = FCCheckBox(_('Plot Object'))
         self.plot_cb = FCCheckBox(_('Plot Object'))
         self.plot_cb.setToolTip(
         self.plot_cb.setToolTip(
-            _(
-                "Plot (show) this object."
-            )
+            _("Plot (show) this object.")
         )
         )
         self.plot_cb.setLayoutDirection(QtCore.Qt.RightToLeft)
         self.plot_cb.setLayoutDirection(QtCore.Qt.RightToLeft)
         hlay.addStretch()
         hlay.addStretch()
@@ -1483,20 +1468,18 @@ class CNCObjectUI(ObjectUI):
         # ####################
         # ####################
         # ## Export G-Code ##
         # ## 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(
         self.export_gcode_label.setToolTip(
             _("Export and save G-Code to\n"
             _("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)
         self.custom_box.addWidget(self.export_gcode_label)
 
 
         # Prepend text to GCode
         # Prepend text to GCode
-        prependlabel = QtWidgets.QLabel(_('Prepend to CNC Code:'))
+        prependlabel = QtWidgets.QLabel('%s:' % _('Prepend to CNC Code'))
         prependlabel.setToolTip(
         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)
         self.custom_box.addWidget(prependlabel)
 
 
@@ -1504,13 +1487,11 @@ class CNCObjectUI(ObjectUI):
         self.custom_box.addWidget(self.prepend_text)
         self.custom_box.addWidget(self.prepend_text)
 
 
         # Append text to GCode
         # Append text to GCode
-        appendlabel = QtWidgets.QLabel(_('Append to CNC Code:'))
+        appendlabel = QtWidgets.QLabel('%s:' % _('Append to CNC Code'))
         appendlabel.setToolTip(
         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)
         self.custom_box.addWidget(appendlabel)
 
 
@@ -1525,7 +1506,7 @@ class CNCObjectUI(ObjectUI):
         self.cnc_frame.setLayout(self.cnc_box)
         self.cnc_frame.setLayout(self.cnc_box)
 
 
         # Toolchange Custom G-Code
         # Toolchange Custom G-Code
-        self.toolchangelabel = QtWidgets.QLabel(_('Toolchange G-Code:'))
+        self.toolchangelabel = QtWidgets.QLabel('%s:' % _('Toolchange G-Code'))
         self.toolchangelabel.setToolTip(
         self.toolchangelabel.setToolTip(
             _(
             _(
                 "Type here any G-Code commands you would\n"
                 "Type here any G-Code commands you would\n"
@@ -1547,12 +1528,10 @@ class CNCObjectUI(ObjectUI):
         self.cnc_box.addLayout(cnclay)
         self.cnc_box.addLayout(cnclay)
 
 
         # Toolchange Replacement Enable
         # Toolchange Replacement Enable
-        self.toolchange_cb = FCCheckBox(label=_('Use Toolchange Macro'))
+        self.toolchange_cb = FCCheckBox(label='%s' % _('Use Toolchange Macro'))
         self.toolchange_cb.setToolTip(
         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
         # Variable list
@@ -1598,19 +1577,15 @@ class CNCObjectUI(ObjectUI):
         # Edit GCode Button
         # Edit GCode Button
         self.modify_gcode_button = QtWidgets.QPushButton(_('View CNC Code'))
         self.modify_gcode_button = QtWidgets.QPushButton(_('View CNC Code'))
         self.modify_gcode_button.setToolTip(
         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
         # GO Button
         self.export_gcode_button = QtWidgets.QPushButton(_('Save CNC Code'))
         self.export_gcode_button = QtWidgets.QPushButton(_('Save CNC Code'))
         self.export_gcode_button.setToolTip(
         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)
         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')
 log = logging.getLogger('base')
 
 
 
 
-class PlotCanvas(QtCore.QObject):
+class PlotCanvas(QtCore.QObject, VisPyCanvas):
     """
     """
     Class handling the plotting area in the application.
     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
         The constructor configures the VisPy figure that
         will contain all plots, creates the base axes and connects
         will contain all plots, creates the base axes and connects
@@ -34,8 +34,12 @@ class PlotCanvas(QtCore.QObject):
         """
         """
 
 
         super(PlotCanvas, self).__init__()
         super(PlotCanvas, self).__init__()
+        VisPyCanvas.__init__(self)
 
 
-        self.app = app
+        # VisPyCanvas does not allow new attributes. Override.
+        self.unfreeze()
+
+        self.fcapp = fcapp
 
 
         # Parent container
         # Parent container
         self.container = container
         self.container = container
@@ -44,19 +48,19 @@ class PlotCanvas(QtCore.QObject):
         # which might decrease performance
         # which might decrease performance
         self.b_line, self.r_line, self.t_line, self.l_line = None, None, None, None
         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 # ##
         # ## AXIS # ##
         self.v_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 1.0), vertical=True,
         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,
         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
         # 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
         # all CNC have a limited workspace
@@ -70,12 +74,15 @@ class PlotCanvas(QtCore.QObject):
         self.shape_collections = []
         self.shape_collections = []
 
 
         self.shape_collection = self.new_shape_collection()
         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()
         self.text_collection = self.new_text_collection()
 
 
         # TODO: Should be setting to show/hide CNC job annotations (global or per object)
         # TODO: Should be setting to show/hide CNC job annotations (global or per object)
         self.text_collection.enabled = True
         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
     # 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
     # all CNC have a limited workspace
     def draw_workspace(self):
     def draw_workspace(self):
@@ -91,38 +98,38 @@ class PlotCanvas(QtCore.QObject):
         a3p_mm = np.array([(0, 0), (297, 0), (297, 420), (0, 420)])
         a3p_mm = np.array([(0, 0), (297, 0), (297, 420), (0, 420)])
         a3l_mm = np.array([(0, 0), (420, 0), (420, 297), (0, 297)])
         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
                 a = a4p_mm
-            elif self.app.defaults['global_workspaceT'] == 'A4L':
+            elif self.fcapp.defaults['global_workspaceT'] == 'A4L':
                 a = a4l_mm
                 a = a4l_mm
-            elif self.app.defaults['global_workspaceT'] == 'A3P':
+            elif self.fcapp.defaults['global_workspaceT'] == 'A3P':
                 a = a3p_mm
                 a = a3p_mm
-            elif self.app.defaults['global_workspaceT'] == 'A3L':
+            elif self.fcapp.defaults['global_workspaceT'] == 'A3L':
                 a = a3l_mm
                 a = a3l_mm
         else:
         else:
-            if self.app.defaults['global_workspaceT'] == 'A4P':
+            if self.fcapp.defaults['global_workspaceT'] == 'A4P':
                 a = a4p_in
                 a = a4p_in
-            elif self.app.defaults['global_workspaceT'] == 'A4L':
+            elif self.fcapp.defaults['global_workspaceT'] == 'A4L':
                 a = a4l_in
                 a = a4l_in
-            elif self.app.defaults['global_workspaceT'] == 'A3P':
+            elif self.fcapp.defaults['global_workspaceT'] == 'A3P':
                 a = a3p_in
                 a = a3p_in
-            elif self.app.defaults['global_workspaceT'] == 'A3L':
+            elif self.fcapp.defaults['global_workspaceT'] == 'A3L':
                 a = a3l_in
                 a = a3l_in
 
 
         self.delete_workspace()
         self.delete_workspace()
 
 
         self.b_line = Line(pos=a[0:2], color=(0.70, 0.3, 0.3, 1.0),
         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),
         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),
         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),
         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()
             self.delete_workspace()
 
 
     # delete the workspace lines from the plot by removing the parent
     # 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
     # redraw the workspace lines on the plot by readding them to the parent view.scene
     def restore_workspace(self):
     def restore_workspace(self):
         try:
         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:
         except Exception as e:
             pass
             pass
 
 
     def vis_connect(self, event_name, callback):
     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):
     def vis_disconnect(self, event_name, callback=None):
         if callback is None:
         if callback is None:
-            getattr(self.vispy_canvas.events, event_name).disconnect()
+            getattr(self.events, event_name).disconnect()
         else:
         else:
-            getattr(self.vispy_canvas.events, event_name).disconnect(callback)
+            getattr(self.events, event_name).disconnect(callback)
 
 
     def zoom(self, factor, center=None):
     def zoom(self, factor, center=None):
         """
         """
@@ -165,7 +172,7 @@ class PlotCanvas(QtCore.QObject):
         :type center: list
         :type center: list
         :return: None
         :return: None
         """
         """
-        self.vispy_canvas.view.camera.zoom(factor, center)
+        self.view.camera.zoom(factor, center)
 
 
     def new_shape_group(self, shape_collection=None):
     def new_shape_group(self, shape_collection=None):
         if shape_collection:
         if shape_collection:
@@ -173,21 +180,24 @@ class PlotCanvas(QtCore.QObject):
         return ShapeGroup(self.shape_collection)
         return ShapeGroup(self.shape_collection)
 
 
     def new_shape_collection(self, **kwargs):
     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)
         # self.shape_collections.append(sc)
         # return 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):
     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
         c.antialias = 0
         return c
         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):
     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):
     def fit_view(self, rect=None):
 
 
@@ -209,7 +219,7 @@ class PlotCanvas(QtCore.QObject):
         rect.right *= 1.01
         rect.right *= 1.01
         rect.top *= 1.01
         rect.top *= 1.01
 
 
-        self.vispy_canvas.view.camera.rect = rect
+        self.view.camera.rect = rect
 
 
         self.shape_collection.unlock_updates()
         self.shape_collection.unlock_updates()
 
 
@@ -224,7 +234,7 @@ class PlotCanvas(QtCore.QObject):
             except TypeError:
             except TypeError:
                 pass
                 pass
 
 
-        self.vispy_canvas.view.camera.rect = rect
+        self.view.camera.rect = rect
 
 
         self.shape_collection.unlock_updates()
         self.shape_collection.unlock_updates()
 
 

+ 13 - 5
flatcamGUI/VisPyCanvas.py

@@ -8,12 +8,13 @@
 
 
 import numpy as np
 import numpy as np
 from PyQt5.QtGui import QPalette
 from PyQt5.QtGui import QPalette
+from PyQt5.QtCore import QSettings
 import vispy.scene as scene
 import vispy.scene as scene
 from vispy.scene.cameras.base_camera import BaseCamera
 from vispy.scene.cameras.base_camera import BaseCamera
 from vispy.color import Color
 from vispy.color import Color
 import time
 import time
 
 
-white = Color("#ffffff" )
+white = Color("#ffffff")
 black = Color("#000000")
 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 = self.grid_widget.add_widget(row=0, col=0, col_span=2)
         top_padding.height_max = 0
         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.yaxis.width_max = 55
         self.grid_widget.add_widget(self.yaxis, row=1, col=0)
         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)
         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)
         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
         right_padding.width_max = 0
 
 
         view = self.grid_widget.add_view(row=1, col=1, border_color='black', bgcolor='white')
         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,
         # Following function was removed from 'prepare_draw()' of 'Grid' class by patch,
         # it is necessary to call manually
         # 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            #
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # http://flatcam.org                                       #
 # File Author: Marius Adrian Stanciu (c)                   #
 # File Author: Marius Adrian Stanciu (c)                   #
 # Date: 3/10/2019                                          #
 # Date: 3/10/2019                                          #
 # MIT Licence                                              #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 
 from shapely.geometry import LineString
 from shapely.geometry import LineString
 import logging
 import logging

+ 14 - 2
flatcamParsers/ParseSVG.py

@@ -284,7 +284,7 @@ def svgpolygon2shapely(polygon):
     # return LinearRing(points)
     # return LinearRing(points)
 
 
 
 
-def getsvggeo(node, object_type):
+def getsvggeo(node, object_type, root = None):
     """
     """
     Extracts and flattens all geometry from an SVG node
     Extracts and flattens all geometry from an SVG node
     into a list of Shapely geometry.
     into a list of Shapely geometry.
@@ -293,13 +293,16 @@ def getsvggeo(node, object_type):
     :return: List of Shapely geometry
     :return: List of Shapely geometry
     :rtype: list
     :rtype: list
     """
     """
+    if root is None:
+        root = node
+
     kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
     kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
     geo = []
     geo = []
 
 
     # Recurse
     # Recurse
     if len(node) > 0:
     if len(node) > 0:
         for child in node:
         for child in node:
-            subgeo = getsvggeo(child, object_type)
+            subgeo = getsvggeo(child, object_type, root)
             if subgeo is not None:
             if subgeo is not None:
                 geo += subgeo
                 geo += subgeo
 
 
@@ -341,6 +344,15 @@ def getsvggeo(node, object_type):
         pline = svgpolyline2shapely(node)
         pline = svgpolyline2shapely(node)
         geo = [pline]
         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:
     else:
         log.warning("Unknown kind: " + kind)
         log.warning("Unknown kind: " + kind)
         geo = None
         geo = None

+ 20 - 14
flatcamTools/ToolCalculators.py

@@ -89,28 +89,29 @@ class ToolCalculator(FlatCAMTool):
         form_layout = QtWidgets.QFormLayout()
         form_layout = QtWidgets.QFormLayout()
         self.layout.addLayout(form_layout)
         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 = FCEntry()
         # self.tipDia_entry.setFixedWidth(70)
         # self.tipDia_entry.setFixedWidth(70)
         self.tipDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.tipDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-        self.tipDia_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 = FCEntry()
         # self.tipAngle_entry.setFixedWidth(70)
         # self.tipAngle_entry.setFixedWidth(70)
         self.tipAngle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.tipAngle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.tipAngle_label.setToolTip(_("This is the angle of the tip of the tool.\n"
         self.tipAngle_label.setToolTip(_("This is the angle of the tip of the tool.\n"
                                          "It is specified by manufacturer."))
                                          "It is specified by manufacturer."))
 
 
-        self.cutDepth_label = QtWidgets.QLabel(_("Cut Z:"))
+        self.cutDepth_label = QtWidgets.QLabel('%s:' % _("Cut Z"))
         self.cutDepth_entry = FCEntry()
         self.cutDepth_entry = FCEntry()
         # self.cutDepth_entry.setFixedWidth(70)
         # self.cutDepth_entry.setFixedWidth(70)
         self.cutDepth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.cutDepth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.cutDepth_label.setToolTip(_("This is the depth to cut into the material.\n"
         self.cutDepth_label.setToolTip(_("This is the depth to cut into the material.\n"
                                          "In the CNCJob is the CutZ parameter."))
                                          "In the CNCJob is the CutZ parameter."))
 
 
-        self.effectiveToolDia_label = QtWidgets.QLabel(_("Tool Diameter:"))
+        self.effectiveToolDia_label = QtWidgets.QLabel('%s:' % _("Tool Diameter"))
         self.effectiveToolDia_entry = FCEntry()
         self.effectiveToolDia_entry = FCEntry()
         # self.effectiveToolDia_entry.setFixedWidth(70)
         # self.effectiveToolDia_entry.setFixedWidth(70)
         self.effectiveToolDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.effectiveToolDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
@@ -154,26 +155,26 @@ class ToolCalculator(FlatCAMTool):
         plate_form_layout = QtWidgets.QFormLayout()
         plate_form_layout = QtWidgets.QFormLayout()
         self.layout.addLayout(plate_form_layout)
         self.layout.addLayout(plate_form_layout)
 
 
-        self.pcblengthlabel = QtWidgets.QLabel(_("Board Length:"))
+        self.pcblengthlabel = QtWidgets.QLabel('%s:' % _("Board Length"))
         self.pcblength_entry = FCEntry()
         self.pcblength_entry = FCEntry()
         # self.pcblengthlabel.setFixedWidth(70)
         # self.pcblengthlabel.setFixedWidth(70)
         self.pcblength_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.pcblength_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.pcblengthlabel.setToolTip(_('This is the board length. In centimeters.'))
         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.pcbwidth_entry = FCEntry()
         # self.pcbwidthlabel.setFixedWidth(70)
         # self.pcbwidthlabel.setFixedWidth(70)
         self.pcbwidth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.pcbwidth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.pcbwidthlabel.setToolTip(_('This is the board width.In centimeters.'))
         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 = FCEntry()
         # self.cdensity_entry.setFixedWidth(70)
         # self.cdensity_entry.setFixedWidth(70)
         self.cdensity_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.cdensity_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.cdensity_label.setToolTip(_("Current density to pass through the board. \n"
         self.cdensity_label.setToolTip(_("Current density to pass through the board. \n"
                                          "In Amps per Square Feet ASF."))
                                          "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 = FCEntry()
         # self.growth_entry.setFixedWidth(70)
         # self.growth_entry.setFixedWidth(70)
         self.growth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.growth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
@@ -182,7 +183,7 @@ class ToolCalculator(FlatCAMTool):
 
 
         # self.growth_entry.setEnabled(False)
         # self.growth_entry.setEnabled(False)
 
 
-        self.cvaluelabel = QtWidgets.QLabel(_("Current Value:"))
+        self.cvaluelabel = QtWidgets.QLabel('%s:' % _("Current Value"))
         self.cvalue_entry = FCEntry()
         self.cvalue_entry = FCEntry()
         # self.cvaluelabel.setFixedWidth(70)
         # self.cvaluelabel.setFixedWidth(70)
         self.cvalue_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         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.'))
                                       'to be set on the Power Supply. In Amps.'))
         self.cvalue_entry.setDisabled(True)
         self.cvalue_entry.setDisabled(True)
 
 
-        self.timelabel = QtWidgets.QLabel(_("Time:"))
+        self.timelabel = QtWidgets.QLabel('%s:' % _("Time"))
         self.time_entry = FCEntry()
         self.time_entry = FCEntry()
         # self.timelabel.setFixedWidth(70)
         # self.timelabel.setFixedWidth(70)
         self.time_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         self.time_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
@@ -242,7 +243,12 @@ class ToolCalculator(FlatCAMTool):
             else:
             else:
                 try:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
                     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:
                 except AttributeError:
                     pass
                     pass
         else:
         else:

+ 261 - 137
flatcamTools/ToolCutOut.py

@@ -16,7 +16,6 @@ if '_' not in builtins.__dict__:
 class CutOut(FlatCAMTool):
 class CutOut(FlatCAMTool):
 
 
     toolName = _("Cutout PCB")
     toolName = _("Cutout PCB")
-    gapFinished = pyqtSignal()
 
 
     def __init__(self, app):
     def __init__(self, app):
         FlatCAMTool.__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(1, QtGui.QIcon("share/drill16.png"))
         self.type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.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(
         self.type_obj_combo_label.setToolTip(
             _("Specify the type of object to be cutout.\n"
             _("Specify the type of object to be cutout.\n"
               "It can be of type: Gerber or Geometry.\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.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.obj_combo.setCurrentIndex(1)
         self.obj_combo.setCurrentIndex(1)
 
 
-        self.object_label = QtWidgets.QLabel(_("Object:"))
+        self.object_label = QtWidgets.QLabel('%s:' % _("Object"))
         self.object_label.setToolTip(
         self.object_label.setToolTip(
             _("Object to be cutout.                        ")
             _("Object to be cutout.                        ")
         )
         )
         form_layout.addRow(self.object_label, self.obj_combo)
         form_layout.addRow(self.object_label, self.obj_combo)
 
 
         # Object kind
         # Object kind
-        self.kindlabel = QtWidgets.QLabel(_('Obj kind:'))
+        self.kindlabel = QtWidgets.QLabel('%s:' % _('Obj kind'))
         self.kindlabel.setToolTip(
         self.kindlabel.setToolTip(
             _("Choice of what kind the object we want to cutout is.<BR>"
             _("Choice of what kind the object we want to cutout is.<BR>"
               "- <B>Single</B>: contain a single PCB Gerber outline object.<BR>"
               "- <B>Single</B>: contain a single PCB Gerber outline object.<BR>"
@@ -89,7 +88,7 @@ class CutOut(FlatCAMTool):
 
 
         # Tool Diameter
         # Tool Diameter
         self.dia = FCEntry()
         self.dia = FCEntry()
-        self.dia_label = QtWidgets.QLabel(_("Tool Dia:"))
+        self.dia_label = QtWidgets.QLabel('%s:' % _("Tool dia"))
         self.dia_label.setToolTip(
         self.dia_label.setToolTip(
            _("Diameter of the tool used to cutout\n"
            _("Diameter of the tool used to cutout\n"
              "the PCB shape out of the surrounding material.")
              "the PCB shape out of the surrounding material.")
@@ -98,7 +97,7 @@ class CutOut(FlatCAMTool):
 
 
         # Margin
         # Margin
         self.margin = FCEntry()
         self.margin = FCEntry()
-        self.margin_label = QtWidgets.QLabel(_("Margin:"))
+        self.margin_label = QtWidgets.QLabel('%s:' % _("Margin:"))
         self.margin_label.setToolTip(
         self.margin_label.setToolTip(
            _("Margin over bounds. A positive value here\n"
            _("Margin over bounds. A positive value here\n"
              "will make the cutout of the PCB further from\n"
              "will make the cutout of the PCB further from\n"
@@ -108,7 +107,7 @@ class CutOut(FlatCAMTool):
 
 
         # Gapsize
         # Gapsize
         self.gapsize = FCEntry()
         self.gapsize = FCEntry()
-        self.gapsize_label = QtWidgets.QLabel(_("Gap size:"))
+        self.gapsize_label = QtWidgets.QLabel('%s:' % _("Gap size:"))
         self.gapsize_label.setToolTip(
         self.gapsize_label.setToolTip(
            _("The size of the bridge gaps in the cutout\n"
            _("The size of the bridge gaps in the cutout\n"
              "used to keep the board connected to\n"
              "used to keep the board connected to\n"
@@ -127,7 +126,7 @@ class CutOut(FlatCAMTool):
 
 
         # Surrounding convex box shape
         # Surrounding convex box shape
         self.convex_box = FCCheckBox()
         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(
         self.convex_box_label.setToolTip(
             _("Create a convex shape surrounding the entire PCB.\n"
             _("Create a convex shape surrounding the entire PCB.\n"
               "Used only if the source object type is Gerber.")
               "Used only if the source object type is Gerber.")
@@ -146,11 +145,12 @@ class CutOut(FlatCAMTool):
         self.layout.addLayout(form_layout_2)
         self.layout.addLayout(form_layout_2)
 
 
         # Gaps
         # Gaps
-        gaps_label = QtWidgets.QLabel(_('Gaps:'))
+        gaps_label = QtWidgets.QLabel('%s:' % _('Gaps'))
         gaps_label.setToolTip(
         gaps_label.setToolTip(
             _("Number of gaps used for the Automatic cutout.\n"
             _("Number of gaps used for the Automatic cutout.\n"
               "There can be maximum 8 bridges/gaps.\n"
               "There can be maximum 8 bridges/gaps.\n"
               "The choices are:\n"
               "The choices are:\n"
+              "- None  - no gaps\n"
               "- lr    - left + right\n"
               "- lr    - left + right\n"
               "- tb    - top + bottom\n"
               "- tb    - top + bottom\n"
               "- 4     - left + right +top + bottom\n"
               "- 4     - left + right +top + bottom\n"
@@ -161,7 +161,7 @@ class CutOut(FlatCAMTool):
         gaps_label.setMinimumWidth(60)
         gaps_label.setMinimumWidth(60)
 
 
         self.gaps = FCComboBox()
         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:
         for it in gaps_items:
             self.gaps.addItem(it)
             self.gaps.addItem(it)
             self.gaps.setStyleSheet('background-color: rgb(255,255,255)')
             self.gaps.setStyleSheet('background-color: rgb(255,255,255)')
@@ -171,7 +171,7 @@ class CutOut(FlatCAMTool):
         hlay = QtWidgets.QHBoxLayout()
         hlay = QtWidgets.QHBoxLayout()
         self.layout.addLayout(hlay)
         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(
         title_ff_label.setToolTip(
             _("The cutout shape can be of ny shape.\n"
             _("The cutout shape can be of ny shape.\n"
               "Useful when the PCB has a non-rectangular shape.")
               "Useful when the PCB has a non-rectangular shape.")
@@ -191,7 +191,7 @@ class CutOut(FlatCAMTool):
         hlay2 = QtWidgets.QHBoxLayout()
         hlay2 = QtWidgets.QHBoxLayout()
         self.layout.addLayout(hlay2)
         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(
         title_rct_label.setToolTip(
             _("The resulting cutout shape is\n"
             _("The resulting cutout shape is\n"
               "always a rectangle shape and it will be\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.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
         self.man_object_combo.setCurrentIndex(1)
         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(
         self.man_object_label.setToolTip(
             _("Geometry object used to create the manual cutout.")
             _("Geometry object used to create the manual cutout.")
         )
         )
@@ -241,7 +241,7 @@ class CutOut(FlatCAMTool):
         hlay3 = QtWidgets.QHBoxLayout()
         hlay3 = QtWidgets.QHBoxLayout()
         self.layout.addLayout(hlay3)
         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(
         self.man_geo_label.setToolTip(
             _("If the object to be cutout is a Gerber\n"
             _("If the object to be cutout is a Gerber\n"
               "first create a Geometry that surrounds it,\n"
               "first create a Geometry that surrounds it,\n"
@@ -263,7 +263,7 @@ class CutOut(FlatCAMTool):
         hlay4 = QtWidgets.QHBoxLayout()
         hlay4 = QtWidgets.QHBoxLayout()
         self.layout.addLayout(hlay4)
         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(
         self.man_bridge_gaps_label.setToolTip(
             _("Use the left mouse button (LMB) click\n"
             _("Use the left mouse button (LMB) click\n"
               "to create a bridge gap to separate the PCB from\n"
               "to create a bridge gap to separate the PCB from\n"
@@ -292,6 +292,16 @@ class CutOut(FlatCAMTool):
 
 
         self.flat_geometry = []
         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
         # Signals
         self.ff_cutout_object_btn.clicked.connect(self.on_freeform_cutout)
         self.ff_cutout_object_btn.clicked.connect(self.on_freeform_cutout)
         self.rect_cutout_object_btn.clicked.connect(self.on_rectangular_cutout)
         self.rect_cutout_object_btn.clicked.connect(self.on_rectangular_cutout)
@@ -315,7 +325,12 @@ class CutOut(FlatCAMTool):
             else:
             else:
                 try:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
                     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:
                 except AttributeError:
                     pass
                     pass
         else:
         else:
@@ -340,8 +355,6 @@ class CutOut(FlatCAMTool):
         self.gaps.set_value(self.app.defaults["tools_gaps_ff"])
         self.gaps.set_value(self.app.defaults["tools_gaps_ff"])
         self.convex_box.set_value(self.app.defaults['tools_cutout_convexshape'])
         self.convex_box.set_value(self.app.defaults['tools_cutout_convexshape'])
 
 
-        self.gapFinished.connect(self.on_gap_finished)
-
     def on_freeform_cutout(self):
     def on_freeform_cutout(self):
 
 
         # def subtract_rectangle(obj_, x0, y0, x1, y1):
         # 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."))
             self.app.inform.emit(_("[WARNING_NOTCL] Number of gaps value is missing. Add it and retry."))
             return
             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. "))
                                    "Fill in a correct value and retry. "))
             return
             return
 
 
@@ -446,44 +460,46 @@ class CutOut(FlatCAMTool):
                 leny = (ymax - ymin) + (margin * 2)
                 leny = (ymax - ymin) + (margin * 2)
 
 
                 proc_geometry = []
                 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:
                 try:
                     for g in geom:
                     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."))
             self.app.inform.emit(_("[WARNING_NOTCL] Number of gaps value is missing. Add it and retry."))
             return
             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. "))
                                    "Fill in a correct value and retry. "))
             return
             return
 
 
@@ -630,43 +647,46 @@ class CutOut(FlatCAMTool):
                 lenx = (xmax - xmin) + (margin * 2)
                 lenx = (xmax - xmin) + (margin * 2)
                 leny = (ymax - ymin) + (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:
                 try:
                     for g in geom:
                     for g in geom:
                         proc_geometry.append(g)
                         proc_geometry.append(g)
@@ -743,66 +763,50 @@ class CutOut(FlatCAMTool):
                                      "Add it and retry."))
                                      "Add it and retry."))
                 return
                 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('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_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_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_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('key_press', self.on_key_press)
         self.app.plotcanvas.vis_connect('mouse_move', self.on_mouse_move)
         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):
     def on_manual_cutout(self, click_pos):
         name = self.man_object_combo.currentText()
         name = self.man_object_combo.currentText()
 
 
         # Get source object.
         # Get source object.
         try:
         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:
         except Exception as e:
             log.debug("CutOut.on_manual_cutout() --> %s" % str(e))
             log.debug("CutOut.on_manual_cutout() --> %s" % str(e))
             self.app.inform.emit(_("[ERROR_NOTCL] Could not retrieve Geometry object: %s") % name)
             self.app.inform.emit(_("[ERROR_NOTCL] Could not retrieve Geometry object: %s") % name)
             return "Could not retrieve 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
             return
 
 
         # use the snapped position as reference
         # use the snapped position as reference
         snapped_pos = self.app.geo_editor.snap(click_pos[0], click_pos[1])
         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]))
         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.inform.emit(_("[success] Added manual Bridge Gap."))
 
 
         self.app.should_we_save = True
         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):
     def on_manual_geo(self):
         name = self.obj_combo.currentText()
         name = self.obj_combo.currentText()
 
 
@@ -864,9 +868,17 @@ class CutOut(FlatCAMTool):
                 geo = geo_union.convex_hull
                 geo = geo_union.convex_hull
                 geo_obj.solid_geometry = geo.buffer(margin + abs(dia / 2))
                 geo_obj.solid_geometry = geo.buffer(margin + abs(dia / 2))
             elif kind == 'single':
             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:
             else:
                 geo = geo_union
                 geo = geo_union
                 geo = geo.buffer(margin + abs(dia / 2))
                 geo = geo.buffer(margin + abs(dia / 2))
@@ -896,26 +908,127 @@ class CutOut(FlatCAMTool):
         cut_poly = box(xmin, ymin, xmax, ymax)
         cut_poly = box(xmin, ymin, xmax, ymax)
         return cut_poly
         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):
     def on_mouse_move(self, event):
 
 
         self.app.on_mouse_move_over_plot(event=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]
         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:
         try:
             x = float(event.xdata)
             x = float(event.xdata)
             y = float(event.ydata)
             y = float(event.ydata)
         except TypeError:
         except TypeError:
             return
             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
         # Remove any previous utility shape
         self.app.geo_editor.tool_shape.clear(update=True)
         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):
     def draw_utility_geometry(self, geo):
         self.app.geo_editor.tool_shape.add(
         self.app.geo_editor.tool_shape.add(
@@ -941,7 +1054,7 @@ class CutOut(FlatCAMTool):
         if key == QtCore.Qt.Key_Escape or key == 'Escape':
         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('key_press', self.on_key_press)
             self.app.plotcanvas.vis_disconnect('mouse_move', self.on_mouse_move)
             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('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_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_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.clear(update=True)
             self.app.geo_editor.tool_shape.enabled = False
             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):
     def subtract_poly_from_geo(self, solid_geo, x0, y0, x1, y1):
         """
         """
         Subtract polygon made from points from the given object.
         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.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.gerber_object_combo.setCurrentIndex(1)
         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(
         self.botlay_label.setToolTip(
             "Gerber  to be mirrored."
             "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.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
         self.exc_object_combo.setCurrentIndex(1)
         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(
         self.excobj_label.setToolTip(
             _("Excellon Object to be mirrored.")
             _("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.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
         self.geo_object_combo.setCurrentIndex(1)
         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(
         self.geoobj_label.setToolTip(
             _("Geometry Obj to be mirrored.")
             _("Geometry Obj to be mirrored.")
         )
         )
@@ -149,7 +149,7 @@ class DblSidedTool(FlatCAMTool):
 
 
         # ## Point/Box
         # ## Point/Box
         self.point_box_container = QtWidgets.QVBoxLayout()
         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(
         self.pb_label.setToolTip(
             _("If 'Point' is selected above it store the coordinates (x, y) through which\n"
             _("If 'Point' is selected above it store the coordinates (x, y) through which\n"
               "the mirroring axis passes.\n"
               "the mirroring axis passes.\n"
@@ -189,7 +189,7 @@ class DblSidedTool(FlatCAMTool):
         self.box_combo_type.hide()
         self.box_combo_type.hide()
 
 
         # ## Alignment holes
         # ## 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(
         self.ah_label.setToolTip(
            _("Alignment holes (x1, y1), (x2, y2), ... "
            _("Alignment holes (x1, y1), (x2, y2), ... "
              "on one side of the mirror axis. For each set of (x, y) coordinates\n"
              "on one side of the mirror axis. For each set of (x, y) coordinates\n"
@@ -220,7 +220,7 @@ class DblSidedTool(FlatCAMTool):
         grid_lay3.addWidget(self.add_drill_point_button, 0, 1)
         grid_lay3.addWidget(self.add_drill_point_button, 0, 1)
 
 
         # ## Drill diameter for alignment holes
         # ## 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(
         self.dt_label.setToolTip(
             _("Diameter of the drill for the "
             _("Diameter of the drill for the "
               "alignment holes.")
               "alignment holes.")
@@ -231,7 +231,7 @@ class DblSidedTool(FlatCAMTool):
         self.layout.addLayout(hlay)
         self.layout.addLayout(hlay)
 
 
         self.drill_dia = FCEntry()
         self.drill_dia = FCEntry()
-        self.dd_label = QtWidgets.QLabel(_("Drill diam.:"))
+        self.dd_label = QtWidgets.QLabel('%s:' % _("Drill dia"))
         self.dd_label.setToolTip(
         self.dd_label.setToolTip(
             _("Diameter of the drill for the "
             _("Diameter of the drill for the "
               "alignment holes.")
               "alignment holes.")
@@ -288,7 +288,12 @@ class DblSidedTool(FlatCAMTool):
             else:
             else:
                 try:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
                     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:
                 except AttributeError:
                     pass
                     pass
         else:
         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(0, QtGui.QIcon("share/flatcam_icon16.png"))
         self.tf_type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.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(
         self.tf_type_obj_combo_label.setToolTip(
             _("Specify the type of object for which to create the film.\n"
             _("Specify the type of object for which to create the film.\n"
               "The object can be of type: Gerber or Geometry.\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.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.tf_object_combo.setCurrentIndex(1)
         self.tf_object_combo.setCurrentIndex(1)
 
 
-        self.tf_object_label = QtWidgets.QLabel(_("Film Object:"))
+        self.tf_object_label = QtWidgets.QLabel('%s:' % _("Film Object"))
         self.tf_object_label.setToolTip(
         self.tf_object_label.setToolTip(
             _("Object for which to create the film.")
             _("Object for which to create the film.")
         )
         )
@@ -101,7 +101,7 @@ class Film(FlatCAMTool):
         self.tf_box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.tf_box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.tf_box_combo.setCurrentIndex(1)
         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(
         self.tf_box_combo_label.setToolTip(
             _("The actual object that is used a container for the\n "
             _("The actual object that is used a container for the\n "
               "selected object for which we create the film.\n"
               "selected object for which we create the film.\n"
@@ -127,7 +127,7 @@ class Film(FlatCAMTool):
         # Boundary for negative film generation
         # Boundary for negative film generation
 
 
         self.boundary_entry = FCEntry()
         self.boundary_entry = FCEntry()
-        self.boundary_label = QtWidgets.QLabel(_("Border:"))
+        self.boundary_label = QtWidgets.QLabel('%s:' % _("Border"))
         self.boundary_label.setToolTip(
         self.boundary_label.setToolTip(
             _("Specify a border around the object.\n"
             _("Specify a border around the object.\n"
               "Only for negative film.\n"
               "Only for negative film.\n"
@@ -141,7 +141,7 @@ class Film(FlatCAMTool):
         tf_form_layout.addRow(self.boundary_label, self.boundary_entry)
         tf_form_layout.addRow(self.boundary_label, self.boundary_entry)
 
 
         self.film_scale_entry = FCEntry()
         self.film_scale_entry = FCEntry()
-        self.film_scale_label = QtWidgets.QLabel(_("Scale Stroke:"))
+        self.film_scale_label = QtWidgets.QLabel('%s:' % _("Scale Stroke"))
         self.film_scale_label.setToolTip(
         self.film_scale_label.setToolTip(
             _("Scale the line stroke thickness of each feature in the SVG file.\n"
             _("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"
               "It means that the line that envelope each SVG feature will be thicker or thinner,\n"
@@ -190,7 +190,12 @@ class Film(FlatCAMTool):
             else:
             else:
                 try:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
                     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:
                 except AttributeError:
                     pass
                     pass
         else:
         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(0, QtGui.QIcon("share/flatcam_icon16.png"))
         self.tf_type_obj_combo.setItemIcon(1, QtGui.QIcon("share/geometry16.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(
         self.tf_type_obj_combo_label.setToolTip(
            _("Specify the type of object to create from the image.\n"
            _("Specify the type of object to create from the image.\n"
              "It can be of type: Gerber or Geometry.")
              "It can be of type: Gerber or Geometry.")
@@ -60,7 +60,7 @@ class ToolImage(FlatCAMTool):
 
 
         # DPI value of the imported image
         # DPI value of the imported image
         self.dpi_entry = IntEntry()
         self.dpi_entry = IntEntry()
-        self.dpi_label = QtWidgets.QLabel(_("DPI value:"))
+        self.dpi_label = QtWidgets.QLabel('%s:' % _("DPI value"))
         self.dpi_label.setToolTip(
         self.dpi_label.setToolTip(
            _("Specify a DPI value for the image.")
            _("Specify a DPI value for the image.")
         )
         )
@@ -69,7 +69,7 @@ class ToolImage(FlatCAMTool):
         self.emty_lbl = QtWidgets.QLabel("")
         self.emty_lbl = QtWidgets.QLabel("")
         self.layout.addWidget(self.emty_lbl)
         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)
         self.layout.addWidget(self.detail_label)
 
 
         ti2_form_layout = QtWidgets.QFormLayout()
         ti2_form_layout = QtWidgets.QFormLayout()
@@ -157,7 +157,12 @@ class ToolImage(FlatCAMTool):
             else:
             else:
                 try:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
                     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:
                 except AttributeError:
                     pass
                     pass
         else:
         else:

+ 8 - 8
flatcamTools/ToolMeasurement.py

@@ -40,7 +40,7 @@ class Measurement(FlatCAMTool):
         form_layout = QtWidgets.QFormLayout()
         form_layout = QtWidgets.QFormLayout()
         self.layout.addLayout(form_layout)
         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_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 = QtWidgets.QLabel("%s" % str({'mm': _("METRIC (mm)"), 'in': _("INCH (in)")}[self.units]))
         self.units_value.setDisabled(True)
         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 = QtWidgets.QLabel("<b>%s</b> %s:" % (_('Stop'), _('Coords')))
         self.stop_label.setToolTip(_("This is the measuring Stop point coordinates."))
         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_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.distance_y_label.setToolTip(_("This is the distance measured over the Y axis."))
 
 
         self.total_distance_label = QtWidgets.QLabel("<b>%s:</b>" % _('DISTANCE'))
         self.total_distance_label = QtWidgets.QLabel("<b>%s:</b>" % _('DISTANCE'))
@@ -113,7 +113,7 @@ class Measurement(FlatCAMTool):
         self.original_call_source = 'app'
         self.original_call_source = 'app'
 
 
         # VisPy visuals
         # 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)
         self.measure_btn.clicked.connect(self.activate_measure_tool)
 
 
@@ -247,9 +247,9 @@ class Measurement(FlatCAMTool):
         log.debug("Measuring Tool --> mouse click release")
         log.debug("Measuring Tool --> mouse click release")
 
 
         if event.button == 1:
         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 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])
                 pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
             else:
             else:
                 pos = pos_canvas[0], pos_canvas[1]
                 pos = pos_canvas[0], pos_canvas[1]
@@ -286,8 +286,8 @@ class Measurement(FlatCAMTool):
 
 
     def on_mouse_move_meas(self, event):
     def on_mouse_move_meas(self, event):
         try:  # May fail in case mouse not within axes
         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])
                 pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
                 self.app.app_cursor.enabled = True
                 self.app.app_cursor.enabled = True
                 # Update cursor
                 # Update cursor

+ 4 - 4
flatcamTools/ToolMove.py

@@ -43,7 +43,7 @@ class ToolMove(FlatCAMTool):
         self.old_coords = []
         self.old_coords = []
 
 
         # VisPy visuals
         # 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):
     def install(self, icon=None, separator=None, **kwargs):
         FlatCAMTool.install(self, icon, separator, shortcut='M', **kwargs)
         FlatCAMTool.install(self, icon, separator, shortcut='M', **kwargs)
@@ -94,7 +94,7 @@ class ToolMove(FlatCAMTool):
 
 
         if event.button == 1:
         if event.button == 1:
             if self.clicked_move == 0:
             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 GRID is active we need to get the snapped positions
                 if self.app.grid_status() == True:
                 if self.app.grid_status() == True:
@@ -111,7 +111,7 @@ class ToolMove(FlatCAMTool):
 
 
             if self.clicked_move == 1:
             if self.clicked_move == 1:
                 try:
                 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
                     # delete the selection bounding box
                     self.delete_shape()
                     self.delete_shape()
@@ -178,7 +178,7 @@ class ToolMove(FlatCAMTool):
             self.clicked_move = 1
             self.clicked_move = 1
 
 
     def on_move(self, event):
     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 GRID is active we need to get the snapped positions
         if self.app.grid_status() == True:
         if self.app.grid_status() == True:

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 606 - 172
flatcamTools/ToolNonCopperClear.py


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 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(1, QtGui.QIcon("share/drill16.png"))
         self.type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.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(
         self.type_obj_combo_label.setToolTip(
             _("Specify the type of object to be panelized\n"
             _("Specify the type of object to be panelized\n"
               "It can be of type: Gerber, Excellon or Geometry.\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.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.object_combo.setCurrentIndex(1)
         self.object_combo.setCurrentIndex(1)
 
 
-        self.object_label = QtWidgets.QLabel(_("Object:"))
+        self.object_label = QtWidgets.QLabel('%s:' % _("Object"))
         self.object_label.setToolTip(
         self.object_label.setToolTip(
             _("Object to be panelized. This means that it will\n"
             _("Object to be panelized. This means that it will\n"
               "be duplicated in an array of rows and columns.")
               "be duplicated in an array of rows and columns.")
@@ -83,7 +83,7 @@ class Panelize(FlatCAMTool):
         # Type of box Panel object
         # Type of box Panel object
         self.reference_radio = RadioSet([{'label': _('Object'), 'value': 'object'},
         self.reference_radio = RadioSet([{'label': _('Object'), 'value': 'object'},
                                          {'label': _('Bounding Box'), 'value': 'bbox'}])
                                          {'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(
         self.box_label.setToolTip(
             _("Choose the reference for panelization:\n"
             _("Choose the reference for panelization:\n"
               "- Object = the bounding box of a different object\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(0, QtGui.QIcon("share/flatcam_icon16.png"))
         self.type_box_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.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(
         self.type_box_combo_label.setToolTip(
             _("Specify the type of object to be used as an container for\n"
             _("Specify the type of object to be used as an container for\n"
               "panelization. It can be: Gerber or Geometry type.\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.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.box_combo.setCurrentIndex(1)
         self.box_combo.setCurrentIndex(1)
 
 
-        self.box_combo_label = QtWidgets.QLabel(_("Box Object:"))
+        self.box_combo_label = QtWidgets.QLabel('%s:' % _("Box Object"))
         self.box_combo_label.setToolTip(
         self.box_combo_label.setToolTip(
             _("The actual object that is used a container for the\n "
             _("The actual object that is used a container for the\n "
               "selected object that is to be panelized.")
               "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(self.box_combo_label, self.box_combo)
         form_layout.addRow(QtWidgets.QLabel(""))
         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(
         panel_data_label.setToolTip(
             _("This informations will shape the resulting panel.\n"
             _("This informations will shape the resulting panel.\n"
               "The number of rows and columns will set how many\n"
               "The number of rows and columns will set how many\n"
@@ -144,7 +144,7 @@ class Panelize(FlatCAMTool):
 
 
         # Spacing Columns
         # Spacing Columns
         self.spacing_columns = FCEntry()
         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(
         self.spacing_columns_label.setToolTip(
             _("Spacing between columns of the desired panel.\n"
             _("Spacing between columns of the desired panel.\n"
               "In current units.")
               "In current units.")
@@ -153,7 +153,7 @@ class Panelize(FlatCAMTool):
 
 
         # Spacing Rows
         # Spacing Rows
         self.spacing_rows = FCEntry()
         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(
         self.spacing_rows_label.setToolTip(
             _("Spacing between rows of the desired panel.\n"
             _("Spacing between rows of the desired panel.\n"
               "In current units.")
               "In current units.")
@@ -162,7 +162,7 @@ class Panelize(FlatCAMTool):
 
 
         # Columns
         # Columns
         self.columns = FCEntry()
         self.columns = FCEntry()
-        self.columns_label = QtWidgets.QLabel(_("Columns:"))
+        self.columns_label = QtWidgets.QLabel('%s:' % _("Columns"))
         self.columns_label.setToolTip(
         self.columns_label.setToolTip(
             _("Number of columns of the desired panel")
             _("Number of columns of the desired panel")
         )
         )
@@ -170,7 +170,7 @@ class Panelize(FlatCAMTool):
 
 
         # Rows
         # Rows
         self.rows = FCEntry()
         self.rows = FCEntry()
-        self.rows_label = QtWidgets.QLabel(_("Rows:"))
+        self.rows_label = QtWidgets.QLabel('%s:' % _("Rows"))
         self.rows_label.setToolTip(
         self.rows_label.setToolTip(
             _("Number of rows of the desired panel")
             _("Number of rows of the desired panel")
         )
         )
@@ -180,7 +180,7 @@ class Panelize(FlatCAMTool):
         # Type of resulting Panel object
         # Type of resulting Panel object
         self.panel_type_radio = RadioSet([{'label': _('Gerber'), 'value': 'gerber'},
         self.panel_type_radio = RadioSet([{'label': _('Gerber'), 'value': 'gerber'},
                                           {'label': _('Geo'), 'value': 'geometry'}])
                                           {'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(
         self.panel_type_label.setToolTip(
             _("Choose the type of object for the panel object:\n"
             _("Choose the type of object for the panel object:\n"
               "- Geometry\n"
               "- Geometry\n"
@@ -190,7 +190,7 @@ class Panelize(FlatCAMTool):
         form_layout.addRow(self.panel_type_radio)
         form_layout.addRow(self.panel_type_radio)
 
 
         # Constrains
         # Constrains
-        self.constrain_cb = FCCheckBox(_("Constrain panel within:"))
+        self.constrain_cb = FCCheckBox('%s:' % _("Constrain panel within"))
         self.constrain_cb.setToolTip(
         self.constrain_cb.setToolTip(
             _("Area define by DX and DY within to constrain the panel.\n"
             _("Area define by DX and DY within to constrain the panel.\n"
               "DX and DY values are in current units.\n"
               "DX and DY values are in current units.\n"
@@ -201,7 +201,7 @@ class Panelize(FlatCAMTool):
         form_layout.addRow(self.constrain_cb)
         form_layout.addRow(self.constrain_cb)
 
 
         self.x_width_entry = FCEntry()
         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(
         self.x_width_lbl.setToolTip(
             _("The width (DX) within which the panel must fit.\n"
             _("The width (DX) within which the panel must fit.\n"
               "In current units.")
               "In current units.")
@@ -209,7 +209,7 @@ class Panelize(FlatCAMTool):
         form_layout.addRow(self.x_width_lbl, self.x_width_entry)
         form_layout.addRow(self.x_width_lbl, self.x_width_entry)
 
 
         self.y_height_entry = FCEntry()
         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(
         self.y_height_lbl.setToolTip(
             _("The height (DY)within which the panel must fit.\n"
             _("The height (DY)within which the panel must fit.\n"
               "In current units.")
               "In current units.")
@@ -259,7 +259,12 @@ class Panelize(FlatCAMTool):
             else:
             else:
                 try:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
                     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:
                 except AttributeError:
                     pass
                     pass
         else:
         else:

+ 13 - 8
flatcamTools/ToolPcbWizard.py

@@ -48,13 +48,13 @@ class PcbWizard(FlatCAMTool):
         self.layout.addWidget(title_label)
         self.layout.addWidget(title_label)
 
 
         self.layout.addWidget(QtWidgets.QLabel(""))
         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
         form_layout = QtWidgets.QFormLayout()
         form_layout = QtWidgets.QFormLayout()
         self.layout.addLayout(form_layout)
         self.layout.addLayout(form_layout)
 
 
-        self.excellon_label = QtWidgets.QLabel(_("Excellon file:"))
+        self.excellon_label = QtWidgets.QLabel('%s:' % _("Excellon file"))
         self.excellon_label.setToolTip(
         self.excellon_label.setToolTip(
            _("Load the Excellon file.\n"
            _("Load the Excellon file.\n"
              "Usually it has a .DRL extension")
              "Usually it has a .DRL extension")
@@ -62,7 +62,7 @@ class PcbWizard(FlatCAMTool):
         self.excellon_brn = FCButton(_("Open"))
         self.excellon_brn = FCButton(_("Open"))
         form_layout.addRow(self.excellon_label, self.excellon_brn)
         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(
         self.inf_label.setToolTip(
             _("Load the INF file.")
             _("Load the INF file.")
         )
         )
@@ -84,7 +84,7 @@ class PcbWizard(FlatCAMTool):
         self.tools_table.setVisible(False)
         self.tools_table.setVisible(False)
 
 
         self.layout.addWidget(QtWidgets.QLabel(""))
         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 Layout
         form_layout1 = QtWidgets.QFormLayout()
         form_layout1 = QtWidgets.QFormLayout()
         self.layout.addLayout(form_layout1)
         self.layout.addLayout(form_layout1)
@@ -92,7 +92,7 @@ class PcbWizard(FlatCAMTool):
         # Integral part of the coordinates
         # Integral part of the coordinates
         self.int_entry = FCSpinner()
         self.int_entry = FCSpinner()
         self.int_entry.set_range(1, 10)
         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(
         self.int_label.setToolTip(
            _("The number of digits for the integral part of the coordinates.")
            _("The number of digits for the integral part of the coordinates.")
         )
         )
@@ -101,7 +101,7 @@ class PcbWizard(FlatCAMTool):
         # Fractional part of the coordinates
         # Fractional part of the coordinates
         self.frac_entry = FCSpinner()
         self.frac_entry = FCSpinner()
         self.frac_entry.set_range(1, 10)
         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(
         self.frac_label.setToolTip(
             _("The number of digits for the fractional part of the coordinates.")
             _("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'},
         self.zeros_radio = RadioSet([{'label': _('LZ'), 'value': 'LZ'},
                                      {'label': _('TZ'), 'value': 'TZ'},
                                      {'label': _('TZ'), 'value': 'TZ'},
                                      {'label': _('No Suppression'), 'value': 'D'}])
                                      {'label': _('No Suppression'), 'value': 'D'}])
-        self.zeros_label = QtWidgets.QLabel(_("Zeros supp.:"))
+        self.zeros_label = QtWidgets.QLabel('%s:' % _("Zeros supp."))
         self.zeros_label.setToolTip(
         self.zeros_label.setToolTip(
             _("The type of zeros suppression used.\n"
             _("The type of zeros suppression used.\n"
               "Can be of type:\n"
               "Can be of type:\n"
@@ -179,7 +179,12 @@ class PcbWizard(FlatCAMTool):
             else:
             else:
                 try:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
                     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:
                 except AttributeError:
                     pass
                     pass
         else:
         else:

+ 49 - 20
flatcamTools/ToolProperties.py

@@ -77,7 +77,12 @@ class Properties(FlatCAMTool):
             else:
             else:
                 try:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
                     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:
                 except AttributeError:
                     pass
                     pass
         else:
         else:
@@ -117,23 +122,24 @@ class Properties(FlatCAMTool):
 
 
         font = QtGui.QFont()
         font = QtGui.QFont()
         font.setBold(True)
         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':
         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:
         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, '')
         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:
         try:
             self.addChild(obj_type,
             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)
                           True)
         except Exception as e:
         except Exception as e:
             log.debug("Properties.addItems() --> %s" % str(e))
             log.debug("Properties.addItems() --> %s" % str(e))
@@ -150,22 +156,45 @@ class Properties(FlatCAMTool):
         length = abs(xmax - xmin)
         length = abs(xmax - xmin)
         width = abs(ymax - ymin)
         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)
             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)
             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':
         if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower() == 'mm':
             area = (length * width) / 100
             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:
         else:
             area = length * width
             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,
         self.addChild(units,
                       ['FlatCAM 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())]
                        [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)
                 geo_tool = self.addParent(tools, str(tool), expanded=True, color=QtGui.QColor("#000000"), font=font)
                 for k, v in value.items():
                 for k, v in value.items():
                     if k == 'solid_geometry':
                     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)
                         self.addChild(geo_tool, [str(k), printed_value], True)
                     elif k == 'data':
                     elif k == 'data':
                         tool_data = self.addParent(geo_tool, str(k).capitalize(),
                         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)
                 geo_tool = self.addParent(tools, str(tool), expanded=True, color=QtGui.QColor("#000000"), font=font)
                 for k, v in value.items():
                 for k, v in value.items():
                     if k == 'solid_geometry':
                     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)
                         self.addChild(geo_tool, [str(k), printed_value], True)
                     elif k == 'gcode':
                     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)
                         self.addChild(geo_tool, [str(k), printed_value], True)
                     elif k == 'gcode_parsed':
                     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)
                         self.addChild(geo_tool, [str(k), printed_value], True)
                     elif k == 'data':
                     elif k == 'data':
                         tool_data = self.addParent(geo_tool, str(k).capitalize(),
                         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()
         grid0_1 = QtWidgets.QGridLayout()
         self.layout.addLayout(grid0_1)
         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(
         step1_lbl.setToolTip(
             _("First step is to select a number of nozzle tools for usage\n"
             _("First step is to select a number of nozzle tools for usage\n"
               "and then optionally modify the GCode parameters bellow.")
               "and then optionally modify the GCode parameters bellow.")
@@ -163,7 +163,7 @@ class SolderPaste(FlatCAMTool):
 
 
         # Z dispense start
         # Z dispense start
         self.z_start_entry = FCEntry()
         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(
         self.z_start_label.setToolTip(
             _("The height (Z) when solder paste dispensing starts.")
             _("The height (Z) when solder paste dispensing starts.")
         )
         )
@@ -171,7 +171,7 @@ class SolderPaste(FlatCAMTool):
 
 
         # Z dispense
         # Z dispense
         self.z_dispense_entry = FCEntry()
         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(
         self.z_dispense_label.setToolTip(
             _("The height (Z) when doing solder paste dispensing.")
             _("The height (Z) when doing solder paste dispensing.")
         )
         )
@@ -179,7 +179,7 @@ class SolderPaste(FlatCAMTool):
 
 
         # Z dispense stop
         # Z dispense stop
         self.z_stop_entry = FCEntry()
         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(
         self.z_stop_label.setToolTip(
             _("The height (Z) when solder paste dispensing stops.")
             _("The height (Z) when solder paste dispensing stops.")
         )
         )
@@ -187,7 +187,7 @@ class SolderPaste(FlatCAMTool):
 
 
         # Z travel
         # Z travel
         self.z_travel_entry = FCEntry()
         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(
         self.z_travel_label.setToolTip(
            _("The height (Z) for travel between pads\n"
            _("The height (Z) for travel between pads\n"
              "(without dispensing solder paste).")
              "(without dispensing solder paste).")
@@ -196,7 +196,7 @@ class SolderPaste(FlatCAMTool):
 
 
         # Z toolchange location
         # Z toolchange location
         self.z_toolchange_entry = FCEntry()
         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(
         self.z_toolchange_label.setToolTip(
            _("The height (Z) for tool (nozzle) change.")
            _("The height (Z) for tool (nozzle) change.")
         )
         )
@@ -204,7 +204,7 @@ class SolderPaste(FlatCAMTool):
 
 
         # X,Y Toolchange location
         # X,Y Toolchange location
         self.xy_toolchange_entry = FCEntry()
         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(
         self.xy_toolchange_label.setToolTip(
             _("The X,Y location for tool (nozzle) change.\n"
             _("The X,Y location for tool (nozzle) change.\n"
               "The format is (x, y) where x and y are real numbers.")
               "The format is (x, y) where x and y are real numbers.")
@@ -213,7 +213,7 @@ class SolderPaste(FlatCAMTool):
 
 
         # Feedrate X-Y
         # Feedrate X-Y
         self.frxy_entry = FCEntry()
         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(
         self.frxy_label.setToolTip(
            _("Feedrate (speed) while moving on the X-Y plane.")
            _("Feedrate (speed) while moving on the X-Y plane.")
         )
         )
@@ -221,7 +221,7 @@ class SolderPaste(FlatCAMTool):
 
 
         # Feedrate Z
         # Feedrate Z
         self.frz_entry = FCEntry()
         self.frz_entry = FCEntry()
-        self.frz_label = QtWidgets.QLabel(_("Feedrate Z:"))
+        self.frz_label = QtWidgets.QLabel('%s:' % _("Feedrate Z"))
         self.frz_label.setToolTip(
         self.frz_label.setToolTip(
             _("Feedrate (speed) while moving vertically\n"
             _("Feedrate (speed) while moving vertically\n"
               "(on Z plane).")
               "(on Z plane).")
@@ -230,7 +230,7 @@ class SolderPaste(FlatCAMTool):
 
 
         # Feedrate Z Dispense
         # Feedrate Z Dispense
         self.frz_dispense_entry = FCEntry()
         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(
         self.frz_dispense_label.setToolTip(
            _("Feedrate (speed) while moving up vertically\n"
            _("Feedrate (speed) while moving up vertically\n"
              " to Dispense position (on Z plane).")
              " to Dispense position (on Z plane).")
@@ -239,7 +239,7 @@ class SolderPaste(FlatCAMTool):
 
 
         # Spindle Speed Forward
         # Spindle Speed Forward
         self.speedfwd_entry = FCEntry()
         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(
         self.speedfwd_label.setToolTip(
            _("The dispenser speed while pushing solder paste\n"
            _("The dispenser speed while pushing solder paste\n"
              "through the dispenser nozzle.")
              "through the dispenser nozzle.")
@@ -248,7 +248,7 @@ class SolderPaste(FlatCAMTool):
 
 
         # Dwell Forward
         # Dwell Forward
         self.dwellfwd_entry = FCEntry()
         self.dwellfwd_entry = FCEntry()
-        self.dwellfwd_label = QtWidgets.QLabel(_("Dwell FWD:"))
+        self.dwellfwd_label = QtWidgets.QLabel('%s:' % _("Dwell FWD"))
         self.dwellfwd_label.setToolTip(
         self.dwellfwd_label.setToolTip(
             _("Pause after solder dispensing.")
             _("Pause after solder dispensing.")
         )
         )
@@ -256,7 +256,7 @@ class SolderPaste(FlatCAMTool):
 
 
         # Spindle Speed Reverse
         # Spindle Speed Reverse
         self.speedrev_entry = FCEntry()
         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(
         self.speedrev_label.setToolTip(
            _("The dispenser speed while retracting solder paste\n"
            _("The dispenser speed while retracting solder paste\n"
              "through the dispenser nozzle.")
              "through the dispenser nozzle.")
@@ -265,7 +265,7 @@ class SolderPaste(FlatCAMTool):
 
 
         # Dwell Reverse
         # Dwell Reverse
         self.dwellrev_entry = FCEntry()
         self.dwellrev_entry = FCEntry()
-        self.dwellrev_label = QtWidgets.QLabel(_("Dwell REV:"))
+        self.dwellrev_label = QtWidgets.QLabel('%s:' % _("Dwell REV"))
         self.dwellrev_label.setToolTip(
         self.dwellrev_label.setToolTip(
             _("Pause after solder paste dispenser retracted,\n"
             _("Pause after solder paste dispenser retracted,\n"
               "to allow pressure equilibrium.")
               "to allow pressure equilibrium.")
@@ -273,7 +273,7 @@ class SolderPaste(FlatCAMTool):
         self.gcode_form_layout.addRow(self.dwellrev_label, self.dwellrev_entry)
         self.gcode_form_layout.addRow(self.dwellrev_label, self.dwellrev_entry)
 
 
         # Postprocessors
         # Postprocessors
-        pp_label = QtWidgets.QLabel(_('PostProcessors:'))
+        pp_label = QtWidgets.QLabel('%s:' % _('PostProcessor'))
         pp_label.setToolTip(
         pp_label.setToolTip(
             _("Files that control the GCode generation.")
             _("Files that control the GCode generation.")
         )
         )
@@ -303,7 +303,7 @@ class SolderPaste(FlatCAMTool):
         grid2 = QtWidgets.QGridLayout()
         grid2 = QtWidgets.QGridLayout()
         self.generation_box.addLayout(grid2)
         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(
         step2_lbl.setToolTip(
             _("Second step is to create a solder paste dispensing\n"
             _("Second step is to create a solder paste dispensing\n"
               "geometry out of an Solder Paste Mask Gerber file.")
               "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.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
         self.geo_obj_combo.setCurrentIndex(1)
         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(
         self.geo_object_label.setToolTip(
            _("Geometry Solder Paste object.\n"
            _("Geometry Solder Paste object.\n"
              "The name of the object has to end in:\n"
              "The name of the object has to end in:\n"
@@ -332,7 +332,7 @@ class SolderPaste(FlatCAMTool):
         grid3 = QtWidgets.QGridLayout()
         grid3 = QtWidgets.QGridLayout()
         self.generation_box.addLayout(grid3)
         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(
         step3_lbl.setToolTip(
            _("Third step is to select a solder paste dispensing geometry,\n"
            _("Third step is to select a solder paste dispensing geometry,\n"
              "and then generate a CNCJob object.\n\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.setRootModelIndex(self.app.collection.index(3, 0, QtCore.QModelIndex()))
         self.cnc_obj_combo.setCurrentIndex(1)
         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(
         self.cnc_object_label.setToolTip(
            _("CNCJob Solder paste object.\n"
            _("CNCJob Solder paste object.\n"
              "In order to enable the GCode save section,\n"
              "In order to enable the GCode save section,\n"
@@ -378,7 +378,7 @@ class SolderPaste(FlatCAMTool):
              "on PCB pads, to a file.")
              "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(
         step4_lbl.setToolTip(
            _("Fourth step (and last) is to select a CNCJob made from \n"
            _("Fourth step (and last) is to select a CNCJob made from \n"
              "a solder paste dispensing geometry, and then view/save it's GCode.")
              "a solder paste dispensing geometry, and then view/save it's GCode.")
@@ -436,7 +436,12 @@ class SolderPaste(FlatCAMTool):
             else:
             else:
                 try:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
                     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:
                 except AttributeError:
                     pass
                     pass
         else:
         else:

+ 116 - 37
flatcamTools/ToolSub.py

@@ -23,6 +23,8 @@ if '_' not in builtins.__dict__:
 
 
 class ToolSub(FlatCAMTool):
 class ToolSub(FlatCAMTool):
 
 
+    job_finished = QtCore.pyqtSignal(bool)
+
     toolName = _("Substract Tool")
     toolName = _("Substract Tool")
 
 
     def __init__(self, app):
     def __init__(self, app):
@@ -52,7 +54,7 @@ class ToolSub(FlatCAMTool):
         form_layout = QtWidgets.QFormLayout()
         form_layout = QtWidgets.QFormLayout()
         self.tools_box.addLayout(form_layout)
         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)
         form_layout.addRow(self.gerber_title)
 
 
         # Target Gerber Object
         # 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.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.target_gerber_combo.setCurrentIndex(1)
         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(
         self.target_gerber_label.setToolTip(
             _("Gerber object from which to substract\n"
             _("Gerber object from which to substract\n"
               "the substractor Gerber object.")
               "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.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.sub_gerber_combo.setCurrentIndex(1)
         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(
         self.sub_gerber_label.setToolTip(
             _("Gerber object that will be substracted\n"
             _("Gerber object that will be substracted\n"
               "from the target Gerber object.")
               "from the target Gerber object.")
@@ -98,7 +100,7 @@ class ToolSub(FlatCAMTool):
         form_geo_layout = QtWidgets.QFormLayout()
         form_geo_layout = QtWidgets.QFormLayout()
         self.tools_box.addLayout(form_geo_layout)
         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)
         form_geo_layout.addRow(self.geo_title)
 
 
         # Target Geometry Object
         # 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.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
         self.target_geo_combo.setCurrentIndex(1)
         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(
         self.target_geo_label.setToolTip(
             _("Geometry object from which to substract\n"
             _("Geometry object from which to substract\n"
               "the substractor Geometry object.")
               "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.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
         self.sub_geo_combo.setCurrentIndex(1)
         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(
         self.sub_geo_label.setToolTip(
             _("Geometry object that will be substracted\n"
             _("Geometry object that will be substracted\n"
               "from the target Geometry object.")
               "from the target Geometry object.")
@@ -130,6 +132,10 @@ class ToolSub(FlatCAMTool):
 
 
         form_geo_layout.addRow(self.sub_geo_label, self.sub_geo_combo)
         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 = FCButton(_('Substract Geometry'))
         self.intersect_geo_btn.setToolTip(
         self.intersect_geo_btn.setToolTip(
             _("Will remove the area occupied by the substractor\n"
             _("Will remove the area occupied by the substractor\n"
@@ -184,6 +190,7 @@ class ToolSub(FlatCAMTool):
         except (TypeError, AttributeError):
         except (TypeError, AttributeError):
             pass
             pass
         self.intersect_geo_btn.clicked.connect(self.on_geo_intersection_click)
         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):
     def install(self, icon=None, separator=None, **kwargs):
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+W', **kwargs)
         FlatCAMTool.install(self, icon, separator, shortcut='ALT+W', **kwargs)
@@ -198,7 +205,12 @@ class ToolSub(FlatCAMTool):
             else:
             else:
                 try:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
                     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:
                 except AttributeError:
                     pass
                     pass
         else:
         else:
@@ -217,6 +229,7 @@ class ToolSub(FlatCAMTool):
 
 
     def set_tool_ui(self):
     def set_tool_ui(self):
         self.tools_frame.show()
         self.tools_frame.show()
+        self.close_paths_cb.setChecked(self.app.defaults["tools_sub_close_paths"])
 
 
     def on_grb_intersection_click(self):
     def on_grb_intersection_click(self):
         # reset previous values
         # reset previous values
@@ -231,7 +244,7 @@ class ToolSub(FlatCAMTool):
             self.app.inform.emit(_("[ERROR_NOTCL] No Target object loaded."))
             self.app.inform.emit(_("[ERROR_NOTCL] No Target object loaded."))
             return
             return
 
 
-        # Get source object.
+        # Get target object.
         try:
         try:
             self.target_grb_obj = self.app.collection.get_by_name(self.target_grb_obj_name)
             self.target_grb_obj = self.app.collection.get_by_name(self.target_grb_obj_name)
         except Exception as e:
         except Exception as e:
@@ -244,7 +257,7 @@ class ToolSub(FlatCAMTool):
             self.app.inform.emit(_("[ERROR_NOTCL] No Substractor object loaded."))
             self.app.inform.emit(_("[ERROR_NOTCL] No Substractor object loaded."))
             return
             return
 
 
-        # Get source object.
+        # Get substractor object.
         try:
         try:
             self.sub_grb_obj = self.app.collection.get_by_name(self.sub_grb_obj_name)
             self.sub_grb_obj = self.app.collection.get_by_name(self.sub_grb_obj_name)
         except Exception as e:
         except Exception as e:
@@ -281,7 +294,7 @@ class ToolSub(FlatCAMTool):
         for apid in self.target_grb_obj.apertures:
         for apid in self.target_grb_obj.apertures:
             self.promises.append(apid)
             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)
         self.periodic_check(500, reset=True)
 
 
         for apid in self.target_grb_obj.apertures:
         for apid in self.target_grb_obj.apertures:
@@ -408,7 +421,10 @@ class ToolSub(FlatCAMTool):
             # cleanup
             # cleanup
             self.new_apertures.clear()
             self.new_apertures.clear()
             self.new_solid_geometry[:] = []
             self.new_solid_geometry[:] = []
-            self.sub_union[:] = []
+            try:
+                self.sub_union[:] = []
+            except TypeError:
+                self.sub_union = []
 
 
     def on_geo_intersection_click(self):
     def on_geo_intersection_click(self):
         # reset previous values
         # reset previous values
@@ -424,7 +440,7 @@ class ToolSub(FlatCAMTool):
             self.app.inform.emit(_("[ERROR_NOTCL] No Target object loaded."))
             self.app.inform.emit(_("[ERROR_NOTCL] No Target object loaded."))
             return
             return
 
 
-        # Get source object.
+        # Get target object.
         try:
         try:
             self.target_geo_obj = self.app.collection.get_by_name(self.target_geo_obj_name)
             self.target_geo_obj = self.app.collection.get_by_name(self.target_geo_obj_name)
         except Exception as e:
         except Exception as e:
@@ -437,7 +453,7 @@ class ToolSub(FlatCAMTool):
             self.app.inform.emit(_("[ERROR_NOTCL] No Substractor object loaded."))
             self.app.inform.emit(_("[ERROR_NOTCL] No Substractor object loaded."))
             return
             return
 
 
-        # Get source object.
+        # Get substractor object.
         try:
         try:
             self.sub_geo_obj = self.app.collection.get_by_name(self.sub_geo_obj_name)
             self.sub_geo_obj = self.app.collection.get_by_name(self.sub_geo_obj_name)
         except Exception as e:
         except Exception as e:
@@ -450,14 +466,14 @@ class ToolSub(FlatCAMTool):
             return
             return
 
 
         # create the target_options obj
         # 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
         # crate the new_tools dict structure
         for tool in self.target_geo_obj.tools:
         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]:
             for key in self.target_geo_obj.tools[tool]:
                 if key == 'solid_geometry':
                 if key == 'solid_geometry':
                     self.new_tools[tool][key] = []
                     self.new_tools[tool][key] = []
@@ -496,10 +512,53 @@ class ToolSub(FlatCAMTool):
             text = _("Parsing tool %s geometry ...") % str(tool)
             text = _("Parsing tool %s geometry ...") % str(tool)
 
 
         with self.app.proc_container.new(text):
         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 new_geometry:
             if tool == "single":
             if tool == "single":
@@ -522,10 +581,14 @@ class ToolSub(FlatCAMTool):
         log.debug("Promise fulfilled: %s" % str(tool))
         log.debug("Promise fulfilled: %s" % str(tool))
 
 
     def new_geo_object(self, outname):
     def new_geo_object(self, outname):
+        geo_name = outname
         def obj_init(geo_obj, app_obj):
         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:
             if self.target_geo_obj.multigeo:
                 geo_obj.tools = deepcopy(self.new_tools)
                 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)
                         geo_obj.tools[tool]['solid_geometry'] = deepcopy(self.new_solid_geometry)
                 except Exception as e:
                 except Exception as e:
                     log.debug("ToolSub.new_geo_object() --> %s" % str(e))
                     log.debug("ToolSub.new_geo_object() --> %s" % str(e))
+                geo_obj.multigeo = False
 
 
         with self.app.proc_container.new(_("Generating new object ...")):
         with self.app.proc_container.new(_("Generating new object ...")):
             ret = self.app.new_object('geometry', outname, obj_init, autoselected=False)
             ret = self.app.new_object('geometry', outname, obj_init, autoselected=False)
@@ -554,7 +618,7 @@ class ToolSub(FlatCAMTool):
             # cleanup
             # cleanup
             self.new_tools.clear()
             self.new_tools.clear()
             self.new_solid_geometry[:] = []
             self.new_solid_geometry[:] = []
-            self.sub_union[:] = []
+            self.sub_union = []
 
 
     def periodic_check(self, check_period, reset=False):
     def periodic_check(self, check_period, reset=False):
         """
         """
@@ -592,27 +656,39 @@ class ToolSub(FlatCAMTool):
         try:
         try:
             if not self.promises:
             if not self.promises:
                 self.check_thread.stop()
                 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
                 # reset the type of substraction for next time
                 self.sub_type = None
                 self.sub_type = None
 
 
                 log.debug("ToolSub --> Periodic check finished.")
                 log.debug("ToolSub --> Periodic check finished.")
         except Exception as e:
         except Exception as e:
+            self.job_finished.emit(False)
             log.debug("ToolSub().periodic_check_handler() --> %s" % str(e))
             log.debug("ToolSub().periodic_check_handler() --> %s" % str(e))
             traceback.print_exc()
             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):
     def reset_fields(self):
         self.target_gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         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()))
         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.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()))
         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
 # end of file

+ 22 - 16
flatcamTools/ToolTransform.py

@@ -65,7 +65,7 @@ class ToolTransform(FlatCAMTool):
         self.transform_lay.addLayout(form_layout)
         self.transform_lay.addLayout(form_layout)
         form_child = QtWidgets.QHBoxLayout()
         form_child = QtWidgets.QHBoxLayout()
 
 
-        self.rotate_label = QtWidgets.QLabel(_("Angle:"))
+        self.rotate_label = QtWidgets.QLabel('%s:' % _("Angle"))
         self.rotate_label.setToolTip(
         self.rotate_label.setToolTip(
             _("Angle for Rotation action, in degrees.\n"
             _("Angle for Rotation action, in degrees.\n"
               "Float number between -360 and 359.\n"
               "Float number between -360 and 359.\n"
@@ -104,7 +104,7 @@ class ToolTransform(FlatCAMTool):
         form1_child_1 = QtWidgets.QHBoxLayout()
         form1_child_1 = QtWidgets.QHBoxLayout()
         form1_child_2 = 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(
         self.skewx_label.setToolTip(
             _("Angle for Skew action, in degrees.\n"
             _("Angle for Skew action, in degrees.\n"
               "Float number between -360 and 359.")
               "Float number between -360 and 359.")
@@ -122,7 +122,7 @@ class ToolTransform(FlatCAMTool):
               "the bounding box for all selected objects."))
               "the bounding box for all selected objects."))
         self.skewx_button.setMinimumWidth(90)
         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(
         self.skewy_label.setToolTip(
             _("Angle for Skew action, in degrees.\n"
             _("Angle for Skew action, in degrees.\n"
               "Float number between -360 and 359.")
               "Float number between -360 and 359.")
@@ -161,9 +161,9 @@ class ToolTransform(FlatCAMTool):
         form2_child_1 = QtWidgets.QHBoxLayout()
         form2_child_1 = QtWidgets.QHBoxLayout()
         form2_child_2 = 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(
         self.scalex_label.setToolTip(
-            _("Factor for Scale action over X axis.")
+            _("Factor for scaling on X axis.")
         )
         )
         self.scalex_label.setMinimumWidth(70)
         self.scalex_label.setMinimumWidth(70)
         self.scalex_entry = FCEntry()
         self.scalex_entry = FCEntry()
@@ -178,9 +178,9 @@ class ToolTransform(FlatCAMTool):
               "the Scale reference checkbox state."))
               "the Scale reference checkbox state."))
         self.scalex_button.setMinimumWidth(90)
         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(
         self.scaley_label.setToolTip(
-            _("Factor for Scale action over Y axis.")
+            _("Factor for scaling on Y axis.")
         )
         )
         self.scaley_label.setMinimumWidth(70)
         self.scaley_label.setMinimumWidth(70)
         self.scaley_entry = FCEntry()
         self.scaley_entry = FCEntry()
@@ -200,12 +200,13 @@ class ToolTransform(FlatCAMTool):
         self.scale_link_cb.setText(_("Link"))
         self.scale_link_cb.setText(_("Link"))
         self.scale_link_cb.setToolTip(
         self.scale_link_cb.setToolTip(
             _("Scale the selected object(s)\n"
             _("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_link_cb.setMinimumWidth(70)
 
 
         self.scale_zero_ref_cb = FCCheckBox()
         self.scale_zero_ref_cb = FCCheckBox()
         self.scale_zero_ref_cb.set_value(True)
         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(
         self.scale_zero_ref_cb.setToolTip(
             _("Scale the selected object(s)\n"
             _("Scale the selected object(s)\n"
               "using the origin reference when checked,\n"
               "using the origin reference when checked,\n"
@@ -235,9 +236,9 @@ class ToolTransform(FlatCAMTool):
         form3_child_1 = QtWidgets.QHBoxLayout()
         form3_child_1 = QtWidgets.QHBoxLayout()
         form3_child_2 = 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(
         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_label.setMinimumWidth(70)
         self.offx_entry = FCEntry()
         self.offx_entry = FCEntry()
@@ -252,9 +253,9 @@ class ToolTransform(FlatCAMTool):
               "the bounding box for all selected objects.\n"))
               "the bounding box for all selected objects.\n"))
         self.offx_button.setMinimumWidth(90)
         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(
         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_label.setMinimumWidth(70)
         self.offy_entry = FCEntry()
         self.offy_entry = FCEntry()
@@ -309,7 +310,7 @@ class ToolTransform(FlatCAMTool):
 
 
         self.flip_ref_cb = FCCheckBox()
         self.flip_ref_cb = FCCheckBox()
         self.flip_ref_cb.set_value(True)
         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(
         self.flip_ref_cb.setToolTip(
             _("Flip the selected object(s)\n"
             _("Flip the selected object(s)\n"
               "around the point in Point Entry Field.\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)"))
               "Point Entry field and click Flip on X(Y)"))
         self.flip_ref_cb.setMinimumWidth(70)
         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(
         self.flip_ref_label.setToolTip(
             _("Coordinates in format (x, y) used as reference for mirroring.\n"
             _("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"
               "The 'x' in (x, y) will be used when using Flip on X and\n"
@@ -384,7 +385,12 @@ class ToolTransform(FlatCAMTool):
             else:
             else:
                 try:
                 try:
                     if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
                     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:
                 except AttributeError:
                     pass
                     pass
         else:
         else:

BIN
locale/de/LC_MESSAGES/strings.mo


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 222 - 195
locale/de/LC_MESSAGES/strings.po


BIN
locale/en/LC_MESSAGES/strings.mo


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 228 - 201
locale/en/LC_MESSAGES/strings.po


BIN
locale/es/LC_MESSAGES/strings.mo


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 222 - 195
locale/es/LC_MESSAGES/strings.po


BIN
locale/pt_BR/LC_MESSAGES/strings.mo


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 228 - 201
locale/pt_BR/LC_MESSAGES/strings.po


BIN
locale/ro/LC_MESSAGES/strings.mo


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 228 - 201
locale/ro/LC_MESSAGES/strings.po


BIN
locale/ru/LC_MESSAGES/strings.mo


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 227 - 200
locale/ru/LC_MESSAGES/strings.po


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 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(("locale", "lib/locale"))
 include_files.append(("postprocessors", "lib/postprocessors"))
 include_files.append(("postprocessors", "lib/postprocessors"))
 include_files.append(("share", "lib/share"))
 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(("README.md", "README.md"))
 include_files.append(("LICENSE", "LICENSE"))
 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)
         arguments, options = self.parse_arguments(args)
-
         named_args = {}
         named_args = {}
         unnamed_args = []
         unnamed_args = []
 
 
@@ -274,7 +273,7 @@ class TclCommand(object):
         :return: None, output text or exception
         :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:
         try:
             self.log.debug("TCL command '%s' executed." % str(self.__class__))
             self.log.debug("TCL command '%s' executed." % str(self.__class__))
@@ -283,7 +282,7 @@ class TclCommand(object):
             return self.execute(args, unnamed_args)
             return self.execute(args, unnamed_args)
         except Exception as unknown:
         except Exception as unknown:
             error_info = sys.exc_info()
             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.app.display_tcl_error(unknown, error_info)
             self.raise_tcl_unknown_error(unknown)
             self.raise_tcl_unknown_error(unknown)
 
 

+ 2 - 1
tclCommands/TclCommandAddPolygon.py

@@ -55,7 +55,8 @@ class TclCommandAddPolygon(TclCommandSignaled):
         if len(unnamed_args) % 2 != 0:
         if len(unnamed_args) % 2 != 0:
             self.raise_tcl_error("Incomplete coordinates.")
             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.add_polygon(points)
         obj.plot()
         obj.plot()

+ 2 - 1
tclCommands/TclCommandAddPolyline.py

@@ -55,7 +55,8 @@ class TclCommandAddPolyline(TclCommandSignaled):
         if len(unnamed_args) % 2 != 0:
         if len(unnamed_args) % 2 != 0:
             self.raise_tcl_error("Incomplete coordinates.")
             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.add_polyline(points)
         obj.plot()
         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):
 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:
     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
     # dictionary of types from Tcl command, needs to be ordered , this  is  for options  like -optionname value
     option_types = collections.OrderedDict([
     option_types = collections.OrderedDict([
+        ('tooldia', float),
         ('z_cut', float),
         ('z_cut', float),
         ('z_move', float),
         ('z_move', float),
         ('feedrate', float),
         ('feedrate', float),
         ('feedrate_rapid', float),
         ('feedrate_rapid', float),
-        ('tooldia', float),
         ('spindlespeed', int),
         ('spindlespeed', int),
         ('multidepth', bool),
         ('multidepth', bool),
         ('extracut', bool),
         ('extracut', bool),
         ('depthperpass', float),
         ('depthperpass', float),
+        ('toolchange', int),
+        ('toolchangez', float),
+        ('toolchangexy', tuple),
         ('endz', float),
         ('endz', float),
         ('ppname_g', str),
         ('ppname_g', str),
         ('outname', str)
         ('outname', str)
@@ -62,7 +65,7 @@ class TclCommandCncjob(TclCommandSignaled):
             ('outname', 'Name of the resulting Geometry object.'),
             ('outname', 'Name of the resulting Geometry object.'),
             ('ppname_g', 'Name of the Geometry postprocessor. No quotes, case sensitive')
             ('ppname_g', 'Name of the Geometry postprocessor. No quotes, case sensitive')
         ]),
         ]),
-        '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):
     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']
         del args['name']
         try:
         try:
-            obj.follow(**args)
+            obj.follow_geo(**args)
         except Exception as e:
         except Exception as e:
             return "Operation failed: %s" % str(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 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
     Paint the interior of polygons
     """
     """
@@ -13,32 +21,54 @@ class TclCommandPaint(TclCommandSignaled):
     # dictionary of types from Tcl command, needs to be ordered
     # dictionary of types from Tcl command, needs to be ordered
     arg_names = collections.OrderedDict([
     arg_names = collections.OrderedDict([
         ('name', str),
         ('name', str),
-        ('tooldia', float),
-        ('overlap', float)
     ])
     ])
 
 
     # dictionary of types from Tcl command, needs to be ordered , this  is  for options  like -optionname value
     # dictionary of types from Tcl command, needs to be ordered , this  is  for options  like -optionname value
     option_types = collections.OrderedDict([
     option_types = collections.OrderedDict([
-        ('outname', str),
+        ('tooldia', str),
+        ('overlap', float),
+        ('order', str),
+        ('margin', float),
+        ('method', str),
+        ('connect', bool),
+        ('contour', bool),
+
         ('all', bool),
         ('all', bool),
+        ('single', bool),
+        ('ref', bool),
+        ('box', str),
         ('x', float),
         ('x', float),
-        ('y', float)
+        ('y', float),
+        ('outname', str),
     ])
     ])
 
 
     # array of mandatory options for current Tcl command: required = {'name','outname'}
     # 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
     # structured help for current command, args needs to be ordered
     help = {
     help = {
         'main': "Paint polygons",
         'main': "Paint polygons",
         'args': collections.OrderedDict([
         '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': []
         'examples': []
     }
     }
@@ -54,31 +84,177 @@ class TclCommandPaint(TclCommandSignaled):
         """
         """
 
 
         name = args['name']
         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:
         if 'outname' in args:
             outname = args['outname']
             outname = args['outname']
         else:
         else:
             outname = name + "_paint"
             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
             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
     # Dictionary of types from Tcl command, needs to be ordered , this  is  for options  like -optionname value
     option_types = collections.OrderedDict([
     option_types = collections.OrderedDict([
-
+        ('x', float),
+        ('y', float),
+        ('origin', str)
     ])
     ])
 
 
     # array of mandatory options for current Tcl command: required = {'name','outname'}
     # 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
     # structured help for current command, args needs to be ordered
     help = {
     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([
         'args': collections.OrderedDict([
             ('name', 'Name of the object to resize.'),
             ('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):
     def execute(self, args, unnamed_args):
@@ -46,6 +56,49 @@ class TclCommandScale(TclCommand):
         """
         """
 
 
         name = args['name']
         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
     # structured help for current command, args needs to be ordered
     help = {
     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([
         'args': collections.OrderedDict([
             ('name', 'Name of the object to skew.'),
             ('name', 'Name of the object to skew.'),
             ('angle_x', 'Angle in degrees by which to skew on the X axis.'),
             ('angle_x', 'Angle in degrees by which to skew on the X axis.'),
@@ -48,7 +49,9 @@ class TclCommandSkew(TclCommand):
         """
         """
 
 
         name = args['name']
         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):
 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)
     # 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.
     # Dictionary of types from Tcl command, needs to be ordered.
     # For positional arguments
     # For positional arguments
     arg_names = collections.OrderedDict([
     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.
     # 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'}
     # 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
     # structured help for current command, args needs to be ordered
     help = {
     help = {
@@ -37,7 +33,7 @@ class TclCommandSubtractRectangle(TclCommandSignaled):
             ('x0 y0', 'Bottom left corner coordinates.'),
             ('x0 y0', 'Bottom left corner coordinates.'),
             ('x1 y1', 'Top right corner coordinates.')
             ('x1 y1', 'Top right corner coordinates.')
         ]),
         ]),
-        'examples': []
+        'examples': ['subtract_rectangle geo_obj 8 8 15 15']
     }
     }
 
 
     def execute(self, args, unnamed_args):
     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
             without -somename and  we do not have them in known arg_names
         :return: None or exception
         :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']
         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:
         try:
             obj = self.app.collection.get_by_name(str(obj_name))
             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([
         'args': collections.OrderedDict([
 
 
         ]),
         ]),
-        'examples': []
+        'examples': ['version']
     }
     }
 
 
     def execute(self, args, unnamed_args):
     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.'),
             ('preamble', 'Text to append at the beginning.'),
             ('postamble', 'Text to append at the end.')
             ('postamble', 'Text to append at the end.')
         ]),
         ]),
-        'examples': []
+        'examples': ["write_gcode name c:\\\\gcode_repo"]
     }
     }
 
 
     def execute(self, args, unnamed_args):
     def execute(self, args, unnamed_args):

+ 3 - 0
tclCommands/__init__.py

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

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است