diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ad80810..2859e8fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,12 @@ CHANGELOG for FlatCAM beta - more typos fixed in Excellon parser, slots processing - fixed Extract Drills Tool to work with the new Excellon data format - minor fix in App Tools that were updated to have UI in a separate class +- Tool Punch Gerber - updated the UI +- Tool Panelize - updated the UI +- Tool Extract Drills - updated the UI +- Tool QRcode - updated the UI +- Tool SolderPaste - updated the UI +- Tool DblSided - updated the UI 15.06.2020 diff --git a/appTools/ToolDblSided.py b/appTools/ToolDblSided.py index 6f869acd..1812346d 100644 --- a/appTools/ToolDblSided.py +++ b/appTools/ToolDblSided.py @@ -23,21 +23,450 @@ log = logging.getLogger('base') class DblSidedTool(AppTool): - toolName = _("2-Sided PCB") - def __init__(self, app): AppTool.__init__(self, app) self.decimals = self.app.decimals + # ############################################################################# + # ######################### Tool GUI ########################################## + # ############################################################################# + self.ui = DsidedUI(layout=self.layout, app=self.app) + self.toolName = self.ui.toolName + + # ## Signals + self.ui.mirror_gerber_button.clicked.connect(self.on_mirror_gerber) + self.ui.mirror_exc_button.clicked.connect(self.on_mirror_exc) + self.ui.mirror_geo_button.clicked.connect(self.on_mirror_geo) + + self.ui.add_point_button.clicked.connect(self.on_point_add) + self.ui.add_drill_point_button.clicked.connect(self.on_drill_add) + self.ui.delete_drill_point_button.clicked.connect(self.on_drill_delete_last) + self.ui.box_type_radio.activated_custom.connect(self.on_combo_box_type) + + self.ui.axis_location.group_toggle_fn = self.on_toggle_pointbox + + self.ui.point_entry.textChanged.connect(lambda val: self.ui.align_ref_label_val.set_value(val)) + + self.ui.xmin_btn.clicked.connect(self.on_xmin_clicked) + self.ui.ymin_btn.clicked.connect(self.on_ymin_clicked) + self.ui.xmax_btn.clicked.connect(self.on_xmax_clicked) + self.ui.ymax_btn.clicked.connect(self.on_ymax_clicked) + + self.ui.center_btn.clicked.connect( + lambda: self.ui.point_entry.set_value(self.ui.center_entry.get_value()) + ) + + self.ui.create_alignment_hole_button.clicked.connect(self.on_create_alignment_holes) + self.ui.calculate_bb_button.clicked.connect(self.on_bbox_coordinates) + + self.ui.reset_button.clicked.connect(self.set_tool_ui) + + self.drill_values = "" + + def install(self, icon=None, separator=None, **kwargs): + AppTool.install(self, icon, separator, shortcut='Alt+D', **kwargs) + + def run(self, toggle=True): + self.app.defaults.report_usage("Tool2Sided()") + + 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]) + + AppTool.run(self) + self.set_tool_ui() + + self.app.ui.notebook.setTabText(2, _("2-Sided Tool")) + + def set_tool_ui(self): + self.reset_fields() + + self.ui.point_entry.set_value("") + self.ui.alignment_holes.set_value("") + + self.ui.mirror_axis.set_value(self.app.defaults["tools_2sided_mirror_axis"]) + self.ui.axis_location.set_value(self.app.defaults["tools_2sided_axis_loc"]) + self.ui.drill_dia.set_value(self.app.defaults["tools_2sided_drilldia"]) + self.ui.align_axis_radio.set_value(self.app.defaults["tools_2sided_allign_axis"]) + + self.ui.xmin_entry.set_value(0.0) + self.ui.ymin_entry.set_value(0.0) + self.ui.xmax_entry.set_value(0.0) + self.ui.ymax_entry.set_value(0.0) + self.ui.center_entry.set_value('') + + self.ui.align_ref_label_val.set_value('%.*f' % (self.decimals, 0.0)) + + # run once to make sure that the obj_type attribute is updated in the FCComboBox + self.ui.box_type_radio.set_value('grb') + self.on_combo_box_type('grb') + + def on_combo_box_type(self, val): + obj_type = {'grb': 0, 'exc': 1, 'geo': 2}[val] + self.ui.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) + self.ui.box_combo.setCurrentIndex(0) + self.ui.box_combo.obj_type = { + "grb": "Gerber", "exc": "Excellon", "geo": "Geometry"}[val] + + def on_create_alignment_holes(self): + axis = self.ui.align_axis_radio.get_value() + mode = self.ui.axis_location.get_value() + + if mode == "point": + try: + px, py = self.ui.point_entry.get_value() + except TypeError: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("'Point' reference is selected and 'Point' coordinates " + "are missing. Add them and retry.")) + return + else: + selection_index = self.ui.box_combo.currentIndex() + model_index = self.app.collection.index(selection_index, 0, self.ui.gerber_object_combo.rootModelIndex()) + try: + bb_obj = model_index.internalPointer().obj + except AttributeError: + model_index = self.app.collection.index(selection_index, 0, self.ui.exc_object_combo.rootModelIndex()) + try: + bb_obj = model_index.internalPointer().obj + except AttributeError: + model_index = self.app.collection.index(selection_index, 0, + self.ui.geo_object_combo.rootModelIndex()) + try: + bb_obj = model_index.internalPointer().obj + except AttributeError: + self.app.inform.emit( + '[WARNING_NOTCL] %s' % _("There is no Box reference object loaded. Load one and retry.")) + return + + xmin, ymin, xmax, ymax = bb_obj.bounds() + px = 0.5 * (xmin + xmax) + py = 0.5 * (ymin + ymax) + + xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis] + + dia = float(self.drill_dia.get_value()) + if dia == '': + self.app.inform.emit('[WARNING_NOTCL] %s' % + _("No value or wrong format in Drill Dia entry. Add it and retry.")) + return + + tools = {} + tools[1] = {} + tools[1]["tooldia"] = dia + tools[1]['solid_geometry'] = [] + + # holes = self.alignment_holes.get_value() + holes = eval('[{}]'.format(self.ui.alignment_holes.text())) + if not holes: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("There are no Alignment Drill Coordinates to use. " + "Add them and retry.")) + return + + for hole in holes: + point = Point(hole) + point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py)) + + tools[1]['drills'] = [point, point_mirror] + tools[1]['solid_geometry'].append(point) + tools[1]['solid_geometry'].append(point_mirror) + + def obj_init(obj_inst, app_inst): + obj_inst.tools = tools + obj_inst.create_geometry() + obj_inst.source_file = app_inst.export_excellon(obj_name=obj_inst.options['name'], local_use=obj_inst, + filename=None, use_thread=False) + + self.app.app_obj.new_object("excellon", "Alignment Drills", obj_init) + self.drill_values = '' + self.app.inform.emit('[success] %s' % _("Excellon object with alignment drills created...")) + + def on_mirror_gerber(self): + selection_index = self.ui.gerber_object_combo.currentIndex() + # fcobj = self.app.collection.object_list[selection_index] + model_index = self.app.collection.index(selection_index, 0, self.ui.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 + + if fcobj.kind != 'gerber': + self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Gerber, Excellon and Geometry objects can be mirrored.")) + return + + axis = self.ui.mirror_axis.get_value() + mode = self.ui.axis_location.get_value() + + if mode == "point": + try: + px, py = self.ui.point_entry.get_value() + except TypeError: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("There are no Point coordinates in the Point field. " + "Add coords and try again ...")) + return + + else: + selection_index_box = self.ui.box_combo.currentIndex() + model_index_box = self.app.collection.index(selection_index_box, 0, self.ui.box_combo.rootModelIndex()) + try: + bb_obj = model_index_box.internalPointer().obj + except Exception: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Box object loaded ...")) + return + + xmin, ymin, xmax, ymax = bb_obj.bounds() + px = 0.5 * (xmin + xmax) + py = 0.5 * (ymin + ymax) + + fcobj.mirror(axis, [px, py]) + self.app.app_obj.object_changed.emit(fcobj) + fcobj.plot() + self.app.inform.emit('[success] Gerber %s %s...' % (str(fcobj.options['name']), _("was mirrored"))) + + def on_mirror_exc(self): + selection_index = self.ui.exc_object_combo.currentIndex() + # fcobj = self.app.collection.object_list[selection_index] + model_index = self.app.collection.index(selection_index, 0, self.ui.exc_object_combo.rootModelIndex()) + try: + fcobj = model_index.internalPointer().obj + except Exception: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Excellon object loaded ...")) + return + + if fcobj.kind != 'excellon': + self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Gerber, Excellon and Geometry objects can be mirrored.")) + return + + axis = self.ui.mirror_axis.get_value() + mode = self.ui.axis_location.get_value() + + if mode == "point": + try: + px, py = self.ui.point_entry.get_value() + except Exception as e: + log.debug("DblSidedTool.on_mirror_geo() --> %s" % str(e)) + self.app.inform.emit('[WARNING_NOTCL] %s' % _("There are no Point coordinates in the Point field. " + "Add coords and try again ...")) + return + else: + selection_index_box = self.ui.box_combo.currentIndex() + model_index_box = self.app.collection.index(selection_index_box, 0, self.ui.box_combo.rootModelIndex()) + try: + bb_obj = model_index_box.internalPointer().obj + except Exception as e: + log.debug("DblSidedTool.on_mirror_geo() --> %s" % str(e)) + self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Box object loaded ...")) + return + + xmin, ymin, xmax, ymax = bb_obj.bounds() + px = 0.5 * (xmin + xmax) + py = 0.5 * (ymin + ymax) + + fcobj.mirror(axis, [px, py]) + self.app.app_obj.object_changed.emit(fcobj) + fcobj.plot() + self.app.inform.emit('[success] Excellon %s %s...' % (str(fcobj.options['name']), _("was mirrored"))) + + def on_mirror_geo(self): + selection_index = self.ui.geo_object_combo.currentIndex() + # fcobj = self.app.collection.object_list[selection_index] + model_index = self.app.collection.index(selection_index, 0, self.ui.geo_object_combo.rootModelIndex()) + try: + fcobj = model_index.internalPointer().obj + except Exception: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Geometry object loaded ...")) + return + + if fcobj.kind != 'geometry': + self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Gerber, Excellon and Geometry objects can be mirrored.")) + return + + axis = self.ui.mirror_axis.get_value() + mode = self.ui.axis_location.get_value() + + if mode == "point": + px, py = self.ui.point_entry.get_value() + else: + selection_index_box = self.ui.box_combo.currentIndex() + model_index_box = self.app.collection.index(selection_index_box, 0, self.ui.box_combo.rootModelIndex()) + try: + bb_obj = model_index_box.internalPointer().obj + except Exception: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Box object loaded ...")) + return + + xmin, ymin, xmax, ymax = bb_obj.bounds() + px = 0.5 * (xmin + xmax) + py = 0.5 * (ymin + ymax) + + fcobj.mirror(axis, [px, py]) + self.app.app_obj.object_changed.emit(fcobj) + fcobj.plot() + self.app.inform.emit('[success] Geometry %s %s...' % (str(fcobj.options['name']), _("was mirrored"))) + + def on_point_add(self): + val = self.app.defaults["global_point_clipboard_format"] % \ + (self.decimals, self.app.pos[0], self.decimals, self.app.pos[1]) + self.ui.point_entry.set_value(val) + + def on_drill_add(self): + self.drill_values += (self.app.defaults["global_point_clipboard_format"] % + (self.decimals, self.app.pos[0], self.decimals, self.app.pos[1])) + ',' + self.ui.alignment_holes.set_value(self.drill_values) + + def on_drill_delete_last(self): + drill_values_without_last_tupple = self.drill_values.rpartition('(')[0] + self.drill_values = drill_values_without_last_tupple + self.ui.alignment_holes.set_value(self.drill_values) + + def on_toggle_pointbox(self): + if self.ui.axis_location.get_value() == "point": + self.ui.point_entry.show() + self.ui.add_point_button.show() + self.ui.box_type_label.hide() + self.ui.box_type_radio.hide() + self.ui.box_combo.hide() + + self.ui.align_ref_label_val.set_value(self.ui.point_entry.get_value()) + else: + self.ui.point_entry.hide() + self.ui.add_point_button.hide() + + self.ui.box_type_label.show() + self.ui.box_type_radio.show() + self.ui.box_combo.show() + + self.ui.align_ref_label_val.set_value("Box centroid") + + def on_bbox_coordinates(self): + + xmin = Inf + ymin = Inf + xmax = -Inf + ymax = -Inf + + obj_list = self.app.collection.get_selected() + + if not obj_list: + self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed. No object(s) selected...")) + return + + for obj in obj_list: + try: + gxmin, gymin, gxmax, gymax = obj.bounds() + xmin = min([xmin, gxmin]) + ymin = min([ymin, gymin]) + xmax = max([xmax, gxmax]) + ymax = max([ymax, gymax]) + except Exception as e: + log.warning("DEV WARNING: Tried to get bounds of empty geometry in DblSidedTool. %s" % str(e)) + + self.ui.xmin_entry.set_value(xmin) + self.ui.ymin_entry.set_value(ymin) + self.ui.xmax_entry.set_value(xmax) + self.ui.ymax_entry.set_value(ymax) + cx = '%.*f' % (self.decimals, (((xmax - xmin) / 2.0) + xmin)) + cy = '%.*f' % (self.decimals, (((ymax - ymin) / 2.0) + ymin)) + val_txt = '(%s, %s)' % (cx, cy) + + self.ui.center_entry.set_value(val_txt) + self.ui.axis_location.set_value('point') + self.ui.point_entry.set_value(val_txt) + self.app.delete_selection_shape() + + def on_xmin_clicked(self): + xmin = self.ui.xmin_entry.get_value() + self.ui.axis_location.set_value('point') + + try: + px, py = self.ui.point_entry.get_value() + val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmin, self.decimals, py) + except TypeError: + val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmin, self.decimals, 0.0) + self.ui.point_entry.set_value(val) + + def on_ymin_clicked(self): + ymin = self.ui.ymin_entry.get_value() + self.ui.axis_location.set_value('point') + + try: + px, py = self.ui.point_entry.get_value() + val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, px, self.decimals, ymin) + except TypeError: + val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, 0.0, self.decimals, ymin) + self.ui.point_entry.set_value(val) + + def on_xmax_clicked(self): + xmax = self.ui.xmax_entry.get_value() + self.ui.axis_location.set_value('point') + + try: + px, py = self.ui.point_entry.get_value() + val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmax, self.decimals, py) + except TypeError: + val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmax, self.decimals, 0.0) + self.ui.point_entry.set_value(val) + + def on_ymax_clicked(self): + ymax = self.ui.ymax_entry.get_value() + self.ui.axis_location.set_value('point') + + try: + px, py = self.ui.point_entry.get_value() + val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, px, self.decimals, ymax) + except TypeError: + val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, 0.0, self.decimals, ymax) + self.ui.point_entry.set_value(val) + + def reset_fields(self): + self.ui.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + self.ui.exc_object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex())) + self.ui.geo_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex())) + self.ui.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + + self.ui.gerber_object_combo.setCurrentIndex(0) + self.ui.exc_object_combo.setCurrentIndex(0) + self.ui.geo_object_combo.setCurrentIndex(0) + self.ui.box_combo.setCurrentIndex(0) + self.ui.box_type_radio.set_value('grb') + + self.drill_values = "" + self.ui.align_ref_label_val.set_value('') + + +class DsidedUI: + + toolName = _("2-Sided PCB") + + def __init__(self, layout, app): + self.app = app + self.decimals = self.app.decimals + self.layout = layout + # ## Title title_label = QtWidgets.QLabel("%s" % self.toolName) title_label.setStyleSheet(""" - QLabel - { - font-size: 16px; - font-weight: bold; - } - """) + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) self.layout.addWidget(QtWidgets.QLabel("")) @@ -71,11 +500,11 @@ class DblSidedTool(AppTool): "object, but modifies it.") ) self.mirror_gerber_button.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) + QPushButton + { + font-weight: bold; + } + """) self.mirror_gerber_button.setMinimumWidth(60) grid_lay.addWidget(self.botlay_label, 1, 0) @@ -99,11 +528,11 @@ class DblSidedTool(AppTool): "object, but modifies it.") ) self.mirror_exc_button.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) + QPushButton + { + font-weight: bold; + } + """) self.mirror_exc_button.setMinimumWidth(60) grid_lay.addWidget(self.excobj_label, 3, 0) @@ -129,11 +558,11 @@ class DblSidedTool(AppTool): "object, but modifies it.") ) self.mirror_geo_button.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) + QPushButton + { + font-weight: bold; + } + """) self.mirror_geo_button.setMinimumWidth(60) # grid_lay.addRow("Bottom Layer:", self.object_combo) @@ -197,11 +626,11 @@ class DblSidedTool(AppTool): "and left mouse button click on canvas or you can enter the coordinates manually.") ) self.add_point_button.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) + QPushButton + { + font-weight: bold; + } + """) self.add_point_button.setMinimumWidth(60) grid_lay1.addWidget(self.point_entry, 7, 0, 1, 2) @@ -334,11 +763,11 @@ class DblSidedTool(AppTool): "The envelope shape is parallel with the X, Y axis.") ) self.calculate_bb_button.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) + QPushButton + { + font-weight: bold; + } + """) grid_lay2.addWidget(self.calculate_bb_button, 13, 0, 1, 2) separator_line = QtWidgets.QFrame() @@ -409,11 +838,11 @@ class DblSidedTool(AppTool): # ## Alignment holes self.ah_label = QtWidgets.QLabel("%s:" % _('Alignment Drill Coordinates')) self.ah_label.setToolTip( - _("Alignment holes (x1, y1), (x2, y2), ... " - "on one side of the mirror axis. For each set of (x, y) coordinates\n" - "entered here, a pair of drills will be created:\n\n" - "- one drill at the coordinates from the field\n" - "- one drill in mirror position over the axis selected above in the 'Align Axis'.") + _("Alignment holes (x1, y1), (x2, y2), ... " + "on one side of the mirror axis. For each set of (x, y) coordinates\n" + "entered here, a pair of drills will be created:\n\n" + "- one drill at the coordinates from the field\n" + "- one drill in mirror position over the axis selected above in the 'Align Axis'.") ) self.alignment_holes = EvalEntry() @@ -458,11 +887,11 @@ class DblSidedTool(AppTool): "images.") ) self.create_alignment_hole_button.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) + QPushButton + { + font-weight: bold; + } + """) self.layout.addWidget(self.create_alignment_hole_button) self.layout.addStretch() @@ -473,424 +902,29 @@ class DblSidedTool(AppTool): _("Will reset the tool parameters.") ) self.reset_button.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) + QPushButton + { + font-weight: bold; + } + """) self.layout.addWidget(self.reset_button) - # ## Signals - self.mirror_gerber_button.clicked.connect(self.on_mirror_gerber) - self.mirror_exc_button.clicked.connect(self.on_mirror_exc) - self.mirror_geo_button.clicked.connect(self.on_mirror_geo) + # #################################### FINSIHED GUI ########################### + # ############################################################################# - self.add_point_button.clicked.connect(self.on_point_add) - self.add_drill_point_button.clicked.connect(self.on_drill_add) - self.delete_drill_point_button.clicked.connect(self.on_drill_delete_last) - self.box_type_radio.activated_custom.connect(self.on_combo_box_type) - - self.axis_location.group_toggle_fn = self.on_toggle_pointbox - - self.point_entry.textChanged.connect(lambda val: self.align_ref_label_val.set_value(val)) - - self.xmin_btn.clicked.connect(self.on_xmin_clicked) - self.ymin_btn.clicked.connect(self.on_ymin_clicked) - self.xmax_btn.clicked.connect(self.on_xmax_clicked) - self.ymax_btn.clicked.connect(self.on_ymax_clicked) - - self.center_btn.clicked.connect( - lambda: self.point_entry.set_value(self.center_entry.get_value()) - ) - - self.create_alignment_hole_button.clicked.connect(self.on_create_alignment_holes) - self.calculate_bb_button.clicked.connect(self.on_bbox_coordinates) - - self.reset_button.clicked.connect(self.set_tool_ui) - - self.drill_values = "" - - def install(self, icon=None, separator=None, **kwargs): - AppTool.install(self, icon, separator, shortcut='Alt+D', **kwargs) - - def run(self, toggle=True): - self.app.defaults.report_usage("Tool2Sided()") - - 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 + def confirmation_message(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"), + self.decimals, + minval, + self.decimals, + maxval), False) else: - if self.app.ui.splitter.sizes()[0] == 0: - self.app.ui.splitter.setSizes([1, 1]) + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) - AppTool.run(self) - self.set_tool_ui() - - self.app.ui.notebook.setTabText(2, _("2-Sided Tool")) - - def set_tool_ui(self): - self.reset_fields() - - self.point_entry.set_value("") - self.alignment_holes.set_value("") - - self.mirror_axis.set_value(self.app.defaults["tools_2sided_mirror_axis"]) - self.axis_location.set_value(self.app.defaults["tools_2sided_axis_loc"]) - self.drill_dia.set_value(self.app.defaults["tools_2sided_drilldia"]) - self.align_axis_radio.set_value(self.app.defaults["tools_2sided_allign_axis"]) - - self.xmin_entry.set_value(0.0) - self.ymin_entry.set_value(0.0) - self.xmax_entry.set_value(0.0) - self.ymax_entry.set_value(0.0) - self.center_entry.set_value('') - - self.align_ref_label_val.set_value('%.*f' % (self.decimals, 0.0)) - - # run once to make sure that the obj_type attribute is updated in the FCComboBox - self.box_type_radio.set_value('grb') - self.on_combo_box_type('grb') - - def on_combo_box_type(self, val): - obj_type = {'grb': 0, 'exc': 1, 'geo': 2}[val] - self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) - self.box_combo.setCurrentIndex(0) - self.box_combo.obj_type = { - "grb": "Gerber", "exc": "Excellon", "geo": "Geometry"}[val] - - def on_create_alignment_holes(self): - axis = self.align_axis_radio.get_value() - mode = self.axis_location.get_value() - - if mode == "point": - try: - px, py = self.point_entry.get_value() - except TypeError: - self.app.inform.emit('[WARNING_NOTCL] %s' % _("'Point' reference is selected and 'Point' coordinates " - "are missing. Add them and retry.")) - return + def confirmation_message_int(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' % + (_("Edited value is out of range"), minval, maxval), False) else: - selection_index = self.box_combo.currentIndex() - model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex()) - try: - bb_obj = model_index.internalPointer().obj - except AttributeError: - model_index = self.app.collection.index(selection_index, 0, self.exc_object_combo.rootModelIndex()) - try: - bb_obj = model_index.internalPointer().obj - except AttributeError: - model_index = self.app.collection.index(selection_index, 0, - self.geo_object_combo.rootModelIndex()) - try: - bb_obj = model_index.internalPointer().obj - except AttributeError: - self.app.inform.emit( - '[WARNING_NOTCL] %s' % _("There is no Box reference object loaded. Load one and retry.")) - return - - xmin, ymin, xmax, ymax = bb_obj.bounds() - px = 0.5 * (xmin + xmax) - py = 0.5 * (ymin + ymax) - - xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis] - - dia = float(self.drill_dia.get_value()) - if dia == '': - self.app.inform.emit('[WARNING_NOTCL] %s' % - _("No value or wrong format in Drill Dia entry. Add it and retry.")) - return - - tools = {} - tools[1] = {} - tools[1]["tooldia"] = dia - tools[1]['solid_geometry'] = [] - - # holes = self.alignment_holes.get_value() - holes = eval('[{}]'.format(self.alignment_holes.text())) - if not holes: - self.app.inform.emit('[WARNING_NOTCL] %s' % _("There are no Alignment Drill Coordinates to use. " - "Add them and retry.")) - return - - for hole in holes: - point = Point(hole) - point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py)) - - tools[1]['drills'] = [point, point_mirror] - tools[1]['solid_geometry'].append(point) - tools[1]['solid_geometry'].append(point_mirror) - - def obj_init(obj_inst, app_inst): - obj_inst.tools = tools - obj_inst.create_geometry() - obj_inst.source_file = app_inst.export_excellon(obj_name=obj_inst.options['name'], local_use=obj_inst, - filename=None, use_thread=False) - - self.app.app_obj.new_object("excellon", "Alignment Drills", obj_init) - self.drill_values = '' - self.app.inform.emit('[success] %s' % _("Excellon object with alignment drills created...")) - - def on_mirror_gerber(self): - selection_index = self.gerber_object_combo.currentIndex() - # fcobj = self.app.collection.object_list[selection_index] - 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 - - if fcobj.kind != 'gerber': - self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Gerber, Excellon and Geometry objects can be mirrored.")) - return - - axis = self.mirror_axis.get_value() - mode = self.axis_location.get_value() - - if mode == "point": - try: - px, py = self.point_entry.get_value() - except TypeError: - self.app.inform.emit('[WARNING_NOTCL] %s' % _("There are no Point coordinates in the Point field. " - "Add coords and try again ...")) - return - - else: - selection_index_box = self.box_combo.currentIndex() - model_index_box = self.app.collection.index(selection_index_box, 0, self.box_combo.rootModelIndex()) - try: - bb_obj = model_index_box.internalPointer().obj - except Exception: - self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Box object loaded ...")) - return - - xmin, ymin, xmax, ymax = bb_obj.bounds() - px = 0.5 * (xmin + xmax) - py = 0.5 * (ymin + ymax) - - fcobj.mirror(axis, [px, py]) - self.app.app_obj.object_changed.emit(fcobj) - fcobj.plot() - self.app.inform.emit('[success] Gerber %s %s...' % (str(fcobj.options['name']), _("was mirrored"))) - - def on_mirror_exc(self): - selection_index = self.exc_object_combo.currentIndex() - # fcobj = self.app.collection.object_list[selection_index] - model_index = self.app.collection.index(selection_index, 0, self.exc_object_combo.rootModelIndex()) - try: - fcobj = model_index.internalPointer().obj - except Exception: - self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Excellon object loaded ...")) - return - - if fcobj.kind != 'excellon': - self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Gerber, Excellon and Geometry objects can be mirrored.")) - return - - axis = self.mirror_axis.get_value() - mode = self.axis_location.get_value() - - if mode == "point": - try: - px, py = self.point_entry.get_value() - except Exception as e: - log.debug("DblSidedTool.on_mirror_geo() --> %s" % str(e)) - self.app.inform.emit('[WARNING_NOTCL] %s' % _("There are no Point coordinates in the Point field. " - "Add coords and try again ...")) - return - else: - selection_index_box = self.box_combo.currentIndex() - model_index_box = self.app.collection.index(selection_index_box, 0, self.box_combo.rootModelIndex()) - try: - bb_obj = model_index_box.internalPointer().obj - except Exception as e: - log.debug("DblSidedTool.on_mirror_geo() --> %s" % str(e)) - self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Box object loaded ...")) - return - - xmin, ymin, xmax, ymax = bb_obj.bounds() - px = 0.5 * (xmin + xmax) - py = 0.5 * (ymin + ymax) - - fcobj.mirror(axis, [px, py]) - self.app.app_obj.object_changed.emit(fcobj) - fcobj.plot() - self.app.inform.emit('[success] Excellon %s %s...' % (str(fcobj.options['name']), _("was mirrored"))) - - def on_mirror_geo(self): - selection_index = self.geo_object_combo.currentIndex() - # fcobj = self.app.collection.object_list[selection_index] - model_index = self.app.collection.index(selection_index, 0, self.geo_object_combo.rootModelIndex()) - try: - fcobj = model_index.internalPointer().obj - except Exception: - self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Geometry object loaded ...")) - return - - if fcobj.kind != 'geometry': - self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Gerber, Excellon and Geometry objects can be mirrored.")) - return - - axis = self.mirror_axis.get_value() - mode = self.axis_location.get_value() - - if mode == "point": - px, py = self.point_entry.get_value() - else: - selection_index_box = self.box_combo.currentIndex() - model_index_box = self.app.collection.index(selection_index_box, 0, self.box_combo.rootModelIndex()) - try: - bb_obj = model_index_box.internalPointer().obj - except Exception: - self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Box object loaded ...")) - return - - xmin, ymin, xmax, ymax = bb_obj.bounds() - px = 0.5 * (xmin + xmax) - py = 0.5 * (ymin + ymax) - - fcobj.mirror(axis, [px, py]) - self.app.app_obj.object_changed.emit(fcobj) - fcobj.plot() - self.app.inform.emit('[success] Geometry %s %s...' % (str(fcobj.options['name']), _("was mirrored"))) - - def on_point_add(self): - val = self.app.defaults["global_point_clipboard_format"] % \ - (self.decimals, self.app.pos[0], self.decimals, self.app.pos[1]) - self.point_entry.set_value(val) - - def on_drill_add(self): - self.drill_values += (self.app.defaults["global_point_clipboard_format"] % - (self.decimals, self.app.pos[0], self.decimals, self.app.pos[1])) + ',' - self.alignment_holes.set_value(self.drill_values) - - def on_drill_delete_last(self): - drill_values_without_last_tupple = self.drill_values.rpartition('(')[0] - self.drill_values = drill_values_without_last_tupple - self.alignment_holes.set_value(self.drill_values) - - def on_toggle_pointbox(self): - if self.axis_location.get_value() == "point": - self.point_entry.show() - self.add_point_button.show() - self.box_type_label.hide() - self.box_type_radio.hide() - self.box_combo.hide() - - self.align_ref_label_val.set_value(self.point_entry.get_value()) - else: - self.point_entry.hide() - self.add_point_button.hide() - - self.box_type_label.show() - self.box_type_radio.show() - self.box_combo.show() - - self.align_ref_label_val.set_value("Box centroid") - - def on_bbox_coordinates(self): - - xmin = Inf - ymin = Inf - xmax = -Inf - ymax = -Inf - - obj_list = self.app.collection.get_selected() - - if not obj_list: - self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed. No object(s) selected...")) - return - - for obj in obj_list: - try: - gxmin, gymin, gxmax, gymax = obj.bounds() - xmin = min([xmin, gxmin]) - ymin = min([ymin, gymin]) - xmax = max([xmax, gxmax]) - ymax = max([ymax, gymax]) - except Exception as e: - log.warning("DEV WARNING: Tried to get bounds of empty geometry in DblSidedTool. %s" % str(e)) - - self.xmin_entry.set_value(xmin) - self.ymin_entry.set_value(ymin) - self.xmax_entry.set_value(xmax) - self.ymax_entry.set_value(ymax) - cx = '%.*f' % (self.decimals, (((xmax - xmin) / 2.0) + xmin)) - cy = '%.*f' % (self.decimals, (((ymax - ymin) / 2.0) + ymin)) - val_txt = '(%s, %s)' % (cx, cy) - - self.center_entry.set_value(val_txt) - self.axis_location.set_value('point') - self.point_entry.set_value(val_txt) - self.app.delete_selection_shape() - - def on_xmin_clicked(self): - xmin = self.xmin_entry.get_value() - self.axis_location.set_value('point') - - try: - px, py = self.point_entry.get_value() - val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmin, self.decimals, py) - except TypeError: - val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmin, self.decimals, 0.0) - self.point_entry.set_value(val) - - def on_ymin_clicked(self): - ymin = self.ymin_entry.get_value() - self.axis_location.set_value('point') - - try: - px, py = self.point_entry.get_value() - val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, px, self.decimals, ymin) - except TypeError: - val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, 0.0, self.decimals, ymin) - self.point_entry.set_value(val) - - def on_xmax_clicked(self): - xmax = self.xmax_entry.get_value() - self.axis_location.set_value('point') - - try: - px, py = self.point_entry.get_value() - val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmax, self.decimals, py) - except TypeError: - val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, xmax, self.decimals, 0.0) - self.point_entry.set_value(val) - - def on_ymax_clicked(self): - ymax = self.ymax_entry.get_value() - self.axis_location.set_value('point') - - try: - px, py = self.point_entry.get_value() - val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, px, self.decimals, ymax) - except TypeError: - val = self.app.defaults["global_point_clipboard_format"] % (self.decimals, 0.0, self.decimals, ymax) - self.point_entry.set_value(val) - - def reset_fields(self): - self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) - self.exc_object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex())) - self.geo_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex())) - self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) - - self.gerber_object_combo.setCurrentIndex(0) - self.exc_object_combo.setCurrentIndex(0) - self.geo_object_combo.setCurrentIndex(0) - self.box_combo.setCurrentIndex(0) - self.box_type_radio.set_value('grb') - - self.drill_values = "" - self.align_ref_label_val.set_value('') + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) diff --git a/appTools/ToolEtchCompensation.py b/appTools/ToolEtchCompensation.py index 0243ff6a..2a9250f0 100644 --- a/appTools/ToolEtchCompensation.py +++ b/appTools/ToolEtchCompensation.py @@ -29,14 +29,232 @@ log = logging.getLogger('base') class ToolEtchCompensation(AppTool): - toolName = _("Etch Compensation Tool") - def __init__(self, app): self.app = app self.decimals = self.app.decimals AppTool.__init__(self, app) + # ############################################################################# + # ######################### Tool GUI ########################################## + # ############################################################################# + self.ui = EtchUI(layout=self.layout, app=self.app) + self.toolName = self.ui.toolName + + self.ui.compensate_btn.clicked.connect(self.on_compensate) + self.ui.reset_button.clicked.connect(self.set_tool_ui) + self.ui.ratio_radio.activated_custom.connect(self.on_ratio_change) + + self.ui.oz_entry.textChanged.connect(self.on_oz_conversion) + self.ui.mils_entry.textChanged.connect(self.on_mils_conversion) + + def install(self, icon=None, separator=None, **kwargs): + AppTool.install(self, icon, separator, shortcut='', **kwargs) + + def run(self, toggle=True): + self.app.defaults.report_usage("ToolEtchCompensation()") + log.debug("ToolEtchCompensation() is running ...") + + 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]) + + AppTool.run(self) + self.set_tool_ui() + + self.app.ui.notebook.setTabText(2, _("Etch Compensation Tool")) + + def set_tool_ui(self): + self.ui.thick_entry.set_value(18.0) + self.ui.ratio_radio.set_value('factor') + + def on_ratio_change(self, val): + """ + Called on activated_custom signal of the RadioSet GUI element self.radio_ratio + + :param val: 'c' or 'p': 'c' means custom factor and 'p' means preselected etchants + :type val: str + :return: None + :rtype: + """ + if val == 'factor': + self.ui.etchants_label.hide() + self.ui.etchants_combo.hide() + self.ui.factor_label.show() + self.ui.factor_entry.show() + self.ui.offset_label.hide() + self.ui.offset_entry.hide() + elif val == 'etch_list': + self.ui.etchants_label.show() + self.ui.etchants_combo.show() + self.ui.factor_label.hide() + self.ui.factor_entry.hide() + self.ui.offset_label.hide() + self.ui.offset_entry.hide() + else: + self.ui.etchants_label.hide() + self.ui.etchants_combo.hide() + self.ui.factor_label.hide() + self.ui.factor_entry.hide() + self.ui.offset_label.show() + self.ui.offset_entry.show() + + def on_oz_conversion(self, txt): + try: + val = eval(txt) + # oz thickness to mils by multiplying with 1.37 + # mils to microns by multiplying with 25.4 + val *= 34.798 + except Exception: + self.ui.oz_to_um_entry.set_value('') + return + self.ui.oz_to_um_entry.set_value(val, self.decimals) + + def on_mils_conversion(self, txt): + try: + val = eval(txt) + val *= 25.4 + except Exception: + self.ui.mils_to_um_entry.set_value('') + return + self.ui.mils_to_um_entry.set_value(val, self.decimals) + + def on_compensate(self): + log.debug("ToolEtchCompensation.on_compensate()") + + ratio_type = self.ui.ratio_radio.get_value() + thickness = self.ui.thick_entry.get_value() / 1000 # in microns + + grb_circle_steps = int(self.app.defaults["gerber_circle_steps"]) + obj_name = self.ui.gerber_combo.currentText() + + outname = obj_name + "_comp" + + # Get source object. + try: + grb_obj = self.app.collection.get_by_name(obj_name) + except Exception as e: + self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(obj_name))) + return "Could not retrieve object: %s with error: %s" % (obj_name, str(e)) + + if grb_obj is None: + if obj_name == '': + obj_name = 'None' + self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(obj_name))) + return + + if ratio_type == 'factor': + etch_factor = 1 / self.ui.factor_entry.get_value() + offset = thickness / etch_factor + elif ratio_type == 'etch_list': + etchant = self.ui.etchants_combo.get_value() + if etchant == "CuCl2": + etch_factor = 0.33 + else: + etch_factor = 0.25 + offset = thickness / etch_factor + else: + offset = self.ui.offset_entry.get_value() / 1000 # in microns + + try: + __ = iter(grb_obj.solid_geometry) + except TypeError: + grb_obj.solid_geometry = list(grb_obj.solid_geometry) + + new_solid_geometry = [] + + for poly in grb_obj.solid_geometry: + new_solid_geometry.append(poly.buffer(offset, int(grb_circle_steps))) + new_solid_geometry = unary_union(new_solid_geometry) + + new_options = {} + for opt in grb_obj.options: + new_options[opt] = deepcopy(grb_obj.options[opt]) + + new_apertures = deepcopy(grb_obj.apertures) + + # update the apertures attributes (keys in the apertures dict) + for ap in new_apertures: + type = new_apertures[ap]['type'] + for k in new_apertures[ap]: + if type == 'R' or type == 'O': + if k == 'width' or k == 'height': + new_apertures[ap][k] += offset + else: + if k == 'size' or k == 'width' or k == 'height': + new_apertures[ap][k] += offset + + if k == 'geometry': + for geo_el in new_apertures[ap][k]: + if 'solid' in geo_el: + geo_el['solid'] = geo_el['solid'].buffer(offset, int(grb_circle_steps)) + + # in case of 'R' or 'O' aperture type we need to update the aperture 'size' after + # the 'width' and 'height' keys were updated + for ap in new_apertures: + type = new_apertures[ap]['type'] + for k in new_apertures[ap]: + if type == 'R' or type == 'O': + if k == 'size': + new_apertures[ap][k] = math.sqrt( + new_apertures[ap]['width'] ** 2 + new_apertures[ap]['height'] ** 2) + + def init_func(new_obj, app_obj): + """ + Init a new object in FlatCAM Object collection + + :param new_obj: New object + :type new_obj: ObjectCollection + :param app_obj: App + :type app_obj: app_Main.App + :return: None + :rtype: + """ + new_obj.options.update(new_options) + new_obj.options['name'] = outname + new_obj.fill_color = deepcopy(grb_obj.fill_color) + new_obj.outline_color = deepcopy(grb_obj.outline_color) + + new_obj.apertures = deepcopy(new_apertures) + + new_obj.solid_geometry = deepcopy(new_solid_geometry) + new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None, + local_use=new_obj, use_thread=False) + + self.app.app_obj.new_object('gerber', outname, init_func) + + def reset_fields(self): + self.ui.gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + + @staticmethod + def poly2rings(poly): + return [poly.exterior] + [interior for interior in poly.interiors] + + +class EtchUI: + + toolName = _("Etch Compensation Tool") + + def __init__(self, layout, app): + self.app = app + self.decimals = self.app.decimals + self.layout = layout + self.tools_frame = QtWidgets.QFrame() self.tools_frame.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.tools_frame) @@ -47,12 +265,12 @@ class ToolEtchCompensation(AppTool): # Title title_label = QtWidgets.QLabel("%s" % self.toolName) title_label.setStyleSheet(""" - QLabel - { - font-size: 16px; - font-weight: bold; - } - """) + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.tools_box.addWidget(title_label) # Grid Layout @@ -227,11 +445,11 @@ class ToolEtchCompensation(AppTool): _("Will increase the copper features thickness to compensate the lateral etch.") ) self.compensate_btn.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) + QPushButton + { + font-weight: bold; + } + """) grid0.addWidget(self.compensate_btn, 24, 0, 1, 2) self.tools_box.addStretch() @@ -242,214 +460,31 @@ class ToolEtchCompensation(AppTool): _("Will reset the tool parameters.") ) self.reset_button.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) + QPushButton + { + font-weight: bold; + } + """) self.tools_box.addWidget(self.reset_button) - self.compensate_btn.clicked.connect(self.on_compensate) - self.reset_button.clicked.connect(self.set_tool_ui) - self.ratio_radio.activated_custom.connect(self.on_ratio_change) + # #################################### FINSIHED GUI ########################### + # ############################################################################# - self.oz_entry.textChanged.connect(self.on_oz_conversion) - self.mils_entry.textChanged.connect(self.on_mils_conversion) - - def install(self, icon=None, separator=None, **kwargs): - AppTool.install(self, icon, separator, shortcut='', **kwargs) - - def run(self, toggle=True): - self.app.defaults.report_usage("ToolEtchCompensation()") - log.debug("ToolEtchCompensation() is running ...") - - 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 + def confirmation_message(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"), + self.decimals, + minval, + self.decimals, + maxval), False) else: - if self.app.ui.splitter.sizes()[0] == 0: - self.app.ui.splitter.setSizes([1, 1]) + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) - AppTool.run(self) - self.set_tool_ui() - - self.app.ui.notebook.setTabText(2, _("Etch Compensation Tool")) - - def set_tool_ui(self): - self.thick_entry.set_value(18.0) - self.ratio_radio.set_value('factor') - - def on_ratio_change(self, val): - """ - Called on activated_custom signal of the RadioSet GUI element self.radio_ratio - - :param val: 'c' or 'p': 'c' means custom factor and 'p' means preselected etchants - :type val: str - :return: None - :rtype: - """ - if val == 'factor': - self.etchants_label.hide() - self.etchants_combo.hide() - self.factor_label.show() - self.factor_entry.show() - self.offset_label.hide() - self.offset_entry.hide() - elif val == 'etch_list': - self.etchants_label.show() - self.etchants_combo.show() - self.factor_label.hide() - self.factor_entry.hide() - self.offset_label.hide() - self.offset_entry.hide() + def confirmation_message_int(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' % + (_("Edited value is out of range"), minval, maxval), False) else: - self.etchants_label.hide() - self.etchants_combo.hide() - self.factor_label.hide() - self.factor_entry.hide() - self.offset_label.show() - self.offset_entry.show() + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) - def on_oz_conversion(self, txt): - try: - val = eval(txt) - # oz thickness to mils by multiplying with 1.37 - # mils to microns by multiplying with 25.4 - val *= 34.798 - except Exception: - self.oz_to_um_entry.set_value('') - return - self.oz_to_um_entry.set_value(val, self.decimals) - - def on_mils_conversion(self, txt): - try: - val = eval(txt) - val *= 25.4 - except Exception: - self.mils_to_um_entry.set_value('') - return - self.mils_to_um_entry.set_value(val, self.decimals) - - def on_compensate(self): - log.debug("ToolEtchCompensation.on_compensate()") - - ratio_type = self.ratio_radio.get_value() - thickness = self.thick_entry.get_value() / 1000 # in microns - - grb_circle_steps = int(self.app.defaults["gerber_circle_steps"]) - obj_name = self.gerber_combo.currentText() - - outname = obj_name + "_comp" - - # Get source object. - try: - grb_obj = self.app.collection.get_by_name(obj_name) - except Exception as e: - self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(obj_name))) - return "Could not retrieve object: %s with error: %s" % (obj_name, str(e)) - - if grb_obj is None: - if obj_name == '': - obj_name = 'None' - self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(obj_name))) - return - - if ratio_type == 'factor': - etch_factor = 1 / self.factor_entry.get_value() - offset = thickness / etch_factor - elif ratio_type == 'etch_list': - etchant = self.etchants_combo.get_value() - if etchant == "CuCl2": - etch_factor = 0.33 - else: - etch_factor = 0.25 - offset = thickness / etch_factor - else: - offset = self.offset_entry.get_value() / 1000 # in microns - - try: - __ = iter(grb_obj.solid_geometry) - except TypeError: - grb_obj.solid_geometry = list(grb_obj.solid_geometry) - - new_solid_geometry = [] - - for poly in grb_obj.solid_geometry: - new_solid_geometry.append(poly.buffer(offset, int(grb_circle_steps))) - new_solid_geometry = unary_union(new_solid_geometry) - - new_options = {} - for opt in grb_obj.options: - new_options[opt] = deepcopy(grb_obj.options[opt]) - - new_apertures = deepcopy(grb_obj.apertures) - - # update the apertures attributes (keys in the apertures dict) - for ap in new_apertures: - type = new_apertures[ap]['type'] - for k in new_apertures[ap]: - if type == 'R' or type == 'O': - if k == 'width' or k == 'height': - new_apertures[ap][k] += offset - else: - if k == 'size' or k == 'width' or k == 'height': - new_apertures[ap][k] += offset - - if k == 'geometry': - for geo_el in new_apertures[ap][k]: - if 'solid' in geo_el: - geo_el['solid'] = geo_el['solid'].buffer(offset, int(grb_circle_steps)) - - # in case of 'R' or 'O' aperture type we need to update the aperture 'size' after - # the 'width' and 'height' keys were updated - for ap in new_apertures: - type = new_apertures[ap]['type'] - for k in new_apertures[ap]: - if type == 'R' or type == 'O': - if k == 'size': - new_apertures[ap][k] = math.sqrt( - new_apertures[ap]['width'] ** 2 + new_apertures[ap]['height'] ** 2) - - def init_func(new_obj, app_obj): - """ - Init a new object in FlatCAM Object collection - - :param new_obj: New object - :type new_obj: ObjectCollection - :param app_obj: App - :type app_obj: app_Main.App - :return: None - :rtype: - """ - new_obj.options.update(new_options) - new_obj.options['name'] = outname - new_obj.fill_color = deepcopy(grb_obj.fill_color) - new_obj.outline_color = deepcopy(grb_obj.outline_color) - - new_obj.apertures = deepcopy(new_apertures) - - new_obj.solid_geometry = deepcopy(new_solid_geometry) - new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None, - local_use=new_obj, use_thread=False) - - self.app.app_obj.new_object('gerber', outname, init_func) - - def reset_fields(self): - self.gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) - - @staticmethod - def poly2rings(poly): - return [poly.exterior] + [interior for interior in poly.interiors] # end of file diff --git a/appTools/ToolExtractDrills.py b/appTools/ToolExtractDrills.py index 9b1a3942..580dbd12 100644 --- a/appTools/ToolExtractDrills.py +++ b/appTools/ToolExtractDrills.py @@ -26,21 +26,413 @@ log = logging.getLogger('base') class ToolExtractDrills(AppTool): - toolName = _("Extract Drills") - def __init__(self, app): AppTool.__init__(self, app) self.decimals = self.app.decimals + # ############################################################################# + # ######################### Tool GUI ########################################## + # ############################################################################# + self.ui = ExtractDrillsUI(layout=self.layout, app=self.app) + self.toolName = self.ui.toolName + + # ## Signals + self.ui.hole_size_radio.activated_custom.connect(self.on_hole_size_toggle) + self.ui.e_drills_button.clicked.connect(self.on_extract_drills_click) + self.ui.reset_button.clicked.connect(self.set_tool_ui) + + self.ui.circular_cb.stateChanged.connect( + lambda state: + self.ui.circular_ring_entry.setDisabled(False) if state else self.ui.circular_ring_entry.setDisabled(True) + ) + + self.ui.oblong_cb.stateChanged.connect( + lambda state: + self.ui.oblong_ring_entry.setDisabled(False) if state else self.ui.oblong_ring_entry.setDisabled(True) + ) + + self.ui.square_cb.stateChanged.connect( + lambda state: + self.ui.square_ring_entry.setDisabled(False) if state else self.ui.square_ring_entry.setDisabled(True) + ) + + self.ui.rectangular_cb.stateChanged.connect( + lambda state: + self.ui.rectangular_ring_entry.setDisabled(False) if state else + self.ui.rectangular_ring_entry.setDisabled(True) + ) + + self.ui.other_cb.stateChanged.connect( + lambda state: + self.ui.other_ring_entry.setDisabled(False) if state else self.ui.other_ring_entry.setDisabled(True) + ) + + def install(self, icon=None, separator=None, **kwargs): + AppTool.install(self, icon, separator, shortcut='Alt+I', **kwargs) + + def run(self, toggle=True): + self.app.defaults.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]) + + AppTool.run(self) + self.set_tool_ui() + + self.app.ui.notebook.setTabText(2, _("Extract Drills Tool")) + + def set_tool_ui(self): + self.reset_fields() + + self.ui.hole_size_radio.set_value(self.app.defaults["tools_edrills_hole_type"]) + + self.ui.dia_entry.set_value(float(self.app.defaults["tools_edrills_hole_fixed_dia"])) + + self.ui.circular_ring_entry.set_value(float(self.app.defaults["tools_edrills_circular_ring"])) + self.ui.oblong_ring_entry.set_value(float(self.app.defaults["tools_edrills_oblong_ring"])) + self.ui.square_ring_entry.set_value(float(self.app.defaults["tools_edrills_square_ring"])) + self.ui.rectangular_ring_entry.set_value(float(self.app.defaults["tools_edrills_rectangular_ring"])) + self.ui.other_ring_entry.set_value(float(self.app.defaults["tools_edrills_others_ring"])) + + self.ui.circular_cb.set_value(self.app.defaults["tools_edrills_circular"]) + self.ui.oblong_cb.set_value(self.app.defaults["tools_edrills_oblong"]) + self.ui.square_cb.set_value(self.app.defaults["tools_edrills_square"]) + self.ui.rectangular_cb.set_value(self.app.defaults["tools_edrills_rectangular"]) + self.ui.other_cb.set_value(self.app.defaults["tools_edrills_others"]) + + self.ui.factor_entry.set_value(float(self.app.defaults["tools_edrills_hole_prop_factor"])) + + def on_extract_drills_click(self): + + drill_dia = self.ui.dia_entry.get_value() + circ_r_val = self.ui.circular_ring_entry.get_value() + oblong_r_val = self.ui.oblong_ring_entry.get_value() + square_r_val = self.ui.square_ring_entry.get_value() + rect_r_val = self.ui.rectangular_ring_entry.get_value() + other_r_val = self.ui.other_ring_entry.get_value() + + prop_factor = self.ui.factor_entry.get_value() / 100.0 + + drills = [] + tools = {} + + selection_index = self.ui.gerber_object_combo.currentIndex() + model_index = self.app.collection.index(selection_index, 0, self.ui.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.ui.hole_size_radio.get_value() + + if mode == 'fixed': + tools = { + 1: { + "tooldia": drill_dia, + "drills": [], + "slots": [] + } + } + for apid, apid_value in fcobj.apertures.items(): + ap_type = apid_value['type'] + + if ap_type == 'C': + if self.ui.circular_cb.get_value() is False: + continue + elif ap_type == 'O': + if self.ui.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.ui.square_cb.get_value() is False: + continue + else: + if self.ui.rectangular_cb.get_value() is False: + continue + else: + if self.ui.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): + tools[1]["drills"].append(geo_el['follow']) + if 'solid_geometry' not in tools[1]: + tools[1]['solid_geometry'] = [] + 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.ui.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.ui.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.ui.square_cb.get_value(): + dia = float(apid_value['height']) - (2 * square_r_val) + else: + if self.ui.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.ui.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["tooldia"])) - 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 = 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] = { + "tooldia": dia, + "drills": [], + "slots": [] + } + + tools[tool_in_drills]['drills'].append(geo_el['follow']) + + if 'solid_geometry' not in tools[tool_in_drills]: + tools[tool_in_drills]['solid_geometry'] = [] + 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.ui.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.ui.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.ui.square_cb.get_value(): + dia = float(apid_value['height']) * prop_factor + else: + if self.ui.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.ui.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["tooldia"])) - 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 = 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] = { + "tooldia": dia, + "drills": [], + "slots": [] + } + + tools[tool_in_drills]['drills'].append(geo_el['follow']) + + if 'solid_geometry' not in tools[tool_in_drills]: + tools[tool_in_drills]['solid_geometry'] = [] + 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.app_obj.new_object("excellon", outname, obj_init) + + def on_hole_size_toggle(self, val): + if val == "fixed": + self.ui.fixed_label.setDisabled(False) + self.ui.dia_entry.setDisabled(False) + self.ui.dia_label.setDisabled(False) + + self.ui.ring_frame.setDisabled(True) + + self.ui.prop_label.setDisabled(True) + self.ui.factor_label.setDisabled(True) + self.ui.factor_entry.setDisabled(True) + elif val == "ring": + self.ui.fixed_label.setDisabled(True) + self.ui.dia_entry.setDisabled(True) + self.ui.dia_label.setDisabled(True) + + self.ui.ring_frame.setDisabled(False) + + self.ui.prop_label.setDisabled(True) + self.ui.factor_label.setDisabled(True) + self.ui.factor_entry.setDisabled(True) + elif val == "prop": + self.ui.fixed_label.setDisabled(True) + self.ui.dia_entry.setDisabled(True) + self.ui.dia_label.setDisabled(True) + + self.ui.ring_frame.setDisabled(True) + + self.ui.prop_label.setDisabled(False) + self.ui.factor_label.setDisabled(False) + self.ui.factor_entry.setDisabled(False) + + def reset_fields(self): + self.ui.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + self.ui.gerber_object_combo.setCurrentIndex(0) + + +class ExtractDrillsUI: + + toolName = _("Extract Drills") + + def __init__(self, layout, app): + self.app = app + self.decimals = self.app.decimals + self.layout = layout + # ## Title title_label = QtWidgets.QLabel("%s" % self.toolName) title_label.setStyleSheet(""" - QLabel - { - font-size: 16px; - font-weight: bold; - } - """) + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) self.layout.addWidget(QtWidgets.QLabel("")) @@ -297,11 +689,11 @@ class ToolExtractDrills(AppTool): _("Extract drills from a given Gerber file.") ) self.e_drills_button.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) + QPushButton + { + font-weight: bold; + } + """) self.layout.addWidget(self.e_drills_button) self.layout.addStretch() @@ -312,11 +704,11 @@ class ToolExtractDrills(AppTool): _("Will reset the tool parameters.") ) self.reset_button.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) + QPushButton + { + font-weight: bold; + } + """) self.layout.addWidget(self.reset_button) self.circular_ring_entry.setEnabled(False) @@ -331,380 +723,22 @@ class ToolExtractDrills(AppTool): self.factor_entry.setDisabled(True) self.ring_frame.setDisabled(True) + # #################################### FINSIHED GUI ########################### + # ############################################################################# - # ## 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): - AppTool.install(self, icon, separator, shortcut='Alt+I', **kwargs) - - def run(self, toggle=True): - self.app.defaults.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 + def confirmation_message(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"), + self.decimals, + minval, + self.decimals, + maxval), False) else: - if self.app.ui.splitter.sizes()[0] == 0: - self.app.ui.splitter.setSizes([1, 1]) + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) - AppTool.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 = [] - tools = {} - - 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: { - "tooldia": drill_dia, - "drills": [], - "slots": [] - } - } - 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): - tools[1]["drills"].append(geo_el['follow']) - if 'solid_geometry' not in tools[1]: - tools[1]['solid_geometry'] = [] - 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["tooldia"])) - 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 = 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] = { - "tooldia": dia, - "drills": [], - "slots": [] - } - - tools[tool_in_drills]['drills'].append(geo_el['follow']) - - if 'solid_geometry' not in tools[tool_in_drills]: - tools[tool_in_drills]['solid_geometry'] = [] - 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 confirmation_message_int(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' % + (_("Edited value is out of range"), minval, maxval), False) 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["tooldia"])) - 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 = 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] = { - "tooldia": dia, - "drills": [], - "slots": [] - } - - tools[tool_in_drills]['drills'].append(geo_el['follow']) - - if 'solid_geometry' not in tools[tool_in_drills]: - tools[tool_in_drills]['solid_geometry'] = [] - 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.app_obj.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) + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) diff --git a/appTools/ToolPanelize.py b/appTools/ToolPanelize.py index e742038c..7137d415 100644 --- a/appTools/ToolPanelize.py +++ b/appTools/ToolPanelize.py @@ -35,266 +35,22 @@ class Panelize(AppTool): toolName = _("Panelize PCB") def __init__(self, app): - self.decimals = app.decimals - AppTool.__init__(self, app) + self.decimals = app.decimals + self.app = app - # ## Title - title_label = QtWidgets.QLabel("%s" % self.toolName) - title_label.setStyleSheet(""" - QLabel - { - font-size: 16px; - font-weight: bold; - } - """) - self.layout.addWidget(title_label) - - self.object_label = QtWidgets.QLabel('%s:' % _("Source Object")) - self.object_label.setToolTip( - _("Specify the type of object to be panelized\n" - "It can be of type: Gerber, Excellon or Geometry.\n" - "The selection here decide the type of objects that will be\n" - "in the Object combobox.") - ) - - self.layout.addWidget(self.object_label) - - # Form Layout - form_layout_0 = QtWidgets.QFormLayout() - self.layout.addLayout(form_layout_0) - - # Type of object to be panelized - self.type_obj_combo = FCComboBox() - self.type_obj_combo.addItem("Gerber") - self.type_obj_combo.addItem("Excellon") - self.type_obj_combo.addItem("Geometry") - - 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.setItemIcon(2, QtGui.QIcon(self.app.resource_location + "/geometry16.png")) - - self.type_object_label = QtWidgets.QLabel('%s:' % _("Object Type")) - - form_layout_0.addRow(self.type_object_label, self.type_obj_combo) - - # Object to be panelized - 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.is_last = True - - self.object_combo.setToolTip( - _("Object to be panelized. This means that it will\n" - "be duplicated in an array of rows and columns.") - ) - form_layout_0.addRow(self.object_combo) - - # Form Layout - form_layout = QtWidgets.QFormLayout() - self.layout.addLayout(form_layout) - - # Type of box Panel object - self.reference_radio = RadioSet([{'label': _('Object'), 'value': 'object'}, - {'label': _('Bounding Box'), 'value': 'bbox'}]) - self.box_label = QtWidgets.QLabel("%s:" % _("Penelization Reference")) - self.box_label.setToolTip( - _("Choose the reference for panelization:\n" - "- Object = the bounding box of a different object\n" - "- Bounding Box = the bounding box of the object to be panelized\n" - "\n" - "The reference is useful when doing panelization for more than one\n" - "object. The spacings (really offsets) will be applied in reference\n" - "to this reference object therefore maintaining the panelized\n" - "objects in sync.") - ) - form_layout.addRow(self.box_label) - form_layout.addRow(self.reference_radio) - - # Type of Box Object to be used as an envelope for panelization - self.type_box_combo = FCComboBox() - self.type_box_combo.addItems([_("Gerber"), _("Geometry")]) - - # we get rid of item1 ("Excellon") as it is not suitable for use as a "box" for panelizing - # self.type_box_combo.view().setRowHidden(1, True) - self.type_box_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png")) - self.type_box_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/geometry16.png")) - - self.type_box_combo_label = QtWidgets.QLabel('%s:' % _("Box Type")) - self.type_box_combo_label.setToolTip( - _("Specify the type of object to be used as an container for\n" - "panelization. It can be: Gerber or Geometry type.\n" - "The selection here decide the type of objects that will be\n" - "in the Box Object combobox.") - ) - form_layout.addRow(self.type_box_combo_label, self.type_box_combo) - - # Box - self.box_combo = FCComboBox() - self.box_combo.setModel(self.app.collection) - self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) - self.box_combo.is_last = True - - self.box_combo.setToolTip( - _("The actual object that is used as container for the\n " - "selected object that is to be panelized.") - ) - form_layout.addRow(self.box_combo) - - separator_line = QtWidgets.QFrame() - separator_line.setFrameShape(QtWidgets.QFrame.HLine) - separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) - form_layout.addRow(separator_line) - - panel_data_label = QtWidgets.QLabel("%s:" % _("Panel Data")) - panel_data_label.setToolTip( - _("This informations will shape the resulting panel.\n" - "The number of rows and columns will set how many\n" - "duplicates of the original geometry will be generated.\n" - "\n" - "The spacings will set the distance between any two\n" - "elements of the panel array.") - ) - form_layout.addRow(panel_data_label) - - # Spacing Columns - self.spacing_columns = FCDoubleSpinner(callback=self.confirmation_message) - self.spacing_columns.set_range(0, 9999) - self.spacing_columns.set_precision(4) - - self.spacing_columns_label = QtWidgets.QLabel('%s:' % _("Spacing cols")) - self.spacing_columns_label.setToolTip( - _("Spacing between columns of the desired panel.\n" - "In current units.") - ) - form_layout.addRow(self.spacing_columns_label, self.spacing_columns) - - # Spacing Rows - self.spacing_rows = FCDoubleSpinner(callback=self.confirmation_message) - self.spacing_rows.set_range(0, 9999) - self.spacing_rows.set_precision(4) - - self.spacing_rows_label = QtWidgets.QLabel('%s:' % _("Spacing rows")) - self.spacing_rows_label.setToolTip( - _("Spacing between rows of the desired panel.\n" - "In current units.") - ) - form_layout.addRow(self.spacing_rows_label, self.spacing_rows) - - # Columns - self.columns = FCSpinner(callback=self.confirmation_message_int) - self.columns.set_range(0, 9999) - - self.columns_label = QtWidgets.QLabel('%s:' % _("Columns")) - self.columns_label.setToolTip( - _("Number of columns of the desired panel") - ) - form_layout.addRow(self.columns_label, self.columns) - - # Rows - self.rows = FCSpinner(callback=self.confirmation_message_int) - self.rows.set_range(0, 9999) - - self.rows_label = QtWidgets.QLabel('%s:' % _("Rows")) - self.rows_label.setToolTip( - _("Number of rows of the desired panel") - ) - form_layout.addRow(self.rows_label, self.rows) - - separator_line = QtWidgets.QFrame() - separator_line.setFrameShape(QtWidgets.QFrame.HLine) - separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) - form_layout.addRow(separator_line) - - # Type of resulting Panel object - self.panel_type_radio = RadioSet([{'label': _('Gerber'), 'value': 'gerber'}, - {'label': _('Geo'), 'value': 'geometry'}]) - self.panel_type_label = QtWidgets.QLabel("%s:" % _("Panel Type")) - self.panel_type_label.setToolTip( - _("Choose the type of object for the panel object:\n" - "- Geometry\n" - "- Gerber") - ) - form_layout.addRow(self.panel_type_label) - form_layout.addRow(self.panel_type_radio) - - # Constrains - self.constrain_cb = FCCheckBox('%s:' % _("Constrain panel within")) - self.constrain_cb.setToolTip( - _("Area define by DX and DY within to constrain the panel.\n" - "DX and DY values are in current units.\n" - "Regardless of how many columns and rows are desired,\n" - "the final panel will have as many columns and rows as\n" - "they fit completely within selected area.") - ) - form_layout.addRow(self.constrain_cb) - - self.x_width_entry = FCDoubleSpinner(callback=self.confirmation_message) - self.x_width_entry.set_precision(4) - self.x_width_entry.set_range(0, 9999) - - self.x_width_lbl = QtWidgets.QLabel('%s:' % _("Width (DX)")) - self.x_width_lbl.setToolTip( - _("The width (DX) within which the panel must fit.\n" - "In current units.") - ) - form_layout.addRow(self.x_width_lbl, self.x_width_entry) - - self.y_height_entry = FCDoubleSpinner(callback=self.confirmation_message) - self.y_height_entry.set_range(0, 9999) - self.y_height_entry.set_precision(4) - - self.y_height_lbl = QtWidgets.QLabel('%s:' % _("Height (DY)")) - self.y_height_lbl.setToolTip( - _("The height (DY)within which the panel must fit.\n" - "In current units.") - ) - form_layout.addRow(self.y_height_lbl, self.y_height_entry) - - self.constrain_sel = OptionalInputSection( - self.constrain_cb, [self.x_width_lbl, self.x_width_entry, self.y_height_lbl, self.y_height_entry]) - - separator_line = QtWidgets.QFrame() - separator_line.setFrameShape(QtWidgets.QFrame.HLine) - separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) - form_layout.addRow(separator_line) - - # Buttons - self.panelize_object_button = QtWidgets.QPushButton(_("Panelize Object")) - self.panelize_object_button.setToolTip( - _("Panelize the specified object around the specified box.\n" - "In other words it creates multiple copies of the source object,\n" - "arranged in a 2D array of rows and columns.") - ) - self.panelize_object_button.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) - self.layout.addWidget(self.panelize_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) + # ############################################################################# + # ######################### Tool GUI ########################################## + # ############################################################################# + self.ui = PanelizeUI(layout=self.layout, app=self.app) + self.toolName = self.ui.toolName # Signals - self.reference_radio.activated_custom.connect(self.on_reference_radio_changed) - self.panelize_object_button.clicked.connect(self.on_panelize) - self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed) - self.type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed) - self.reset_button.clicked.connect(self.set_tool_ui) + self.ui.reference_radio.activated_custom.connect(self.on_reference_radio_changed) + self.ui.panelize_object_button.clicked.connect(self.on_panelize) + self.ui.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed) + self.ui.type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed) + self.ui.reset_button.clicked.connect(self.set_tool_ui) # list to hold the temporary objects self.objs = [] @@ -342,35 +98,35 @@ class Panelize(AppTool): sp_c = self.app.defaults["tools_panelize_spacing_columns"] if \ self.app.defaults["tools_panelize_spacing_columns"] else 0.0 - self.spacing_columns.set_value(float(sp_c)) + self.ui.spacing_columns.set_value(float(sp_c)) sp_r = self.app.defaults["tools_panelize_spacing_rows"] if \ self.app.defaults["tools_panelize_spacing_rows"] else 0.0 - self.spacing_rows.set_value(float(sp_r)) + self.ui.spacing_rows.set_value(float(sp_r)) rr = self.app.defaults["tools_panelize_rows"] if \ self.app.defaults["tools_panelize_rows"] else 0.0 - self.rows.set_value(int(rr)) + self.ui.rows.set_value(int(rr)) cc = self.app.defaults["tools_panelize_columns"] if \ self.app.defaults["tools_panelize_columns"] else 0.0 - self.columns.set_value(int(cc)) + self.ui.columns.set_value(int(cc)) c_cb = self.app.defaults["tools_panelize_constrain"] if \ self.app.defaults["tools_panelize_constrain"] else False - self.constrain_cb.set_value(c_cb) + self.ui.constrain_cb.set_value(c_cb) x_w = self.app.defaults["tools_panelize_constrainx"] if \ self.app.defaults["tools_panelize_constrainx"] else 0.0 - self.x_width_entry.set_value(float(x_w)) + self.ui.x_width_entry.set_value(float(x_w)) y_w = self.app.defaults["tools_panelize_constrainy"] if \ self.app.defaults["tools_panelize_constrainy"] else 0.0 - self.y_height_entry.set_value(float(y_w)) + self.ui.y_height_entry.set_value(float(y_w)) panel_type = self.app.defaults["tools_panelize_panel_type"] if \ self.app.defaults["tools_panelize_panel_type"] else 'gerber' - self.panel_type_radio.set_value(panel_type) + self.ui.panel_type_radio.set_value(panel_type) # run once the following so the obj_type attribute is updated in the FCComboBoxes # such that the last loaded object is populated in the combo boxes @@ -378,43 +134,43 @@ class Panelize(AppTool): self.on_type_box_index_changed() 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) - self.object_combo.obj_type = { + obj_type = self.ui.type_obj_combo.currentIndex() + self.ui.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) + self.ui.object_combo.setCurrentIndex(0) + self.ui.object_combo.obj_type = { _("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry" - }[self.type_obj_combo.get_value()] + }[self.ui.type_obj_combo.get_value()] # hide the panel type for Excellons, the panel can be only of type Geometry - if self.type_obj_combo.currentText() != 'Excellon': - self.panel_type_label.setDisabled(False) - self.panel_type_radio.setDisabled(False) + if self.ui.type_obj_combo.currentText() != 'Excellon': + self.ui.panel_type_label.setDisabled(False) + self.ui.panel_type_radio.setDisabled(False) else: - self.panel_type_label.setDisabled(True) - self.panel_type_radio.setDisabled(True) - self.panel_type_radio.set_value('geometry') + self.ui.panel_type_label.setDisabled(True) + self.ui.panel_type_radio.setDisabled(True) + self.ui.panel_type_radio.set_value('geometry') def on_type_box_index_changed(self): - obj_type = self.type_box_combo.currentIndex() + obj_type = self.ui.type_box_combo.currentIndex() obj_type = 2 if obj_type == 1 else obj_type - self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) - self.box_combo.setCurrentIndex(0) - self.box_combo.obj_type = { + self.ui.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) + self.ui.box_combo.setCurrentIndex(0) + self.ui.box_combo.obj_type = { _("Gerber"): "Gerber", _("Geometry"): "Geometry" - }[self.type_box_combo.get_value()] + }[self.ui.type_box_combo.get_value()] def on_reference_radio_changed(self, current_val): if current_val == 'object': - self.type_box_combo.setDisabled(False) - self.type_box_combo_label.setDisabled(False) - self.box_combo.setDisabled(False) + self.ui.type_box_combo.setDisabled(False) + self.ui.type_box_combo_label.setDisabled(False) + self.ui.box_combo.setDisabled(False) else: - self.type_box_combo.setDisabled(True) - self.type_box_combo_label.setDisabled(True) - self.box_combo.setDisabled(True) + self.ui.type_box_combo.setDisabled(True) + self.ui.type_box_combo_label.setDisabled(True) + self.ui.box_combo.setDisabled(True) def on_panelize(self): - name = self.object_combo.currentText() + name = self.ui.object_combo.currentText() # Get source object to be panelized. try: @@ -429,7 +185,7 @@ class Panelize(AppTool): (_("Object not found"), panel_source_obj)) return - boxname = self.box_combo.currentText() + boxname = self.ui.box_combo.currentText() try: box = self.app.collection.get_by_name(boxname) @@ -440,29 +196,29 @@ class Panelize(AppTool): if box is None: self.app.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), panel_source_obj)) - self.reference_radio.set_value('bbox') + self.ui.reference_radio.set_value('bbox') - if self.reference_radio.get_value() == 'bbox': + if self.ui.reference_radio.get_value() == 'bbox': box = panel_source_obj self.outname = name + '_panelized' - spacing_columns = float(self.spacing_columns.get_value()) + spacing_columns = float(self.ui.spacing_columns.get_value()) spacing_columns = spacing_columns if spacing_columns is not None else 0 - spacing_rows = float(self.spacing_rows.get_value()) + spacing_rows = float(self.ui.spacing_rows.get_value()) spacing_rows = spacing_rows if spacing_rows is not None else 0 - rows = int(self.rows.get_value()) + rows = int(self.ui.rows.get_value()) rows = rows if rows is not None else 1 - columns = int(self.columns.get_value()) + columns = int(self.ui.columns.get_value()) columns = columns if columns is not None else 1 - constrain_dx = float(self.x_width_entry.get_value()) - constrain_dy = float(self.y_height_entry.get_value()) + constrain_dx = float(self.ui.x_width_entry.get_value()) + constrain_dy = float(self.ui.y_height_entry.get_value()) - panel_type = str(self.panel_type_radio.get_value()) + panel_type = str(self.ui.panel_type_radio.get_value()) if 0 in {columns, rows}: self.app.inform.emit('[ERROR_NOTCL] %s' % @@ -474,7 +230,7 @@ class Panelize(AppTool): lenghty = ymax - ymin + spacing_rows # check if constrain within an area is desired - if self.constrain_cb.isChecked(): + if self.ui.constrain_cb.isChecked(): panel_lengthx = ((xmax - xmin) * columns) + (spacing_columns * (columns - 1)) panel_lengthy = ((ymax - ymin) * rows) + (spacing_rows * (rows - 1)) @@ -820,3 +576,283 @@ class Panelize(AppTool): def reset_fields(self): self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + + +class PanelizeUI: + + toolName = _("Panelize PCB") + + def __init__(self, layout, app): + self.app = app + self.decimals = self.app.decimals + self.layout = layout + + # ## Title + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) + self.layout.addWidget(title_label) + + self.object_label = QtWidgets.QLabel('%s:' % _("Source Object")) + self.object_label.setToolTip( + _("Specify the type of object to be panelized\n" + "It can be of type: Gerber, Excellon or Geometry.\n" + "The selection here decide the type of objects that will be\n" + "in the Object combobox.") + ) + + self.layout.addWidget(self.object_label) + + # Form Layout + form_layout_0 = QtWidgets.QFormLayout() + self.layout.addLayout(form_layout_0) + + # Type of object to be panelized + self.type_obj_combo = FCComboBox() + self.type_obj_combo.addItem("Gerber") + self.type_obj_combo.addItem("Excellon") + self.type_obj_combo.addItem("Geometry") + + 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.setItemIcon(2, QtGui.QIcon(self.app.resource_location + "/geometry16.png")) + + self.type_object_label = QtWidgets.QLabel('%s:' % _("Object Type")) + + form_layout_0.addRow(self.type_object_label, self.type_obj_combo) + + # Object to be panelized + 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.is_last = True + + self.object_combo.setToolTip( + _("Object to be panelized. This means that it will\n" + "be duplicated in an array of rows and columns.") + ) + form_layout_0.addRow(self.object_combo) + + # Form Layout + form_layout = QtWidgets.QFormLayout() + self.layout.addLayout(form_layout) + + # Type of box Panel object + self.reference_radio = RadioSet([{'label': _('Object'), 'value': 'object'}, + {'label': _('Bounding Box'), 'value': 'bbox'}]) + self.box_label = QtWidgets.QLabel("%s:" % _("Penelization Reference")) + self.box_label.setToolTip( + _("Choose the reference for panelization:\n" + "- Object = the bounding box of a different object\n" + "- Bounding Box = the bounding box of the object to be panelized\n" + "\n" + "The reference is useful when doing panelization for more than one\n" + "object. The spacings (really offsets) will be applied in reference\n" + "to this reference object therefore maintaining the panelized\n" + "objects in sync.") + ) + form_layout.addRow(self.box_label) + form_layout.addRow(self.reference_radio) + + # Type of Box Object to be used as an envelope for panelization + self.type_box_combo = FCComboBox() + self.type_box_combo.addItems([_("Gerber"), _("Geometry")]) + + # we get rid of item1 ("Excellon") as it is not suitable for use as a "box" for panelizing + # self.type_box_combo.view().setRowHidden(1, True) + self.type_box_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png")) + self.type_box_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/geometry16.png")) + + self.type_box_combo_label = QtWidgets.QLabel('%s:' % _("Box Type")) + self.type_box_combo_label.setToolTip( + _("Specify the type of object to be used as an container for\n" + "panelization. It can be: Gerber or Geometry type.\n" + "The selection here decide the type of objects that will be\n" + "in the Box Object combobox.") + ) + form_layout.addRow(self.type_box_combo_label, self.type_box_combo) + + # Box + self.box_combo = FCComboBox() + self.box_combo.setModel(self.app.collection) + self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + self.box_combo.is_last = True + + self.box_combo.setToolTip( + _("The actual object that is used as container for the\n " + "selected object that is to be panelized.") + ) + form_layout.addRow(self.box_combo) + + separator_line = QtWidgets.QFrame() + separator_line.setFrameShape(QtWidgets.QFrame.HLine) + separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) + form_layout.addRow(separator_line) + + panel_data_label = QtWidgets.QLabel("%s:" % _("Panel Data")) + panel_data_label.setToolTip( + _("This informations will shape the resulting panel.\n" + "The number of rows and columns will set how many\n" + "duplicates of the original geometry will be generated.\n" + "\n" + "The spacings will set the distance between any two\n" + "elements of the panel array.") + ) + form_layout.addRow(panel_data_label) + + # Spacing Columns + self.spacing_columns = FCDoubleSpinner(callback=self.confirmation_message) + self.spacing_columns.set_range(0, 9999) + self.spacing_columns.set_precision(4) + + self.spacing_columns_label = QtWidgets.QLabel('%s:' % _("Spacing cols")) + self.spacing_columns_label.setToolTip( + _("Spacing between columns of the desired panel.\n" + "In current units.") + ) + form_layout.addRow(self.spacing_columns_label, self.spacing_columns) + + # Spacing Rows + self.spacing_rows = FCDoubleSpinner(callback=self.confirmation_message) + self.spacing_rows.set_range(0, 9999) + self.spacing_rows.set_precision(4) + + self.spacing_rows_label = QtWidgets.QLabel('%s:' % _("Spacing rows")) + self.spacing_rows_label.setToolTip( + _("Spacing between rows of the desired panel.\n" + "In current units.") + ) + form_layout.addRow(self.spacing_rows_label, self.spacing_rows) + + # Columns + self.columns = FCSpinner(callback=self.confirmation_message_int) + self.columns.set_range(0, 9999) + + self.columns_label = QtWidgets.QLabel('%s:' % _("Columns")) + self.columns_label.setToolTip( + _("Number of columns of the desired panel") + ) + form_layout.addRow(self.columns_label, self.columns) + + # Rows + self.rows = FCSpinner(callback=self.confirmation_message_int) + self.rows.set_range(0, 9999) + + self.rows_label = QtWidgets.QLabel('%s:' % _("Rows")) + self.rows_label.setToolTip( + _("Number of rows of the desired panel") + ) + form_layout.addRow(self.rows_label, self.rows) + + separator_line = QtWidgets.QFrame() + separator_line.setFrameShape(QtWidgets.QFrame.HLine) + separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) + form_layout.addRow(separator_line) + + # Type of resulting Panel object + self.panel_type_radio = RadioSet([{'label': _('Gerber'), 'value': 'gerber'}, + {'label': _('Geo'), 'value': 'geometry'}]) + self.panel_type_label = QtWidgets.QLabel("%s:" % _("Panel Type")) + self.panel_type_label.setToolTip( + _("Choose the type of object for the panel object:\n" + "- Geometry\n" + "- Gerber") + ) + form_layout.addRow(self.panel_type_label) + form_layout.addRow(self.panel_type_radio) + + # Constrains + self.constrain_cb = FCCheckBox('%s:' % _("Constrain panel within")) + self.constrain_cb.setToolTip( + _("Area define by DX and DY within to constrain the panel.\n" + "DX and DY values are in current units.\n" + "Regardless of how many columns and rows are desired,\n" + "the final panel will have as many columns and rows as\n" + "they fit completely within selected area.") + ) + form_layout.addRow(self.constrain_cb) + + self.x_width_entry = FCDoubleSpinner(callback=self.confirmation_message) + self.x_width_entry.set_precision(4) + self.x_width_entry.set_range(0, 9999) + + self.x_width_lbl = QtWidgets.QLabel('%s:' % _("Width (DX)")) + self.x_width_lbl.setToolTip( + _("The width (DX) within which the panel must fit.\n" + "In current units.") + ) + form_layout.addRow(self.x_width_lbl, self.x_width_entry) + + self.y_height_entry = FCDoubleSpinner(callback=self.confirmation_message) + self.y_height_entry.set_range(0, 9999) + self.y_height_entry.set_precision(4) + + self.y_height_lbl = QtWidgets.QLabel('%s:' % _("Height (DY)")) + self.y_height_lbl.setToolTip( + _("The height (DY)within which the panel must fit.\n" + "In current units.") + ) + form_layout.addRow(self.y_height_lbl, self.y_height_entry) + + self.constrain_sel = OptionalInputSection( + self.constrain_cb, [self.x_width_lbl, self.x_width_entry, self.y_height_lbl, self.y_height_entry]) + + separator_line = QtWidgets.QFrame() + separator_line.setFrameShape(QtWidgets.QFrame.HLine) + separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) + form_layout.addRow(separator_line) + + # Buttons + self.panelize_object_button = QtWidgets.QPushButton(_("Panelize Object")) + self.panelize_object_button.setToolTip( + _("Panelize the specified object around the specified box.\n" + "In other words it creates multiple copies of the source object,\n" + "arranged in a 2D array of rows and columns.") + ) + self.panelize_object_button.setStyleSheet(""" + QPushButton + { + font-weight: bold; + } + """) + self.layout.addWidget(self.panelize_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) + + # #################################### FINSIHED GUI ########################### + # ############################################################################# + + def confirmation_message(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"), + self.decimals, + minval, + self.decimals, + maxval), False) + else: + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) + + def confirmation_message_int(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' % + (_("Edited value is out of range"), minval, maxval), False) + else: + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) diff --git a/appTools/ToolPunchGerber.py b/appTools/ToolPunchGerber.py index 15a69618..0edbbf9b 100644 --- a/appTools/ToolPunchGerber.py +++ b/appTools/ToolPunchGerber.py @@ -27,22 +27,676 @@ log = logging.getLogger('base') class ToolPunchGerber(AppTool): - toolName = _("Punch Gerber") - def __init__(self, app): AppTool.__init__(self, app) + self.app = app self.decimals = self.app.decimals + self.units = self.app.defaults['units'] - # Title + # ############################################################################# + # ######################### Tool GUI ########################################## + # ############################################################################# + self.ui = PunchUI(layout=self.layout, app=self.app) + self.toolName = self.ui.toolName + + # ## Signals + self.ui.method_punch.activated_custom.connect(self.on_method) + self.ui.reset_button.clicked.connect(self.set_tool_ui) + self.ui.punch_object_button.clicked.connect(self.on_generate_object) + + self.ui.circular_cb.stateChanged.connect( + lambda state: + self.ui.circular_ring_entry.setDisabled(False) if state else + self.ui.circular_ring_entry.setDisabled(True) + ) + + self.ui.oblong_cb.stateChanged.connect( + lambda state: + self.ui.oblong_ring_entry.setDisabled(False) if state else self.ui.oblong_ring_entry.setDisabled(True) + ) + + self.ui.square_cb.stateChanged.connect( + lambda state: + self.ui.square_ring_entry.setDisabled(False) if state else self.ui.square_ring_entry.setDisabled(True) + ) + + self.ui.rectangular_cb.stateChanged.connect( + lambda state: + self.ui.rectangular_ring_entry.setDisabled(False) if state else + self.ui.rectangular_ring_entry.setDisabled(True) + ) + + self.ui.other_cb.stateChanged.connect( + lambda state: + self.ui.other_ring_entry.setDisabled(False) if state else self.ui.other_ring_entry.setDisabled(True) + ) + + def run(self, toggle=True): + self.app.defaults.report_usage("ToolPunchGerber()") + + 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]) + + AppTool.run(self) + + self.set_tool_ui() + + self.app.ui.notebook.setTabText(2, _("Punch Tool")) + + def install(self, icon=None, separator=None, **kwargs): + AppTool.install(self, icon, separator, shortcut='Alt+H', **kwargs) + + def set_tool_ui(self): + self.reset_fields() + + self.ui_connect() + self.ui.method_punch.set_value(self.app.defaults["tools_punch_hole_type"]) + self.ui.select_all_cb.set_value(False) + + self.ui.dia_entry.set_value(float(self.app.defaults["tools_punch_hole_fixed_dia"])) + + self.ui.circular_ring_entry.set_value(float(self.app.defaults["tools_punch_circular_ring"])) + self.ui.oblong_ring_entry.set_value(float(self.app.defaults["tools_punch_oblong_ring"])) + self.ui.square_ring_entry.set_value(float(self.app.defaults["tools_punch_square_ring"])) + self.ui.rectangular_ring_entry.set_value(float(self.app.defaults["tools_punch_rectangular_ring"])) + self.ui.other_ring_entry.set_value(float(self.app.defaults["tools_punch_others_ring"])) + + self.ui.circular_cb.set_value(self.app.defaults["tools_punch_circular"]) + self.ui.oblong_cb.set_value(self.app.defaults["tools_punch_oblong"]) + self.ui.square_cb.set_value(self.app.defaults["tools_punch_square"]) + self.ui.rectangular_cb.set_value(self.app.defaults["tools_punch_rectangular"]) + self.ui.other_cb.set_value(self.app.defaults["tools_punch_others"]) + + self.ui.factor_entry.set_value(float(self.app.defaults["tools_punch_hole_prop_factor"])) + + def on_select_all(self, state): + self.ui_disconnect() + if state: + self.ui.circular_cb.setChecked(True) + self.ui.oblong_cb.setChecked(True) + self.ui.square_cb.setChecked(True) + self.ui.rectangular_cb.setChecked(True) + self.ui.other_cb.setChecked(True) + else: + self.ui.circular_cb.setChecked(False) + self.ui.oblong_cb.setChecked(False) + self.ui.square_cb.setChecked(False) + self.ui.rectangular_cb.setChecked(False) + self.ui.other_cb.setChecked(False) + self.ui_connect() + + def on_method(self, val): + self.ui.exc_label.setEnabled(False) + self.ui.exc_combo.setEnabled(False) + self.ui.fixed_label.setEnabled(False) + self.ui.dia_label.setEnabled(False) + self.ui.dia_entry.setEnabled(False) + self.ui.ring_frame.setEnabled(False) + self.ui.prop_label.setEnabled(False) + self.ui.factor_label.setEnabled(False) + self.ui.factor_entry.setEnabled(False) + + if val == 'exc': + self.ui.exc_label.setEnabled(True) + self.ui.exc_combo.setEnabled(True) + elif val == 'fixed': + self.ui.fixed_label.setEnabled(True) + self.ui.dia_label.setEnabled(True) + self.ui.dia_entry.setEnabled(True) + elif val == 'ring': + self.ui.ring_frame.setEnabled(True) + elif val == 'prop': + self.ui.prop_label.setEnabled(True) + self.ui.factor_label.setEnabled(True) + self.ui.factor_entry.setEnabled(True) + + def ui_connect(self): + self.ui.select_all_cb.stateChanged.connect(self.on_select_all) + + def ui_disconnect(self): + try: + self.ui.select_all_cb.stateChanged.disconnect() + except (AttributeError, TypeError): + pass + + def on_generate_object(self): + + # get the Gerber file who is the source of the punched Gerber + selection_index = self.ui.gerber_object_combo.currentIndex() + model_index = self.app.collection.index(selection_index, 0, self.ui.gerber_object_combo.rootModelIndex()) + + try: + grb_obj = model_index.internalPointer().obj + except Exception: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ...")) + return + + name = grb_obj.options['name'].rpartition('.')[0] + outname = name + "_punched" + + punch_method = self.ui.method_punch.get_value() + + new_options = {} + for opt in grb_obj.options: + new_options[opt] = deepcopy(grb_obj.options[opt]) + + if punch_method == 'exc': + + # get the Excellon file whose geometry will create the punch holes + selection_index = self.ui.exc_combo.currentIndex() + model_index = self.app.collection.index(selection_index, 0, self.ui.exc_combo.rootModelIndex()) + + try: + exc_obj = model_index.internalPointer().obj + except Exception: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Excellon object loaded ...")) + return + + # this is the punching geometry + exc_solid_geometry = MultiPolygon(exc_obj.solid_geometry) + if isinstance(grb_obj.solid_geometry, list): + grb_solid_geometry = MultiPolygon(grb_obj.solid_geometry) + else: + grb_solid_geometry = grb_obj.solid_geometry + + # create the punched Gerber solid_geometry + punched_solid_geometry = grb_solid_geometry.difference(exc_solid_geometry) + + # update the gerber apertures to include the clear geometry so it can be exported successfully + new_apertures = deepcopy(grb_obj.apertures) + new_apertures_items = new_apertures.items() + + # find maximum aperture id + new_apid = max([int(x) for x, __ in new_apertures_items]) + + # store here the clear geometry, the key is the drill size + holes_apertures = {} + + for apid, val in new_apertures_items: + for elem in val['geometry']: + # make it work only for Gerber Flashes who are Points in 'follow' + if 'solid' in elem and isinstance(elem['follow'], Point): + for drill in exc_obj.drills: + clear_apid_size = exc_obj.tools[drill['tool']]['tooldia'] + + # since there may be drills that do not drill into a pad we test only for drills in a pad + if drill['point'].within(elem['solid']): + geo_elem = {} + geo_elem['clear'] = drill['point'] + + if clear_apid_size not in holes_apertures: + holes_apertures[clear_apid_size] = {} + holes_apertures[clear_apid_size]['type'] = 'C' + holes_apertures[clear_apid_size]['size'] = clear_apid_size + holes_apertures[clear_apid_size]['geometry'] = [] + + holes_apertures[clear_apid_size]['geometry'].append(deepcopy(geo_elem)) + + # add the clear geometry to new apertures; it's easier than to test if there are apertures with the same + # size and add there the clear geometry + for hole_size, ap_val in holes_apertures.items(): + new_apid += 1 + new_apertures[str(new_apid)] = deepcopy(ap_val) + + def init_func(new_obj, app_obj): + new_obj.options.update(new_options) + new_obj.options['name'] = outname + new_obj.fill_color = deepcopy(grb_obj.fill_color) + new_obj.outline_color = deepcopy(grb_obj.outline_color) + + new_obj.apertures = deepcopy(new_apertures) + + new_obj.solid_geometry = deepcopy(punched_solid_geometry) + new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None, + local_use=new_obj, use_thread=False) + + self.app.app_obj.new_object('gerber', outname, init_func) + elif punch_method == 'fixed': + punch_size = float(self.ui.dia_entry.get_value()) + + if punch_size == 0.0: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("The value of the fixed diameter is 0.0. Aborting.")) + return 'fail' + + fail_msg = _("Could not generate punched hole Gerber because the punch hole size is bigger than" + " some of the apertures in the Gerber object.") + + punching_geo = [] + for apid in grb_obj.apertures: + if grb_obj.apertures[apid]['type'] == 'C' and self.ui.circular_cb.get_value(): + for elem in grb_obj.apertures[apid]['geometry']: + if 'follow' in elem: + if isinstance(elem['follow'], Point): + if punch_size >= float(grb_obj.apertures[apid]['size']): + self.app.inform.emit('[ERROR_NOTCL] %s' % fail_msg) + return 'fail' + punching_geo.append(elem['follow'].buffer(punch_size / 2)) + elif grb_obj.apertures[apid]['type'] == 'R': + + if round(float(grb_obj.apertures[apid]['width']), self.decimals) == \ + round(float(grb_obj.apertures[apid]['height']), self.decimals) and \ + self.ui.square_cb.get_value(): + for elem in grb_obj.apertures[apid]['geometry']: + if 'follow' in elem: + if isinstance(elem['follow'], Point): + if punch_size >= float(grb_obj.apertures[apid]['width']) or \ + punch_size >= float(grb_obj.apertures[apid]['height']): + self.app.inform.emit('[ERROR_NOTCL] %s' % fail_msg) + return 'fail' + punching_geo.append(elem['follow'].buffer(punch_size / 2)) + elif round(float(grb_obj.apertures[apid]['width']), self.decimals) != \ + round(float(grb_obj.apertures[apid]['height']), self.decimals) and \ + self.ui.rectangular_cb.get_value(): + for elem in grb_obj.apertures[apid]['geometry']: + if 'follow' in elem: + if isinstance(elem['follow'], Point): + if punch_size >= float(grb_obj.apertures[apid]['width']) or \ + punch_size >= float(grb_obj.apertures[apid]['height']): + self.app.inform.emit('[ERROR_NOTCL] %s' % fail_msg) + return 'fail' + punching_geo.append(elem['follow'].buffer(punch_size / 2)) + elif grb_obj.apertures[apid]['type'] == 'O' and self.ui.oblong_cb.get_value(): + for elem in grb_obj.apertures[apid]['geometry']: + if 'follow' in elem: + if isinstance(elem['follow'], Point): + if punch_size >= float(grb_obj.apertures[apid]['size']): + self.app.inform.emit('[ERROR_NOTCL] %s' % fail_msg) + return 'fail' + punching_geo.append(elem['follow'].buffer(punch_size / 2)) + elif grb_obj.apertures[apid]['type'] not in ['C', 'R', 'O'] and self.ui.other_cb.get_value(): + for elem in grb_obj.apertures[apid]['geometry']: + if 'follow' in elem: + if isinstance(elem['follow'], Point): + if punch_size >= float(grb_obj.apertures[apid]['size']): + self.app.inform.emit('[ERROR_NOTCL] %s' % fail_msg) + return 'fail' + punching_geo.append(elem['follow'].buffer(punch_size / 2)) + + punching_geo = MultiPolygon(punching_geo) + if isinstance(grb_obj.solid_geometry, list): + temp_solid_geometry = MultiPolygon(grb_obj.solid_geometry) + else: + temp_solid_geometry = grb_obj.solid_geometry + punched_solid_geometry = temp_solid_geometry.difference(punching_geo) + + if punched_solid_geometry == temp_solid_geometry: + self.app.inform.emit('[WARNING_NOTCL] %s' % + _("Could not generate punched hole Gerber because the newly created object " + "geometry is the same as the one in the source object geometry...")) + return 'fail' + + # update the gerber apertures to include the clear geometry so it can be exported successfully + new_apertures = deepcopy(grb_obj.apertures) + new_apertures_items = new_apertures.items() + + # find maximum aperture id + new_apid = max([int(x) for x, __ in new_apertures_items]) + + # store here the clear geometry, the key is the drill size + holes_apertures = {} + + for apid, val in new_apertures_items: + for elem in val['geometry']: + # make it work only for Gerber Flashes who are Points in 'follow' + if 'solid' in elem and isinstance(elem['follow'], Point): + for geo in punching_geo: + clear_apid_size = punch_size + + # since there may be drills that do not drill into a pad we test only for drills in a pad + if geo.within(elem['solid']): + geo_elem = {} + geo_elem['clear'] = geo.centroid + + if clear_apid_size not in holes_apertures: + holes_apertures[clear_apid_size] = {} + holes_apertures[clear_apid_size]['type'] = 'C' + holes_apertures[clear_apid_size]['size'] = clear_apid_size + holes_apertures[clear_apid_size]['geometry'] = [] + + holes_apertures[clear_apid_size]['geometry'].append(deepcopy(geo_elem)) + + # add the clear geometry to new apertures; it's easier than to test if there are apertures with the same + # size and add there the clear geometry + for hole_size, ap_val in holes_apertures.items(): + new_apid += 1 + new_apertures[str(new_apid)] = deepcopy(ap_val) + + def init_func(new_obj, app_obj): + new_obj.options.update(new_options) + new_obj.options['name'] = outname + new_obj.fill_color = deepcopy(grb_obj.fill_color) + new_obj.outline_color = deepcopy(grb_obj.outline_color) + + new_obj.apertures = deepcopy(new_apertures) + + new_obj.solid_geometry = deepcopy(punched_solid_geometry) + new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None, + local_use=new_obj, use_thread=False) + + self.app.app_obj.new_object('gerber', outname, init_func) + elif punch_method == 'ring': + circ_r_val = self.ui.circular_ring_entry.get_value() + oblong_r_val = self.ui.oblong_ring_entry.get_value() + square_r_val = self.ui.square_ring_entry.get_value() + rect_r_val = self.ui.rectangular_ring_entry.get_value() + other_r_val = self.ui.other_ring_entry.get_value() + + dia = None + + if isinstance(grb_obj.solid_geometry, list): + temp_solid_geometry = MultiPolygon(grb_obj.solid_geometry) + else: + temp_solid_geometry = grb_obj.solid_geometry + + punched_solid_geometry = temp_solid_geometry + + new_apertures = deepcopy(grb_obj.apertures) + new_apertures_items = new_apertures.items() + + # find maximum aperture id + new_apid = max([int(x) for x, __ in new_apertures_items]) + + # store here the clear geometry, the key is the new aperture size + holes_apertures = {} + + for apid, apid_value in grb_obj.apertures.items(): + ap_type = apid_value['type'] + punching_geo = [] + + if ap_type == 'C' and self.ui.circular_cb.get_value(): + dia = float(apid_value['size']) - (2 * circ_r_val) + for elem in apid_value['geometry']: + if 'follow' in elem and isinstance(elem['follow'], Point): + punching_geo.append(elem['follow'].buffer(dia / 2)) + + elif ap_type == 'O' and self.ui.oblong_cb.get_value(): + width = float(apid_value['width']) + height = float(apid_value['height']) + + if width > height: + dia = float(apid_value['height']) - (2 * oblong_r_val) + else: + dia = float(apid_value['width']) - (2 * oblong_r_val) + + for elem in grb_obj.apertures[apid]['geometry']: + if 'follow' in elem: + if isinstance(elem['follow'], Point): + punching_geo.append(elem['follow'].buffer(dia / 2)) + + 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.ui.square_cb.get_value(): + dia = float(apid_value['height']) - (2 * square_r_val) + + for elem in grb_obj.apertures[apid]['geometry']: + if 'follow' in elem: + if isinstance(elem['follow'], Point): + punching_geo.append(elem['follow'].buffer(dia / 2)) + elif self.ui.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) + + for elem in grb_obj.apertures[apid]['geometry']: + if 'follow' in elem: + if isinstance(elem['follow'], Point): + punching_geo.append(elem['follow'].buffer(dia / 2)) + + elif self.ui.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) + + for elem in grb_obj.apertures[apid]['geometry']: + if 'follow' in elem: + if isinstance(elem['follow'], Point): + punching_geo.append(elem['follow'].buffer(dia / 2)) + + # if dia is None then none of the above applied so we skip the following + if dia is None: + continue + + punching_geo = MultiPolygon(punching_geo) + + if punching_geo is None or punching_geo.is_empty: + continue + + punched_solid_geometry = punched_solid_geometry.difference(punching_geo) + + # update the gerber apertures to include the clear geometry so it can be exported successfully + for elem in apid_value['geometry']: + # make it work only for Gerber Flashes who are Points in 'follow' + if 'solid' in elem and isinstance(elem['follow'], Point): + clear_apid_size = dia + for geo in punching_geo: + + # since there may be drills that do not drill into a pad we test only for geos in a pad + if geo.within(elem['solid']): + geo_elem = {} + geo_elem['clear'] = geo.centroid + + if clear_apid_size not in holes_apertures: + holes_apertures[clear_apid_size] = {} + holes_apertures[clear_apid_size]['type'] = 'C' + holes_apertures[clear_apid_size]['size'] = clear_apid_size + holes_apertures[clear_apid_size]['geometry'] = [] + + holes_apertures[clear_apid_size]['geometry'].append(deepcopy(geo_elem)) + + # add the clear geometry to new apertures; it's easier than to test if there are apertures with the same + # size and add there the clear geometry + for hole_size, ap_val in holes_apertures.items(): + new_apid += 1 + new_apertures[str(new_apid)] = deepcopy(ap_val) + + def init_func(new_obj, app_obj): + new_obj.options.update(new_options) + new_obj.options['name'] = outname + new_obj.fill_color = deepcopy(grb_obj.fill_color) + new_obj.outline_color = deepcopy(grb_obj.outline_color) + + new_obj.apertures = deepcopy(new_apertures) + + new_obj.solid_geometry = deepcopy(punched_solid_geometry) + new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None, + local_use=new_obj, use_thread=False) + + self.app.app_obj.new_object('gerber', outname, init_func) + + elif punch_method == 'prop': + prop_factor = self.ui.factor_entry.get_value() / 100.0 + + dia = None + + if isinstance(grb_obj.solid_geometry, list): + temp_solid_geometry = MultiPolygon(grb_obj.solid_geometry) + else: + temp_solid_geometry = grb_obj.solid_geometry + + punched_solid_geometry = temp_solid_geometry + + new_apertures = deepcopy(grb_obj.apertures) + new_apertures_items = new_apertures.items() + + # find maximum aperture id + new_apid = max([int(x) for x, __ in new_apertures_items]) + + # store here the clear geometry, the key is the new aperture size + holes_apertures = {} + + for apid, apid_value in grb_obj.apertures.items(): + ap_type = apid_value['type'] + punching_geo = [] + + if ap_type == 'C' and self.ui.circular_cb.get_value(): + dia = float(apid_value['size']) * prop_factor + for elem in apid_value['geometry']: + if 'follow' in elem and isinstance(elem['follow'], Point): + punching_geo.append(elem['follow'].buffer(dia / 2)) + + elif ap_type == 'O' and self.ui.oblong_cb.get_value(): + width = float(apid_value['width']) + height = float(apid_value['height']) + + if width > height: + dia = float(apid_value['height']) * prop_factor + else: + dia = float(apid_value['width']) * prop_factor + + for elem in grb_obj.apertures[apid]['geometry']: + if 'follow' in elem: + if isinstance(elem['follow'], Point): + punching_geo.append(elem['follow'].buffer(dia / 2)) + + 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.ui.square_cb.get_value(): + dia = float(apid_value['height']) * prop_factor + + for elem in grb_obj.apertures[apid]['geometry']: + if 'follow' in elem: + if isinstance(elem['follow'], Point): + punching_geo.append(elem['follow'].buffer(dia / 2)) + elif self.ui.rectangular_cb.get_value(): + if width > height: + dia = float(apid_value['height']) * prop_factor + else: + dia = float(apid_value['width']) * prop_factor + + for elem in grb_obj.apertures[apid]['geometry']: + if 'follow' in elem: + if isinstance(elem['follow'], Point): + punching_geo.append(elem['follow'].buffer(dia / 2)) + + elif self.ui.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 + + for elem in grb_obj.apertures[apid]['geometry']: + if 'follow' in elem: + if isinstance(elem['follow'], Point): + punching_geo.append(elem['follow'].buffer(dia / 2)) + + # if dia is None then none of the above applied so we skip the following + if dia is None: + continue + + punching_geo = MultiPolygon(punching_geo) + + if punching_geo is None or punching_geo.is_empty: + continue + + punched_solid_geometry = punched_solid_geometry.difference(punching_geo) + + # update the gerber apertures to include the clear geometry so it can be exported successfully + for elem in apid_value['geometry']: + # make it work only for Gerber Flashes who are Points in 'follow' + if 'solid' in elem and isinstance(elem['follow'], Point): + clear_apid_size = dia + for geo in punching_geo: + + # since there may be drills that do not drill into a pad we test only for geos in a pad + if geo.within(elem['solid']): + geo_elem = {} + geo_elem['clear'] = geo.centroid + + if clear_apid_size not in holes_apertures: + holes_apertures[clear_apid_size] = {} + holes_apertures[clear_apid_size]['type'] = 'C' + holes_apertures[clear_apid_size]['size'] = clear_apid_size + holes_apertures[clear_apid_size]['geometry'] = [] + + holes_apertures[clear_apid_size]['geometry'].append(deepcopy(geo_elem)) + + # add the clear geometry to new apertures; it's easier than to test if there are apertures with the same + # size and add there the clear geometry + for hole_size, ap_val in holes_apertures.items(): + new_apid += 1 + new_apertures[str(new_apid)] = deepcopy(ap_val) + + def init_func(new_obj, app_obj): + new_obj.options.update(new_options) + new_obj.options['name'] = outname + new_obj.fill_color = deepcopy(grb_obj.fill_color) + new_obj.outline_color = deepcopy(grb_obj.outline_color) + + new_obj.apertures = deepcopy(new_apertures) + + new_obj.solid_geometry = deepcopy(punched_solid_geometry) + new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None, + local_use=new_obj, use_thread=False) + + self.app.app_obj.new_object('gerber', outname, init_func) + + def reset_fields(self): + self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex())) + self.ui_disconnect() + + +class PunchUI: + + toolName = _("Punch Gerber") + + def __init__(self, layout, app): + self.app = app + self.decimals = self.app.decimals + self.layout = layout + + # ## Title title_label = QtWidgets.QLabel("%s" % self.toolName) title_label.setStyleSheet(""" - QLabel - { - font-size: 16px; - font-weight: bold; - } - """) + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) # Punch Drill holes @@ -326,11 +980,11 @@ class ToolPunchGerber(AppTool): "the specified box.") ) self.punch_object_button.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) + QPushButton + { + font-weight: bold; + } + """) self.layout.addWidget(self.punch_object_button) self.layout.addStretch() @@ -341,20 +995,13 @@ class ToolPunchGerber(AppTool): _("Will reset the tool parameters.") ) self.reset_button.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) + QPushButton + { + font-weight: bold; + } + """) self.layout.addWidget(self.reset_button) - self.units = self.app.defaults['units'] - - # self.cb_items = [ - # self.grid1.itemAt(w).widget() for w in range(self.grid1.count()) - # if isinstance(self.grid1.itemAt(w).widget(), FCCheckBox) - # ] - self.circular_ring_entry.setEnabled(False) self.oblong_ring_entry.setEnabled(False) self.square_ring_entry.setEnabled(False) @@ -366,638 +1013,22 @@ class ToolPunchGerber(AppTool): self.factor_label.setDisabled(True) self.factor_entry.setDisabled(True) - # ## Signals - self.method_punch.activated_custom.connect(self.on_method) - self.reset_button.clicked.connect(self.set_tool_ui) - self.punch_object_button.clicked.connect(self.on_generate_object) + # #################################### FINSIHED GUI ########################### + # ############################################################################# - 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 run(self, toggle=True): - self.app.defaults.report_usage("ToolPunchGerber()") - - 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 + def confirmation_message(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"), + self.decimals, + minval, + self.decimals, + maxval), False) else: - if self.app.ui.splitter.sizes()[0] == 0: - self.app.ui.splitter.setSizes([1, 1]) + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) - AppTool.run(self) - - self.set_tool_ui() - - self.app.ui.notebook.setTabText(2, _("Punch Tool")) - - def install(self, icon=None, separator=None, **kwargs): - AppTool.install(self, icon, separator, shortcut='Alt+H', **kwargs) - - def set_tool_ui(self): - self.reset_fields() - - self.ui_connect() - self.method_punch.set_value(self.app.defaults["tools_punch_hole_type"]) - self.select_all_cb.set_value(False) - - self.dia_entry.set_value(float(self.app.defaults["tools_punch_hole_fixed_dia"])) - - self.circular_ring_entry.set_value(float(self.app.defaults["tools_punch_circular_ring"])) - self.oblong_ring_entry.set_value(float(self.app.defaults["tools_punch_oblong_ring"])) - self.square_ring_entry.set_value(float(self.app.defaults["tools_punch_square_ring"])) - self.rectangular_ring_entry.set_value(float(self.app.defaults["tools_punch_rectangular_ring"])) - self.other_ring_entry.set_value(float(self.app.defaults["tools_punch_others_ring"])) - - self.circular_cb.set_value(self.app.defaults["tools_punch_circular"]) - self.oblong_cb.set_value(self.app.defaults["tools_punch_oblong"]) - self.square_cb.set_value(self.app.defaults["tools_punch_square"]) - self.rectangular_cb.set_value(self.app.defaults["tools_punch_rectangular"]) - self.other_cb.set_value(self.app.defaults["tools_punch_others"]) - - self.factor_entry.set_value(float(self.app.defaults["tools_punch_hole_prop_factor"])) - - def on_select_all(self, state): - self.ui_disconnect() - if state: - self.circular_cb.setChecked(True) - self.oblong_cb.setChecked(True) - self.square_cb.setChecked(True) - self.rectangular_cb.setChecked(True) - self.other_cb.setChecked(True) + def confirmation_message_int(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' % + (_("Edited value is out of range"), minval, maxval), False) else: - self.circular_cb.setChecked(False) - self.oblong_cb.setChecked(False) - self.square_cb.setChecked(False) - self.rectangular_cb.setChecked(False) - self.other_cb.setChecked(False) - self.ui_connect() - - def on_method(self, val): - self.exc_label.setEnabled(False) - self.exc_combo.setEnabled(False) - self.fixed_label.setEnabled(False) - self.dia_label.setEnabled(False) - self.dia_entry.setEnabled(False) - self.ring_frame.setEnabled(False) - self.prop_label.setEnabled(False) - self.factor_label.setEnabled(False) - self.factor_entry.setEnabled(False) - - if val == 'exc': - self.exc_label.setEnabled(True) - self.exc_combo.setEnabled(True) - elif val == 'fixed': - self.fixed_label.setEnabled(True) - self.dia_label.setEnabled(True) - self.dia_entry.setEnabled(True) - elif val == 'ring': - self.ring_frame.setEnabled(True) - elif val == 'prop': - self.prop_label.setEnabled(True) - self.factor_label.setEnabled(True) - self.factor_entry.setEnabled(True) - - def ui_connect(self): - self.select_all_cb.stateChanged.connect(self.on_select_all) - - def ui_disconnect(self): - try: - self.select_all_cb.stateChanged.disconnect() - except (AttributeError, TypeError): - pass - - def on_generate_object(self): - - # get the Gerber file who is the source of the punched Gerber - selection_index = self.gerber_object_combo.currentIndex() - model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex()) - - try: - grb_obj = model_index.internalPointer().obj - except Exception: - self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ...")) - return - - name = grb_obj.options['name'].rpartition('.')[0] - outname = name + "_punched" - - punch_method = self.method_punch.get_value() - - new_options = {} - for opt in grb_obj.options: - new_options[opt] = deepcopy(grb_obj.options[opt]) - - if punch_method == 'exc': - - # get the Excellon file whose geometry will create the punch holes - selection_index = self.exc_combo.currentIndex() - model_index = self.app.collection.index(selection_index, 0, self.exc_combo.rootModelIndex()) - - try: - exc_obj = model_index.internalPointer().obj - except Exception: - self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Excellon object loaded ...")) - return - - # this is the punching geometry - exc_solid_geometry = MultiPolygon(exc_obj.solid_geometry) - if isinstance(grb_obj.solid_geometry, list): - grb_solid_geometry = MultiPolygon(grb_obj.solid_geometry) - else: - grb_solid_geometry = grb_obj.solid_geometry - - # create the punched Gerber solid_geometry - punched_solid_geometry = grb_solid_geometry.difference(exc_solid_geometry) - - # update the gerber apertures to include the clear geometry so it can be exported successfully - new_apertures = deepcopy(grb_obj.apertures) - new_apertures_items = new_apertures.items() - - # find maximum aperture id - new_apid = max([int(x) for x, __ in new_apertures_items]) - - # store here the clear geometry, the key is the drill size - holes_apertures = {} - - for apid, val in new_apertures_items: - for elem in val['geometry']: - # make it work only for Gerber Flashes who are Points in 'follow' - if 'solid' in elem and isinstance(elem['follow'], Point): - for drill in exc_obj.drills: - clear_apid_size = exc_obj.tools[drill['tool']]['tooldia'] - - # since there may be drills that do not drill into a pad we test only for drills in a pad - if drill['point'].within(elem['solid']): - geo_elem = {} - geo_elem['clear'] = drill['point'] - - if clear_apid_size not in holes_apertures: - holes_apertures[clear_apid_size] = {} - holes_apertures[clear_apid_size]['type'] = 'C' - holes_apertures[clear_apid_size]['size'] = clear_apid_size - holes_apertures[clear_apid_size]['geometry'] = [] - - holes_apertures[clear_apid_size]['geometry'].append(deepcopy(geo_elem)) - - # add the clear geometry to new apertures; it's easier than to test if there are apertures with the same - # size and add there the clear geometry - for hole_size, ap_val in holes_apertures.items(): - new_apid += 1 - new_apertures[str(new_apid)] = deepcopy(ap_val) - - def init_func(new_obj, app_obj): - new_obj.options.update(new_options) - new_obj.options['name'] = outname - new_obj.fill_color = deepcopy(grb_obj.fill_color) - new_obj.outline_color = deepcopy(grb_obj.outline_color) - - new_obj.apertures = deepcopy(new_apertures) - - new_obj.solid_geometry = deepcopy(punched_solid_geometry) - new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None, - local_use=new_obj, use_thread=False) - - self.app.app_obj.new_object('gerber', outname, init_func) - elif punch_method == 'fixed': - punch_size = float(self.dia_entry.get_value()) - - if punch_size == 0.0: - self.app.inform.emit('[WARNING_NOTCL] %s' % _("The value of the fixed diameter is 0.0. Aborting.")) - return 'fail' - - fail_msg = _("Could not generate punched hole Gerber because the punch hole size is bigger than" - " some of the apertures in the Gerber object.") - - punching_geo = [] - for apid in grb_obj.apertures: - if grb_obj.apertures[apid]['type'] == 'C' and self.circular_cb.get_value(): - for elem in grb_obj.apertures[apid]['geometry']: - if 'follow' in elem: - if isinstance(elem['follow'], Point): - if punch_size >= float(grb_obj.apertures[apid]['size']): - self.app.inform.emit('[ERROR_NOTCL] %s' % fail_msg) - return 'fail' - punching_geo.append(elem['follow'].buffer(punch_size / 2)) - elif grb_obj.apertures[apid]['type'] == 'R': - - if round(float(grb_obj.apertures[apid]['width']), self.decimals) == \ - round(float(grb_obj.apertures[apid]['height']), self.decimals) and \ - self.square_cb.get_value(): - for elem in grb_obj.apertures[apid]['geometry']: - if 'follow' in elem: - if isinstance(elem['follow'], Point): - if punch_size >= float(grb_obj.apertures[apid]['width']) or \ - punch_size >= float(grb_obj.apertures[apid]['height']): - self.app.inform.emit('[ERROR_NOTCL] %s' % fail_msg) - return 'fail' - punching_geo.append(elem['follow'].buffer(punch_size / 2)) - elif round(float(grb_obj.apertures[apid]['width']), self.decimals) != \ - round(float(grb_obj.apertures[apid]['height']), self.decimals) and \ - self.rectangular_cb.get_value(): - for elem in grb_obj.apertures[apid]['geometry']: - if 'follow' in elem: - if isinstance(elem['follow'], Point): - if punch_size >= float(grb_obj.apertures[apid]['width']) or \ - punch_size >= float(grb_obj.apertures[apid]['height']): - self.app.inform.emit('[ERROR_NOTCL] %s' % fail_msg) - return 'fail' - punching_geo.append(elem['follow'].buffer(punch_size / 2)) - elif grb_obj.apertures[apid]['type'] == 'O' and self.oblong_cb.get_value(): - for elem in grb_obj.apertures[apid]['geometry']: - if 'follow' in elem: - if isinstance(elem['follow'], Point): - if punch_size >= float(grb_obj.apertures[apid]['size']): - self.app.inform.emit('[ERROR_NOTCL] %s' % fail_msg) - return 'fail' - punching_geo.append(elem['follow'].buffer(punch_size / 2)) - elif grb_obj.apertures[apid]['type'] not in ['C', 'R', 'O'] and self.other_cb.get_value(): - for elem in grb_obj.apertures[apid]['geometry']: - if 'follow' in elem: - if isinstance(elem['follow'], Point): - if punch_size >= float(grb_obj.apertures[apid]['size']): - self.app.inform.emit('[ERROR_NOTCL] %s' % fail_msg) - return 'fail' - punching_geo.append(elem['follow'].buffer(punch_size / 2)) - - punching_geo = MultiPolygon(punching_geo) - if isinstance(grb_obj.solid_geometry, list): - temp_solid_geometry = MultiPolygon(grb_obj.solid_geometry) - else: - temp_solid_geometry = grb_obj.solid_geometry - punched_solid_geometry = temp_solid_geometry.difference(punching_geo) - - if punched_solid_geometry == temp_solid_geometry: - self.app.inform.emit('[WARNING_NOTCL] %s' % - _("Could not generate punched hole Gerber because the newly created object " - "geometry is the same as the one in the source object geometry...")) - return 'fail' - - # update the gerber apertures to include the clear geometry so it can be exported successfully - new_apertures = deepcopy(grb_obj.apertures) - new_apertures_items = new_apertures.items() - - # find maximum aperture id - new_apid = max([int(x) for x, __ in new_apertures_items]) - - # store here the clear geometry, the key is the drill size - holes_apertures = {} - - for apid, val in new_apertures_items: - for elem in val['geometry']: - # make it work only for Gerber Flashes who are Points in 'follow' - if 'solid' in elem and isinstance(elem['follow'], Point): - for geo in punching_geo: - clear_apid_size = punch_size - - # since there may be drills that do not drill into a pad we test only for drills in a pad - if geo.within(elem['solid']): - geo_elem = {} - geo_elem['clear'] = geo.centroid - - if clear_apid_size not in holes_apertures: - holes_apertures[clear_apid_size] = {} - holes_apertures[clear_apid_size]['type'] = 'C' - holes_apertures[clear_apid_size]['size'] = clear_apid_size - holes_apertures[clear_apid_size]['geometry'] = [] - - holes_apertures[clear_apid_size]['geometry'].append(deepcopy(geo_elem)) - - # add the clear geometry to new apertures; it's easier than to test if there are apertures with the same - # size and add there the clear geometry - for hole_size, ap_val in holes_apertures.items(): - new_apid += 1 - new_apertures[str(new_apid)] = deepcopy(ap_val) - - def init_func(new_obj, app_obj): - new_obj.options.update(new_options) - new_obj.options['name'] = outname - new_obj.fill_color = deepcopy(grb_obj.fill_color) - new_obj.outline_color = deepcopy(grb_obj.outline_color) - - new_obj.apertures = deepcopy(new_apertures) - - new_obj.solid_geometry = deepcopy(punched_solid_geometry) - new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None, - local_use=new_obj, use_thread=False) - - self.app.app_obj.new_object('gerber', outname, init_func) - elif punch_method == 'ring': - 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() - - dia = None - - if isinstance(grb_obj.solid_geometry, list): - temp_solid_geometry = MultiPolygon(grb_obj.solid_geometry) - else: - temp_solid_geometry = grb_obj.solid_geometry - - punched_solid_geometry = temp_solid_geometry - - new_apertures = deepcopy(grb_obj.apertures) - new_apertures_items = new_apertures.items() - - # find maximum aperture id - new_apid = max([int(x) for x, __ in new_apertures_items]) - - # store here the clear geometry, the key is the new aperture size - holes_apertures = {} - - for apid, apid_value in grb_obj.apertures.items(): - ap_type = apid_value['type'] - punching_geo = [] - - if ap_type == 'C' and self.circular_cb.get_value(): - dia = float(apid_value['size']) - (2 * circ_r_val) - for elem in apid_value['geometry']: - if 'follow' in elem and isinstance(elem['follow'], Point): - punching_geo.append(elem['follow'].buffer(dia / 2)) - - elif ap_type == 'O' and self.oblong_cb.get_value(): - width = float(apid_value['width']) - height = float(apid_value['height']) - - if width > height: - dia = float(apid_value['height']) - (2 * oblong_r_val) - else: - dia = float(apid_value['width']) - (2 * oblong_r_val) - - for elem in grb_obj.apertures[apid]['geometry']: - if 'follow' in elem: - if isinstance(elem['follow'], Point): - punching_geo.append(elem['follow'].buffer(dia / 2)) - - 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(): - dia = float(apid_value['height']) - (2 * square_r_val) - - for elem in grb_obj.apertures[apid]['geometry']: - if 'follow' in elem: - if isinstance(elem['follow'], Point): - punching_geo.append(elem['follow'].buffer(dia / 2)) - elif 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) - - for elem in grb_obj.apertures[apid]['geometry']: - if 'follow' in elem: - if isinstance(elem['follow'], Point): - punching_geo.append(elem['follow'].buffer(dia / 2)) - - elif 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) - - for elem in grb_obj.apertures[apid]['geometry']: - if 'follow' in elem: - if isinstance(elem['follow'], Point): - punching_geo.append(elem['follow'].buffer(dia / 2)) - - # if dia is None then none of the above applied so we skip the following - if dia is None: - continue - - punching_geo = MultiPolygon(punching_geo) - - if punching_geo is None or punching_geo.is_empty: - continue - - punched_solid_geometry = punched_solid_geometry.difference(punching_geo) - - # update the gerber apertures to include the clear geometry so it can be exported successfully - for elem in apid_value['geometry']: - # make it work only for Gerber Flashes who are Points in 'follow' - if 'solid' in elem and isinstance(elem['follow'], Point): - clear_apid_size = dia - for geo in punching_geo: - - # since there may be drills that do not drill into a pad we test only for geos in a pad - if geo.within(elem['solid']): - geo_elem = {} - geo_elem['clear'] = geo.centroid - - if clear_apid_size not in holes_apertures: - holes_apertures[clear_apid_size] = {} - holes_apertures[clear_apid_size]['type'] = 'C' - holes_apertures[clear_apid_size]['size'] = clear_apid_size - holes_apertures[clear_apid_size]['geometry'] = [] - - holes_apertures[clear_apid_size]['geometry'].append(deepcopy(geo_elem)) - - # add the clear geometry to new apertures; it's easier than to test if there are apertures with the same - # size and add there the clear geometry - for hole_size, ap_val in holes_apertures.items(): - new_apid += 1 - new_apertures[str(new_apid)] = deepcopy(ap_val) - - def init_func(new_obj, app_obj): - new_obj.options.update(new_options) - new_obj.options['name'] = outname - new_obj.fill_color = deepcopy(grb_obj.fill_color) - new_obj.outline_color = deepcopy(grb_obj.outline_color) - - new_obj.apertures = deepcopy(new_apertures) - - new_obj.solid_geometry = deepcopy(punched_solid_geometry) - new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None, - local_use=new_obj, use_thread=False) - - self.app.app_obj.new_object('gerber', outname, init_func) - - elif punch_method == 'prop': - prop_factor = self.factor_entry.get_value() / 100.0 - - dia = None - - if isinstance(grb_obj.solid_geometry, list): - temp_solid_geometry = MultiPolygon(grb_obj.solid_geometry) - else: - temp_solid_geometry = grb_obj.solid_geometry - - punched_solid_geometry = temp_solid_geometry - - new_apertures = deepcopy(grb_obj.apertures) - new_apertures_items = new_apertures.items() - - # find maximum aperture id - new_apid = max([int(x) for x, __ in new_apertures_items]) - - # store here the clear geometry, the key is the new aperture size - holes_apertures = {} - - for apid, apid_value in grb_obj.apertures.items(): - ap_type = apid_value['type'] - punching_geo = [] - - if ap_type == 'C' and self.circular_cb.get_value(): - dia = float(apid_value['size']) * prop_factor - for elem in apid_value['geometry']: - if 'follow' in elem and isinstance(elem['follow'], Point): - punching_geo.append(elem['follow'].buffer(dia / 2)) - - elif ap_type == 'O' and self.oblong_cb.get_value(): - width = float(apid_value['width']) - height = float(apid_value['height']) - - if width > height: - dia = float(apid_value['height']) * prop_factor - else: - dia = float(apid_value['width']) * prop_factor - - for elem in grb_obj.apertures[apid]['geometry']: - if 'follow' in elem: - if isinstance(elem['follow'], Point): - punching_geo.append(elem['follow'].buffer(dia / 2)) - - 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(): - dia = float(apid_value['height']) * prop_factor - - for elem in grb_obj.apertures[apid]['geometry']: - if 'follow' in elem: - if isinstance(elem['follow'], Point): - punching_geo.append(elem['follow'].buffer(dia / 2)) - elif self.rectangular_cb.get_value(): - if width > height: - dia = float(apid_value['height']) * prop_factor - else: - dia = float(apid_value['width']) * prop_factor - - for elem in grb_obj.apertures[apid]['geometry']: - if 'follow' in elem: - if isinstance(elem['follow'], Point): - punching_geo.append(elem['follow'].buffer(dia / 2)) - - elif 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 - - for elem in grb_obj.apertures[apid]['geometry']: - if 'follow' in elem: - if isinstance(elem['follow'], Point): - punching_geo.append(elem['follow'].buffer(dia / 2)) - - # if dia is None then none of the above applied so we skip the following - if dia is None: - continue - - punching_geo = MultiPolygon(punching_geo) - - if punching_geo is None or punching_geo.is_empty: - continue - - punched_solid_geometry = punched_solid_geometry.difference(punching_geo) - - # update the gerber apertures to include the clear geometry so it can be exported successfully - for elem in apid_value['geometry']: - # make it work only for Gerber Flashes who are Points in 'follow' - if 'solid' in elem and isinstance(elem['follow'], Point): - clear_apid_size = dia - for geo in punching_geo: - - # since there may be drills that do not drill into a pad we test only for geos in a pad - if geo.within(elem['solid']): - geo_elem = {} - geo_elem['clear'] = geo.centroid - - if clear_apid_size not in holes_apertures: - holes_apertures[clear_apid_size] = {} - holes_apertures[clear_apid_size]['type'] = 'C' - holes_apertures[clear_apid_size]['size'] = clear_apid_size - holes_apertures[clear_apid_size]['geometry'] = [] - - holes_apertures[clear_apid_size]['geometry'].append(deepcopy(geo_elem)) - - # add the clear geometry to new apertures; it's easier than to test if there are apertures with the same - # size and add there the clear geometry - for hole_size, ap_val in holes_apertures.items(): - new_apid += 1 - new_apertures[str(new_apid)] = deepcopy(ap_val) - - def init_func(new_obj, app_obj): - new_obj.options.update(new_options) - new_obj.options['name'] = outname - new_obj.fill_color = deepcopy(grb_obj.fill_color) - new_obj.outline_color = deepcopy(grb_obj.outline_color) - - new_obj.apertures = deepcopy(new_apertures) - - new_obj.solid_geometry = deepcopy(punched_solid_geometry) - new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None, - local_use=new_obj, use_thread=False) - - self.app.app_obj.new_object('gerber', outname, init_func) - - def reset_fields(self): - self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) - self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex())) - self.ui_disconnect() + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) diff --git a/appTools/ToolQRCode.py b/appTools/ToolQRCode.py index 5c79a7be..133b0912 100644 --- a/appTools/ToolQRCode.py +++ b/appTools/ToolQRCode.py @@ -40,8 +40,6 @@ log = logging.getLogger('base') class QRCode(AppTool): - toolName = _("QRCode Tool") - def __init__(self, app): AppTool.__init__(self, app) @@ -51,15 +49,595 @@ class QRCode(AppTool): self.decimals = self.app.decimals self.units = '' + # ############################################################################# + # ######################### Tool GUI ########################################## + # ############################################################################# + self.ui = QRcodeUI(layout=self.layout, app=self.app) + self.toolName = self.ui.toolName + + self.grb_object = None + self.box_poly = None + self.proc = None + + self.origin = (0, 0) + + self.mm = None + self.mr = None + self.kr = None + + self.shapes = self.app.move_tool.sel_shapes + self.qrcode_geometry = MultiPolygon() + self.qrcode_utility_geometry = MultiPolygon() + + self.old_back_color = '' + + # Signals # + self.ui.qrcode_button.clicked.connect(self.execute) + self.ui.export_cb.stateChanged.connect(self.on_export_frame) + self.ui.export_png_button.clicked.connect(self.export_png_file) + self.ui.export_svg_button.clicked.connect(self.export_svg_file) + + self.ui.fill_color_entry.editingFinished.connect(self.on_qrcode_fill_color_entry) + self.ui.fill_color_button.clicked.connect(self.on_qrcode_fill_color_button) + self.ui.back_color_entry.editingFinished.connect(self.on_qrcode_back_color_entry) + self.ui.back_color_button.clicked.connect(self.on_qrcode_back_color_button) + + self.ui.transparent_cb.stateChanged.connect(self.on_transparent_back_color) + self.ui.reset_button.clicked.connect(self.set_tool_ui) + + def run(self, toggle=True): + self.app.defaults.report_usage("QRCode()") + + 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]) + + AppTool.run(self) + + self.set_tool_ui() + + self.app.ui.notebook.setTabText(2, _("QRCode Tool")) + + def install(self, icon=None, separator=None, **kwargs): + AppTool.install(self, icon, separator, shortcut='Alt+Q', **kwargs) + + def set_tool_ui(self): + self.units = self.app.defaults['units'] + self.ui.border_size_entry.set_value(4) + + self.ui.version_entry.set_value(int(self.app.defaults["tools_qrcode_version"])) + self.ui.error_radio.set_value(self.app.defaults["tools_qrcode_error"]) + self.ui.bsize_entry.set_value(int(self.app.defaults["tools_qrcode_box_size"])) + self.ui.border_size_entry.set_value(int(self.app.defaults["tools_qrcode_border_size"])) + self.ui.pol_radio.set_value(self.app.defaults["tools_qrcode_polarity"]) + self.ui.bb_radio.set_value(self.app.defaults["tools_qrcode_rounded"]) + + self.ui.text_data.set_value(self.app.defaults["tools_qrcode_qrdata"]) + + self.ui.fill_color_entry.set_value(self.app.defaults['tools_qrcode_fill_color']) + self.ui.fill_color_button.setStyleSheet("background-color:%s" % + str(self.app.defaults['tools_qrcode_fill_color'])[:7]) + + self.ui.back_color_entry.set_value(self.app.defaults['tools_qrcode_back_color']) + self.ui.back_color_button.setStyleSheet("background-color:%s" % + str(self.app.defaults['tools_qrcode_back_color'])[:7]) + + def on_export_frame(self, state): + self.ui.export_frame.setVisible(state) + self.ui.qrcode_button.setVisible(not state) + + def execute(self): + text_data = self.ui.text_data.get_value() + if text_data == '': + self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box.")) + return + + # get the Gerber object on which the QRCode will be inserted + selection_index = self.ui.grb_object_combo.currentIndex() + model_index = self.app.collection.index(selection_index, 0, self.ui.grb_object_combo.rootModelIndex()) + + try: + self.grb_object = model_index.internalPointer().obj + except Exception as e: + log.debug("QRCode.execute() --> %s" % str(e)) + self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ...")) + return + + # we can safely activate the mouse events + self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move) + self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release) + self.kr = self.app.plotcanvas.graph_event_connect('key_release', self.on_key_release) + + self.proc = self.app.proc_container.new('%s...' % _("Generating QRCode geometry")) + + def job_thread_qr(app_obj): + error_code = { + 'L': qrcode.constants.ERROR_CORRECT_L, + 'M': qrcode.constants.ERROR_CORRECT_M, + 'Q': qrcode.constants.ERROR_CORRECT_Q, + 'H': qrcode.constants.ERROR_CORRECT_H + }[self.ui.error_radio.get_value()] + + qr = qrcode.QRCode( + version=self.ui.version_entry.get_value(), + error_correction=error_code, + box_size=self.ui.bsize_entry.get_value(), + border=self.ui.border_size_entry.get_value(), + image_factory=qrcode.image.svg.SvgFragmentImage + ) + qr.add_data(text_data) + qr.make() + + svg_file = BytesIO() + img = qr.make_image() + img.save(svg_file) + + svg_text = StringIO(svg_file.getvalue().decode('UTF-8')) + svg_geometry = self.convert_svg_to_geo(svg_text, units=self.units) + self.qrcode_geometry = deepcopy(svg_geometry) + + svg_geometry = unary_union(svg_geometry).buffer(0.0000001).buffer(-0.0000001) + self.qrcode_utility_geometry = svg_geometry + + # make a bounding box of the QRCode geometry to help drawing the utility geometry in case it is too + # complicated + try: + a, b, c, d = self.qrcode_utility_geometry.bounds + self.box_poly = box(minx=a, miny=b, maxx=c, maxy=d) + except Exception as ee: + log.debug("QRCode.make() bounds error --> %s" % str(ee)) + + app_obj.call_source = 'qrcode_tool' + app_obj.inform.emit(_("Click on the Destination point ...")) + + self.app.worker_task.emit({'fcn': job_thread_qr, 'params': [self.app]}) + + def make(self, pos): + self.on_exit() + + # make sure that the source object solid geometry is an Iterable + if not isinstance(self.grb_object.solid_geometry, Iterable): + self.grb_object.solid_geometry = [self.grb_object.solid_geometry] + + # I use the utility geometry (self.qrcode_utility_geometry) because it is already buffered + geo_list = self.grb_object.solid_geometry + if isinstance(self.grb_object.solid_geometry, MultiPolygon): + geo_list = list(self.grb_object.solid_geometry.geoms) + + # this is the bounding box of the QRCode geometry + a, b, c, d = self.qrcode_utility_geometry.bounds + buff_val = self.ui.border_size_entry.get_value() * (self.ui.bsize_entry.get_value() / 10) + + if self.ui.bb_radio.get_value() == 'r': + mask_geo = box(a, b, c, d).buffer(buff_val) + else: + mask_geo = box(a, b, c, d).buffer(buff_val, join_style=2) + + # update the solid geometry with the cutout (if it is the case) + new_solid_geometry = [] + offset_mask_geo = translate(mask_geo, xoff=pos[0], yoff=pos[1]) + for poly in geo_list: + if poly.contains(offset_mask_geo): + new_solid_geometry.append(poly.difference(offset_mask_geo)) + else: + if poly not in new_solid_geometry: + new_solid_geometry.append(poly) + + geo_list = deepcopy(list(new_solid_geometry)) + + # Polarity + if self.ui.pol_radio.get_value() == 'pos': + working_geo = self.qrcode_utility_geometry + else: + working_geo = mask_geo.difference(self.qrcode_utility_geometry) + + try: + for geo in working_geo: + geo_list.append(translate(geo, xoff=pos[0], yoff=pos[1])) + except TypeError: + geo_list.append(translate(working_geo, xoff=pos[0], yoff=pos[1])) + + self.grb_object.solid_geometry = deepcopy(geo_list) + + box_size = float(self.ui.bsize_entry.get_value()) / 10.0 + + sort_apid = [] + new_apid = '10' + if self.grb_object.apertures: + for k, v in list(self.grb_object.apertures.items()): + sort_apid.append(int(k)) + sorted_apertures = sorted(sort_apid) + max_apid = max(sorted_apertures) + if max_apid >= 10: + new_apid = str(max_apid + 1) + else: + new_apid = '10' + + # don't know if the condition is required since I already made sure above that the new_apid is a new one + if new_apid not in self.grb_object.apertures: + self.grb_object.apertures[new_apid] = {} + self.grb_object.apertures[new_apid]['geometry'] = [] + self.grb_object.apertures[new_apid]['type'] = 'R' + # TODO: HACK + # I've artificially added 1% to the height and width because otherwise after loading the + # exported file, it will not be correctly reconstructed (it will be made from multiple shapes instead of + # one shape which show that the buffering didn't worked well). It may be the MM to INCH conversion. + self.grb_object.apertures[new_apid]['height'] = deepcopy(box_size * 1.01) + self.grb_object.apertures[new_apid]['width'] = deepcopy(box_size * 1.01) + self.grb_object.apertures[new_apid]['size'] = deepcopy(math.sqrt(box_size ** 2 + box_size ** 2)) + + if '0' not in self.grb_object.apertures: + self.grb_object.apertures['0'] = {} + self.grb_object.apertures['0']['geometry'] = [] + self.grb_object.apertures['0']['type'] = 'REG' + self.grb_object.apertures['0']['size'] = 0.0 + + # in case that the QRCode geometry is dropped onto a copper region (found in the '0' aperture) + # make sure that I place a cutout there + zero_elem = {} + zero_elem['clear'] = offset_mask_geo + self.grb_object.apertures['0']['geometry'].append(deepcopy(zero_elem)) + + try: + a, b, c, d = self.grb_object.bounds() + self.grb_object.options['xmin'] = a + self.grb_object.options['ymin'] = b + self.grb_object.options['xmax'] = c + self.grb_object.options['ymax'] = d + except Exception as e: + log.debug("QRCode.make() bounds error --> %s" % str(e)) + + try: + for geo in self.qrcode_geometry: + geo_elem = {} + geo_elem['solid'] = translate(geo, xoff=pos[0], yoff=pos[1]) + geo_elem['follow'] = translate(geo.centroid, xoff=pos[0], yoff=pos[1]) + self.grb_object.apertures[new_apid]['geometry'].append(deepcopy(geo_elem)) + except TypeError: + geo_elem = {} + geo_elem['solid'] = self.qrcode_geometry + self.grb_object.apertures[new_apid]['geometry'].append(deepcopy(geo_elem)) + + # update the source file with the new geometry: + self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'], filename=None, + local_use=self.grb_object, use_thread=False) + + self.replot(obj=self.grb_object) + self.app.inform.emit('[success] %s' % _("QRCode Tool done.")) + + def draw_utility_geo(self, pos): + + # face = '#0000FF' + str(hex(int(0.2 * 255)))[2:] + outline = '#0000FFAF' + + offset_geo = [] + + # I use the len of self.qrcode_geometry instead of the utility one because the complexity of the polygons is + # better seen in this (bit what if the sel.qrcode_geometry is just one geo element? len will fail ... + if len(self.qrcode_geometry) <= self.app.defaults["tools_qrcode_sel_limit"]: + try: + for poly in self.qrcode_utility_geometry: + offset_geo.append(translate(poly.exterior, xoff=pos[0], yoff=pos[1])) + for geo_int in poly.interiors: + offset_geo.append(translate(geo_int, xoff=pos[0], yoff=pos[1])) + except TypeError: + offset_geo.append(translate(self.qrcode_utility_geometry.exterior, xoff=pos[0], yoff=pos[1])) + for geo_int in self.qrcode_utility_geometry.interiors: + offset_geo.append(translate(geo_int, xoff=pos[0], yoff=pos[1])) + else: + offset_geo = [translate(self.box_poly, xoff=pos[0], yoff=pos[1])] + + for shape in offset_geo: + self.shapes.add(shape, color=outline, update=True, layer=0, tolerance=None) + + if self.app.is_legacy is True: + self.shapes.redraw() + + def delete_utility_geo(self): + self.shapes.clear(update=True) + self.shapes.redraw() + + def on_mouse_move(self, event): + if self.app.is_legacy is False: + event_pos = event.pos + else: + event_pos = (event.xdata, event.ydata) + + try: + x = float(event_pos[0]) + y = float(event_pos[1]) + except TypeError: + return + + pos_canvas = self.app.plotcanvas.translate_coords((x, y)) + + # if GRID is active we need to get the snapped positions + if self.app.grid_status(): + pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1]) + else: + pos = pos_canvas + + dx = pos[0] - self.origin[0] + dy = pos[1] - self.origin[1] + + # delete the utility geometry + self.delete_utility_geo() + self.draw_utility_geo((dx, dy)) + + def on_mouse_release(self, event): + # mouse click will be accepted only if the left button is clicked + # this is necessary because right mouse click and middle mouse click + # are used for panning on the canvas + + if self.app.is_legacy is False: + event_pos = event.pos + else: + event_pos = (event.xdata, event.ydata) + + if event.button == 1: + pos_canvas = self.app.plotcanvas.translate_coords(event_pos) + self.delete_utility_geo() + + # if GRID is active we need to get the snapped positions + if self.app.grid_status(): + pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1]) + else: + pos = pos_canvas + + dx = pos[0] - self.origin[0] + dy = pos[1] - self.origin[1] + + self.make(pos=(dx, dy)) + + def on_key_release(self, event): + pass + + def convert_svg_to_geo(self, filename, object_type=None, flip=True, units='MM'): + """ + Convert shapes from an SVG file into a geometry list. + + :param filename: A String Stream file. + :param object_type: parameter passed further along. What kind the object will receive the SVG geometry + :param flip: Flip the vertically. + :type flip: bool + :param units: FlatCAM units + :return: None + """ + + # Parse into list of shapely objects + svg_tree = ET.parse(filename) + svg_root = svg_tree.getroot() + + # Change origin to bottom left + # h = float(svg_root.get('height')) + # w = float(svg_root.get('width')) + h = svgparselength(svg_root.get('height'))[0] # TODO: No units support yet + geos = getsvggeo(svg_root, object_type) + + if flip: + geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos] + + # flatten the svg geometry for the case when the QRCode SVG is added into a Gerber object + solid_geometry = list(self.flatten_list(geos)) + + geos_text = getsvgtext(svg_root, object_type, units=units) + if geos_text is not None: + geos_text_f = [] + if flip: + # Change origin to bottom left + for i in geos_text: + _, minimy, _, maximy = i.bounds + h2 = (maximy - minimy) * 0.5 + geos_text_f.append(translate(scale(i, 1.0, -1.0, origin=(0, 0)), yoff=(h + h2))) + if geos_text_f: + solid_geometry += geos_text_f + return solid_geometry + + def flatten_list(self, geo_list): + for item in geo_list: + if isinstance(item, Iterable) and not isinstance(item, (str, bytes)): + yield from self.flatten_list(item) + else: + yield item + + def replot(self, obj): + def worker_task(): + with self.app.proc_container.new('%s...' % _("Plotting")): + obj.plot() + + self.app.worker_task.emit({'fcn': worker_task, 'params': []}) + + def on_exit(self): + if self.app.is_legacy is False: + self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move) + self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release) + self.app.plotcanvas.graph_event_disconnect('key_release', self.on_key_release) + else: + self.app.plotcanvas.graph_event_disconnect(self.mm) + self.app.plotcanvas.graph_event_disconnect(self.mr) + self.app.plotcanvas.graph_event_disconnect(self.kr) + + # delete the utility geometry + self.delete_utility_geo() + self.app.call_source = 'app' + + def export_png_file(self): + text_data = self.ui.text_data.get_value() + if text_data == '': + self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box.")) + return 'fail' + + def job_thread_qr_png(app_obj, fname): + error_code = { + 'L': qrcode.constants.ERROR_CORRECT_L, + 'M': qrcode.constants.ERROR_CORRECT_M, + 'Q': qrcode.constants.ERROR_CORRECT_Q, + 'H': qrcode.constants.ERROR_CORRECT_H + }[self.ui.error_radio.get_value()] + + qr = qrcode.QRCode( + version=self.ui.version_entry.get_value(), + error_correction=error_code, + box_size=self.ui.bsize_entry.get_value(), + border=self.ui.border_size_entry.get_value(), + image_factory=qrcode.image.pil.PilImage + ) + qr.add_data(text_data) + qr.make(fit=True) + + img = qr.make_image(fill_color=self.ui.fill_color_entry.get_value(), + back_color=self.ui.back_color_entry.get_value()) + img.save(fname) + + app_obj.call_source = 'qrcode_tool' + + name = 'qr_code' + + _filter = "PNG File (*.png);;All Files (*.*)" + try: + filename, _f = FCFileSaveDialog.get_saved_filename( + caption=_("Export PNG"), + directory=self.app.get_last_save_folder() + '/' + str(name) + '_png', + ext_filter=_filter) + except TypeError: + filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export PNG"), ext_filter=_filter) + + filename = str(filename) + + if filename == "": + self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled.")) + return + else: + self.app.worker_task.emit({'fcn': job_thread_qr_png, 'params': [self.app, filename]}) + + def export_svg_file(self): + text_data = self.ui.text_data.get_value() + if text_data == '': + self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box.")) + return 'fail' + + def job_thread_qr_svg(app_obj, fname): + error_code = { + 'L': qrcode.constants.ERROR_CORRECT_L, + 'M': qrcode.constants.ERROR_CORRECT_M, + 'Q': qrcode.constants.ERROR_CORRECT_Q, + 'H': qrcode.constants.ERROR_CORRECT_H + }[self.ui.error_radio.get_value()] + + qr = qrcode.QRCode( + version=self.ui.version_entry.get_value(), + error_correction=error_code, + box_size=self.ui.bsize_entry.get_value(), + border=self.ui.border_size_entry.get_value(), + image_factory=qrcode.image.svg.SvgPathImage + ) + qr.add_data(text_data) + img = qr.make_image(fill_color=self.ui.fill_color_entry.get_value(), + back_color=self.ui.back_color_entry.get_value()) + img.save(fname) + + app_obj.call_source = 'qrcode_tool' + + name = 'qr_code' + + _filter = "SVG File (*.svg);;All Files (*.*)" + try: + filename, _f = FCFileSaveDialog.get_saved_filename( + caption=_("Export SVG"), + directory=self.app.get_last_save_folder() + '/' + str(name) + '_svg', + ext_filter=_filter) + except TypeError: + filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export SVG"), ext_filter=_filter) + + filename = str(filename) + + if filename == "": + self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled.")) + return + else: + self.app.worker_task.emit({'fcn': job_thread_qr_svg, 'params': [self.app, filename]}) + + def on_qrcode_fill_color_entry(self): + color = self.ui.fill_color_entry.get_value() + self.ui.fill_color_button.setStyleSheet("background-color:%s" % str(color)) + + def on_qrcode_fill_color_button(self): + current_color = QtGui.QColor(self.ui.fill_color_entry.get_value()) + + c_dialog = QtWidgets.QColorDialog() + fill_color = c_dialog.getColor(initial=current_color) + + if fill_color.isValid() is False: + return + + self.ui.fill_color_button.setStyleSheet("background-color:%s" % str(fill_color.name())) + + new_val_sel = str(fill_color.name()) + self.ui.fill_color_entry.set_value(new_val_sel) + + def on_qrcode_back_color_entry(self): + color = self.ui.back_color_entry.get_value() + self.ui.back_color_button.setStyleSheet("background-color:%s" % str(color)) + + def on_qrcode_back_color_button(self): + current_color = QtGui.QColor(self.ui.back_color_entry.get_value()) + + c_dialog = QtWidgets.QColorDialog() + back_color = c_dialog.getColor(initial=current_color) + + if back_color.isValid() is False: + return + + self.ui.back_color_button.setStyleSheet("background-color:%s" % str(back_color.name())) + + new_val_sel = str(back_color.name()) + self.ui.back_color_entry.set_value(new_val_sel) + + def on_transparent_back_color(self, state): + if state: + self.ui.back_color_entry.setDisabled(True) + self.ui.back_color_button.setDisabled(True) + self.old_back_color = self.ui.back_color_entry.get_value() + self.ui.back_color_entry.set_value('transparent') + else: + self.ui.back_color_entry.setDisabled(False) + self.ui.back_color_button.setDisabled(False) + self.ui.back_color_entry.set_value(self.old_back_color) + + +class QRcodeUI: + + toolName = _("QRCode Tool") + + def __init__(self, layout, app): + self.app = app + self.decimals = self.app.decimals + self.layout = layout + # ## Title title_label = QtWidgets.QLabel("%s" % self.toolName) title_label.setStyleSheet(""" - QLabel - { - font-size: 16px; - font-weight: bold; - } - """) + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) self.layout.addWidget(title_label) self.layout.addWidget(QtWidgets.QLabel('')) @@ -284,11 +862,11 @@ class QRCode(AppTool): _("Export a SVG file with the QRCode content.") ) self.export_svg_button.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) + QPushButton + { + font-weight: bold; + } + """) self.export_lay.addWidget(self.export_svg_button, 3, 0, 1, 2) # ## Export QRCode as PNG image @@ -297,11 +875,11 @@ class QRCode(AppTool): _("Export a PNG image file with the QRCode content.") ) self.export_png_button.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) + QPushButton + { + font-weight: bold; + } + """) self.export_lay.addWidget(self.export_png_button, 4, 0, 1, 2) # ## Insert QRCode @@ -310,11 +888,11 @@ class QRCode(AppTool): _("Create the QRCode object.") ) self.qrcode_button.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) + QPushButton + { + font-weight: bold; + } + """) self.layout.addWidget(self.qrcode_button) self.layout.addStretch() @@ -325,573 +903,29 @@ class QRCode(AppTool): _("Will reset the tool parameters.") ) self.reset_button.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) + QPushButton + { + font-weight: bold; + } + """) self.layout.addWidget(self.reset_button) - self.grb_object = None - self.box_poly = None - self.proc = None + # #################################### FINSIHED GUI ########################### + # ############################################################################# - self.origin = (0, 0) - - self.mm = None - self.mr = None - self.kr = None - - self.shapes = self.app.move_tool.sel_shapes - self.qrcode_geometry = MultiPolygon() - self.qrcode_utility_geometry = MultiPolygon() - - self.old_back_color = '' - - # Signals # - self.qrcode_button.clicked.connect(self.execute) - self.export_cb.stateChanged.connect(self.on_export_frame) - self.export_png_button.clicked.connect(self.export_png_file) - self.export_svg_button.clicked.connect(self.export_svg_file) - - self.fill_color_entry.editingFinished.connect(self.on_qrcode_fill_color_entry) - self.fill_color_button.clicked.connect(self.on_qrcode_fill_color_button) - self.back_color_entry.editingFinished.connect(self.on_qrcode_back_color_entry) - self.back_color_button.clicked.connect(self.on_qrcode_back_color_button) - - self.transparent_cb.stateChanged.connect(self.on_transparent_back_color) - self.reset_button.clicked.connect(self.set_tool_ui) - - def run(self, toggle=True): - self.app.defaults.report_usage("QRCode()") - - 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 + def confirmation_message(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"), + self.decimals, + minval, + self.decimals, + maxval), False) else: - if self.app.ui.splitter.sizes()[0] == 0: - self.app.ui.splitter.setSizes([1, 1]) + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) - AppTool.run(self) - - self.set_tool_ui() - - self.app.ui.notebook.setTabText(2, _("QRCode Tool")) - - def install(self, icon=None, separator=None, **kwargs): - AppTool.install(self, icon, separator, shortcut='Alt+Q', **kwargs) - - def set_tool_ui(self): - self.units = self.app.defaults['units'] - self.border_size_entry.set_value(4) - - self.version_entry.set_value(int(self.app.defaults["tools_qrcode_version"])) - self.error_radio.set_value(self.app.defaults["tools_qrcode_error"]) - self.bsize_entry.set_value(int(self.app.defaults["tools_qrcode_box_size"])) - self.border_size_entry.set_value(int(self.app.defaults["tools_qrcode_border_size"])) - self.pol_radio.set_value(self.app.defaults["tools_qrcode_polarity"]) - self.bb_radio.set_value(self.app.defaults["tools_qrcode_rounded"]) - - self.text_data.set_value(self.app.defaults["tools_qrcode_qrdata"]) - - self.fill_color_entry.set_value(self.app.defaults['tools_qrcode_fill_color']) - self.fill_color_button.setStyleSheet("background-color:%s" % - str(self.app.defaults['tools_qrcode_fill_color'])[:7]) - - self.back_color_entry.set_value(self.app.defaults['tools_qrcode_back_color']) - self.back_color_button.setStyleSheet("background-color:%s" % - str(self.app.defaults['tools_qrcode_back_color'])[:7]) - - def on_export_frame(self, state): - self.export_frame.setVisible(state) - self.qrcode_button.setVisible(not state) - - def execute(self): - text_data = self.text_data.get_value() - if text_data == '': - self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box.")) - return 'fail' - - # get the Gerber object on which the QRCode will be inserted - selection_index = self.grb_object_combo.currentIndex() - model_index = self.app.collection.index(selection_index, 0, self.grb_object_combo.rootModelIndex()) - - try: - self.grb_object = model_index.internalPointer().obj - except Exception as e: - log.debug("QRCode.execute() --> %s" % str(e)) - self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ...")) - return 'fail' - - # we can safely activate the mouse events - self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move) - self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release) - self.kr = self.app.plotcanvas.graph_event_connect('key_release', self.on_key_release) - - self.proc = self.app.proc_container.new('%s...' % _("Generating QRCode geometry")) - - def job_thread_qr(app_obj): - error_code = { - 'L': qrcode.constants.ERROR_CORRECT_L, - 'M': qrcode.constants.ERROR_CORRECT_M, - 'Q': qrcode.constants.ERROR_CORRECT_Q, - 'H': qrcode.constants.ERROR_CORRECT_H - }[self.error_radio.get_value()] - - qr = qrcode.QRCode( - version=self.version_entry.get_value(), - error_correction=error_code, - box_size=self.bsize_entry.get_value(), - border=self.border_size_entry.get_value(), - image_factory=qrcode.image.svg.SvgFragmentImage - ) - qr.add_data(text_data) - qr.make() - - svg_file = BytesIO() - img = qr.make_image() - img.save(svg_file) - - svg_text = StringIO(svg_file.getvalue().decode('UTF-8')) - svg_geometry = self.convert_svg_to_geo(svg_text, units=self.units) - self.qrcode_geometry = deepcopy(svg_geometry) - - svg_geometry = unary_union(svg_geometry).buffer(0.0000001).buffer(-0.0000001) - self.qrcode_utility_geometry = svg_geometry - - # make a bounding box of the QRCode geometry to help drawing the utility geometry in case it is too - # complicated - try: - a, b, c, d = self.qrcode_utility_geometry.bounds - self.box_poly = box(minx=a, miny=b, maxx=c, maxy=d) - except Exception as ee: - log.debug("QRCode.make() bounds error --> %s" % str(ee)) - - app_obj.call_source = 'qrcode_tool' - app_obj.inform.emit(_("Click on the Destination point ...")) - - self.app.worker_task.emit({'fcn': job_thread_qr, 'params': [self.app]}) - - def make(self, pos): - self.on_exit() - - # make sure that the source object solid geometry is an Iterable - if not isinstance(self.grb_object.solid_geometry, Iterable): - self.grb_object.solid_geometry = [self.grb_object.solid_geometry] - - # I use the utility geometry (self.qrcode_utility_geometry) because it is already buffered - geo_list = self.grb_object.solid_geometry - if isinstance(self.grb_object.solid_geometry, MultiPolygon): - geo_list = list(self.grb_object.solid_geometry.geoms) - - # this is the bounding box of the QRCode geometry - a, b, c, d = self.qrcode_utility_geometry.bounds - buff_val = self.border_size_entry.get_value() * (self.bsize_entry.get_value() / 10) - - if self.bb_radio.get_value() == 'r': - mask_geo = box(a, b, c, d).buffer(buff_val) + def confirmation_message_int(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' % + (_("Edited value is out of range"), minval, maxval), False) else: - mask_geo = box(a, b, c, d).buffer(buff_val, join_style=2) - - # update the solid geometry with the cutout (if it is the case) - new_solid_geometry = [] - offset_mask_geo = translate(mask_geo, xoff=pos[0], yoff=pos[1]) - for poly in geo_list: - if poly.contains(offset_mask_geo): - new_solid_geometry.append(poly.difference(offset_mask_geo)) - else: - if poly not in new_solid_geometry: - new_solid_geometry.append(poly) - - geo_list = deepcopy(list(new_solid_geometry)) - - # Polarity - if self.pol_radio.get_value() == 'pos': - working_geo = self.qrcode_utility_geometry - else: - working_geo = mask_geo.difference(self.qrcode_utility_geometry) - - try: - for geo in working_geo: - geo_list.append(translate(geo, xoff=pos[0], yoff=pos[1])) - except TypeError: - geo_list.append(translate(working_geo, xoff=pos[0], yoff=pos[1])) - - self.grb_object.solid_geometry = deepcopy(geo_list) - - box_size = float(self.bsize_entry.get_value()) / 10.0 - - sort_apid = [] - new_apid = '10' - if self.grb_object.apertures: - for k, v in list(self.grb_object.apertures.items()): - sort_apid.append(int(k)) - sorted_apertures = sorted(sort_apid) - max_apid = max(sorted_apertures) - if max_apid >= 10: - new_apid = str(max_apid + 1) - else: - new_apid = '10' - - # don't know if the condition is required since I already made sure above that the new_apid is a new one - if new_apid not in self.grb_object.apertures: - self.grb_object.apertures[new_apid] = {} - self.grb_object.apertures[new_apid]['geometry'] = [] - self.grb_object.apertures[new_apid]['type'] = 'R' - # TODO: HACK - # I've artificially added 1% to the height and width because otherwise after loading the - # exported file, it will not be correctly reconstructed (it will be made from multiple shapes instead of - # one shape which show that the buffering didn't worked well). It may be the MM to INCH conversion. - self.grb_object.apertures[new_apid]['height'] = deepcopy(box_size * 1.01) - self.grb_object.apertures[new_apid]['width'] = deepcopy(box_size * 1.01) - self.grb_object.apertures[new_apid]['size'] = deepcopy(math.sqrt(box_size ** 2 + box_size ** 2)) - - if '0' not in self.grb_object.apertures: - self.grb_object.apertures['0'] = {} - self.grb_object.apertures['0']['geometry'] = [] - self.grb_object.apertures['0']['type'] = 'REG' - self.grb_object.apertures['0']['size'] = 0.0 - - # in case that the QRCode geometry is dropped onto a copper region (found in the '0' aperture) - # make sure that I place a cutout there - zero_elem = {} - zero_elem['clear'] = offset_mask_geo - self.grb_object.apertures['0']['geometry'].append(deepcopy(zero_elem)) - - try: - a, b, c, d = self.grb_object.bounds() - self.grb_object.options['xmin'] = a - self.grb_object.options['ymin'] = b - self.grb_object.options['xmax'] = c - self.grb_object.options['ymax'] = d - except Exception as e: - log.debug("QRCode.make() bounds error --> %s" % str(e)) - - try: - for geo in self.qrcode_geometry: - geo_elem = {} - geo_elem['solid'] = translate(geo, xoff=pos[0], yoff=pos[1]) - geo_elem['follow'] = translate(geo.centroid, xoff=pos[0], yoff=pos[1]) - self.grb_object.apertures[new_apid]['geometry'].append(deepcopy(geo_elem)) - except TypeError: - geo_elem = {} - geo_elem['solid'] = self.qrcode_geometry - self.grb_object.apertures[new_apid]['geometry'].append(deepcopy(geo_elem)) - - # update the source file with the new geometry: - self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'], filename=None, - local_use=self.grb_object, use_thread=False) - - self.replot(obj=self.grb_object) - self.app.inform.emit('[success] %s' % _("QRCode Tool done.")) - - def draw_utility_geo(self, pos): - - # face = '#0000FF' + str(hex(int(0.2 * 255)))[2:] - outline = '#0000FFAF' - - offset_geo = [] - - # I use the len of self.qrcode_geometry instead of the utility one because the complexity of the polygons is - # better seen in this (bit what if the sel.qrcode_geometry is just one geo element? len will fail ... - if len(self.qrcode_geometry) <= self.app.defaults["tools_qrcode_sel_limit"]: - try: - for poly in self.qrcode_utility_geometry: - offset_geo.append(translate(poly.exterior, xoff=pos[0], yoff=pos[1])) - for geo_int in poly.interiors: - offset_geo.append(translate(geo_int, xoff=pos[0], yoff=pos[1])) - except TypeError: - offset_geo.append(translate(self.qrcode_utility_geometry.exterior, xoff=pos[0], yoff=pos[1])) - for geo_int in self.qrcode_utility_geometry.interiors: - offset_geo.append(translate(geo_int, xoff=pos[0], yoff=pos[1])) - else: - offset_geo = [translate(self.box_poly, xoff=pos[0], yoff=pos[1])] - - for shape in offset_geo: - self.shapes.add(shape, color=outline, update=True, layer=0, tolerance=None) - - if self.app.is_legacy is True: - self.shapes.redraw() - - def delete_utility_geo(self): - self.shapes.clear(update=True) - self.shapes.redraw() - - def on_mouse_move(self, event): - if self.app.is_legacy is False: - event_pos = event.pos - else: - event_pos = (event.xdata, event.ydata) - - try: - x = float(event_pos[0]) - y = float(event_pos[1]) - except TypeError: - return - - pos_canvas = self.app.plotcanvas.translate_coords((x, y)) - - # if GRID is active we need to get the snapped positions - if self.app.grid_status() == True: - pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1]) - else: - pos = pos_canvas - - dx = pos[0] - self.origin[0] - dy = pos[1] - self.origin[1] - - # delete the utility geometry - self.delete_utility_geo() - self.draw_utility_geo((dx, dy)) - - def on_mouse_release(self, event): - # mouse click will be accepted only if the left button is clicked - # this is necessary because right mouse click and middle mouse click - # are used for panning on the canvas - - if self.app.is_legacy is False: - event_pos = event.pos - else: - event_pos = (event.xdata, event.ydata) - - if event.button == 1: - pos_canvas = self.app.plotcanvas.translate_coords(event_pos) - self.delete_utility_geo() - - # if GRID is active we need to get the snapped positions - if self.app.grid_status() == True: - pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1]) - else: - pos = pos_canvas - - dx = pos[0] - self.origin[0] - dy = pos[1] - self.origin[1] - - self.make(pos=(dx, dy)) - - def on_key_release(self, event): - pass - - def convert_svg_to_geo(self, filename, object_type=None, flip=True, units='MM'): - """ - Convert shapes from an SVG file into a geometry list. - - :param filename: A String Stream file. - :param object_type: parameter passed further along. What kind the object will receive the SVG geometry - :param flip: Flip the vertically. - :type flip: bool - :param units: FlatCAM units - :return: None - """ - - # Parse into list of shapely objects - svg_tree = ET.parse(filename) - svg_root = svg_tree.getroot() - - # Change origin to bottom left - # h = float(svg_root.get('height')) - # w = float(svg_root.get('width')) - h = svgparselength(svg_root.get('height'))[0] # TODO: No units support yet - geos = getsvggeo(svg_root, object_type) - - if flip: - geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos] - - # flatten the svg geometry for the case when the QRCode SVG is added into a Gerber object - solid_geometry = list(self.flatten_list(geos)) - - geos_text = getsvgtext(svg_root, object_type, units=units) - if geos_text is not None: - geos_text_f = [] - if flip: - # Change origin to bottom left - for i in geos_text: - _, minimy, _, maximy = i.bounds - h2 = (maximy - minimy) * 0.5 - geos_text_f.append(translate(scale(i, 1.0, -1.0, origin=(0, 0)), yoff=(h + h2))) - if geos_text_f: - solid_geometry += geos_text_f - return solid_geometry - - def flatten_list(self, geo_list): - for item in geo_list: - if isinstance(item, Iterable) and not isinstance(item, (str, bytes)): - yield from self.flatten_list(item) - else: - yield item - - def replot(self, obj): - def worker_task(): - with self.app.proc_container.new('%s...' % _("Plotting")): - obj.plot() - - self.app.worker_task.emit({'fcn': worker_task, 'params': []}) - - def on_exit(self): - if self.app.is_legacy is False: - self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move) - self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release) - self.app.plotcanvas.graph_event_disconnect('key_release', self.on_key_release) - else: - self.app.plotcanvas.graph_event_disconnect(self.mm) - self.app.plotcanvas.graph_event_disconnect(self.mr) - self.app.plotcanvas.graph_event_disconnect(self.kr) - - # delete the utility geometry - self.delete_utility_geo() - self.app.call_source = 'app' - - def export_png_file(self): - text_data = self.text_data.get_value() - if text_data == '': - self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box.")) - return 'fail' - - def job_thread_qr_png(app_obj, fname): - error_code = { - 'L': qrcode.constants.ERROR_CORRECT_L, - 'M': qrcode.constants.ERROR_CORRECT_M, - 'Q': qrcode.constants.ERROR_CORRECT_Q, - 'H': qrcode.constants.ERROR_CORRECT_H - }[self.error_radio.get_value()] - - qr = qrcode.QRCode( - version=self.version_entry.get_value(), - error_correction=error_code, - box_size=self.bsize_entry.get_value(), - border=self.border_size_entry.get_value(), - image_factory=qrcode.image.pil.PilImage - ) - qr.add_data(text_data) - qr.make(fit=True) - - img = qr.make_image(fill_color=self.fill_color_entry.get_value(), - back_color=self.back_color_entry.get_value()) - img.save(fname) - - app_obj.call_source = 'qrcode_tool' - - name = 'qr_code' - - _filter = "PNG File (*.png);;All Files (*.*)" - try: - filename, _f = FCFileSaveDialog.get_saved_filename( - caption=_("Export PNG"), - directory=self.app.get_last_save_folder() + '/' + str(name) + '_png', - ext_filter=_filter) - except TypeError: - filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export PNG"), ext_filter=_filter) - - filename = str(filename) - - if filename == "": - self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled.")) - return - else: - self.app.worker_task.emit({'fcn': job_thread_qr_png, 'params': [self.app, filename]}) - - def export_svg_file(self): - text_data = self.text_data.get_value() - if text_data == '': - self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box.")) - return 'fail' - - def job_thread_qr_svg(app_obj, fname): - error_code = { - 'L': qrcode.constants.ERROR_CORRECT_L, - 'M': qrcode.constants.ERROR_CORRECT_M, - 'Q': qrcode.constants.ERROR_CORRECT_Q, - 'H': qrcode.constants.ERROR_CORRECT_H - }[self.error_radio.get_value()] - - qr = qrcode.QRCode( - version=self.version_entry.get_value(), - error_correction=error_code, - box_size=self.bsize_entry.get_value(), - border=self.border_size_entry.get_value(), - image_factory=qrcode.image.svg.SvgPathImage - ) - qr.add_data(text_data) - img = qr.make_image(fill_color=self.fill_color_entry.get_value(), - back_color=self.back_color_entry.get_value()) - img.save(fname) - - app_obj.call_source = 'qrcode_tool' - - name = 'qr_code' - - _filter = "SVG File (*.svg);;All Files (*.*)" - try: - filename, _f = FCFileSaveDialog.get_saved_filename( - caption=_("Export SVG"), - directory=self.app.get_last_save_folder() + '/' + str(name) + '_svg', - ext_filter=_filter) - except TypeError: - filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export SVG"), ext_filter=_filter) - - filename = str(filename) - - if filename == "": - self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled.")) - return - else: - self.app.worker_task.emit({'fcn': job_thread_qr_svg, 'params': [self.app, filename]}) - - def on_qrcode_fill_color_entry(self): - color = self.fill_color_entry.get_value() - self.fill_color_button.setStyleSheet("background-color:%s" % str(color)) - - def on_qrcode_fill_color_button(self): - current_color = QtGui.QColor(self.fill_color_entry.get_value()) - - c_dialog = QtWidgets.QColorDialog() - fill_color = c_dialog.getColor(initial=current_color) - - if fill_color.isValid() is False: - return - - self.fill_color_button.setStyleSheet("background-color:%s" % str(fill_color.name())) - - new_val_sel = str(fill_color.name()) - self.fill_color_entry.set_value(new_val_sel) - - def on_qrcode_back_color_entry(self): - color = self.back_color_entry.get_value() - self.back_color_button.setStyleSheet("background-color:%s" % str(color)) - - def on_qrcode_back_color_button(self): - current_color = QtGui.QColor(self.back_color_entry.get_value()) - - c_dialog = QtWidgets.QColorDialog() - back_color = c_dialog.getColor(initial=current_color) - - if back_color.isValid() is False: - return - - self.back_color_button.setStyleSheet("background-color:%s" % str(back_color.name())) - - new_val_sel = str(back_color.name()) - self.back_color_entry.set_value(new_val_sel) - - def on_transparent_back_color(self, state): - if state: - self.back_color_entry.setDisabled(True) - self.back_color_button.setDisabled(True) - self.old_back_color = self.back_color_entry.get_value() - self.back_color_entry.set_value('transparent') - else: - self.back_color_entry.setDisabled(False) - self.back_color_button.setDisabled(False) - self.back_color_entry.set_value(self.old_back_color) + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) diff --git a/appTools/ToolSolderPaste.py b/appTools/ToolSolderPaste.py index 60039d51..83d289ba 100644 --- a/appTools/ToolSolderPaste.py +++ b/appTools/ToolSolderPaste.py @@ -34,464 +34,19 @@ if '_' not in builtins.__dict__: class SolderPaste(AppTool): - toolName = _("Solder Paste Tool") - + def __init__(self, app): AppTool.__init__(self, app) - + self.app = app + # Number of decimals to be used for tools/nozzles in this FlatCAM Tool 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) - - # ## Form Layout - obj_form_layout = QtWidgets.QFormLayout() - self.layout.addLayout(obj_form_layout) - - # ## Gerber Object to be used for solderpaste dispensing - self.obj_combo = FCComboBox(callback=self.on_rmb_combo) - self.obj_combo.setModel(self.app.collection) - self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) - self.obj_combo.is_last = True - self.obj_combo.obj_type = "Gerber" - - self.object_label = QtWidgets.QLabel('%s:'% _("GERBER")) - self.object_label.setToolTip(_("Gerber Solderpaste object.") - ) - obj_form_layout.addRow(self.object_label) - obj_form_layout.addRow(self.obj_combo) - - separator_line = QtWidgets.QFrame() - separator_line.setFrameShape(QtWidgets.QFrame.HLine) - separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) - obj_form_layout.addRow(separator_line) - - # ### Tools ## ## - self.tools_table_label = QtWidgets.QLabel('%s' % _('Tools Table')) - self.tools_table_label.setToolTip( - _("Tools pool from which the algorithm\n" - "will pick the ones used for dispensing solder paste.") - ) - self.layout.addWidget(self.tools_table_label) - - self.tools_table = FCTable() - self.layout.addWidget(self.tools_table) - - self.tools_table.setColumnCount(3) - self.tools_table.setHorizontalHeaderLabels(['#', _('Diameter'), '']) - self.tools_table.setColumnHidden(2, True) - self.tools_table.setSortingEnabled(False) - # self.tools_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) - - self.tools_table.horizontalHeaderItem(0).setToolTip( - _("This is the Tool Number.\n" - "The solder dispensing will start with the tool with the biggest \n" - "diameter, continuing until there are no more Nozzle tools.\n" - "If there are no longer tools but there are still pads not covered\n " - "with solder paste, the app will issue a warning message box.") - ) - self.tools_table.horizontalHeaderItem(1).setToolTip( - _("Nozzle tool Diameter. It's value (in current FlatCAM units)\n" - "is the width of the solder paste dispensed.")) - - # ### Add a new Tool ## ## - hlay_tools = QtWidgets.QHBoxLayout() - self.layout.addLayout(hlay_tools) - - self.addtool_entry_lbl = QtWidgets.QLabel('%s:' % _('New Nozzle Tool')) - self.addtool_entry_lbl.setToolTip( - _("Diameter for the new Nozzle tool to add in the Tool Table") - ) - self.addtool_entry = FCDoubleSpinner(callback=self.confirmation_message) - self.addtool_entry.set_range(0.0000001, 9999.9999) - self.addtool_entry.set_precision(self.decimals) - self.addtool_entry.setSingleStep(0.1) - - # hlay.addWidget(self.addtool_label) - # hlay.addStretch() - hlay_tools.addWidget(self.addtool_entry_lbl) - hlay_tools.addWidget(self.addtool_entry) - - grid0 = QtWidgets.QGridLayout() - self.layout.addLayout(grid0) - - self.addtool_btn = QtWidgets.QPushButton(_('Add')) - self.addtool_btn.setToolTip( - _("Add a new nozzle tool to the Tool Table\n" - "with the diameter specified above.") - ) - - self.deltool_btn = QtWidgets.QPushButton(_('Delete')) - self.deltool_btn.setToolTip( - _("Delete a selection of tools in the Tool Table\n" - "by first selecting a row(s) in the Tool Table.") - ) - - grid0.addWidget(self.addtool_btn, 0, 0) - grid0.addWidget(self.deltool_btn, 0, 2) - - separator_line = QtWidgets.QFrame() - separator_line.setFrameShape(QtWidgets.QFrame.HLine) - separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) - grid0.addWidget(separator_line, 1, 0, 1, 3) - - # ## Buttons - grid0_1 = QtWidgets.QGridLayout() - self.layout.addLayout(grid0_1) - - step1_lbl = QtWidgets.QLabel("%s:" % _('STEP 1')) - step1_lbl.setToolTip( - _("First step is to select a number of nozzle tools for usage\n" - "and then optionally modify the GCode parameters below.") - ) - step1_description_lbl = QtWidgets.QLabel(_("Select tools.\n" - "Modify parameters.")) - - grid0_1.addWidget(step1_lbl, 0, 0, alignment=Qt.AlignTop) - grid0_1.addWidget(step1_description_lbl, 0, 2, alignment=Qt.AlignBottom) - - self.gcode_frame = QtWidgets.QFrame() - self.gcode_frame.setContentsMargins(0, 0, 0, 0) - self.layout.addWidget(self.gcode_frame) - self.gcode_box = QtWidgets.QVBoxLayout() - self.gcode_box.setContentsMargins(0, 0, 0, 0) - self.gcode_frame.setLayout(self.gcode_box) - - # ## Form Layout - self.gcode_form_layout = QtWidgets.QFormLayout() - self.gcode_box.addLayout(self.gcode_form_layout) - - # Z dispense start - self.z_start_entry = FCDoubleSpinner(callback=self.confirmation_message) - self.z_start_entry.set_range(0.0000001, 9999.9999) - self.z_start_entry.set_precision(self.decimals) - self.z_start_entry.setSingleStep(0.1) - - self.z_start_label = QtWidgets.QLabel('%s:' % _("Z Dispense Start")) - self.z_start_label.setToolTip( - _("The height (Z) when solder paste dispensing starts.") - ) - self.gcode_form_layout.addRow(self.z_start_label, self.z_start_entry) - - # Z dispense - self.z_dispense_entry = FCDoubleSpinner(callback=self.confirmation_message) - self.z_dispense_entry.set_range(0.0000001, 9999.9999) - self.z_dispense_entry.set_precision(self.decimals) - self.z_dispense_entry.setSingleStep(0.1) - - self.z_dispense_label = QtWidgets.QLabel('%s:' % _("Z Dispense")) - self.z_dispense_label.setToolTip( - _("The height (Z) when doing solder paste dispensing.") - ) - self.gcode_form_layout.addRow(self.z_dispense_label, self.z_dispense_entry) - - # Z dispense stop - self.z_stop_entry = FCDoubleSpinner(callback=self.confirmation_message) - self.z_stop_entry.set_range(0.0000001, 9999.9999) - self.z_stop_entry.set_precision(self.decimals) - self.z_stop_entry.setSingleStep(0.1) - - self.z_stop_label = QtWidgets.QLabel('%s:' % _("Z Dispense Stop")) - self.z_stop_label.setToolTip( - _("The height (Z) when solder paste dispensing stops.") - ) - self.gcode_form_layout.addRow(self.z_stop_label, self.z_stop_entry) - - # Z travel - self.z_travel_entry = FCDoubleSpinner(callback=self.confirmation_message) - self.z_travel_entry.set_range(0.0000001, 9999.9999) - self.z_travel_entry.set_precision(self.decimals) - self.z_travel_entry.setSingleStep(0.1) - - self.z_travel_label = QtWidgets.QLabel('%s:' % _("Z Travel")) - self.z_travel_label.setToolTip( - _("The height (Z) for travel between pads\n" - "(without dispensing solder paste).") - ) - self.gcode_form_layout.addRow(self.z_travel_label, self.z_travel_entry) - - # Z toolchange location - self.z_toolchange_entry = FCDoubleSpinner(callback=self.confirmation_message) - self.z_toolchange_entry.set_range(0.0000001, 9999.9999) - self.z_toolchange_entry.set_precision(self.decimals) - self.z_toolchange_entry.setSingleStep(0.1) - - self.z_toolchange_label = QtWidgets.QLabel('%s:' % _("Z Toolchange")) - self.z_toolchange_label.setToolTip( - _("The height (Z) for tool (nozzle) change.") - ) - self.gcode_form_layout.addRow(self.z_toolchange_label, self.z_toolchange_entry) - - # X,Y Toolchange location - self.xy_toolchange_entry = FCEntry() - self.xy_toolchange_label = QtWidgets.QLabel('%s:' % _("Toolchange X-Y")) - self.xy_toolchange_label.setToolTip( - _("The X,Y location for tool (nozzle) change.\n" - "The format is (x, y) where x and y are real numbers.") - ) - self.gcode_form_layout.addRow(self.xy_toolchange_label, self.xy_toolchange_entry) - - # Feedrate X-Y - self.frxy_entry = FCDoubleSpinner(callback=self.confirmation_message) - self.frxy_entry.set_range(0.0000, 99999.9999) - self.frxy_entry.set_precision(self.decimals) - self.frxy_entry.setSingleStep(0.1) - - self.frxy_label = QtWidgets.QLabel('%s:' % _("Feedrate X-Y")) - self.frxy_label.setToolTip( - _("Feedrate (speed) while moving on the X-Y plane.") - ) - self.gcode_form_layout.addRow(self.frxy_label, self.frxy_entry) - - # Feedrate Z - self.frz_entry = FCDoubleSpinner(callback=self.confirmation_message) - self.frz_entry.set_range(0.0000, 99999.9999) - self.frz_entry.set_precision(self.decimals) - self.frz_entry.setSingleStep(0.1) - - self.frz_label = QtWidgets.QLabel('%s:' % _("Feedrate Z")) - self.frz_label.setToolTip( - _("Feedrate (speed) while moving vertically\n" - "(on Z plane).") - ) - self.gcode_form_layout.addRow(self.frz_label, self.frz_entry) - - # Feedrate Z Dispense - self.frz_dispense_entry = FCDoubleSpinner(callback=self.confirmation_message) - self.frz_dispense_entry.set_range(0.0000, 99999.9999) - self.frz_dispense_entry.set_precision(self.decimals) - self.frz_dispense_entry.setSingleStep(0.1) - - self.frz_dispense_label = QtWidgets.QLabel('%s:' % _("Feedrate Z Dispense")) - self.frz_dispense_label.setToolTip( - _("Feedrate (speed) while moving up vertically\n" - " to Dispense position (on Z plane).") - ) - self.gcode_form_layout.addRow(self.frz_dispense_label, self.frz_dispense_entry) - - # Spindle Speed Forward - self.speedfwd_entry = FCSpinner(callback=self.confirmation_message_int) - self.speedfwd_entry.set_range(0, 999999) - self.speedfwd_entry.set_step(1000) - - self.speedfwd_label = QtWidgets.QLabel('%s:' % _("Spindle Speed FWD")) - self.speedfwd_label.setToolTip( - _("The dispenser speed while pushing solder paste\n" - "through the dispenser nozzle.") - ) - self.gcode_form_layout.addRow(self.speedfwd_label, self.speedfwd_entry) - - # Dwell Forward - self.dwellfwd_entry = FCDoubleSpinner(callback=self.confirmation_message) - self.dwellfwd_entry.set_range(0.0000001, 9999.9999) - self.dwellfwd_entry.set_precision(self.decimals) - self.dwellfwd_entry.setSingleStep(0.1) - - self.dwellfwd_label = QtWidgets.QLabel('%s:' % _("Dwell FWD")) - self.dwellfwd_label.setToolTip( - _("Pause after solder dispensing.") - ) - self.gcode_form_layout.addRow(self.dwellfwd_label, self.dwellfwd_entry) - - # Spindle Speed Reverse - self.speedrev_entry = FCSpinner(callback=self.confirmation_message_int) - self.speedrev_entry.set_range(0, 999999) - self.speedrev_entry.set_step(1000) - - self.speedrev_label = QtWidgets.QLabel('%s:' % _("Spindle Speed REV")) - self.speedrev_label.setToolTip( - _("The dispenser speed while retracting solder paste\n" - "through the dispenser nozzle.") - ) - self.gcode_form_layout.addRow(self.speedrev_label, self.speedrev_entry) - - # Dwell Reverse - self.dwellrev_entry = FCDoubleSpinner(callback=self.confirmation_message) - self.dwellrev_entry.set_range(0.0000001, 9999.9999) - self.dwellrev_entry.set_precision(self.decimals) - self.dwellrev_entry.setSingleStep(0.1) - - self.dwellrev_label = QtWidgets.QLabel('%s:' % _("Dwell REV")) - self.dwellrev_label.setToolTip( - _("Pause after solder paste dispenser retracted,\n" - "to allow pressure equilibrium.") - ) - self.gcode_form_layout.addRow(self.dwellrev_label, self.dwellrev_entry) - - # Preprocessors - pp_label = QtWidgets.QLabel('%s:' % _('Preprocessor')) - pp_label.setToolTip( - _("Files that control the GCode generation.") - ) - - self.pp_combo = FCComboBox() - # self.pp_combo.setStyleSheet('background-color: rgb(255,255,255)') - self.gcode_form_layout.addRow(pp_label, self.pp_combo) - - # ## Buttons - # grid1 = QtWidgets.QGridLayout() - # self.gcode_box.addLayout(grid1) - - self.solder_gcode_btn = QtWidgets.QPushButton(_("Generate GCode")) - self.solder_gcode_btn.setToolTip( - _("Generate GCode for Solder Paste dispensing\n" - "on PCB pads.") - ) - self.solder_gcode_btn.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) - - self.generation_frame = QtWidgets.QFrame() - self.generation_frame.setContentsMargins(0, 0, 0, 0) - self.layout.addWidget(self.generation_frame) - self.generation_box = QtWidgets.QVBoxLayout() - self.generation_box.setContentsMargins(0, 0, 0, 0) - self.generation_frame.setLayout(self.generation_box) - - # ## Buttons - grid2 = QtWidgets.QGridLayout() - self.generation_box.addLayout(grid2) - - step2_lbl = QtWidgets.QLabel("%s:" % _('STEP 2')) - step2_lbl.setToolTip( - _("Second step is to create a solder paste dispensing\n" - "geometry out of an Solder Paste Mask Gerber file.") - ) - - self.soldergeo_btn = QtWidgets.QPushButton(_("Generate Geo")) - self.soldergeo_btn.setToolTip( - _("Generate solder paste dispensing geometry.") - ) - self.soldergeo_btn.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) - - grid2.addWidget(step2_lbl, 0, 0) - grid2.addWidget(self.soldergeo_btn, 0, 2) - - # ## Form Layout - geo_form_layout = QtWidgets.QFormLayout() - self.generation_box.addLayout(geo_form_layout) - - # ## Geometry Object to be used for solderpaste dispensing - self.geo_obj_combo = FCComboBox(callback=self.on_rmb_combo) - self.geo_obj_combo.setModel(self.app.collection) - self.geo_obj_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex())) - self.geo_obj_combo.is_last = True - self.geo_obj_combo.obj_type = "Geometry" - - self.geo_object_label = QtWidgets.QLabel('%s:' % _("Geo Result")) - self.geo_object_label.setToolTip( - _("Geometry Solder Paste object.\n" - "The name of the object has to end in:\n" - "'_solderpaste' as a protection.") - ) - geo_form_layout.addRow(self.geo_object_label, self.geo_obj_combo) - - grid3 = QtWidgets.QGridLayout() - self.generation_box.addLayout(grid3) - - step3_lbl = QtWidgets.QLabel("%s:" % _('STEP 3')) - step3_lbl.setToolTip( - _("Third step is to select a solder paste dispensing geometry,\n" - "and then generate a CNCJob object.\n\n" - "REMEMBER: if you want to create a CNCJob with new parameters,\n" - "first you need to generate a geometry with those new params,\n" - "and only after that you can generate an updated CNCJob.") - ) - - grid3.addWidget(step3_lbl, 0, 0) - grid3.addWidget(self.solder_gcode_btn, 0, 2) - - # ## Form Layout - cnc_form_layout = QtWidgets.QFormLayout() - self.generation_box.addLayout(cnc_form_layout) - - # ## Gerber Object to be used for solderpaste dispensing - self.cnc_obj_combo = FCComboBox(callback=self.on_rmb_combo) - self.cnc_obj_combo.setModel(self.app.collection) - self.cnc_obj_combo.setRootModelIndex(self.app.collection.index(3, 0, QtCore.QModelIndex())) - self.cnc_obj_combo.is_last = True - self.geo_obj_combo.obj_type = "CNCJob" - - self.cnc_object_label = QtWidgets.QLabel('%s:' % _("CNC Result")) - self.cnc_object_label.setToolTip( - _("CNCJob Solder paste object.\n" - "In order to enable the GCode save section,\n" - "the name of the object has to end in:\n" - "'_solderpaste' as a protection.") - ) - cnc_form_layout.addRow(self.cnc_object_label, self.cnc_obj_combo) - - grid4 = QtWidgets.QGridLayout() - self.generation_box.addLayout(grid4) - - self.solder_gcode_view_btn = QtWidgets.QPushButton(_("View GCode")) - self.solder_gcode_view_btn.setToolTip( - _("View the generated GCode for Solder Paste dispensing\n" - "on PCB pads.") - ) - self.solder_gcode_view_btn.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) - - self.solder_gcode_save_btn = QtWidgets.QPushButton(_("Save GCode")) - self.solder_gcode_save_btn.setToolTip( - _("Save the generated GCode for Solder Paste dispensing\n" - "on PCB pads, to a file.") - ) - self.solder_gcode_save_btn.setStyleSheet(""" - QPushButton - { - font-weight: bold; - } - """) - - step4_lbl = QtWidgets.QLabel("%s:" % _('STEP 4')) - step4_lbl.setToolTip( - _("Fourth step (and last) is to select a CNCJob made from \n" - "a solder paste dispensing geometry, and then view/save it's GCode.") - ) - - grid4.addWidget(step4_lbl, 0, 0) - grid4.addWidget(self.solder_gcode_view_btn, 0, 2) - grid4.addWidget(self.solder_gcode_save_btn, 1, 0, 1, 3) - - 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.gcode_frame.setDisabled(True) - # self.save_gcode_frame.setDisabled(True) + # ############################################################################# + # ######################### Tool GUI ########################################## + # ############################################################################# + self.ui = SolderUI(layout=self.layout, app=self.app, solder_class=self) + self.toolName = self.ui.toolName self.tooltable_tools = {} self.tooluid = 0 @@ -503,7 +58,6 @@ class SolderPaste(AppTool): self.name = "" self.obj = None - self.text_editor_tab = None # this will be used in the combobox context menu, for delete entry @@ -518,19 +72,20 @@ class SolderPaste(AppTool): # ## Signals self.combo_context_del_action.triggered.connect(self.on_delete_object) - self.addtool_btn.clicked.connect(self.on_tool_add) - self.addtool_entry.returnPressed.connect(self.on_tool_add) - self.deltool_btn.clicked.connect(self.on_tool_delete) - self.soldergeo_btn.clicked.connect(self.on_create_geo_click) - self.solder_gcode_btn.clicked.connect(self.on_create_gcode_click) - self.solder_gcode_view_btn.clicked.connect(self.on_view_gcode) - self.solder_gcode_save_btn.clicked.connect(self.on_save_gcode) + + self.ui.addtool_btn.clicked.connect(self.on_tool_add) + self.ui.addtool_entry.returnPressed.connect(self.on_tool_add) + self.ui.deltool_btn.clicked.connect(self.on_tool_delete) + self.ui.soldergeo_btn.clicked.connect(self.on_create_geo_click) + self.ui.solder_gcode_btn.clicked.connect(self.on_create_gcode_click) + self.ui.solder_gcode_view_btn.clicked.connect(self.on_view_gcode) + self.ui.solder_gcode_save_btn.clicked.connect(self.on_save_gcode) - self.geo_obj_combo.currentIndexChanged.connect(self.on_geo_select) - self.cnc_obj_combo.currentIndexChanged.connect(self.on_cncjob_select) + self.ui.geo_obj_combo.currentIndexChanged.connect(self.on_geo_select) + self.ui.cnc_obj_combo.currentIndexChanged.connect(self.on_cncjob_select) self.app.object_status_changed.connect(self.update_comboboxes) - self.reset_button.clicked.connect(self.set_tool_ui) + self.ui.reset_button.clicked.connect(self.set_tool_ui) def run(self, toggle=True): self.app.defaults.report_usage("ToolSolderPaste()") @@ -581,32 +136,32 @@ class SolderPaste(AppTool): def set_tool_ui(self): self.form_fields.update({ - "tools_solderpaste_new": self.addtool_entry, - "tools_solderpaste_z_start": self.z_start_entry, - "tools_solderpaste_z_dispense": self.z_dispense_entry, - "tools_solderpaste_z_stop": self.z_stop_entry, - "tools_solderpaste_z_travel": self.z_travel_entry, - "tools_solderpaste_z_toolchange": self.z_toolchange_entry, - "tools_solderpaste_xy_toolchange": self.xy_toolchange_entry, - "tools_solderpaste_frxy": self.frxy_entry, - "tools_solderpaste_frz": self.frz_entry, - "tools_solderpaste_frz_dispense": self.frz_dispense_entry, - "tools_solderpaste_speedfwd": self.speedfwd_entry, - "tools_solderpaste_dwellfwd": self.dwellfwd_entry, - "tools_solderpaste_speedrev": self.speedrev_entry, - "tools_solderpaste_dwellrev": self.dwellrev_entry, - "tools_solderpaste_pp": self.pp_combo + "tools_solderpaste_new": self.ui.addtool_entry, + "tools_solderpaste_z_start": self.ui.z_start_entry, + "tools_solderpaste_z_dispense": self.ui.z_dispense_entry, + "tools_solderpaste_z_stop": self.ui.z_stop_entry, + "tools_solderpaste_z_travel": self.ui.z_travel_entry, + "tools_solderpaste_z_toolchange": self.ui.z_toolchange_entry, + "tools_solderpaste_xy_toolchange": self.ui.xy_toolchange_entry, + "tools_solderpaste_frxy": self.ui.frxy_entry, + "tools_solderpaste_frz": self.ui.frz_entry, + "tools_solderpaste_frz_dispense": self.ui.frz_dispense_entry, + "tools_solderpaste_speedfwd": self.ui.speedfwd_entry, + "tools_solderpaste_dwellfwd": self.ui.dwellfwd_entry, + "tools_solderpaste_speedrev": self.ui.speedrev_entry, + "tools_solderpaste_dwellrev": self.ui.dwellrev_entry, + "tools_solderpaste_pp": self.ui.pp_combo }) self.set_form_from_defaults() self.read_form_to_options() - self.tools_table.setupContextMenu() - self.tools_table.addContextMenu( + self.ui.tools_table.setupContextMenu() + self.ui.tools_table.addContextMenu( _("Add"), lambda: self.on_tool_add(dia=None, muted=None), icon=QtGui.QIcon(self.app.resource_location + "/plus16.png")) - self.tools_table.addContextMenu( + self.ui.tools_table.addContextMenu( _("Delete"), lambda: - self.on_tool_delete(rows_to_delete=None, all=None), + self.on_tool_delete(rows_to_delete=None, all_tools=None), icon=QtGui.QIcon(self.app.resource_location + "/delete32.png") ) @@ -659,58 +214,56 @@ class SolderPaste(AppTool): sorted_tools.sort(reverse=True) n = len(sorted_tools) - self.tools_table.setRowCount(n) + self.ui.tools_table.setRowCount(n) tool_id = 0 for tool_sorted in sorted_tools: for tooluid_key, tooluid_value in self.tooltable_tools.items(): if float('%.*f' % (self.decimals, tooluid_value['tooldia'])) == tool_sorted: tool_id += 1 + + # Tool name/id id_item = QtWidgets.QTableWidgetItem('%d' % int(tool_id)) id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) row_no = tool_id - 1 - self.tools_table.setItem(row_no, 0, id_item) # Tool name/id + self.ui.tools_table.setItem(row_no, 0, id_item) - # Make sure that the drill diameter when in MM is with no more than 2 decimals - # There are no drill bits in MM with more than 2 decimals diameter - # For INCH the decimals should be no more than 4. There are no drills under 10mils + # Diameter dia = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, tooluid_value['tooldia'])) - dia.setFlags(QtCore.Qt.ItemIsEnabled) + self.ui.tools_table.setItem(row_no, 1, dia) + # Tool unique ID tool_uid_item = QtWidgets.QTableWidgetItem(str(int(tooluid_key))) - - self.tools_table.setItem(row_no, 1, dia) # Diameter - - self.tools_table.setItem(row_no, 2, tool_uid_item) # Tool unique ID + self.ui.tools_table.setItem(row_no, 2, tool_uid_item) # make the diameter column editable for row in range(tool_id): - self.tools_table.item(row, 1).setFlags( + self.ui.tools_table.item(row, 1).setFlags( QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) # all the tools are selected by default - self.tools_table.selectColumn(0) + self.ui.tools_table.selectColumn(0) # - self.tools_table.resizeColumnsToContents() - self.tools_table.resizeRowsToContents() + self.ui.tools_table.resizeColumnsToContents() + self.ui.tools_table.resizeRowsToContents() - vertical_header = self.tools_table.verticalHeader() + vertical_header = self.ui.tools_table.verticalHeader() vertical_header.hide() - self.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.ui.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - horizontal_header = self.tools_table.horizontalHeader() + horizontal_header = self.ui.tools_table.horizontalHeader() horizontal_header.setMinimumSectionSize(10) horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed) horizontal_header.resizeSection(0, 20) horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch) - # self.tools_table.setSortingEnabled(True) + # self.ui.tools_table.setSortingEnabled(True) # sort by tool diameter - # self.tools_table.sortItems(1) + # self.ui.tools_table.sortItems(1) - self.tools_table.setMinimumHeight(self.tools_table.getHeight()) - self.tools_table.setMaximumHeight(self.tools_table.getHeight()) + self.ui.tools_table.setMinimumHeight(self.ui.tools_table.getHeight()) + self.ui.tools_table.setMaximumHeight(self.ui.tools_table.getHeight()) self.ui_connect() @@ -724,7 +277,7 @@ class SolderPaste(AppTool): if row is None: try: - current_row = self.tools_table.currentRow() + current_row = self.ui.tools_table.currentRow() except Exception: current_row = 0 else: @@ -735,7 +288,7 @@ class SolderPaste(AppTool): # populate the form with the data from the tool associated with the row parameter try: - tooluid = int(self.tools_table.item(current_row, 2).text()) + tooluid = int(self.ui.tools_table.item(current_row, 2).text()) except Exception as e: log.debug("Tool missing. Add a tool in Tool Table. %s" % str(e)) return @@ -757,37 +310,37 @@ class SolderPaste(AppTool): def ui_connect(self): # on any change to the widgets that matter it will be called self.gui_form_to_storage which will save the # changes in geometry UI - for i in range(self.gcode_form_layout.count()): - if isinstance(self.gcode_form_layout.itemAt(i).widget(), FCComboBox): - self.gcode_form_layout.itemAt(i).widget().currentIndexChanged.connect(self.read_form_to_tooldata) - if isinstance(self.gcode_form_layout.itemAt(i).widget(), FCEntry): - self.gcode_form_layout.itemAt(i).widget().editingFinished.connect(self.read_form_to_tooldata) + for i in range(self.ui.gcode_form_layout.count()): + if isinstance(self.ui.gcode_form_layout.itemAt(i).widget(), FCComboBox): + self.ui.gcode_form_layout.itemAt(i).widget().currentIndexChanged.connect(self.read_form_to_tooldata) + if isinstance(self.ui.gcode_form_layout.itemAt(i).widget(), FCEntry): + self.ui.gcode_form_layout.itemAt(i).widget().editingFinished.connect(self.read_form_to_tooldata) - self.tools_table.itemChanged.connect(self.on_tool_edit) - self.tools_table.currentItemChanged.connect(self.on_row_selection_change) + self.ui.tools_table.itemChanged.connect(self.on_tool_edit) + self.ui.tools_table.currentItemChanged.connect(self.on_row_selection_change) def ui_disconnect(self): # if connected, disconnect the signal from the slot on item_changed as it creates issues - for i in range(self.gcode_form_layout.count()): - if isinstance(self.gcode_form_layout.itemAt(i).widget(), FCComboBox): + for i in range(self.ui.gcode_form_layout.count()): + if isinstance(self.ui.gcode_form_layout.itemAt(i).widget(), FCComboBox): try: - self.gcode_form_layout.itemAt(i).widget().currentIndexChanged.disconnect() + self.ui.gcode_form_layout.itemAt(i).widget().currentIndexChanged.disconnect() except (TypeError, AttributeError): pass - if isinstance(self.gcode_form_layout.itemAt(i).widget(), FCEntry): + if isinstance(self.ui.gcode_form_layout.itemAt(i).widget(), FCEntry): try: - self.gcode_form_layout.itemAt(i).widget().editingFinished.disconnect() + self.ui.gcode_form_layout.itemAt(i).widget().editingFinished.disconnect() except (TypeError, AttributeError): pass try: - self.tools_table.itemChanged.disconnect(self.on_tool_edit) + self.ui.tools_table.itemChanged.disconnect(self.on_tool_edit) except (TypeError, AttributeError): pass try: - self.tools_table.currentItemChanged.disconnect(self.on_row_selection_change) + self.ui.tools_table.currentItemChanged.disconnect(self.on_row_selection_change) except (TypeError, AttributeError): pass @@ -808,17 +361,17 @@ class SolderPaste(AppTool): return if status == 'append': - idx = self.obj_combo.findText(obj_name) + idx = self.ui.obj_combo.findText(obj_name) if idx != -1: - self.obj_combo.setCurrentIndex(idx) + self.ui.obj_combo.setCurrentIndex(idx) - idx = self.geo_obj_combo.findText(obj_name) + idx = self.ui.geo_obj_combo.findText(obj_name) if idx != -1: - self.geo_obj_combo.setCurrentIndex(idx) + self.ui.geo_obj_combo.setCurrentIndex(idx) - idx = self.cnc_obj_combo.findText(obj_name) + idx = self.ui.cnc_obj_combo.findText(obj_name) if idx != -1: - self.cnc_obj_combo.setCurrentIndex(idx) + self.ui.cnc_obj_combo.setCurrentIndex(idx) def read_form_to_options(self): """ @@ -835,8 +388,8 @@ class SolderPaste(AppTool): :param tooluid: the uid of the tool to be updated in the obj.tools :return: """ - current_row = self.tools_table.currentRow() - uid = tooluid if tooluid else int(self.tools_table.item(current_row, 2).text()) + current_row = self.ui.tools_table.currentRow() + uid = tooluid if tooluid else int(self.ui.tools_table.item(current_row, 2).text()) for key in self.form_fields: self.tooltable_tools[uid]['data'].update({ key: self.form_fields[key].get_value() @@ -881,19 +434,18 @@ class SolderPaste(AppTool): tool_dia = dia else: try: - tool_dia = float(self.addtool_entry.get_value()) + tool_dia = float(self.ui.addtool_entry.get_value()) except ValueError: # try to convert comma to decimal point. if it's still not working error message and return try: - tool_dia = float(self.addtool_entry.get_value().replace(',', '.')) + tool_dia = float(self.ui.addtool_entry.get_value().replace(',', '.')) except ValueError: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number.")) return if tool_dia is None: self.build_ui() - self.app.inform.emit('[WARNING_NOTCL] %s' % - _("Please enter a tool diameter to add, in Float format.")) + self.app.inform.emit('[WARNING_NOTCL] %s' % _("Please enter a tool diameter to add, in Float format.")) return if tool_dia == 0: @@ -923,16 +475,16 @@ class SolderPaste(AppTool): if float('%.*f' % (self.decimals, tool_dia)) in tool_dias: if muted is None: self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. Tool already in Tool Table.")) - self.tools_table.itemChanged.connect(self.on_tool_edit) + self.ui.tools_table.itemChanged.connect(self.on_tool_edit) return else: if muted is None: self.app.inform.emit('[success] %s' % _("New Nozzle tool added to Tool Table.")) self.tooltable_tools.update({ int(self.tooluid): { - 'tooldia': float('%.*f' % (self.decimals, tool_dia)), - 'data': deepcopy(self.options), - 'solid_geometry': [] + 'tooldia': float('%.*f' % (self.decimals, tool_dia)), + 'data': deepcopy(self.options), + 'solid_geometry': [] } }) @@ -951,26 +503,25 @@ class SolderPaste(AppTool): if tool_v == 'tooldia': tool_dias.append(float('%.*f' % (self.decimals, v[tool_v]))) - for row in range(self.tools_table.rowCount()): + for row in range(self.ui.tools_table.rowCount()): try: - new_tool_dia = float(self.tools_table.item(row, 1).text()) + new_tool_dia = float(self.ui.tools_table.item(row, 1).text()) except ValueError: # try to convert comma to decimal point. if it's still not working error message and return try: - new_tool_dia = float(self.tools_table.item(row, 1).text().replace(',', '.')) + new_tool_dia = float(self.ui.tools_table.item(row, 1).text().replace(',', '.')) except ValueError: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number.")) return - tooluid = int(self.tools_table.item(row, 2).text()) + tooluid = int(self.ui.tools_table.item(row, 2).text()) # identify the tool that was edited and get it's tooluid if new_tool_dia not in tool_dias: self.tooltable_tools[tooluid]['tooldia'] = new_tool_dia - self.app.inform.emit('[success] %s' % - _("Nozzle tool from Tool Table was edited.")) + self.app.inform.emit('[success] %s' % _("Nozzle tool from Tool Table was edited.")) self.build_ui() return else: @@ -980,24 +531,24 @@ class SolderPaste(AppTool): if k == tooluid: old_tool_dia = v['tooldia'] break - restore_dia_item = self.tools_table.item(row, 1) + restore_dia_item = self.ui.tools_table.item(row, 1) restore_dia_item.setText(str(old_tool_dia)) self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. New diameter value is already in the Tool Table.")) self.build_ui() - def on_tool_delete(self, rows_to_delete=None, all=None): + def on_tool_delete(self, rows_to_delete=None, all_tools=None): """ Will delete tool(s) in the Tool Table - :param rows_to_delete: tell which row (tool) to delete - :param all: to delete all tools at once + :param rows_to_delete: tell which row (tool) to delete + :param all_tools: to delete all tools at once :return: """ self.ui_disconnect() deleted_tools_list = [] - if all: + if all_tools: self.tooltable_tools.clear() self.build_ui() return @@ -1005,7 +556,7 @@ class SolderPaste(AppTool): if rows_to_delete: try: for row in rows_to_delete: - tooluid_del = int(self.tools_table.item(row, 2).text()) + tooluid_del = int(self.ui.tools_table.item(row, 2).text()) deleted_tools_list.append(tooluid_del) except TypeError: deleted_tools_list.append(rows_to_delete) @@ -1016,26 +567,24 @@ class SolderPaste(AppTool): return try: - if self.tools_table.selectedItems(): - for row_sel in self.tools_table.selectedItems(): + if self.ui.tools_table.selectedItems(): + for row_sel in self.ui.tools_table.selectedItems(): row = row_sel.row() if row < 0: continue - tooluid_del = int(self.tools_table.item(row, 2).text()) + tooluid_del = int(self.ui.tools_table.item(row, 2).text()) deleted_tools_list.append(tooluid_del) for t in deleted_tools_list: self.tooltable_tools.pop(t, None) except AttributeError: - self.app.inform.emit('[WARNING_NOTCL] %s' % - _("Delete failed. Select a Nozzle tool to delete.")) + self.app.inform.emit('[WARNING_NOTCL] %s' % _("Delete failed. Select a Nozzle tool to delete.")) return except Exception as e: log.debug(str(e)) - self.app.inform.emit('[success] %s' % - _("Nozzle tool(s) deleted from Tool Table.")) + self.app.inform.emit('[success] %s' % _("Nozzle tool(s) deleted from Tool Table.")) self.build_ui() def on_rmb_combo(self, pos, combo): @@ -1081,17 +630,15 @@ class SolderPaste(AppTool): # self.save_gcode_frame.setDisabled(True) pass - def on_create_geo_click(self, signal): + def on_create_geo_click(self): """ Will create a solderpaste dispensing geometry. - :param signal: passed by the signal that called this slot :return: """ - name = self.obj_combo.currentText() + name = self.ui.obj_combo.currentText() if name == '': - self.app.inform.emit('[WARNING_NOTCL] %s' % - _("No SolderPaste mask Gerber object loaded.")) + self.app.inform.emit('[WARNING_NOTCL] %s' % _("No SolderPaste mask Gerber object loaded.")) return obj = self.app.collection.get_by_name(name) @@ -1281,25 +828,24 @@ class SolderPaste(AppTool): else: self.app.app_obj.new_object("geometry", name + "_solderpaste", geo_init) - def on_create_gcode_click(self, signal): + def on_create_gcode_click(self): """ Will create a CNCJob object from the solderpaste dispensing geometry. - :param signal: parameter passed by the signal that called this slot :return: """ - name = self.geo_obj_combo.currentText() + name = self.ui.geo_obj_combo.currentText() obj = self.app.collection.get_by_name(name) if name == '': self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Geometry object available.")) - return 'fail' + return if obj.special_group != 'solder_paste_tool': self.app.inform.emit('[WARNING_NOTCL] %s' % _("This Geometry can't be processed. " "NOT a solder_paste_tool geometry.")) - return 'fail' + return a = 0 for tooluid_key in obj.tools: @@ -1307,7 +853,7 @@ class SolderPaste(AppTool): a += 1 if a == len(obj.tools): self.app.inform.emit('[ERROR_NOTCL] %s...' % _('Cancelled. Empty file, it has no geometry')) - return 'fail' + return # use the name of the first tool selected in self.geo_tools_table which has the diameter passed as tool_dia originar_name = obj.options['name'].partition('_')[0] @@ -1423,7 +969,7 @@ class SolderPaste(AppTool): # Switch plot_area to CNCJob tab self.app.ui.plot_tab_area.setCurrentWidget(self.text_editor_tab) - name = self.cnc_obj_combo.currentText() + name = self.ui.cnc_obj_combo.currentText() obj = self.app.collection.get_by_name(name) try: @@ -1458,8 +1004,7 @@ class SolderPaste(AppTool): lines = StringIO(gcode) except Exception as e: log.debug("ToolSolderpaste.on_view_gcode() --> %s" % str(e)) - self.app.inform.emit('[ERROR_NOTCL] %s...' % - _("No Gcode in the object")) + self.app.inform.emit('[ERROR_NOTCL] %s...' % _("No Gcode in the object")) return try: @@ -1468,8 +1013,7 @@ class SolderPaste(AppTool): self.text_editor_tab.code_editor.append(proc_line) except Exception as e: log.debug('ToolSolderPaste.on_view_gcode() -->%s' % str(e)) - self.app.inform.emit('[ERROR] %s --> %s' % - ('ToolSolderPaste.on_view_gcode()', str(e))) + self.app.inform.emit('[ERROR] %s --> %s' % ('ToolSolderPaste.on_view_gcode()', str(e))) return self.text_editor_tab.code_editor.moveCursor(QtGui.QTextCursor.Start) @@ -1484,7 +1028,7 @@ class SolderPaste(AppTool): :return: """ time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now()) - name = self.cnc_obj_combo.currentText() + name = self.ui.cnc_obj_combo.currentText() obj = self.app.collection.get_by_name(name) if obj.special_group != 'solder_paste_tool': @@ -1546,10 +1090,487 @@ class SolderPaste(AppTool): if self.app.defaults["global_open_style"] is False: self.app.file_opened.emit("gcode", filename) self.app.file_saved.emit("gcode", filename) - self.app.inform.emit('[success] %s: %s' % - (_("Solder paste dispenser GCode file saved to"), filename)) + self.app.inform.emit('[success] %s: %s' % (_("Solder paste dispenser GCode file saved to"), filename)) def reset_fields(self): + self.ui.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + self.ui.geo_obj_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex())) + self.ui.cnc_obj_combo.setRootModelIndex(self.app.collection.index(3, 0, QtCore.QModelIndex())) + + +class SolderUI: + + toolName = _("Solder Paste Tool") + + def __init__(self, layout, app, solder_class): + self.app = app + self.decimals = self.app.decimals + self.layout = layout + + # ## 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 + obj_form_layout = QtWidgets.QFormLayout() + self.layout.addLayout(obj_form_layout) + + # ## Gerber Object to be used for solderpaste dispensing + self.obj_combo = FCComboBox(callback=solder_class.on_rmb_combo) + self.obj_combo.setModel(self.app.collection) self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + self.obj_combo.is_last = True + self.obj_combo.obj_type = "Gerber" + + self.object_label = QtWidgets.QLabel('%s:' % _("GERBER")) + self.object_label.setToolTip(_("Gerber Solderpaste object.") + ) + obj_form_layout.addRow(self.object_label) + obj_form_layout.addRow(self.obj_combo) + + separator_line = QtWidgets.QFrame() + separator_line.setFrameShape(QtWidgets.QFrame.HLine) + separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) + obj_form_layout.addRow(separator_line) + + # ### Tools ## ## + self.tools_table_label = QtWidgets.QLabel('%s' % _('Tools Table')) + self.tools_table_label.setToolTip( + _("Tools pool from which the algorithm\n" + "will pick the ones used for dispensing solder paste.") + ) + self.layout.addWidget(self.tools_table_label) + + self.tools_table = FCTable() + self.layout.addWidget(self.tools_table) + + self.tools_table.setColumnCount(3) + self.tools_table.setHorizontalHeaderLabels(['#', _('Diameter'), '']) + self.tools_table.setColumnHidden(2, True) + self.tools_table.setSortingEnabled(False) + # self.tools_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + + self.tools_table.horizontalHeaderItem(0).setToolTip( + _("This is the Tool Number.\n" + "The solder dispensing will start with the tool with the biggest \n" + "diameter, continuing until there are no more Nozzle tools.\n" + "If there are no longer tools but there are still pads not covered\n " + "with solder paste, the app will issue a warning message box.") + ) + self.tools_table.horizontalHeaderItem(1).setToolTip( + _("Nozzle tool Diameter. It's value (in current FlatCAM units)\n" + "is the width of the solder paste dispensed.")) + + # ### Add a new Tool ## ## + hlay_tools = QtWidgets.QHBoxLayout() + self.layout.addLayout(hlay_tools) + + self.addtool_entry_lbl = QtWidgets.QLabel('%s:' % _('New Nozzle Tool')) + self.addtool_entry_lbl.setToolTip( + _("Diameter for the new Nozzle tool to add in the Tool Table") + ) + self.addtool_entry = FCDoubleSpinner(callback=self.confirmation_message) + self.addtool_entry.set_range(0.0000001, 9999.9999) + self.addtool_entry.set_precision(self.decimals) + self.addtool_entry.setSingleStep(0.1) + + # hlay.addWidget(self.addtool_label) + # hlay.addStretch() + hlay_tools.addWidget(self.addtool_entry_lbl) + hlay_tools.addWidget(self.addtool_entry) + + grid0 = QtWidgets.QGridLayout() + self.layout.addLayout(grid0) + + self.addtool_btn = QtWidgets.QPushButton(_('Add')) + self.addtool_btn.setToolTip( + _("Add a new nozzle tool to the Tool Table\n" + "with the diameter specified above.") + ) + + self.deltool_btn = QtWidgets.QPushButton(_('Delete')) + self.deltool_btn.setToolTip( + _("Delete a selection of tools in the Tool Table\n" + "by first selecting a row(s) in the Tool Table.") + ) + + grid0.addWidget(self.addtool_btn, 0, 0) + grid0.addWidget(self.deltool_btn, 0, 2) + + separator_line = QtWidgets.QFrame() + separator_line.setFrameShape(QtWidgets.QFrame.HLine) + separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) + grid0.addWidget(separator_line, 1, 0, 1, 3) + + # ## Buttons + grid0_1 = QtWidgets.QGridLayout() + self.layout.addLayout(grid0_1) + + step1_lbl = QtWidgets.QLabel("%s:" % _('STEP 1')) + step1_lbl.setToolTip( + _("First step is to select a number of nozzle tools for usage\n" + "and then optionally modify the GCode parameters below.") + ) + step1_description_lbl = QtWidgets.QLabel(_("Select tools.\n" + "Modify parameters.")) + + grid0_1.addWidget(step1_lbl, 0, 0, alignment=Qt.AlignTop) + grid0_1.addWidget(step1_description_lbl, 0, 2, alignment=Qt.AlignBottom) + + self.gcode_frame = QtWidgets.QFrame() + self.gcode_frame.setContentsMargins(0, 0, 0, 0) + self.layout.addWidget(self.gcode_frame) + self.gcode_box = QtWidgets.QVBoxLayout() + self.gcode_box.setContentsMargins(0, 0, 0, 0) + self.gcode_frame.setLayout(self.gcode_box) + + # ## Form Layout + self.gcode_form_layout = QtWidgets.QFormLayout() + self.gcode_box.addLayout(self.gcode_form_layout) + + # Z dispense start + self.z_start_entry = FCDoubleSpinner(callback=self.confirmation_message) + self.z_start_entry.set_range(0.0000001, 9999.9999) + self.z_start_entry.set_precision(self.decimals) + self.z_start_entry.setSingleStep(0.1) + + self.z_start_label = QtWidgets.QLabel('%s:' % _("Z Dispense Start")) + self.z_start_label.setToolTip( + _("The height (Z) when solder paste dispensing starts.") + ) + self.gcode_form_layout.addRow(self.z_start_label, self.z_start_entry) + + # Z dispense + self.z_dispense_entry = FCDoubleSpinner(callback=self.confirmation_message) + self.z_dispense_entry.set_range(0.0000001, 9999.9999) + self.z_dispense_entry.set_precision(self.decimals) + self.z_dispense_entry.setSingleStep(0.1) + + self.z_dispense_label = QtWidgets.QLabel('%s:' % _("Z Dispense")) + self.z_dispense_label.setToolTip( + _("The height (Z) when doing solder paste dispensing.") + ) + self.gcode_form_layout.addRow(self.z_dispense_label, self.z_dispense_entry) + + # Z dispense stop + self.z_stop_entry = FCDoubleSpinner(callback=self.confirmation_message) + self.z_stop_entry.set_range(0.0000001, 9999.9999) + self.z_stop_entry.set_precision(self.decimals) + self.z_stop_entry.setSingleStep(0.1) + + self.z_stop_label = QtWidgets.QLabel('%s:' % _("Z Dispense Stop")) + self.z_stop_label.setToolTip( + _("The height (Z) when solder paste dispensing stops.") + ) + self.gcode_form_layout.addRow(self.z_stop_label, self.z_stop_entry) + + # Z travel + self.z_travel_entry = FCDoubleSpinner(callback=self.confirmation_message) + self.z_travel_entry.set_range(0.0000001, 9999.9999) + self.z_travel_entry.set_precision(self.decimals) + self.z_travel_entry.setSingleStep(0.1) + + self.z_travel_label = QtWidgets.QLabel('%s:' % _("Z Travel")) + self.z_travel_label.setToolTip( + _("The height (Z) for travel between pads\n" + "(without dispensing solder paste).") + ) + self.gcode_form_layout.addRow(self.z_travel_label, self.z_travel_entry) + + # Z toolchange location + self.z_toolchange_entry = FCDoubleSpinner(callback=self.confirmation_message) + self.z_toolchange_entry.set_range(0.0000001, 9999.9999) + self.z_toolchange_entry.set_precision(self.decimals) + self.z_toolchange_entry.setSingleStep(0.1) + + self.z_toolchange_label = QtWidgets.QLabel('%s:' % _("Z Toolchange")) + self.z_toolchange_label.setToolTip( + _("The height (Z) for tool (nozzle) change.") + ) + self.gcode_form_layout.addRow(self.z_toolchange_label, self.z_toolchange_entry) + + # X,Y Toolchange location + self.xy_toolchange_entry = FCEntry() + self.xy_toolchange_label = QtWidgets.QLabel('%s:' % _("Toolchange X-Y")) + self.xy_toolchange_label.setToolTip( + _("The X,Y location for tool (nozzle) change.\n" + "The format is (x, y) where x and y are real numbers.") + ) + self.gcode_form_layout.addRow(self.xy_toolchange_label, self.xy_toolchange_entry) + + # Feedrate X-Y + self.frxy_entry = FCDoubleSpinner(callback=self.confirmation_message) + self.frxy_entry.set_range(0.0000, 99999.9999) + self.frxy_entry.set_precision(self.decimals) + self.frxy_entry.setSingleStep(0.1) + + self.frxy_label = QtWidgets.QLabel('%s:' % _("Feedrate X-Y")) + self.frxy_label.setToolTip( + _("Feedrate (speed) while moving on the X-Y plane.") + ) + self.gcode_form_layout.addRow(self.frxy_label, self.frxy_entry) + + # Feedrate Z + self.frz_entry = FCDoubleSpinner(callback=self.confirmation_message) + self.frz_entry.set_range(0.0000, 99999.9999) + self.frz_entry.set_precision(self.decimals) + self.frz_entry.setSingleStep(0.1) + + self.frz_label = QtWidgets.QLabel('%s:' % _("Feedrate Z")) + self.frz_label.setToolTip( + _("Feedrate (speed) while moving vertically\n" + "(on Z plane).") + ) + self.gcode_form_layout.addRow(self.frz_label, self.frz_entry) + + # Feedrate Z Dispense + self.frz_dispense_entry = FCDoubleSpinner(callback=self.confirmation_message) + self.frz_dispense_entry.set_range(0.0000, 99999.9999) + self.frz_dispense_entry.set_precision(self.decimals) + self.frz_dispense_entry.setSingleStep(0.1) + + self.frz_dispense_label = QtWidgets.QLabel('%s:' % _("Feedrate Z Dispense")) + self.frz_dispense_label.setToolTip( + _("Feedrate (speed) while moving up vertically\n" + " to Dispense position (on Z plane).") + ) + self.gcode_form_layout.addRow(self.frz_dispense_label, self.frz_dispense_entry) + + # Spindle Speed Forward + self.speedfwd_entry = FCSpinner(callback=self.confirmation_message_int) + self.speedfwd_entry.set_range(0, 999999) + self.speedfwd_entry.set_step(1000) + + self.speedfwd_label = QtWidgets.QLabel('%s:' % _("Spindle Speed FWD")) + self.speedfwd_label.setToolTip( + _("The dispenser speed while pushing solder paste\n" + "through the dispenser nozzle.") + ) + self.gcode_form_layout.addRow(self.speedfwd_label, self.speedfwd_entry) + + # Dwell Forward + self.dwellfwd_entry = FCDoubleSpinner(callback=self.confirmation_message) + self.dwellfwd_entry.set_range(0.0000001, 9999.9999) + self.dwellfwd_entry.set_precision(self.decimals) + self.dwellfwd_entry.setSingleStep(0.1) + + self.dwellfwd_label = QtWidgets.QLabel('%s:' % _("Dwell FWD")) + self.dwellfwd_label.setToolTip( + _("Pause after solder dispensing.") + ) + self.gcode_form_layout.addRow(self.dwellfwd_label, self.dwellfwd_entry) + + # Spindle Speed Reverse + self.speedrev_entry = FCSpinner(callback=self.confirmation_message_int) + self.speedrev_entry.set_range(0, 999999) + self.speedrev_entry.set_step(1000) + + self.speedrev_label = QtWidgets.QLabel('%s:' % _("Spindle Speed REV")) + self.speedrev_label.setToolTip( + _("The dispenser speed while retracting solder paste\n" + "through the dispenser nozzle.") + ) + self.gcode_form_layout.addRow(self.speedrev_label, self.speedrev_entry) + + # Dwell Reverse + self.dwellrev_entry = FCDoubleSpinner(callback=self.confirmation_message) + self.dwellrev_entry.set_range(0.0000001, 9999.9999) + self.dwellrev_entry.set_precision(self.decimals) + self.dwellrev_entry.setSingleStep(0.1) + + self.dwellrev_label = QtWidgets.QLabel('%s:' % _("Dwell REV")) + self.dwellrev_label.setToolTip( + _("Pause after solder paste dispenser retracted,\n" + "to allow pressure equilibrium.") + ) + self.gcode_form_layout.addRow(self.dwellrev_label, self.dwellrev_entry) + + # Preprocessors + pp_label = QtWidgets.QLabel('%s:' % _('Preprocessor')) + pp_label.setToolTip( + _("Files that control the GCode generation.") + ) + + self.pp_combo = FCComboBox() + # self.pp_combo.setStyleSheet('background-color: rgb(255,255,255)') + self.gcode_form_layout.addRow(pp_label, self.pp_combo) + + # ## Buttons + # grid1 = QtWidgets.QGridLayout() + # self.gcode_box.addLayout(grid1) + + self.solder_gcode_btn = QtWidgets.QPushButton(_("Generate GCode")) + self.solder_gcode_btn.setToolTip( + _("Generate GCode for Solder Paste dispensing\n" + "on PCB pads.") + ) + self.solder_gcode_btn.setStyleSheet(""" + QPushButton + { + font-weight: bold; + } + """) + + self.generation_frame = QtWidgets.QFrame() + self.generation_frame.setContentsMargins(0, 0, 0, 0) + self.layout.addWidget(self.generation_frame) + self.generation_box = QtWidgets.QVBoxLayout() + self.generation_box.setContentsMargins(0, 0, 0, 0) + self.generation_frame.setLayout(self.generation_box) + + # ## Buttons + grid2 = QtWidgets.QGridLayout() + self.generation_box.addLayout(grid2) + + step2_lbl = QtWidgets.QLabel("%s:" % _('STEP 2')) + step2_lbl.setToolTip( + _("Second step is to create a solder paste dispensing\n" + "geometry out of an Solder Paste Mask Gerber file.") + ) + + self.soldergeo_btn = QtWidgets.QPushButton(_("Generate Geo")) + self.soldergeo_btn.setToolTip( + _("Generate solder paste dispensing geometry.") + ) + self.soldergeo_btn.setStyleSheet(""" + QPushButton + { + font-weight: bold; + } + """) + + grid2.addWidget(step2_lbl, 0, 0) + grid2.addWidget(self.soldergeo_btn, 0, 2) + + # ## Form Layout + geo_form_layout = QtWidgets.QFormLayout() + self.generation_box.addLayout(geo_form_layout) + + # ## Geometry Object to be used for solderpaste dispensing + self.geo_obj_combo = FCComboBox(callback=solder_class.on_rmb_combo) + self.geo_obj_combo.setModel(self.app.collection) self.geo_obj_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex())) + self.geo_obj_combo.is_last = True + self.geo_obj_combo.obj_type = "Geometry" + + self.geo_object_label = QtWidgets.QLabel('%s:' % _("Geo Result")) + self.geo_object_label.setToolTip( + _("Geometry Solder Paste object.\n" + "The name of the object has to end in:\n" + "'_solderpaste' as a protection.") + ) + geo_form_layout.addRow(self.geo_object_label, self.geo_obj_combo) + + grid3 = QtWidgets.QGridLayout() + self.generation_box.addLayout(grid3) + + step3_lbl = QtWidgets.QLabel("%s:" % _('STEP 3')) + step3_lbl.setToolTip( + _("Third step is to select a solder paste dispensing geometry,\n" + "and then generate a CNCJob object.\n\n" + "REMEMBER: if you want to create a CNCJob with new parameters,\n" + "first you need to generate a geometry with those new params,\n" + "and only after that you can generate an updated CNCJob.") + ) + + grid3.addWidget(step3_lbl, 0, 0) + grid3.addWidget(self.solder_gcode_btn, 0, 2) + + # ## Form Layout + cnc_form_layout = QtWidgets.QFormLayout() + self.generation_box.addLayout(cnc_form_layout) + + # ## Gerber Object to be used for solderpaste dispensing + self.cnc_obj_combo = FCComboBox(callback=solder_class.on_rmb_combo) + self.cnc_obj_combo.setModel(self.app.collection) self.cnc_obj_combo.setRootModelIndex(self.app.collection.index(3, 0, QtCore.QModelIndex())) + self.cnc_obj_combo.is_last = True + self.geo_obj_combo.obj_type = "CNCJob" + + self.cnc_object_label = QtWidgets.QLabel('%s:' % _("CNC Result")) + self.cnc_object_label.setToolTip( + _("CNCJob Solder paste object.\n" + "In order to enable the GCode save section,\n" + "the name of the object has to end in:\n" + "'_solderpaste' as a protection.") + ) + cnc_form_layout.addRow(self.cnc_object_label, self.cnc_obj_combo) + + grid4 = QtWidgets.QGridLayout() + self.generation_box.addLayout(grid4) + + self.solder_gcode_view_btn = QtWidgets.QPushButton(_("View GCode")) + self.solder_gcode_view_btn.setToolTip( + _("View the generated GCode for Solder Paste dispensing\n" + "on PCB pads.") + ) + self.solder_gcode_view_btn.setStyleSheet(""" + QPushButton + { + font-weight: bold; + } + """) + + self.solder_gcode_save_btn = QtWidgets.QPushButton(_("Save GCode")) + self.solder_gcode_save_btn.setToolTip( + _("Save the generated GCode for Solder Paste dispensing\n" + "on PCB pads, to a file.") + ) + self.solder_gcode_save_btn.setStyleSheet(""" + QPushButton + { + font-weight: bold; + } + """) + + step4_lbl = QtWidgets.QLabel("%s:" % _('STEP 4')) + step4_lbl.setToolTip( + _("Fourth step (and last) is to select a CNCJob made from \n" + "a solder paste dispensing geometry, and then view/save it's GCode.") + ) + + grid4.addWidget(step4_lbl, 0, 0) + grid4.addWidget(self.solder_gcode_view_btn, 0, 2) + grid4.addWidget(self.solder_gcode_save_btn, 1, 0, 1, 3) + + 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) + + # #################################### FINSIHED GUI ########################### + # ############################################################################# + + def confirmation_message(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"), + self.decimals, + minval, + self.decimals, + maxval), False) + else: + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False) + + def confirmation_message_int(self, accepted, minval, maxval): + if accepted is False: + self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' % + (_("Edited value is out of range"), minval, maxval), False) + else: + self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)