ToolSolderPaste.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. from FlatCAMTool import FlatCAMTool
  2. from copy import copy,deepcopy
  3. from ObjectCollection import *
  4. from FlatCAMApp import *
  5. from PyQt5 import QtGui, QtCore, QtWidgets
  6. from GUIElements import IntEntry, RadioSet, LengthEntry
  7. from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber
  8. class ToolSolderPaste(FlatCAMTool):
  9. toolName = "Solder Paste Tool"
  10. def __init__(self, app):
  11. FlatCAMTool.__init__(self, app)
  12. ## Title
  13. title_label = QtWidgets.QLabel("%s" % self.toolName)
  14. title_label.setStyleSheet("""
  15. QLabel
  16. {
  17. font-size: 16px;
  18. font-weight: bold;
  19. }
  20. """)
  21. self.layout.addWidget(title_label)
  22. ## Form Layout
  23. form_layout = QtWidgets.QFormLayout()
  24. self.layout.addLayout(form_layout)
  25. ## Type of object to be cutout
  26. self.type_obj_combo = QtWidgets.QComboBox()
  27. self.type_obj_combo.addItem("Gerber")
  28. self.type_obj_combo.addItem("Excellon")
  29. self.type_obj_combo.addItem("Geometry")
  30. # we get rid of item1 ("Excellon") as it is not suitable for creating solderpaste
  31. self.type_obj_combo.view().setRowHidden(1, True)
  32. self.type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
  33. self.type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
  34. self.type_obj_combo_label = QtWidgets.QLabel("Object Type:")
  35. self.type_obj_combo_label.setToolTip(
  36. "Specify the type of object to be used for solder paste dispense.\n"
  37. "It can be of type: Gerber or Geometry.\n"
  38. "What is selected here will dictate the kind\n"
  39. "of objects that will populate the 'Object' combobox."
  40. )
  41. form_layout.addRow(self.type_obj_combo_label, self.type_obj_combo)
  42. ## Object to be used for solderpaste dispensing
  43. self.obj_combo = QtWidgets.QComboBox()
  44. self.obj_combo.setModel(self.app.collection)
  45. self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  46. self.obj_combo.setCurrentIndex(1)
  47. self.object_label = QtWidgets.QLabel("Object:")
  48. self.object_label.setToolTip(
  49. "Solder paste object. "
  50. )
  51. form_layout.addRow(self.object_label, self.obj_combo)
  52. # Offset distance
  53. self.nozzle_dia_entry = FloatEntry()
  54. self.nozzle_dia_entry.setValidator(QtGui.QDoubleValidator(0.0000, 9.9999, 4))
  55. self.nozzle_dia_label = QtWidgets.QLabel("Nozzle Diameter:")
  56. self.nozzle_dia_label.setToolTip(
  57. "The offset for the solder paste.\n"
  58. "Due of the diameter of the solder paste dispenser\n"
  59. "we need to adjust the quantity of solder paste."
  60. )
  61. form_layout.addRow(self.nozzle_dia_label, self.nozzle_dia_entry)
  62. # Z dispense start
  63. self.z_start_entry = FCEntry()
  64. self.z_start_label = QtWidgets.QLabel("Z Dispense Start:")
  65. self.z_start_label.setToolTip(
  66. "The size of the gaps in the cutout\n"
  67. "used to keep the board connected to\n"
  68. "the surrounding material (the one \n"
  69. "from which the PCB is cutout)."
  70. )
  71. form_layout.addRow(self.z_start_label, self.z_start_entry)
  72. # Z dispense
  73. self.z_dispense_entry = FCEntry()
  74. self.z_dispense_label = QtWidgets.QLabel("Z Dispense:")
  75. self.z_dispense_label.setToolTip(
  76. "Margin over bounds. A positive value here\n"
  77. "will make the cutout of the PCB further from\n"
  78. "the actual PCB border"
  79. )
  80. form_layout.addRow(self.z_dispense_label, self.z_dispense_entry)
  81. # Z dispense stop
  82. self.z_stop_entry = FCEntry()
  83. self.z_stop_label = QtWidgets.QLabel("Z Dispense Stop:")
  84. self.z_stop_label.setToolTip(
  85. "The size of the gaps in the cutout\n"
  86. "used to keep the board connected to\n"
  87. "the surrounding material (the one \n"
  88. "from which the PCB is cutout)."
  89. )
  90. form_layout.addRow(self.z_stop_label, self.z_stop_entry)
  91. # Z travel
  92. self.z_travel_entry = FCEntry()
  93. self.z_travel_label = QtWidgets.QLabel("Z Travel:")
  94. self.z_travel_label.setToolTip(
  95. "The size of the gaps in the cutout\n"
  96. "used to keep the board connected to\n"
  97. "the surrounding material (the one \n"
  98. "from which the PCB is cutout)."
  99. )
  100. form_layout.addRow(self.z_travel_label, self.z_travel_entry)
  101. # Feedrate X-Y
  102. self.frxy_entry = FCEntry()
  103. self.frxy_label = QtWidgets.QLabel("Feedrate X-Y:")
  104. self.frxy_label.setToolTip(
  105. "The size of the gaps in the cutout\n"
  106. "used to keep the board connected to\n"
  107. "the surrounding material (the one \n"
  108. "from which the PCB is cutout)."
  109. )
  110. form_layout.addRow(self.frxy_label, self.frxy_entry)
  111. # Feedrate Z
  112. self.frz_entry = FCEntry()
  113. self.frz_label = QtWidgets.QLabel("Feedrate Z:")
  114. self.frz_label.setToolTip(
  115. "The size of the gaps in the cutout\n"
  116. "used to keep the board connected to\n"
  117. "the surrounding material (the one \n"
  118. "from which the PCB is cutout)."
  119. )
  120. form_layout.addRow(self.frz_label, self.frz_entry)
  121. # Spindle Speed Forward
  122. self.speedfwd_entry = FCEntry()
  123. self.speedfwd_label = QtWidgets.QLabel("Spindle Speed FWD:")
  124. self.speedfwd_label.setToolTip(
  125. "The size of the gaps in the cutout\n"
  126. "used to keep the board connected to\n"
  127. "the surrounding material (the one \n"
  128. "from which the PCB is cutout)."
  129. )
  130. form_layout.addRow(self.speedfwd_label, self.speedfwd_entry)
  131. # Dwell Forward
  132. self.dwellfwd_entry = FCEntry()
  133. self.dwellfwd_label = QtWidgets.QLabel("Dwell FWD:")
  134. self.dwellfwd_label.setToolTip(
  135. "The size of the gaps in the cutout\n"
  136. "used to keep the board connected to\n"
  137. "the surrounding material (the one \n"
  138. "from which the PCB is cutout)."
  139. )
  140. form_layout.addRow(self.dwellfwd_label, self.dwellfwd_entry)
  141. # Spindle Speed Reverse
  142. self.speedrev_entry = FCEntry()
  143. self.speedrev_label = QtWidgets.QLabel("Spindle Speed REV:")
  144. self.speedrev_label.setToolTip(
  145. "The size of the gaps in the cutout\n"
  146. "used to keep the board connected to\n"
  147. "the surrounding material (the one \n"
  148. "from which the PCB is cutout)."
  149. )
  150. form_layout.addRow(self.speedrev_label, self.speedrev_entry)
  151. # Dwell Reverse
  152. self.dwellrev_entry = FCEntry()
  153. self.dwellrev_label = QtWidgets.QLabel("Dwell REV:")
  154. self.dwellrev_label.setToolTip(
  155. "The size of the gaps in the cutout\n"
  156. "used to keep the board connected to\n"
  157. "the surrounding material (the one \n"
  158. "from which the PCB is cutout)."
  159. )
  160. form_layout.addRow(self.dwellrev_label, self.dwellrev_entry)
  161. # Postprocessors
  162. pp_label = QtWidgets.QLabel('PostProcessors:')
  163. pp_label.setToolTip(
  164. "Files that control the GCoe generation."
  165. )
  166. self.pp_combo = FCComboBox()
  167. pp_items = [1, 2, 3, 4, 5]
  168. for it in pp_items:
  169. self.pp_combo.addItem(str(it))
  170. self.pp_combo.setStyleSheet('background-color: rgb(255,255,255)')
  171. form_layout.addRow(pp_label, self.pp_combo)
  172. ## Buttons
  173. hlay = QtWidgets.QHBoxLayout()
  174. self.layout.addLayout(hlay)
  175. hlay.addStretch()
  176. self.soldergeo_btn = QtWidgets.QPushButton("Generate Geo")
  177. self.soldergeo_btn.setToolTip(
  178. "Generate solder paste dispensing geometry."
  179. )
  180. hlay.addWidget(self.soldergeo_btn)
  181. self.solder_gcode = QtWidgets.QPushButton("Generate GCode")
  182. self.solder_gcode.setToolTip(
  183. "Generate GCode to dispense Solder Paste\n"
  184. "on PCB pads."
  185. )
  186. hlay.addWidget(self.solder_gcode)
  187. self.layout.addStretch()
  188. ## Signals
  189. self.soldergeo_btn.clicked.connect(self.on_create_geo)
  190. self.solder_gcode.clicked.connect(self.on_create_gcode)
  191. self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
  192. def on_type_obj_index_changed(self, index):
  193. obj_type = self.type_obj_combo.currentIndex()
  194. self.obj_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
  195. self.obj_combo.setCurrentIndex(0)
  196. def run(self):
  197. self.app.report_usage("ToolSolderPaste()")
  198. FlatCAMTool.run(self)
  199. self.set_tool_ui()
  200. # if the splitter us hidden, display it
  201. if self.app.ui.splitter.sizes()[0] == 0:
  202. self.app.ui.splitter.setSizes([1, 1])
  203. self.app.ui.notebook.setTabText(2, "SolderPaste Tool")
  204. def install(self, icon=None, separator=None, **kwargs):
  205. FlatCAMTool.install(self, icon, separator, shortcut='ALT+K', **kwargs)
  206. def set_tool_ui(self):
  207. self.reset_fields()
  208. pass
  209. @staticmethod
  210. def distance(pt1, pt2):
  211. return sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2)
  212. def on_create_geo(self):
  213. proc = self.app.proc_container.new("Creating Solder Paste dispensing geometry.")
  214. name = self.obj_combo.currentText()
  215. obj = self.app.collection.get_by_name(name)
  216. if type(obj.solid_geometry) is not list:
  217. obj.solid_geometry = [obj.solid_geometry]
  218. try:
  219. offset = self.nozzle_dia_entry.get_value() / 2
  220. except Exception as e:
  221. log.debug("ToolSoderPaste.on_create_geo() --> %s" % str(e))
  222. self.app.inform.emit("[ERROR_NOTCL] Failed. Offset value is missing ...")
  223. return
  224. if offset is None:
  225. self.app.inform.emit("[ERROR_NOTCL] Failed. Offset value is missing ...")
  226. return
  227. def geo_init(geo_obj, app_obj):
  228. geo_obj.solid_geometry = []
  229. geo_obj.multigeo = False
  230. geo_obj.multitool = False
  231. geo_obj.tools = {}
  232. def solder_line(p, offset):
  233. xmin, ymin, xmax, ymax = p.bounds
  234. min = [xmin, ymin]
  235. max = [xmax, ymax]
  236. min_r = [xmin, ymax]
  237. max_r = [xmax, ymin]
  238. diagonal_1 = LineString([min, max])
  239. diagonal_2 = LineString([min_r, max_r])
  240. round_diag_1 = round(diagonal_1.intersection(p).length, 4)
  241. round_diag_2 = round(diagonal_2.intersection(p).length, 4)
  242. if round_diag_1 == round_diag_2:
  243. l = distance((xmin, ymin), (xmax, ymin))
  244. h = distance((xmin, ymin), (xmin, ymax))
  245. if offset >= l /2 or offset >= h / 2:
  246. return "fail"
  247. if l > h:
  248. h_half = h / 2
  249. start = [xmin, (ymin + h_half)]
  250. stop = [(xmin + l), (ymin + h_half)]
  251. else:
  252. l_half = l / 2
  253. start = [(xmin + l_half), ymin]
  254. stop = [(xmin + l_half), (ymin + h)]
  255. geo = LineString([start, stop])
  256. elif round_diag_1 > round_diag_2:
  257. geo = diagonal_1.intersection(p)
  258. else:
  259. geo = diagonal_2.intersection(p)
  260. offseted_poly = p.buffer(-offset)
  261. geo = geo.intersection(offseted_poly)
  262. return geo
  263. for g in obj.solid_geometry:
  264. if type(g) == MultiPolygon:
  265. for poly in g:
  266. geom = solder_line(poly, offset=offset)
  267. if geom == 'fail':
  268. app_obj.inform.emit("[ERROR_NOTCL] The Nozzle diameter is too big for certain features.")
  269. return 'fail'
  270. if not geom.is_empty:
  271. geo_obj.solid_geometry.append(geom)
  272. elif type(g) == Polygon:
  273. geom = solder_line(g, offset=offset)
  274. if geom == 'fail':
  275. app_obj.inform.emit("[ERROR_NOTCL] The Nozzle diameter is too big for certain features.")
  276. return 'fail'
  277. if not geom.is_empty:
  278. geo_obj.solid_geometry.append(geom)
  279. def job_thread(app_obj):
  280. try:
  281. app_obj.new_object("geometry", name + "_temp_solderpaste", geo_init)
  282. except Exception as e:
  283. proc.done()
  284. traceback.print_stack()
  285. return
  286. proc.done()
  287. self.app.inform.emit("Generating Solder Paste dispensing geometry...")
  288. # Promise object with the new name
  289. self.app.collection.promise(name)
  290. # Background
  291. self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
  292. # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
  293. def on_create_gcode(self):
  294. name = self.obj_combo.currentText()
  295. def geo_init(geo_obj, app_obj):
  296. pass
  297. # self.app.new_object("geometry", name + "_cutout", geo_init)
  298. # self.app.inform.emit("[success] Rectangular CutOut operation finished.")
  299. # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
  300. def reset_fields(self):
  301. self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))