- finished added a Tool Table for Tool SolderPaste

- working on multi tool soder paste dispensing
This commit is contained in:
Marius Stanciu
2019-02-20 17:10:43 +02:00
committed by Marius
parent 3ee6eb4a87
commit 448f34c090
3 changed files with 457 additions and 81 deletions

View File

@@ -708,7 +708,9 @@ class App(QtCore.QObject):
"tools_transform_offset_x": 0.0,
"tools_transform_offset_y": 0.0,
"tools_transform_mirror_reference": False,
"tools_transform_mirror_point": (0, 0)
"tools_transform_mirror_point": (0, 0),
"tools_solderpaste_tools": "1.0, 0.3",
})
###############################

View File

@@ -9,6 +9,11 @@ CAD program, and create G-Code for Isolation routing.
=================================================
20.02.2019
- finished added a Tool Table for Tool SolderPaste
- working on multi tool soder paste dispensing
19.02.2019
- added the ability to compress the FlatCAM project on save with LZMA compression. There is a setting in Edit -> Preferences -> Compression Level between 0 and 9. 9 level yields best compression at the price of RAM usage and time spent.

View File

@@ -27,51 +27,121 @@ class ToolSolderPaste(FlatCAMTool):
self.layout.addWidget(title_label)
## Form Layout
form_layout = QtWidgets.QFormLayout()
self.layout.addLayout(form_layout)
obj_form_layout = QtWidgets.QFormLayout()
self.layout.addLayout(obj_form_layout)
## Type of object to be cutout
self.type_obj_combo = QtWidgets.QComboBox()
self.type_obj_combo.addItem("Gerber")
self.type_obj_combo.addItem("Excellon")
self.type_obj_combo.addItem("Geometry")
# we get rid of item1 ("Excellon") as it is not suitable for creating solderpaste
self.type_obj_combo.view().setRowHidden(1, True)
self.type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
self.type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
self.type_obj_combo_label = QtWidgets.QLabel("Object Type:")
self.type_obj_combo_label.setToolTip(
"Specify the type of object to be used for solder paste dispense.\n"
"It can be of type: Gerber or Geometry.\n"
"What is selected here will dictate the kind\n"
"of objects that will populate the 'Object' combobox."
)
form_layout.addRow(self.type_obj_combo_label, self.type_obj_combo)
## Object to be used for solderpaste dispensing
## Gerber Object to be used for solderpaste dispensing
self.obj_combo = QtWidgets.QComboBox()
self.obj_combo.setModel(self.app.collection)
self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.obj_combo.setCurrentIndex(1)
self.object_label = QtWidgets.QLabel("Object:")
self.object_label = QtWidgets.QLabel("Gerber: ")
self.object_label.setToolTip(
"Solder paste object. "
"Gerber Solder paste object. "
)
form_layout.addRow(self.object_label, self.obj_combo)
obj_form_layout.addRow(self.object_label, self.obj_combo)
# Offset distance
self.nozzle_dia_entry = FloatEntry()
self.nozzle_dia_entry.setValidator(QtGui.QDoubleValidator(0.0000, 9.9999, 4))
self.nozzle_dia_label = QtWidgets.QLabel("Nozzle Diameter:")
self.nozzle_dia_label.setToolTip(
"The offset for the solder paste.\n"
"Due of the diameter of the solder paste dispenser\n"
"we need to adjust the quantity of solder paste."
#### Tools ####
self.tools_table_label = QtWidgets.QLabel('<b>Tools Table</b>')
self.tools_table_label.setToolTip(
"Tools pool from which the algorithm\n"
"will pick the ones used for dispensing solder paste."
)
form_layout.addRow(self.nozzle_dia_label, self.nozzle_dia_entry)
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.")
self.empty_label = QtWidgets.QLabel('')
self.layout.addWidget(self.empty_label)
#### Add a new Tool ####
hlay_tools = QtWidgets.QHBoxLayout()
self.layout.addLayout(hlay_tools)
self.addtool_entry_lbl = QtWidgets.QLabel('<b>Nozzle Dia:</b>')
self.addtool_entry_lbl.setToolTip(
"Diameter for the new Nozzle tool to add in the Tool Table"
)
self.addtool_entry = FCEntry()
# 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."
)
self.soldergeo_btn = QtWidgets.QPushButton("Generate Geo")
self.soldergeo_btn.setToolTip(
"Generate solder paste dispensing geometry."
)
grid0.addWidget(self.addtool_btn, 0, 0)
# grid2.addWidget(self.copytool_btn, 0, 1)
grid0.addWidget(self.deltool_btn, 0, 2)
grid0.addWidget(self.soldergeo_btn, 2, 2)
## Form Layout
geo_form_layout = QtWidgets.QFormLayout()
self.layout.addLayout(geo_form_layout)
## Gerber Object to be used for solderpaste dispensing
self.geo_obj_combo = QtWidgets.QComboBox()
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.setCurrentIndex(1)
self.geo_object_label = QtWidgets.QLabel("Geometry:")
self.geo_object_label.setToolTip(
"Geometry Solder paste object.\n"
"In order to enable the GCode generation section,\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)
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
form_layout = QtWidgets.QFormLayout()
self.gcode_box.addLayout(form_layout)
# Z dispense start
self.z_start_entry = FCEntry()
@@ -197,15 +267,9 @@ class ToolSolderPaste(FlatCAMTool):
## Buttons
hlay = QtWidgets.QHBoxLayout()
self.layout.addLayout(hlay)
self.gcode_box.addLayout(hlay)
hlay.addStretch()
self.soldergeo_btn = QtWidgets.QPushButton("Generate Geo")
self.soldergeo_btn.setToolTip(
"Generate solder paste dispensing geometry."
)
hlay.addWidget(self.soldergeo_btn)
self.solder_gcode = QtWidgets.QPushButton("Generate GCode")
self.solder_gcode.setToolTip(
@@ -214,19 +278,19 @@ class ToolSolderPaste(FlatCAMTool):
)
hlay.addWidget(self.solder_gcode)
self.layout.addStretch()
self.gcode_frame.setDisabled(True)
self.tools = {}
self.tooluid = 0
## Signals
self.addtool_btn.clicked.connect(self.on_tool_add)
self.deltool_btn.clicked.connect(self.on_tool_delete)
self.soldergeo_btn.clicked.connect(self.on_create_geo)
self.solder_gcode.clicked.connect(self.on_create_gcode)
self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
def on_type_obj_index_changed(self, index):
obj_type = self.type_obj_combo.currentIndex()
self.obj_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
self.obj_combo.setCurrentIndex(0)
self.geo_obj_combo.currentIndexChanged.connect(self.on_geo_select)
def run(self):
self.app.report_usage("ToolSolderPaste()")
@@ -238,14 +302,290 @@ class ToolSolderPaste(FlatCAMTool):
if self.app.ui.splitter.sizes()[0] == 0:
self.app.ui.splitter.setSizes([1, 1])
self.build_ui()
self.app.ui.notebook.setTabText(2, "SolderPaste Tool")
def install(self, icon=None, separator=None, **kwargs):
FlatCAMTool.install(self, icon, separator, shortcut='ALT+K', **kwargs)
def set_tool_ui(self):
# self.ncc_overlap_entry.set_value(self.app.defaults["tools_nccoverlap"])
# self.ncc_margin_entry.set_value(self.app.defaults["tools_nccmargin"])
# self.ncc_method_radio.set_value(self.app.defaults["tools_nccmethod"])
# self.ncc_connect_cb.set_value(self.app.defaults["tools_nccconnect"])
# self.ncc_contour_cb.set_value(self.app.defaults["tools_ncccontour"])
# self.ncc_rest_cb.set_value(self.app.defaults["tools_nccrest"])
self.tools_table.setupContextMenu()
self.tools_table.addContextMenu(
"Add", lambda: self.on_tool_add(dia=None, muted=None), icon=QtGui.QIcon("share/plus16.png"))
self.tools_table.addContextMenu(
"Delete", lambda:
self.on_tool_delete(rows_to_delete=None, all=None), icon=QtGui.QIcon("share/delete32.png"))
try:
dias = [float(eval(dia)) for dia in self.app.defaults["tools_solderpaste_tools"].split(",")]
except:
log.error("At least one Nozzle tool diameter needed. "
"Verify in Edit -> Preferences -> TOOLS -> Solder Paste Tools.")
return
self.tooluid = 0
self.tools.clear()
for tool_dia in dias:
self.tooluid += 1
self.tools.update({
int(self.tooluid): {
'tooldia': float('%.4f' % tool_dia),
'solid_geometry': []
}
})
self.name = ""
self.obj = None
self.units = self.app.general_options_form.general_app_group.units_radio.get_value().upper()
self.reset_fields()
pass
def build_ui(self):
self.ui_disconnect()
# updated units
self.units = self.app.general_options_form.general_app_group.units_radio.get_value().upper()
if self.units == "IN":
self.addtool_entry.set_value(0.039)
else:
self.addtool_entry.set_value(1)
sorted_tools = []
for k, v in self.tools.items():
sorted_tools.append(float('%.4f' % float(v['tooldia'])))
sorted_tools.sort(reverse=True)
n = len(sorted_tools)
self.tools_table.setRowCount(n)
tool_id = 0
for tool_sorted in sorted_tools:
for tooluid_key, tooluid_value in self.tools.items():
if float('%.4f' % tooluid_value['tooldia']) == tool_sorted:
tool_id += 1
id = QtWidgets.QTableWidgetItem('%d' % int(tool_id))
id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
row_no = tool_id - 1
self.tools_table.setItem(row_no, 0, id) # Tool name/id
# 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 3 decimals diameter
# For INCH the decimals should be no more than 3. There are no drills under 10mils
if self.units == 'MM':
dia = QtWidgets.QTableWidgetItem('%.2f' % tooluid_value['tooldia'])
else:
dia = QtWidgets.QTableWidgetItem('%.3f' % tooluid_value['tooldia'])
dia.setFlags(QtCore.Qt.ItemIsEnabled)
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
# make the diameter column editable
for row in range(tool_id):
self.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.tools_table.resizeColumnsToContents()
self.tools_table.resizeRowsToContents()
vertical_header = self.tools_table.verticalHeader()
vertical_header.hide()
self.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
horizontal_header = self.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)
# sort by tool diameter
# self.tools_table.sortItems(1)
self.tools_table.setMinimumHeight(self.tools_table.getHeight())
self.tools_table.setMaximumHeight(self.tools_table.getHeight())
self.ui_connect()
def ui_connect(self):
self.tools_table.itemChanged.connect(self.on_tool_edit)
def ui_disconnect(self):
try:
# if connected, disconnect the signal from the slot on item_changed as it creates issues
self.tools_table.itemChanged.disconnect(self.on_tool_edit)
except:
pass
def on_tool_add(self, dia=None, muted=None):
self.ui_disconnect()
if dia:
tool_dia = dia
else:
try:
tool_dia = float(self.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(',', '.'))
except ValueError:
self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
"use a number.")
return
if tool_dia is None:
self.build_ui()
self.app.inform.emit("[WARNING_NOTCL] Please enter a tool diameter to add, in Float format.")
return
if tool_dia == 0:
self.app.inform.emit("[WARNING_NOTCL] Please enter a tool diameter with non-zero value, in Float format.")
return
# construct a list of all 'tooluid' in the self.tools
tool_uid_list = []
for tooluid_key in self.tools:
tool_uid_item = int(tooluid_key)
tool_uid_list.append(tool_uid_item)
# find maximum from the temp_uid, add 1 and this is the new 'tooluid'
if not tool_uid_list:
max_uid = 0
else:
max_uid = max(tool_uid_list)
self.tooluid = int(max_uid + 1)
tool_dias = []
for k, v in self.tools.items():
for tool_v in v.keys():
if tool_v == 'tooldia':
tool_dias.append(float('%.4f' % v[tool_v]))
if float('%.4f' % tool_dia) in tool_dias:
if muted is None:
self.app.inform.emit("[WARNING_NOTCL]Adding Nozzle tool cancelled. Tool already in Tool Table.")
self.tools_table.itemChanged.connect(self.on_tool_edit)
return
else:
if muted is None:
self.app.inform.emit("[success] New Nozzle tool added to Tool Table.")
self.tools.update({
int(self.tooluid): {
'tooldia': float('%.4f' % tool_dia),
'solid_geometry': []
}
})
self.build_ui()
def on_tool_edit(self):
self.ui_disconnect()
tool_dias = []
for k, v in self.tools.items():
for tool_v in v.keys():
if tool_v == 'tooldia':
tool_dias.append(float('%.4f' % v[tool_v]))
for row in range(self.tools_table.rowCount()):
try:
new_tool_dia = float(self.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(',', '.'))
except ValueError:
self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
"use a number.")
return
tooluid = int(self.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.tools[tooluid]['tooldia'] = new_tool_dia
self.app.inform.emit("[success] Nozzle tool from Tool Table was edited.")
self.build_ui()
return
else:
# identify the old tool_dia and restore the text in tool table
for k, v in self.tools.items():
if k == tooluid:
old_tool_dia = v['tooldia']
break
restore_dia_item = self.tools_table.item(row, 1)
restore_dia_item.setText(str(old_tool_dia))
self.app.inform.emit("[WARNING_NOTCL] Edit cancelled. New diameter value is already in the Tool Table.")
self.build_ui()
def on_tool_delete(self, rows_to_delete=None, all=None):
self.ui_disconnect()
deleted_tools_list = []
if all:
self.tools.clear()
self.build_ui()
return
if rows_to_delete:
try:
for row in rows_to_delete:
tooluid_del = int(self.tools_table.item(row, 2).text())
deleted_tools_list.append(tooluid_del)
except TypeError:
deleted_tools_list.append(rows_to_delete)
for t in deleted_tools_list:
self.tools.pop(t, None)
self.build_ui()
return
try:
if self.tools_table.selectedItems():
for row_sel in self.tools_table.selectedItems():
row = row_sel.row()
if row < 0:
continue
tooluid_del = int(self.tools_table.item(row, 2).text())
deleted_tools_list.append(tooluid_del)
for t in deleted_tools_list:
self.tools.pop(t, None)
except AttributeError:
self.app.inform.emit("[WARNING_NOTCL]Delete failed. Select a Nozzle tool to delete.")
return
except Exception as e:
log.debug(str(e))
self.app.inform.emit("[success] Nozzle tool(s) deleted from Tool Table.")
self.build_ui()
def on_geo_select(self):
if self.geo_obj_combo.currentText().rpartition('_')[2] == 'solderpaste':
self.gcode_frame.setDisabled(False)
else:
self.gcode_frame.setDisabled(True)
@staticmethod
def distance(pt1, pt2):
@@ -260,24 +600,21 @@ class ToolSolderPaste(FlatCAMTool):
if type(obj.solid_geometry) is not list:
obj.solid_geometry = [obj.solid_geometry]
try:
offset = self.nozzle_dia_entry.get_value() / 2
except Exception as e:
log.debug("ToolSoderPaste.on_create_geo() --> %s" % str(e))
self.app.inform.emit("[ERROR_NOTCL] Failed. Offset value is missing ...")
return
if offset is None:
self.app.inform.emit("[ERROR_NOTCL] Failed. Offset value is missing ...")
return
# Sort tools in descending order
sorted_tools = []
for k, v in self.tools.items():
sorted_tools.append(float('%.4f' % float(v['tooldia'])))
sorted_tools.sort(reverse=True)
def geo_init(geo_obj, app_obj):
geo_obj.solid_geometry = []
geo_obj.multigeo = False
geo_obj.multitool = False
geo_obj.tools = {}
geo_obj.multigeo = True
geo_obj.multitool = True
geo_obj.tools = {}
def solder_line(p, offset):
xmin, ymin, xmax, ymax = p.bounds
min = [xmin, ymin]
@@ -313,26 +650,58 @@ class ToolSolderPaste(FlatCAMTool):
geo = geo.intersection(offseted_poly)
return geo
for g in obj.solid_geometry:
if type(g) == MultiPolygon:
for poly in g:
geom = solder_line(poly, offset=offset)
if geom == 'fail':
app_obj.inform.emit("[ERROR_NOTCL] The Nozzle diameter is too big for certain features.")
return 'fail'
if not geom.is_empty:
geo_obj.solid_geometry.append(geom)
elif type(g) == Polygon:
geom = solder_line(g, offset=offset)
if geom == 'fail':
app_obj.inform.emit("[ERROR_NOTCL] The Nozzle diameter is too big for certain features.")
return 'fail'
if not geom.is_empty:
geo_obj.solid_geometry.append(geom)
work_geo = obj.solid_geometry
rest_geo = []
tooluid = 1
for tool in sorted_tools:
offset = tool / 2
for uid, v in self.tools.items():
if float('%.4f' % float(v['tooldia'])) == tool:
tooluid = int(uid)
break
for g in work_geo:
if type(g) == MultiPolygon:
for poly in g:
geom = solder_line(poly, offset=offset)
if geom != 'fail':
try:
geo_obj.tools[tooluid]['solid_geometry'].append(geom)
except KeyError:
geo_obj.tools[tooluid] = {}
geo_obj.tools[tooluid]['solid_geometry'] = []
geo_obj.tools[tooluid]['solid_geometry'].append(geom)
else:
rest_geo.append(poly)
elif type(g) == Polygon:
geom = solder_line(g, offset=offset)
if geom != 'fail':
try:
geo_obj.tools[tooluid]['solid_geometry'].append(geom)
except KeyError:
geo_obj.tools[tooluid] = {}
geo_obj.tools[tooluid]['solid_geometry'] = []
geo_obj.tools[tooluid]['solid_geometry'].append(geom)
else:
rest_geo.append(g)
work_geo = rest_geo
if not work_geo:
app_obj.inform.emit("[success] Solder Paste geometry generated successfully...")
return
# if we still have geometry not processed at the end of the tools then we failed
# some or all the pads are not covered with solder paste
if rest_geo:
app_obj.inform.emit("[WARNING_NOTCL] Some or all pads have no solder "
"due of inadequate nozzle diameters...")
return 'fail'
def job_thread(app_obj):
try:
app_obj.new_object("geometry", name + "_temp_solderpaste", geo_init)
app_obj.new_object("geometry", name + "_solderpaste", geo_init)
except Exception as e:
proc.done()
traceback.print_stack()