ToolSolderPaste.py 52 KB

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