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

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 = 8.992
-    version_date = "2020/01/02"
+    version_date = "2020/01/20"
     beta = True
     engine = '3D'
 
@@ -240,6 +240,9 @@ class App(QtCore.QObject):
     # signal emitted when jumping
     jump_signal = pyqtSignal(tuple)
 
+    # signal emitted when jumping
+    locate_signal = pyqtSignal(tuple, str)
+
     # close app signal
     close_app_signal = pyqtSignal()
 
@@ -429,6 +432,7 @@ class App(QtCore.QObject):
             "global_stats": dict(),
             "global_tabs_detachable": True,
             "global_jump_ref": 'abs',
+            "global_locate_pt": 'bl',
             "global_tpdf_tmargin": 15.0,
             "global_tpdf_bmargin": 10.0,
             "global_tpdf_lmargin": 20.0,
@@ -524,8 +528,8 @@ class App(QtCore.QObject):
             "global_cursor_type": "small",
             "global_cursor_size": 20,
             "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_plot": True,
@@ -954,6 +958,24 @@ class App(QtCore.QObject):
             "tools_cal_toolchange_xy": '',
             "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
             # file associations
             "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_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
             # File associations
             "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.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.menueditselectall.triggered.connect(self.on_selectall)
@@ -2464,12 +2502,14 @@ class App(QtCore.QObject):
         self.qrcode_tool = None
         self.copper_thieving_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
         try:
             self.install_tools()
-        except AttributeError:
-            pass
+        except AttributeError as e:
+            log.debug("App.__init__() install tools() --> %s" % str(e))
 
         # ##################################################################################
         # ########################### SETUP RECENT ITEMS ###################################
@@ -3017,13 +3057,6 @@ class App(QtCore.QObject):
 
         :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.install(icon=QtGui.QIcon(self.resource_location + '/distance16.png'), pos=self.ui.menuedit,
                                    before=self.ui.menueditorigin,
@@ -3035,6 +3068,20 @@ class App(QtCore.QObject):
                                        before=self.ui.menueditorigin,
                                        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.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.origin_btn.triggered.connect(self.on_set_origin)
         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.new_script_btn.triggered.connect(self.on_filenewscript)
@@ -3208,6 +3256,9 @@ class App(QtCore.QObject):
         # Tools Toolbar Signals
         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.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.ncc_btn.triggered.connect(lambda: self.ncclear_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['ymax'] = ymax
             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"
 
+            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
         self.myKeywords.append(obj.options['name'])
 
@@ -7140,15 +7202,13 @@ class App(QtCore.QObject):
                     obj.options['ymin'] = b
                     obj.options['xmax'] = c
                     obj.options['ymax'] = d
-                self.inform.emit('[success] %s...' %
-                                 _('Origin set'))
+                self.inform.emit('[success] %s...' % _('Origin set'))
                 if noplot_sig is False:
                     self.replot_signal.emit([])
 
         if location is not None:
             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'
 
             x, y = location
@@ -7235,7 +7295,151 @@ class App(QtCore.QObject):
 
         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:
             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'],
                                                          face_color=self.defaults['global_alt_sel_fill'])
                         self.selection_type = False
-                    elif dx > 0:
+                    elif dx >= 0:
                         self.draw_moving_selection_shape(self.pos, pos)
                         self.selection_type = True
                     else:
@@ -8862,6 +9066,7 @@ class App(QtCore.QObject):
         pt4 = (float(sel_obj.options['xmin']), float(sel_obj.options['ymax']))
 
         sel_rect = Polygon([pt1, pt2, pt3, pt4])
+
         if self.defaults['units'].upper() == 'MM':
             sel_rect = sel_rect.buffer(-0.1)
             sel_rect = sel_rect.buffer(0.2)
@@ -10378,7 +10583,8 @@ class App(QtCore.QObject):
         self.report_usage("export_svg()")
 
         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()")
 
@@ -10446,7 +10652,8 @@ class App(QtCore.QObject):
         self.report_usage("save source file()")
 
         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()")
 
@@ -10489,7 +10696,10 @@ class App(QtCore.QObject):
         self.report_usage("export_excellon()")
 
         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()")
 
@@ -10645,7 +10855,8 @@ class App(QtCore.QObject):
         self.report_usage("export_gerber()")
 
         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()")
 
@@ -10781,7 +10992,8 @@ class App(QtCore.QObject):
         self.report_usage("export_dxf()")
 
         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()")
 
@@ -11983,8 +12195,13 @@ class App(QtCore.QObject):
             plot_container = container
         else:
             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:
                 self.plotcanvas = PlotCanvas(plot_container, self)
             except Exception as er:
@@ -11997,13 +12214,9 @@ class App(QtCore.QObject):
                 msg += msg_txt
                 self.inform.emit(msg)
                 return 'fail'
-        else:
-            self.plotcanvas = PlotCanvasLegacy(plot_container, self)
-        print("step_2")
 
         # So it can receive key presses
         self.plotcanvas.native.setFocus()
-        print("step_3")
 
         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)
@@ -12012,28 +12225,22 @@ class App(QtCore.QObject):
 
         # Keys over plot enabled
         self.kp = self.plotcanvas.graph_event_connect('key_press', self.ui.keyPressEvent)
-        print("step_4")
 
         if self.defaults['global_cursor_type'] == 'small':
             self.app_cursor = self.plotcanvas.new_cursor()
         else:
             self.app_cursor = self.plotcanvas.new_cursor(big=True)
 
-        print("step_5")
-
         if self.ui.grid_snap_btn.isChecked():
             self.app_cursor.enabled = True
         else:
             self.app_cursor.enabled = False
 
-        print("step_6")
-
         if self.is_legacy is False:
             self.hover_shapes = ShapeCollection(parent=self.plotcanvas.view.scene, layers=1)
         else:
             # will use the default Matplotlib axes
             self.hover_shapes = ShapeCollectionLegacy(obj=self, app=self, name='hover')
-        print("step_7")
 
     def on_zoom_fit(self, event):
         """
@@ -12262,19 +12469,12 @@ class App(QtCore.QObject):
         new_line_color = color_variant(new_color[:7], 0.7)
 
         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):
         if state:

+ 8 - 8
FlatCAMObj.py

@@ -1307,7 +1307,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             else:
                 iso_name = outname
 
-            # TODO: This is ugly. Create way to pass data into init function.
             def iso_init(geo_obj, app_obj):
                 # Propagate options
                 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)
 
                     # 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)
 
                     if geom == 'fail':
@@ -1438,7 +1437,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                     else:
                         iso_name = outname
 
-                # TODO: This is ugly. Create way to pass data into init function.
                 def iso_init(geo_obj, app_obj):
                     # Propagate options
                     geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
@@ -1448,9 +1446,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
                         geo_obj.tool_type = 'C1'
 
                     # 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,
                                                   nr_passes=i)
 
@@ -2641,7 +2638,10 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         horizontal_header.setDefaultSectionSize(70)
         horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
         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(3, QtWidgets.QHeaderView.ResizeToContents)
         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
 - 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
 - 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
 - updates in NCC Tool
 
-6.01.2019
+6.01.2020
 
 - working on new NCC Tool
 

+ 17 - 18
camlib.py

@@ -458,8 +458,8 @@ class Geometry(object):
     """
 
     defaults = {
-        "units": 'in',
-        "geo_steps_per_circle": 64
+        "units": 'mm',
+        # "geo_steps_per_circle": 128
     }
 
     def __init__(self, geo_steps_per_circle=None):
@@ -528,13 +528,13 @@ class Geometry(object):
             self.solid_geometry = []
 
         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
 
         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:
             log.error("Failed to run union on polygons. %s" % str(e))
             return
@@ -944,7 +944,7 @@ class Geometry(object):
                     geo_iso.append(pol)
                 else:
                     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
                 disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
 
@@ -959,8 +959,7 @@ class Geometry(object):
                 geo_iso.append(working_geo)
             else:
                 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"))
         geo_iso = unary_union(geo_iso)
@@ -1225,7 +1224,7 @@ class Geometry(object):
 
         # Can only result in a Polygon or MultiPolygon
         # 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:
             # Otherwise, trying to to insert current.exterior == None
             # into the FlatCAMStorage will fail.
@@ -1254,7 +1253,7 @@ class Geometry(object):
             QtWidgets.QApplication.processEvents()
 
             # 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:
 
                 # current can be a MultiPolygon
@@ -1372,11 +1371,12 @@ class Geometry(object):
 
         # Clean inside edges (contours) of the original polygon
         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 = []
             # 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
                     inner_edges.append(y)
             # geoms += outer_edges + inner_edges
@@ -1626,7 +1626,7 @@ class Geometry(object):
                 # Straight line from current_pt to pt.
                 # Is the toolpath inside the geometry?
                 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:
                     # 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)
                     start = np.arctan2(-gobj['J'], -gobj['I'])
                     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['Y'] = y
@@ -4362,8 +4362,7 @@ class CNCjob(Geometry):
                                           visible=visible, layer=1)
             else:
                 # 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:
                     if geo['kind'][0] == 'T':
                         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'))
         self.menueditjump = self.menuedit.addAction(
             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
         self.menuedit.addSeparator()
@@ -825,6 +827,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
             QtGui.QIcon(self.app.resource_location + '/origin32.png'), _('Set Origin'))
         self.jmp_btn = self.toolbargeo.addAction(
             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# ###############################
@@ -859,6 +863,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         # ########################################################################
         self.dblsided_btn = self.toolbartools.addAction(
             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(
             QtGui.QIcon(self.app.resource_location + '/cut16_bis.png'), _("Cutout Tool"))
         self.ncc_btn = self.toolbartools.addAction(
@@ -1474,6 +1483,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20"><strong>SHIFT+G</strong></td>
                         <td>&nbsp;%s</td>
                     </tr>
+                    <tr height="20">
+                        <td height="20"><strong>SHIFT+J</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
                     <tr height="20">
                         <td height="20"><strong>SHIFT+M</strong></td>
                         <td>&nbsp;%s</td>
@@ -1506,6 +1519,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20">&nbsp;</td>
                         <td>&nbsp;</td>
                     </tr>
+                    <tr height="20">
+                        <td height="20"><strong>ALT+A</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
                     <tr height="20">
                         <td height="20"><strong>ALT+C</strong></td>
                         <td>&nbsp;%s</td>
@@ -1518,6 +1535,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         <td height="20"><strong>ALT+E</strong></td>
                         <td>&nbsp;%s</td>
                     </tr>
+                    <tr height="20">
+                        <td height="20"><strong>ALT+I</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
                     <tr height="20">
                         <td height="20"><strong>ALT+J</strong></td>
                         <td>&nbsp;%s</td>
@@ -1637,11 +1658,13 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
                 # SHIFT section
                 _("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"),
                 _("Skew on Y axis"),
                 # 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"),
                 _("Film PCB Tool"), _("Non-Copper Clearing Tool"), _("Optimal 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'))
         self.jmp_btn = self.toolbargeo.addAction(
             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(
             QtGui.QIcon(self.app.resource_location + '/replot32.png'), _("&Replot"))
         self.clear_plot_btn = self.toolbarview.addAction(
@@ -2470,9 +2497,9 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.zoom_fit_btn = self.toolbarview.addAction(
             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(
             QtGui.QIcon(self.app.resource_location + '/shell32.png'), _("&Command Line"))
         self.new_script_btn = self.toolbarshell.addAction(
@@ -2485,6 +2512,11 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         # ## Tools Toolbar # ##
         self.dblsided_btn = self.toolbartools.addAction(
             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(
             QtGui.QIcon(self.app.resource_location + '/cut16_bis.png'), _("&Cutout Tool"))
         self.ncc_btn = self.toolbartools.addAction(
@@ -2498,10 +2530,13 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.film_btn = self.toolbartools.addAction(
             QtGui.QIcon(self.app.resource_location + '/film16.png'), _("Film Tool"))
         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(
             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()
 
@@ -2834,6 +2869,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 if key == QtCore.Qt.Key_G:
                     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
                 if key == QtCore.Qt.Key_M:
                     self.app.distance_min_tool.run()
@@ -2882,6 +2921,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 if key == Qt.Key_3:
                     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
                 if key == QtCore.Qt.Key_C:
                     self.app.calculator_tool.run(toggle=True)
@@ -2906,6 +2949,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.on_toggle_grid_lines()
                     return
 
+                # Align in Object Tool
+                if key == QtCore.Qt.Key_I:
+                    self.app.edrills_tool.run(toggle=True)
+
                 # Fiducials Tool
                 if key == QtCore.Qt.Key_J:
                     self.app.fiducial_tool.run(toggle=True)

+ 4 - 0
flatcamGUI/GUIElements.py

@@ -2009,6 +2009,10 @@ class FCTable(QtWidgets.QTableWidget):
         palette = QtGui.QPalette()
         palette.setColor(QtGui.QPalette.Inactive, 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)
 
         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.
         :rtype: PlotCanvas
         """
-        print("step_1_1")
 
-        super(PlotCanvas, self).__init__()
+        # super(PlotCanvas, self).__init__()
+        # QtCore.QObject.__init__(self)
         # VisPyCanvas.__init__(self)
-        print("step_1_2")
+        super().__init__()
 
         # VisPyCanvas does not allow new attributes. Override.
         self.unfreeze()
@@ -46,8 +46,6 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         # Parent container
         self.container = container
 
-        print("step_1_3")
-
         settings = QtCore.QSettings("Open Source", "FlatCAM")
         if settings.contains("theme"):
             theme = settings.value('theme', type=str)
@@ -117,8 +115,6 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
             }
         )
 
-        print("step_1_4")
-
         # <VisPyCanvas>
         self.create_native()
         self.native.setParent(self.fcapp.ui)
@@ -126,8 +122,6 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         # <QtCore.QObject>
         self.container.addWidget(self.native)
 
-        print("step_1_5")
-
         # ## AXIS # ##
         self.v_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 0.8), vertical=True,
                                    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,
                                    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
         # all CNC have a limited workspace
         if self.fcapp.defaults['global_workspace'] is True:
             self.draw_workspace(workspace_size=self.fcapp.defaults["global_workspaceT"])
 
-        print("step_1_7")
-
         self.line_parent = None
         if self.fcapp.defaults["global_cursor_color_enabled"]:
             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,
                                           parent=self.line_parent)
 
-        print("step_1_8")
-
         self.shape_collections = []
 
         self.shape_collection = self.new_shape_collection()
@@ -171,10 +159,7 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         self.big_cursor = None
         # Keep VisPy canvas happy by letting it be "frozen" again.
         self.freeze()
-        print("step_1_9")
-
         self.fit_view()
-        print("step_1_10")
 
         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.setMinimumWidth(220)
 
+        self.tools2_edrills_group = Tools2EDrillsPrefGroupUI(decimals=self.decimals)
+        self.tools2_edrills_group.setMinimumWidth(220)
+
         self.vlay = QtWidgets.QVBoxLayout()
         self.vlay.addWidget(self.tools2_checkrules_group)
         self.vlay.addWidget(self.tools2_optimal_group)
 
         self.vlay1 = QtWidgets.QVBoxLayout()
         self.vlay1.addWidget(self.tools2_qrcode_group)
+        self.vlay1.addWidget(self.tools2_fiducials_group)
 
         self.vlay2 = QtWidgets.QVBoxLayout()
         self.vlay2.addWidget(self.tools2_cfill_group)
 
         self.vlay3 = QtWidgets.QVBoxLayout()
-        self.vlay3.addWidget(self.tools2_fiducials_group)
         self.vlay3.addWidget(self.tools2_cal_group)
+        self.vlay3.addWidget(self.tools2_edrills_group)
 
         self.layout.addLayout(self.vlay)
         self.layout.addLayout(self.vlay1)
@@ -333,7 +337,8 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         # Theme selection
         self.theme_label = QtWidgets.QLabel('%s:' % _('Theme'))
         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([
@@ -356,6 +361,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         self.theme_button = FCButton(_("Apply Theme"))
         self.theme_button.setToolTip(
             _("Select a theme for FlatCAM.\n"
+              "It will theme the plot area.\n"
               "The application will restart after change.")
         )
         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.")
         )
         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)
 
         grid0.addWidget(self.worker_number_label, 25, 0)
@@ -1604,21 +1602,13 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
         tol_label = QtWidgets.QLabel('%s:' % _("Geo Tolerance"))
         tol_label.setToolTip(_(
             "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"
             "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 = 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.set_precision(6)
 
@@ -7632,6 +7622,218 @@ class Tools2CalPrefGroupUI(OptionsGroupUI):
         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):
     def __init__(self, decimals=4, 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):
 
     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()
 

+ 3 - 1
flatcamParsers/ParseGerber.py

@@ -595,6 +595,7 @@ class Gerber(Geometry):
                 match = self.units_re.search(gline)
                 if match:
                     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)
                     # Changed for issue #80
                     # self.convert_units({'0': 'IN', '1': 'MM'}[match.group(1)])
@@ -834,7 +835,8 @@ class Gerber(Geometry):
                     # --- Buffered ---
                     geo_dict = dict()
                     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)  # Sprint Layout Gerbers with ground fill are crashed with above
                     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."))
             return
 
-        drills = []
+        drills = list()
 
         for hole in holes:
             point = Point(hole)
             point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
             drills.append({"point": point, "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:
+                tools["1"]['solid_geometry'].append(point)
                 tools["1"]['solid_geometry'].append(point_mirror)
 
         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_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.app.ui.rel_position_label.setText(
@@ -424,11 +425,13 @@ class Distance(FlatCAMTool):
             if len(self.points) == 1:
                 self.utility_geometry(pos=pos)
                 # 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:
             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_reference=skew_reference,
                                  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
                                  )
 
@@ -1080,23 +1080,28 @@ class Film(FlatCAMTool):
                         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,
                         use_thread=True, ftype='svg'):
+
         """
         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_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:
         """
         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_entry.returnPressed.connect(self.on_tool_add)

+ 3 - 2
flatcamTools/__init__.py

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