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

Merged in Beta (pull request #4)

Beta
Marius Stanciu 6 лет назад
Родитель
Сommit
c9af9008fe

+ 248 - 48
FlatCAMApp.py

@@ -141,7 +141,7 @@ class App(QtCore.QObject):
     # ################## Version and VERSION DATE ##############################
     # ################## Version and VERSION DATE ##############################
     # ##########################################################################
     # ##########################################################################
     version = 8.992
     version = 8.992
-    version_date = "2020/01/02"
+    version_date = "2020/01/20"
     beta = True
     beta = True
     engine = '3D'
     engine = '3D'
 
 
@@ -240,6 +240,9 @@ class App(QtCore.QObject):
     # signal emitted when jumping
     # signal emitted when jumping
     jump_signal = pyqtSignal(tuple)
     jump_signal = pyqtSignal(tuple)
 
 
+    # signal emitted when jumping
+    locate_signal = pyqtSignal(tuple, str)
+
     # close app signal
     # close app signal
     close_app_signal = pyqtSignal()
     close_app_signal = pyqtSignal()
 
 
@@ -429,6 +432,7 @@ class App(QtCore.QObject):
             "global_stats": dict(),
             "global_stats": dict(),
             "global_tabs_detachable": True,
             "global_tabs_detachable": True,
             "global_jump_ref": 'abs',
             "global_jump_ref": 'abs',
+            "global_locate_pt": 'bl',
             "global_tpdf_tmargin": 15.0,
             "global_tpdf_tmargin": 15.0,
             "global_tpdf_bmargin": 10.0,
             "global_tpdf_bmargin": 10.0,
             "global_tpdf_lmargin": 20.0,
             "global_tpdf_lmargin": 20.0,
@@ -524,8 +528,8 @@ class App(QtCore.QObject):
             "global_cursor_type": "small",
             "global_cursor_type": "small",
             "global_cursor_size": 20,
             "global_cursor_size": 20,
             "global_cursor_width": 2,
             "global_cursor_width": 2,
-            "global_cursor_color": '#000000',
-            "global_cursor_color_enabled": False,
+            "global_cursor_color": '#FF0000',
+            "global_cursor_color_enabled": True,
 
 
             # Gerber General
             # Gerber General
             "gerber_plot": True,
             "gerber_plot": True,
@@ -954,6 +958,24 @@ class App(QtCore.QObject):
             "tools_cal_toolchange_xy": '',
             "tools_cal_toolchange_xy": '',
             "tools_cal_sec_point": 'tl',
             "tools_cal_sec_point": 'tl',
 
 
+            # Drills Extraction Tool
+            "tools_edrills_hole_type": 'fixed',
+            "tools_edrills_hole_fixed_dia": 0.5,
+            "tools_edrills_hole_prop_factor": 80.0,
+            "tools_edrills_circular_ring": 0.2,
+            "tools_edrills_oblong_ring": 0.2,
+            "tools_edrills_square_ring": 0.2,
+            "tools_edrills_rectangular_ring": 0.2,
+            "tools_edrills_others_ring": 0.2,
+            "tools_edrills_circular": True,
+            "tools_edrills_oblong": False,
+            "tools_edrills_square": False,
+            "tools_edrills_rectangular": False,
+            "tools_edrills_others": False,
+
+            # Align Objects Tool
+            "tools_align_objects_align_type": 'sp',
+
             # Utilities
             # Utilities
             # file associations
             # file associations
             "fa_excellon": 'drd, drl, exc, ncd, tap, xln',
             "fa_excellon": 'drd, drl, exc, ncd, tap, xln',
@@ -1578,6 +1600,21 @@ class App(QtCore.QObject):
             "tools_cal_toolchange_xy": self.ui.tools2_defaults_form.tools2_cal_group.toolchange_xy_entry,
             "tools_cal_toolchange_xy": self.ui.tools2_defaults_form.tools2_cal_group.toolchange_xy_entry,
             "tools_cal_sec_point": self.ui.tools2_defaults_form.tools2_cal_group.second_point_radio,
             "tools_cal_sec_point": self.ui.tools2_defaults_form.tools2_cal_group.second_point_radio,
 
 
+            # Extract Drills Tool
+            "tools_edrills_hole_type": self.ui.tools2_defaults_form.tools2_edrills_group.hole_size_radio,
+            "tools_edrills_hole_fixed_dia": self.ui.tools2_defaults_form.tools2_edrills_group.dia_entry,
+            "tools_edrills_hole_prop_factor": self.ui.tools2_defaults_form.tools2_edrills_group.factor_entry,
+            "tools_edrills_circular_ring": self.ui.tools2_defaults_form.tools2_edrills_group.circular_ring_entry,
+            "tools_edrills_oblong_ring": self.ui.tools2_defaults_form.tools2_edrills_group.oblong_ring_entry,
+            "tools_edrills_square_ring": self.ui.tools2_defaults_form.tools2_edrills_group.square_ring_entry,
+            "tools_edrills_rectangular_ring": self.ui.tools2_defaults_form.tools2_edrills_group.rectangular_ring_entry,
+            "tools_edrills_others_ring": self.ui.tools2_defaults_form.tools2_edrills_group.other_ring_entry,
+            "tools_edrills_circular": self.ui.tools2_defaults_form.tools2_edrills_group.circular_cb,
+            "tools_edrills_oblong": self.ui.tools2_defaults_form.tools2_edrills_group.oblong_cb,
+            "tools_edrills_square": self.ui.tools2_defaults_form.tools2_edrills_group.square_cb,
+            "tools_edrills_rectangular": self.ui.tools2_defaults_form.tools2_edrills_group.rectangular_cb,
+            "tools_edrills_others": self.ui.tools2_defaults_form.tools2_edrills_group.other_cb,
+
             # Utilities
             # Utilities
             # File associations
             # File associations
             "fa_excellon": self.ui.util_defaults_form.fa_excellon_group.exc_list_text,
             "fa_excellon": self.ui.util_defaults_form.fa_excellon_group.exc_list_text,
@@ -1923,6 +1960,7 @@ class App(QtCore.QObject):
 
 
         self.ui.menueditorigin.triggered.connect(self.on_set_origin)
         self.ui.menueditorigin.triggered.connect(self.on_set_origin)
         self.ui.menueditjump.triggered.connect(self.on_jump_to)
         self.ui.menueditjump.triggered.connect(self.on_jump_to)
+        self.ui.menueditlocate.triggered.connect(lambda: self.on_locate(obj=self.collection.get_active()))
 
 
         self.ui.menuedittoggleunits.triggered.connect(self.on_toggle_units_click)
         self.ui.menuedittoggleunits.triggered.connect(self.on_toggle_units_click)
         self.ui.menueditselectall.triggered.connect(self.on_selectall)
         self.ui.menueditselectall.triggered.connect(self.on_selectall)
@@ -2464,12 +2502,14 @@ class App(QtCore.QObject):
         self.qrcode_tool = None
         self.qrcode_tool = None
         self.copper_thieving_tool = None
         self.copper_thieving_tool = None
         self.fiducial_tool = None
         self.fiducial_tool = None
+        self.edrills_tool = None
+        self.align_objects_tool = None
 
 
         # always install tools only after the shell is initialized because the self.inform.emit() depends on shell
         # always install tools only after the shell is initialized because the self.inform.emit() depends on shell
         try:
         try:
             self.install_tools()
             self.install_tools()
-        except AttributeError:
-            pass
+        except AttributeError as e:
+            log.debug("App.__init__() install tools() --> %s" % str(e))
 
 
         # ##################################################################################
         # ##################################################################################
         # ########################### SETUP RECENT ITEMS ###################################
         # ########################### SETUP RECENT ITEMS ###################################
@@ -3017,13 +3057,6 @@ class App(QtCore.QObject):
 
 
         :return: None
         :return: None
         """
         """
-        self.dblsidedtool = DblSidedTool(self)
-        self.dblsidedtool.install(icon=QtGui.QIcon(self.resource_location + '/doubleside16.png'), separator=True)
-
-        self.cal_exc_tool = ToolCalibration(self)
-        self.cal_exc_tool.install(icon=QtGui.QIcon(self.resource_location + '/calibrate_16.png'), pos=self.ui.menutool,
-                                  before=self.dblsidedtool.menuAction,
-                                  separator=False)
         self.distance_tool = Distance(self)
         self.distance_tool = Distance(self)
         self.distance_tool.install(icon=QtGui.QIcon(self.resource_location + '/distance16.png'), pos=self.ui.menuedit,
         self.distance_tool.install(icon=QtGui.QIcon(self.resource_location + '/distance16.png'), pos=self.ui.menuedit,
                                    before=self.ui.menueditorigin,
                                    before=self.ui.menueditorigin,
@@ -3035,6 +3068,20 @@ class App(QtCore.QObject):
                                        before=self.ui.menueditorigin,
                                        before=self.ui.menueditorigin,
                                        separator=True)
                                        separator=True)
 
 
+        self.dblsidedtool = DblSidedTool(self)
+        self.dblsidedtool.install(icon=QtGui.QIcon(self.resource_location + '/doubleside16.png'), separator=False)
+
+        self.cal_exc_tool = ToolCalibration(self)
+        self.cal_exc_tool.install(icon=QtGui.QIcon(self.resource_location + '/calibrate_16.png'), pos=self.ui.menutool,
+                                  before=self.dblsidedtool.menuAction,
+                                  separator=False)
+
+        self.align_objects_tool = AlignObjects(self)
+        self.align_objects_tool.install(icon=QtGui.QIcon(self.resource_location + '/align16.png'), separator=False)
+
+        self.edrills_tool = ToolExtractDrills(self)
+        self.edrills_tool.install(icon=QtGui.QIcon(self.resource_location + '/drill16.png'), separator=True)
+
         self.panelize_tool = Panelize(self)
         self.panelize_tool = Panelize(self)
         self.panelize_tool.install(icon=QtGui.QIcon(self.resource_location + '/panelize16.png'))
         self.panelize_tool.install(icon=QtGui.QIcon(self.resource_location + '/panelize16.png'))
 
 
@@ -3199,6 +3246,7 @@ class App(QtCore.QObject):
         self.ui.distance_min_btn.triggered.connect(lambda: self.distance_min_tool.run(toggle=True))
         self.ui.distance_min_btn.triggered.connect(lambda: self.distance_min_tool.run(toggle=True))
         self.ui.origin_btn.triggered.connect(self.on_set_origin)
         self.ui.origin_btn.triggered.connect(self.on_set_origin)
         self.ui.jmp_btn.triggered.connect(self.on_jump_to)
         self.ui.jmp_btn.triggered.connect(self.on_jump_to)
+        self.ui.locate_btn.triggered.connect(lambda: self.on_locate(obj=self.collection.get_active()))
 
 
         self.ui.shell_btn.triggered.connect(self.on_toggle_shell)
         self.ui.shell_btn.triggered.connect(self.on_toggle_shell)
         self.ui.new_script_btn.triggered.connect(self.on_filenewscript)
         self.ui.new_script_btn.triggered.connect(self.on_filenewscript)
@@ -3208,6 +3256,9 @@ class App(QtCore.QObject):
         # Tools Toolbar Signals
         # Tools Toolbar Signals
         self.ui.dblsided_btn.triggered.connect(lambda: self.dblsidedtool.run(toggle=True))
         self.ui.dblsided_btn.triggered.connect(lambda: self.dblsidedtool.run(toggle=True))
         self.ui.cal_btn.triggered.connect(lambda: self.cal_exc_tool.run(toggle=True))
         self.ui.cal_btn.triggered.connect(lambda: self.cal_exc_tool.run(toggle=True))
+        self.ui.align_btn.triggered.connect(lambda: self.align_objects_tool.run(toggle=True))
+        self.ui.extract_btn.triggered.connect(lambda: self.edrills_tool.run(toggle=True))
+
         self.ui.cutout_btn.triggered.connect(lambda: self.cutout_tool.run(toggle=True))
         self.ui.cutout_btn.triggered.connect(lambda: self.cutout_tool.run(toggle=True))
         self.ui.ncc_btn.triggered.connect(lambda: self.ncclear_tool.run(toggle=True))
         self.ui.ncc_btn.triggered.connect(lambda: self.ncclear_tool.run(toggle=True))
         self.ui.paint_btn.triggered.connect(lambda: self.paint_tool.run(toggle=True))
         self.ui.paint_btn.triggered.connect(lambda: self.paint_tool.run(toggle=True))
@@ -4237,9 +4288,20 @@ class App(QtCore.QObject):
                 obj.options['xmax'] = xmax
                 obj.options['xmax'] = xmax
                 obj.options['ymax'] = ymax
                 obj.options['ymax'] = ymax
             except Exception as e:
             except Exception as e:
-                log.warning("The object has no bounds properties. %s" % str(e))
+                log.warning("App.new_object() -> The object has no bounds properties. %s" % str(e))
                 return "fail"
                 return "fail"
 
 
+            try:
+                if kind == 'excellon':
+                    obj.fill_color = self.app.defaults["excellon_plot_fill"]
+                    obj.outline_color = self.app.defaults["excellon_plot_line"]
+
+                if kind == 'gerber':
+                    obj.fill_color = self.app.defaults["gerber_plot_fill"]
+                    obj.outline_color = self.app.defaults["gerber_plot_line"]
+            except Exception as e:
+                log.warning("App.new_object() -> setting colors error. %s" % str(e))
+
         # update the KeyWords list with the name of the file
         # update the KeyWords list with the name of the file
         self.myKeywords.append(obj.options['name'])
         self.myKeywords.append(obj.options['name'])
 
 
@@ -7140,15 +7202,13 @@ class App(QtCore.QObject):
                     obj.options['ymin'] = b
                     obj.options['ymin'] = b
                     obj.options['xmax'] = c
                     obj.options['xmax'] = c
                     obj.options['ymax'] = d
                     obj.options['ymax'] = d
-                self.inform.emit('[success] %s...' %
-                                 _('Origin set'))
+                self.inform.emit('[success] %s...' % _('Origin set'))
                 if noplot_sig is False:
                 if noplot_sig is False:
                     self.replot_signal.emit([])
                     self.replot_signal.emit([])
 
 
         if location is not None:
         if location is not None:
             if len(location) != 2:
             if len(location) != 2:
-                self.inform.emit('[ERROR_NOTCL] %s...' %
-                                 _("Origin coordinates specified but incomplete."))
+                self.inform.emit('[ERROR_NOTCL] %s...' % _("Origin coordinates specified but incomplete."))
                 return 'fail'
                 return 'fail'
 
 
             x, y = location
             x, y = location
@@ -7235,7 +7295,151 @@ class App(QtCore.QObject):
 
 
         self.jump_signal.emit(location)
         self.jump_signal.emit(location)
 
 
-        units = self.defaults['units'].upper()
+        if fit_center:
+            self.plotcanvas.fit_center(loc=location)
+
+        cursor = QtGui.QCursor()
+
+        if self.is_legacy is False:
+            # I don't know where those differences come from but they are constant for the current
+            # execution of the application and they are multiples of a value around 0.0263mm.
+            # In a random way sometimes they are more sometimes they are less
+            # if units == 'MM':
+            #     cal_factor = 0.0263
+            # else:
+            #     cal_factor = 0.0263 / 25.4
+
+            cal_location = (location[0], location[1])
+
+            canvas_origin = self.plotcanvas.native.mapToGlobal(QtCore.QPoint(0, 0))
+            jump_loc = self.plotcanvas.translate_coords_2((cal_location[0], cal_location[1]))
+
+            j_pos = (
+                int(canvas_origin.x() + round(jump_loc[0])),
+                int(canvas_origin.y() + round(jump_loc[1]))
+            )
+            cursor.setPos(j_pos[0], j_pos[1])
+        else:
+            # find the canvas origin which is in the top left corner
+            canvas_origin = self.plotcanvas.native.mapToGlobal(QtCore.QPoint(0, 0))
+            # determine the coordinates for the lowest left point of the canvas
+            x0, y0 = canvas_origin.x(), canvas_origin.y() + self.ui.right_layout.geometry().height()
+
+            # transform the given location from data coordinates to display coordinates. THe display coordinates are
+            # in pixels where the origin 0,0 is in the lowest left point of the display window (in our case is the
+            # canvas) and the point (width, height) is in the top-right location
+            loc = self.plotcanvas.axes.transData.transform_point(location)
+            j_pos = (
+                int(x0 + loc[0]),
+                int(y0 - loc[1])
+            )
+            cursor.setPos(j_pos[0], j_pos[1])
+            self.plotcanvas.mouse = [location[0], location[1]]
+            if self.defaults["global_cursor_color_enabled"] is True:
+                self.plotcanvas.draw_cursor(x_pos=location[0], y_pos=location[1], color=self.cursor_color_3D)
+            else:
+                self.plotcanvas.draw_cursor(x_pos=location[0], y_pos=location[1])
+
+        if self.grid_status():
+            # Update cursor
+            self.app_cursor.set_data(np.asarray([(location[0], location[1])]),
+                                     symbol='++', edge_color=self.cursor_color_3D,
+                                     edge_width=self.defaults["global_cursor_width"],
+                                     size=self.defaults["global_cursor_size"])
+
+        # Set the position label
+        self.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
+                                       "<b>Y</b>: %.4f" % (location[0], location[1]))
+        # Set the relative position label
+        dx = location[0] - float(self.rel_point1[0])
+        dy = location[1] - float(self.rel_point1[1])
+        self.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                           "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (dx, dy))
+
+        self.inform.emit('[success] %s' % _("Done."))
+        return location
+
+    def on_locate(self, obj, fit_center=True):
+        """
+        Jump to one of the corners (or center) of an object by setting the mouse cursor location
+        :return:
+
+        """
+        self.report_usage("on_locate()")
+
+        if obj is None:
+            self.inform.emit('[WARNING_NOTCL] %s' % _("There is no object selected..."))
+            return 'fail'
+
+        class DialogBoxChoice(QtWidgets.QDialog):
+            def __init__(self, title=None, icon=None, choice='bl'):
+                """
+
+                :param title: string with the window title
+                """
+                super(DialogBoxChoice, self).__init__()
+
+                self.ok = False
+
+                self.setWindowIcon(icon)
+                self.setWindowTitle(str(title))
+
+                self.form = QtWidgets.QFormLayout(self)
+
+                self.ref_radio = RadioSet([
+                    {"label": _("Bottom-Left"), "value": "bl"},
+                    {"label": _("Top-Left"), "value": "tl"},
+                    {"label": _("Bottom-Right"), "value": "br"},
+                    {"label": _("Top-Right"), "value": "tr"},
+                    {"label": _("Center"), "value": "c"}
+                ], orientation='vertical', stretch=False)
+                self.ref_radio.set_value(choice)
+                self.form.addRow(self.ref_radio)
+
+                self.button_box = QtWidgets.QDialogButtonBox(
+                    QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel,
+                    Qt.Horizontal, parent=self)
+                self.form.addRow(self.button_box)
+
+                self.button_box.accepted.connect(self.accept)
+                self.button_box.rejected.connect(self.reject)
+
+                if self.exec_() == QtWidgets.QDialog.Accepted:
+                    self.ok = True
+                    self.location_point = self.ref_radio.get_value()
+                else:
+                    self.ok = False
+                    self.location_point = None
+
+        dia_box = DialogBoxChoice(title=_("Locate ..."),
+                                  icon=QtGui.QIcon(self.resource_location + '/locate16.png'),
+                                  choice=self.defaults['global_locate_pt'])
+
+        if dia_box.ok is True:
+            try:
+                location_point = dia_box.location_point
+                self.defaults['global_locate_pt'] = dia_box.location_point
+            except Exception:
+                return
+        else:
+            return
+
+        loc_b = obj.bounds()
+        if location_point == 'bl':
+            location = (loc_b[0], loc_b[1])
+        elif location_point == 'tl':
+            location = (loc_b[0], loc_b[3])
+        elif location_point == 'br':
+            location = (loc_b[2], loc_b[1])
+        elif location_point == 'tr':
+            location = (loc_b[2], loc_b[3])
+        else:
+            # center
+            cx = loc_b[0] + ((loc_b[2] - loc_b[0]) / 2)
+            cy = loc_b[1] + ((loc_b[3] - loc_b[1]) / 2)
+            location = (cx, cy)
+
+        self.locate_signal.emit(location, location_point)
 
 
         if fit_center:
         if fit_center:
             self.plotcanvas.fit_center(loc=location)
             self.plotcanvas.fit_center(loc=location)
@@ -8465,7 +8669,7 @@ class App(QtCore.QObject):
                         self.draw_moving_selection_shape(self.pos, pos, color=self.defaults['global_alt_sel_line'],
                         self.draw_moving_selection_shape(self.pos, pos, color=self.defaults['global_alt_sel_line'],
                                                          face_color=self.defaults['global_alt_sel_fill'])
                                                          face_color=self.defaults['global_alt_sel_fill'])
                         self.selection_type = False
                         self.selection_type = False
-                    elif dx > 0:
+                    elif dx >= 0:
                         self.draw_moving_selection_shape(self.pos, pos)
                         self.draw_moving_selection_shape(self.pos, pos)
                         self.selection_type = True
                         self.selection_type = True
                     else:
                     else:
@@ -8862,6 +9066,7 @@ class App(QtCore.QObject):
         pt4 = (float(sel_obj.options['xmin']), float(sel_obj.options['ymax']))
         pt4 = (float(sel_obj.options['xmin']), float(sel_obj.options['ymax']))
 
 
         sel_rect = Polygon([pt1, pt2, pt3, pt4])
         sel_rect = Polygon([pt1, pt2, pt3, pt4])
+
         if self.defaults['units'].upper() == 'MM':
         if self.defaults['units'].upper() == 'MM':
             sel_rect = sel_rect.buffer(-0.1)
             sel_rect = sel_rect.buffer(-0.1)
             sel_rect = sel_rect.buffer(0.2)
             sel_rect = sel_rect.buffer(0.2)
@@ -10378,7 +10583,8 @@ class App(QtCore.QObject):
         self.report_usage("export_svg()")
         self.report_usage("export_svg()")
 
 
         if filename is None:
         if filename is None:
-            filename = self.defaults["global_last_save_folder"]
+            filename = self.defaults["global_last_save_folder"] if self.defaults["global_last_save_folder"] \
+                                                                   is not None else self.defaults["global_last_folder"]
 
 
         self.log.debug("export_svg()")
         self.log.debug("export_svg()")
 
 
@@ -10446,7 +10652,8 @@ class App(QtCore.QObject):
         self.report_usage("save source file()")
         self.report_usage("save source file()")
 
 
         if filename is None:
         if filename is None:
-            filename = self.defaults["global_last_save_folder"]
+            filename = self.defaults["global_last_save_folder"] if self.defaults["global_last_save_folder"] \
+                                                                   is not None else self.defaults["global_last_folder"]
 
 
         self.log.debug("save source file()")
         self.log.debug("save source file()")
 
 
@@ -10489,7 +10696,10 @@ class App(QtCore.QObject):
         self.report_usage("export_excellon()")
         self.report_usage("export_excellon()")
 
 
         if filename is None:
         if filename is None:
-            filename = self.defaults["global_last_save_folder"] + '/' + 'exported_excellon'
+            if self.defaults["global_last_save_folder"]:
+                filename = self.defaults["global_last_save_folder"] + '/' + 'exported_excellon'
+            else:
+                filename = self.defaults["global_last_folder"] + '/' + 'exported_excellon'
 
 
         self.log.debug("export_excellon()")
         self.log.debug("export_excellon()")
 
 
@@ -10645,7 +10855,8 @@ class App(QtCore.QObject):
         self.report_usage("export_gerber()")
         self.report_usage("export_gerber()")
 
 
         if filename is None:
         if filename is None:
-            filename = self.defaults["global_last_save_folder"]
+            filename = self.defaults["global_last_save_folder"] if self.defaults["global_last_save_folder"] \
+                                                                   is not None else self.defaults["global_last_folder"]
 
 
         self.log.debug("export_gerber()")
         self.log.debug("export_gerber()")
 
 
@@ -10781,7 +10992,8 @@ class App(QtCore.QObject):
         self.report_usage("export_dxf()")
         self.report_usage("export_dxf()")
 
 
         if filename is None:
         if filename is None:
-            filename = self.defaults["global_last_save_folder"]
+            filename = self.defaults["global_last_save_folder"] if self.defaults["global_last_save_folder"] \
+                                                                   is not None else self.defaults["global_last_folder"]
 
 
         self.log.debug("export_dxf()")
         self.log.debug("export_dxf()")
 
 
@@ -11983,8 +12195,13 @@ class App(QtCore.QObject):
             plot_container = container
             plot_container = container
         else:
         else:
             plot_container = self.ui.right_layout
             plot_container = self.ui.right_layout
-        print("step_1")
-        if self.is_legacy is False:
+
+        modifier = QtWidgets.QApplication.queryKeyboardModifiers()
+        if self.is_legacy is True or modifier == QtCore.Qt.ControlModifier:
+            self.is_legacy = True
+            self.defaults["global_graphic_engine"] = "2D"
+            self.plotcanvas = PlotCanvasLegacy(plot_container, self)
+        else:
             try:
             try:
                 self.plotcanvas = PlotCanvas(plot_container, self)
                 self.plotcanvas = PlotCanvas(plot_container, self)
             except Exception as er:
             except Exception as er:
@@ -11997,13 +12214,9 @@ class App(QtCore.QObject):
                 msg += msg_txt
                 msg += msg_txt
                 self.inform.emit(msg)
                 self.inform.emit(msg)
                 return 'fail'
                 return 'fail'
-        else:
-            self.plotcanvas = PlotCanvasLegacy(plot_container, self)
-        print("step_2")
 
 
         # So it can receive key presses
         # So it can receive key presses
         self.plotcanvas.native.setFocus()
         self.plotcanvas.native.setFocus()
-        print("step_3")
 
 
         self.mm = self.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move_over_plot)
         self.mm = self.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move_over_plot)
         self.mp = self.plotcanvas.graph_event_connect('mouse_press', self.on_mouse_click_over_plot)
         self.mp = self.plotcanvas.graph_event_connect('mouse_press', self.on_mouse_click_over_plot)
@@ -12012,28 +12225,22 @@ class App(QtCore.QObject):
 
 
         # Keys over plot enabled
         # Keys over plot enabled
         self.kp = self.plotcanvas.graph_event_connect('key_press', self.ui.keyPressEvent)
         self.kp = self.plotcanvas.graph_event_connect('key_press', self.ui.keyPressEvent)
-        print("step_4")
 
 
         if self.defaults['global_cursor_type'] == 'small':
         if self.defaults['global_cursor_type'] == 'small':
             self.app_cursor = self.plotcanvas.new_cursor()
             self.app_cursor = self.plotcanvas.new_cursor()
         else:
         else:
             self.app_cursor = self.plotcanvas.new_cursor(big=True)
             self.app_cursor = self.plotcanvas.new_cursor(big=True)
 
 
-        print("step_5")
-
         if self.ui.grid_snap_btn.isChecked():
         if self.ui.grid_snap_btn.isChecked():
             self.app_cursor.enabled = True
             self.app_cursor.enabled = True
         else:
         else:
             self.app_cursor.enabled = False
             self.app_cursor.enabled = False
 
 
-        print("step_6")
-
         if self.is_legacy is False:
         if self.is_legacy is False:
             self.hover_shapes = ShapeCollection(parent=self.plotcanvas.view.scene, layers=1)
             self.hover_shapes = ShapeCollection(parent=self.plotcanvas.view.scene, layers=1)
         else:
         else:
             # will use the default Matplotlib axes
             # will use the default Matplotlib axes
             self.hover_shapes = ShapeCollectionLegacy(obj=self, app=self, name='hover')
             self.hover_shapes = ShapeCollectionLegacy(obj=self, app=self, name='hover')
-        print("step_7")
 
 
     def on_zoom_fit(self, event):
     def on_zoom_fit(self, event):
         """
         """
@@ -12262,19 +12469,12 @@ class App(QtCore.QObject):
         new_line_color = color_variant(new_color[:7], 0.7)
         new_line_color = color_variant(new_color[:7], 0.7)
 
 
         for sel_obj in sel_obj_list:
         for sel_obj in sel_obj_list:
-            if self.is_legacy is False:
-                sel_obj.fill_color = new_color
-                sel_obj.outline_color = new_line_color
+            sel_obj.fill_color = new_color
+            sel_obj.outline_color = new_line_color
 
 
-                sel_obj.shapes.redraw(
-                    update_colors=(new_color, new_line_color)
-                )
-            else:
-                sel_obj.fill_color = new_color
-                sel_obj.outline_color = new_line_color
-                sel_obj.shapes.redraw(
-                    update_colors=(new_color, new_line_color)
-                )
+            sel_obj.shapes.redraw(
+                update_colors=(new_color, new_line_color)
+            )
 
 
     def on_grid_snap_triggered(self, state):
     def on_grid_snap_triggered(self, state):
         if state:
         if state:

+ 8 - 8
FlatCAMObj.py

@@ -1307,7 +1307,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             else:
             else:
                 iso_name = outname
                 iso_name = outname
 
 
-            # TODO: This is ugly. Create way to pass data into init function.
             def iso_init(geo_obj, app_obj):
             def iso_init(geo_obj, app_obj):
                 # Propagate options
                 # Propagate options
                 geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
                 geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
@@ -1318,8 +1317,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                     iso_offset = dia * ((2 * i + 1) / 2.0) - (i * overlap * dia)
                     iso_offset = dia * ((2 * i + 1) / 2.0) - (i * overlap * dia)
 
 
                     # if milling type is climb then the move is counter-clockwise around features
                     # if milling type is climb then the move is counter-clockwise around features
-                    mill_t = 1 if milling_type == 'cl' else 0
-                    geom = self.generate_envelope(iso_offset, mill_t, geometry=work_geo, env_iso_type=iso_t,
+                    mill_dir = 1 if milling_type == 'cl' else 0
+                    geom = self.generate_envelope(iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
                                                   follow=follow, nr_passes=i)
                                                   follow=follow, nr_passes=i)
 
 
                     if geom == 'fail':
                     if geom == 'fail':
@@ -1438,7 +1437,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                     else:
                     else:
                         iso_name = outname
                         iso_name = outname
 
 
-                # TODO: This is ugly. Create way to pass data into init function.
                 def iso_init(geo_obj, app_obj):
                 def iso_init(geo_obj, app_obj):
                     # Propagate options
                     # Propagate options
                     geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
                     geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
@@ -1448,9 +1446,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                         geo_obj.tool_type = 'C1'
                         geo_obj.tool_type = 'C1'
 
 
                     # if milling type is climb then the move is counter-clockwise around features
                     # if milling type is climb then the move is counter-clockwise around features
-                    mill_t = 1 if milling_type == 'cl' else 0
-                    mill_t = 1 if milling_type == 'cl' else 0
-                    geom = self.generate_envelope(offset, mill_t, geometry=work_geo, env_iso_type=iso_t,
+                    mill_dir = 1 if milling_type == 'cl' else 0
+                    geom = self.generate_envelope(offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
                                                   follow=follow,
                                                   follow=follow,
                                                   nr_passes=i)
                                                   nr_passes=i)
 
 
@@ -2641,7 +2638,10 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         horizontal_header.setDefaultSectionSize(70)
         horizontal_header.setDefaultSectionSize(70)
         horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
         horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
         horizontal_header.resizeSection(0, 20)
         horizontal_header.resizeSection(0, 20)
-        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
+        if self.app.defaults["global_app_level"] == 'b':
+            horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+        else:
+            horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
         horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
         horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
         horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
         horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
         horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Stretch)
         horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Stretch)

+ 43 - 3
README.md

@@ -9,19 +9,59 @@ CAD program, and create G-Code for Isolation routing.
 
 
 =================================================
 =================================================
 
 
-8.01.2019
+15.01.2020
+
+- added key shortcuts and toolbar icons for the new tools: Align Object Tool (ALT+A) and Extract Drills (ALT+I)
+- added new functionality (key shortcut SHIFT+J) to locate the corners of the bounding box (and center) in a selected object
+
+14.01.2020
+
+- in Extract Drill Tool added a new method of drills extraction. The methods are: fixed diameter, fixed annular ring and proportional
+- in Align Objects Tool finished the Single Point method of alignment
+- working on the Dual Point option in Align Objects Tool - angle has to be recalculated
+- finished Dual Point option in Align Objects Tool
+
+13.01.2020
+
+- fixed a small GUI issue in Excellon UI when Basic mode is active
+- started the add of a new Tool: Align Objects Tool which will align (sync) objects of Gerber or Excellon type
+- fixed an issue in Gerber parser introduced recently due of changes made to make Gerber files produced by Sprint Layout
+- working on the Align Objects Tool
+
+12.01.2020
+
+- improved the circle approximation resolution
+- fixed an issue in Gerber parser with detecting old kind of units
+- if CTRL key is pressed during app startup the app will start in the Legacy(2D) graphic engine compatibility mode
+
+11.01.2020
+
+- fixed an issue in the Distance Tool
+- expanded the Extract Drills Tool to use a particular annular ring for each type of aperture flash (pad)
+- Extract Drills Tool: fixed issue with oblong pads and with pads made from aperture macros
+- Extract Drills Tool: added controls in Edit -> Preferences
+
+10.02.2020
+
+- working on a new tool: Extract Drills Tool who will create a Excellon object out of the apertures of a Gerber object
+- finished the GUI in the Extract Drills Tool
+- fixed issue in Film Tool where some parameters names in calls of method export_positive() were not matching the actual parameters name
+- finished the Extract Drills Tool
+- fixed a small issue in the DoubleSided Tool
+
+8.01.2020
 
 
 - working in NCC Tool
 - working in NCC Tool
 - selected rows in the Tools Tables will stay colored in blue after loosing focus instead of the default gray
 - selected rows in the Tools Tables will stay colored in blue after loosing focus instead of the default gray
 - in NCC Tool the Tool name in the Parameters section will be the Tool ID in the Tool Table
 - in NCC Tool the Tool name in the Parameters section will be the Tool ID in the Tool Table
 - added an exception catch in case the plotcanvas init failed for the OpenGL graphic engine and warn user about what happened
 - added an exception catch in case the plotcanvas init failed for the OpenGL graphic engine and warn user about what happened
 
 
-7.01.2019
+7.01.2020
 
 
 - solved issue #368 - when using the Enable/Disable prj context menu entries the plotted status is not updated in the object properties
 - solved issue #368 - when using the Enable/Disable prj context menu entries the plotted status is not updated in the object properties
 - updates in NCC Tool
 - updates in NCC Tool
 
 
-6.01.2019
+6.01.2020
 
 
 - working on new NCC Tool
 - working on new NCC Tool
 
 

+ 17 - 18
camlib.py

@@ -458,8 +458,8 @@ class Geometry(object):
     """
     """
 
 
     defaults = {
     defaults = {
-        "units": 'in',
-        "geo_steps_per_circle": 64
+        "units": 'mm',
+        # "geo_steps_per_circle": 128
     }
     }
 
 
     def __init__(self, geo_steps_per_circle=None):
     def __init__(self, geo_steps_per_circle=None):
@@ -528,13 +528,13 @@ class Geometry(object):
             self.solid_geometry = []
             self.solid_geometry = []
 
 
         if type(self.solid_geometry) is list:
         if type(self.solid_geometry) is list:
-            self.solid_geometry.append(Point(origin).buffer(
-                radius, int(int(self.geo_steps_per_circle) / 4)))
+            self.solid_geometry.append(Point(origin).buffer(radius, int(self.geo_steps_per_circle)))
             return
             return
 
 
         try:
         try:
-            self.solid_geometry = self.solid_geometry.union(Point(origin).buffer(
-                radius, int(int(self.geo_steps_per_circle) / 4)))
+            self.solid_geometry = self.solid_geometry.union(
+                Point(origin).buffer(radius, int(self.geo_steps_per_circle))
+            )
         except Exception as e:
         except Exception as e:
             log.error("Failed to run union on polygons. %s" % str(e))
             log.error("Failed to run union on polygons. %s" % str(e))
             return
             return
@@ -944,7 +944,7 @@ class Geometry(object):
                     geo_iso.append(pol)
                     geo_iso.append(pol)
                 else:
                 else:
                     corner_type = 1 if corner is None else corner
                     corner_type = 1 if corner is None else corner
-                    geo_iso.append(pol.buffer(offset, int(int(self.geo_steps_per_circle) / 4), join_style=corner_type))
+                    geo_iso.append(pol.buffer(offset, int(self.geo_steps_per_circle), join_style=corner_type))
                 pol_nr += 1
                 pol_nr += 1
                 disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
                 disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
 
 
@@ -959,8 +959,7 @@ class Geometry(object):
                 geo_iso.append(working_geo)
                 geo_iso.append(working_geo)
             else:
             else:
                 corner_type = 1 if corner is None else corner
                 corner_type = 1 if corner is None else corner
-                geo_iso.append(working_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4),
-                                                  join_style=corner_type))
+                geo_iso.append(working_geo.buffer(offset, int(self.geo_steps_per_circle), join_style=corner_type))
 
 
         self.app.proc_container.update_view_text(' %s' % _("Buffering"))
         self.app.proc_container.update_view_text(' %s' % _("Buffering"))
         geo_iso = unary_union(geo_iso)
         geo_iso = unary_union(geo_iso)
@@ -1225,7 +1224,7 @@ class Geometry(object):
 
 
         # Can only result in a Polygon or MultiPolygon
         # Can only result in a Polygon or MultiPolygon
         # NOTE: The resulting polygon can be "empty".
         # NOTE: The resulting polygon can be "empty".
-        current = polygon.buffer((-tooldia / 1.999999), int(int(steps_per_circle) / 4))
+        current = polygon.buffer((-tooldia / 1.999999), int(steps_per_circle))
         if current.area == 0:
         if current.area == 0:
             # Otherwise, trying to to insert current.exterior == None
             # Otherwise, trying to to insert current.exterior == None
             # into the FlatCAMStorage will fail.
             # into the FlatCAMStorage will fail.
@@ -1254,7 +1253,7 @@ class Geometry(object):
             QtWidgets.QApplication.processEvents()
             QtWidgets.QApplication.processEvents()
 
 
             # Can only result in a Polygon or MultiPolygon
             # Can only result in a Polygon or MultiPolygon
-            current = current.buffer(-tooldia * (1 - overlap), int(int(steps_per_circle) / 4))
+            current = current.buffer(-tooldia * (1 - overlap), int(steps_per_circle))
             if current.area > 0:
             if current.area > 0:
 
 
                 # current can be a MultiPolygon
                 # current can be a MultiPolygon
@@ -1372,11 +1371,12 @@ class Geometry(object):
 
 
         # Clean inside edges (contours) of the original polygon
         # Clean inside edges (contours) of the original polygon
         if contour:
         if contour:
-            outer_edges = [x.exterior for x in autolist(
-                polygon_to_clear.buffer(-tooldia / 2, int(steps_per_circle / 4)))]
+            outer_edges = [
+                x.exterior for x in autolist(polygon_to_clear.buffer(-tooldia / 2, int(steps_per_circle)))
+            ]
             inner_edges = []
             inner_edges = []
             # Over resulting polygons
             # Over resulting polygons
-            for x in autolist(polygon_to_clear.buffer(-tooldia / 2, int(steps_per_circle / 4))):
+            for x in autolist(polygon_to_clear.buffer(-tooldia / 2, int(steps_per_circle))):
                 for y in x.interiors:  # Over interiors of each polygon
                 for y in x.interiors:  # Over interiors of each polygon
                     inner_edges.append(y)
                     inner_edges.append(y)
             # geoms += outer_edges + inner_edges
             # geoms += outer_edges + inner_edges
@@ -1626,7 +1626,7 @@ class Geometry(object):
                 # Straight line from current_pt to pt.
                 # Straight line from current_pt to pt.
                 # Is the toolpath inside the geometry?
                 # Is the toolpath inside the geometry?
                 walk_path = LineString([current_pt, pt])
                 walk_path = LineString([current_pt, pt])
-                walk_cut = walk_path.buffer(tooldia / 2, int(steps_per_circle / 4))
+                walk_cut = walk_path.buffer(tooldia / 2, int(steps_per_circle))
 
 
                 if walk_cut.within(boundary) and walk_path.length < max_walk:
                 if walk_cut.within(boundary) and walk_path.length < max_walk:
                     # log.debug("Walk to path #%d is inside. Joining." % path_count)
                     # log.debug("Walk to path #%d is inside. Joining." % path_count)
@@ -4213,7 +4213,7 @@ class CNCjob(Geometry):
                     radius = np.sqrt(gobj['I']**2 + gobj['J']**2)
                     radius = np.sqrt(gobj['I']**2 + gobj['J']**2)
                     start = np.arctan2(-gobj['J'], -gobj['I'])
                     start = np.arctan2(-gobj['J'], -gobj['I'])
                     stop = np.arctan2(-center[1] + y, -center[0] + x)
                     stop = np.arctan2(-center[1] + y, -center[0] + x)
-                    path += arc(center, radius, start, stop, arcdir[current['G']], int(self.steps_per_circle / 4))
+                    path += arc(center, radius, start, stop, arcdir[current['G']], int(self.steps_per_circle))
 
 
                 current['X'] = x
                 current['X'] = x
                 current['Y'] = y
                 current['Y'] = y
@@ -4362,8 +4362,7 @@ class CNCjob(Geometry):
                                           visible=visible, layer=1)
                                           visible=visible, layer=1)
             else:
             else:
                 # For Incremental coordinates type G91
                 # For Incremental coordinates type G91
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _('G91 coordinates not implemented ...'))
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _('G91 coordinates not implemented ...'))
                 for geo in gcode_parsed:
                 for geo in gcode_parsed:
                     if geo['kind'][0] == 'T':
                     if geo['kind'][0] == 'T':
                         current_position = geo['geom'].coords[0]
                         current_position = geo['geom'].coords[0]

+ 55 - 8
flatcamGUI/FlatCAMGUI.py

@@ -373,6 +373,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
             QtGui.QIcon(self.app.resource_location + '/origin16.png'), _('Se&t Origin\tO'))
             QtGui.QIcon(self.app.resource_location + '/origin16.png'), _('Se&t Origin\tO'))
         self.menueditjump = self.menuedit.addAction(
         self.menueditjump = self.menuedit.addAction(
             QtGui.QIcon(self.app.resource_location + '/jump_to16.png'), _('Jump to Location\tJ'))
             QtGui.QIcon(self.app.resource_location + '/jump_to16.png'), _('Jump to Location\tJ'))
+        self.menueditlocate = self.menuedit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/locate16.png'), _('Locate in Object\tSHIFT+J'))
 
 
         # Separator
         # Separator
         self.menuedit.addSeparator()
         self.menuedit.addSeparator()
@@ -825,6 +827,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
             QtGui.QIcon(self.app.resource_location + '/origin32.png'), _('Set Origin'))
             QtGui.QIcon(self.app.resource_location + '/origin32.png'), _('Set Origin'))
         self.jmp_btn = self.toolbargeo.addAction(
         self.jmp_btn = self.toolbargeo.addAction(
             QtGui.QIcon(self.app.resource_location + '/jump_to16.png'), _('Jump to Location'))
             QtGui.QIcon(self.app.resource_location + '/jump_to16.png'), _('Jump to Location'))
+        self.locate_btn = self.toolbargeo.addAction(
+            QtGui.QIcon(self.app.resource_location + '/locate32.png'), _('Locate in Object'))
 
 
         # ########################################################################
         # ########################################################################
         # ########################## View Toolbar# ###############################
         # ########################## View Toolbar# ###############################
@@ -859,6 +863,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         # ########################################################################
         # ########################################################################
         self.dblsided_btn = self.toolbartools.addAction(
         self.dblsided_btn = self.toolbartools.addAction(
             QtGui.QIcon(self.app.resource_location + '/doubleside32.png'), _("2Sided Tool"))
             QtGui.QIcon(self.app.resource_location + '/doubleside32.png'), _("2Sided Tool"))
+        self.align_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/align32.png'), _("Align Objects Tool"))
+        self.extract_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/extract_drill32.png'), _("Extract Drills Tool"))
+
         self.cutout_btn = self.toolbartools.addAction(
         self.cutout_btn = self.toolbartools.addAction(
             QtGui.QIcon(self.app.resource_location + '/cut16_bis.png'), _("Cutout Tool"))
             QtGui.QIcon(self.app.resource_location + '/cut16_bis.png'), _("Cutout Tool"))
         self.ncc_btn = self.toolbartools.addAction(
         self.ncc_btn = self.toolbartools.addAction(
@@ -1474,6 +1483,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20"><strong>SHIFT+G</strong></td>
                         <td height="20"><strong>SHIFT+G</strong></td>
                         <td>&nbsp;%s</td>
                         <td>&nbsp;%s</td>
                     </tr>
                     </tr>
+                    <tr height="20">
+                        <td height="20"><strong>SHIFT+J</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
                     <tr height="20">
                     <tr height="20">
                         <td height="20"><strong>SHIFT+M</strong></td>
                         <td height="20"><strong>SHIFT+M</strong></td>
                         <td>&nbsp;%s</td>
                         <td>&nbsp;%s</td>
@@ -1506,6 +1519,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>ALT+A</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
                     <tr height="20">
                     <tr height="20">
                         <td height="20"><strong>ALT+C</strong></td>
                         <td height="20"><strong>ALT+C</strong></td>
                         <td>&nbsp;%s</td>
                         <td>&nbsp;%s</td>
@@ -1518,6 +1535,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20"><strong>ALT+E</strong></td>
                         <td height="20"><strong>ALT+E</strong></td>
                         <td>&nbsp;%s</td>
                         <td>&nbsp;%s</td>
                     </tr>
                     </tr>
+                    <tr height="20">
+                        <td height="20"><strong>ALT+I</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
                     <tr height="20">
                     <tr height="20">
                         <td height="20"><strong>ALT+J</strong></td>
                         <td height="20"><strong>ALT+J</strong></td>
                         <td>&nbsp;%s</td>
                         <td>&nbsp;%s</td>
@@ -1637,11 +1658,13 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
 
                 # SHIFT section
                 # SHIFT section
                 _("Copy Obj_Name"),
                 _("Copy Obj_Name"),
-                _("Toggle Code Editor"), _("Toggle the axis"), _("Distance Minimum Tool"), _("Open Preferences Window"),
+                _("Toggle Code Editor"), _("Toggle the axis"), _("Locate in Object"), _("Distance Minimum Tool"),
+                _("Open Preferences Window"),
                 _("Rotate by 90 degree CCW"), _("Run a Script"), _("Toggle the workspace"), _("Skew on X axis"),
                 _("Rotate by 90 degree CCW"), _("Run a Script"), _("Toggle the workspace"), _("Skew on X axis"),
                 _("Skew on Y axis"),
                 _("Skew on Y axis"),
                 # ALT section
                 # ALT section
-                _("Calculators Tool"), _("2-Sided PCB Tool"), _("Transformations Tool"), _("Fiducials Tool"),
+                _("Align Objects Tool"), _("Calculators Tool"), _("2-Sided PCB Tool"), _("Transformations Tool"),
+                _("Extract Drills Tool"), _("Fiducials Tool"),
                 _("Solder Paste Dispensing Tool"),
                 _("Solder Paste Dispensing Tool"),
                 _("Film PCB Tool"), _("Non-Copper Clearing Tool"), _("Optimal Tool"),
                 _("Film PCB Tool"), _("Non-Copper Clearing Tool"), _("Optimal Tool"),
                 _("Paint Area Tool"), _("QRCode Tool"), _("Rules Check Tool"),
                 _("Paint Area Tool"), _("QRCode Tool"), _("Rules Check Tool"),
@@ -2457,8 +2480,12 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
             QtGui.QIcon(self.app.resource_location + '/origin32.png'), _('Set Origin'))
             QtGui.QIcon(self.app.resource_location + '/origin32.png'), _('Set Origin'))
         self.jmp_btn = self.toolbargeo.addAction(
         self.jmp_btn = self.toolbargeo.addAction(
             QtGui.QIcon(self.app.resource_location + '/jump_to16.png'), _('Jump to Location'))
             QtGui.QIcon(self.app.resource_location + '/jump_to16.png'), _('Jump to Location'))
+        self.locate_btn = self.toolbargeo.addAction(
+            QtGui.QIcon(self.app.resource_location + '/locate32.png'), _('Locate in Object'))
 
 
-        # ## View Toolbar # ##
+        # ########################################################################
+        # ########################## View Toolbar# ###############################
+        # ########################################################################
         self.replot_btn = self.toolbarview.addAction(
         self.replot_btn = self.toolbarview.addAction(
             QtGui.QIcon(self.app.resource_location + '/replot32.png'), _("&Replot"))
             QtGui.QIcon(self.app.resource_location + '/replot32.png'), _("&Replot"))
         self.clear_plot_btn = self.toolbarview.addAction(
         self.clear_plot_btn = self.toolbarview.addAction(
@@ -2470,9 +2497,9 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.zoom_fit_btn = self.toolbarview.addAction(
         self.zoom_fit_btn = self.toolbarview.addAction(
             QtGui.QIcon(self.app.resource_location + '/zoom_fit32.png'), _("Zoom Fit"))
             QtGui.QIcon(self.app.resource_location + '/zoom_fit32.png'), _("Zoom Fit"))
 
 
-        # self.toolbarview.setVisible(False)
-
-        # ## Shell Toolbar # ##
+        # ########################################################################
+        # ########################## Shell Toolbar# ##############################
+        # ########################################################################
         self.shell_btn = self.toolbarshell.addAction(
         self.shell_btn = self.toolbarshell.addAction(
             QtGui.QIcon(self.app.resource_location + '/shell32.png'), _("&Command Line"))
             QtGui.QIcon(self.app.resource_location + '/shell32.png'), _("&Command Line"))
         self.new_script_btn = self.toolbarshell.addAction(
         self.new_script_btn = self.toolbarshell.addAction(
@@ -2485,6 +2512,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         # ## Tools Toolbar # ##
         # ## Tools Toolbar # ##
         self.dblsided_btn = self.toolbartools.addAction(
         self.dblsided_btn = self.toolbartools.addAction(
             QtGui.QIcon(self.app.resource_location + '/doubleside32.png'), _("2Sided Tool"))
             QtGui.QIcon(self.app.resource_location + '/doubleside32.png'), _("2Sided Tool"))
+        self.align_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/align32.png'), _("Align Objects Tool"))
+        self.extract_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/extract_drill32.png'), _("Extract Drills Tool"))
+
         self.cutout_btn = self.toolbartools.addAction(
         self.cutout_btn = self.toolbartools.addAction(
             QtGui.QIcon(self.app.resource_location + '/cut16_bis.png'), _("&Cutout Tool"))
             QtGui.QIcon(self.app.resource_location + '/cut16_bis.png'), _("&Cutout Tool"))
         self.ncc_btn = self.toolbartools.addAction(
         self.ncc_btn = self.toolbartools.addAction(
@@ -2498,10 +2530,13 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.film_btn = self.toolbartools.addAction(
         self.film_btn = self.toolbartools.addAction(
             QtGui.QIcon(self.app.resource_location + '/film16.png'), _("Film Tool"))
             QtGui.QIcon(self.app.resource_location + '/film16.png'), _("Film Tool"))
         self.solder_btn = self.toolbartools.addAction(
         self.solder_btn = self.toolbartools.addAction(
-            QtGui.QIcon(self.app.resource_location + '/solderpastebis32.png'),
-                                                      _("SolderPaste Tool"))
+            QtGui.QIcon(self.app.resource_location + '/solderpastebis32.png'), _("SolderPaste Tool"))
         self.sub_btn = self.toolbartools.addAction(
         self.sub_btn = self.toolbartools.addAction(
             QtGui.QIcon(self.app.resource_location + '/sub32.png'), _("Subtract Tool"))
             QtGui.QIcon(self.app.resource_location + '/sub32.png'), _("Subtract Tool"))
+        self.rules_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/rules32.png'), _("Rules Tool"))
+        self.optimal_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/open_excellon32.png'), _("Optimal Tool"))
 
 
         self.toolbartools.addSeparator()
         self.toolbartools.addSeparator()
 
 
@@ -2834,6 +2869,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 if key == QtCore.Qt.Key_G:
                 if key == QtCore.Qt.Key_G:
                     self.app.on_toggle_axis()
                     self.app.on_toggle_axis()
 
 
+                # Locate in Object
+                if key == QtCore.Qt.Key_J:
+                    self.app.on_locate(obj=self.app.collection.get_active())
+
                 # Run Distance Minimum Tool
                 # Run Distance Minimum Tool
                 if key == QtCore.Qt.Key_M:
                 if key == QtCore.Qt.Key_M:
                     self.app.distance_min_tool.run()
                     self.app.distance_min_tool.run()
@@ -2882,6 +2921,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 if key == Qt.Key_3:
                 if key == Qt.Key_3:
                     self.app.disable_other_plots()
                     self.app.disable_other_plots()
 
 
+                # Align in Object Tool
+                if key == QtCore.Qt.Key_A:
+                    self.app.align_objects_tool.run(toggle=True)
+
                 # Calculator Tool
                 # Calculator Tool
                 if key == QtCore.Qt.Key_C:
                 if key == QtCore.Qt.Key_C:
                     self.app.calculator_tool.run(toggle=True)
                     self.app.calculator_tool.run(toggle=True)
@@ -2906,6 +2949,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.on_toggle_grid_lines()
                     self.app.on_toggle_grid_lines()
                     return
                     return
 
 
+                # Align in Object Tool
+                if key == QtCore.Qt.Key_I:
+                    self.app.edrills_tool.run(toggle=True)
+
                 # Fiducials Tool
                 # Fiducials Tool
                 if key == QtCore.Qt.Key_J:
                 if key == QtCore.Qt.Key_J:
                     self.app.fiducial_tool.run(toggle=True)
                     self.app.fiducial_tool.run(toggle=True)

+ 4 - 0
flatcamGUI/GUIElements.py

@@ -2009,6 +2009,10 @@ class FCTable(QtWidgets.QTableWidget):
         palette = QtGui.QPalette()
         palette = QtGui.QPalette()
         palette.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Highlight,
         palette.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Highlight,
                          palette.color(QtGui.QPalette.Active, QtGui.QPalette.Highlight))
                          palette.color(QtGui.QPalette.Active, QtGui.QPalette.Highlight))
+
+        # make inactive rows text some color as active; may be useful in the future
+        # palette.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.HighlightedText,
+        #                  palette.color(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText))
         self.setPalette(palette)
         self.setPalette(palette)
 
 
         if drag_drop:
         if drag_drop:

+ 3 - 18
flatcamGUI/PlotCanvas.py

@@ -32,11 +32,11 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         :param container: The parent container in which to draw plots.
         :param container: The parent container in which to draw plots.
         :rtype: PlotCanvas
         :rtype: PlotCanvas
         """
         """
-        print("step_1_1")
 
 
-        super(PlotCanvas, self).__init__()
+        # super(PlotCanvas, self).__init__()
+        # QtCore.QObject.__init__(self)
         # VisPyCanvas.__init__(self)
         # VisPyCanvas.__init__(self)
-        print("step_1_2")
+        super().__init__()
 
 
         # VisPyCanvas does not allow new attributes. Override.
         # VisPyCanvas does not allow new attributes. Override.
         self.unfreeze()
         self.unfreeze()
@@ -46,8 +46,6 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         # Parent container
         # Parent container
         self.container = container
         self.container = container
 
 
-        print("step_1_3")
-
         settings = QtCore.QSettings("Open Source", "FlatCAM")
         settings = QtCore.QSettings("Open Source", "FlatCAM")
         if settings.contains("theme"):
         if settings.contains("theme"):
             theme = settings.value('theme', type=str)
             theme = settings.value('theme', type=str)
@@ -117,8 +115,6 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
             }
             }
         )
         )
 
 
-        print("step_1_4")
-
         # <VisPyCanvas>
         # <VisPyCanvas>
         self.create_native()
         self.create_native()
         self.native.setParent(self.fcapp.ui)
         self.native.setParent(self.fcapp.ui)
@@ -126,8 +122,6 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         # <QtCore.QObject>
         # <QtCore.QObject>
         self.container.addWidget(self.native)
         self.container.addWidget(self.native)
 
 
-        print("step_1_5")
-
         # ## AXIS # ##
         # ## AXIS # ##
         self.v_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 0.8), vertical=True,
         self.v_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 0.8), vertical=True,
                                    parent=self.view.scene)
                                    parent=self.view.scene)
@@ -135,15 +129,11 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         self.h_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 0.8), vertical=False,
         self.h_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 0.8), vertical=False,
                                    parent=self.view.scene)
                                    parent=self.view.scene)
 
 
-        print("step_1_6")
-
         # draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
         # draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
         # all CNC have a limited workspace
         # all CNC have a limited workspace
         if self.fcapp.defaults['global_workspace'] is True:
         if self.fcapp.defaults['global_workspace'] is True:
             self.draw_workspace(workspace_size=self.fcapp.defaults["global_workspaceT"])
             self.draw_workspace(workspace_size=self.fcapp.defaults["global_workspaceT"])
 
 
-        print("step_1_7")
-
         self.line_parent = None
         self.line_parent = None
         if self.fcapp.defaults["global_cursor_color_enabled"]:
         if self.fcapp.defaults["global_cursor_color_enabled"]:
             c_color = Color(self.fcapp.defaults["global_cursor_color"]).rgba
             c_color = Color(self.fcapp.defaults["global_cursor_color"]).rgba
@@ -156,8 +146,6 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         self.cursor_h_line = InfiniteLine(pos=None, color=c_color, vertical=False,
         self.cursor_h_line = InfiniteLine(pos=None, color=c_color, vertical=False,
                                           parent=self.line_parent)
                                           parent=self.line_parent)
 
 
-        print("step_1_8")
-
         self.shape_collections = []
         self.shape_collections = []
 
 
         self.shape_collection = self.new_shape_collection()
         self.shape_collection = self.new_shape_collection()
@@ -171,10 +159,7 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         self.big_cursor = None
         self.big_cursor = None
         # Keep VisPy canvas happy by letting it be "frozen" again.
         # Keep VisPy canvas happy by letting it be "frozen" again.
         self.freeze()
         self.freeze()
-        print("step_1_9")
-
         self.fit_view()
         self.fit_view()
-        print("step_1_10")
 
 
         self.graph_event_connect('mouse_wheel', self.on_mouse_scroll)
         self.graph_event_connect('mouse_wheel', self.on_mouse_scroll)
 
 

+ 221 - 19
flatcamGUI/PreferencesUI.py

@@ -242,19 +242,23 @@ class Tools2PreferencesUI(QtWidgets.QWidget):
         self.tools2_cal_group = Tools2CalPrefGroupUI(decimals=self.decimals)
         self.tools2_cal_group = Tools2CalPrefGroupUI(decimals=self.decimals)
         self.tools2_cal_group.setMinimumWidth(220)
         self.tools2_cal_group.setMinimumWidth(220)
 
 
+        self.tools2_edrills_group = Tools2EDrillsPrefGroupUI(decimals=self.decimals)
+        self.tools2_edrills_group.setMinimumWidth(220)
+
         self.vlay = QtWidgets.QVBoxLayout()
         self.vlay = QtWidgets.QVBoxLayout()
         self.vlay.addWidget(self.tools2_checkrules_group)
         self.vlay.addWidget(self.tools2_checkrules_group)
         self.vlay.addWidget(self.tools2_optimal_group)
         self.vlay.addWidget(self.tools2_optimal_group)
 
 
         self.vlay1 = QtWidgets.QVBoxLayout()
         self.vlay1 = QtWidgets.QVBoxLayout()
         self.vlay1.addWidget(self.tools2_qrcode_group)
         self.vlay1.addWidget(self.tools2_qrcode_group)
+        self.vlay1.addWidget(self.tools2_fiducials_group)
 
 
         self.vlay2 = QtWidgets.QVBoxLayout()
         self.vlay2 = QtWidgets.QVBoxLayout()
         self.vlay2.addWidget(self.tools2_cfill_group)
         self.vlay2.addWidget(self.tools2_cfill_group)
 
 
         self.vlay3 = QtWidgets.QVBoxLayout()
         self.vlay3 = QtWidgets.QVBoxLayout()
-        self.vlay3.addWidget(self.tools2_fiducials_group)
         self.vlay3.addWidget(self.tools2_cal_group)
         self.vlay3.addWidget(self.tools2_cal_group)
+        self.vlay3.addWidget(self.tools2_edrills_group)
 
 
         self.layout.addLayout(self.vlay)
         self.layout.addLayout(self.vlay)
         self.layout.addLayout(self.vlay1)
         self.layout.addLayout(self.vlay1)
@@ -333,7 +337,8 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         # Theme selection
         # Theme selection
         self.theme_label = QtWidgets.QLabel('%s:' % _('Theme'))
         self.theme_label = QtWidgets.QLabel('%s:' % _('Theme'))
         self.theme_label.setToolTip(
         self.theme_label.setToolTip(
-            _("Select a theme for FlatCAM.")
+            _("Select a theme for FlatCAM.\n"
+              "It will theme the plot area.")
         )
         )
 
 
         self.theme_radio = RadioSet([
         self.theme_radio = RadioSet([
@@ -356,6 +361,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         self.theme_button = FCButton(_("Apply Theme"))
         self.theme_button = FCButton(_("Apply Theme"))
         self.theme_button.setToolTip(
         self.theme_button.setToolTip(
             _("Select a theme for FlatCAM.\n"
             _("Select a theme for FlatCAM.\n"
+              "It will theme the plot area.\n"
               "The application will restart after change.")
               "The application will restart after change.")
         )
         )
         grid0.addWidget(self.theme_button, 2, 0, 1, 3)
         grid0.addWidget(self.theme_button, 2, 0, 1, 3)
@@ -1587,14 +1593,6 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
               "After change, it will be applied at next App start.")
               "After change, it will be applied at next App start.")
         )
         )
         self.worker_number_sb = FCSpinner()
         self.worker_number_sb = FCSpinner()
-        self.worker_number_sb.setToolTip(
-            _("The number of Qthreads made available to the App.\n"
-              "A bigger number may finish the jobs more quickly but\n"
-              "depending on your computer speed, may make the App\n"
-              "unresponsive. Can have a value between 2 and 16.\n"
-              "Default value is 2.\n"
-              "After change, it will be applied at next App start.")
-        )
         self.worker_number_sb.set_range(2, 16)
         self.worker_number_sb.set_range(2, 16)
 
 
         grid0.addWidget(self.worker_number_label, 25, 0)
         grid0.addWidget(self.worker_number_label, 25, 0)
@@ -1604,21 +1602,13 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
         tol_label = QtWidgets.QLabel('%s:' % _("Geo Tolerance"))
         tol_label = QtWidgets.QLabel('%s:' % _("Geo Tolerance"))
         tol_label.setToolTip(_(
         tol_label.setToolTip(_(
             "This value can counter the effect of the Circle Steps\n"
             "This value can counter the effect of the Circle Steps\n"
-            "parameter. Default value is 0.01.\n"
+            "parameter. Default value is 0.005.\n"
             "A lower value will increase the detail both in image\n"
             "A lower value will increase the detail both in image\n"
             "and in Gcode for the circles, with a higher cost in\n"
             "and in Gcode for the circles, with a higher cost in\n"
             "performance. Higher value will provide more\n"
             "performance. Higher value will provide more\n"
             "performance at the expense of level of detail."
             "performance at the expense of level of detail."
         ))
         ))
         self.tol_entry = FCDoubleSpinner()
         self.tol_entry = FCDoubleSpinner()
-        self.tol_entry.setToolTip(_(
-            "This value can counter the effect of the Circle Steps\n"
-            "parameter. Default value is 0.01.\n"
-            "A lower value will increase the detail both in image\n"
-            "and in Gcode for the circles, with a higher cost in\n"
-            "performance. Higher value will provide more\n"
-            "performance at the expense of level of detail."
-        ))
         self.tol_entry.setSingleStep(0.001)
         self.tol_entry.setSingleStep(0.001)
         self.tol_entry.set_precision(6)
         self.tol_entry.set_precision(6)
 
 
@@ -7632,6 +7622,218 @@ class Tools2CalPrefGroupUI(OptionsGroupUI):
         self.layout.addStretch()
         self.layout.addStretch()
 
 
 
 
+class Tools2EDrillsPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+
+        super(Tools2EDrillsPrefGroupUI, self).__init__(self)
+
+        self.setTitle(str(_("Extract Drills Options")))
+        self.decimals = decimals
+
+        # ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 0)
+        grid_lay.setColumnStretch(1, 1)
+
+        self.param_label = QtWidgets.QLabel('<b>%s:</b>' % _('Parameters'))
+        self.param_label.setToolTip(
+            _("Parameters used for this tool.")
+        )
+        grid_lay.addWidget(self.param_label, 0, 0, 1, 2)
+
+        self.padt_label = QtWidgets.QLabel("<b>%s:</b>" % _("Processed Pads Type"))
+        self.padt_label.setToolTip(
+            _("The type of pads shape to be processed.\n"
+              "If the PCB has many SMD pads with rectangular pads,\n"
+              "disable the Rectangular aperture.")
+        )
+
+        grid_lay.addWidget(self.padt_label, 2, 0, 1, 2)
+
+        # Circular Aperture Selection
+        self.circular_cb = FCCheckBox('%s' % _("Circular"))
+        self.circular_cb.setToolTip(
+            _("Create drills from circular pads.")
+        )
+
+        grid_lay.addWidget(self.circular_cb, 3, 0, 1, 2)
+
+        # Oblong Aperture Selection
+        self.oblong_cb = FCCheckBox('%s' % _("Oblong"))
+        self.oblong_cb.setToolTip(
+            _("Create drills from oblong pads.")
+        )
+
+        grid_lay.addWidget(self.oblong_cb, 4, 0, 1, 2)
+
+        # Square Aperture Selection
+        self.square_cb = FCCheckBox('%s' % _("Square"))
+        self.square_cb.setToolTip(
+            _("Create drills from square pads.")
+        )
+
+        grid_lay.addWidget(self.square_cb, 5, 0, 1, 2)
+
+        # Rectangular Aperture Selection
+        self.rectangular_cb = FCCheckBox('%s' % _("Rectangular"))
+        self.rectangular_cb.setToolTip(
+            _("Create drills from rectangular pads.")
+        )
+
+        grid_lay.addWidget(self.rectangular_cb, 6, 0, 1, 2)
+
+        # Others type of Apertures Selection
+        self.other_cb = FCCheckBox('%s' % _("Others"))
+        self.other_cb.setToolTip(
+            _("Create drills from other types of pad shape.")
+        )
+
+        grid_lay.addWidget(self.other_cb, 7, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line, 8, 0, 1, 2)
+
+        # ## Axis
+        self.hole_size_radio = RadioSet(
+            [
+                {'label': _("Fixed Diameter"), 'value': 'fixed'},
+                {'label': _("Fixed Annular Ring"), 'value': 'ring'},
+                {'label': _("Proportional"), 'value': 'prop'}
+            ],
+            orientation='vertical',
+            stretch=False)
+        self.hole_size_label = QtWidgets.QLabel('<b>%s:</b>' % _("Method"))
+        self.hole_size_label.setToolTip(
+            _("The selected method of extracting the drills. Can be:\n"
+              "- Fixed Diameter -> all holes will have a set size\n"
+              "- Fixed Annular Ring -> all holes will have a set annular ring\n"
+              "- Proportional -> each hole size will be a fraction of the pad size"))
+
+        grid_lay.addWidget(self.hole_size_label, 9, 0)
+        grid_lay.addWidget(self.hole_size_radio, 9, 1)
+
+        # grid_lay1.addWidget(QtWidgets.QLabel(''))
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line, 10, 0, 1, 2)
+
+        # Annular Ring
+        self.fixed_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Diameter"))
+        grid_lay.addWidget(self.fixed_label, 11, 0, 1, 2)
+
+        # Diameter value
+        self.dia_entry = FCDoubleSpinner()
+        self.dia_entry.set_precision(self.decimals)
+        self.dia_entry.set_range(0.0000, 9999.9999)
+
+        self.dia_label = QtWidgets.QLabel('%s:' % _("value"))
+        self.dia_label.setToolTip(
+            _("Fixed hole diameter.")
+        )
+
+        grid_lay.addWidget(self.dia_label, 12, 0)
+        grid_lay.addWidget(self.dia_entry, 12, 1)
+
+        # Annular Ring value
+        self.ring_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Annular Ring"))
+        self.ring_label.setToolTip(
+            _("The size of annular ring.\n"
+              "The copper sliver between the drill hole exterior\n"
+              "and the margin of the copper pad.")
+        )
+        grid_lay.addWidget(self.ring_label, 13, 0, 1, 2)
+
+        # Circular Annular Ring Value
+        self.circular_ring_label = QtWidgets.QLabel('%s:' % _("Circular"))
+        self.circular_ring_label.setToolTip(
+            _("The size of annular ring for circular pads.")
+        )
+
+        self.circular_ring_entry = FCDoubleSpinner()
+        self.circular_ring_entry.set_precision(self.decimals)
+        self.circular_ring_entry.set_range(0.0000, 9999.9999)
+
+        grid_lay.addWidget(self.circular_ring_label, 14, 0)
+        grid_lay.addWidget(self.circular_ring_entry, 14, 1)
+
+        # Oblong Annular Ring Value
+        self.oblong_ring_label = QtWidgets.QLabel('%s:' % _("Oblong"))
+        self.oblong_ring_label.setToolTip(
+            _("The size of annular ring for oblong pads.")
+        )
+
+        self.oblong_ring_entry = FCDoubleSpinner()
+        self.oblong_ring_entry.set_precision(self.decimals)
+        self.oblong_ring_entry.set_range(0.0000, 9999.9999)
+
+        grid_lay.addWidget(self.oblong_ring_label, 15, 0)
+        grid_lay.addWidget(self.oblong_ring_entry, 15, 1)
+
+        # Square Annular Ring Value
+        self.square_ring_label = QtWidgets.QLabel('%s:' % _("Square"))
+        self.square_ring_label.setToolTip(
+            _("The size of annular ring for square pads.")
+        )
+
+        self.square_ring_entry = FCDoubleSpinner()
+        self.square_ring_entry.set_precision(self.decimals)
+        self.square_ring_entry.set_range(0.0000, 9999.9999)
+
+        grid_lay.addWidget(self.square_ring_label, 16, 0)
+        grid_lay.addWidget(self.square_ring_entry, 16, 1)
+
+        # Rectangular Annular Ring Value
+        self.rectangular_ring_label = QtWidgets.QLabel('%s:' % _("Rectangular"))
+        self.rectangular_ring_label.setToolTip(
+            _("The size of annular ring for rectangular pads.")
+        )
+
+        self.rectangular_ring_entry = FCDoubleSpinner()
+        self.rectangular_ring_entry.set_precision(self.decimals)
+        self.rectangular_ring_entry.set_range(0.0000, 9999.9999)
+
+        grid_lay.addWidget(self.rectangular_ring_label, 17, 0)
+        grid_lay.addWidget(self.rectangular_ring_entry, 17, 1)
+
+        # Others Annular Ring Value
+        self.other_ring_label = QtWidgets.QLabel('%s:' % _("Others"))
+        self.other_ring_label.setToolTip(
+            _("The size of annular ring for other pads.")
+        )
+
+        self.other_ring_entry = FCDoubleSpinner()
+        self.other_ring_entry.set_precision(self.decimals)
+        self.other_ring_entry.set_range(0.0000, 9999.9999)
+
+        grid_lay.addWidget(self.other_ring_label, 18, 0)
+        grid_lay.addWidget(self.other_ring_entry, 18, 1)
+
+        self.prop_label = QtWidgets.QLabel('<b>%s</b>' % _("Proportional Diameter"))
+        grid_lay.addWidget(self.prop_label, 19, 0, 1, 2)
+
+        # Factor value
+        self.factor_entry = FCDoubleSpinner(suffix='%')
+        self.factor_entry.set_precision(self.decimals)
+        self.factor_entry.set_range(0.0000, 100.0000)
+        self.factor_entry.setSingleStep(0.1)
+
+        self.factor_label = QtWidgets.QLabel('%s:' % _("Factor"))
+        self.factor_label.setToolTip(
+            _("Proportional Diameter.\n"
+              "The drill diameter will be a fraction of the pad size.")
+        )
+
+        grid_lay.addWidget(self.factor_label, 20, 0)
+        grid_lay.addWidget(self.factor_entry, 20, 1)
+
+        self.layout.addStretch()
+
+
 class FAExcPrefGroupUI(OptionsGroupUI):
 class FAExcPrefGroupUI(OptionsGroupUI):
     def __init__(self, decimals=4, parent=None):
     def __init__(self, decimals=4, parent=None):
         # OptionsGroupUI.__init__(self, "Excellon File associations Preferences", parent=None)
         # OptionsGroupUI.__init__(self, "Excellon File associations Preferences", parent=None)

+ 2 - 1
flatcamGUI/VisPyCanvas.py

@@ -24,7 +24,8 @@ black = Color("#000000")
 class VisPyCanvas(scene.SceneCanvas):
 class VisPyCanvas(scene.SceneCanvas):
 
 
     def __init__(self, config=None):
     def __init__(self, config=None):
-        scene.SceneCanvas.__init__(self, keys=None, config=config)
+        # scene.SceneCanvas.__init__(self, keys=None, config=config)
+        super().__init__(config=config, keys=None)
 
 
         self.unfreeze()
         self.unfreeze()
 
 

+ 3 - 1
flatcamParsers/ParseGerber.py

@@ -595,6 +595,7 @@ class Gerber(Geometry):
                 match = self.units_re.search(gline)
                 match = self.units_re.search(gline)
                 if match:
                 if match:
                     obs_gerber_units = {'0': 'IN', '1': 'MM'}[match.group(1)]
                     obs_gerber_units = {'0': 'IN', '1': 'MM'}[match.group(1)]
+                    self.units = obs_gerber_units
                     log.warning("Gerber obsolete units found = %s" % obs_gerber_units)
                     log.warning("Gerber obsolete units found = %s" % obs_gerber_units)
                     # Changed for issue #80
                     # Changed for issue #80
                     # self.convert_units({'0': 'IN', '1': 'MM'}[match.group(1)])
                     # self.convert_units({'0': 'IN', '1': 'MM'}[match.group(1)])
@@ -834,7 +835,8 @@ class Gerber(Geometry):
                     # --- Buffered ---
                     # --- Buffered ---
                     geo_dict = dict()
                     geo_dict = dict()
                     if current_aperture in self.apertures:
                     if current_aperture in self.apertures:
-                        buff_value = float(self.apertures[current_aperture]['size']) / 2.0
+                        # the following line breaks loading of Circuit Studio Gerber files
+                        # buff_value = float(self.apertures[current_aperture]['size']) / 2.0
                         # region_geo = Polygon(path).buffer(buff_value, int(self.steps_per_circle))
                         # region_geo = Polygon(path).buffer(buff_value, int(self.steps_per_circle))
                         region_geo = Polygon(path)  # Sprint Layout Gerbers with ground fill are crashed with above
                         region_geo = Polygon(path)  # Sprint Layout Gerbers with ground fill are crashed with above
                     else:
                     else:

+ 499 - 0
flatcamTools/ToolAlignObjects.py

@@ -0,0 +1,499 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Date: 1/13/2020                                          #
+# MIT Licence                                              #
+# ##########################################################
+
+from PyQt5 import QtWidgets, QtGui, QtCore
+from FlatCAMTool import FlatCAMTool
+
+from flatcamGUI.GUIElements import FCComboBox, RadioSet
+
+import math
+
+from shapely.geometry import Point
+from shapely.affinity import translate
+
+import gettext
+import FlatCAMTranslation as fcTranslate
+import builtins
+import logging
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+log = logging.getLogger('base')
+
+
+class AlignObjects(FlatCAMTool):
+
+    toolName = _("Align Objects")
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+
+        self.app = app
+        self.decimals = app.decimals
+
+        self.canvas = self.app.plotcanvas
+
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                        QLabel
+                        {
+                            font-size: 16px;
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(title_label)
+
+        # Form Layout
+        grid0 = QtWidgets.QGridLayout()
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+        self.layout.addLayout(grid0)
+
+        self.aligned_label = QtWidgets.QLabel('<b>%s</b>' % _("Selection of the WORKING object"))
+        grid0.addWidget(self.aligned_label, 0, 0, 1, 2)
+
+        # Type of object to be aligned
+        self.type_obj_combo = FCComboBox()
+        self.type_obj_combo.addItem("Gerber")
+        self.type_obj_combo.addItem("Excellon")
+
+        self.type_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
+        self.type_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
+
+        self.type_obj_combo_label = QtWidgets.QLabel('%s:' % _("Object Type"))
+        self.type_obj_combo_label.setToolTip(
+            _("Specify the type of object to be aligned.\n"
+              "It can be of type: Gerber or Excellon.\n"
+              "The selection here decide the type of objects that will be\n"
+              "in the Object combobox.")
+        )
+        grid0.addWidget(self.type_obj_combo_label, 2, 0)
+        grid0.addWidget(self.type_obj_combo, 2, 1)
+
+        # Object to be aligned
+        self.object_combo = FCComboBox()
+        self.object_combo.setModel(self.app.collection)
+        self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.object_combo.setCurrentIndex(1)
+
+        self.object_label = QtWidgets.QLabel('%s:' % _("Object"))
+        self.object_label.setToolTip(
+            _("Object to be aligned.")
+        )
+
+        grid0.addWidget(self.object_label, 3, 0)
+        grid0.addWidget(self.object_combo, 3, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 4, 0, 1, 2)
+
+        self.aligned_label = QtWidgets.QLabel('<b>%s</b>' % _("Selection of the TARGET object"))
+        self.aligned_label.setToolTip(
+            _("Object to which the other objects will be aligned to (moved to).")
+        )
+        grid0.addWidget(self.aligned_label, 6, 0, 1, 2)
+
+        # Type of object to be aligned to = aligner
+        self.type_aligner_obj_combo = FCComboBox()
+        self.type_aligner_obj_combo.addItem("Gerber")
+        self.type_aligner_obj_combo.addItem("Excellon")
+
+        self.type_aligner_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
+        self.type_aligner_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
+
+        self.type_aligner_obj_combo_label = QtWidgets.QLabel('%s:' % _("Object Type"))
+        self.type_aligner_obj_combo_label.setToolTip(
+            _("Specify the type of object to be aligned to.\n"
+              "It can be of type: Gerber or Excellon.\n"
+              "The selection here decide the type of objects that will be\n"
+              "in the Object combobox.")
+        )
+        grid0.addWidget(self.type_aligner_obj_combo_label, 7, 0)
+        grid0.addWidget(self.type_aligner_obj_combo, 7, 1)
+
+        # Object to be aligned to = aligner
+        self.aligner_object_combo = FCComboBox()
+        self.aligner_object_combo.setModel(self.app.collection)
+        self.aligner_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.aligner_object_combo.setCurrentIndex(1)
+
+        self.aligner_object_label = QtWidgets.QLabel('%s:' % _("Object"))
+        self.aligner_object_label.setToolTip(
+            _("Object to be aligned to. Aligner.")
+        )
+
+        grid0.addWidget(self.aligner_object_label, 8, 0)
+        grid0.addWidget(self.aligner_object_combo, 8, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 9, 0, 1, 2)
+
+        # Alignment Type
+        self.a_type_lbl = QtWidgets.QLabel('<b>%s:</b>' % _("Alignment Type"))
+        self.a_type_lbl.setToolTip(
+            _("The type of alignment can be:\n"
+              "- Single Point -> it require a single point of sync, the action will be a translation\n"
+              "- Dual Point -> it require two points of sync, the action will be translation followed by rotation")
+        )
+        self.a_type_radio = RadioSet(
+            [
+                {'label': _('Single Point'), 'value': 'sp'},
+                {'label': _('Dual Point'), 'value': 'dp'}
+            ],
+            orientation='horizontal',
+            stretch=False
+        )
+
+        grid0.addWidget(self.a_type_lbl, 10, 0, 1, 2)
+        grid0.addWidget(self.a_type_radio, 11, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 12, 0, 1, 2)
+
+        # Buttons
+        self.align_object_button = QtWidgets.QPushButton(_("Align Object"))
+        self.align_object_button.setToolTip(
+            _("Align the specified object to the aligner object.\n"
+              "If only one point is used then it assumes translation.\n"
+              "If tho points are used it assume translation and rotation.")
+        )
+        self.align_object_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.align_object_button)
+
+        self.layout.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.reset_button)
+
+        # Signals
+        self.align_object_button.clicked.connect(self.on_align)
+        self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
+        self.type_aligner_obj_combo.currentIndexChanged.connect(self.on_type_aligner_index_changed)
+        self.reset_button.clicked.connect(self.set_tool_ui)
+
+        self.mr = None
+
+        # if the mouse events are connected to a local method set this True
+        self.local_connected = False
+
+        # store the status of the grid
+        self.grid_status_memory = None
+
+        self.aligned_obj = None
+        self.aligner_obj = None
+
+        # this is one of the objects: self.aligned_obj or self.aligner_obj
+        self.target_obj = None
+
+        # here store the alignment points
+        self.clicked_points = list()
+
+        self.align_type = None
+
+        # old colors of objects involved in the alignment
+        self.aligner_old_fill_color = None
+        self.aligner_old_line_color = None
+        self.aligned_old_fill_color = None
+        self.aligned_old_line_color = None
+
+    def run(self, toggle=True):
+        self.app.report_usage("ToolAlignObjects()")
+
+        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:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
+                except AttributeError:
+                    pass
+        else:
+            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, _("Align Tool"))
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+A', **kwargs)
+
+    def set_tool_ui(self):
+        self.reset_fields()
+
+        self.clicked_points = list()
+        self.target_obj = None
+        self.aligned_obj = None
+        self.aligner_obj = None
+
+        self.aligner_old_fill_color = None
+        self.aligner_old_line_color = None
+        self.aligned_old_fill_color = None
+        self.aligned_old_line_color = None
+
+        self.a_type_radio.set_value(self.app.defaults["tools_align_objects_align_type"])
+
+        if self.local_connected is True:
+            self.disconnect_cal_events()
+
+    def on_type_obj_index_changed(self):
+        obj_type = self.type_obj_combo.currentIndex()
+        self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.object_combo.setCurrentIndex(0)
+
+    def on_type_aligner_index_changed(self):
+        obj_type = self.type_aligner_obj_combo.currentIndex()
+        self.aligner_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
+        self.aligner_object_combo.setCurrentIndex(0)
+
+    def on_align(self):
+        self.app.delete_selection_shape()
+
+        obj_sel_index = self.object_combo.currentIndex()
+        obj_model_index = self.app.collection.index(obj_sel_index, 0, self.object_combo.rootModelIndex())
+        try:
+            self.aligned_obj = obj_model_index.internalPointer().obj
+        except AttributeError:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no aligned FlatCAM object selected..."))
+            return
+
+        aligner_obj_sel_index = self.aligner_object_combo.currentIndex()
+        aligner_obj_model_index = self.app.collection.index(
+            aligner_obj_sel_index, 0, self.aligner_object_combo.rootModelIndex())
+
+        try:
+            self.aligner_obj = aligner_obj_model_index.internalPointer().obj
+        except AttributeError:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no aligner FlatCAM object selected..."))
+            return
+
+        self.align_type = self.a_type_radio.get_value()
+
+        # disengage the grid snapping since it will be hard to find the drills or pads on grid
+        if self.app.ui.grid_snap_btn.isChecked():
+            self.grid_status_memory = True
+            self.app.ui.grid_snap_btn.trigger()
+        else:
+            self.grid_status_memory = False
+
+        self.mr = self.canvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
+
+        if self.app.is_legacy is False:
+            self.canvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
+        else:
+            self.canvas.graph_event_disconnect(self.app.mr)
+
+        self.local_connected = True
+
+        self.aligner_old_fill_color = self.aligner_obj.fill_color
+        self.aligner_old_line_color = self.aligner_obj.outline_color
+        self.aligned_old_fill_color = self.aligned_obj.fill_color
+        self.aligned_old_line_color = self.aligned_obj.outline_color
+
+        self.app.inform.emit('%s: %s' % (_("First Point"), _("Click on the START point.")))
+        self.target_obj = self.aligned_obj
+        self.set_color()
+
+    def on_mouse_click_release(self, event):
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            right_button = 2
+            self.app.event_is_dragging = self.app.event_is_dragging
+        else:
+            event_pos = (event.xdata, event.ydata)
+            right_button = 3
+            self.app.event_is_dragging = self.app.ui.popMenu.mouse_is_panning
+
+        pos_canvas = self.canvas.translate_coords(event_pos)
+
+        if event.button == 1:
+            click_pt = Point([pos_canvas[0], pos_canvas[1]])
+
+            if self.app.selection_type is not None:
+                # delete previous selection shape
+                self.app.delete_selection_shape()
+                self.app.selection_type = None
+            else:
+                if self.target_obj.kind.lower() == 'excellon':
+                    for tool, tool_dict in self.target_obj.tools.items():
+                        for geo in tool_dict['solid_geometry']:
+                            if click_pt.within(geo):
+                                center_pt = geo.centroid
+                                self.clicked_points.append(
+                                    [
+                                        float('%.*f' % (self.decimals, center_pt.x)),
+                                        float('%.*f' % (self.decimals, center_pt.y))
+                                    ]
+                                )
+                                self.check_points()
+                elif self.target_obj.kind.lower() == 'gerber':
+                    for apid, apid_val in self.target_obj.apertures.items():
+                        for geo_el in apid_val['geometry']:
+                            if 'solid' in geo_el:
+                                if click_pt.within(geo_el['solid']):
+                                    if isinstance(geo_el['follow'], Point):
+                                        center_pt = geo_el['solid'].centroid
+                                        self.clicked_points.append(
+                                            [
+                                                float('%.*f' % (self.decimals, center_pt.x)),
+                                                float('%.*f' % (self.decimals, center_pt.y))
+                                            ]
+                                        )
+                                        self.check_points()
+
+        elif event.button == right_button and self.app.event_is_dragging is False:
+            self.reset_color()
+            self.clicked_points = list()
+            self.disconnect_cal_events()
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled by user request."))
+
+    def check_points(self):
+        if len(self.clicked_points) == 1:
+            self.app.inform.emit('%s: %s. %s' % (
+                _("First Point"), _("Click on the DESTINATION point."), _(" Or right click to cancel.")))
+            self.target_obj = self.aligner_obj
+            self.reset_color()
+            self.set_color()
+
+        if len(self.clicked_points) == 2:
+            if self.align_type == 'sp':
+                self.align_translate()
+                self.app.inform.emit('[success] %s' % _("Done."))
+                self.app.plot_all()
+
+                self.disconnect_cal_events()
+                return
+            else:
+                self.app.inform.emit('%s: %s. %s' % (
+                    _("Second Point"), _("Click on the START point."), _(" Or right click to cancel.")))
+                self.target_obj = self.aligned_obj
+                self.reset_color()
+                self.set_color()
+
+        if len(self.clicked_points) == 3:
+            self.app.inform.emit('%s: %s. %s' % (
+                _("Second Point"), _("Click on the DESTINATION point."), _(" Or right click to cancel.")))
+            self.target_obj = self.aligner_obj
+            self.reset_color()
+            self.set_color()
+
+        if len(self.clicked_points) == 4:
+            self.align_translate()
+            self.align_rotate()
+            self.app.inform.emit('[success] %s' % _("Done."))
+
+            self.disconnect_cal_events()
+            self.app.plot_all()
+
+    def align_translate(self):
+        dx = self.clicked_points[1][0] - self.clicked_points[0][0]
+        dy = self.clicked_points[1][1] - self.clicked_points[0][1]
+
+        self.aligned_obj.offset((dx, dy))
+
+        # Update the object bounding box options
+        a, b, c, d = self.aligned_obj.bounds()
+        self.aligned_obj.options['xmin'] = a
+        self.aligned_obj.options['ymin'] = b
+        self.aligned_obj.options['xmax'] = c
+        self.aligned_obj.options['ymax'] = d
+
+    def align_rotate(self):
+        dx = self.clicked_points[1][0] - self.clicked_points[0][0]
+        dy = self.clicked_points[1][1] - self.clicked_points[0][1]
+
+        test_rotation_pt = translate(Point(self.clicked_points[2]), xoff=dx, yoff=dy)
+        new_start = (test_rotation_pt.x, test_rotation_pt.y)
+        new_dest = self.clicked_points[3]
+
+        origin_pt = self.clicked_points[1]
+
+        dxd = new_dest[0] - origin_pt[0]
+        dyd = new_dest[1] - origin_pt[1]
+
+        dxs = new_start[0] - origin_pt[0]
+        dys = new_start[1] - origin_pt[1]
+
+        rotation_not_needed = (abs(new_start[0] - new_dest[0]) <= (10 ** -self.decimals)) or \
+                              (abs(new_start[1] - new_dest[1]) <= (10 ** -self.decimals))
+        if rotation_not_needed is False:
+            # calculate rotation angle
+            angle_dest = math.degrees(math.atan(dyd / dxd))
+            angle_start = math.degrees(math.atan(dys / dxs))
+            angle = angle_dest - angle_start
+            self.aligned_obj.rotate(angle=angle, point=origin_pt)
+
+    def disconnect_cal_events(self):
+        # restore the Grid snapping if it was active before
+        if self.grid_status_memory is True:
+            self.app.ui.grid_snap_btn.trigger()
+
+        self.app.mr = self.canvas.graph_event_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
+
+        if self.app.is_legacy is False:
+            self.canvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
+        else:
+            self.canvas.graph_event_disconnect(self.mr)
+
+        self.local_connected = False
+
+        self.aligner_old_fill_color = None
+        self.aligner_old_line_color = None
+        self.aligned_old_fill_color = None
+        self.aligned_old_line_color = None
+
+    def set_color(self):
+        new_color = "#15678abf"
+        new_line_color = new_color
+        self.target_obj.shapes.redraw(
+            update_colors=(new_color, new_line_color)
+        )
+
+    def reset_color(self):
+        self.aligned_obj.shapes.redraw(
+            update_colors=(self.aligned_old_fill_color, self.aligned_old_line_color)
+        )
+
+        self.aligner_obj.shapes.redraw(
+            update_colors=(self.aligner_old_fill_color, self.aligner_old_line_color)
+        )
+
+    def reset_fields(self):
+        self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.aligner_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))

+ 4 - 3
flatcamTools/ToolDblSided.py

@@ -533,16 +533,17 @@ class DblSidedTool(FlatCAMTool):
                                                           "Add them and retry."))
                                                           "Add them and retry."))
             return
             return
 
 
-        drills = []
+        drills = list()
 
 
         for hole in holes:
         for hole in holes:
             point = Point(hole)
             point = Point(hole)
             point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
             point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
             drills.append({"point": point, "tool": "1"})
             drills.append({"point": point, "tool": "1"})
             drills.append({"point": point_mirror, "tool": "1"})
             drills.append({"point": point_mirror, "tool": "1"})
-            if 'solid_geometry' not in tools:
-                tools["1"]['solid_geometry'] = []
+            if 'solid_geometry' not in tools["1"]:
+                tools["1"]['solid_geometry'] = list()
             else:
             else:
+                tools["1"]['solid_geometry'].append(point)
                 tools["1"]['solid_geometry'].append(point_mirror)
                 tools["1"]['solid_geometry'].append(point_mirror)
 
 
         def obj_init(obj_inst, app_inst):
         def obj_init(obj_inst, app_inst):

+ 13 - 10
flatcamTools/ToolDistance.py

@@ -361,11 +361,12 @@ class Distance(FlatCAMTool):
                 self.distance_x_entry.set_value('%.*f' % (self.decimals, abs(dx)))
                 self.distance_x_entry.set_value('%.*f' % (self.decimals, abs(dx)))
                 self.distance_y_entry.set_value('%.*f' % (self.decimals, abs(dy)))
                 self.distance_y_entry.set_value('%.*f' % (self.decimals, abs(dy)))
 
 
-                try:
-                    angle = math.degrees(math.atan(dy / dx))
-                    self.angle_entry.set_value('%.*f' % (self.decimals, angle))
-                except Exception as e:
-                    pass
+                if dx != 0.0:
+                    try:
+                        angle = math.degrees(math.atan(dy / dx))
+                        self.angle_entry.set_value('%.*f' % (self.decimals, angle))
+                    except Exception as e:
+                        pass
 
 
                 self.total_distance_entry.set_value('%.*f' % (self.decimals, abs(d)))
                 self.total_distance_entry.set_value('%.*f' % (self.decimals, abs(d)))
                 self.app.ui.rel_position_label.setText(
                 self.app.ui.rel_position_label.setText(
@@ -424,11 +425,13 @@ class Distance(FlatCAMTool):
             if len(self.points) == 1:
             if len(self.points) == 1:
                 self.utility_geometry(pos=pos)
                 self.utility_geometry(pos=pos)
                 # and display the temporary angle
                 # and display the temporary angle
-                try:
-                    angle = math.degrees(math.atan(dy / dx))
-                    self.angle_entry.set_value('%.*f' % (self.decimals, angle))
-                except Exception as e:
-                    pass
+                if dx != 0.0:
+                    try:
+                        angle = math.degrees(math.atan(dy / dx))
+                        self.angle_entry.set_value('%.*f' % (self.decimals, angle))
+                    except Exception as e:
+                        log.debug("Distance.on_mouse_move_meas() -> update utility geometry -> %s" % str(e))
+                        pass
 
 
         except Exception as e:
         except Exception as e:
             log.debug("Distance.on_mouse_move_meas() --> %s" % str(e))
             log.debug("Distance.on_mouse_move_meas() --> %s" % str(e))

+ 697 - 0
flatcamTools/ToolExtractDrills.py

@@ -0,0 +1,697 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Date: 1/10/2020                                          #
+# MIT Licence                                              #
+# ##########################################################
+
+from PyQt5 import QtWidgets, QtCore
+
+from FlatCAMTool import FlatCAMTool
+from flatcamGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox
+
+from shapely.geometry import Point
+
+import logging
+import gettext
+import FlatCAMTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+log = logging.getLogger('base')
+
+
+class ToolExtractDrills(FlatCAMTool):
+
+    toolName = _("Extract Drills")
+
+    def __init__(self, app):
+        FlatCAMTool.__init__(self, app)
+        self.decimals = self.app.decimals
+
+        # ## Title
+        title_label = QtWidgets.QLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                        QLabel
+                        {
+                            font-size: 16px;
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(title_label)
+
+        self.empty_lb = QtWidgets.QLabel("")
+        self.layout.addWidget(self.empty_lb)
+
+        # ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 1)
+        grid_lay.setColumnStretch(1, 0)
+
+        # ## Gerber Object
+        self.gerber_object_combo = QtWidgets.QComboBox()
+        self.gerber_object_combo.setModel(self.app.collection)
+        self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.gerber_object_combo.setCurrentIndex(1)
+
+        self.grb_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
+        self.grb_label.setToolTip('%s.' % _("Gerber from which to extract drill holes"))
+
+        # grid_lay.addRow("Bottom Layer:", self.object_combo)
+        grid_lay.addWidget(self.grb_label, 0, 0, 1, 2)
+        grid_lay.addWidget(self.gerber_object_combo, 1, 0, 1, 2)
+
+        self.padt_label = QtWidgets.QLabel("<b>%s</b>" % _("Processed Pads Type"))
+        self.padt_label.setToolTip(
+            _("The type of pads shape to be processed.\n"
+              "If the PCB has many SMD pads with rectangular pads,\n"
+              "disable the Rectangular aperture.")
+        )
+
+        grid_lay.addWidget(self.padt_label, 2, 0, 1, 2)
+
+        # Circular Aperture Selection
+        self.circular_cb = FCCheckBox('%s' % _("Circular"))
+        self.circular_cb.setToolTip(
+            _("Create drills from circular pads.")
+        )
+
+        grid_lay.addWidget(self.circular_cb, 3, 0, 1, 2)
+
+        # Oblong Aperture Selection
+        self.oblong_cb = FCCheckBox('%s' % _("Oblong"))
+        self.oblong_cb.setToolTip(
+            _("Create drills from oblong pads.")
+        )
+
+        grid_lay.addWidget(self.oblong_cb, 4, 0, 1, 2)
+
+        # Square Aperture Selection
+        self.square_cb = FCCheckBox('%s' % _("Square"))
+        self.square_cb.setToolTip(
+            _("Create drills from square pads.")
+        )
+
+        grid_lay.addWidget(self.square_cb, 5, 0, 1, 2)
+
+        # Rectangular Aperture Selection
+        self.rectangular_cb = FCCheckBox('%s' % _("Rectangular"))
+        self.rectangular_cb.setToolTip(
+            _("Create drills from rectangular pads.")
+        )
+
+        grid_lay.addWidget(self.rectangular_cb, 6, 0, 1, 2)
+
+        # Others type of Apertures Selection
+        self.other_cb = FCCheckBox('%s' % _("Others"))
+        self.other_cb.setToolTip(
+            _("Create drills from other types of pad shape.")
+        )
+
+        grid_lay.addWidget(self.other_cb, 7, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line, 8, 0, 1, 2)
+
+        # ## Grid Layout
+        grid1 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid1)
+        grid1.setColumnStretch(0, 0)
+        grid1.setColumnStretch(1, 1)
+
+        self.method_label = QtWidgets.QLabel('<b>%s</b>' % _("Method"))
+        grid1.addWidget(self.method_label, 2, 0, 1, 2)
+
+        # ## Axis
+        self.hole_size_radio = RadioSet(
+            [
+                {'label': _("Fixed Diameter"), 'value': 'fixed'},
+                {'label': _("Fixed Annular Ring"), 'value': 'ring'},
+                {'label': _("Proportional"), 'value': 'prop'}
+            ],
+            orientation='vertical',
+            stretch=False)
+
+        self.hole_size_label = QtWidgets.QLabel('%s:' % _("Hole Size"))
+        self.hole_size_label.setToolTip(
+            _("The selected method of extracting the drills. Can be:\n"
+              "- Fixed Diameter -> all holes will have a set size\n"
+              "- Fixed Annular Ring -> all holes will have a set annular ring\n"
+              "- Proportional -> each hole size will be a fraction of the pad size"))
+
+        grid1.addWidget(self.hole_size_label, 3, 0)
+        grid1.addWidget(self.hole_size_radio, 3, 1)
+
+        # grid_lay1.addWidget(QtWidgets.QLabel(''))
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid1.addWidget(separator_line, 5, 0, 1, 2)
+
+        # Annular Ring
+        self.fixed_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Diameter"))
+        grid1.addWidget(self.fixed_label, 6, 0, 1, 2)
+
+        # Diameter value
+        self.dia_entry = FCDoubleSpinner()
+        self.dia_entry.set_precision(self.decimals)
+        self.dia_entry.set_range(0.0000, 9999.9999)
+
+        self.dia_label = QtWidgets.QLabel('%s:' % _("Value"))
+        self.dia_label.setToolTip(
+            _("Fixed hole diameter.")
+        )
+
+        grid1.addWidget(self.dia_label, 8, 0)
+        grid1.addWidget(self.dia_entry, 8, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid1.addWidget(separator_line, 9, 0, 1, 2)
+
+        self.ring_frame = QtWidgets.QFrame()
+        self.ring_frame.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.ring_frame)
+
+        self.ring_box = QtWidgets.QVBoxLayout()
+        self.ring_box.setContentsMargins(0, 0, 0, 0)
+        self.ring_frame.setLayout(self.ring_box)
+
+        # ## Grid Layout
+        grid2 = QtWidgets.QGridLayout()
+        grid2.setColumnStretch(0, 0)
+        grid2.setColumnStretch(1, 1)
+        self.ring_box.addLayout(grid2)
+
+        # Annular Ring value
+        self.ring_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Annular Ring"))
+        self.ring_label.setToolTip(
+            _("The size of annular ring.\n"
+              "The copper sliver between the drill hole exterior\n"
+              "and the margin of the copper pad.")
+        )
+        grid2.addWidget(self.ring_label, 0, 0, 1, 2)
+
+        # Circular Annular Ring Value
+        self.circular_ring_label = QtWidgets.QLabel('%s:' % _("Circular"))
+        self.circular_ring_label.setToolTip(
+            _("The size of annular ring for circular pads.")
+        )
+
+        self.circular_ring_entry = FCDoubleSpinner()
+        self.circular_ring_entry.set_precision(self.decimals)
+        self.circular_ring_entry.set_range(0.0000, 9999.9999)
+
+        grid2.addWidget(self.circular_ring_label, 1, 0)
+        grid2.addWidget(self.circular_ring_entry, 1, 1)
+
+        # Oblong Annular Ring Value
+        self.oblong_ring_label = QtWidgets.QLabel('%s:' % _("Oblong"))
+        self.oblong_ring_label.setToolTip(
+            _("The size of annular ring for oblong pads.")
+        )
+
+        self.oblong_ring_entry = FCDoubleSpinner()
+        self.oblong_ring_entry.set_precision(self.decimals)
+        self.oblong_ring_entry.set_range(0.0000, 9999.9999)
+
+        grid2.addWidget(self.oblong_ring_label, 2, 0)
+        grid2.addWidget(self.oblong_ring_entry, 2, 1)
+
+        # Square Annular Ring Value
+        self.square_ring_label = QtWidgets.QLabel('%s:' % _("Square"))
+        self.square_ring_label.setToolTip(
+            _("The size of annular ring for square pads.")
+        )
+
+        self.square_ring_entry = FCDoubleSpinner()
+        self.square_ring_entry.set_precision(self.decimals)
+        self.square_ring_entry.set_range(0.0000, 9999.9999)
+
+        grid2.addWidget(self.square_ring_label, 3, 0)
+        grid2.addWidget(self.square_ring_entry, 3, 1)
+
+        # Rectangular Annular Ring Value
+        self.rectangular_ring_label = QtWidgets.QLabel('%s:' % _("Rectangular"))
+        self.rectangular_ring_label.setToolTip(
+            _("The size of annular ring for rectangular pads.")
+        )
+
+        self.rectangular_ring_entry = FCDoubleSpinner()
+        self.rectangular_ring_entry.set_precision(self.decimals)
+        self.rectangular_ring_entry.set_range(0.0000, 9999.9999)
+
+        grid2.addWidget(self.rectangular_ring_label, 4, 0)
+        grid2.addWidget(self.rectangular_ring_entry, 4, 1)
+
+        # Others Annular Ring Value
+        self.other_ring_label = QtWidgets.QLabel('%s:' % _("Others"))
+        self.other_ring_label.setToolTip(
+            _("The size of annular ring for other pads.")
+        )
+
+        self.other_ring_entry = FCDoubleSpinner()
+        self.other_ring_entry.set_precision(self.decimals)
+        self.other_ring_entry.set_range(0.0000, 9999.9999)
+
+        grid2.addWidget(self.other_ring_label, 5, 0)
+        grid2.addWidget(self.other_ring_entry, 5, 1)
+
+        grid3 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid3)
+        grid3.setColumnStretch(0, 0)
+        grid3.setColumnStretch(1, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid3.addWidget(separator_line, 1, 0, 1, 2)
+
+        # Annular Ring value
+        self.prop_label = QtWidgets.QLabel('<b>%s</b>' % _("Proportional Diameter"))
+        grid3.addWidget(self.prop_label, 2, 0, 1, 2)
+
+        # Diameter value
+        self.factor_entry = FCDoubleSpinner(suffix='%')
+        self.factor_entry.set_precision(self.decimals)
+        self.factor_entry.set_range(0.0000, 100.0000)
+        self.factor_entry.setSingleStep(0.1)
+
+        self.factor_label = QtWidgets.QLabel('%s:' % _("Value"))
+        self.factor_label.setToolTip(
+            _("Proportional Diameter.\n"
+              "The drill diameter will be a fraction of the pad size.")
+        )
+
+        grid3.addWidget(self.factor_label, 3, 0)
+        grid3.addWidget(self.factor_entry, 3, 1)
+
+        # Extract drills from Gerber apertures flashes (pads)
+        self.e_drills_button = QtWidgets.QPushButton(_("Extract Drills"))
+        self.e_drills_button.setToolTip(
+            _("Extract drills from a given Gerber file.")
+        )
+        self.e_drills_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.layout.addWidget(self.e_drills_button)
+
+        self.layout.addStretch()
+
+        # ## Reset Tool
+        self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
+        self.reset_button.setToolTip(
+            _("Will reset the tool parameters.")
+        )
+        self.reset_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.layout.addWidget(self.reset_button)
+
+        self.circular_ring_entry.setEnabled(False)
+        self.oblong_ring_entry.setEnabled(False)
+        self.square_ring_entry.setEnabled(False)
+        self.rectangular_ring_entry.setEnabled(False)
+        self.other_ring_entry.setEnabled(False)
+
+        self.dia_entry.setDisabled(True)
+        self.dia_label.setDisabled(True)
+        self.factor_label.setDisabled(True)
+        self.factor_entry.setDisabled(True)
+
+        self.ring_frame.setDisabled(True)
+
+        # ## Signals
+        self.hole_size_radio.activated_custom.connect(self.on_hole_size_toggle)
+        self.e_drills_button.clicked.connect(self.on_extract_drills_click)
+        self.reset_button.clicked.connect(self.set_tool_ui)
+
+        self.circular_cb.stateChanged.connect(
+            lambda state:
+                self.circular_ring_entry.setDisabled(False) if state else self.circular_ring_entry.setDisabled(True)
+        )
+
+        self.oblong_cb.stateChanged.connect(
+            lambda state:
+            self.oblong_ring_entry.setDisabled(False) if state else self.oblong_ring_entry.setDisabled(True)
+        )
+
+        self.square_cb.stateChanged.connect(
+            lambda state:
+            self.square_ring_entry.setDisabled(False) if state else self.square_ring_entry.setDisabled(True)
+        )
+
+        self.rectangular_cb.stateChanged.connect(
+            lambda state:
+            self.rectangular_ring_entry.setDisabled(False) if state else self.rectangular_ring_entry.setDisabled(True)
+        )
+
+        self.other_cb.stateChanged.connect(
+            lambda state:
+            self.other_ring_entry.setDisabled(False) if state else self.other_ring_entry.setDisabled(True)
+        )
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+I', **kwargs)
+
+    def run(self, toggle=True):
+        self.app.report_usage("Extract Drills()")
+
+        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:
+                        # if tab is populated with the tool but it does not have the focus, focus on it
+                        if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
+                            # focus on Tool Tab
+                            self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+                        else:
+                            self.app.ui.splitter.setSizes([0, 1])
+                except AttributeError:
+                    pass
+        else:
+            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, _("Extract Drills Tool"))
+
+    def set_tool_ui(self):
+        self.reset_fields()
+
+        self.hole_size_radio.set_value(self.app.defaults["tools_edrills_hole_type"])
+
+        self.dia_entry.set_value(float(self.app.defaults["tools_edrills_hole_fixed_dia"]))
+
+        self.circular_ring_entry.set_value(float(self.app.defaults["tools_edrills_circular_ring"]))
+        self.oblong_ring_entry.set_value(float(self.app.defaults["tools_edrills_oblong_ring"]))
+        self.square_ring_entry.set_value(float(self.app.defaults["tools_edrills_square_ring"]))
+        self.rectangular_ring_entry.set_value(float(self.app.defaults["tools_edrills_rectangular_ring"]))
+        self.other_ring_entry.set_value(float(self.app.defaults["tools_edrills_others_ring"]))
+
+        self.circular_cb.set_value(self.app.defaults["tools_edrills_circular"])
+        self.oblong_cb.set_value(self.app.defaults["tools_edrills_oblong"])
+        self.square_cb.set_value(self.app.defaults["tools_edrills_square"])
+        self.rectangular_cb.set_value(self.app.defaults["tools_edrills_rectangular"])
+        self.other_cb.set_value(self.app.defaults["tools_edrills_others"])
+
+        self.factor_entry.set_value(float(self.app.defaults["tools_edrills_hole_prop_factor"]))
+
+    def on_extract_drills_click(self):
+
+        drill_dia = self.dia_entry.get_value()
+        circ_r_val = self.circular_ring_entry.get_value()
+        oblong_r_val = self.oblong_ring_entry.get_value()
+        square_r_val = self.square_ring_entry.get_value()
+        rect_r_val = self.rectangular_ring_entry.get_value()
+        other_r_val = self.other_ring_entry.get_value()
+
+        prop_factor = self.factor_entry.get_value() / 100.0
+
+        drills = list()
+        tools = dict()
+
+        selection_index = self.gerber_object_combo.currentIndex()
+        model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
+
+        try:
+            fcobj = model_index.internalPointer().obj
+        except Exception:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
+            return
+
+        outname = fcobj.options['name'].rpartition('.')[0]
+
+        mode = self.hole_size_radio.get_value()
+
+        if mode == 'fixed':
+            tools = {"1": {"C": drill_dia}}
+            for apid, apid_value in fcobj.apertures.items():
+                ap_type = apid_value['type']
+
+                if ap_type == 'C':
+                    if self.circular_cb.get_value() is False:
+                        continue
+                elif ap_type == 'O':
+                    if self.oblong_cb.get_value() is False:
+                        continue
+                elif ap_type == 'R':
+                    width = float(apid_value['width'])
+                    height = float(apid_value['height'])
+
+                    # if the height == width (float numbers so the reason for the following)
+                    if round(width, self.decimals) == round(height, self.decimals):
+                        if self.square_cb.get_value() is False:
+                            continue
+                    else:
+                        if self.rectangular_cb.get_value() is False:
+                            continue
+                else:
+                    if self.other_cb.get_value() is False:
+                        continue
+
+                for geo_el in apid_value['geometry']:
+                    if 'follow' in geo_el and isinstance(geo_el['follow'], Point):
+                        drills.append({"point": geo_el['follow'], "tool": "1"})
+                        if 'solid_geometry' not in tools["1"]:
+                            tools["1"]['solid_geometry'] = list()
+                        else:
+                            tools["1"]['solid_geometry'].append(geo_el['follow'])
+
+            if 'solid_geometry' not in tools["1"] or not tools["1"]['solid_geometry']:
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("No drills extracted. Try different parameters."))
+                return
+        elif mode == 'ring':
+            drills_found = set()
+            for apid, apid_value in fcobj.apertures.items():
+                ap_type = apid_value['type']
+
+                dia = None
+                if ap_type == 'C':
+                    if self.circular_cb.get_value():
+                        dia = float(apid_value['size']) - (2 * circ_r_val)
+                elif ap_type == 'O':
+                    width = float(apid_value['width'])
+                    height = float(apid_value['height'])
+                    if self.oblong_cb.get_value():
+                        if width > height:
+                            dia = float(apid_value['height']) - (2 * oblong_r_val)
+                        else:
+                            dia = float(apid_value['width']) - (2 * oblong_r_val)
+                elif ap_type == 'R':
+                    width = float(apid_value['width'])
+                    height = float(apid_value['height'])
+
+                    # if the height == width (float numbers so the reason for the following)
+                    if abs(float('%.*f' % (self.decimals, width)) - float('%.*f' % (self.decimals, height))) < \
+                            (10 ** -self.decimals):
+                        if self.square_cb.get_value():
+                            dia = float(apid_value['height']) - (2 * square_r_val)
+                    else:
+                        if self.rectangular_cb.get_value():
+                            if width > height:
+                                dia = float(apid_value['height']) - (2 * rect_r_val)
+                            else:
+                                dia = float(apid_value['width']) - (2 * rect_r_val)
+                else:
+                    if self.other_cb.get_value():
+                        try:
+                            dia = float(apid_value['size']) - (2 * other_r_val)
+                        except KeyError:
+                            if ap_type == 'AM':
+                                pol = apid_value['geometry'][0]['solid']
+                                x0, y0, x1, y1 = pol.bounds
+                                dx = x1 - x0
+                                dy = y1 - y0
+                                if dx <= dy:
+                                    dia = dx - (2 * other_r_val)
+                                else:
+                                    dia = dy - (2 * other_r_val)
+
+                # if dia is None then none of the above applied so we skip the following
+                if dia is None:
+                    continue
+
+                tool_in_drills = False
+                for tool, tool_val in tools.items():
+                    if abs(float('%.*f' % (self.decimals, tool_val["C"])) - float('%.*f' % (self.decimals, dia))) < \
+                            (10 ** -self.decimals):
+                        tool_in_drills = tool
+
+                if tool_in_drills is False:
+                    if tools:
+                        new_tool = max([int(t) for t in tools]) + 1
+                        tool_in_drills = str(new_tool)
+                    else:
+                        tool_in_drills = "1"
+
+                for geo_el in apid_value['geometry']:
+                    if 'follow' in geo_el and isinstance(geo_el['follow'], Point):
+                        if tool_in_drills not in tools:
+                            tools[tool_in_drills] = {"C": dia}
+
+                        drills.append({"point": geo_el['follow'], "tool": tool_in_drills})
+
+                        if 'solid_geometry' not in tools[tool_in_drills]:
+                            tools[tool_in_drills]['solid_geometry'] = list()
+                        else:
+                            tools[tool_in_drills]['solid_geometry'].append(geo_el['follow'])
+
+                if tool_in_drills in tools:
+                    if 'solid_geometry' not in tools[tool_in_drills] or not tools[tool_in_drills]['solid_geometry']:
+                        drills_found.add(False)
+                    else:
+                        drills_found.add(True)
+
+            if True not in drills_found:
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("No drills extracted. Try different parameters."))
+                return
+        else:
+            drills_found = set()
+            for apid, apid_value in fcobj.apertures.items():
+                ap_type = apid_value['type']
+
+                dia = None
+                if ap_type == 'C':
+                    if self.circular_cb.get_value():
+                        dia = float(apid_value['size']) * prop_factor
+                elif ap_type == 'O':
+                    width = float(apid_value['width'])
+                    height = float(apid_value['height'])
+                    if self.oblong_cb.get_value():
+                        if width > height:
+                            dia = float(apid_value['height']) * prop_factor
+                        else:
+                            dia = float(apid_value['width']) * prop_factor
+                elif ap_type == 'R':
+                    width = float(apid_value['width'])
+                    height = float(apid_value['height'])
+
+                    # if the height == width (float numbers so the reason for the following)
+                    if abs(float('%.*f' % (self.decimals, width)) - float('%.*f' % (self.decimals, height))) < \
+                            (10 ** -self.decimals):
+                        if self.square_cb.get_value():
+                            dia = float(apid_value['height']) * prop_factor
+                    else:
+                        if self.rectangular_cb.get_value():
+                            if width > height:
+                                dia = float(apid_value['height']) * prop_factor
+                            else:
+                                dia = float(apid_value['width']) * prop_factor
+                else:
+                    if self.other_cb.get_value():
+                        try:
+                            dia = float(apid_value['size']) * prop_factor
+                        except KeyError:
+                            if ap_type == 'AM':
+                                pol = apid_value['geometry'][0]['solid']
+                                x0, y0, x1, y1 = pol.bounds
+                                dx = x1 - x0
+                                dy = y1 - y0
+                                if dx <= dy:
+                                    dia = dx * prop_factor
+                                else:
+                                    dia = dy * prop_factor
+
+                # if dia is None then none of the above applied so we skip the following
+                if dia is None:
+                    continue
+
+                tool_in_drills = False
+                for tool, tool_val in tools.items():
+                    if abs(float('%.*f' % (self.decimals, tool_val["C"])) - float('%.*f' % (self.decimals, dia))) < \
+                            (10 ** -self.decimals):
+                        tool_in_drills = tool
+
+                if tool_in_drills is False:
+                    if tools:
+                        new_tool = max([int(t) for t in tools]) + 1
+                        tool_in_drills = str(new_tool)
+                    else:
+                        tool_in_drills = "1"
+
+                for geo_el in apid_value['geometry']:
+                    if 'follow' in geo_el and isinstance(geo_el['follow'], Point):
+                        if tool_in_drills not in tools:
+                            tools[tool_in_drills] = {"C": dia}
+
+                        drills.append({"point": geo_el['follow'], "tool": tool_in_drills})
+
+                        if 'solid_geometry' not in tools[tool_in_drills]:
+                            tools[tool_in_drills]['solid_geometry'] = list()
+                        else:
+                            tools[tool_in_drills]['solid_geometry'].append(geo_el['follow'])
+
+                if tool_in_drills in tools:
+                    if 'solid_geometry' not in tools[tool_in_drills] or not tools[tool_in_drills]['solid_geometry']:
+                        drills_found.add(False)
+                    else:
+                        drills_found.add(True)
+
+            if True not in drills_found:
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("No drills extracted. Try different parameters."))
+                return
+
+        def obj_init(obj_inst, app_inst):
+            obj_inst.tools = tools
+            obj_inst.drills = drills
+            obj_inst.create_geometry()
+            obj_inst.source_file = self.app.export_excellon(obj_name=outname, local_use=obj_inst, filename=None,
+                                                            use_thread=False)
+
+        self.app.new_object("excellon", outname, obj_init)
+
+    def on_hole_size_toggle(self, val):
+        if val == "fixed":
+            self.fixed_label.setDisabled(False)
+            self.dia_entry.setDisabled(False)
+            self.dia_label.setDisabled(False)
+
+            self.ring_frame.setDisabled(True)
+
+            self.prop_label.setDisabled(True)
+            self.factor_label.setDisabled(True)
+            self.factor_entry.setDisabled(True)
+        elif val == "ring":
+            self.fixed_label.setDisabled(True)
+            self.dia_entry.setDisabled(True)
+            self.dia_label.setDisabled(True)
+
+            self.ring_frame.setDisabled(False)
+
+            self.prop_label.setDisabled(True)
+            self.factor_label.setDisabled(True)
+            self.factor_entry.setDisabled(True)
+        elif val == "prop":
+            self.fixed_label.setDisabled(True)
+            self.dia_entry.setDisabled(True)
+            self.dia_label.setDisabled(True)
+
+            self.ring_frame.setDisabled(True)
+
+            self.prop_label.setDisabled(False)
+            self.factor_label.setDisabled(False)
+            self.factor_entry.setDisabled(False)
+
+    def reset_fields(self):
+        self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
+        self.gerber_object_combo.setCurrentIndex(0)

+ 18 - 13
flatcamTools/ToolFilm.py

@@ -752,7 +752,7 @@ class Film(FlatCAMTool):
                                  skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
                                  skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
                                  skew_reference=skew_reference,
                                  skew_reference=skew_reference,
                                  mirror=mirror,
                                  mirror=mirror,
-                                 pagesize=pagesize, orientation=orientation, color=color, opacity=1.0,
+                                 pagesize_val=pagesize, orientation_val=orientation, color_val=color, opacity_val=1.0,
                                  ftype=ftype
                                  ftype=ftype
                                  )
                                  )
 
 
@@ -1080,23 +1080,28 @@ class Film(FlatCAMTool):
                         skew_factor_x=None, skew_factor_y=None, skew_reference='center',
                         skew_factor_x=None, skew_factor_y=None, skew_reference='center',
                         mirror=None,  orientation_val='p', pagesize_val='A4', color_val='black', opacity_val=1.0,
                         mirror=None,  orientation_val='p', pagesize_val='A4', color_val='black', opacity_val=1.0,
                         use_thread=True, ftype='svg'):
                         use_thread=True, ftype='svg'):
+
         """
         """
         Exports a Geometry Object to an SVG file in positive black.
         Exports a Geometry Object to an SVG file in positive black.
 
 
-        :param obj_name: the name of the FlatCAM object to be saved as SVG
-        :param box_name: the name of the FlatCAM object to be used as delimitation of the content to be saved
-        :param filename: Path to the SVG file to save to.
+        :param obj_name:            the name of the FlatCAM object to be saved
+        :param box_name:            the name of the FlatCAM object to be used as delimitation of the content to be saved
+        :param filename:            Path to the file to save to.
         :param scale_stroke_factor: factor by which to change/scale the thickness of the features
         :param scale_stroke_factor: factor by which to change/scale the thickness of the features
-        :param scale_factor_x: factor to scale the svg geometry on the X axis
-        :param scale_factor_y: factor to scale the svg geometry on the Y axis
-        :param skew_factor_x: factor to skew the svg geometry on the X axis
-        :param skew_factor_y: factor to skew the svg geometry on the Y axis
-        :param skew_reference: reference to use for skew. Can be 'bottomleft', 'bottomright', 'topleft', 'topright' and
-        those are the 4 points of the bounding box of the geometry to be skewed.
-        :param mirror: can be 'x' or 'y' or 'both'. Axis on which to mirror the svg geometry
+        :param scale_factor_x:      factor to scale the geometry on the X axis
+        :param scale_factor_y:      factor to scale the geometry on the Y axis
+        :param skew_factor_x:       factor to skew the geometry on the X axis
+        :param skew_factor_y:       factor to skew the geometry on the Y axis
+        :param skew_reference:      reference to use for skew. Can be 'bottomleft', 'bottomright', 'topleft',
+        'topright' and those are the 4 points of the bounding box of the geometry to be skewed.
+        :param mirror:              can be 'x' or 'y' or 'both'. Axis on which to mirror the svg geometry
+        :param orientation_val:
+        :param pagesize_val:
+        :param color_val:
+        :param opacity_val:
+        :param use_thread:          if to be run in a separate thread; boolean
+        :param ftype:               the type of file for saving the film: 'svg', 'png' or 'pdf'
 
 
-        :param use_thread: if to be run in a separate thread; boolean
-        :param ftype: the type of file for saving the film: 'svg', 'png' or 'pdf'
         :return:
         :return:
         """
         """
         self.app.report_usage("export_positive()")
         self.app.report_usage("export_positive()")

+ 1 - 1
flatcamTools/ToolNonCopperClear.py

@@ -651,7 +651,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         }
         }
 
 
         # #############################################################################
         # #############################################################################
-        # ############################ SGINALS ########################################
+        # ############################ 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.addtool_entry.returnPressed.connect(self.on_tool_add)

+ 3 - 2
flatcamTools/__init__.py

@@ -1,11 +1,12 @@
 import sys
 import sys
 
 
-
 from flatcamTools.ToolCalculators import ToolCalculator
 from flatcamTools.ToolCalculators import ToolCalculator
 from flatcamTools.ToolCalibration import ToolCalibration
 from flatcamTools.ToolCalibration import ToolCalibration
 from flatcamTools.ToolCutOut import CutOut
 from flatcamTools.ToolCutOut import CutOut
 
 
 from flatcamTools.ToolDblSided import DblSidedTool
 from flatcamTools.ToolDblSided import DblSidedTool
+from flatcamTools.ToolExtractDrills import ToolExtractDrills
+from flatcamTools.ToolAlignObjects import AlignObjects
 
 
 from flatcamTools.ToolFilm import Film
 from flatcamTools.ToolFilm import Film
 
 
@@ -17,10 +18,10 @@ from flatcamTools.ToolDistanceMin import DistanceMin
 from flatcamTools.ToolMove import ToolMove
 from flatcamTools.ToolMove import ToolMove
 
 
 from flatcamTools.ToolNonCopperClear import NonCopperClear
 from flatcamTools.ToolNonCopperClear import NonCopperClear
+from flatcamTools.ToolPaint import ToolPaint
 
 
 from flatcamTools.ToolOptimal import ToolOptimal
 from flatcamTools.ToolOptimal import ToolOptimal
 
 
-from flatcamTools.ToolPaint import ToolPaint
 from flatcamTools.ToolPanelize import Panelize
 from flatcamTools.ToolPanelize import Panelize
 from flatcamTools.ToolPcbWizard import PcbWizard
 from flatcamTools.ToolPcbWizard import PcbWizard
 from flatcamTools.ToolPDF import ToolPDF
 from flatcamTools.ToolPDF import ToolPDF

BIN
share/align16.png


BIN
share/align32.png


BIN
share/extract_drill16.png


BIN
share/extract_drill32.png


BIN
share/locate16.png


BIN
share/locate32.png