diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 36ee019d..dce2111f 100644 --- a/FlatCAMApp.py +++ b/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("    X: %.4f   " + "Y: %.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("Dx: %.4f   Dy: " + "%.4f    " % (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: diff --git a/FlatCAMObj.py b/FlatCAMObj.py index 712d766a..6b98f6f2 100644 --- a/FlatCAMObj.py +++ b/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) diff --git a/README.md b/README.md index db7ea631..2c446e8a 100644 --- a/README.md +++ b/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 diff --git a/camlib.py b/camlib.py index 1e58a423..063478b1 100644 --- a/camlib.py +++ b/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] diff --git a/flatcamGUI/FlatCAMGUI.py b/flatcamGUI/FlatCAMGUI.py index 111a9895..86974e3f 100644 --- a/flatcamGUI/FlatCAMGUI.py +++ b/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): SHIFT+G  %s + + SHIFT+J +  %s + SHIFT+M  %s @@ -1506,6 +1519,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow):     + + ALT+A +  %s + ALT+C  %s @@ -1518,6 +1535,10 @@ class FlatCAMGUI(QtWidgets.QMainWindow): ALT+E  %s + + ALT+I +  %s + ALT+J  %s @@ -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) diff --git a/flatcamGUI/GUIElements.py b/flatcamGUI/GUIElements.py index f52aced9..05314ffe 100644 --- a/flatcamGUI/GUIElements.py +++ b/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: diff --git a/flatcamGUI/PlotCanvas.py b/flatcamGUI/PlotCanvas.py index a2db822a..b2fd945a 100644 --- a/flatcamGUI/PlotCanvas.py +++ b/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") - # self.create_native() self.native.setParent(self.fcapp.ui) @@ -126,8 +122,6 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas): # 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) diff --git a/flatcamGUI/PreferencesUI.py b/flatcamGUI/PreferencesUI.py index 365fd17e..41ae3f32 100644 --- a/flatcamGUI/PreferencesUI.py +++ b/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('%s:' % _('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("%s:" % _("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('%s:' % _("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('%s' % _("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('%s' % _("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('%s' % _("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) diff --git a/flatcamGUI/VisPyCanvas.py b/flatcamGUI/VisPyCanvas.py index cc9aab7d..7d7efe13 100644 --- a/flatcamGUI/VisPyCanvas.py +++ b/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() diff --git a/flatcamParsers/ParseGerber.py b/flatcamParsers/ParseGerber.py index 6f79c31a..16ce8f37 100644 --- a/flatcamParsers/ParseGerber.py +++ b/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: diff --git a/flatcamTools/ToolAlignObjects.py b/flatcamTools/ToolAlignObjects.py new file mode 100644 index 00000000..d1e77442 --- /dev/null +++ b/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('%s' % _("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('%s' % _("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('%s:' % _("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())) diff --git a/flatcamTools/ToolDblSided.py b/flatcamTools/ToolDblSided.py index 3308c159..0b00fc6e 100644 --- a/flatcamTools/ToolDblSided.py +++ b/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): diff --git a/flatcamTools/ToolDistance.py b/flatcamTools/ToolDistance.py index c1f9ed8b..1a5bc568 100644 --- a/flatcamTools/ToolDistance.py +++ b/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)) diff --git a/flatcamTools/ToolExtractDrills.py b/flatcamTools/ToolExtractDrills.py new file mode 100644 index 00000000..d75a61b5 --- /dev/null +++ b/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("%s:" % _("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("%s" % _("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('%s' % _("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('%s' % _("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('%s' % _("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('%s' % _("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) diff --git a/flatcamTools/ToolFilm.py b/flatcamTools/ToolFilm.py index ead110b1..a3542e29 100644 --- a/flatcamTools/ToolFilm.py +++ b/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()") diff --git a/flatcamTools/ToolNonCopperClear.py b/flatcamTools/ToolNonCopperClear.py index c97f0ac3..b8e365c5 100644 --- a/flatcamTools/ToolNonCopperClear.py +++ b/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) diff --git a/flatcamTools/__init__.py b/flatcamTools/__init__.py index d986974e..a593e6c4 100644 --- a/flatcamTools/__init__.py +++ b/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 diff --git a/share/align16.png b/share/align16.png new file mode 100644 index 00000000..d21f1cec Binary files /dev/null and b/share/align16.png differ diff --git a/share/align32.png b/share/align32.png new file mode 100644 index 00000000..b81511a2 Binary files /dev/null and b/share/align32.png differ diff --git a/share/extract_drill16.png b/share/extract_drill16.png new file mode 100644 index 00000000..4b3a29ab Binary files /dev/null and b/share/extract_drill16.png differ diff --git a/share/extract_drill32.png b/share/extract_drill32.png new file mode 100644 index 00000000..41f740f2 Binary files /dev/null and b/share/extract_drill32.png differ diff --git a/share/locate16.png b/share/locate16.png new file mode 100644 index 00000000..1cdc85ed Binary files /dev/null and b/share/locate16.png differ diff --git a/share/locate32.png b/share/locate32.png new file mode 100644 index 00000000..66d630b0 Binary files /dev/null and b/share/locate32.png differ