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)