ToolSolderPaste.py 63 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # File Author: Marius Adrian Stanciu (c) #
  4. # Date: 3/10/2019 #
  5. # MIT Licence #
  6. # ##########################################################
  7. from appTool import AppTool
  8. from Common import LoudDict
  9. from appGUI.GUIElements import FCComboBox, FCEntry, FCTable, \
  10. FCInputDialog, FCDoubleSpinner, FCSpinner, FCFileSaveDialog
  11. from app_Main import log
  12. from camlib import distance
  13. from appEditors.FlatCAMTextEditor import TextEditor
  14. from PyQt5 import QtGui, QtCore, QtWidgets
  15. from PyQt5.QtCore import Qt
  16. from copy import deepcopy
  17. from datetime import datetime
  18. from shapely.geometry import Polygon, LineString
  19. from shapely.ops import cascaded_union
  20. import traceback
  21. from io import StringIO
  22. import gettext
  23. import appTranslation as fcTranslate
  24. import builtins
  25. fcTranslate.apply_language('strings')
  26. if '_' not in builtins.__dict__:
  27. _ = gettext.gettext
  28. class SolderPaste(AppTool):
  29. toolName = _("Solder Paste Tool")
  30. def __init__(self, app):
  31. AppTool.__init__(self, app)
  32. # Number of decimals to be used for tools/nozzles in this FlatCAM Tool
  33. self.decimals = self.app.decimals
  34. # ## Title
  35. title_label = QtWidgets.QLabel("%s" % self.toolName)
  36. title_label.setStyleSheet("""
  37. QLabel
  38. {
  39. font-size: 16px;
  40. font-weight: bold;
  41. }
  42. """)
  43. self.layout.addWidget(title_label)
  44. # ## Form Layout
  45. obj_form_layout = QtWidgets.QFormLayout()
  46. self.layout.addLayout(obj_form_layout)
  47. # ## Gerber Object to be used for solderpaste dispensing
  48. self.obj_combo = FCComboBox(callback=self.on_rmb_combo)
  49. self.obj_combo.setModel(self.app.collection)
  50. self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  51. self.obj_combo.is_last = True
  52. self.obj_combo.obj_type = "Gerber"
  53. self.object_label = QtWidgets.QLabel('<b>%s</b>:'% _("GERBER"))
  54. self.object_label.setToolTip(_("Gerber Solderpaste object.")
  55. )
  56. obj_form_layout.addRow(self.object_label)
  57. obj_form_layout.addRow(self.obj_combo)
  58. separator_line = QtWidgets.QFrame()
  59. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  60. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  61. obj_form_layout.addRow(separator_line)
  62. # ### Tools ## ##
  63. self.tools_table_label = QtWidgets.QLabel('<b>%s</b>' % _('Tools Table'))
  64. self.tools_table_label.setToolTip(
  65. _("Tools pool from which the algorithm\n"
  66. "will pick the ones used for dispensing solder paste.")
  67. )
  68. self.layout.addWidget(self.tools_table_label)
  69. self.tools_table = FCTable()
  70. self.layout.addWidget(self.tools_table)
  71. self.tools_table.setColumnCount(3)
  72. self.tools_table.setHorizontalHeaderLabels(['#', _('Diameter'), ''])
  73. self.tools_table.setColumnHidden(2, True)
  74. self.tools_table.setSortingEnabled(False)
  75. # self.tools_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
  76. self.tools_table.horizontalHeaderItem(0).setToolTip(
  77. _("This is the Tool Number.\n"
  78. "The solder dispensing will start with the tool with the biggest \n"
  79. "diameter, continuing until there are no more Nozzle tools.\n"
  80. "If there are no longer tools but there are still pads not covered\n "
  81. "with solder paste, the app will issue a warning message box.")
  82. )
  83. self.tools_table.horizontalHeaderItem(1).setToolTip(
  84. _("Nozzle tool Diameter. It's value (in current FlatCAM units)\n"
  85. "is the width of the solder paste dispensed."))
  86. # ### Add a new Tool ## ##
  87. hlay_tools = QtWidgets.QHBoxLayout()
  88. self.layout.addLayout(hlay_tools)
  89. self.addtool_entry_lbl = QtWidgets.QLabel('<b>%s:</b>' % _('New Nozzle Tool'))
  90. self.addtool_entry_lbl.setToolTip(
  91. _("Diameter for the new Nozzle tool to add in the Tool Table")
  92. )
  93. self.addtool_entry = FCDoubleSpinner(callback=self.confirmation_message)
  94. self.addtool_entry.set_range(0.0000001, 9999.9999)
  95. self.addtool_entry.set_precision(self.decimals)
  96. self.addtool_entry.setSingleStep(0.1)
  97. # hlay.addWidget(self.addtool_label)
  98. # hlay.addStretch()
  99. hlay_tools.addWidget(self.addtool_entry_lbl)
  100. hlay_tools.addWidget(self.addtool_entry)
  101. grid0 = QtWidgets.QGridLayout()
  102. self.layout.addLayout(grid0)
  103. self.addtool_btn = QtWidgets.QPushButton(_('Add'))
  104. self.addtool_btn.setToolTip(
  105. _("Add a new nozzle tool to the Tool Table\n"
  106. "with the diameter specified above.")
  107. )
  108. self.deltool_btn = QtWidgets.QPushButton(_('Delete'))
  109. self.deltool_btn.setToolTip(
  110. _("Delete a selection of tools in the Tool Table\n"
  111. "by first selecting a row(s) in the Tool Table.")
  112. )
  113. grid0.addWidget(self.addtool_btn, 0, 0)
  114. grid0.addWidget(self.deltool_btn, 0, 2)
  115. separator_line = QtWidgets.QFrame()
  116. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  117. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  118. grid0.addWidget(separator_line, 1, 0, 1, 3)
  119. # ## Buttons
  120. grid0_1 = QtWidgets.QGridLayout()
  121. self.layout.addLayout(grid0_1)
  122. step1_lbl = QtWidgets.QLabel("<b>%s:</b>" % _('STEP 1'))
  123. step1_lbl.setToolTip(
  124. _("First step is to select a number of nozzle tools for usage\n"
  125. "and then optionally modify the GCode parameters below.")
  126. )
  127. step1_description_lbl = QtWidgets.QLabel(_("Select tools.\n"
  128. "Modify parameters."))
  129. grid0_1.addWidget(step1_lbl, 0, 0, alignment=Qt.AlignTop)
  130. grid0_1.addWidget(step1_description_lbl, 0, 2, alignment=Qt.AlignBottom)
  131. self.gcode_frame = QtWidgets.QFrame()
  132. self.gcode_frame.setContentsMargins(0, 0, 0, 0)
  133. self.layout.addWidget(self.gcode_frame)
  134. self.gcode_box = QtWidgets.QVBoxLayout()
  135. self.gcode_box.setContentsMargins(0, 0, 0, 0)
  136. self.gcode_frame.setLayout(self.gcode_box)
  137. # ## Form Layout
  138. self.gcode_form_layout = QtWidgets.QFormLayout()
  139. self.gcode_box.addLayout(self.gcode_form_layout)
  140. # Z dispense start
  141. self.z_start_entry = FCDoubleSpinner(callback=self.confirmation_message)
  142. self.z_start_entry.set_range(0.0000001, 9999.9999)
  143. self.z_start_entry.set_precision(self.decimals)
  144. self.z_start_entry.setSingleStep(0.1)
  145. self.z_start_label = QtWidgets.QLabel('%s:' % _("Z Dispense Start"))
  146. self.z_start_label.setToolTip(
  147. _("The height (Z) when solder paste dispensing starts.")
  148. )
  149. self.gcode_form_layout.addRow(self.z_start_label, self.z_start_entry)
  150. # Z dispense
  151. self.z_dispense_entry = FCDoubleSpinner(callback=self.confirmation_message)
  152. self.z_dispense_entry.set_range(0.0000001, 9999.9999)
  153. self.z_dispense_entry.set_precision(self.decimals)
  154. self.z_dispense_entry.setSingleStep(0.1)
  155. self.z_dispense_label = QtWidgets.QLabel('%s:' % _("Z Dispense"))
  156. self.z_dispense_label.setToolTip(
  157. _("The height (Z) when doing solder paste dispensing.")
  158. )
  159. self.gcode_form_layout.addRow(self.z_dispense_label, self.z_dispense_entry)
  160. # Z dispense stop
  161. self.z_stop_entry = FCDoubleSpinner(callback=self.confirmation_message)
  162. self.z_stop_entry.set_range(0.0000001, 9999.9999)
  163. self.z_stop_entry.set_precision(self.decimals)
  164. self.z_stop_entry.setSingleStep(0.1)
  165. self.z_stop_label = QtWidgets.QLabel('%s:' % _("Z Dispense Stop"))
  166. self.z_stop_label.setToolTip(
  167. _("The height (Z) when solder paste dispensing stops.")
  168. )
  169. self.gcode_form_layout.addRow(self.z_stop_label, self.z_stop_entry)
  170. # Z travel
  171. self.z_travel_entry = FCDoubleSpinner(callback=self.confirmation_message)
  172. self.z_travel_entry.set_range(0.0000001, 9999.9999)
  173. self.z_travel_entry.set_precision(self.decimals)
  174. self.z_travel_entry.setSingleStep(0.1)
  175. self.z_travel_label = QtWidgets.QLabel('%s:' % _("Z Travel"))
  176. self.z_travel_label.setToolTip(
  177. _("The height (Z) for travel between pads\n"
  178. "(without dispensing solder paste).")
  179. )
  180. self.gcode_form_layout.addRow(self.z_travel_label, self.z_travel_entry)
  181. # Z toolchange location
  182. self.z_toolchange_entry = FCDoubleSpinner(callback=self.confirmation_message)
  183. self.z_toolchange_entry.set_range(0.0000001, 9999.9999)
  184. self.z_toolchange_entry.set_precision(self.decimals)
  185. self.z_toolchange_entry.setSingleStep(0.1)
  186. self.z_toolchange_label = QtWidgets.QLabel('%s:' % _("Z Toolchange"))
  187. self.z_toolchange_label.setToolTip(
  188. _("The height (Z) for tool (nozzle) change.")
  189. )
  190. self.gcode_form_layout.addRow(self.z_toolchange_label, self.z_toolchange_entry)
  191. # X,Y Toolchange location
  192. self.xy_toolchange_entry = FCEntry()
  193. self.xy_toolchange_label = QtWidgets.QLabel('%s:' % _("Toolchange X-Y"))
  194. self.xy_toolchange_label.setToolTip(
  195. _("The X,Y location for tool (nozzle) change.\n"
  196. "The format is (x, y) where x and y are real numbers.")
  197. )
  198. self.gcode_form_layout.addRow(self.xy_toolchange_label, self.xy_toolchange_entry)
  199. # Feedrate X-Y
  200. self.frxy_entry = FCDoubleSpinner(callback=self.confirmation_message)
  201. self.frxy_entry.set_range(0.0000, 99999.9999)
  202. self.frxy_entry.set_precision(self.decimals)
  203. self.frxy_entry.setSingleStep(0.1)
  204. self.frxy_label = QtWidgets.QLabel('%s:' % _("Feedrate X-Y"))
  205. self.frxy_label.setToolTip(
  206. _("Feedrate (speed) while moving on the X-Y plane.")
  207. )
  208. self.gcode_form_layout.addRow(self.frxy_label, self.frxy_entry)
  209. # Feedrate Z
  210. self.frz_entry = FCDoubleSpinner(callback=self.confirmation_message)
  211. self.frz_entry.set_range(0.0000, 99999.9999)
  212. self.frz_entry.set_precision(self.decimals)
  213. self.frz_entry.setSingleStep(0.1)
  214. self.frz_label = QtWidgets.QLabel('%s:' % _("Feedrate Z"))
  215. self.frz_label.setToolTip(
  216. _("Feedrate (speed) while moving vertically\n"
  217. "(on Z plane).")
  218. )
  219. self.gcode_form_layout.addRow(self.frz_label, self.frz_entry)
  220. # Feedrate Z Dispense
  221. self.frz_dispense_entry = FCDoubleSpinner(callback=self.confirmation_message)
  222. self.frz_dispense_entry.set_range(0.0000, 99999.9999)
  223. self.frz_dispense_entry.set_precision(self.decimals)
  224. self.frz_dispense_entry.setSingleStep(0.1)
  225. self.frz_dispense_label = QtWidgets.QLabel('%s:' % _("Feedrate Z Dispense"))
  226. self.frz_dispense_label.setToolTip(
  227. _("Feedrate (speed) while moving up vertically\n"
  228. " to Dispense position (on Z plane).")
  229. )
  230. self.gcode_form_layout.addRow(self.frz_dispense_label, self.frz_dispense_entry)
  231. # Spindle Speed Forward
  232. self.speedfwd_entry = FCSpinner(callback=self.confirmation_message_int)
  233. self.speedfwd_entry.set_range(0, 999999)
  234. self.speedfwd_entry.set_step(1000)
  235. self.speedfwd_label = QtWidgets.QLabel('%s:' % _("Spindle Speed FWD"))
  236. self.speedfwd_label.setToolTip(
  237. _("The dispenser speed while pushing solder paste\n"
  238. "through the dispenser nozzle.")
  239. )
  240. self.gcode_form_layout.addRow(self.speedfwd_label, self.speedfwd_entry)
  241. # Dwell Forward
  242. self.dwellfwd_entry = FCDoubleSpinner(callback=self.confirmation_message)
  243. self.dwellfwd_entry.set_range(0.0000001, 9999.9999)
  244. self.dwellfwd_entry.set_precision(self.decimals)
  245. self.dwellfwd_entry.setSingleStep(0.1)
  246. self.dwellfwd_label = QtWidgets.QLabel('%s:' % _("Dwell FWD"))
  247. self.dwellfwd_label.setToolTip(
  248. _("Pause after solder dispensing.")
  249. )
  250. self.gcode_form_layout.addRow(self.dwellfwd_label, self.dwellfwd_entry)
  251. # Spindle Speed Reverse
  252. self.speedrev_entry = FCSpinner(callback=self.confirmation_message_int)
  253. self.speedrev_entry.set_range(0, 999999)
  254. self.speedrev_entry.set_step(1000)
  255. self.speedrev_label = QtWidgets.QLabel('%s:' % _("Spindle Speed REV"))
  256. self.speedrev_label.setToolTip(
  257. _("The dispenser speed while retracting solder paste\n"
  258. "through the dispenser nozzle.")
  259. )
  260. self.gcode_form_layout.addRow(self.speedrev_label, self.speedrev_entry)
  261. # Dwell Reverse
  262. self.dwellrev_entry = FCDoubleSpinner(callback=self.confirmation_message)
  263. self.dwellrev_entry.set_range(0.0000001, 9999.9999)
  264. self.dwellrev_entry.set_precision(self.decimals)
  265. self.dwellrev_entry.setSingleStep(0.1)
  266. self.dwellrev_label = QtWidgets.QLabel('%s:' % _("Dwell REV"))
  267. self.dwellrev_label.setToolTip(
  268. _("Pause after solder paste dispenser retracted,\n"
  269. "to allow pressure equilibrium.")
  270. )
  271. self.gcode_form_layout.addRow(self.dwellrev_label, self.dwellrev_entry)
  272. # Preprocessors
  273. pp_label = QtWidgets.QLabel('%s:' % _('Preprocessor'))
  274. pp_label.setToolTip(
  275. _("Files that control the GCode generation.")
  276. )
  277. self.pp_combo = FCComboBox()
  278. # self.pp_combo.setStyleSheet('background-color: rgb(255,255,255)')
  279. self.gcode_form_layout.addRow(pp_label, self.pp_combo)
  280. # ## Buttons
  281. # grid1 = QtWidgets.QGridLayout()
  282. # self.gcode_box.addLayout(grid1)
  283. self.solder_gcode_btn = QtWidgets.QPushButton(_("Generate GCode"))
  284. self.solder_gcode_btn.setToolTip(
  285. _("Generate GCode for Solder Paste dispensing\n"
  286. "on PCB pads.")
  287. )
  288. self.solder_gcode_btn.setStyleSheet("""
  289. QPushButton
  290. {
  291. font-weight: bold;
  292. }
  293. """)
  294. self.generation_frame = QtWidgets.QFrame()
  295. self.generation_frame.setContentsMargins(0, 0, 0, 0)
  296. self.layout.addWidget(self.generation_frame)
  297. self.generation_box = QtWidgets.QVBoxLayout()
  298. self.generation_box.setContentsMargins(0, 0, 0, 0)
  299. self.generation_frame.setLayout(self.generation_box)
  300. # ## Buttons
  301. grid2 = QtWidgets.QGridLayout()
  302. self.generation_box.addLayout(grid2)
  303. step2_lbl = QtWidgets.QLabel("<b>%s:</b>" % _('STEP 2'))
  304. step2_lbl.setToolTip(
  305. _("Second step is to create a solder paste dispensing\n"
  306. "geometry out of an Solder Paste Mask Gerber file.")
  307. )
  308. self.soldergeo_btn = QtWidgets.QPushButton(_("Generate Geo"))
  309. self.soldergeo_btn.setToolTip(
  310. _("Generate solder paste dispensing geometry.")
  311. )
  312. self.soldergeo_btn.setStyleSheet("""
  313. QPushButton
  314. {
  315. font-weight: bold;
  316. }
  317. """)
  318. grid2.addWidget(step2_lbl, 0, 0)
  319. grid2.addWidget(self.soldergeo_btn, 0, 2)
  320. # ## Form Layout
  321. geo_form_layout = QtWidgets.QFormLayout()
  322. self.generation_box.addLayout(geo_form_layout)
  323. # ## Geometry Object to be used for solderpaste dispensing
  324. self.geo_obj_combo = FCComboBox(callback=self.on_rmb_combo)
  325. self.geo_obj_combo.setModel(self.app.collection)
  326. self.geo_obj_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
  327. self.geo_obj_combo.is_last = True
  328. self.geo_obj_combo.obj_type = "Geometry"
  329. self.geo_object_label = QtWidgets.QLabel('%s:' % _("Geo Result"))
  330. self.geo_object_label.setToolTip(
  331. _("Geometry Solder Paste object.\n"
  332. "The name of the object has to end in:\n"
  333. "'_solderpaste' as a protection.")
  334. )
  335. geo_form_layout.addRow(self.geo_object_label, self.geo_obj_combo)
  336. grid3 = QtWidgets.QGridLayout()
  337. self.generation_box.addLayout(grid3)
  338. step3_lbl = QtWidgets.QLabel("<b>%s:</b>" % _('STEP 3'))
  339. step3_lbl.setToolTip(
  340. _("Third step is to select a solder paste dispensing geometry,\n"
  341. "and then generate a CNCJob object.\n\n"
  342. "REMEMBER: if you want to create a CNCJob with new parameters,\n"
  343. "first you need to generate a geometry with those new params,\n"
  344. "and only after that you can generate an updated CNCJob.")
  345. )
  346. grid3.addWidget(step3_lbl, 0, 0)
  347. grid3.addWidget(self.solder_gcode_btn, 0, 2)
  348. # ## Form Layout
  349. cnc_form_layout = QtWidgets.QFormLayout()
  350. self.generation_box.addLayout(cnc_form_layout)
  351. # ## Gerber Object to be used for solderpaste dispensing
  352. self.cnc_obj_combo = FCComboBox(callback=self.on_rmb_combo)
  353. self.cnc_obj_combo.setModel(self.app.collection)
  354. self.cnc_obj_combo.setRootModelIndex(self.app.collection.index(3, 0, QtCore.QModelIndex()))
  355. self.cnc_obj_combo.is_last = True
  356. self.geo_obj_combo.obj_type = "CNCJob"
  357. self.cnc_object_label = QtWidgets.QLabel('%s:' % _("CNC Result"))
  358. self.cnc_object_label.setToolTip(
  359. _("CNCJob Solder paste object.\n"
  360. "In order to enable the GCode save section,\n"
  361. "the name of the object has to end in:\n"
  362. "'_solderpaste' as a protection.")
  363. )
  364. cnc_form_layout.addRow(self.cnc_object_label, self.cnc_obj_combo)
  365. grid4 = QtWidgets.QGridLayout()
  366. self.generation_box.addLayout(grid4)
  367. self.solder_gcode_view_btn = QtWidgets.QPushButton(_("View GCode"))
  368. self.solder_gcode_view_btn.setToolTip(
  369. _("View the generated GCode for Solder Paste dispensing\n"
  370. "on PCB pads.")
  371. )
  372. self.solder_gcode_view_btn.setStyleSheet("""
  373. QPushButton
  374. {
  375. font-weight: bold;
  376. }
  377. """)
  378. self.solder_gcode_save_btn = QtWidgets.QPushButton(_("Save GCode"))
  379. self.solder_gcode_save_btn.setToolTip(
  380. _("Save the generated GCode for Solder Paste dispensing\n"
  381. "on PCB pads, to a file.")
  382. )
  383. self.solder_gcode_save_btn.setStyleSheet("""
  384. QPushButton
  385. {
  386. font-weight: bold;
  387. }
  388. """)
  389. step4_lbl = QtWidgets.QLabel("<b>%s:</b>" % _('STEP 4'))
  390. step4_lbl.setToolTip(
  391. _("Fourth step (and last) is to select a CNCJob made from \n"
  392. "a solder paste dispensing geometry, and then view/save it's GCode.")
  393. )
  394. grid4.addWidget(step4_lbl, 0, 0)
  395. grid4.addWidget(self.solder_gcode_view_btn, 0, 2)
  396. grid4.addWidget(self.solder_gcode_save_btn, 1, 0, 1, 3)
  397. self.layout.addStretch()
  398. # ## Reset Tool
  399. self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
  400. self.reset_button.setToolTip(
  401. _("Will reset the tool parameters.")
  402. )
  403. self.reset_button.setStyleSheet("""
  404. QPushButton
  405. {
  406. font-weight: bold;
  407. }
  408. """)
  409. self.layout.addWidget(self.reset_button)
  410. # self.gcode_frame.setDisabled(True)
  411. # self.save_gcode_frame.setDisabled(True)
  412. self.tooltable_tools = {}
  413. self.tooluid = 0
  414. self.options = LoudDict()
  415. self.form_fields = {}
  416. self.units = ''
  417. self.name = ""
  418. self.obj = None
  419. self.text_editor_tab = None
  420. # this will be used in the combobox context menu, for delete entry
  421. self.obj_to_be_deleted_name = ''
  422. # stpre here the flattened geometry
  423. self.flat_geometry = []
  424. # action to be added in the combobox context menu
  425. self.combo_context_del_action = QtWidgets.QAction(QtGui.QIcon(self.app.resource_location + '/trash16.png'),
  426. _("Delete Object"))
  427. # ## Signals
  428. self.combo_context_del_action.triggered.connect(self.on_delete_object)
  429. self.addtool_btn.clicked.connect(self.on_tool_add)
  430. self.addtool_entry.returnPressed.connect(self.on_tool_add)
  431. self.deltool_btn.clicked.connect(self.on_tool_delete)
  432. self.soldergeo_btn.clicked.connect(self.on_create_geo_click)
  433. self.solder_gcode_btn.clicked.connect(self.on_create_gcode_click)
  434. self.solder_gcode_view_btn.clicked.connect(self.on_view_gcode)
  435. self.solder_gcode_save_btn.clicked.connect(self.on_save_gcode)
  436. self.geo_obj_combo.currentIndexChanged.connect(self.on_geo_select)
  437. self.cnc_obj_combo.currentIndexChanged.connect(self.on_cncjob_select)
  438. self.app.object_status_changed.connect(self.update_comboboxes)
  439. self.reset_button.clicked.connect(self.set_tool_ui)
  440. def run(self, toggle=True):
  441. self.app.defaults.report_usage("ToolSolderPaste()")
  442. if toggle:
  443. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  444. if self.app.ui.splitter.sizes()[0] == 0:
  445. self.app.ui.splitter.setSizes([1, 1])
  446. else:
  447. try:
  448. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  449. # if tab is populated with the tool but it does not have the focus, focus on it
  450. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  451. # focus on Tool Tab
  452. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  453. else:
  454. self.app.ui.splitter.setSizes([0, 1])
  455. except AttributeError:
  456. pass
  457. else:
  458. if self.app.ui.splitter.sizes()[0] == 0:
  459. self.app.ui.splitter.setSizes([1, 1])
  460. AppTool.run(self)
  461. self.set_tool_ui()
  462. self.build_ui()
  463. self.app.ui.notebook.setTabText(2, _("SolderPaste Tool"))
  464. def install(self, icon=None, separator=None, **kwargs):
  465. AppTool.install(self, icon, separator, shortcut='Alt+K', **kwargs)
  466. def on_add_tool_by_key(self):
  467. tool_add_popup = FCInputDialog(title='%s...' % _("New Tool"),
  468. text='%s:' % _('Enter a Tool Diameter'),
  469. min=0.0000, max=99.9999, decimals=4)
  470. tool_add_popup.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/letter_t_32.png'))
  471. val, ok = tool_add_popup.get_value()
  472. if ok:
  473. if float(val) == 0:
  474. self.app.inform.emit('[WARNING_NOTCL] %s' %
  475. _("Please enter a tool diameter with non-zero value, in Float format."))
  476. return
  477. self.on_tool_add(dia=float(val))
  478. else:
  479. self.app.inform.emit('[WARNING_NOTCL] %s...' % _("Adding Tool cancelled"))
  480. def set_tool_ui(self):
  481. self.form_fields.update({
  482. "tools_solderpaste_new": self.addtool_entry,
  483. "tools_solderpaste_z_start": self.z_start_entry,
  484. "tools_solderpaste_z_dispense": self.z_dispense_entry,
  485. "tools_solderpaste_z_stop": self.z_stop_entry,
  486. "tools_solderpaste_z_travel": self.z_travel_entry,
  487. "tools_solderpaste_z_toolchange": self.z_toolchange_entry,
  488. "tools_solderpaste_xy_toolchange": self.xy_toolchange_entry,
  489. "tools_solderpaste_frxy": self.frxy_entry,
  490. "tools_solderpaste_frz": self.frz_entry,
  491. "tools_solderpaste_frz_dispense": self.frz_dispense_entry,
  492. "tools_solderpaste_speedfwd": self.speedfwd_entry,
  493. "tools_solderpaste_dwellfwd": self.dwellfwd_entry,
  494. "tools_solderpaste_speedrev": self.speedrev_entry,
  495. "tools_solderpaste_dwellrev": self.dwellrev_entry,
  496. "tools_solderpaste_pp": self.pp_combo
  497. })
  498. self.set_form_from_defaults()
  499. self.read_form_to_options()
  500. self.tools_table.setupContextMenu()
  501. self.tools_table.addContextMenu(
  502. _("Add"), lambda: self.on_tool_add(dia=None, muted=None),
  503. icon=QtGui.QIcon(self.app.resource_location + "/plus16.png"))
  504. self.tools_table.addContextMenu(
  505. _("Delete"), lambda:
  506. self.on_tool_delete(rows_to_delete=None, all=None),
  507. icon=QtGui.QIcon(self.app.resource_location + "/delete32.png")
  508. )
  509. try:
  510. dias = [float(eval(dia)) for dia in self.app.defaults["tools_solderpaste_tools"].split(",") if dia != '']
  511. except Exception:
  512. log.error("At least one Nozzle tool diameter needed. "
  513. "Verify in Edit -> Preferences -> TOOLS -> Solder Paste Tools.")
  514. return
  515. self.tooluid = 0
  516. self.tooltable_tools.clear()
  517. for tool_dia in dias:
  518. self.tooluid += 1
  519. self.tooltable_tools.update({
  520. int(self.tooluid): {
  521. 'tooldia': float('%.*f' % (self.decimals, tool_dia)),
  522. 'data': deepcopy(self.options),
  523. 'solid_geometry': []
  524. }
  525. })
  526. self.name = ""
  527. self.obj = None
  528. self.units = self.app.defaults['units'].upper()
  529. for name in list(self.app.preprocessors.keys()):
  530. # populate only with preprocessor files that start with 'Paste_'
  531. if name.partition('_')[0] != 'Paste':
  532. continue
  533. self.pp_combo.addItem(name)
  534. self.reset_fields()
  535. def build_ui(self):
  536. """
  537. Will rebuild the UI populating it (tools table)
  538. :return:
  539. """
  540. self.ui_disconnect()
  541. # updated units
  542. self.units = self.app.defaults['units'].upper()
  543. sorted_tools = []
  544. for k, v in self.tooltable_tools.items():
  545. sorted_tools.append(float('%.*f' % (self.decimals, float(v['tooldia']))))
  546. sorted_tools.sort(reverse=True)
  547. n = len(sorted_tools)
  548. self.tools_table.setRowCount(n)
  549. tool_id = 0
  550. for tool_sorted in sorted_tools:
  551. for tooluid_key, tooluid_value in self.tooltable_tools.items():
  552. if float('%.*f' % (self.decimals, tooluid_value['tooldia'])) == tool_sorted:
  553. tool_id += 1
  554. id_item = QtWidgets.QTableWidgetItem('%d' % int(tool_id))
  555. id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
  556. row_no = tool_id - 1
  557. self.tools_table.setItem(row_no, 0, id_item) # Tool name/id
  558. # Make sure that the drill diameter when in MM is with no more than 2 decimals
  559. # There are no drill bits in MM with more than 2 decimals diameter
  560. # For INCH the decimals should be no more than 4. There are no drills under 10mils
  561. dia = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, tooluid_value['tooldia']))
  562. dia.setFlags(QtCore.Qt.ItemIsEnabled)
  563. tool_uid_item = QtWidgets.QTableWidgetItem(str(int(tooluid_key)))
  564. self.tools_table.setItem(row_no, 1, dia) # Diameter
  565. self.tools_table.setItem(row_no, 2, tool_uid_item) # Tool unique ID
  566. # make the diameter column editable
  567. for row in range(tool_id):
  568. self.tools_table.item(row, 1).setFlags(
  569. QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
  570. # all the tools are selected by default
  571. self.tools_table.selectColumn(0)
  572. #
  573. self.tools_table.resizeColumnsToContents()
  574. self.tools_table.resizeRowsToContents()
  575. vertical_header = self.tools_table.verticalHeader()
  576. vertical_header.hide()
  577. self.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  578. horizontal_header = self.tools_table.horizontalHeader()
  579. horizontal_header.setMinimumSectionSize(10)
  580. horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
  581. horizontal_header.resizeSection(0, 20)
  582. horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
  583. # self.tools_table.setSortingEnabled(True)
  584. # sort by tool diameter
  585. # self.tools_table.sortItems(1)
  586. self.tools_table.setMinimumHeight(self.tools_table.getHeight())
  587. self.tools_table.setMaximumHeight(self.tools_table.getHeight())
  588. self.ui_connect()
  589. def update_ui(self, row=None):
  590. """
  591. Will update the UI form with the data from obj.tools
  592. :param row: the row (tool) from which to extract information's used to populate the form
  593. :return:
  594. """
  595. self.ui_disconnect()
  596. if row is None:
  597. try:
  598. current_row = self.tools_table.currentRow()
  599. except Exception:
  600. current_row = 0
  601. else:
  602. current_row = row
  603. if current_row < 0:
  604. current_row = 0
  605. # populate the form with the data from the tool associated with the row parameter
  606. try:
  607. tooluid = int(self.tools_table.item(current_row, 2).text())
  608. except Exception as e:
  609. log.debug("Tool missing. Add a tool in Tool Table. %s" % str(e))
  610. return
  611. # update the form
  612. try:
  613. # set the form with data from the newly selected tool
  614. for tooluid_key, tooluid_value in self.tooltable_tools.items():
  615. if int(tooluid_key) == tooluid:
  616. self.set_form(deepcopy(tooluid_value['data']))
  617. except Exception as e:
  618. log.debug("FlatCAMObj ---> update_ui() " + str(e))
  619. self.ui_connect()
  620. def on_row_selection_change(self):
  621. self.update_ui()
  622. def ui_connect(self):
  623. # on any change to the widgets that matter it will be called self.gui_form_to_storage which will save the
  624. # changes in geometry UI
  625. for i in range(self.gcode_form_layout.count()):
  626. if isinstance(self.gcode_form_layout.itemAt(i).widget(), FCComboBox):
  627. self.gcode_form_layout.itemAt(i).widget().currentIndexChanged.connect(self.read_form_to_tooldata)
  628. if isinstance(self.gcode_form_layout.itemAt(i).widget(), FCEntry):
  629. self.gcode_form_layout.itemAt(i).widget().editingFinished.connect(self.read_form_to_tooldata)
  630. self.tools_table.itemChanged.connect(self.on_tool_edit)
  631. self.tools_table.currentItemChanged.connect(self.on_row_selection_change)
  632. def ui_disconnect(self):
  633. # if connected, disconnect the signal from the slot on item_changed as it creates issues
  634. for i in range(self.gcode_form_layout.count()):
  635. if isinstance(self.gcode_form_layout.itemAt(i).widget(), FCComboBox):
  636. try:
  637. self.gcode_form_layout.itemAt(i).widget().currentIndexChanged.disconnect()
  638. except (TypeError, AttributeError):
  639. pass
  640. if isinstance(self.gcode_form_layout.itemAt(i).widget(), FCEntry):
  641. try:
  642. self.gcode_form_layout.itemAt(i).widget().editingFinished.disconnect()
  643. except (TypeError, AttributeError):
  644. pass
  645. try:
  646. self.tools_table.itemChanged.disconnect(self.on_tool_edit)
  647. except (TypeError, AttributeError):
  648. pass
  649. try:
  650. self.tools_table.currentItemChanged.disconnect(self.on_row_selection_change)
  651. except (TypeError, AttributeError):
  652. pass
  653. def update_comboboxes(self, obj, status):
  654. """
  655. Modify the current text of the comboboxes to show the last object
  656. that was created.
  657. :param obj: object that was changed and called this PyQt slot
  658. :param status: what kind of change happened: 'append' or 'delete'
  659. :return:
  660. """
  661. try:
  662. obj_name = obj.options['name']
  663. except AttributeError:
  664. # this happen when the 'delete all' is emitted since in that case the obj is set to None and None has no
  665. # attribute named 'options'
  666. return
  667. if status == 'append':
  668. idx = self.obj_combo.findText(obj_name)
  669. if idx != -1:
  670. self.obj_combo.setCurrentIndex(idx)
  671. idx = self.geo_obj_combo.findText(obj_name)
  672. if idx != -1:
  673. self.geo_obj_combo.setCurrentIndex(idx)
  674. idx = self.cnc_obj_combo.findText(obj_name)
  675. if idx != -1:
  676. self.cnc_obj_combo.setCurrentIndex(idx)
  677. def read_form_to_options(self):
  678. """
  679. Will read all the parameters from Solder Paste Tool UI and update the self.options dictionary
  680. :return:
  681. """
  682. for key in self.form_fields:
  683. self.options[key] = self.form_fields[key].get_value()
  684. def read_form_to_tooldata(self, tooluid=None):
  685. """
  686. Will read all the items in the UI form and set the self.tools data accordingly
  687. :param tooluid: the uid of the tool to be updated in the obj.tools
  688. :return:
  689. """
  690. current_row = self.tools_table.currentRow()
  691. uid = tooluid if tooluid else int(self.tools_table.item(current_row, 2).text())
  692. for key in self.form_fields:
  693. self.tooltable_tools[uid]['data'].update({
  694. key: self.form_fields[key].get_value()
  695. })
  696. def set_form_from_defaults(self):
  697. """
  698. Will read all the parameters of Solder Paste Tool from the app self.defaults and update the UI
  699. :return:
  700. """
  701. for key in self.form_fields:
  702. if key in self.app.defaults:
  703. self.form_fields[key].set_value(self.app.defaults[key])
  704. def set_form(self, val):
  705. """
  706. Will read all the parameters of Solder Paste Tool from the provided val parameter and update the UI
  707. :param val: dictionary with values to store in the form
  708. param_type: dictionary
  709. :return:
  710. """
  711. if not isinstance(val, dict):
  712. log.debug("ToolSoderPaste.set_form() --> parameter not a dict")
  713. return
  714. for key in self.form_fields:
  715. if key in val:
  716. self.form_fields[key].set_value(val[key])
  717. def on_tool_add(self, dia=None, muted=None):
  718. """
  719. Add a Tool in the Tool Table
  720. :param dia: diameter of the tool to be added
  721. :param muted: if True will not send status bar messages about adding tools
  722. :return:
  723. """
  724. self.ui_disconnect()
  725. if dia:
  726. tool_dia = dia
  727. else:
  728. try:
  729. tool_dia = float(self.addtool_entry.get_value())
  730. except ValueError:
  731. # try to convert comma to decimal point. if it's still not working error message and return
  732. try:
  733. tool_dia = float(self.addtool_entry.get_value().replace(',', '.'))
  734. except ValueError:
  735. self.app.inform.emit('[ERROR_NOTCL] %s' %
  736. _("Wrong value format entered, use a number."))
  737. return
  738. if tool_dia is None:
  739. self.build_ui()
  740. self.app.inform.emit('[WARNING_NOTCL] %s' %
  741. _("Please enter a tool diameter to add, in Float format."))
  742. return
  743. if tool_dia == 0:
  744. self.app.inform.emit('[WARNING_NOTCL] %s' %
  745. _("Please enter a tool diameter with non-zero value, in Float format."))
  746. return
  747. # construct a list of all 'tooluid' in the self.tooltable_tools
  748. tool_uid_list = []
  749. for tooluid_key in self.tooltable_tools:
  750. tool_uid_item = int(tooluid_key)
  751. tool_uid_list.append(tool_uid_item)
  752. # find maximum from the temp_uid, add 1 and this is the new 'tooluid'
  753. if not tool_uid_list:
  754. max_uid = 0
  755. else:
  756. max_uid = max(tool_uid_list)
  757. self.tooluid = int(max_uid + 1)
  758. tool_dias = []
  759. for k, v in self.tooltable_tools.items():
  760. for tool_v in v.keys():
  761. if tool_v == 'tooldia':
  762. tool_dias.append(float('%.*f' % (self.decimals, v[tool_v])))
  763. if float('%.*f' % (self.decimals, tool_dia)) in tool_dias:
  764. if muted is None:
  765. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. Tool already in Tool Table."))
  766. self.tools_table.itemChanged.connect(self.on_tool_edit)
  767. return
  768. else:
  769. if muted is None:
  770. self.app.inform.emit('[success] %s' % _("New Nozzle tool added to Tool Table."))
  771. self.tooltable_tools.update({
  772. int(self.tooluid): {
  773. 'tooldia': float('%.*f' % (self.decimals, tool_dia)),
  774. 'data': deepcopy(self.options),
  775. 'solid_geometry': []
  776. }
  777. })
  778. self.build_ui()
  779. def on_tool_edit(self):
  780. """
  781. Edit a tool in the Tool Table
  782. :return:
  783. """
  784. self.ui_disconnect()
  785. tool_dias = []
  786. for k, v in self.tooltable_tools.items():
  787. for tool_v in v.keys():
  788. if tool_v == 'tooldia':
  789. tool_dias.append(float('%.*f' % (self.decimals, v[tool_v])))
  790. for row in range(self.tools_table.rowCount()):
  791. try:
  792. new_tool_dia = float(self.tools_table.item(row, 1).text())
  793. except ValueError:
  794. # try to convert comma to decimal point. if it's still not working error message and return
  795. try:
  796. new_tool_dia = float(self.tools_table.item(row, 1).text().replace(',', '.'))
  797. except ValueError:
  798. self.app.inform.emit('[ERROR_NOTCL] %s' %
  799. _("Wrong value format entered, use a number."))
  800. return
  801. tooluid = int(self.tools_table.item(row, 2).text())
  802. # identify the tool that was edited and get it's tooluid
  803. if new_tool_dia not in tool_dias:
  804. self.tooltable_tools[tooluid]['tooldia'] = new_tool_dia
  805. self.app.inform.emit('[success] %s' %
  806. _("Nozzle tool from Tool Table was edited."))
  807. self.build_ui()
  808. return
  809. else:
  810. old_tool_dia = ''
  811. # identify the old tool_dia and restore the text in tool table
  812. for k, v in self.tooltable_tools.items():
  813. if k == tooluid:
  814. old_tool_dia = v['tooldia']
  815. break
  816. restore_dia_item = self.tools_table.item(row, 1)
  817. restore_dia_item.setText(str(old_tool_dia))
  818. self.app.inform.emit('[WARNING_NOTCL] %s' %
  819. _("Cancelled. New diameter value is already in the Tool Table."))
  820. self.build_ui()
  821. def on_tool_delete(self, rows_to_delete=None, all=None):
  822. """
  823. Will delete tool(s) in the Tool Table
  824. :param rows_to_delete: tell which row (tool) to delete
  825. :param all: to delete all tools at once
  826. :return:
  827. """
  828. self.ui_disconnect()
  829. deleted_tools_list = []
  830. if all:
  831. self.tooltable_tools.clear()
  832. self.build_ui()
  833. return
  834. if rows_to_delete:
  835. try:
  836. for row in rows_to_delete:
  837. tooluid_del = int(self.tools_table.item(row, 2).text())
  838. deleted_tools_list.append(tooluid_del)
  839. except TypeError:
  840. deleted_tools_list.append(rows_to_delete)
  841. for t in deleted_tools_list:
  842. self.tooltable_tools.pop(t, None)
  843. self.build_ui()
  844. return
  845. try:
  846. if self.tools_table.selectedItems():
  847. for row_sel in self.tools_table.selectedItems():
  848. row = row_sel.row()
  849. if row < 0:
  850. continue
  851. tooluid_del = int(self.tools_table.item(row, 2).text())
  852. deleted_tools_list.append(tooluid_del)
  853. for t in deleted_tools_list:
  854. self.tooltable_tools.pop(t, None)
  855. except AttributeError:
  856. self.app.inform.emit('[WARNING_NOTCL] %s' %
  857. _("Delete failed. Select a Nozzle tool to delete."))
  858. return
  859. except Exception as e:
  860. log.debug(str(e))
  861. self.app.inform.emit('[success] %s' %
  862. _("Nozzle tool(s) deleted from Tool Table."))
  863. self.build_ui()
  864. def on_rmb_combo(self, pos, combo):
  865. """
  866. Will create a context menu on the combobox items
  867. :param pos: mouse click position passed by the signal that called this slot
  868. :param combo: the actual combo from where the signal was triggered
  869. :return:
  870. """
  871. view = combo.view
  872. idx = view.indexAt(pos)
  873. if not idx.isValid():
  874. return
  875. self.obj_to_be_deleted_name = combo.model().itemData(idx)[0]
  876. menu = QtWidgets.QMenu()
  877. menu.addAction(self.combo_context_del_action)
  878. menu.exec(view.mapToGlobal(pos))
  879. def on_delete_object(self):
  880. """
  881. Slot for the 'delete' action triggered in the combobox context menu.
  882. The name of the object to be deleted is collected when the combobox context menu is created.
  883. :return:
  884. """
  885. if self.obj_to_be_deleted_name != '':
  886. self.app.collection.set_active(self.obj_to_be_deleted_name)
  887. self.app.collection.delete_active(select_project=False)
  888. self.obj_to_be_deleted_name = ''
  889. def on_geo_select(self):
  890. # if self.geo_obj_combo.currentText().rpartition('_')[2] == 'solderpaste':
  891. # self.gcode_frame.setDisabled(False)
  892. # else:
  893. # self.gcode_frame.setDisabled(True)
  894. pass
  895. def on_cncjob_select(self):
  896. # if self.cnc_obj_combo.currentText().rpartition('_')[2] == 'solderpaste':
  897. # self.save_gcode_frame.setDisabled(False)
  898. # else:
  899. # self.save_gcode_frame.setDisabled(True)
  900. pass
  901. def on_create_geo_click(self, signal):
  902. """
  903. Will create a solderpaste dispensing geometry.
  904. :param signal: passed by the signal that called this slot
  905. :return:
  906. """
  907. name = self.obj_combo.currentText()
  908. if name == '':
  909. self.app.inform.emit('[WARNING_NOTCL] %s' %
  910. _("No SolderPaste mask Gerber object loaded."))
  911. return
  912. obj = self.app.collection.get_by_name(name)
  913. # update the self.options
  914. self.read_form_to_options()
  915. self.on_create_geo(name=name, work_object=obj)
  916. def on_create_geo(self, name, work_object, use_thread=True):
  917. """
  918. The actual work for creating solderpaste dispensing geometry is done here.
  919. :param name: the outname for the resulting geometry object
  920. :param work_object: the source Gerber object from which the geometry is created
  921. :param use_thread: use thread, True or False
  922. :return: a Geometry type object
  923. """
  924. proc = self.app.proc_container.new(_("Creating Solder Paste dispensing geometry."))
  925. obj = work_object
  926. # Sort tools in descending order
  927. sorted_tools = []
  928. for k, v in self.tooltable_tools.items():
  929. # make sure that the tools diameter is more than zero and not zero
  930. if float(v['tooldia']) > 0:
  931. sorted_tools.append(float('%.*f' % (self.decimals, float(v['tooldia']))))
  932. sorted_tools.sort(reverse=True)
  933. if not sorted_tools:
  934. self.app.inform.emit('[WARNING_NOTCL] %s' %
  935. _("No Nozzle tools in the tool table."))
  936. return 'fail'
  937. def flatten(geometry=None, reset=True, pathonly=False):
  938. """
  939. Creates a list of non-iterable linear geometry objects.
  940. Polygons are expanded into its exterior pathonly param if specified.
  941. Results are placed in flat_geometry
  942. :param geometry: Shapely type or list or list of list of such.
  943. :param reset: Clears the contents of self.flat_geometry.
  944. :param pathonly: Expands polygons into linear elements from the exterior attribute.
  945. """
  946. if reset:
  947. self.flat_geometry = []
  948. # ## If iterable, expand recursively.
  949. try:
  950. for geo in geometry:
  951. if geo is not None:
  952. flatten(geometry=geo,
  953. reset=False,
  954. pathonly=pathonly)
  955. # ## Not iterable, do the actual indexing and add.
  956. except TypeError:
  957. if pathonly and type(geometry) == Polygon:
  958. self.flat_geometry.append(geometry.exterior)
  959. else:
  960. self.flat_geometry.append(geometry)
  961. return self.flat_geometry
  962. # TODO when/if the Gerber files will have solid_geometry in the self.apertures I will have to take care here
  963. flatten(geometry=obj.solid_geometry, pathonly=True)
  964. def geo_init(geo_obj, app_obj):
  965. geo_obj.options.update(self.options)
  966. geo_obj.solid_geometry = []
  967. geo_obj.tools = {}
  968. geo_obj.multigeo = True
  969. geo_obj.multitool = True
  970. geo_obj.special_group = 'solder_paste_tool'
  971. geo = LineString()
  972. work_geo = self.flat_geometry
  973. rest_geo = []
  974. tooluid = 1
  975. for tool in sorted_tools:
  976. offset = tool / 2
  977. for uid, vl in self.tooltable_tools.items():
  978. if float('%.*f' % (self.decimals, float(vl['tooldia']))) == tool:
  979. tooluid = int(uid)
  980. break
  981. geo_obj.tools[tooluid] = {}
  982. geo_obj.tools[tooluid]['tooldia'] = tool
  983. geo_obj.tools[tooluid]['data'] = deepcopy(self.tooltable_tools[tooluid]['data'])
  984. geo_obj.tools[tooluid]['solid_geometry'] = []
  985. geo_obj.tools[tooluid]['offset'] = 'Path'
  986. geo_obj.tools[tooluid]['offset_value'] = 0.0
  987. geo_obj.tools[tooluid]['type'] = 'SolderPaste'
  988. geo_obj.tools[tooluid]['tool_type'] = 'DN'
  989. # self.flat_geometry is a list of LinearRings produced by flatten() from the exteriors of the Polygons
  990. # We get possible issues if we try to directly use the Polygons, due of possible the interiors,
  991. # so we do a hack: get first the exterior in a form of LinearRings and then convert back to Polygon
  992. # because intersection does not work on LinearRings
  993. for g in work_geo:
  994. # for whatever reason intersection on LinearRings does not work so we convert back to Polygons
  995. poly = Polygon(g)
  996. x_min, y_min, x_max, y_max = poly.bounds
  997. diag_1_intersect = LineString([(x_min, y_min), (x_max, y_max)]).intersection(poly)
  998. diag_2_intersect = LineString([(x_min, y_max), (x_max, y_min)]).intersection(poly)
  999. if self.units == 'MM':
  1000. round_diag_1 = round(diag_1_intersect.length, 1)
  1001. round_diag_2 = round(diag_2_intersect.length, 1)
  1002. else:
  1003. round_diag_1 = round(diag_1_intersect.length, 2)
  1004. round_diag_2 = round(diag_2_intersect.length, 2)
  1005. if round_diag_1 == round_diag_2:
  1006. length = distance((x_min, y_min), (x_max, y_min))
  1007. h = distance((x_min, y_min), (x_min, y_max))
  1008. if offset >= length / 2 or offset >= h / 2:
  1009. pass
  1010. else:
  1011. if length > h:
  1012. h_half = h / 2
  1013. start = [x_min, (y_min + h_half)]
  1014. stop = [(x_min + length), (y_min + h_half)]
  1015. geo = LineString([start, stop])
  1016. else:
  1017. l_half = length / 2
  1018. start = [(x_min + l_half), y_min]
  1019. stop = [(x_min + l_half), (y_min + h)]
  1020. geo = LineString([start, stop])
  1021. elif round_diag_1 > round_diag_2:
  1022. geo = diag_1_intersect
  1023. else:
  1024. geo = diag_2_intersect
  1025. offseted_poly = poly.buffer(-offset)
  1026. geo = geo.intersection(offseted_poly)
  1027. if not geo.is_empty:
  1028. try:
  1029. geo_obj.tools[tooluid]['solid_geometry'].append(geo)
  1030. except Exception as e:
  1031. log.debug('ToolSolderPaste.on_create_geo() --> %s' % str(e))
  1032. else:
  1033. rest_geo.append(g)
  1034. work_geo = deepcopy(rest_geo)
  1035. rest_geo[:] = []
  1036. if not work_geo:
  1037. a = 0
  1038. for tooluid_key in geo_obj.tools:
  1039. if not geo_obj.tools[tooluid_key]['solid_geometry']:
  1040. a += 1
  1041. if a == len(geo_obj.tools):
  1042. self.app.inform.emit('[ERROR_NOTCL] %s' % _('Cancelled. Empty file, it has no geometry...'))
  1043. return 'fail'
  1044. app_obj.inform.emit('[success] %s...' % _("Solder Paste geometry generated successfully"))
  1045. return
  1046. # if we still have geometry not processed at the end of the tools then we failed
  1047. # some or all the pads are not covered with solder paste
  1048. if work_geo:
  1049. app_obj.inform.emit('[WARNING_NOTCL] %s' %
  1050. _("Some or all pads have no solder "
  1051. "due of inadequate nozzle diameters..."))
  1052. return 'fail'
  1053. if use_thread:
  1054. def job_thread(app_obj):
  1055. try:
  1056. app_obj.app_obj.new_object("geometry", name + "_solderpaste", geo_init)
  1057. except Exception as e:
  1058. log.error("SolderPaste.on_create_geo() --> %s" % str(e))
  1059. proc.done()
  1060. return
  1061. proc.done()
  1062. self.app.inform.emit(_("Generating Solder Paste dispensing geometry..."))
  1063. # Promise object with the new name
  1064. self.app.collection.promise(name)
  1065. # Background
  1066. self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
  1067. else:
  1068. self.app.app_obj.new_object("geometry", name + "_solderpaste", geo_init)
  1069. def on_create_gcode_click(self, signal):
  1070. """
  1071. Will create a CNCJob object from the solderpaste dispensing geometry.
  1072. :param signal: parameter passed by the signal that called this slot
  1073. :return:
  1074. """
  1075. name = self.geo_obj_combo.currentText()
  1076. obj = self.app.collection.get_by_name(name)
  1077. if name == '':
  1078. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Geometry object available."))
  1079. return 'fail'
  1080. if obj.special_group != 'solder_paste_tool':
  1081. self.app.inform.emit('[WARNING_NOTCL] %s' %
  1082. _("This Geometry can't be processed. "
  1083. "NOT a solder_paste_tool geometry."))
  1084. return 'fail'
  1085. a = 0
  1086. for tooluid_key in obj.tools:
  1087. if obj.tools[tooluid_key]['solid_geometry'] is None:
  1088. a += 1
  1089. if a == len(obj.tools):
  1090. self.app.inform.emit('[ERROR_NOTCL] %s...' % _('Cancelled. Empty file, it has no geometry'))
  1091. return 'fail'
  1092. # use the name of the first tool selected in self.geo_tools_table which has the diameter passed as tool_dia
  1093. originar_name = obj.options['name'].partition('_')[0]
  1094. outname = "%s_%s" % (originar_name, 'cnc_solderpaste')
  1095. self.on_create_gcode(name=outname, workobject=obj)
  1096. def on_create_gcode(self, name, workobject, use_thread=True):
  1097. """
  1098. Creates a multi-tool CNCJob. The actual work is done here.
  1099. :param name: outname for the resulting CNCJob object
  1100. :param workobject: the solderpaste dispensing Geometry object that is the source
  1101. :param use_thread: True if threaded execution is desired
  1102. :return:
  1103. """
  1104. obj = workobject
  1105. try:
  1106. xmin = obj.options['xmin']
  1107. ymin = obj.options['ymin']
  1108. xmax = obj.options['xmax']
  1109. ymax = obj.options['ymax']
  1110. except Exception as e:
  1111. log.debug("SolderPaste.on_create_gcode() --> %s\n" % str(e))
  1112. msg = '[ERROR] %s' % _("An internal error has ocurred. See shell.\n")
  1113. msg += 'SolderPaste.on_create_gcode() --> %s' % str(e)
  1114. msg += traceback.format_exc()
  1115. self.app.inform.emit(msg)
  1116. return
  1117. # Object initialization function for app.app_obj.new_object()
  1118. # RUNNING ON SEPARATE THREAD!
  1119. def job_init(job_obj):
  1120. assert job_obj.kind == 'cncjob', \
  1121. "Initializer expected a CNCJobObject, got %s" % type(job_obj)
  1122. # this turn on the FlatCAMCNCJob plot for multiple tools
  1123. job_obj.multitool = True
  1124. job_obj.multigeo = True
  1125. job_obj.cnc_tools.clear()
  1126. job_obj.special_group = 'solder_paste_tool'
  1127. job_obj.options['xmin'] = xmin
  1128. job_obj.options['ymin'] = ymin
  1129. job_obj.options['xmax'] = xmax
  1130. job_obj.options['ymax'] = ymax
  1131. for tooluid_key, tooluid_value in obj.tools.items():
  1132. # find the tool_dia associated with the tooluid_key
  1133. tool_dia = tooluid_value['tooldia']
  1134. tool_cnc_dict = deepcopy(tooluid_value)
  1135. job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"]
  1136. job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"]
  1137. job_obj.tool = int(tooluid_key)
  1138. # Propagate options
  1139. job_obj.options["tooldia"] = tool_dia
  1140. job_obj.options['tool_dia'] = tool_dia
  1141. # ## CREATE GCODE # ##
  1142. res = job_obj.generate_gcode_from_solderpaste_geo(**tooluid_value)
  1143. if res == 'fail':
  1144. log.debug("GeometryObject.mtool_gen_cncjob() --> generate_from_geometry2() failed")
  1145. return 'fail'
  1146. else:
  1147. tool_cnc_dict['gcode'] = res
  1148. # ## PARSE GCODE # ##
  1149. tool_cnc_dict['gcode_parsed'] = job_obj.gcode_parse()
  1150. # TODO this serve for bounding box creation only; should be optimized
  1151. tool_cnc_dict['solid_geometry'] = cascaded_union([geo['geom'] for geo in tool_cnc_dict['gcode_parsed']])
  1152. # tell gcode_parse from which point to start drawing the lines depending on what kind of
  1153. # object is the source of gcode
  1154. job_obj.toolchange_xy_type = "geometry"
  1155. job_obj.cnc_tools.update({
  1156. tooluid_key: deepcopy(tool_cnc_dict)
  1157. })
  1158. tool_cnc_dict.clear()
  1159. if use_thread:
  1160. # To be run in separate thread
  1161. def job_thread(app_obj):
  1162. with self.app.proc_container.new("Generating CNC Code"):
  1163. if app_obj.app_obj.new_object("cncjob", name, job_init) != 'fail':
  1164. app_obj.inform.emit('[success] [success] %s: %s' %
  1165. (_("ToolSolderPaste CNCjob created"), name))
  1166. # Create a promise with the name
  1167. self.app.collection.promise(name)
  1168. # Send to worker
  1169. self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
  1170. else:
  1171. self.app.app_obj.new_object("cncjob", name, job_init)
  1172. def on_view_gcode(self):
  1173. """
  1174. View GCode in the Editor Tab.
  1175. :return:
  1176. """
  1177. time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
  1178. self.text_editor_tab = TextEditor(app=self.app)
  1179. # add the tab if it was closed
  1180. self.app.ui.plot_tab_area.addTab(self.text_editor_tab, _("SP GCode Editor"))
  1181. self.text_editor_tab.setObjectName('solderpaste_gcode_editor_tab')
  1182. # Switch plot_area to CNCJob tab
  1183. self.app.ui.plot_tab_area.setCurrentWidget(self.text_editor_tab)
  1184. name = self.cnc_obj_combo.currentText()
  1185. obj = self.app.collection.get_by_name(name)
  1186. try:
  1187. if obj.special_group != 'solder_paste_tool':
  1188. self.app.inform.emit('[WARNING_NOTCL] %s' %
  1189. _("This CNCJob object can't be processed. "
  1190. "NOT a solder_paste_tool CNCJob object."))
  1191. return
  1192. except AttributeError:
  1193. self.app.inform.emit('[WARNING_NOTCL] %s' %
  1194. _("This CNCJob object can't be processed. "
  1195. "NOT a solder_paste_tool CNCJob object."))
  1196. return
  1197. gcode = '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \
  1198. (str(self.app.version), str(self.app.version_date)) + '\n'
  1199. gcode += '(Name: ' + str(name) + ')\n'
  1200. gcode += '(Type: ' + "G-code from " + str(obj.options['type']) + " for Solder Paste dispenser" + ')\n'
  1201. # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
  1202. # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
  1203. gcode += '(Units: ' + self.units.upper() + ')\n' + "\n"
  1204. gcode += '(Created on ' + time_str + ')\n' + '\n'
  1205. for tool in obj.cnc_tools:
  1206. gcode += obj.cnc_tools[tool]['gcode']
  1207. # then append the text from GCode to the text editor
  1208. try:
  1209. lines = StringIO(gcode)
  1210. except Exception as e:
  1211. log.debug("ToolSolderpaste.on_view_gcode() --> %s" % str(e))
  1212. self.app.inform.emit('[ERROR_NOTCL] %s...' %
  1213. _("No Gcode in the object"))
  1214. return
  1215. try:
  1216. for line in lines:
  1217. proc_line = str(line).strip('\n')
  1218. self.text_editor_tab.code_editor.append(proc_line)
  1219. except Exception as e:
  1220. log.debug('ToolSolderPaste.on_view_gcode() -->%s' % str(e))
  1221. self.app.inform.emit('[ERROR] %s --> %s' %
  1222. ('ToolSolderPaste.on_view_gcode()', str(e)))
  1223. return
  1224. self.text_editor_tab.code_editor.moveCursor(QtGui.QTextCursor.Start)
  1225. self.text_editor_tab.handleTextChanged()
  1226. # self.app.ui.show()
  1227. def on_save_gcode(self):
  1228. """
  1229. Save solderpaste dispensing GCode to a file on HDD.
  1230. :return:
  1231. """
  1232. time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
  1233. name = self.cnc_obj_combo.currentText()
  1234. obj = self.app.collection.get_by_name(name)
  1235. if obj.special_group != 'solder_paste_tool':
  1236. self.app.inform.emit('[WARNING_NOTCL] %s' %
  1237. _("This CNCJob object can't be processed. "
  1238. "NOT a solder_paste_tool CNCJob object."))
  1239. return
  1240. _filter_ = "G-Code Files (*.nc);;G-Code Files (*.txt);;G-Code Files (*.tap);;G-Code Files (*.cnc);;" \
  1241. "G-Code Files (*.g-code);;All Files (*.*);;G-Code Files (*.gcode);;G-Code Files (*.ngc)"
  1242. try:
  1243. dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name)
  1244. filename, _f = FCFileSaveDialog.get_saved_filename(
  1245. caption=_("Export GCode ..."),
  1246. directory=dir_file_to_save,
  1247. ext_filter=_filter_
  1248. )
  1249. except TypeError:
  1250. filename, _f = FCFileSaveDialog.get_saved_filename(
  1251. caption=_("Export Code ..."), ext_filter=_filter_)
  1252. if filename == '':
  1253. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export cancelled ..."))
  1254. return
  1255. gcode = '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \
  1256. (str(self.app.version), str(self.app.version_date)) + '\n'
  1257. gcode += '(Name: ' + str(name) + ')\n'
  1258. gcode += '(Type: ' + "G-code from " + str(obj.options['type']) + " for Solder Paste dispenser" + ')\n'
  1259. # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
  1260. # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
  1261. gcode += '(Units: ' + self.units.upper() + ')\n' + "\n"
  1262. gcode += '(Created on ' + time_str + ')\n' + '\n'
  1263. for tool in obj.cnc_tools:
  1264. gcode += obj.cnc_tools[tool]['gcode']
  1265. lines = StringIO(gcode)
  1266. # ## Write
  1267. if filename is not None:
  1268. try:
  1269. with open(filename, 'w') as f:
  1270. for line in lines:
  1271. f.write(line)
  1272. except FileNotFoundError:
  1273. self.app.inform.emit('[WARNING_NOTCL] %s' %
  1274. _("No such file or directory"))
  1275. return
  1276. except PermissionError:
  1277. self.app.inform.emit('[WARNING] %s' %
  1278. _("Permission denied, saving not possible.\n"
  1279. "Most likely another app is holding the file open and not accessible."))
  1280. return 'fail'
  1281. if self.app.defaults["global_open_style"] is False:
  1282. self.app.file_opened.emit("gcode", filename)
  1283. self.app.file_saved.emit("gcode", filename)
  1284. self.app.inform.emit('[success] %s: %s' %
  1285. (_("Solder paste dispenser GCode file saved to"), filename))
  1286. def reset_fields(self):
  1287. self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  1288. self.geo_obj_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
  1289. self.cnc_obj_combo.setRootModelIndex(self.app.collection.index(3, 0, QtCore.QModelIndex()))