Просмотр исходного кода

Merged in test_beta914 (pull request #143)

Test beta914
Marius Stanciu 6 лет назад
Родитель
Сommit
79b402d198
45 измененных файлов с 4090 добавлено и 1313 удалено
  1. 24 26
      FlatCAMApp.py
  2. 2 0
      FlatCAMObj.py
  3. 2 2
      FlatCAMProcess.py
  4. 85 0
      README.md
  5. 148 24
      camlib.py
  6. 88 38
      flatcamEditors/FlatCAMExcEditor.py
  7. 147 35
      flatcamEditors/FlatCAMGeoEditor.py
  8. 830 35
      flatcamEditors/FlatCAMGrbEditor.py
  9. 308 220
      flatcamGUI/FlatCAMGUI.py
  10. 3 2
      flatcamGUI/GUIElements.py
  11. 1 1
      flatcamGUI/ObjectUI.py
  12. 134 119
      flatcamTools/ToolMeasurement.py
  13. 2 1
      flatcamTools/ToolNonCopperClear.py
  14. 1001 0
      flatcamTools/ToolPDF.py
  15. 2 1
      flatcamTools/ToolPaint.py
  16. 466 0
      flatcamTools/ToolPcbWizard.py
  17. 3 2
      flatcamTools/ToolSolderPaste.py
  18. 2 0
      flatcamTools/__init__.py
  19. BIN
      locale/de/LC_MESSAGES/strings.mo
  20. 210 201
      locale/de/LC_MESSAGES/strings.po
  21. BIN
      locale/en/LC_MESSAGES/strings.mo
  22. 209 201
      locale/en/LC_MESSAGES/strings.po
  23. BIN
      locale/ro/LC_MESSAGES/strings.mo
  24. 214 205
      locale/ro/LC_MESSAGES/strings.po
  25. 209 200
      locale_template/strings.pot
  26. BIN
      share/aero.png
  27. BIN
      share/aero_arc.png
  28. BIN
      share/aero_array.png
  29. BIN
      share/aero_buffer.png
  30. BIN
      share/aero_circle.png
  31. BIN
      share/aero_circle_geo.png
  32. BIN
      share/aero_disc.png
  33. BIN
      share/aero_drill.png
  34. BIN
      share/aero_drill_array.png
  35. BIN
      share/aero_path1.png
  36. BIN
      share/aero_path2.png
  37. BIN
      share/aero_path3.png
  38. BIN
      share/aero_path4.png
  39. BIN
      share/aero_path5.png
  40. BIN
      share/aero_semidisc.png
  41. BIN
      share/aero_text.png
  42. BIN
      share/disc32.png
  43. BIN
      share/pdf32.png
  44. BIN
      share/poligonize32.png
  45. BIN
      share/semidisc32.png

+ 24 - 26
FlatCAMApp.py

@@ -94,8 +94,8 @@ class App(QtCore.QObject):
     log.addHandler(handler)
     log.addHandler(handler)
 
 
     # Version
     # Version
-    version = 8.913
-    version_date = "2019/04/13"
+    version = 8.914
+    version_date = "2019/04/23"
     beta = True
     beta = True
 
 
     # current date now
     # current date now
@@ -1838,6 +1838,7 @@ class App(QtCore.QObject):
                       'mpf']
                       'mpf']
         self.svg_list = ['svg']
         self.svg_list = ['svg']
         self.dxf_list = ['dxf']
         self.dxf_list = ['dxf']
+        self.pdf_list = ['pdf']
         self.prj_list = ['flatprj']
         self.prj_list = ['flatprj']
 
 
         # global variable used by NCC Tool to signal that some polygons could not be cleared, if True
         # global variable used by NCC Tool to signal that some polygons could not be cleared, if True
@@ -2003,9 +2004,15 @@ class App(QtCore.QObject):
         self.properties_tool = Properties(self)
         self.properties_tool = Properties(self)
         self.properties_tool.install(icon=QtGui.QIcon('share/properties32.png'), pos=self.ui.menuoptions)
         self.properties_tool.install(icon=QtGui.QIcon('share/properties32.png'), pos=self.ui.menuoptions)
 
 
+        self.pdf_tool = ToolPDF(self)
+        self.pdf_tool.install(icon=QtGui.QIcon('share/pdf32.png'), pos=self.ui.menufileimport,
+                              separator=True)
+
         self.image_tool = ToolImage(self)
         self.image_tool = ToolImage(self)
         self.image_tool.install(icon=QtGui.QIcon('share/image32.png'), pos=self.ui.menufileimport,
         self.image_tool.install(icon=QtGui.QIcon('share/image32.png'), pos=self.ui.menufileimport,
                                 separator=True)
                                 separator=True)
+        self.pcb_wizard_tool = PcbWizard(self)
+        self.pcb_wizard_tool.install(icon=QtGui.QIcon('share/drill32.png'), pos=self.ui.menufileimport)
 
 
         self.log.debug("Tools are installed.")
         self.log.debug("Tools are installed.")
 
 
@@ -2090,12 +2097,9 @@ class App(QtCore.QObject):
 
 
         if isinstance(edited_object, FlatCAMGerber) or isinstance(edited_object, FlatCAMGeometry) or \
         if isinstance(edited_object, FlatCAMGerber) or isinstance(edited_object, FlatCAMGeometry) or \
                 isinstance(edited_object, FlatCAMExcellon):
                 isinstance(edited_object, FlatCAMExcellon):
-
-            # adjust the status of the menu entries related to the editor
-            self.ui.menueditedit.setDisabled(True)
-            self.ui.menueditok.setDisabled(False)
+            pass
         else:
         else:
-            self.inform.emit(_("[WARNING_NOTCL] Select a Geometry or Excellon Object to edit."))
+            self.inform.emit(_("[WARNING_NOTCL] Select a Geometry, Gerber or Excellon Object to edit."))
             return
             return
 
 
         if isinstance(edited_object, FlatCAMGeometry):
         if isinstance(edited_object, FlatCAMGeometry):
@@ -2106,7 +2110,8 @@ class App(QtCore.QObject):
                 edited_tools = [int(x.text()) for x in edited_object.ui.geo_tools_table.selectedItems()]
                 edited_tools = [int(x.text()) for x in edited_object.ui.geo_tools_table.selectedItems()]
                 if len(edited_tools) > 1:
                 if len(edited_tools) > 1:
                     self.inform.emit(_("[WARNING_NOTCL] Simultanoeus editing of tools geometry in a MultiGeo Geometry "
                     self.inform.emit(_("[WARNING_NOTCL] Simultanoeus editing of tools geometry in a MultiGeo Geometry "
-                                       "is not possible.\n Edit only one geometry at a time."))
+                                       "is not possible.\n"
+                                       "Edit only one geometry at a time."))
                 self.geo_editor.edit_fcgeometry(edited_object, multigeo_tool=edited_tools[0])
                 self.geo_editor.edit_fcgeometry(edited_object, multigeo_tool=edited_tools[0])
             else:
             else:
                 self.geo_editor.edit_fcgeometry(edited_object)
                 self.geo_editor.edit_fcgeometry(edited_object)
@@ -2153,16 +2158,8 @@ class App(QtCore.QObject):
         """
         """
         self.report_usage("editor2object()")
         self.report_usage("editor2object()")
 
 
-        # adjust the status of the menu entries related to the editor
-        self.ui.menueditedit.setDisabled(False)
-        self.ui.menueditok.setDisabled(True)
-
         # do not update a geometry or excellon object unless it comes out of an editor
         # do not update a geometry or excellon object unless it comes out of an editor
         if self.call_source != 'app':
         if self.call_source != 'app':
-            # adjust the visibility of some of the canvas context menu
-            self.ui.popmenu_edit.setVisible(True)
-            self.ui.popmenu_save.setVisible(False)
-
             edited_obj = self.collection.get_active()
             edited_obj = self.collection.get_active()
             obj_type = ""
             obj_type = ""
 
 
@@ -2241,6 +2238,8 @@ class App(QtCore.QObject):
                         self.grb_editor.deactivate_grb_editor()
                         self.grb_editor.deactivate_grb_editor()
                     elif isinstance(edited_obj, FlatCAMExcellon):
                     elif isinstance(edited_obj, FlatCAMExcellon):
                         self.exc_editor.deactivate()
                         self.exc_editor.deactivate()
+                        # set focus on the project tab
+                        self.ui.notebook.setCurrentWidget(self.ui.project_tab)
                     else:
                     else:
                         self.inform.emit(_("[WARNING_NOTCL] Select a Gerber, Geometry or Excellon Object to update."))
                         self.inform.emit(_("[WARNING_NOTCL] Select a Gerber, Geometry or Excellon Object to update."))
                         return
                         return
@@ -4472,7 +4471,6 @@ class App(QtCore.QObject):
         self.report_usage("on_set_origin()")
         self.report_usage("on_set_origin()")
 
 
         self.inform.emit(_('Click to set the origin ...'))
         self.inform.emit(_('Click to set the origin ...'))
-
         self.plotcanvas.vis_connect('mouse_press', self.on_set_zero_click)
         self.plotcanvas.vis_connect('mouse_press', self.on_set_zero_click)
 
 
     def on_jump_to(self, custom_location=None, fit_center=True):
     def on_jump_to(self, custom_location=None, fit_center=True):
@@ -4510,7 +4508,7 @@ class App(QtCore.QObject):
         jump_loc = self.plotcanvas.vispy_canvas.translate_coords_2((location[0], location[1]))
         jump_loc = self.plotcanvas.vispy_canvas.translate_coords_2((location[0], location[1]))
 
 
         cursor.setPos(canvas_origin.x() + jump_loc[0], (canvas_origin.y() + jump_loc[1]))
         cursor.setPos(canvas_origin.x() + jump_loc[0], (canvas_origin.y() + jump_loc[1]))
-        self.inform.emit(_("Done."))
+        self.inform.emit(_("[success] Done."))
 
 
     def on_copy_object(self):
     def on_copy_object(self):
         self.report_usage("on_copy_object()")
         self.report_usage("on_copy_object()")
@@ -5117,9 +5115,7 @@ class App(QtCore.QObject):
 
 
     def on_mouse_move_over_plot(self, event, origin_click=None):
     def on_mouse_move_over_plot(self, event, origin_click=None):
         """
         """
-        Callback for the mouse motion event over the plot. This event is generated
-        by the Matplotlib backend and has been registered in ``self.__init__()``.
-        For details, see: http://matplotlib.org/users/event_handling.html
+        Callback for the mouse motion event over the plot.
 
 
         :param event: Contains information about the event.
         :param event: Contains information about the event.
         :param origin_click
         :param origin_click
@@ -5314,7 +5310,6 @@ class App(QtCore.QObject):
     def select_objects(self, key=None):
     def select_objects(self, key=None):
         # list where we store the overlapped objects under our mouse left click position
         # list where we store the overlapped objects under our mouse left click position
         objects_under_the_click_list = []
         objects_under_the_click_list = []
-
         # Populate the list with the overlapped objects on the click position
         # Populate the list with the overlapped objects on the click position
         curr_x, curr_y = self.pos
         curr_x, curr_y = self.pos
         for obj in self.all_objects_list:
         for obj in self.all_objects_list:
@@ -5662,7 +5657,7 @@ class App(QtCore.QObject):
 
 
     def obj_move(self):
     def obj_move(self):
         self.report_usage("obj_move()")
         self.report_usage("obj_move()")
-        self.move_tool.run()
+        self.move_tool.run(toggle=False)
 
 
     def on_fileopengerber(self):
     def on_fileopengerber(self):
         """
         """
@@ -7085,7 +7080,7 @@ class App(QtCore.QObject):
             # self.progress.emit(20)
             # self.progress.emit(20)
 
 
             try:
             try:
-                ret = excellon_obj.parse_file(filename)
+                ret = excellon_obj.parse_file(filename=filename)
                 if ret == "fail":
                 if ret == "fail":
                     log.debug("Excellon parsing failed.")
                     log.debug("Excellon parsing failed.")
                     self.inform.emit(_("[ERROR_NOTCL] This is not Excellon file."))
                     self.inform.emit(_("[ERROR_NOTCL] This is not Excellon file."))
@@ -7606,6 +7601,7 @@ class App(QtCore.QObject):
             "project": "share/project16.png",
             "project": "share/project16.png",
             "svg": "share/geometry16.png",
             "svg": "share/geometry16.png",
             "dxf": "share/dxf16.png",
             "dxf": "share/dxf16.png",
+            "pdf": "share/pdf32.png",
             "image": "share/image16.png"
             "image": "share/image16.png"
 
 
         }
         }
@@ -7613,11 +7609,13 @@ class App(QtCore.QObject):
         openers = {
         openers = {
             'gerber': lambda fname: self.worker_task.emit({'fcn': self.open_gerber, 'params': [fname]}),
             'gerber': lambda fname: self.worker_task.emit({'fcn': self.open_gerber, 'params': [fname]}),
             'excellon': lambda fname: self.worker_task.emit({'fcn': self.open_excellon, 'params': [fname]}),
             'excellon': lambda fname: self.worker_task.emit({'fcn': self.open_excellon, 'params': [fname]}),
+            'geometry': lambda fname: self.worker_task.emit({'fcn': self.import_dxf, 'params': [fname]}),
             'cncjob': lambda fname: self.worker_task.emit({'fcn': self.open_gcode, 'params': [fname]}),
             'cncjob': lambda fname: self.worker_task.emit({'fcn': self.open_gcode, 'params': [fname]}),
             'project': self.open_project,
             'project': self.open_project,
             'svg': self.import_svg,
             'svg': self.import_svg,
             'dxf': self.import_dxf,
             'dxf': self.import_dxf,
-            'image': self.import_image
+            'image': self.import_image,
+            'pdf': lambda fname: self.worker_task.emit({'fcn': self.pdf_tool.open_pdf, 'params': [fname]})
         }
         }
 
 
         # Open file
         # Open file
@@ -7781,7 +7779,7 @@ The normal flow when working in FlatCAM is the following:</span></p>
 
 
         self.log.debug("version_check()")
         self.log.debug("version_check()")
 
 
-        if self.ui.general_defaults_form.general_gui_group.send_stats_cb.get_value() is True:
+        if self.ui.general_defaults_form.general_app_group.send_stats_cb.get_value() is True:
             full_url = App.version_url + \
             full_url = App.version_url + \
                        "?s=" + str(self.defaults['global_serial']) + \
                        "?s=" + str(self.defaults['global_serial']) + \
                        "&v=" + str(self.version) + \
                        "&v=" + str(self.version) + \

+ 2 - 0
FlatCAMObj.py

@@ -3032,6 +3032,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         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))
         self.ui.pp_geometry_name_cb.activated.connect(self.on_pp_changed)
         self.ui.pp_geometry_name_cb.activated.connect(self.on_pp_changed)
+        self.ui.addtool_entry.returnPressed.connect(lambda: self.on_tool_add())
 
 
     def set_tool_offset_visibility(self, current_row):
     def set_tool_offset_visibility(self, current_row):
         if current_row is None:
         if current_row is None:
@@ -3107,6 +3108,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
 
 
         # I use lambda's because the connected functions have parameters that could be used in certain scenarios
         # I use lambda's because the connected functions have parameters that could be used in certain scenarios
         self.ui.addtool_btn.clicked.connect(lambda: self.on_tool_add())
         self.ui.addtool_btn.clicked.connect(lambda: self.on_tool_add())
+
         self.ui.copytool_btn.clicked.connect(lambda: self.on_tool_copy())
         self.ui.copytool_btn.clicked.connect(lambda: self.on_tool_copy())
         self.ui.deltool_btn.clicked.connect(lambda: self.on_tool_delete())
         self.ui.deltool_btn.clicked.connect(lambda: self.on_tool_delete())
 
 

+ 2 - 2
FlatCAMProcess.py

@@ -134,13 +134,13 @@ class FCVisibleProcessContainer(QtCore.QObject, FCProcessContainer):
         self.something_changed.connect(self.update_view)
         self.something_changed.connect(self.update_view)
 
 
     def on_done(self, proc):
     def on_done(self, proc):
-        self.app.log.debug("FCVisibleProcessContainer.on_done()")
+        # self.app.log.debug("FCVisibleProcessContainer.on_done()")
         super(FCVisibleProcessContainer, self).on_done(proc)
         super(FCVisibleProcessContainer, self).on_done(proc)
 
 
         self.something_changed.emit()
         self.something_changed.emit()
 
 
     def on_change(self, proc):
     def on_change(self, proc):
-        self.app.log.debug("FCVisibleProcessContainer.on_change()")
+        # self.app.log.debug("FCVisibleProcessContainer.on_change()")
         super(FCVisibleProcessContainer, self).on_change(proc)
         super(FCVisibleProcessContainer, self).on_change(proc)
 
 
         self.something_changed.emit()
         self.something_changed.emit()

+ 85 - 0
README.md

@@ -9,6 +9,91 @@ CAD program, and create G-Code for Isolation routing.
 
 
 =================================================
 =================================================
 
 
+23.04.2019
+
+- Gerber Editor: added two new tools: Add Disc and Add SemiDisc (porting of Circle and Arc from Geometry Editor)
+- Gerber Editor: made Add Pad repeat until user exits the Add Pad through either mouse right click, or ESC key or deselecting the Add Pad menu item
+- Gerber and Geometry Editors: fixed some issues with the Add Arc/Add Semidisc; in mode 132, the norm() function was not the one from numpy but from a FlatCAM Class. Also fixed some of the texts and made sure that when changing the mode, the current points are reset to prepare for the newly selected mode.
+- Fixed Measurement Tool to show the mouse coordinates on the status bar (it was broken at some point)
+- updated the translation files
+- added more custom mouse cursors in Geometry and Gerber Editors
+- RELEASE 8.914
+
+22.04.2019
+
+- added PDF file as type in the Recent File list and capability to load it from there
+- PDF's can be drag & dropped on the GUI to be loaded
+- PDF import tool: added support for save/restore Graphics stack. Only for scale and offset transformations and for the linewidth. This is the final fix for Microsoft PDF printer who saves in PDF format 1.7
+- PDF Import tool: added support for PDF files that embed multiple Gerber layers (top, bottom, outline, silkscreen etc). Each will be opened in it's own Gerber file. The requirement is that each one is drawn in a different color
+- PDF Import tool: fixed bugs when drag & dropping PDF files on canvas the files geometry previously opened was added to the new one. Also scaling issues. Solved.
+- PDF Import tool: added support for detection of circular geometry drawn with white color which means actually invisible color. When detected, FlatCAM will build an Excellon file out of those geoms.
+- PDF Import tool: fixed storing geometries in apertures with the right size (before they were all stored in aperture D10)
+
+21.04.2019
+
+- fixed the PDF import tool to work with files generated by the Microsoft PDF printer (chained subpaths)
+- in PDF import tool added support for paths filled and at the same time stroked ('B' and 'B*'commands)
+- added a shortcut key for PDF Import Tool (ALT+Q) and updated the Shortcut list (also with the 'T' and 'R' keys for Gerber Editor where they control the bend in Track and Region tool and the 'M' and 'D' keys for Add Arc tool in Geometry Editor)
+
+20.04.2019
+
+- finished adding the PDF import tool although it does not support all kinds of outputs from PDF printers. Microsoft PDF printer is not supported.
+
+19.04.2019
+
+- started to work on PDF import tool
+
+
+18.04.2019
+
+- Gerber Editor: added custom mouse cursors for each mode in Add Track Tool
+- Gerber Editor: Poligonize Tool will first fuse polygons that touch each other and at a second try will create a polygon. The polygon will be automatically moved to Aperture '0' (regions).
+- Gerber Editor: Region Tool will add regions only in '0' aperture
+- Gerber Editor: the bending mode will now survive until the tool is exited
+- Gerber Editor: solved some bugs related with deleting an aperture and updating the last_selected_aperture
+
+17.04.2019
+
+- Gerber Editor: added some messages to warn user if no selection exists when trying to do aperture deletion or aperture geometry deletion
+- fixed version check
+- added custom mouse cursors for some tools in Gerber Editor
+- Gerber Editor: added multiple modes to lay a Region: 45-degrees, reverse 45-degrees, 90-degrees, reverse 90-degrees and free-angle. Added also key shortcuts 'T' and 'R' to cycle forward, respectively in reverse through the modes.
+- Excellon Editor: fixed issue not remembering last tool after adding a new tool
+- added custom mouse cursors for Excellon and Geometry Editors in some of their tools
+
+16.04.2019
+
+- added ability to use ENTER key to finish tool adding in Editors, NCC Tool, Paint Tool and SolderPaste Tool.
+- Gerber Editor: started to add modes of laying a track
+- Gerber Editor: Add Track Tool: added 5 modes for laying a track: 45-degrees, reverse-45 degrees, 90-degrees, reverse 90-degrees and free angle. Key 'T' will cycle forward through the modes and key 'R' will cycle in reverse through the track laying modes.
+- Gerber Editor: Add Track Tool: first right click will finish the track. Second right click will exit the Track Tool and return to Select Tool.
+- Gerber Editor: added protections for the Pad Array and Pad Tool for the case when the aperture size is zero (the aperture where to store the regions)
+
+15.04.2019
+
+- working on a new tool to process automatically PcbWizard Excellon files which are generated in 2 files
+- finished ToolPcbWizard; it will autodetect the Excellon format, units from the INF file
+- Gerber Editor: reduced the delay to show UI when editing an empty Gerber object
+- update the order of event handlers connection in Editors to first connect new handlers then disconnect old handlers. It seems that if nothing is connected some VispY functions like canvas panning no longer works if there is at least once nothing connected to the 'mouse_move' event
+- Excellon Editor: update so always there is a tool selected even after the Excellon object was just edited; before it always required a click inside of the tool table, not you do it only if needed.
+- fixed the menu File -> Edit -> Edit/Close Editor entry to reflect the status of the app (Editor active or not)
+- added support in Excellon parser for autodetection of Excellon file format for the Excellon files generated by the following ECAD sw: DipTrace, Eagle, Altium, Sprint Layout
+- Gerber Editor: finished a new tool: Poligonize Tool (ALT+N in Editor). It will fuse a selection of tracks into a polygon. It will fill a selection of polygons if they are apart and it will make a single polygon if the selection is overlapped. All the newly created filled polygons will be stored in aperture '0' (if it does not exist it will be automatically created)
+- fixed a bug in Move command in context menu who crashed the app when triggered
+- Gerber Editor: when adding a new aperture it will be store as the last selected and it will be used for any tools that are triggered until a new aperture is selected.
+
+14.04.2019
+
+- Gerber Editor: Remade the processing of 'clear_geometry' (geometry generated by polygons made with Gerber LPC command) to work if more than one such polygon exists
+- Gerber Editor: a disabled/enabled sequence for the VisPy cursor on Gerber edit make the graphics better
+- Editors: activated an old function that was no longer active: each tool can have it's own set of shortcut keys, the Editor general shortcut keys that are letters are overridden
+- Gerber and Geometry editors, when using the Backspace keys for certain tools, they will backtrack one point but now the utility geometry is immediately updated
+- In Geometry Editor I fixed bug in Arc modes. Arc mode shortcut key is now key 'M' and arc direction change shortcut key is 'D'
+- moved the key handler out of the Measurement tool to flatcamGUI.FlatCAMGui.keyPressEvent()
+- Gerber Editor: started to add new function of poligonize which should make a filled polygon out of a shape
+- cleaned up Measuring Tool
+- solved bug in Gerber apertures size and dimensions values conversion when file units are different than app units
+
 13.04.2019
 13.04.2019
 
 
 - updating the German translation
 - updating the German translation

+ 148 - 24
camlib.py

@@ -26,10 +26,11 @@ from rtree import index as rtindex
 from lxml import etree as ET
 from lxml import etree as ET
 
 
 # See: http://toblerity.org/shapely/manual.html
 # See: http://toblerity.org/shapely/manual.html
+
 from shapely.geometry import Polygon, LineString, Point, LinearRing, MultiLineString
 from shapely.geometry import Polygon, LineString, Point, LinearRing, MultiLineString
 from shapely.geometry import MultiPoint, MultiPolygon
 from shapely.geometry import MultiPoint, MultiPolygon
 from shapely.geometry import box as shply_box
 from shapely.geometry import box as shply_box
-from shapely.ops import cascaded_union, unary_union
+from shapely.ops import cascaded_union, unary_union, polygonize
 import shapely.affinity as affinity
 import shapely.affinity as affinity
 from shapely.wkt import loads as sloads
 from shapely.wkt import loads as sloads
 from shapely.wkt import dumps as sdumps
 from shapely.wkt import dumps as sdumps
@@ -45,6 +46,7 @@ import ezdxf
 
 
 # TODO: Commented for FlatCAM packaging with cx_freeze
 # TODO: Commented for FlatCAM packaging with cx_freeze
 # from scipy.spatial import KDTree, Delaunay
 # from scipy.spatial import KDTree, Delaunay
+# from scipy.spatial import Delaunay
 
 
 from flatcamParsers.ParseSVG import *
 from flatcamParsers.ParseSVG import *
 from flatcamParsers.ParseDXF import *
 from flatcamParsers.ParseDXF import *
@@ -2160,6 +2162,9 @@ class Gerber (Geometry):
         # Coordinates of the current path, each is [x, y]
         # Coordinates of the current path, each is [x, y]
         path = []
         path = []
 
 
+        # store the file units here:
+        gerber_units = 'IN'
+
         # this is for temporary storage of geometry until it is added to poly_buffer
         # this is for temporary storage of geometry until it is added to poly_buffer
         geo = None
         geo = None
 
 
@@ -3178,21 +3183,32 @@ class Gerber (Geometry):
                                 self.apertures[last_path_aperture]['solid_geometry'] = []
                                 self.apertures[last_path_aperture]['solid_geometry'] = []
                                 self.apertures[last_path_aperture]['solid_geometry'].append(geo)
                                 self.apertures[last_path_aperture]['solid_geometry'].append(geo)
 
 
+            # TODO: make sure to keep track of units changes because right now it seems to happen in a weird way
+            # find out the conversion factor used to convert inside the self.apertures keys: size, width, height
+            file_units = gerber_units if gerber_units else 'IN'
+            app_units = self.app.defaults['units']
+
+            conversion_factor = 25.4 if file_units == 'IN' else (1/25.4) if file_units != app_units else 1
+
             # first check if we have any clear_geometry (LPC) and if yes then we need to substract it
             # first check if we have any clear_geometry (LPC) and if yes then we need to substract it
             # from the apertures solid_geometry
             # from the apertures solid_geometry
             temp_geo = []
             temp_geo = []
             for apid in self.apertures:
             for apid in self.apertures:
                 if 'clear_geometry' in self.apertures[apid]:
                 if 'clear_geometry' in self.apertures[apid]:
-                    for clear_geo in self.apertures[apid]['clear_geometry']:
-                        for solid_geo in self.apertures[apid]['solid_geometry']:
-                            if solid_geo.intersects(clear_geo):
-                                res_geo = clear_geo.symmetric_difference(solid_geo)
-                                temp_geo.append(res_geo)
-                            else:
-                                temp_geo.append(solid_geo)
+                    clear_geo = MultiPolygon(self.apertures[apid]['clear_geometry'])
+                    for solid_geo in self.apertures[apid]['solid_geometry']:
+                        if clear_geo.intersects(solid_geo):
+                            res_geo = solid_geo.difference(clear_geo)
+                            temp_geo.append(res_geo)
+                        else:
+                            temp_geo.append(solid_geo)
                     self.apertures[apid]['solid_geometry'] = deepcopy(temp_geo)
                     self.apertures[apid]['solid_geometry'] = deepcopy(temp_geo)
                     self.apertures[apid].pop('clear_geometry', None)
                     self.apertures[apid].pop('clear_geometry', None)
 
 
+                for k, v in self.apertures[apid].items():
+                    if k == 'size' or k == 'width' or k == 'height':
+                        self.apertures[apid][k] = v * conversion_factor
+
             # --- Apply buffer ---
             # --- Apply buffer ---
             # this treats the case when we are storing geometry as paths
             # this treats the case when we are storing geometry as paths
             self.follow_geometry = follow_buffer
             self.follow_geometry = follow_buffer
@@ -3722,6 +3738,8 @@ class Excellon(Geometry):
         self.excellon_format_upper_mm = excellon_format_upper_mm or self.defaults["excellon_format_upper_mm"]
         self.excellon_format_upper_mm = excellon_format_upper_mm or self.defaults["excellon_format_upper_mm"]
         self.excellon_format_lower_mm = excellon_format_lower_mm or self.defaults["excellon_format_lower_mm"]
         self.excellon_format_lower_mm = excellon_format_lower_mm or self.defaults["excellon_format_lower_mm"]
         self.excellon_units = excellon_units or self.defaults["excellon_units"]
         self.excellon_units = excellon_units or self.defaults["excellon_units"]
+        # detected Excellon format is stored here:
+        self.excellon_format = None
 
 
         # Attributes to be included in serialization
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # Always append to it because it carries contents
@@ -3750,10 +3768,10 @@ class Excellon(Geometry):
         # Ignored in the parser
         # Ignored in the parser
         #self.fmat_re = re.compile(r'^FMAT,([12])$')
         #self.fmat_re = re.compile(r'^FMAT,([12])$')
 
 
-        # Number format and units
+        # Uunits and possible Excellon zeros and possible Excellon format
         # INCH uses 6 digits
         # INCH uses 6 digits
         # METRIC uses 5/6
         # METRIC uses 5/6
-        self.units_re = re.compile(r'^(INCH|METRIC)(?:,([TL])Z)?.*$')
+        self.units_re = re.compile(r'^(INCH|METRIC)(?:,([TL])Z)?,?(\d*\.\d+)?.*$')
 
 
         # Tool definition/parameters (?= is look-ahead
         # Tool definition/parameters (?= is look-ahead
         # NOTE: This might be an overkill!
         # NOTE: This might be an overkill!
@@ -3815,13 +3833,17 @@ class Excellon(Geometry):
         # Allegro Excellon format support
         # Allegro Excellon format support
         self.tool_units_re = re.compile(r'(\;\s*Holesize \d+.\s*\=\s*(\d+.\d+).*(MILS|MM))')
         self.tool_units_re = re.compile(r'(\;\s*Holesize \d+.\s*\=\s*(\d+.\d+).*(MILS|MM))')
 
 
+        # Altium Excellon format support
+        # it's a comment like this: ";FILE_FORMAT=2:5"
+        self.altium_format = re.compile(r'^;\s*(?:FILE_FORMAT)?(?:Format)?[=|:]\s*(\d+)[:|.](\d+).*$')
+
         # Parse coordinates
         # Parse coordinates
         self.leadingzeros_re = re.compile(r'^[-\+]?(0*)(\d*)')
         self.leadingzeros_re = re.compile(r'^[-\+]?(0*)(\d*)')
 
 
         # Repeating command
         # Repeating command
         self.repeat_re = re.compile(r'R(\d+)')
         self.repeat_re = re.compile(r'R(\d+)')
 
 
-    def parse_file(self, filename):
+    def parse_file(self, filename=None, file_obj=None):
         """
         """
         Reads the specified file as array of lines as
         Reads the specified file as array of lines as
         passes it to ``parse_lines()``.
         passes it to ``parse_lines()``.
@@ -3830,9 +3852,15 @@ class Excellon(Geometry):
         :type filename: str
         :type filename: str
         :return: None
         :return: None
         """
         """
-        efile = open(filename, 'r')
-        estr = efile.readlines()
-        efile.close()
+        if file_obj:
+            estr = file_obj
+        else:
+            if filename is None:
+                return "fail"
+            efile = open(filename, 'r')
+            estr = efile.readlines()
+            efile.close()
+
         try:
         try:
             self.parse_lines(estr)
             self.parse_lines(estr)
         except:
         except:
@@ -3900,9 +3928,10 @@ class Excellon(Geometry):
                     log.warning("Found ALLEGRO start of the header: %s" % eline)
                     log.warning("Found ALLEGRO start of the header: %s" % eline)
                     continue
                     continue
 
 
-                # Header End #
-                # Since there might be comments in the header that include char % or M95
-                # we ignore the lines starting with ';' which show they are comments
+                # Search for Header End #
+                # Since there might be comments in the header that include header end char (% or M95)
+                # we ignore the lines starting with ';' that contains such header end chars because it is not a
+                # real header end.
                 if self.comm_re.search(eline):
                 if self.comm_re.search(eline):
                     match = self.tool_units_re.search(eline)
                     match = self.tool_units_re.search(eline)
                     if match:
                     if match:
@@ -3910,7 +3939,7 @@ class Excellon(Geometry):
                             line_units_found = True
                             line_units_found = True
                             line_units = match.group(3)
                             line_units = match.group(3)
                             self.convert_units({"MILS": "IN", "MM": "MM"}[line_units])
                             self.convert_units({"MILS": "IN", "MM": "MM"}[line_units])
-                            log.warning("Type of Allegro UNITS found inline: %s" % line_units)
+                            log.warning("Type of Allegro UNITS found inline in comments: %s" % line_units)
 
 
                         if match.group(2):
                         if match.group(2):
                             name_tool += 1
                             name_tool += 1
@@ -3924,6 +3953,17 @@ class Excellon(Geometry):
                                 log.debug("  Tool definition: %s %s" % (name_tool, spec))
                                 log.debug("  Tool definition: %s %s" % (name_tool, spec))
                             spec['solid_geometry'] = []
                             spec['solid_geometry'] = []
                             continue
                             continue
+                    # search for Altium Excellon Format / Sprint Layout who is included as a comment
+                    match = self.altium_format.search(eline)
+                    if match:
+                        self.excellon_format_upper_mm = match.group(1)
+                        self.excellon_format_lower_mm = match.group(2)
+
+                        self.excellon_format_upper_in = match.group(1)
+                        self.excellon_format_lower_in = match.group(2)
+                        log.warning("Altium Excellon format preset found in comments: %s:%s" %
+                                    (match.group(1), match.group(2)))
+                        continue
                     else:
                     else:
                         log.warning("Line ignored, it's a comment: %s" % eline)
                         log.warning("Line ignored, it's a comment: %s" % eline)
                 else:
                 else:
@@ -3986,6 +4026,13 @@ class Excellon(Geometry):
                                     # the bellow construction is so each tool will have a slightly different diameter
                                     # the bellow construction is so each tool will have a slightly different diameter
                                     # starting with a default value, to allow Excellon editing after that
                                     # starting with a default value, to allow Excellon editing after that
                                     self.diameterless = True
                                     self.diameterless = True
+                                    self.app.inform.emit(_("[WARNING] No tool diameter info's. See shell.\n"
+                                                           "A tool change event: T%s was found but the Excellon file "
+                                                           "have no informations regarding the tool "
+                                                           "diameters therefore the application will try to load it by "
+                                                           "using some 'fake' diameters.\nThe user needs to edit the "
+                                                           "resulting Excellon object and change the diameters to "
+                                                           "reflect the real diameters.") % current_tool)
 
 
                                     if self.excellon_units == 'MM':
                                     if self.excellon_units == 'MM':
                                         diam = self.toolless_diam + (int(current_tool) - 1) / 100
                                         diam = self.toolless_diam + (int(current_tool) - 1) / 100
@@ -4351,8 +4398,16 @@ class Excellon(Geometry):
                     if match:
                     if match:
                         self.units_found = match.group(1)
                         self.units_found = match.group(1)
                         self.zeros = match.group(2)  # "T" or "L". Might be empty
                         self.zeros = match.group(2)  # "T" or "L". Might be empty
-
-                        # self.units = {"INCH": "IN", "METRIC": "MM"}[match.group(1)]
+                        self.excellon_format = match.group(3)
+                        if self.excellon_format:
+                            upper = len(self.excellon_format.partition('.')[0])
+                            lower = len(self.excellon_format.partition('.')[2])
+                            if self.units == 'MM':
+                                self.excellon_format_upper_mm = upper
+                                self.excellon_format_lower_mm = lower
+                            else:
+                                self.excellon_format_upper_in = upper
+                                self.excellon_format_lower_in = lower
 
 
                         # Modified for issue #80
                         # Modified for issue #80
                         self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.units_found])
                         self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.units_found])
@@ -4401,8 +4456,16 @@ class Excellon(Geometry):
                 if match:
                 if match:
                     self.units_found = match.group(1)
                     self.units_found = match.group(1)
                     self.zeros = match.group(2)  # "T" or "L". Might be empty
                     self.zeros = match.group(2)  # "T" or "L". Might be empty
-
-                    # self.units = {"INCH": "IN", "METRIC": "MM"}[match.group(1)]
+                    self.excellon_format = match.group(3)
+                    if self.excellon_format:
+                        upper = len(self.excellon_format.partition('.')[0])
+                        lower = len(self.excellon_format.partition('.')[2])
+                        if self.units == 'MM':
+                            self.excellon_format_upper_mm = upper
+                            self.excellon_format_lower_mm = lower
+                        else:
+                            self.excellon_format_upper_in = upper
+                            self.excellon_format_lower_in = lower
 
 
                     # Modified for issue #80
                     # Modified for issue #80
                     self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.units_found])
                     self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.units_found])
@@ -7346,6 +7409,63 @@ def parse_gerber_number(strnumber, int_digits, frac_digits, zeros):
     return ret_val
     return ret_val
 
 
 
 
+# def alpha_shape(points, alpha):
+#     """
+#     Compute the alpha shape (concave hull) of a set of points.
+#
+#     @param points: Iterable container of points.
+#     @param alpha: alpha value to influence the gooeyness of the border. Smaller
+#                   numbers don't fall inward as much as larger numbers. Too large,
+#                   and you lose everything!
+#     """
+#     if len(points) < 4:
+#         # When you have a triangle, there is no sense in computing an alpha
+#         # shape.
+#         return MultiPoint(list(points)).convex_hull
+#
+#     def add_edge(edges, edge_points, coords, i, j):
+#         """Add a line between the i-th and j-th points, if not in the list already"""
+#         if (i, j) in edges or (j, i) in edges:
+#             # already added
+#             return
+#         edges.add( (i, j) )
+#         edge_points.append(coords[ [i, j] ])
+#
+#     coords = np.array([point.coords[0] for point in points])
+#
+#     tri = Delaunay(coords)
+#     edges = set()
+#     edge_points = []
+#     # loop over triangles:
+#     # ia, ib, ic = indices of corner points of the triangle
+#     for ia, ib, ic in tri.vertices:
+#         pa = coords[ia]
+#         pb = coords[ib]
+#         pc = coords[ic]
+#
+#         # Lengths of sides of triangle
+#         a = math.sqrt((pa[0]-pb[0])**2 + (pa[1]-pb[1])**2)
+#         b = math.sqrt((pb[0]-pc[0])**2 + (pb[1]-pc[1])**2)
+#         c = math.sqrt((pc[0]-pa[0])**2 + (pc[1]-pa[1])**2)
+#
+#         # Semiperimeter of triangle
+#         s = (a + b + c)/2.0
+#
+#         # Area of triangle by Heron's formula
+#         area = math.sqrt(s*(s-a)*(s-b)*(s-c))
+#         circum_r = a*b*c/(4.0*area)
+#
+#         # Here's the radius filter.
+#         #print circum_r
+#         if circum_r < 1.0/alpha:
+#             add_edge(edges, edge_points, coords, ia, ib)
+#             add_edge(edges, edge_points, coords, ib, ic)
+#             add_edge(edges, edge_points, coords, ic, ia)
+#
+#     m = MultiLineString(edge_points)
+#     triangles = list(polygonize(m))
+#     return cascaded_union(triangles), edge_points
+
 # def voronoi(P):
 # def voronoi(P):
 #     """
 #     """
 #     Returns a list of all edges of the voronoi diagram for the given input points.
 #     Returns a list of all edges of the voronoi diagram for the given input points.
@@ -7581,13 +7701,17 @@ def three_point_circle(p1, p2, p3):
     b2 = dot((p3 - p2), array([[0, 1], [-1, 0]], dtype=float32))
     b2 = dot((p3 - p2), array([[0, 1], [-1, 0]], dtype=float32))
 
 
     # Params
     # Params
-    T = solve(transpose(array([-b1, b2])), a1 - a2)
+    try:
+        T = solve(transpose(array([-b1, b2])), a1 - a2)
+    except Exception as e:
+        log.debug("camlib.three_point_circle() --> %s" % str(e))
+        return
 
 
     # Center
     # Center
     center = a1 + b1 * T[0]
     center = a1 + b1 * T[0]
 
 
     # Radius
     # Radius
-    radius = norm(center - p1)
+    radius = np.linalg.norm(center - p1)
 
 
     return center, radius, T[0]
     return center, radius, T[0]
 
 

+ 88 - 38
flatcamEditors/FlatCAMExcEditor.py

@@ -12,6 +12,8 @@ from camlib import *
 from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, LengthEntry, RadioSet, SpinBoxDelegate
 from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, LengthEntry, RadioSet, SpinBoxDelegate
 from flatcamEditors.FlatCAMGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, FlatCAMGeoEditor
 from flatcamEditors.FlatCAMGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, FlatCAMGeoEditor
 
 
+from copy import copy, deepcopy
+
 import gettext
 import gettext
 import FlatCAMTranslation as fcTranslate
 import FlatCAMTranslation as fcTranslate
 
 
@@ -45,6 +47,13 @@ class FCDrillAdd(FCShapeTool):
             self.draw_app.select_tool("select")
             self.draw_app.select_tool("select")
             return
             return
 
 
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+        self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_drill.png'))
+        QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+
         geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y))
         geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y))
 
 
         if isinstance(geo, DrawToolShape) and geo.geo is not None:
         if isinstance(geo, DrawToolShape) and geo.geo is not None:
@@ -80,6 +89,11 @@ class FCDrillAdd(FCShapeTool):
 
 
     def make(self):
     def make(self):
 
 
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+
         # add the point to drills if the diameter is a key in the dict, if not, create it add the drill location
         # add the point to drills if the diameter is a key in the dict, if not, create it add the drill location
         # to the value, as a list of itself
         # to the value, as a list of itself
         if self.selected_dia in self.draw_app.points_edit:
         if self.selected_dia in self.draw_app.points_edit:
@@ -135,6 +149,13 @@ class FCDrillArray(FCShapeTool):
             self.draw_app.app.inform.emit(_("[WARNING_NOTCL] To add an Drill Array first select a tool in Tool Table"))
             self.draw_app.app.inform.emit(_("[WARNING_NOTCL] To add an Drill Array first select a tool in Tool Table"))
             return
             return
 
 
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+        self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_drill_array.png'))
+        QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+
         geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y), static=True)
         geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y), static=True)
 
 
         if isinstance(geo, DrawToolShape) and geo.geo is not None:
         if isinstance(geo, DrawToolShape) and geo.geo is not None:
@@ -250,6 +271,11 @@ class FCDrillArray(FCShapeTool):
         self.geometry = []
         self.geometry = []
         geo = None
         geo = None
 
 
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+
         # add the point to drills if the diameter is a key in the dict, if not, create it add the drill location
         # add the point to drills if the diameter is a key in the dict, if not, create it add the drill location
         # to the value, as a list of itself
         # to the value, as a list of itself
         if self.selected_dia not in self.draw_app.points_edit:
         if self.selected_dia not in self.draw_app.points_edit:
@@ -535,6 +561,11 @@ class FCDrillSelect(DrawTool):
         DrawTool.__init__(self, exc_editor_app)
         DrawTool.__init__(self, exc_editor_app)
         self.name = 'drill_select'
         self.name = 'drill_select'
 
 
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+
         self.exc_editor_app = exc_editor_app
         self.exc_editor_app = exc_editor_app
         self.storage = self.exc_editor_app.storage_dict
         self.storage = self.exc_editor_app.storage_dict
         # self.selected = self.exc_editor_app.selected
         # self.selected = self.exc_editor_app.selected
@@ -695,12 +726,23 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
         self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
 
 
         self.exc_edit_widget = QtWidgets.QWidget()
         self.exc_edit_widget = QtWidgets.QWidget()
+        ## Box for custom widgets
+        # This gets populated in offspring implementations.
         layout = QtWidgets.QVBoxLayout()
         layout = QtWidgets.QVBoxLayout()
         self.exc_edit_widget.setLayout(layout)
         self.exc_edit_widget.setLayout(layout)
 
 
+        # add a frame and inside add a vertical box layout. Inside this vbox layout I add all the Drills widgets
+        # this way I can hide/show the frame
+        self.drills_frame = QtWidgets.QFrame()
+        self.drills_frame.setContentsMargins(0, 0, 0, 0)
+        layout.addWidget(self.drills_frame)
+        self.tools_box = QtWidgets.QVBoxLayout()
+        self.tools_box.setContentsMargins(0, 0, 0, 0)
+        self.drills_frame.setLayout(self.tools_box)
+
         ## Page Title box (spacing between children)
         ## Page Title box (spacing between children)
         self.title_box = QtWidgets.QHBoxLayout()
         self.title_box = QtWidgets.QHBoxLayout()
-        layout.addLayout(self.title_box)
+        self.tools_box.addLayout(self.title_box)
 
 
         ## Page Title icon
         ## Page Title icon
         pixmap = QtGui.QPixmap('share/flatcam_icon32.png')
         pixmap = QtGui.QPixmap('share/flatcam_icon32.png')
@@ -715,26 +757,12 @@ class FlatCAMExcEditor(QtCore.QObject):
 
 
         ## Object name
         ## Object name
         self.name_box = QtWidgets.QHBoxLayout()
         self.name_box = QtWidgets.QHBoxLayout()
-        layout.addLayout(self.name_box)
+        self.tools_box.addLayout(self.name_box)
         name_label = QtWidgets.QLabel(_("Name:"))
         name_label = QtWidgets.QLabel(_("Name:"))
         self.name_box.addWidget(name_label)
         self.name_box.addWidget(name_label)
         self.name_entry = FCEntry()
         self.name_entry = FCEntry()
         self.name_box.addWidget(self.name_entry)
         self.name_box.addWidget(self.name_entry)
 
 
-        ## Box box for custom widgets
-        # This gets populated in offspring implementations.
-        self.custom_box = QtWidgets.QVBoxLayout()
-        layout.addLayout(self.custom_box)
-
-        # add a frame and inside add a vertical box layout. Inside this vbox layout I add all the Drills widgets
-        # this way I can hide/show the frame
-        self.drills_frame = QtWidgets.QFrame()
-        self.drills_frame.setContentsMargins(0, 0, 0, 0)
-        self.custom_box.addWidget(self.drills_frame)
-        self.tools_box = QtWidgets.QVBoxLayout()
-        self.tools_box.setContentsMargins(0, 0, 0, 0)
-        self.drills_frame.setLayout(self.tools_box)
-
         #### Tools Drills ####
         #### Tools Drills ####
         self.tools_table_label = QtWidgets.QLabel("<b>%s</b>" % _('Tools Table'))
         self.tools_table_label = QtWidgets.QLabel("<b>%s</b>" % _('Tools Table'))
         self.tools_table_label.setToolTip(
         self.tools_table_label.setToolTip(
@@ -1021,7 +1049,7 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.app.ui.delete_drill_btn.triggered.connect(self.on_delete_btn)
         self.app.ui.delete_drill_btn.triggered.connect(self.on_delete_btn)
         self.name_entry.returnPressed.connect(self.on_name_activate)
         self.name_entry.returnPressed.connect(self.on_name_activate)
         self.addtool_btn.clicked.connect(self.on_tool_add)
         self.addtool_btn.clicked.connect(self.on_tool_add)
-        # self.addtool_entry.editingFinished.connect(self.on_tool_add)
+        self.addtool_entry.returnPressed.connect(self.on_tool_add)
         self.deltool_btn.clicked.connect(self.on_tool_delete)
         self.deltool_btn.clicked.connect(self.on_tool_delete)
         # self.tools_table_exc.selectionModel().currentChanged.connect(self.on_row_selected)
         # self.tools_table_exc.selectionModel().currentChanged.connect(self.on_row_selected)
         self.tools_table_exc.cellPressed.connect(self.on_row_selected)
         self.tools_table_exc.cellPressed.connect(self.on_row_selected)
@@ -1134,6 +1162,7 @@ class FlatCAMExcEditor(QtCore.QObject):
         return storage
         return storage
 
 
     def set_ui(self):
     def set_ui(self):
+
         # updated units
         # updated units
         self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
         self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
 
 
@@ -1177,7 +1206,7 @@ class FlatCAMExcEditor(QtCore.QObject):
                     tool_dia = float('%.2f' % v['C'])
                     tool_dia = float('%.2f' % v['C'])
                 self.tool2tooldia[int(k)] = tool_dia
                 self.tool2tooldia[int(k)] = tool_dia
 
 
-    def build_ui(self):
+    def build_ui(self, first_run=None):
 
 
         try:
         try:
             # if connected, disconnect the signal from the slot on item_changed as it creates issues
             # if connected, disconnect the signal from the slot on item_changed as it creates issues
@@ -1271,6 +1300,11 @@ class FlatCAMExcEditor(QtCore.QObject):
             self.tools_table_exc.setItem(self.tool_row, 1, dia)  # Diameter
             self.tools_table_exc.setItem(self.tool_row, 1, dia)  # Diameter
             self.tools_table_exc.setItem(self.tool_row, 2, drill_count)  # Number of drills per tool
             self.tools_table_exc.setItem(self.tool_row, 2, drill_count)  # Number of drills per tool
             self.tools_table_exc.setItem(self.tool_row, 3, slot_count)  # Number of drills per tool
             self.tools_table_exc.setItem(self.tool_row, 3, slot_count)  # Number of drills per tool
+
+            if first_run is True:
+                # set now the last tool selected
+                self.last_tool_selected = int(tool_id)
+
             self.tool_row += 1
             self.tool_row += 1
 
 
         # make the diameter column editable
         # make the diameter column editable
@@ -1428,6 +1462,7 @@ class FlatCAMExcEditor(QtCore.QObject):
         for key in sorted(self.tool2tooldia):
         for key in sorted(self.tool2tooldia):
             if self.tool2tooldia[key] == tool_dia:
             if self.tool2tooldia[key] == tool_dia:
                 row_to_be_selected = int(key) - 1
                 row_to_be_selected = int(key) - 1
+                self.last_tool_selected = int(key)
                 break
                 break
 
 
         self.tools_table_exc.selectRow(row_to_be_selected)
         self.tools_table_exc.selectRow(row_to_be_selected)
@@ -1568,6 +1603,13 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.edited_obj_name = self.name_entry.get_value()
         self.edited_obj_name = self.name_entry.get_value()
 
 
     def activate(self):
     def activate(self):
+        # adjust the status of the menu entries related to the editor
+        self.app.ui.menueditedit.setDisabled(True)
+        self.app.ui.menueditok.setDisabled(False)
+        # adjust the visibility of some of the canvas context menu
+        self.app.ui.popmenu_edit.setVisible(False)
+        self.app.ui.popmenu_save.setVisible(True)
+
         self.connect_canvas_event_handlers()
         self.connect_canvas_event_handlers()
 
 
         # initialize working objects
         # initialize working objects
@@ -1604,14 +1646,20 @@ class FlatCAMExcEditor(QtCore.QObject):
         if self.app.ui.grid_snap_btn.isChecked() is False:
         if self.app.ui.grid_snap_btn.isChecked() is False:
             self.app.ui.grid_snap_btn.trigger()
             self.app.ui.grid_snap_btn.trigger()
 
 
-        # adjust the visibility of some of the canvas context menu
-        self.app.ui.popmenu_edit.setVisible(False)
-        self.app.ui.popmenu_save.setVisible(True)
-
         # Tell the App that the editor is active
         # Tell the App that the editor is active
         self.editor_active = True
         self.editor_active = True
 
 
+        # show the UI
+        self.drills_frame.show()
+
     def deactivate(self):
     def deactivate(self):
+        # adjust the status of the menu entries related to the editor
+        self.app.ui.menueditedit.setDisabled(False)
+        self.app.ui.menueditok.setDisabled(True)
+        # adjust the visibility of some of the canvas context menu
+        self.app.ui.popmenu_edit.setVisible(True)
+        self.app.ui.popmenu_save.setVisible(False)
+
         self.disconnect_canvas_event_handlers()
         self.disconnect_canvas_event_handlers()
         self.clear()
         self.clear()
         self.app.ui.exc_edit_toolbar.setDisabled(True)
         self.app.ui.exc_edit_toolbar.setDisabled(True)
@@ -1661,42 +1709,44 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.app.ui.g_editor_cmenu.setEnabled(False)
         self.app.ui.g_editor_cmenu.setEnabled(False)
         self.app.ui.e_editor_cmenu.setEnabled(False)
         self.app.ui.e_editor_cmenu.setEnabled(False)
 
 
-        # adjust the visibility of some of the canvas context menu
-        self.app.ui.popmenu_edit.setVisible(True)
-        self.app.ui.popmenu_save.setVisible(False)
-
         # Show original geometry
         # Show original geometry
         if self.exc_obj:
         if self.exc_obj:
             self.exc_obj.visible = True
             self.exc_obj.visible = True
 
 
+        # hide the UI
+        self.drills_frame.hide()
+
     def connect_canvas_event_handlers(self):
     def connect_canvas_event_handlers(self):
         ## Canvas events
         ## Canvas events
 
 
+        # first connect to new, then disconnect the old handlers
+        # don't ask why but if there is nothing connected I've seen issues
+        self.canvas.vis_connect('mouse_press', self.on_canvas_click)
+        self.canvas.vis_connect('mouse_move', self.on_canvas_move)
+        self.canvas.vis_connect('mouse_release', self.on_exc_click_release)
+
         # make sure that the shortcuts key and mouse events will no longer be linked to the methods from FlatCAMApp
         # make sure that the shortcuts key and mouse events will no longer be linked to the methods from FlatCAMApp
         # but those from FlatCAMGeoEditor
         # but those from FlatCAMGeoEditor
-
         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_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_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_double_click', self.app.on_double_click_over_plot)
         self.app.plotcanvas.vis_disconnect('mouse_double_click', self.app.on_double_click_over_plot)
         self.app.collection.view.clicked.disconnect()
         self.app.collection.view.clicked.disconnect()
 
 
-        self.canvas.vis_connect('mouse_press', self.on_canvas_click)
-        self.canvas.vis_connect('mouse_move', self.on_canvas_move)
-        self.canvas.vis_connect('mouse_release', self.on_canvas_click_release)
-
     def disconnect_canvas_event_handlers(self):
     def disconnect_canvas_event_handlers(self):
-        self.canvas.vis_disconnect('mouse_press', self.on_canvas_click)
-        self.canvas.vis_disconnect('mouse_move', self.on_canvas_move)
-        self.canvas.vis_disconnect('mouse_release', self.on_canvas_click_release)
-
         # we restore the key and mouse control to FlatCAMApp method
         # we restore the key and mouse control to FlatCAMApp method
+        # first connect to new, then disconnect the old handlers
+        # don't ask why but if there is nothing connected I've seen issues
         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_move', self.app.on_mouse_move_over_plot)
         self.app.plotcanvas.vis_connect('mouse_move', self.app.on_mouse_move_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)
         self.app.plotcanvas.vis_connect('mouse_double_click', self.app.on_double_click_over_plot)
         self.app.plotcanvas.vis_connect('mouse_double_click', self.app.on_double_click_over_plot)
         self.app.collection.view.clicked.connect(self.app.collection.on_mouse_down)
         self.app.collection.view.clicked.connect(self.app.collection.on_mouse_down)
 
 
+        self.canvas.vis_disconnect('mouse_press', self.on_canvas_click)
+        self.canvas.vis_disconnect('mouse_move', self.on_canvas_move)
+        self.canvas.vis_disconnect('mouse_release', self.on_exc_click_release)
+
     def clear(self):
     def clear(self):
         self.active_tool = None
         self.active_tool = None
         # self.shape_buffer = []
         # self.shape_buffer = []
@@ -1741,7 +1791,7 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.set_ui()
         self.set_ui()
 
 
         # now that we hava data, create the GUI interface and add it to the Tool Tab
         # now that we hava data, create the GUI interface and add it to the Tool Tab
-        self.build_ui()
+        self.build_ui(first_run=True)
 
 
         # we activate this after the initial build as we don't need to see the tool been populated
         # we activate this after the initial build as we don't need to see the tool been populated
         self.tools_table_exc.itemChanged.connect(self.on_tool_edit)
         self.tools_table_exc.itemChanged.connect(self.on_tool_edit)
@@ -1992,7 +2042,7 @@ class FlatCAMExcEditor(QtCore.QObject):
 
 
             try:
             try:
                 selected_dia = self.tool2tooldia[self.tools_table_exc.currentRow() + 1]
                 selected_dia = self.tool2tooldia[self.tools_table_exc.currentRow() + 1]
-                self.last_tool_selected = self.tools_table_exc.currentRow() + 1
+                self.last_tool_selected = copy(self.tools_table_exc.currentRow()) + 1
                 for obj in self.storage_dict[selected_dia].get_objects():
                 for obj in self.storage_dict[selected_dia].get_objects():
                     self.selected.append(obj)
                     self.selected.append(obj)
             except Exception as e:
             except Exception as e:
@@ -2136,7 +2186,7 @@ class FlatCAMExcEditor(QtCore.QObject):
         else:
         else:
             self.storage.insert(shape)  # TODO: Check performance
             self.storage.insert(shape)  # TODO: Check performance
 
 
-    def on_canvas_click_release(self, event):
+    def on_exc_click_release(self, event):
         pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
         pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
 
 
         self.modifiers = QtWidgets.QApplication.keyboardModifiers()
         self.modifiers = QtWidgets.QApplication.keyboardModifiers()

+ 147 - 35
flatcamEditors/FlatCAMGeoEditor.py

@@ -22,6 +22,7 @@ from shapely.ops import cascaded_union
 import shapely.affinity as affinity
 import shapely.affinity as affinity
 
 
 from numpy import arctan2, Inf, array, sqrt, sign, dot
 from numpy import arctan2, Inf, array, sqrt, sign, dot
+from numpy.linalg import norm as numpy_norm
 
 
 from rtree import index as rtindex
 from rtree import index as rtindex
 from flatcamGUI.GUIElements import OptionalInputSection, FCCheckBox, FCEntry, FCComboBox, FCTextAreaRich, \
 from flatcamGUI.GUIElements import OptionalInputSection, FCCheckBox, FCEntry, FCComboBox, FCTextAreaRich, \
@@ -1931,14 +1932,22 @@ class FCCircle(FCShapeTool):
         DrawTool.__init__(self, draw_app)
         DrawTool.__init__(self, draw_app)
         self.name = 'circle'
         self.name = 'circle'
 
 
-        self.start_msg = _("Click on CENTER ...")
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+        self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_circle_geo.png'))
+        QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+
+        self.start_msg = _("Click on Center point ...")
+        self.draw_app.app.inform.emit(_("Click on Center point ..."))
         self.steps_per_circ = self.draw_app.app.defaults["geometry_circle_steps"]
         self.steps_per_circ = self.draw_app.app.defaults["geometry_circle_steps"]
 
 
     def click(self, point):
     def click(self, point):
         self.points.append(point)
         self.points.append(point)
 
 
         if len(self.points) == 1:
         if len(self.points) == 1:
-            self.draw_app.app.inform.emit(_("Click on Circle perimeter point to complete ..."))
+            self.draw_app.app.inform.emit(_("Click on Perimeter point to complete ..."))
             return "Click on perimeter to complete ..."
             return "Click on perimeter to complete ..."
 
 
         if len(self.points) == 2:
         if len(self.points) == 2:
@@ -1957,6 +1966,11 @@ class FCCircle(FCShapeTool):
         return None
         return None
 
 
     def make(self):
     def make(self):
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+
         p1 = self.points[0]
         p1 = self.points[0]
         p2 = self.points[1]
         p2 = self.points[1]
         radius = distance(p1, p2)
         radius = distance(p1, p2)
@@ -1970,7 +1984,15 @@ class FCArc(FCShapeTool):
         DrawTool.__init__(self, draw_app)
         DrawTool.__init__(self, draw_app)
         self.name = 'arc'
         self.name = 'arc'
 
 
-        self.start_msg = _("Click on CENTER ...")
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+        self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_arc.png'))
+        QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+
+        self.start_msg = _("Click on Center point ...")
+        self.draw_app.app.inform.emit(_("Click on Center point ..."))
 
 
         # Direction of rotation between point 1 and 2.
         # Direction of rotation between point 1 and 2.
         # 'cw' or 'ccw'. Switch direction by hitting the
         # 'cw' or 'ccw'. Switch direction by hitting the
@@ -1989,11 +2011,21 @@ class FCArc(FCShapeTool):
         self.points.append(point)
         self.points.append(point)
 
 
         if len(self.points) == 1:
         if len(self.points) == 1:
-            self.draw_app.app.inform.emit(_("Click on Start arc point ..."))
+            if self.mode == 'c12':
+                self.draw_app.app.inform.emit(_("Click on Start point ..."))
+            elif self.mode == '132':
+                self.draw_app.app.inform.emit(_("Click on Point3 ..."))
+            else:
+                self.draw_app.app.inform.emit(_("Click on Stop point ..."))
             return "Click on 1st point ..."
             return "Click on 1st point ..."
 
 
         if len(self.points) == 2:
         if len(self.points) == 2:
-            self.draw_app.app.inform.emit(_("Click on End arc point to complete ..."))
+            if self.mode == 'c12':
+                self.draw_app.app.inform.emit(_("Click on Stop point to complete ..."))
+            elif self.mode == '132':
+                self.draw_app.app.inform.emit(_("Click on Point2 to complete ..."))
+            else:
+                self.draw_app.app.inform.emit(_("Click on Center point to complete ..."))
             return "Click on 2nd point to complete ..."
             return "Click on 2nd point to complete ..."
 
 
         if len(self.points) == 3:
         if len(self.points) == 3:
@@ -2003,18 +2035,25 @@ class FCArc(FCShapeTool):
         return ""
         return ""
 
 
     def on_key(self, key):
     def on_key(self, key):
-        if key == 'o':
+        if key == 'D' or key == QtCore.Qt.Key_D:
             self.direction = 'cw' if self.direction == 'ccw' else 'ccw'
             self.direction = 'cw' if self.direction == 'ccw' else 'ccw'
-            return 'Direction: ' + self.direction.upper()
+            return _('Direction: %s') % self.direction.upper()
+
+        if key == 'M' or key == QtCore.Qt.Key_M:
+            # delete the possible points made before this action; we want to start anew
+            self.points[:] = []
+            # and delete the utility geometry made up until this point
+            self.draw_app.delete_utility_geometry()
 
 
-        if key == 'p':
             if self.mode == 'c12':
             if self.mode == 'c12':
                 self.mode = '12c'
                 self.mode = '12c'
+                return _('Mode: Start -> Stop -> Center. Click on Start point ...')
             elif self.mode == '12c':
             elif self.mode == '12c':
                 self.mode = '132'
                 self.mode = '132'
+                return _('Mode: Point1 -> Point3 -> Point2. Click on Point1 ...')
             else:
             else:
                 self.mode = 'c12'
                 self.mode = 'c12'
-            return 'Mode: ' + self.mode
+                return _('Mode: Center -> Start -> Stop. Click on Center point ...')
 
 
     def utility_geometry(self, data=None):
     def utility_geometry(self, data=None):
         if len(self.points) == 1:  # Show the radius
         if len(self.points) == 1:  # Show the radius
@@ -2043,7 +2082,11 @@ class FCArc(FCShapeTool):
                 p3 = array(self.points[1])
                 p3 = array(self.points[1])
                 p2 = array(data)
                 p2 = array(data)
 
 
-                center, radius, t = three_point_circle(p1, p2, p3)
+                try:
+                    center, radius, t = three_point_circle(p1, p2, p3)
+                except TypeError:
+                    return
+
                 direction = 'cw' if sign(t) > 0 else 'ccw'
                 direction = 'cw' if sign(t) > 0 else 'ccw'
 
 
                 startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
                 startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
@@ -2065,7 +2108,7 @@ class FCArc(FCShapeTool):
 
 
                 # Perpendicular vector
                 # Perpendicular vector
                 b = dot(c, array([[0, -1], [1, 0]], dtype=float32))
                 b = dot(c, array([[0, -1], [1, 0]], dtype=float32))
-                b /= norm(b)
+                b /= numpy_norm(b)
 
 
                 # Distance
                 # Distance
                 t = distance(data, a)
                 t = distance(data, a)
@@ -2078,7 +2121,7 @@ class FCArc(FCShapeTool):
                 # Center = a + bt
                 # Center = a + bt
                 center = a + b * t
                 center = a + b * t
 
 
-                radius = norm(center - p1)
+                radius = numpy_norm(center - p1)
                 startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
                 startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
                 stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
                 stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
 
 
@@ -2128,7 +2171,7 @@ class FCArc(FCShapeTool):
 
 
             # Perpendicular vector
             # Perpendicular vector
             b = dot(c, array([[0, -1], [1, 0]], dtype=float32))
             b = dot(c, array([[0, -1], [1, 0]], dtype=float32))
-            b /= norm(b)
+            b /= numpy_norm(b)
 
 
             # Distance
             # Distance
             t = distance(pc, a)
             t = distance(pc, a)
@@ -2141,7 +2184,7 @@ class FCArc(FCShapeTool):
             # Center = a + bt
             # Center = a + bt
             center = a + b * t
             center = a + b * t
 
 
-            radius = norm(center - p1)
+            radius = numpy_norm(center - p1)
             startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
             startangle = arctan2(p1[1] - center[1], p1[0] - center[0])
             stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
             stopangle = arctan2(p2[1] - center[1], p2[0] - center[0])
 
 
@@ -2160,6 +2203,13 @@ class FCRectangle(FCShapeTool):
         DrawTool.__init__(self, draw_app)
         DrawTool.__init__(self, draw_app)
         self.name = 'rectangle'
         self.name = 'rectangle'
 
 
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+        self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero.png'))
+        QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+
         self.start_msg = _("Click on 1st corner ...")
         self.start_msg = _("Click on 1st corner ...")
 
 
     def click(self, point):
     def click(self, point):
@@ -2183,6 +2233,11 @@ class FCRectangle(FCShapeTool):
         return None
         return None
 
 
     def make(self):
     def make(self):
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+
         p1 = self.points[0]
         p1 = self.points[0]
         p2 = self.points[1]
         p2 = self.points[1]
         # self.geometry = LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])
         # self.geometry = LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])
@@ -2200,6 +2255,13 @@ class FCPolygon(FCShapeTool):
         DrawTool.__init__(self, draw_app)
         DrawTool.__init__(self, draw_app)
         self.name = 'polygon'
         self.name = 'polygon'
 
 
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+        self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero.png'))
+        QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+
         self.start_msg = _("Click on 1st point ...")
         self.start_msg = _("Click on 1st point ...")
 
 
     def click(self, point):
     def click(self, point):
@@ -2226,6 +2288,11 @@ class FCPolygon(FCShapeTool):
         return None
         return None
 
 
     def make(self):
     def make(self):
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+
         # self.geometry = LinearRing(self.points)
         # self.geometry = LinearRing(self.points)
         self.geometry = DrawToolShape(Polygon(self.points))
         self.geometry = DrawToolShape(Polygon(self.points))
         self.draw_app.in_action = False
         self.draw_app.in_action = False
@@ -2233,20 +2300,39 @@ class FCPolygon(FCShapeTool):
         self.draw_app.app.inform.emit(_("[success] Done. Polygon completed."))
         self.draw_app.app.inform.emit(_("[success] Done. Polygon completed."))
 
 
     def on_key(self, key):
     def on_key(self, key):
-        if key == 'backspace':
+        if key == 'Backspace' or key == QtCore.Qt.Key_Backspace:
             if len(self.points) > 0:
             if len(self.points) > 0:
                 self.points = self.points[0:-1]
                 self.points = self.points[0:-1]
+                # Remove any previous utility shape
+                self.draw_app.tool_shape.clear(update=False)
+                geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y))
+                self.draw_app.draw_utility_geometry(geo=geo)
+                return _("Backtracked one point ...")
 
 
 
 
 class FCPath(FCPolygon):
 class FCPath(FCPolygon):
     """
     """
     Resulting type: LineString
     Resulting type: LineString
     """
     """
+    def __init__(self, draw_app):
+        FCPolygon.__init__(self, draw_app)
+
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+        self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_path5.png'))
+        QtGui.QGuiApplication.setOverrideCursor(self.cursor)
 
 
     def make(self):
     def make(self):
         self.geometry = DrawToolShape(LineString(self.points))
         self.geometry = DrawToolShape(LineString(self.points))
         self.name = 'path'
         self.name = 'path'
 
 
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+
         self.draw_app.in_action = False
         self.draw_app.in_action = False
         self.complete = True
         self.complete = True
         self.draw_app.app.inform.emit(_("[success] Done. Path completed."))
         self.draw_app.app.inform.emit(_("[success] Done. Path completed."))
@@ -2260,9 +2346,14 @@ class FCPath(FCPolygon):
         return None
         return None
 
 
     def on_key(self, key):
     def on_key(self, key):
-        if key == 'backspace':
+        if key == 'Backspace' or key == QtCore.Qt.Key_Backspace:
             if len(self.points) > 0:
             if len(self.points) > 0:
                 self.points = self.points[0:-1]
                 self.points = self.points[0:-1]
+                # Remove any previous utility shape
+                self.draw_app.tool_shape.clear(update=False)
+                geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y))
+                self.draw_app.draw_utility_geometry(geo=geo)
+                return _("Backtracked one point ...")
 
 
 
 
 class FCSelect(DrawTool):
 class FCSelect(DrawTool):
@@ -2270,6 +2361,11 @@ class FCSelect(DrawTool):
         DrawTool.__init__(self, draw_app)
         DrawTool.__init__(self, draw_app)
         self.name = 'select'
         self.name = 'select'
 
 
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+
         self.storage = self.draw_app.storage
         self.storage = self.draw_app.storage
         # self.shape_buffer = self.draw_app.shape_buffer
         # self.shape_buffer = self.draw_app.shape_buffer
         # self.selected = self.draw_app.selected
         # self.selected = self.draw_app.selected
@@ -2442,6 +2538,14 @@ class FCText(FCShapeTool):
         FCShapeTool.__init__(self, draw_app)
         FCShapeTool.__init__(self, draw_app)
         self.name = 'text'
         self.name = 'text'
 
 
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except:
+            pass
+        self.cursor = QtGui.QCursor(QtGui.QPixmap('share/aero_text.png'))
+        QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+
+
         # self.shape_buffer = self.draw_app.shape_buffer
         # self.shape_buffer = self.draw_app.shape_buffer
         self.draw_app = draw_app
         self.draw_app = draw_app
         self.app = draw_app.app
         self.app = draw_app.app
@@ -2832,6 +2936,13 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.replot()
         self.replot()
 
 
     def activate(self):
     def activate(self):
+        # adjust the status of the menu entries related to the editor
+        self.app.ui.menueditedit.setDisabled(True)
+        self.app.ui.menueditok.setDisabled(False)
+        # adjust the visibility of some of the canvas context menu
+        self.app.ui.popmenu_edit.setVisible(False)
+        self.app.ui.popmenu_save.setVisible(True)
+
         self.connect_canvas_event_handlers()
         self.connect_canvas_event_handlers()
 
 
         # initialize working objects
         # initialize working objects
@@ -2864,14 +2975,17 @@ class FlatCAMGeoEditor(QtCore.QObject):
         for w in sel_tab_widget_list:
         for w in sel_tab_widget_list:
             w.setEnabled(False)
             w.setEnabled(False)
 
 
-        # adjust the visibility of some of the canvas context menu
-        self.app.ui.popmenu_edit.setVisible(False)
-        self.app.ui.popmenu_save.setVisible(True)
-
         # Tell the App that the editor is active
         # Tell the App that the editor is active
         self.editor_active = True
         self.editor_active = True
 
 
     def deactivate(self):
     def deactivate(self):
+        # adjust the status of the menu entries related to the editor
+        self.app.ui.menueditedit.setDisabled(False)
+        self.app.ui.menueditok.setDisabled(True)
+        # adjust the visibility of some of the canvas context menu
+        self.app.ui.popmenu_edit.setVisible(True)
+        self.app.ui.popmenu_save.setVisible(False)
+
         self.disconnect_canvas_event_handlers()
         self.disconnect_canvas_event_handlers()
         self.clear()
         self.clear()
         self.app.ui.geo_edit_toolbar.setDisabled(True)
         self.app.ui.geo_edit_toolbar.setDisabled(True)
@@ -2919,10 +3033,6 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # Tell the app that the editor is no longer active
         # Tell the app that the editor is no longer active
         self.editor_active = False
         self.editor_active = False
 
 
-        # adjust the visibility of some of the canvas context menu
-        self.app.ui.popmenu_edit.setVisible(True)
-        self.app.ui.popmenu_save.setVisible(False)
-
         try:
         try:
             # re-enable all the widgets in the Selected Tab that were disabled after entering in Edit Geometry Mode
             # re-enable all the widgets in the Selected Tab that were disabled after entering in Edit Geometry Mode
             sel_tab_widget_list = self.app.ui.selected_tab.findChildren(QtWidgets.QWidget)
             sel_tab_widget_list = self.app.ui.selected_tab.findChildren(QtWidgets.QWidget)
@@ -2938,9 +3048,14 @@ class FlatCAMGeoEditor(QtCore.QObject):
     def connect_canvas_event_handlers(self):
     def connect_canvas_event_handlers(self):
         ## Canvas events
         ## Canvas events
 
 
+        # first connect to new, then disconnect the old handlers
+        # don't ask why but if there is nothing connected I've seen issues
+        self.canvas.vis_connect('mouse_press', self.on_canvas_click)
+        self.canvas.vis_connect('mouse_move', self.on_canvas_move)
+        self.canvas.vis_connect('mouse_release', self.on_geo_click_release)
+
         # make sure that the shortcuts key and mouse events will no longer be linked to the methods from FlatCAMApp
         # make sure that the shortcuts key and mouse events will no longer be linked to the methods from FlatCAMApp
         # but those from FlatCAMGeoEditor
         # but those from FlatCAMGeoEditor
-
         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_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_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)
@@ -2948,23 +3063,20 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
 
         self.app.collection.view.clicked.disconnect()
         self.app.collection.view.clicked.disconnect()
 
 
-        self.canvas.vis_connect('mouse_press', self.on_canvas_click)
-        self.canvas.vis_connect('mouse_move', self.on_canvas_move)
-        self.canvas.vis_connect('mouse_release', self.on_canvas_click_release)
-
     def disconnect_canvas_event_handlers(self):
     def disconnect_canvas_event_handlers(self):
-
-        self.canvas.vis_disconnect('mouse_press', self.on_canvas_click)
-        self.canvas.vis_disconnect('mouse_move', self.on_canvas_move)
-        self.canvas.vis_disconnect('mouse_release', self.on_canvas_click_release)
-
         # we restore the key and mouse control to FlatCAMApp method
         # we restore the key and mouse control to FlatCAMApp method
+        # first connect to new, then disconnect the old handlers
+        # don't ask why but if there is nothing connected I've seen issues
         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_move', self.app.on_mouse_move_over_plot)
         self.app.plotcanvas.vis_connect('mouse_move', self.app.on_mouse_move_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)
         self.app.plotcanvas.vis_connect('mouse_double_click', self.app.on_double_click_over_plot)
         self.app.plotcanvas.vis_connect('mouse_double_click', self.app.on_double_click_over_plot)
         self.app.collection.view.clicked.connect(self.app.collection.on_mouse_down)
         self.app.collection.view.clicked.connect(self.app.collection.on_mouse_down)
 
 
+        self.canvas.vis_disconnect('mouse_press', self.on_canvas_click)
+        self.canvas.vis_disconnect('mouse_move', self.on_canvas_move)
+        self.canvas.vis_disconnect('mouse_release', self.on_geo_click_release)
+
     def add_shape(self, shape):
     def add_shape(self, shape):
         """
         """
         Adds a shape to the shape storage.
         Adds a shape to the shape storage.
@@ -3287,7 +3399,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # Update cursor
         # Update cursor
         self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color='black', size=20)
         self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color='black', size=20)
 
 
-    def on_canvas_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.vispy_canvas.translate_coords(event.pos)
 
 
         if self.app.grid_status():
         if self.app.grid_status():

Разница между файлами не показана из-за своего большого размера
+ 830 - 35
flatcamEditors/FlatCAMGrbEditor.py


+ 308 - 220
flatcamGUI/FlatCAMGUI.py

@@ -470,6 +470,12 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                                                                      _('Add Region\tN'))
                                                                      _('Add Region\tN'))
         self.grb_editor_menu.addSeparator()
         self.grb_editor_menu.addSeparator()
 
 
+        self.grb_convert_poly_menuitem  = self.grb_editor_menu.addAction(QtGui.QIcon('share/poligonize32.png'),
+                                                                    _("Poligonize\tALT+N"))
+        self.grb_add_semidisc_menuitem = self.grb_editor_menu.addAction(QtGui.QIcon('share/semidisc32.png'),
+                                                                        _("Add SemiDisc\tE"))
+        self.grb_add_disc_menuitem = self.grb_editor_menu.addAction(QtGui.QIcon('share/disc32.png'),
+                                                                        _("Add Disc\tD"))
         self.grb_add_buffer_menuitem = self.grb_editor_menu.addAction(QtGui.QIcon('share/buffer16-2.png'),
         self.grb_add_buffer_menuitem = self.grb_editor_menu.addAction(QtGui.QIcon('share/buffer16-2.png'),
                                                                     _('Buffer\tB'))
                                                                     _('Buffer\tB'))
         self.grb_add_scale_menuitem = self.grb_editor_menu.addAction(QtGui.QIcon('share/scale32.png'),
         self.grb_add_scale_menuitem = self.grb_editor_menu.addAction(QtGui.QIcon('share/scale32.png'),
@@ -690,6 +696,12 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.add_pad_ar_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/padarray32.png'), _('Add Pad Array'))
         self.add_pad_ar_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/padarray32.png'), _('Add Pad Array'))
         self.grb_add_track_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/track32.png'), _("Add Track"))
         self.grb_add_track_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/track32.png'), _("Add Track"))
         self.grb_add_region_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/polygon32.png'), _("Add Region"))
         self.grb_add_region_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/polygon32.png'), _("Add Region"))
+        self.grb_convert_poly_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/poligonize32.png'),
+                                                                    _("Poligonize"))
+
+
+        self.grb_add_semidisc_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/semidisc32.png'), _("SemiDisc"))
+        self.grb_add_disc_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/disc32.png'), _("Disc"))
         self.grb_edit_toolbar.addSeparator()
         self.grb_edit_toolbar.addSeparator()
 
 
         self.aperture_buffer_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/buffer16-2.png'), _('Buffer'))
         self.aperture_buffer_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/buffer16-2.png'), _('Buffer'))
@@ -965,6 +977,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20">&nbsp;</td>
                         <td height="20">&nbsp;</td>
                         <td>&nbsp;</td>
                         <td>&nbsp;</td>
                     </tr>
                     </tr>
+                    <tr height="20">
+                        <td height="20"><strong>B</strong></td>
+                        <td>&nbsp;New Gerber</td>
+                    </tr>
                     <tr height="20">
                     <tr height="20">
                         <td height="20"><strong>E</strong></td>
                         <td height="20"><strong>E</strong></td>
                         <td>&nbsp;Edit Object (if selected)</td>
                         <td>&nbsp;Edit Object (if selected)</td>
@@ -1141,6 +1157,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20"><strong>ALT+P</strong></td>
                         <td height="20"><strong>ALT+P</strong></td>
                         <td>&nbsp;Paint Area Tool</td>
                         <td>&nbsp;Paint Area Tool</td>
                     </tr>
                     </tr>
+                    <tr height="20">
+                        <td height="20"><strong>ALT+Q</strong></td>
+                        <td>&nbsp;PDF Import Tool</td>
+                    </tr>
                     <tr height="20">
                     <tr height="20">
                         <td height="20"><strong>ALT+R</strong></td>
                         <td height="20"><strong>ALT+R</strong></td>
                         <td>&nbsp;Transformations Tool</td>
                         <td>&nbsp;Transformations Tool</td>
@@ -1233,6 +1253,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20"><strong>C</strong></td>
                         <td height="20"><strong>C</strong></td>
                         <td>&nbsp;Copy Geo Item</td>
                         <td>&nbsp;Copy Geo Item</td>
                     </tr>
                     </tr>
+                    <tr height="20">
+                        <td height="20"><strong>D</strong></td>
+                        <td>&nbsp;Within Add Arc will toogle the ARC direction: CW or CCW</td>
+                    </tr>
                     <tr height="20">
                     <tr height="20">
                         <td height="20"><strong>E</strong></td>
                         <td height="20"><strong>E</strong></td>
                         <td>&nbsp;Polygon Intersection Tool</td>
                         <td>&nbsp;Polygon Intersection Tool</td>
@@ -1253,6 +1277,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20"><strong>M</strong></td>
                         <td height="20"><strong>M</strong></td>
                         <td>&nbsp;Move Geo Item</td>
                         <td>&nbsp;Move Geo Item</td>
                     </tr>
                     </tr>
+                    <tr height="20">
+                        <td height="20"><strong>M</strong></td>
+                        <td>&nbsp;Within Add Arc will cycle through the ARC modes</td>
+                    </tr>
                     <tr height="20">
                     <tr height="20">
                         <td height="20"><strong>N</strong></td>
                         <td height="20"><strong>N</strong></td>
                         <td>&nbsp;Draw a Polygon</td>
                         <td>&nbsp;Draw a Polygon</td>
@@ -1431,6 +1459,14 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20"><strong>C</strong></td>
                         <td height="20"><strong>C</strong></td>
                         <td>&nbsp;Copy</td>
                         <td>&nbsp;Copy</td>
                     </tr>
                     </tr>
+                    <tr height="20">
+                        <td height="20"><strong>D</strong></td>
+                        <td>&nbsp;Add Disc</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>E</strong></td>
+                        <td>&nbsp;Add SemiDisc</td>
+                    </tr>
                     <tr height="20">
                     <tr height="20">
                         <td height="20"><strong>J</strong></td>
                         <td height="20"><strong>J</strong></td>
                         <td>&nbsp;Jump to Location (x, y)</td>
                         <td>&nbsp;Jump to Location (x, y)</td>
@@ -1447,6 +1483,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20"><strong>P</strong></td>
                         <td height="20"><strong>P</strong></td>
                         <td>&nbsp;Add Pad</td>
                         <td>&nbsp;Add Pad</td>
                     </tr>
                     </tr>
+                    <tr height="20">
+                        <td height="20"><strong>R</strong></td>
+                        <td>&nbsp;Within Track & Region Tools will cycle in REVERSE the bend modes</td>
+                    </tr>
                     <tr height="20">
                     <tr height="20">
                         <td height="20"><strong>S</strong></td>
                         <td height="20"><strong>S</strong></td>
                         <td>&nbsp;Scale</td>
                         <td>&nbsp;Scale</td>
@@ -1455,6 +1495,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20"><strong>T</strong></td>
                         <td height="20"><strong>T</strong></td>
                         <td>&nbsp;Add Track</td>
                         <td>&nbsp;Add Track</td>
                     </tr>
                     </tr>
+                    <tr height="20">
+                        <td height="20"><strong>T</strong></td>
+                        <td>&nbsp;Within Track & Region Tools will cycle FORWARD the bend modes</td>
+                    </tr>
                     <tr height="20">
                     <tr height="20">
                         <td height="20">&nbsp;</td>
                         <td height="20">&nbsp;</td>
                         <td>&nbsp;</td>
                         <td>&nbsp;</td>
@@ -1952,6 +1996,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         # events from Vispy are of type KeyEvent
         # events from Vispy are of type KeyEvent
         else:
         else:
             key = event.key
             key = event.key
+
+        # Propagate to tool
+        response = None
+
         if self.app.call_source == 'app':
         if self.app.call_source == 'app':
             if modifiers == QtCore.Qt.ControlModifier:
             if modifiers == QtCore.Qt.ControlModifier:
                 if key == QtCore.Qt.Key_A:
                 if key == QtCore.Qt.Key_A:
@@ -2078,6 +2126,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.paint_tool.run(toggle=True)
                     self.app.paint_tool.run(toggle=True)
                     return
                     return
 
 
+                # Paint Tool
+                if key == QtCore.Qt.Key_Q:
+                    self.app.pdf_tool.run()
+                    return
+
                 # Transformation Tool
                 # Transformation Tool
                 if key == QtCore.Qt.Key_R:
                 if key == QtCore.Qt.Key_R:
                     self.app.transform_tool.run(toggle=True)
                     self.app.transform_tool.run(toggle=True)
@@ -2380,132 +2433,130 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 if key == QtCore.Qt.Key_3 or key == '3':
                 if key == QtCore.Qt.Key_3 or key == '3':
                     self.app.on_select_tab('tool')
                     self.app.on_select_tab('tool')
 
 
-                # Arc Tool
-                if key == QtCore.Qt.Key_A or key == 'A':
-                    self.app.geo_editor.select_tool('arc')
-
-                # Buffer
-                if key == QtCore.Qt.Key_B or key == 'B':
-                    self.app.geo_editor.select_tool('buffer')
-
-                # Copy
-                if key == QtCore.Qt.Key_C or key == 'C':
-                    self.app.geo_editor.on_copy_click()
-
-                # Substract Tool
-                if key == QtCore.Qt.Key_E or key == 'E':
-                    if self.app.geo_editor.get_selected() is not None:
-                        self.app.geo_editor.intersection()
-                    else:
-                        msg = _("Please select geometry items \n" \
-                              "on which to perform Intersection Tool.")
-
-                        messagebox = QtWidgets.QMessageBox()
-                        messagebox.setText(msg)
-                        messagebox.setWindowTitle(_("Warning"))
-                        messagebox.setWindowIcon(QtGui.QIcon('share/warning.png'))
-                        messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok)
-                        messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok)
-                        messagebox.exec_()
-
-                # Grid Snap
-                if key == QtCore.Qt.Key_G or key == 'G':
-                    self.app.ui.grid_snap_btn.trigger()
-
-                    # make sure that the cursor shape is enabled/disabled, too
-                    if self.app.geo_editor.options['grid_snap'] is True:
-                        self.app.app_cursor.enabled = True
-                    else:
-                        self.app.app_cursor.enabled = False
-
-                # Paint
-                if key == QtCore.Qt.Key_I or key == 'I':
-                    self.app.geo_editor.select_tool('paint')
-
-                # Jump to coords
-                if key == QtCore.Qt.Key_J or key == 'J':
-                    self.app.on_jump_to()
+                if self.app.geo_editor.active_tool is not None and self.geo_select_btn.isChecked() == False:
+                    response = self.app.geo_editor.active_tool.on_key(key=key)
+                    if response is not None:
+                        self.app.inform.emit(response)
+                else:
+                    # Arc Tool
+                    if key == QtCore.Qt.Key_A or key == 'A':
+                        self.app.geo_editor.select_tool('arc')
+
+                    # Buffer
+                    if key == QtCore.Qt.Key_B or key == 'B':
+                        self.app.geo_editor.select_tool('buffer')
+
+                    # Copy
+                    if key == QtCore.Qt.Key_C or key == 'C':
+                        self.app.geo_editor.on_copy_click()
+
+                    # Substract Tool
+                    if key == QtCore.Qt.Key_E or key == 'E':
+                        if self.app.geo_editor.get_selected() is not None:
+                            self.app.geo_editor.intersection()
+                        else:
+                            msg = _("Please select geometry items \n" \
+                                  "on which to perform Intersection Tool.")
+
+                            messagebox = QtWidgets.QMessageBox()
+                            messagebox.setText(msg)
+                            messagebox.setWindowTitle(_("Warning"))
+                            messagebox.setWindowIcon(QtGui.QIcon('share/warning.png'))
+                            messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+                            messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+                            messagebox.exec_()
+
+                    # Grid Snap
+                    if key == QtCore.Qt.Key_G or key == 'G':
+                        self.app.ui.grid_snap_btn.trigger()
+
+                        # make sure that the cursor shape is enabled/disabled, too
+                        if self.app.geo_editor.options['grid_snap'] is True:
+                            self.app.app_cursor.enabled = True
+                        else:
+                            self.app.app_cursor.enabled = False
 
 
-                # Corner Snap
-                if key == QtCore.Qt.Key_K or key == 'K':
-                    self.app.geo_editor.on_corner_snap()
+                    # Paint
+                    if key == QtCore.Qt.Key_I or key == 'I':
+                        self.app.geo_editor.select_tool('paint')
 
 
-                # Move
-                if key == QtCore.Qt.Key_M or key == 'M':
-                    self.app.geo_editor.on_move_click()
+                    # Jump to coords
+                    if key == QtCore.Qt.Key_J or key == 'J':
+                        self.app.on_jump_to()
 
 
-                # Polygon Tool
-                if key == QtCore.Qt.Key_N or key == 'N':
-                    self.app.geo_editor.select_tool('polygon')
+                    # Corner Snap
+                    if key == QtCore.Qt.Key_K or key == 'K':
+                        self.app.geo_editor.on_corner_snap()
 
 
-                # Circle Tool
-                if key == QtCore.Qt.Key_O or key == 'O':
-                    self.app.geo_editor.select_tool('circle')
+                    # Move
+                    if key == QtCore.Qt.Key_M or key == 'M':
+                        self.app.geo_editor.on_move_click()
 
 
-                # Path Tool
-                if key == QtCore.Qt.Key_P or key == 'P':
-                    self.app.geo_editor.select_tool('path')
+                    # Polygon Tool
+                    if key == QtCore.Qt.Key_N or key == 'N':
+                        self.app.geo_editor.select_tool('polygon')
 
 
-                # Rectangle Tool
-                if key == QtCore.Qt.Key_R or key == 'R':
-                    self.app.geo_editor.select_tool('rectangle')
+                    # Circle Tool
+                    if key == QtCore.Qt.Key_O or key == 'O':
+                        self.app.geo_editor.select_tool('circle')
 
 
-                # Substract Tool
-                if key == QtCore.Qt.Key_S or key == 'S':
-                    if self.app.geo_editor.get_selected() is not None:
-                        self.app.geo_editor.subtract()
-                    else:
-                        msg = _(
-                            "Please select geometry items \n"
-                            "on which to perform Substraction Tool.")
+                    # Path Tool
+                    if key == QtCore.Qt.Key_P or key == 'P':
+                        self.app.geo_editor.select_tool('path')
 
 
-                        messagebox = QtWidgets.QMessageBox()
-                        messagebox.setText(msg)
-                        messagebox.setWindowTitle(_("Warning"))
-                        messagebox.setWindowIcon(QtGui.QIcon('share/warning.png'))
-                        messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok)
-                        messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok)
-                        messagebox.exec_()
-
-                # Add Text Tool
-                if key == QtCore.Qt.Key_T or key == 'T':
-                    self.app.geo_editor.select_tool('text')
-
-                # Substract Tool
-                if key == QtCore.Qt.Key_U or key == 'U':
-                    if self.app.geo_editor.get_selected() is not None:
-                        self.app.geo_editor.union()
-                    else:
-                        msg = _("Please select geometry items \n"
-                              "on which to perform union.")
-
-                        messagebox = QtWidgets.QMessageBox()
-                        messagebox.setText(msg)
-                        messagebox.setWindowTitle(_("Warning"))
-                        messagebox.setWindowIcon(QtGui.QIcon('share/warning.png'))
-                        messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok)
-                        messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok)
-                        messagebox.exec_()
-
-                if key == QtCore.Qt.Key_V or key == 'V':
-                    self.app.on_zoom_fit(None)
-
-                # Flip on X axis
-                if key == QtCore.Qt.Key_X or key == 'X':
-                    self.app.geo_editor.transform_tool.on_flipx()
-                    return
-
-                # Flip on Y axis
-                if key == QtCore.Qt.Key_Y or key == 'Y':
-                    self.app.geo_editor.transform_tool.on_flipy()
-                    return
+                    # Rectangle Tool
+                    if key == QtCore.Qt.Key_R or key == 'R':
+                        self.app.geo_editor.select_tool('rectangle')
 
 
-                # Propagate to tool
-                response = None
-                if self.app.geo_editor.active_tool is not None:
-                    response = self.app.geo_editor.active_tool.on_key(key=key)
-                if response is not None:
-                    self.app.inform.emit(response)
+                    # Substract Tool
+                    if key == QtCore.Qt.Key_S or key == 'S':
+                        if self.app.geo_editor.get_selected() is not None:
+                            self.app.geo_editor.subtract()
+                        else:
+                            msg = _(
+                                "Please select geometry items \n"
+                                "on which to perform Substraction Tool.")
+
+                            messagebox = QtWidgets.QMessageBox()
+                            messagebox.setText(msg)
+                            messagebox.setWindowTitle(_("Warning"))
+                            messagebox.setWindowIcon(QtGui.QIcon('share/warning.png'))
+                            messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+                            messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+                            messagebox.exec_()
+
+                    # Add Text Tool
+                    if key == QtCore.Qt.Key_T or key == 'T':
+                        self.app.geo_editor.select_tool('text')
+
+                    # Substract Tool
+                    if key == QtCore.Qt.Key_U or key == 'U':
+                        if self.app.geo_editor.get_selected() is not None:
+                            self.app.geo_editor.union()
+                        else:
+                            msg = _("Please select geometry items \n"
+                                  "on which to perform union.")
+
+                            messagebox = QtWidgets.QMessageBox()
+                            messagebox.setText(msg)
+                            messagebox.setWindowTitle(_("Warning"))
+                            messagebox.setWindowIcon(QtGui.QIcon('share/warning.png'))
+                            messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+                            messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+                            messagebox.exec_()
+
+                    if key == QtCore.Qt.Key_V or key == 'V':
+                        self.app.on_zoom_fit(None)
+
+                    # Flip on X axis
+                    if key == QtCore.Qt.Key_X or key == 'X':
+                        self.app.geo_editor.transform_tool.on_flipx()
+                        return
+
+                    # Flip on Y axis
+                    if key == QtCore.Qt.Key_Y or key == 'Y':
+                        self.app.geo_editor.transform_tool.on_flipy()
+                        return
 
 
                 # Show Shortcut list
                 # Show Shortcut list
                 if key == 'F3':
                 if key == 'F3':
@@ -2525,11 +2576,15 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
             elif modifiers == QtCore.Qt.ShiftModifier:
             elif modifiers == QtCore.Qt.ShiftModifier:
                 pass
                 pass
             elif modifiers == QtCore.Qt.AltModifier:
             elif modifiers == QtCore.Qt.AltModifier:
+                # Poligonize Tool
+                if key == QtCore.Qt.Key_N or key == 'N':
+                    self.app.grb_editor.on_poligonize()
+                    return
+
                 # Transformation Tool
                 # Transformation Tool
                 if key == QtCore.Qt.Key_R or key == 'R':
                 if key == QtCore.Qt.Key_R or key == 'R':
                     self.app.grb_editor.on_transform()
                     self.app.grb_editor.on_transform()
                     return
                     return
-
             elif modifiers == QtCore.Qt.NoModifier:
             elif modifiers == QtCore.Qt.NoModifier:
                 # Abort the current action
                 # Abort the current action
                 if key == QtCore.Qt.Key_Escape or key == 'Escape':
                 if key == QtCore.Qt.Key_Escape or key == 'Escape':
@@ -2599,114 +2654,126 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.on_select_tab('tool')
                     self.app.on_select_tab('tool')
                     return
                     return
 
 
-                # Add Array of pads
-                if key == QtCore.Qt.Key_A or key == 'A':
-                    self.app.grb_editor.launched_from_shortcuts = True
-                    self.app.inform.emit("Click on target point.")
-                    self.app.ui.add_pad_ar_btn.setChecked(True)
-
-                    self.app.grb_editor.x = self.app.mouse[0]
-                    self.app.grb_editor.y = self.app.mouse[1]
-
-                    self.app.grb_editor.select_tool('array')
-                    return
-
-                # Scale Tool
-                if key == QtCore.Qt.Key_B or key == 'B':
-                    self.app.grb_editor.launched_from_shortcuts = True
-                    self.app.grb_editor.select_tool('buffer')
-                    return
-
-                # Copy
-                if key == QtCore.Qt.Key_C or key == 'C':
-                    self.app.grb_editor.launched_from_shortcuts = True
-                    if self.app.grb_editor.selected:
-                        self.app.inform.emit(_("Click on target point."))
-                        self.app.ui.aperture_copy_btn.setChecked(True)
-                        self.app.grb_editor.on_tool_select('copy')
-                        self.app.grb_editor.active_tool.set_origin(
-                            (self.app.grb_editor.snap_x, self.app.grb_editor.snap_y))
-                    else:
-                        self.app.inform.emit(_("[WARNING_NOTCL] Cancelled. Nothing selected to copy."))
-                    return
-
-                # Grid Snap
-                if key == QtCore.Qt.Key_G or key == 'G':
-                    self.app.grb_editor.launched_from_shortcuts = True
-                    # make sure that the cursor shape is enabled/disabled, too
-                    if self.app.grb_editor.options['grid_snap'] is True:
-                        self.app.app_cursor.enabled = False
-                    else:
-                        self.app.app_cursor.enabled = True
-                    self.app.ui.grid_snap_btn.trigger()
-                    return
-
-                # Jump to coords
-                if key == QtCore.Qt.Key_J or key == 'J':
-                    self.app.on_jump_to()
-
-                # Corner Snap
-                if key == QtCore.Qt.Key_K or key == 'K':
-                    self.app.grb_editor.launched_from_shortcuts = True
-                    self.app.ui.corner_snap_btn.trigger()
-                    return
-
-                # Move
-                if key == QtCore.Qt.Key_M or key == 'M':
-                    self.app.grb_editor.launched_from_shortcuts = True
-                    if self.app.grb_editor.selected:
+                # we do this so we can reuse the following keys while inside a Tool
+                # the above keys are general enough so were left outside
+                if self.app.grb_editor.active_tool is not None and self.grb_select_btn.isChecked() == False:
+                    response = self.app.grb_editor.active_tool.on_key(key=key)
+                    if response is not None:
+                        self.app.inform.emit(response)
+                else:
+                    # Add Array of pads
+                    if key == QtCore.Qt.Key_A or key == 'A':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        self.app.inform.emit("Click on target point.")
+                        self.app.ui.add_pad_ar_btn.setChecked(True)
+
+                        self.app.grb_editor.x = self.app.mouse[0]
+                        self.app.grb_editor.y = self.app.mouse[1]
+
+                        self.app.grb_editor.select_tool('array')
+                        return
+
+                    # Scale Tool
+                    if key == QtCore.Qt.Key_B or key == 'B':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        self.app.grb_editor.select_tool('buffer')
+                        return
+
+                    # Copy
+                    if key == QtCore.Qt.Key_C or key == 'C':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        if self.app.grb_editor.selected:
+                            self.app.inform.emit(_("Click on target point."))
+                            self.app.ui.aperture_copy_btn.setChecked(True)
+                            self.app.grb_editor.on_tool_select('copy')
+                            self.app.grb_editor.active_tool.set_origin(
+                                (self.app.grb_editor.snap_x, self.app.grb_editor.snap_y))
+                        else:
+                            self.app.inform.emit(_("[WARNING_NOTCL] Cancelled. Nothing selected to copy."))
+                        return
+
+                    # Add Disc Tool
+                    if key == QtCore.Qt.Key_D or key == 'D':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        self.app.grb_editor.select_tool('disc')
+                        return
+
+                    # Add SemiDisc Tool
+                    if key == QtCore.Qt.Key_E or key == 'E':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        self.app.grb_editor.select_tool('semidisc')
+                        return
+
+                    # Grid Snap
+                    if key == QtCore.Qt.Key_G or key == 'G':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        # make sure that the cursor shape is enabled/disabled, too
+                        if self.app.grb_editor.options['grid_snap'] is True:
+                            self.app.app_cursor.enabled = False
+                        else:
+                            self.app.app_cursor.enabled = True
+                        self.app.ui.grid_snap_btn.trigger()
+                        return
+
+                    # Jump to coords
+                    if key == QtCore.Qt.Key_J or key == 'J':
+                        self.app.on_jump_to()
+
+                    # Corner Snap
+                    if key == QtCore.Qt.Key_K or key == 'K':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        self.app.ui.corner_snap_btn.trigger()
+                        return
+
+                    # Move
+                    if key == QtCore.Qt.Key_M or key == 'M':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        if self.app.grb_editor.selected:
+                            self.app.inform.emit(_("Click on target point."))
+                            self.app.ui.aperture_move_btn.setChecked(True)
+                            self.app.grb_editor.on_tool_select('move')
+                            self.app.grb_editor.active_tool.set_origin(
+                                (self.app.grb_editor.snap_x, self.app.grb_editor.snap_y))
+                        else:
+                            self.app.inform.emit(_("[WARNING_NOTCL] Cancelled. Nothing selected to move."))
+                        return
+
+                    # Add Region Tool
+                    if key == QtCore.Qt.Key_N or key == 'N':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        self.app.grb_editor.select_tool('region')
+                        return
+
+                    # Add Pad Tool
+                    if key == QtCore.Qt.Key_P or key == 'P':
+                        self.app.grb_editor.launched_from_shortcuts = True
                         self.app.inform.emit(_("Click on target point."))
                         self.app.inform.emit(_("Click on target point."))
-                        self.app.ui.aperture_move_btn.setChecked(True)
-                        self.app.grb_editor.on_tool_select('move')
-                        self.app.grb_editor.active_tool.set_origin(
-                            (self.app.grb_editor.snap_x, self.app.grb_editor.snap_y))
-                    else:
-                        self.app.inform.emit(_("[WARNING_NOTCL] Cancelled. Nothing selected to move."))
-                    return
-
-                # Add Region Tool
-                if key == QtCore.Qt.Key_N or key == 'N':
-                    self.app.grb_editor.launched_from_shortcuts = True
-                    self.app.grb_editor.select_tool('region')
-                    return
+                        self.app.ui.add_pad_ar_btn.setChecked(True)
 
 
-                # Add Pad Tool
-                if key == QtCore.Qt.Key_P or key == 'P':
-                    self.app.grb_editor.launched_from_shortcuts = True
-                    self.app.inform.emit(_("Click on target point."))
-                    self.app.ui.add_pad_ar_btn.setChecked(True)
+                        self.app.grb_editor.x = self.app.mouse[0]
+                        self.app.grb_editor.y = self.app.mouse[1]
 
 
-                    self.app.grb_editor.x = self.app.mouse[0]
-                    self.app.grb_editor.y = self.app.mouse[1]
+                        self.app.grb_editor.select_tool('pad')
+                        return
 
 
-                    self.app.grb_editor.select_tool('pad')
-                    return
+                    # Scale Tool
+                    if key == QtCore.Qt.Key_S or key == 'S':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        self.app.grb_editor.select_tool('scale')
+                        return
 
 
-                # Scale Tool
-                if key == QtCore.Qt.Key_S or key == 'S':
-                    self.app.grb_editor.launched_from_shortcuts = True
-                    self.app.grb_editor.select_tool('scale')
-                    return
+                    # Add Track
+                    if key == QtCore.Qt.Key_T or key == 'T':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        ## Current application units in Upper Case
+                        self.app.grb_editor.select_tool('track')
+                        return
 
 
-                # Add Track
-                if key == QtCore.Qt.Key_T or key == 'T':
-                    self.app.grb_editor.launched_from_shortcuts = True
-                    ## Current application units in Upper Case
-                    self.app.grb_editor.select_tool('track')
-                    return
-
-                # Zoom Fit
-                if key == QtCore.Qt.Key_V or key == 'V':
-                    self.app.grb_editor.launched_from_shortcuts = True
-                    self.app.on_zoom_fit(None)
-                    return
-
-                # Propagate to tool
-                response = None
-                if self.app.grb_editor.active_tool is not None:
-                    response = self.app.grb_editor.active_tool.on_key(key=key)
-                if response is not None:
-                    self.app.inform.emit(response)
+                    # Zoom Fit
+                    if key == QtCore.Qt.Key_V or key == 'V':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        self.app.on_zoom_fit(None)
+                        return
 
 
                 # Show Shortcut list
                 # Show Shortcut list
                 if key == QtCore.Qt.Key_F3 or key == 'F3':
                 if key == QtCore.Qt.Key_F3 or key == 'F3':
@@ -2909,6 +2976,23 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 if key == QtCore.Qt.Key_F3 or key == 'F3':
                 if key == QtCore.Qt.Key_F3 or key == 'F3':
                     self.app.on_shortcut_list()
                     self.app.on_shortcut_list()
                     return
                     return
+        elif self.app.call_source == 'measurement':
+            if modifiers == QtCore.Qt.ControlModifier:
+                pass
+            elif modifiers == QtCore.Qt.AltModifier:
+                pass
+            elif modifiers == QtCore.Qt.ShiftModifier:
+                pass
+            elif modifiers == QtCore.Qt.NoModifier:
+                if key == QtCore.Qt.Key_Escape or key == 'Escape':
+                    # abort the measurement action
+                    self.app.measurement_tool.deactivate_measure_tool()
+                    self.app.inform.emit(_("Measurement Tool exit..."))
+                    return
+
+                if key == QtCore.Qt.Key_G or key == 'G':
+                    self.app.ui.grid_snap_btn.trigger()
+                    return
 
 
     def dragEnterEvent(self, event):
     def dragEnterEvent(self, event):
         if event.mimeData().hasUrls:
         if event.mimeData().hasUrls:
@@ -2962,6 +3046,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         self.app.worker_task.emit({'fcn': self.app.import_dxf,
                         self.app.worker_task.emit({'fcn': self.app.import_dxf,
                                                    'params': [self.filename, object_type, None]})
                                                    'params': [self.filename, object_type, None]})
 
 
+                    if extension in self.app.pdf_list:
+                        self.app.worker_task.emit({'fcn': self.app.pdf_tool.open_pdf,
+                                                   'params': [self.filename]})
+
                     if extension in self.app.prj_list:
                     if extension in self.app.prj_list:
                         # self.app.open_project() is not Thread Safe
                         # self.app.open_project() is not Thread Safe
                         self.app.open_project(self.filename)
                         self.app.open_project(self.filename)

+ 3 - 2
flatcamGUI/GUIElements.py

@@ -27,7 +27,7 @@ EDIT_SIZE_HINT = 70
 
 
 
 
 class RadioSet(QtWidgets.QWidget):
 class RadioSet(QtWidgets.QWidget):
-    activated_custom = QtCore.pyqtSignal()
+    activated_custom = QtCore.pyqtSignal(str)
 
 
     def __init__(self, choices, orientation='horizontal', parent=None, stretch=None):
     def __init__(self, choices, orientation='horizontal', parent=None, stretch=None):
         """
         """
@@ -72,7 +72,8 @@ class RadioSet(QtWidgets.QWidget):
         radio = self.sender()
         radio = self.sender()
         if radio.isChecked():
         if radio.isChecked():
             self.group_toggle_fn()
             self.group_toggle_fn()
-            self.activated_custom.emit()
+            ret_val = str(self.get_value())
+            self.activated_custom.emit(ret_val)
         return
         return
 
 
     def get_value(self):
     def get_value(self):

+ 1 - 1
flatcamGUI/ObjectUI.py

@@ -975,7 +975,7 @@ class GeometryObjectUI(ObjectUI):
                 "Diameter for the new tool"
                 "Diameter for the new tool"
             )
             )
         )
         )
-        self.addtool_entry = FCEntry()
+        self.addtool_entry = FCEntry2()
 
 
         # hlay.addWidget(self.addtool_label)
         # hlay.addWidget(self.addtool_label)
         # hlay.addStretch()
         # hlay.addStretch()

+ 134 - 119
flatcamTools/ToolMeasurement.py

@@ -103,33 +103,44 @@ class Measurement(FlatCAMTool):
 
 
         self.layout.addStretch()
         self.layout.addStretch()
 
 
-        self.clicked_meas = 0
+        # store here the first click and second click of the measurement process
+        self.points = []
+
+        self.rel_point1 = None
+        self.rel_point2 = None
 
 
-        self.point1 = None
-        self.point2 = None
+        self.active = False
 
 
-        # the default state is disabled for the Move command
-        # self.setVisible(False)
-        self.active = 0
+        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.vispy_canvas.view.scene, layers=1)
 
 
-        self.measure_btn.clicked.connect(lambda: self.on_measure(activate=True))
+        self.measure_btn.clicked.connect(self.activate_measure_tool)
 
 
     def run(self, toggle=False):
     def run(self, toggle=False):
         self.app.report_usage("ToolMeasurement()")
         self.app.report_usage("ToolMeasurement()")
 
 
+        self.points[:] = []
+
+        self.rel_point1 = None
+        self.rel_point2 = None
+
         if self.app.tool_tab_locked is True:
         if self.app.tool_tab_locked is True:
             return
             return
 
 
         self.app.ui.notebook.setTabText(2, _("Meas. Tool"))
         self.app.ui.notebook.setTabText(2, _("Meas. Tool"))
 
 
-        # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+        # if the splitter is hidden, display it
         if self.app.ui.splitter.sizes()[0] == 0:
         if self.app.ui.splitter.sizes()[0] == 0:
             self.app.ui.splitter.setSizes([1, 1])
             self.app.ui.splitter.setSizes([1, 1])
+        if toggle:
+            pass
 
 
-        self.on_measure(activate=True)
+        if self.active is False:
+            self.activate_measure_tool()
+        else:
+            self.deactivate_measure_tool()
 
 
     def install(self, icon=None, separator=None, **kwargs):
     def install(self, icon=None, separator=None, **kwargs):
         FlatCAMTool.install(self, icon, separator, shortcut='CTRL+M', **kwargs)
         FlatCAMTool.install(self, icon, separator, shortcut='CTRL+M', **kwargs)
@@ -154,123 +165,114 @@ class Measurement(FlatCAMTool):
         self.distance_x_entry.set_value('0')
         self.distance_x_entry.set_value('0')
         self.distance_y_entry.set_value('0')
         self.distance_y_entry.set_value('0')
         self.total_distance_entry.set_value('0')
         self.total_distance_entry.set_value('0')
+        log.debug("Measurement Tool --> tool initialized")
 
 
-    def activate(self):
-        # we disconnect the mouse/key handlers from wherever the measurement tool was called
-        self.canvas.vis_disconnect('key_press')
-        self.canvas.vis_disconnect('mouse_move')
-        self.canvas.vis_disconnect('mouse_press')
-        self.canvas.vis_disconnect('mouse_release')
-        self.canvas.vis_disconnect('key_release')
+    def activate_measure_tool(self):
+        # ENABLE the Measuring TOOL
+        self.active = True
 
 
-        # we can safely connect the app mouse events to the measurement tool
+        self.clicked_meas = 0
+        self.original_call_source = copy(self.app.call_source)
+
+        self.app.inform.emit(_("MEASURING: Click on the Start point ..."))
+        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
+
+        # we can connect the app mouse events to the measurement tool
+        # NEVER DISCONNECT THOSE before connecting some other handlers; it breaks something in VisPy
         self.canvas.vis_connect('mouse_move', self.on_mouse_move_meas)
         self.canvas.vis_connect('mouse_move', self.on_mouse_move_meas)
-        self.canvas.vis_connect('mouse_release', self.on_mouse_click)
-        self.canvas.vis_connect('key_release', self.on_key_release_meas)
+        self.canvas.vis_connect('mouse_release', self.on_mouse_click_release)
 
 
-        self.set_tool_ui()
+        # we disconnect the mouse/key handlers from wherever the measurement tool was called
+        if self.app.call_source == 'app':
+            self.canvas.vis_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
+            self.canvas.vis_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
+            self.canvas.vis_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
+        elif self.app.call_source == 'geo_editor':
+            self.canvas.vis_disconnect('mouse_move', self.app.geo_editor.on_canvas_move)
+            self.canvas.vis_disconnect('mouse_press', self.app.geo_editor.on_canvas_click)
+            self.canvas.vis_disconnect('mouse_release', self.app.geo_editor.on_geo_click_release)
+        elif self.app.call_source == 'exc_editor':
+            self.canvas.vis_disconnect('mouse_move', self.app.exc_editor.on_canvas_move)
+            self.canvas.vis_disconnect('mouse_press', self.app.exc_editor.on_canvas_click)
+            self.canvas.vis_disconnect('mouse_release', self.app.exc_editor.on_exc_click_release)
+        elif self.app.call_source == 'grb_editor':
+            self.canvas.vis_disconnect('mouse_move', self.app.grb_editor.on_canvas_move)
+            self.canvas.vis_disconnect('mouse_press', self.app.grb_editor.on_canvas_click)
+            self.canvas.vis_disconnect('mouse_release', self.app.grb_editor.on_grb_click_release)
 
 
-    def deactivate(self):
-        # disconnect the mouse/key events from functions of measurement tool
-        self.canvas.vis_disconnect('mouse_move', self.on_mouse_move_meas)
-        self.canvas.vis_disconnect('mouse_release', self.on_mouse_click)
-        self.canvas.vis_disconnect('key_release', self.on_key_release_meas)
+        self.app.call_source = 'measurement'
 
 
-        # reconnect the mouse/key events to the functions from where the tool was called
-        self.canvas.vis_connect('key_press', self.app.ui.keyPressEvent)
+        self.set_tool_ui()
 
 
-        if self.app.call_source == 'app':
+    def deactivate_measure_tool(self):
+        # DISABLE the Measuring TOOL
+        self.active = False
+        self.points = []
+
+        self.app.call_source = copy(self.original_call_source)
+        if self.original_call_source == 'app':
             self.canvas.vis_connect('mouse_move', self.app.on_mouse_move_over_plot)
             self.canvas.vis_connect('mouse_move', self.app.on_mouse_move_over_plot)
             self.canvas.vis_connect('mouse_press', self.app.on_mouse_click_over_plot)
             self.canvas.vis_connect('mouse_press', self.app.on_mouse_click_over_plot)
             self.canvas.vis_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
             self.canvas.vis_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
-        elif self.app.call_source == 'geo_editor':
+        elif self.original_call_source == 'geo_editor':
             self.canvas.vis_connect('mouse_move', self.app.geo_editor.on_canvas_move)
             self.canvas.vis_connect('mouse_move', self.app.geo_editor.on_canvas_move)
             self.canvas.vis_connect('mouse_press', self.app.geo_editor.on_canvas_click)
             self.canvas.vis_connect('mouse_press', self.app.geo_editor.on_canvas_click)
-            # self.canvas.vis_connect('key_press', self.app.geo_editor.on_canvas_key)
-            self.canvas.vis_connect('mouse_release', self.app.geo_editor.on_canvas_click_release)
-        elif self.app.call_source == 'exc_editor':
+            self.canvas.vis_connect('mouse_release', self.app.geo_editor.on_geo_click_release)
+        elif self.original_call_source == 'exc_editor':
             self.canvas.vis_connect('mouse_move', self.app.exc_editor.on_canvas_move)
             self.canvas.vis_connect('mouse_move', self.app.exc_editor.on_canvas_move)
             self.canvas.vis_connect('mouse_press', self.app.exc_editor.on_canvas_click)
             self.canvas.vis_connect('mouse_press', self.app.exc_editor.on_canvas_click)
-            # self.canvas.vis_connect('key_press', self.app.exc_editor.on_canvas_key)
-            self.canvas.vis_connect('mouse_release', self.app.exc_editor.on_canvas_click_release)
-        elif self.app.call_source == 'grb_editor':
+            self.canvas.vis_connect('mouse_release', self.app.exc_editor.on_exc_click_release)
+        elif self.original_call_source == 'grb_editor':
             self.canvas.vis_connect('mouse_move', self.app.grb_editor.on_canvas_move)
             self.canvas.vis_connect('mouse_move', self.app.grb_editor.on_canvas_move)
             self.canvas.vis_connect('mouse_press', self.app.grb_editor.on_canvas_click)
             self.canvas.vis_connect('mouse_press', self.app.grb_editor.on_canvas_click)
-            # self.canvas.vis_connect('key_press', self.app.grb_editor.on_canvas_key)
-            self.canvas.vis_connect('mouse_release', self.app.grb_editor.on_canvas_click_release)
-
-        self.app.ui.notebook.setTabText(2, _("Tools"))
-        self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
-
-    def on_measure(self, signal=None, activate=None):
-        log.debug("Measurement.on_measure()")
-        if activate is False or activate is None:
-            # DISABLE the Measuring TOOL
-            self.deactivate()
+            self.canvas.vis_connect('mouse_release', self.app.grb_editor.on_grb_click_release)
 
 
-            self.app.command_active = None
-
-            # delete the measuring line
-            self.delete_shape()
-
-            log.debug("Measurement Tool --> exit tool")
-        elif activate is True:
-            # ENABLE the Measuring TOOL
-            self.clicked_meas = 0
+        # disconnect the mouse/key events from functions of measurement tool
+        self.canvas.vis_disconnect('mouse_move', self.on_mouse_move_meas)
+        self.canvas.vis_disconnect('mouse_release', self.on_mouse_click_release)
 
 
-            self.app.inform.emit(_("MEASURING: Click on the Start point ..."))
-            self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
+        # self.app.ui.notebook.setTabText(2, _("Tools"))
+        # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
 
 
-            self.activate()
-            log.debug("Measurement Tool --> tool initialized")
+        self.app.command_active = None
 
 
-    def on_key_release_meas(self, event):
-        if event.key == 'escape':
-            # abort the measurement action
-            self.on_measure(activate=False)
-            self.app.inform.emit(_("Measurement Tool exit..."))
-            return
+        # delete the measuring line
+        self.delete_shape()
 
 
-        if event.key == 'G':
-            # toggle grid status
-            self.app.ui.grid_snap_btn.trigger()
-            return
+        log.debug("Measurement Tool --> exit tool")
 
 
-    def on_mouse_click(self, event):
+    def on_mouse_click_release(self, event):
         # mouse click releases will be accepted only if the left button is clicked
         # mouse click releases will be accepted only if the left button is clicked
         # this is necessary because right mouse click or middle mouse click
         # this is necessary because right mouse click or middle mouse click
         # are used for panning on the canvas
         # are used for panning on the canvas
+        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.vispy_canvas.translate_coords(event.pos)
+            # if GRID is active we need to get the snapped positions
+            if self.app.grid_status():
+                pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
+            else:
+                pos = pos_canvas[0], pos_canvas[1]
+            self.points.append(pos)
+
+            # Reset here the relative coordinates so there is a new reference on the click position
+            if self.rel_point1 is None:
+                self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                                       "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0.0, 0.0))
+                self.rel_point1 = pos
+            else:
+                self.rel_point2 = copy(self.rel_point1)
+                self.rel_point1 = pos
 
 
-            if self.clicked_meas == 0:
-                self.clicked_meas = 1
-
-                # if GRID is active we need to get the snapped positions
-                if self.app.grid_status() == True:
-                    pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
-                else:
-                    pos = pos_canvas[0], pos_canvas[1]
-
-                self.point1 = pos
+            if len(self.points) == 1:
                 self.start_entry.set_value("(%.4f, %.4f)" % pos)
                 self.start_entry.set_value("(%.4f, %.4f)" % pos)
                 self.app.inform.emit(_("MEASURING: Click on the Destination point ..."))
                 self.app.inform.emit(_("MEASURING: Click on the Destination point ..."))
 
 
-            else:
-                # delete the selection bounding box
-                self.delete_shape()
-
-                # if GRID is active we need to get the snapped positions
-                if self.app.grid_status() is True:
-                    pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
-                else:
-                    pos = pos_canvas[0], pos_canvas[1]
-
-                dx = pos[0] - self.point1[0]
-                dy = pos[1] - self.point1[1]
+            if len(self.points) == 2:
+                dx = self.points[1][0] - self.points[0][0]
+                dy = self.points[1][1] - self.points[0][1]
                 d = sqrt(dx ** 2 + dy ** 2)
                 d = sqrt(dx ** 2 + dy ** 2)
-
                 self.stop_entry.set_value("(%.4f, %.4f)" % pos)
                 self.stop_entry.set_value("(%.4f, %.4f)" % pos)
 
 
                 self.app.inform.emit(_("MEASURING: Result D(x) = {d_x} | D(y) = {d_y} | Distance = {d_z}").format(
                 self.app.inform.emit(_("MEASURING: Result D(x) = {d_x} | D(y) = {d_y} | Distance = {d_z}").format(
@@ -279,34 +281,47 @@ class Measurement(FlatCAMTool):
                 self.distance_x_entry.set_value('%.4f' % abs(dx))
                 self.distance_x_entry.set_value('%.4f' % abs(dx))
                 self.distance_y_entry.set_value('%.4f' % abs(dy))
                 self.distance_y_entry.set_value('%.4f' % abs(dy))
                 self.total_distance_entry.set_value('%.4f' % abs(d))
                 self.total_distance_entry.set_value('%.4f' % abs(d))
-
-                # TODO: I don't understand why I have to do it twice ... but without it the mouse handlers are
-                # TODO: are left disconnected ...
-                self.on_measure(activate=False)
-                self.deactivate()
+                self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                                       "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (pos[0], pos[1]))
+                self.deactivate_measure_tool()
 
 
     def on_mouse_move_meas(self, event):
     def on_mouse_move_meas(self, event):
-        pos_canvas = self.canvas.vispy_canvas.translate_coords(event.pos)
-
-        # if GRID is active we need to get the snapped positions
-        if self.app.grid_status() == True:
-            pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
-            self.app.app_cursor.enabled = True
-            # Update cursor
-            self.app.app_cursor.set_data(np.asarray([(pos[0], pos[1])]), symbol='++', edge_color='black', size=20)
-        else:
-            pos = pos_canvas
-            self.app.app_enabled = False
-
-        self.point2 = (pos[0], pos[1])
-
-        # update utility geometry
-        if self.clicked_meas == 1:
-            # first delete old shape
-            self.delete_shape()
-            # second draw the new shape of the utility geometry
-            self.meas_line = LineString([self.point2, self.point1])
-            self.sel_shapes.add(self.meas_line, color='black', update=True, layer=0, tolerance=None)
+        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 = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
+                self.app.app_cursor.enabled = True
+                # Update cursor
+                self.app.app_cursor.set_data(np.asarray([(pos[0], pos[1])]),
+                                             symbol='++', edge_color='black', size=20)
+            else:
+                pos = (pos_canvas[0], pos_canvas[1])
+                self.app.app_cursor.enabled = False
+
+            if self.rel_point1 is not None:
+                dx = pos[0] - self.rel_point1[0]
+                dy = pos[1] - self.rel_point1[1]
+            else:
+                dx = pos[0]
+                dy = pos[1]
+
+            self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
+                                               "<b>Y</b>: %.4f" % (pos[0], pos[1]))
+            self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                                   "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (dx, dy))
+            # update utility geometry
+            if len(self.points) == 1:
+                self.utility_geometry(pos=pos)
+        except:
+            self.app.ui.position_label.setText("")
+            self.app.ui.rel_position_label.setText("")
+
+    def utility_geometry(self, pos):
+        # first delete old shape
+        self.delete_shape()
+        # second draw the new shape of the utility geometry
+        self.meas_line = LineString([pos, self.points[0]])
+        self.sel_shapes.add(self.meas_line, color='black', update=True, layer=0, tolerance=None)
 
 
     def delete_shape(self):
     def delete_shape(self):
         self.sel_shapes.clear()
         self.sel_shapes.clear()

+ 2 - 1
flatcamTools/ToolNonCopperClear.py

@@ -121,7 +121,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.addtool_entry_lbl.setToolTip(
         self.addtool_entry_lbl.setToolTip(
             _("Diameter for the new tool to add in the Tool Table")
             _("Diameter for the new tool to add in the Tool Table")
         )
         )
-        self.addtool_entry = FCEntry()
+        self.addtool_entry = FCEntry2()
 
 
         # hlay.addWidget(self.addtool_label)
         # hlay.addWidget(self.addtool_label)
         # hlay.addStretch()
         # hlay.addStretch()
@@ -254,6 +254,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.tools_box.addStretch()
         self.tools_box.addStretch()
 
 
         self.addtool_btn.clicked.connect(self.on_tool_add)
         self.addtool_btn.clicked.connect(self.on_tool_add)
+        self.addtool_entry.returnPressed.connect(self.on_tool_add)
         self.deltool_btn.clicked.connect(self.on_tool_delete)
         self.deltool_btn.clicked.connect(self.on_tool_delete)
         self.generate_ncc_button.clicked.connect(self.on_ncc)
         self.generate_ncc_button.clicked.connect(self.on_ncc)
 
 

+ 1001 - 0
flatcamTools/ToolPDF.py

@@ -0,0 +1,1001 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Date: 3/10/2019                                          #
+# MIT Licence                                              #
+############################################################
+
+from FlatCAMTool import FlatCAMTool
+from shapely.geometry import Point, Polygon, LineString
+from shapely.ops import cascaded_union, unary_union
+
+from FlatCAMObj import *
+
+import math
+from copy import copy, deepcopy
+import numpy as np
+
+import zlib
+import re
+
+import gettext
+import FlatCAMTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class ToolPDF(FlatCAMTool):
+    """
+    Parse a PDF file.
+    Reference here: https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/pdf_reference_archives/PDFReference.pdf
+    Return a list of geometries
+    """
+    toolName = _("PDF Import Tool")
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+        self.app = app
+        self.step_per_circles = self.app.defaults["gerber_circle_steps"]
+
+        self.stream_re = re.compile(b'.*?FlateDecode.*?stream(.*?)endstream', re.S)
+
+        # detect stroke color change; it means a new object to be created
+        self.stroke_color_re = re.compile(r'^\s*(\d+\.?\d*) (\d+\.?\d*) (\d+\.?\d*)\s*RG$')
+
+        # detect fill color change; we check here for white color (transparent geometry);
+        # if detected we create an Excellon from it
+        self.fill_color_re = re.compile(r'^\s*(\d+\.?\d*) (\d+\.?\d*) (\d+\.?\d*)\s*rg$')
+
+        # detect 're' command
+        self.rect_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*re$')
+        # detect 'm' command
+        self.start_subpath_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\sm$')
+        # detect 'l' command
+        self.draw_line_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\sl')
+        # detect 'c' command
+        self.draw_arc_3pt_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)'
+                                          r'\s(-?\d+\.?\d*)\s*c$')
+        # detect 'v' command
+        self.draw_arc_2pt_c1start_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*v$')
+        # detect 'y' command
+        self.draw_arc_2pt_c2stop_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*y$')
+        # detect 'h' command
+        self.end_subpath_re = re.compile(r'^h$')
+
+        # detect 'w' command
+        self.strokewidth_re = re.compile(r'^(\d+\.?\d*)\s*w$')
+        # detect 'S' command
+        self.stroke_path__re = re.compile(r'^S\s?[Q]?$')
+        # detect 's' command
+        self.close_stroke_path__re = re.compile(r'^s$')
+        # detect 'f' or 'f*' command
+        self.fill_path_re = re.compile(r'^[f|F][*]?$')
+        # detect 'B' or 'B*' command
+        self.fill_stroke_path_re = re.compile(r'^B[*]?$')
+        # detect 'b' or 'b*' command
+        self.close_fill_stroke_path_re = re.compile(r'^b[*]?$')
+        # detect 'n'
+        self.no_op_re = re.compile(r'^n$')
+
+        # detect offset transformation. Pattern: (1) (0) (0) (1) (x) (y)
+        # self.offset_re = re.compile(r'^1\.?0*\s0?\.?0*\s0?\.?0*\s1\.?0*\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*cm$')
+        # detect scale transformation. Pattern: (factor_x) (0) (0) (factor_y) (0) (0)
+        # self.scale_re = re.compile(r'^q? (-?\d+\.?\d*) 0\.?0* 0\.?0* (-?\d+\.?\d*) 0\.?0* 0\.?0*\s+cm$')
+        # detect combined transformation. Should always be the last
+        self.combined_transform_re = re.compile(r'^(q)?\s*(-?\d+\.?\d*) (-?\d+\.?\d*) (-?\d+\.?\d*) (-?\d+\.?\d*) '
+                                                r'(-?\d+\.?\d*) (-?\d+\.?\d*)\s+cm$')
+
+        # detect clipping path
+        self.clip_path_re = re.compile(r'^W[*]? n?$')
+
+        # detect save graphic state in graphic stack
+        self.save_gs_re = re.compile(r'^q.*?$')
+
+        # detect restore graphic state from graphic stack
+        self.restore_gs_re = re.compile(r'^Q.*$')
+
+        # graphic stack where we save parameters like transformation, line_width
+        self.gs = dict()
+        # each element is a list composed of sublist elements
+        # (each sublist has 2 lists each having 2 elements: first is offset like:
+        # offset_geo = [off_x, off_y], second element is scale list with 2 elements, like: scale_geo = [sc_x, sc_yy])
+        self.gs['transform'] = []
+        self.gs['line_width'] = []   # each element is a float
+
+        self.obj_dict = dict()
+        self.pdf_parsed = ''
+        self.parsed_obj_dict = dict()
+
+        # conversion factor to INCH
+        self.point_to_unit_factor = 0.01388888888
+
+    def run(self, toggle=True):
+        self.app.report_usage("ToolPDF()")
+
+        self.set_tool_ui()
+        self.on_open_pdf_click()
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+Q', **kwargs)
+
+    def set_tool_ui(self):
+        pass
+
+    def on_open_pdf_click(self):
+        """
+        File menu callback for opening an PDF file.
+
+        :return: None
+        """
+
+        self.app.report_usage("ToolPDF.on_open_pdf_click()")
+        self.app.log.debug("ToolPDF.on_open_pdf_click()")
+
+        _filter_ = "Adobe PDF Files (*.pdf);;" \
+                   "All Files (*.*)"
+
+        try:
+            filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open PDF"),
+                                                                   directory=self.app.get_last_folder(),
+                                                                   filter=_filter_)
+        except TypeError:
+            filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open PDF"), filter=_filter_)
+
+        if len(filenames) == 0:
+            self.app.inform.emit(_("[WARNING_NOTCL] Open PDF cancelled."))
+        else:
+            for filename in filenames:
+                if filename != '':
+                    self.app.worker_task.emit({'fcn': self.open_pdf, 'params': [filename]})
+
+    def open_pdf(self, filename):
+        new_name = filename.split('/')[-1].split('\\')[-1]
+        self.obj_dict.clear()
+        self.pdf_parsed = ''
+        self.parsed_obj_dict = {}
+        obj_type = 'gerber'
+
+        # the UNITS in PDF files are points and here we set the factor to convert them to real units (either MM or INCH)
+        if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
+            # 1 inch = 72 points => 1 point = 1 / 72 = 0.01388888888 inch = 0.01388888888 inch * 25.4 = 0.35277777778 mm
+            self.point_to_unit_factor = 25.4 / 72
+        else:
+            # 1 inch = 72 points => 1 point = 1 / 72 = 0.01388888888 inch
+            self.point_to_unit_factor = 1 / 72
+
+        with self.app.proc_container.new(_("Parsing PDF file ...")):
+            with open(filename, "rb") as f:
+                pdf = f.read()
+
+            stream_nr = 0
+            for s in re.findall(self.stream_re, pdf):
+                stream_nr += 1
+                log.debug(" PDF STREAM: %d\n" % stream_nr)
+                s = s.strip(b'\r\n')
+                try:
+                    self.pdf_parsed += (zlib.decompress(s).decode('UTF-8') + '\r\n')
+                except Exception as e:
+                    log.debug("ToolPDF.open_pdf().obj_init() --> %s" % str(e))
+
+            self.parsed_obj_dict = self.parse_pdf(pdf_content=self.pdf_parsed)
+
+        for k in self.parsed_obj_dict:
+            ap_dict = deepcopy(self.parsed_obj_dict[k])
+            if ap_dict:
+                if k == 0:
+                    # Excellon
+                    obj_type = 'excellon'
+
+                    new_name = new_name + "_exc"
+                    # store the points here until reconstitution: keys are diameters and values are list of (x,y) coords
+                    points = {}
+
+                    def obj_init(exc_obj, app_obj):
+                        # print(self.parsed_obj_dict[0])
+
+                        for geo in self.parsed_obj_dict[0]['0']['solid_geometry']:
+                            xmin, ymin, xmax, ymax = geo.bounds
+                            center = (((xmax - xmin) / 2) + xmin, ((ymax - ymin) / 2) + ymin)
+
+                            # for drill bits, even in INCH, it's enough 3 decimals
+                            correction_factor = 0.974
+                            dia = (xmax - xmin) * correction_factor
+                            dia = round(dia, 3)
+                            if dia in points:
+                                points[dia].append(center)
+                            else:
+                                points[dia] = [center]
+
+                        sorted_dia = sorted(points.keys())
+
+                        name_tool = 0
+                        for dia in sorted_dia:
+                            name_tool += 1
+
+                            # create tools dictionary
+                            spec = {"C": dia}
+                            spec['solid_geometry'] = []
+                            exc_obj.tools[str(name_tool)] = spec
+
+                            # create drill list of dictionaries
+                            for dia_points in points:
+                                if dia == dia_points:
+                                    for pt in points[dia_points]:
+                                        exc_obj.drills.append({'point': Point(pt), 'tool': str(name_tool)})
+                                    break
+
+                        ret = exc_obj.create_geometry()
+                        if ret == 'fail':
+                            log.debug("Could not create geometry for Excellon object.")
+                            return "fail"
+                        for tool in exc_obj.tools:
+                            if exc_obj.tools[tool]['solid_geometry']:
+                                return
+                        app_obj.inform.emit(_("[ERROR_NOTCL] No geometry found in file: %s") % new_name)
+                        return "fail"
+                else:
+                    # Gerber
+                    obj_type = 'gerber'
+
+                    def obj_init(grb_obj, app_obj):
+
+                        grb_obj.apertures = ap_dict
+
+                        poly_buff = []
+                        for ap in grb_obj.apertures:
+                            for k in grb_obj.apertures[ap]:
+                                if k == 'solid_geometry':
+                                    poly_buff += ap_dict[ap][k]
+
+                        poly_buff = unary_union(poly_buff)
+                        try:
+                            poly_buff = poly_buff.buffer(0.0000001)
+                        except ValueError:
+                            pass
+                        try:
+                            poly_buff = poly_buff.buffer(-0.0000001)
+                        except ValueError:
+                            pass
+
+                        grb_obj.solid_geometry = deepcopy(poly_buff)
+
+                with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % (int(k) - 2)):
+
+                    ret = self.app.new_object(obj_type, new_name, obj_init, autoselected=False)
+                    if ret == 'fail':
+                        self.app.inform.emit(_('[ERROR_NOTCL] Open PDF file failed.'))
+                        return
+                    # Register recent file
+                    self.app.file_opened.emit(obj_type, filename)
+                    # GUI feedback
+                    self.app.inform.emit(_("[success] Opened: %s") % filename)
+
+    def parse_pdf(self, pdf_content):
+        path = dict()
+        path['lines'] = []      # it's a list of lines subpaths
+        path['bezier'] = []     # it's a list of bezier arcs subpaths
+        path['rectangle'] = []  # it's a list of rectangle subpaths
+
+        subpath = dict()
+        subpath['lines'] = []      # it's a list of points
+        subpath['bezier'] = []     # it's a list of sublists each like this [start, c1, c2, stop]
+        subpath['rectangle'] = []  # it's a list of sublists of points
+
+        # store the start point (when 'm' command is encountered)
+        current_subpath = None
+
+        # set True when 'h' command is encountered (close subpath)
+        close_subpath = False
+
+        start_point = None
+        current_point = None
+        size = 0
+
+        # initial values for the transformations, in case they are not encountered in the PDF file
+        offset_geo = [0, 0]
+        scale_geo = [1, 1]
+
+        # store the objects to be transformed into Gerbers
+        object_dict = {}
+
+        # will serve as key in the object_dict
+        object_nr = 1
+
+        # store the apertures here
+        apertures_dict = {}
+
+        # initial aperture
+        aperture = 10
+
+        # store the apertures with clear geometry here
+        # we are interested only in the circular geometry (drill holes) therefore we target only Bezier subpaths
+        clear_apertures_dict = dict()
+        # everything will be stored in the '0' aperture since we are dealing with clear polygons not strokes
+        clear_apertures_dict['0'] = dict()
+        clear_apertures_dict['0']['size'] = 0.0
+        clear_apertures_dict['0']['type'] = 'C'
+        clear_apertures_dict['0']['solid_geometry'] = []
+
+        # create first object
+        object_dict[object_nr] = apertures_dict
+        object_nr += 1
+
+        # on stroke color change we create a new apertures dictionary and store the old one in a storage from where
+        # it will be transformed into Gerber object
+        old_color = [None, None ,None]
+
+        # signal that we have clear geometry and the geometry will be added to a special object_nr = 0
+        flag_clear_geo = False
+
+        line_nr = 0
+        lines = pdf_content.splitlines()
+
+        for pline in lines:
+            line_nr += 1
+            # log.debug("line %d: %s" % (line_nr, pline))
+
+            # COLOR DETECTION / OBJECT DETECTION
+            match = self.stroke_color_re.search(pline)
+            if match:
+                color = [float(match.group(1)), float(match.group(2)), float(match.group(3))]
+                log.debug(
+                    "ToolPDF.parse_pdf() --> STROKE Color change on line: %s --> RED=%f GREEN=%f BLUE=%f" %
+                    (line_nr, color[0], color[1], color[2]))
+
+                if color[0] == old_color[0] and color[1] == old_color[1] and color[2] == old_color[2]:
+                    # same color, do nothing
+                    continue
+                else:
+                    object_dict[object_nr] = deepcopy(apertures_dict)
+                    object_nr += 1
+
+                    object_dict[object_nr] = dict()
+                    apertures_dict = {}
+                old_color = copy(color)
+                # we make sure that the following geometry is added to the right storage
+                flag_clear_geo = False
+                continue
+
+            # CLEAR GEOMETRY detection
+            match = self.fill_color_re.search(pline)
+            if match:
+                fill_color = [float(match.group(1)), float(match.group(2)), float(match.group(3))]
+                log.debug(
+                    "ToolPDF.parse_pdf() --> FILL Color change on line: %s --> RED=%f GREEN=%f BLUE=%f" %
+                    (line_nr, fill_color[0], fill_color[1], fill_color[2]))
+                # if the color is white we are seeing 'clear_geometry' that can't be seen. It may be that those
+                # geometries are actually holes from which we can make an Excellon file
+                if fill_color[0] == 1 and fill_color[1] == 1 and fill_color[2] == 1:
+                    flag_clear_geo = True
+                else:
+                    flag_clear_geo = False
+                continue
+
+            # TRANSFORMATIONS DETECTION #
+
+            # Detect combined transformation.
+            match = self.combined_transform_re.search(pline)
+            if match:
+                # detect save graphic stack event
+                # sometimes they combine save_to_graphics_stack with the transformation on the same line
+                if match.group(1) == 'q':
+                    log.debug(
+                        "ToolPDF.parse_pdf() --> Save to GS found on line: %s --> offset=[%f, %f] ||| scale=[%f, %f]" %
+                        (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1]))
+
+                    self.gs['transform'].append(deepcopy([offset_geo, scale_geo]))
+                    self.gs['line_width'].append(deepcopy(size))
+
+                # transformation = TRANSLATION (OFFSET)
+                if (float(match.group(3)) == 0 and float(match.group(4)) == 0) and \
+                        (float(match.group(6)) != 0 or float(match.group(7)) != 0):
+                    log.debug(
+                        "ToolPDF.parse_pdf() --> OFFSET transformation found on line: %s --> %s" % (line_nr, pline))
+
+                    offset_geo[0] += float(match.group(6))
+                    offset_geo[1] += float(match.group(7))
+                    # log.debug("Offset= [%f, %f]" % (offset_geo[0], offset_geo[1]))
+
+                # transformation = SCALING
+                if float(match.group(2)) != 1 and float(match.group(5)) != 1:
+                    log.debug(
+                        "ToolPDF.parse_pdf() --> SCALE transformation found on line: %s --> %s" % (line_nr, pline))
+
+                    scale_geo[0] *= float(match.group(2))
+                    scale_geo[1] *= float(match.group(5))
+                # log.debug("Scale= [%f, %f]" % (scale_geo[0], scale_geo[1]))
+
+                continue
+
+            # detect save graphic stack event
+            match = self.save_gs_re.search(pline)
+            if match:
+                log.debug(
+                    "ToolPDF.parse_pdf() --> Save to GS found on line: %s --> offset=[%f, %f] ||| scale=[%f, %f]" %
+                    (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1]))
+                self.gs['transform'].append(deepcopy([offset_geo, scale_geo]))
+                self.gs['line_width'].append(deepcopy(size))
+
+            # detect restore from graphic stack event
+            match = self.restore_gs_re.search(pline)
+            if match:
+                log.debug(
+                    "ToolPDF.parse_pdf() --> Restore from GS found on line: %s --> %s" % (line_nr, pline))
+                try:
+                    restored_transform = self.gs['transform'].pop(-1)
+                    offset_geo = restored_transform[0]
+                    scale_geo = restored_transform[1]
+                except IndexError:
+                    # nothing to remove
+                    log.debug("ToolPDF.parse_pdf() --> Nothing to restore")
+                    pass
+
+                try:
+                    size = self.gs['line_width'].pop(-1)
+                except IndexError:
+                    log.debug("ToolPDF.parse_pdf() --> Nothing to restore")
+                    # nothing to remove
+                    pass
+                # log.debug("Restored Offset= [%f, %f]" % (offset_geo[0], offset_geo[1]))
+                # log.debug("Restored Scale= [%f, %f]" % (scale_geo[0], scale_geo[1]))
+
+            # PATH CONSTRUCTION #
+
+            # Start SUBPATH
+            match = self.start_subpath_re.search(pline)
+            if match:
+                # we just started a subpath so we mark it as not closed yet
+                close_subpath = False
+
+                # init subpaths
+                subpath['lines'] = []
+                subpath['bezier'] = []
+                subpath['rectangle'] = []
+
+                # detect start point to move to
+                x = float(match.group(1)) + offset_geo[0]
+                y = float(match.group(2)) + offset_geo[1]
+                pt = (x * self.point_to_unit_factor * scale_geo[0],
+                      y * self.point_to_unit_factor * scale_geo[1])
+                start_point = pt
+
+                # add the start point to subpaths
+                subpath['lines'].append(start_point)
+                # subpath['bezier'].append(start_point)
+                # subpath['rectangle'].append(start_point)
+                current_point = start_point
+                continue
+
+            # Draw Line
+            match = self.draw_line_re.search(pline)
+            if match:
+                current_subpath = 'lines'
+                x = float(match.group(1)) + offset_geo[0]
+                y = float(match.group(2)) + offset_geo[1]
+                pt = (x * self.point_to_unit_factor * scale_geo[0],
+                      y * self.point_to_unit_factor * scale_geo[1])
+                subpath['lines'].append(pt)
+                current_point = pt
+                continue
+
+            # Draw Bezier 'c'
+            match = self.draw_arc_3pt_re.search(pline)
+            if match:
+                current_subpath = 'bezier'
+                start = current_point
+                x = float(match.group(1)) + offset_geo[0]
+                y = float(match.group(2)) + offset_geo[1]
+                c1 = (x * self.point_to_unit_factor * scale_geo[0],
+                      y * self.point_to_unit_factor * scale_geo[1])
+                x = float(match.group(3)) + offset_geo[0]
+                y = float(match.group(4)) + offset_geo[1]
+                c2 = (x * self.point_to_unit_factor * scale_geo[0],
+                      y * self.point_to_unit_factor * scale_geo[1])
+                x = float(match.group(5)) + offset_geo[0]
+                y = float(match.group(6)) + offset_geo[1]
+                stop = (x * self.point_to_unit_factor * scale_geo[0],
+                        y * self.point_to_unit_factor * scale_geo[1])
+
+                subpath['bezier'].append([start, c1, c2, stop])
+                current_point = stop
+                continue
+
+            # Draw Bezier 'v'
+            match = self.draw_arc_2pt_c1start_re.search(pline)
+            if match:
+                current_subpath = 'bezier'
+                start = current_point
+                x = float(match.group(1)) + offset_geo[0]
+                y = float(match.group(2)) + offset_geo[1]
+                c2 = (x * self.point_to_unit_factor * scale_geo[0],
+                      y * self.point_to_unit_factor * scale_geo[1])
+                x = float(match.group(3)) + offset_geo[0]
+                y = float(match.group(4)) + offset_geo[1]
+                stop = (x * self.point_to_unit_factor * scale_geo[0],
+                        y * self.point_to_unit_factor * scale_geo[1])
+
+                subpath['bezier'].append([start, start, c2, stop])
+                current_point = stop
+                continue
+
+            # Draw Bezier 'y'
+            match = self.draw_arc_2pt_c2stop_re.search(pline)
+            if match:
+                start = current_point
+                x = float(match.group(1)) + offset_geo[0]
+                y = float(match.group(2)) + offset_geo[1]
+                c1 = (x * self.point_to_unit_factor * scale_geo[0],
+                      y * self.point_to_unit_factor * scale_geo[1])
+                x = float(match.group(3)) + offset_geo[0]
+                y = float(match.group(4)) + offset_geo[1]
+                stop = (x * self.point_to_unit_factor * scale_geo[0],
+                        y * self.point_to_unit_factor * scale_geo[1])
+
+                subpath['bezier'].append([start, c1, stop, stop])
+                print(subpath['bezier'])
+                current_point = stop
+                continue
+
+            # Draw Rectangle 're'
+            match = self.rect_re.search(pline)
+            if match:
+                current_subpath = 'rectangle'
+                x = (float(match.group(1)) + offset_geo[0]) * self.point_to_unit_factor * scale_geo[0]
+                y = (float(match.group(2)) + offset_geo[1]) * self.point_to_unit_factor * scale_geo[1]
+                width = (float(match.group(3)) + offset_geo[0]) * \
+                        self.point_to_unit_factor * scale_geo[0]
+                height = (float(match.group(4)) + offset_geo[1]) * \
+                         self.point_to_unit_factor * scale_geo[1]
+                pt1 = (x, y)
+                pt2 = (x+width, y)
+                pt3 = (x+width, y+height)
+                pt4 = (x, y+height)
+                subpath['rectangle'] += [pt1, pt2, pt3, pt4, pt1]
+                current_point = pt1
+                continue
+
+            # Detect clipping path set
+            # ignore this and delete the current subpath
+            match = self.clip_path_re.search(pline)
+            if match:
+                subpath['lines'] = []
+                subpath['bezier'] = []
+                subpath['rectangle'] = []
+                # it measns that we've already added the subpath to path and we need to delete it
+                # clipping path is usually either rectangle or lines
+                if close_subpath is True:
+                    close_subpath = False
+                    if current_subpath == 'lines':
+                        path['lines'].pop(-1)
+                    if current_subpath == 'rectangle':
+                        path['rectangle'].pop(-1)
+                continue
+
+            # Close SUBPATH
+            match = self.end_subpath_re.search(pline)
+            if match:
+                close_subpath = True
+                if current_subpath == 'lines':
+                    subpath['lines'].append(start_point)
+                    # since we are closing the subpath add it to the path, a path may have chained subpaths
+                    path['lines'].append(copy(subpath['lines']))
+                    subpath['lines'] = []
+                elif current_subpath == 'bezier':
+                    # subpath['bezier'].append(start_point)
+                    # since we are closing the subpath add it to the path, a path may have chained subpaths
+                    path['bezier'].append(copy(subpath['bezier']))
+                    subpath['bezier'] = []
+                elif current_subpath == 'rectangle':
+                    # subpath['rectangle'].append(start_point)
+                    # since we are closing the subpath add it to the path, a path may have chained subpaths
+                    path['rectangle'].append(copy(subpath['rectangle']))
+                    subpath['rectangle'] = []
+                continue
+
+            # PATH PAINTING #
+
+            # Detect Stroke width / aperture
+            match = self.strokewidth_re.search(pline)
+            if match:
+                size = float(match.group(1))
+                continue
+
+            # Detect No_Op command, ignore the current subpath
+            match = self.no_op_re.search(pline)
+            if match:
+                subpath['lines'] = []
+                subpath['bezier'] = []
+                subpath['rectangle'] = []
+                continue
+
+            # Stroke the path
+            match = self.stroke_path__re.search(pline)
+            if match:
+                # scale the size here; some PDF printers apply transformation after the size is declared
+                applied_size = size * scale_geo[0] * self.point_to_unit_factor
+
+                path_geo = list()
+                if current_subpath == 'lines':
+                    if path['lines']:
+                        for subp in path['lines']:
+                            geo = copy(subp)
+                            geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
+                            path_geo.append(geo)
+                        # the path was painted therefore initialize it
+                        path['lines'] = []
+                    else:
+                        geo = copy(subpath['lines'])
+                        geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
+                        path_geo.append(geo)
+                        subpath['lines'] = []
+
+                if current_subpath == 'bezier':
+                    if path['bezier']:
+                        for subp in path['bezier']:
+                            geo = []
+                            for b in subp:
+                                geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
+                            geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
+                            path_geo.append(geo)
+                        # the path was painted therefore initialize it
+                        path['bezier'] = []
+                    else:
+                        geo = []
+                        for b in subpath['bezier']:
+                            geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
+                        geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
+                        path_geo.append(geo)
+                        subpath['bezier'] = []
+
+                if current_subpath == 'rectangle':
+                    if path['rectangle']:
+                        for subp in path['rectangle']:
+                            geo = copy(subp)
+                            geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
+                            path_geo.append(geo)
+                        # the path was painted therefore initialize it
+                        path['rectangle'] = []
+                    else:
+                        geo = copy(subpath['rectangle'])
+                        geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
+                        path_geo.append(geo)
+                        subpath['rectangle'] = []
+
+                # store the found geometry
+                found_aperture = None
+                if apertures_dict:
+                    for apid in apertures_dict:
+                        # if we already have an aperture with the current size (rounded to 5 decimals)
+                        if apertures_dict[apid]['size'] == round(applied_size, 5):
+                            found_aperture = apid
+                            break
+
+                    if found_aperture:
+                        apertures_dict[copy(found_aperture)]['solid_geometry'] += path_geo
+                        found_aperture = None
+                    else:
+                        if str(aperture) in apertures_dict.keys():
+                            aperture += 1
+                        apertures_dict[str(aperture)] = {}
+                        apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
+                        apertures_dict[str(aperture)]['type'] = 'C'
+                        apertures_dict[str(aperture)]['solid_geometry'] = []
+                        apertures_dict[str(aperture)]['solid_geometry'] += path_geo
+                else:
+                    apertures_dict[str(aperture)] = {}
+                    apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
+                    apertures_dict[str(aperture)]['type'] = 'C'
+                    apertures_dict[str(aperture)]['solid_geometry'] = []
+                    apertures_dict[str(aperture)]['solid_geometry'] += path_geo
+
+                continue
+
+            # Fill the path
+            match = self.fill_path_re.search(pline)
+            if match:
+                # scale the size here; some PDF printers apply transformation after the size is declared
+                applied_size = size * scale_geo[0] * self.point_to_unit_factor
+                path_geo = list()
+
+                if current_subpath == 'lines':
+                    if path['lines']:
+                        for subp in path['lines']:
+                            geo = copy(subp)
+                            # close the subpath if it was not closed already
+                            if close_subpath is False:
+                                geo.append(geo[0])
+                            geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
+                            path_geo.append(geo_el)
+                        # the path was painted therefore initialize it
+                        path['lines'] = []
+                    else:
+                        geo = copy(subpath['lines'])
+                        # close the subpath if it was not closed already
+                        if close_subpath is False:
+                            geo.append(start_point)
+                        geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
+                        path_geo.append(geo_el)
+                        subpath['lines'] = []
+
+                if current_subpath == 'bezier':
+                    geo = []
+                    if path['bezier']:
+                        for subp in path['bezier']:
+                            for b in subp:
+                                geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
+                                # close the subpath if it was not closed already
+                                if close_subpath is False:
+                                    geo.append(geo[0])
+                                geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
+                                path_geo.append(geo_el)
+                        # the path was painted therefore initialize it
+                        path['bezier'] = []
+                    else:
+                        for b in subpath['bezier']:
+                            geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
+                        if close_subpath is False:
+                            geo.append(start_point)
+                        geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
+                        path_geo.append(geo_el)
+                        subpath['bezier'] = []
+
+                if current_subpath == 'rectangle':
+                    if path['rectangle']:
+                        for subp in path['rectangle']:
+                            geo = copy(subp)
+                            # close the subpath if it was not closed already
+                            if close_subpath is False and start_point is not None:
+                                geo.append(start_point)
+                            geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
+                            path_geo.append(geo_el)
+                        # the path was painted therefore initialize it
+                        path['rectangle'] = []
+                    else:
+                        geo = copy(subpath['rectangle'])
+                        # close the subpath if it was not closed already
+                        if close_subpath is False and start_point is not None:
+                            geo.append(start_point)
+                        geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
+                        path_geo.append(geo_el)
+                        subpath['rectangle'] = []
+
+                # we finished painting and also closed the path if it was the case
+                close_subpath = True
+
+                # if there was a fill color change we look for circular geometries from which we can make drill holes
+                # for the Excellon file
+                if flag_clear_geo is True:
+                    # we llok for circular geometries
+                    if current_subpath == 'bezier':
+                        # if there are geometries in the list
+                        if path_geo:
+                            clear_apertures_dict['0']['solid_geometry'] += path_geo
+                else:
+                    # else, add the geometry as usual
+                    try:
+                        apertures_dict['0']['solid_geometry'] += path_geo
+                    except KeyError:
+                        # in case there is no stroke width yet therefore no aperture
+                        apertures_dict['0'] = {}
+                        apertures_dict['0']['size'] = applied_size
+                        apertures_dict['0']['type'] = 'C'
+                        apertures_dict['0']['solid_geometry'] = []
+                        apertures_dict['0']['solid_geometry'] += path_geo
+                    continue
+
+            # Fill and Stroke the path
+            match = self.fill_stroke_path_re.search(pline)
+            if match:
+                # scale the size here; some PDF printers apply transformation after the size is declared
+                applied_size = size * scale_geo[0] * self.point_to_unit_factor
+                path_geo = list()
+                fill_geo = list()
+
+                if current_subpath == 'lines':
+                    if path['lines']:
+                        # fill
+                        for subp in path['lines']:
+                            geo = copy(subp)
+                            # close the subpath if it was not closed already
+                            if close_subpath is False:
+                                geo.append(geo[0])
+                            geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
+                            fill_geo.append(geo_el)
+                        # stroke
+                        for subp in path['lines']:
+                            geo = copy(subp)
+                            geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
+                            path_geo.append(geo)
+                        # the path was painted therefore initialize it
+                        path['lines'] = []
+                    else:
+                        # fill
+                        geo = copy(subpath['lines'])
+                        # close the subpath if it was not closed already
+                        if close_subpath is False:
+                            geo.append(start_point)
+                        geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
+                        fill_geo.append(geo_el)
+                        # stroke
+                        geo = copy(subpath['lines'])
+                        geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
+                        path_geo.append(geo)
+                        subpath['lines'] = []
+                        subpath['lines'] = []
+
+                if current_subpath == 'bezier':
+                    geo = []
+                    if path['bezier']:
+                        # fill
+                        for subp in path['bezier']:
+                            for b in subp:
+                                geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
+                                # close the subpath if it was not closed already
+                                if close_subpath is False:
+                                    geo.append(geo[0])
+                                geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
+                                fill_geo.append(geo_el)
+                        # stroke
+                        for subp in path['bezier']:
+                            geo = []
+                            for b in subp:
+                                geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
+                            geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
+                            path_geo.append(geo)
+                        # the path was painted therefore initialize it
+                        path['bezier'] = []
+                    else:
+                        # fill
+                        for b in subpath['bezier']:
+                            geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
+                        if close_subpath is False:
+                            geo.append(start_point)
+                        geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
+                        fill_geo.append(geo_el)
+                        # stroke
+                        geo = []
+                        for b in subpath['bezier']:
+                            geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
+                        geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
+                        path_geo.append(geo)
+                        subpath['bezier'] = []
+
+                if current_subpath == 'rectangle':
+                    if path['rectangle']:
+                        # fill
+                        for subp in path['rectangle']:
+                            geo = copy(subp)
+                            # close the subpath if it was not closed already
+                            if close_subpath is False:
+                                geo.append(geo[0])
+                            geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
+                            fill_geo.append(geo_el)
+                        # stroke
+                        for subp in path['rectangle']:
+                            geo = copy(subp)
+                            geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
+                            path_geo.append(geo)
+                        # the path was painted therefore initialize it
+                        path['rectangle'] = []
+                    else:
+                        # fill
+                        geo = copy(subpath['rectangle'])
+                        # close the subpath if it was not closed already
+                        if close_subpath is False:
+                            geo.append(start_point)
+                        geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
+                        fill_geo.append(geo_el)
+                        # stroke
+                        geo = copy(subpath['rectangle'])
+                        geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
+                        path_geo.append(geo)
+                        subpath['rectangle'] = []
+
+                # we finished painting and also closed the path if it was the case
+                close_subpath = True
+
+                # store the found geometry for stroking the path
+                found_aperture = None
+                if apertures_dict:
+                    for apid in apertures_dict:
+                        # if we already have an aperture with the current size (rounded to 5 decimals)
+                        if apertures_dict[apid]['size'] == round(applied_size, 5):
+                            found_aperture = apid
+                            break
+
+                    if found_aperture:
+                        apertures_dict[copy(found_aperture)]['solid_geometry'] += path_geo
+                        found_aperture = None
+                    else:
+                        if str(aperture) in apertures_dict.keys():
+                            aperture += 1
+                        apertures_dict[str(aperture)] = {}
+                        apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
+                        apertures_dict[str(aperture)]['type'] = 'C'
+                        apertures_dict[str(aperture)]['solid_geometry'] = []
+                        apertures_dict[str(aperture)]['solid_geometry'] += path_geo
+                else:
+                    apertures_dict[str(aperture)] = {}
+                    apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
+                    apertures_dict[str(aperture)]['type'] = 'C'
+                    apertures_dict[str(aperture)]['solid_geometry'] = []
+                    apertures_dict[str(aperture)]['solid_geometry'] += path_geo
+
+                # store the found geometry for filling the path
+                try:
+                    apertures_dict['0']['solid_geometry'] += fill_geo
+                except KeyError:
+                    # in case there is no stroke width yet therefore no aperture
+                    apertures_dict['0'] = {}
+                    apertures_dict['0']['size'] = round(applied_size, 5)
+                    apertures_dict['0']['type'] = 'C'
+                    apertures_dict['0']['solid_geometry'] = []
+                    apertures_dict['0']['solid_geometry'] += fill_geo
+
+                continue
+
+        # tidy up. copy the current aperture dict to the object dict but only if it is not empty
+        if apertures_dict:
+            object_dict[object_nr] = deepcopy(apertures_dict)
+
+        if clear_apertures_dict['0']['solid_geometry']:
+            object_dict[0] = deepcopy(clear_apertures_dict)
+
+        return object_dict
+
+    def bezier_to_points(self, start, c1, c2, stop):
+        """
+        # Equation Bezier, page 184 PDF 1.4 reference
+        # https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/pdf_reference_archives/PDFReference.pdf
+        # Given the coordinates of the four points, the curve is generated by varying the parameter t from 0.0 to 1.0
+        # in the following equation:
+        # R(t) = P0*(1 - t) ** 3 + P1*3*t*(1 - t) ** 2 + P2 * 3*(1 - t) * t ** 2  + P3*t ** 3
+        # When t = 0.0, the value from the function coincides with the current point P0; when t = 1.0, R(t) coincides
+        # with the final point P3. Intermediate values of t generate intermediate points along the curve.
+        # The curve does not, in general, pass through the two control points P1 and P2
+
+        :return: LineString geometry
+        """
+
+        # here we store the geometric points
+        points = []
+
+        nr_points = np.arange(0.0, 1.0, (1 / self.step_per_circles))
+        for t in nr_points:
+            term_p0 = (1 - t) ** 3
+            term_p1 = 3 * t * (1 - t) ** 2
+            term_p2 = 3 * (1 - t) * t ** 2
+            term_p3 = t ** 3
+
+            x = start[0] * term_p0 + c1[0] * term_p1 + c2[0] * term_p2 + stop[0] * term_p3
+            y = start[1] * term_p0 + c1[1] * term_p1 + c2[1] * term_p2 + stop[1] * term_p3
+            points.append([x, y])
+
+        return points
+
+    # def bezier_to_circle(self, path):
+    #     lst = []
+    #     for el in range(len(path)):
+    #         if type(path) is list:
+    #             for coord in path[el]:
+    #                 lst.append(coord)
+    #         else:
+    #             lst.append(el)
+    #
+    #     if lst:
+    #         minx = min(lst, key=lambda t: t[0])[0]
+    #         miny = min(lst, key=lambda t: t[1])[1]
+    #         maxx = max(lst, key=lambda t: t[0])[0]
+    #         maxy = max(lst, key=lambda t: t[1])[1]
+    #         center = (maxx-minx, maxy-miny)
+    #         radius = (maxx-minx) / 2
+    #         return [center, radius]
+    #
+    # def circle_to_points(self, center, radius):
+    #     geo = Point(center).buffer(radius, resolution=self.step_per_circles)
+    #     return LineString(list(geo.exterior.coords))
+    #

+ 2 - 1
flatcamTools/ToolPaint.py

@@ -118,7 +118,7 @@ class ToolPaint(FlatCAMTool, Gerber):
         self.addtool_entry_lbl.setToolTip(
         self.addtool_entry_lbl.setToolTip(
             _("Diameter for the new tool.")
             _("Diameter for the new tool.")
         )
         )
-        self.addtool_entry = FCEntry()
+        self.addtool_entry = FCEntry2()
 
 
         # hlay.addWidget(self.addtool_label)
         # hlay.addWidget(self.addtool_label)
         # hlay.addStretch()
         # hlay.addStretch()
@@ -307,6 +307,7 @@ class ToolPaint(FlatCAMTool, Gerber):
 
 
         ## Signals
         ## Signals
         self.addtool_btn.clicked.connect(self.on_tool_add)
         self.addtool_btn.clicked.connect(self.on_tool_add)
+        self.addtool_entry.returnPressed.connect(self.on_tool_add)
         # self.copytool_btn.clicked.connect(lambda: self.on_tool_copy())
         # self.copytool_btn.clicked.connect(lambda: self.on_tool_copy())
         self.tools_table.itemChanged.connect(self.on_tool_edit)
         self.tools_table.itemChanged.connect(self.on_tool_edit)
         self.deltool_btn.clicked.connect(self.on_tool_delete)
         self.deltool_btn.clicked.connect(self.on_tool_delete)

+ 466 - 0
flatcamTools/ToolPcbWizard.py

@@ -0,0 +1,466 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Date: 4/15/2019                                          #
+# MIT Licence                                              #
+############################################################
+
+from FlatCAMTool import FlatCAMTool
+
+from flatcamGUI.GUIElements import RadioSet, FCComboBox, FCSpinner, FCButton, FCTable
+from PyQt5 import QtGui, QtWidgets, QtCore
+from PyQt5.QtCore import pyqtSignal
+import re
+import os
+from datetime import datetime
+from io import StringIO
+
+import gettext
+import FlatCAMTranslation as fcTranslate
+
+fcTranslate.apply_language('strings')
+import builtins
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class PcbWizard(FlatCAMTool):
+
+    file_loaded = pyqtSignal(str, str)
+
+    toolName = _("PcbWizard Import Tool")
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+
+        self.app = app
+
+        # Title
+        title_label = QtWidgets.QLabel("%s" % _('Import 2-file Excellon'))
+        title_label.setStyleSheet("""
+                        QLabel
+                        {
+                            font-size: 16px;
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(title_label)
+
+        self.layout.addWidget(QtWidgets.QLabel(""))
+        self.layout.addWidget(QtWidgets.QLabel("<b>Load files:</b>"))
+
+        # Form Layout
+        form_layout = QtWidgets.QFormLayout()
+        self.layout.addLayout(form_layout)
+
+        self.excellon_label = QtWidgets.QLabel(_("Excellon file:"))
+        self.excellon_label.setToolTip(
+           _( "Load the Excellon file.\n"
+              "Usually it has a .DRL extension")
+
+        )
+        self.excellon_brn = FCButton(_("Open"))
+        form_layout.addRow(self.excellon_label, self.excellon_brn)
+
+        self.inf_label = QtWidgets.QLabel(_("INF file:"))
+        self.inf_label.setToolTip(
+            _("Load the INF file.")
+
+        )
+        self.inf_btn = FCButton(_("Open"))
+        form_layout.addRow(self.inf_label, self.inf_btn)
+
+        self.tools_table = FCTable()
+        self.layout.addWidget(self.tools_table)
+
+        self.tools_table.setColumnCount(2)
+        self.tools_table.setHorizontalHeaderLabels(['#Tool', _('Diameter')])
+
+        self.tools_table.horizontalHeaderItem(0).setToolTip(
+            _("Tool Number"))
+        self.tools_table.horizontalHeaderItem(1).setToolTip(
+            _("Tool diameter in file units."))
+
+        # start with apertures table hidden
+        self.tools_table.setVisible(False)
+
+        self.layout.addWidget(QtWidgets.QLabel(""))
+        self.layout.addWidget(QtWidgets.QLabel("<b>Excellon format:</b>"))
+        # Form Layout
+        form_layout1 = QtWidgets.QFormLayout()
+        self.layout.addLayout(form_layout1)
+
+        # Integral part of the coordinates
+        self.int_entry = FCSpinner()
+        self.int_entry.set_range(1, 10)
+        self.int_label = QtWidgets.QLabel(_("Int. digits:"))
+        self.int_label.setToolTip(
+           _( "The number of digits for the integral part of the coordinates.")
+        )
+        form_layout1.addRow(self.int_label, self.int_entry)
+
+        # Fractional part of the coordinates
+        self.frac_entry = FCSpinner()
+        self.frac_entry.set_range(1, 10)
+        self.frac_label = QtWidgets.QLabel(_("Frac. digits:"))
+        self.frac_label.setToolTip(
+            _("The number of digits for the fractional part of the coordinates.")
+        )
+        form_layout1.addRow(self.frac_label, self.frac_entry)
+
+        # Zeros suppression for coordinates
+        self.zeros_radio = RadioSet([{'label': 'LZ', 'value': 'LZ'},
+                                     {'label': 'TZ', 'value': 'TZ'},
+                                     {'label': 'No Suppression', 'value': 'D'}])
+        self.zeros_label = QtWidgets.QLabel(_("Zeros supp.:"))
+        self.zeros_label.setToolTip(
+            _("The type of zeros suppression used.\n"
+              "Can be of type:\n"
+              "- LZ = leading zeros are kept\n"
+              "- TZ = trailing zeros are kept\n"
+              "- No Suppression = no zero suppression")
+        )
+        form_layout1.addRow(self.zeros_label, self.zeros_radio)
+
+        # Units type
+        self.units_radio = RadioSet([{'label': 'INCH', 'value': 'INCH'},
+                                    {'label': 'MM', 'value': 'METRIC'}])
+        self.units_label = QtWidgets.QLabel("<b>%s:</b>" % _('Units'))
+        self.units_label.setToolTip(
+            _("The type of units that the coordinates and tool\n"
+              "diameters are using. Can be INCH or MM.")
+        )
+        form_layout1.addRow(self.units_label, self.units_radio)
+
+        # Buttons
+
+        self.import_button = QtWidgets.QPushButton(_("Import Excellon"))
+        self.import_button.setToolTip(
+            _("Import in FlatCAM an Excellon file\n"
+              "that store it's information's in 2 files.\n"
+              "One usually has .DRL extension while\n"
+              "the other has .INF extension.")
+        )
+        self.layout.addWidget(self.import_button)
+
+        self.layout.addStretch()
+
+        self.excellon_loaded = False
+        self.inf_loaded = False
+        self.process_finished = False
+
+        self.modified_excellon_file = ''
+
+        ## Signals
+        self.excellon_brn.clicked.connect(self.on_load_excellon_click)
+        self.inf_btn.clicked.connect(self.on_load_inf_click)
+        self.import_button.clicked.connect(lambda: self.on_import_excellon(
+            excellon_fileobj=self.modified_excellon_file))
+
+        self.file_loaded.connect(self.on_file_loaded)
+        self.units_radio.activated_custom.connect(self.on_units_change)
+
+        self.units = 'INCH'
+        self.zeros = 'LZ'
+        self.integral = 2
+        self.fractional = 4
+
+        self.outname = 'file'
+
+        self.exc_file_content = None
+        self.tools_from_inf = {}
+
+    def run(self, toggle=False):
+        self.app.report_usage("PcbWizard Tool()")
+
+        if toggle:
+            # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+            else:
+                try:
+                    if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
+                        self.app.ui.splitter.setSizes([0, 1])
+                except AttributeError:
+                    pass
+        else:
+            if self.app.ui.splitter.sizes()[0] == 0:
+                self.app.ui.splitter.setSizes([1, 1])
+
+        FlatCAMTool.run(self)
+        self.set_tool_ui()
+
+        self.app.ui.notebook.setTabText(2, _("PCBWizard Tool"))
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, **kwargs)
+
+    def set_tool_ui(self):
+        self.units = 'INCH'
+        self.zeros = 'LZ'
+        self.integral = 2
+        self.fractional = 4
+
+        self.outname = 'file'
+
+        self.exc_file_content = None
+        self.tools_from_inf = {}
+
+        ## Initialize form
+        self.int_entry.set_value(self.integral)
+        self.frac_entry.set_value(self.fractional)
+        self.zeros_radio.set_value(self.zeros)
+        self.units_radio.set_value(self.units)
+
+        self.excellon_loaded = False
+        self.inf_loaded = False
+        self.process_finished = False
+        self.modified_excellon_file = ''
+
+        self.build_ui()
+
+    def build_ui(self):
+        sorted_tools = []
+
+        if not self.tools_from_inf:
+            self.tools_table.setVisible(False)
+        else:
+            sort = []
+            for k, v in list(self.tools_from_inf.items()):
+                sort.append(int(k))
+            sorted_tools = sorted(sort)
+            n = len(sorted_tools)
+            self.tools_table.setRowCount(n)
+
+        tool_row = 0
+        for tool in sorted_tools:
+            tool_id_item = QtWidgets.QTableWidgetItem('%d' % int(tool))
+            tool_id_item.setFlags(QtCore.Qt.ItemIsEnabled)
+            self.tools_table.setItem(tool_row, 0, tool_id_item)  # Tool name/id
+
+            tool_dia_item = QtWidgets.QTableWidgetItem(str(self.tools_from_inf[tool]))
+            tool_dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
+            self.tools_table.setItem(tool_row, 1, tool_dia_item)
+            tool_row += 1
+
+        self.tools_table.resizeColumnsToContents()
+        self.tools_table.resizeRowsToContents()
+
+        vertical_header = self.tools_table.verticalHeader()
+        vertical_header.hide()
+        self.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header = self.tools_table.horizontalHeader()
+        # horizontal_header.setMinimumSectionSize(10)
+        # horizontal_header.setDefaultSectionSize(70)
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+
+        self.tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+        self.tools_table.setSortingEnabled(False)
+        self.tools_table.setMinimumHeight(self.tools_table.getHeight())
+        self.tools_table.setMaximumHeight(self.tools_table.getHeight())
+
+    def update_params(self):
+        self.units = self.units_radio.get_value()
+        self.zeros = self.zeros_radio.get_value()
+        self.integral = self.int_entry.get_value()
+        self.fractional = self.frac_entry.get_value()
+
+    def on_units_change(self, val):
+        if val == 'INCH':
+            self.int_entry.set_value(2)
+            self.frac_entry.set_value(4)
+        else:
+            self.int_entry.set_value(3)
+            self.frac_entry.set_value(3)
+
+    def on_load_excellon_click(self):
+        """
+
+        :return: None
+        """
+        self.app.log.debug("on_load_excellon_click()")
+
+        filter = "Excellon Files(*.DRL *.DRD *.TXT);;All Files (*.*)"
+        try:
+            filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Load PcbWizard Excellon file"),
+                                                                 directory=self.app.get_last_folder(),
+                                                                 filter=filter)
+        except TypeError:
+            filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Load PcbWizard Excellon file"),
+                                                                 filter=filter)
+
+        filename = str(filename)
+
+
+        if filename == "":
+            self.app.inform.emit(_("Open cancelled."))
+        else:
+            self.app.worker_task.emit({'fcn': self.load_excellon, 'params': [filename]})
+
+    def on_load_inf_click(self):
+        """
+
+                :return: None
+                """
+        self.app.log.debug("on_load_inf_click()")
+
+        filter = "INF Files(*.INF);;All Files (*.*)"
+        try:
+            filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Load PcbWizard INF file"),
+                                                                 directory=self.app.get_last_folder(),
+                                                                 filter=filter)
+        except TypeError:
+            filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Load PcbWizard INF file"),
+                                                                 filter=filter)
+
+        filename = str(filename)
+
+        if filename == "":
+            self.app.inform.emit(_("Open cancelled."))
+        else:
+            self.app.worker_task.emit({'fcn': self.load_inf, 'params': [filename]})
+
+    def load_inf(self, filename):
+        self.app.log.debug("ToolPcbWizard.load_inf()")
+
+        with open(filename, 'r') as inf_f:
+            inf_file_content = inf_f.readlines()
+
+        tool_re = re.compile(r'^T(\d+)\s+(\d*\.?\d+)$')
+        format_re = re.compile(r'^(\d+)\.?(\d+)\s*format,\s*(inches|metric)?,\s*(absolute|incremental)?.*$')
+
+        for eline in inf_file_content:
+            # Cleanup lines
+            eline = eline.strip(' \r\n')
+
+            match = tool_re.search(eline)
+            if match:
+                tool =int( match.group(1))
+                dia = float(match.group(2))
+                # if dia < 0.1:
+                #     # most likely the file is in INCH
+                #     self.units_radio.set_value('INCH')
+
+                self.tools_from_inf[tool] = dia
+                continue
+            match = format_re.search(eline)
+            if match:
+                self.integral = int(match.group(1))
+                self.fractional = int(match.group(2))
+                units = match.group(3)
+                if units == 'inches':
+                    self.units = 'INCH'
+                else:
+                    self.units = 'METRIC'
+                self.units_radio.set_value(self.units)
+                self.int_entry.set_value(self.integral)
+                self.frac_entry.set_value(self.fractional)
+
+        if not self.tools_from_inf:
+            self.app.inform.emit(_("[ERROR] The INF file does not contain the tool table.\n"
+                                   "Try to open the Excellon file from File -> Open -> Excellon\n"
+                                   "and edit the drill diameters manually."))
+            return "fail"
+
+        self.file_loaded.emit('inf', filename)
+
+    def load_excellon(self, filename):
+        with open(filename, 'r') as exc_f:
+            self.exc_file_content = exc_f.readlines()
+
+        self.file_loaded.emit("excellon", filename)
+
+    def on_file_loaded(self, signal, filename):
+        self.build_ui()
+        time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
+
+        if signal == 'inf':
+            self.inf_loaded = True
+            self.tools_table.setVisible(True)
+            self.app.inform.emit(_("[success] PcbWizard .INF file loaded."))
+        elif signal == 'excellon':
+            self.excellon_loaded = True
+            self.outname = os.path.split(str(filename))[1]
+            self.app.inform.emit(_("[success] Main PcbWizard Excellon file loaded."))
+
+        if self.excellon_loaded and self.inf_loaded:
+            self.update_params()
+            excellon_string = ''
+            for line in self.exc_file_content:
+                excellon_string += line
+                if 'M48' in line:
+                    header = ';EXCELLON RE-GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s\n' % \
+                              (str(self.app.version), str(self.app.version_date))
+                    header += ';Created on : %s' % time_str + '\n'
+                    header += ';FILE_FORMAT={integral}:{fractional}\n'.format(integral=self.integral,
+                                                                               fractional=self.fractional)
+                    header += '{units},{zeros}\n'.format(units=self.units, zeros=self.zeros)
+                    for k, v in self.tools_from_inf.items():
+                        header += 'T{tool}C{dia}\n'.format(tool=int(k), dia=float(v))
+                    excellon_string += header
+            self.modified_excellon_file = StringIO(excellon_string)
+            self.process_finished = True
+
+        # Register recent file
+        self.app.defaults["global_last_folder"] = os.path.split(str(filename))[0]
+
+    def on_import_excellon(self, signal=None, excellon_fileobj=None):
+        self.app.log.debug("import_2files_excellon()")
+
+        # How the object should be initialized
+        def obj_init(excellon_obj, app_obj):
+            # self.progress.emit(20)
+
+            try:
+                ret = excellon_obj.parse_file(file_obj=excellon_fileobj)
+                if ret == "fail":
+                    app_obj.log.debug("Excellon parsing failed.")
+                    app_obj.inform.emit(_("[ERROR_NOTCL] This is not Excellon file."))
+                    return "fail"
+            except IOError:
+                app_obj.inform.emit(_("[ERROR_NOTCL] Cannot parse file: %s") % self.outname)
+                app_obj.log.debug("Could not import Excellon object.")
+                app_obj.progress.emit(0)
+                return "fail"
+            except:
+                msg = _("[ERROR_NOTCL] An internal error has occurred. See shell.\n")
+                msg += app_obj.traceback.format_exc()
+                app_obj.inform.emit(msg)
+                return "fail"
+
+            ret = excellon_obj.create_geometry()
+            if ret == 'fail':
+                app_obj.log.debug("Could not create geometry for Excellon object.")
+                return "fail"
+            app_obj.progress.emit(100)
+            for tool in excellon_obj.tools:
+                if excellon_obj.tools[tool]['solid_geometry']:
+                    return
+            app_obj.inform.emit(_("[ERROR_NOTCL] No geometry found in file: %s") % name)
+            return "fail"
+
+        if excellon_fileobj is not None and excellon_fileobj != '':
+            if self.process_finished:
+                with self.app.proc_container.new(_("Importing Excellon.")):
+
+                    # Object name
+                    name = self.outname
+
+                    ret = self.app.new_object("excellon", name, obj_init, autoselected=False)
+                    if ret == 'fail':
+                        self.app.inform.emit(_('[ERROR_NOTCL] Import Excellon file failed.'))
+                        return
+
+                        # Register recent file
+                    self.app.file_opened.emit("excellon", name)
+
+                    # GUI feedback
+                    self.app.inform.emit(_("[success] Imported: %s") % name)
+                    self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
+            else:
+                self.app.inform.emit(_('[WARNING_NOTCL] Excellon merging is in progress. Please wait...'))
+        else:
+            self.app.inform.emit(_('[ERROR_NOTCL] The imported Excellon file is None.'))

+ 3 - 2
flatcamTools/ToolSolderPaste.py

@@ -8,7 +8,7 @@
 
 
 from FlatCAMTool import FlatCAMTool
 from FlatCAMTool import FlatCAMTool
 from FlatCAMCommon import LoudDict
 from FlatCAMCommon import LoudDict
-from flatcamGUI.GUIElements import FCComboBox, FCEntry, FCTable
+from flatcamGUI.GUIElements import FCComboBox, FCEntry, FCEntry2, FCTable
 from FlatCAMApp import log
 from FlatCAMApp import log
 from camlib import distance
 from camlib import distance
 from FlatCAMObj import FlatCAMCNCjob
 from FlatCAMObj import FlatCAMCNCjob
@@ -102,7 +102,7 @@ class SolderPaste(FlatCAMTool):
         self.addtool_entry_lbl.setToolTip(
         self.addtool_entry_lbl.setToolTip(
             _("Diameter for the new Nozzle tool to add in the Tool Table")
             _("Diameter for the new Nozzle tool to add in the Tool Table")
         )
         )
-        self.addtool_entry = FCEntry()
+        self.addtool_entry = FCEntry2()
 
 
         # hlay.addWidget(self.addtool_label)
         # hlay.addWidget(self.addtool_label)
         # hlay.addStretch()
         # hlay.addStretch()
@@ -415,6 +415,7 @@ class SolderPaste(FlatCAMTool):
         ## Signals
         ## Signals
         self.combo_context_del_action.triggered.connect(self.on_delete_object)
         self.combo_context_del_action.triggered.connect(self.on_delete_object)
         self.addtool_btn.clicked.connect(self.on_tool_add)
         self.addtool_btn.clicked.connect(self.on_tool_add)
+        self.addtool_entry.returnPressed.connect(self.on_tool_add)
         self.deltool_btn.clicked.connect(self.on_tool_delete)
         self.deltool_btn.clicked.connect(self.on_tool_delete)
         self.soldergeo_btn.clicked.connect(self.on_create_geo_click)
         self.soldergeo_btn.clicked.connect(self.on_create_geo_click)
         self.solder_gcode_btn.clicked.connect(self.on_create_gcode_click)
         self.solder_gcode_btn.clicked.connect(self.on_create_gcode_click)

+ 2 - 0
flatcamTools/__init__.py

@@ -14,5 +14,7 @@ from flatcamTools.ToolPaint import ToolPaint
 from flatcamTools.ToolNonCopperClear import NonCopperClear
 from flatcamTools.ToolNonCopperClear import NonCopperClear
 from flatcamTools.ToolTransform import ToolTransform
 from flatcamTools.ToolTransform import ToolTransform
 from flatcamTools.ToolSolderPaste import SolderPaste
 from flatcamTools.ToolSolderPaste import SolderPaste
+from flatcamTools.ToolPcbWizard import PcbWizard
+from flatcamTools.ToolPDF import ToolPDF
 
 
 from flatcamTools.ToolShell import FCShell
 from flatcamTools.ToolShell import FCShell

BIN
locale/de/LC_MESSAGES/strings.mo


Разница между файлами не показана из-за своего большого размера
+ 210 - 201
locale/de/LC_MESSAGES/strings.po


BIN
locale/en/LC_MESSAGES/strings.mo


Разница между файлами не показана из-за своего большого размера
+ 209 - 201
locale/en/LC_MESSAGES/strings.po


BIN
locale/ro/LC_MESSAGES/strings.mo


Разница между файлами не показана из-за своего большого размера
+ 214 - 205
locale/ro/LC_MESSAGES/strings.po


Разница между файлами не показана из-за своего большого размера
+ 209 - 200
locale_template/strings.pot


BIN
share/aero.png


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_text.png


BIN
share/disc32.png


BIN
share/pdf32.png


BIN
share/poligonize32.png


BIN
share/semidisc32.png


Некоторые файлы не были показаны из-за большого количества измененных файлов