FlatCAMCNCJob.py 80 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://flatcam.org #
  4. # Author: Juan Pablo Caram (c) #
  5. # Date: 2/5/2014 #
  6. # MIT Licence #
  7. # ##########################################################
  8. # ##########################################################
  9. # File modified by: Marius Stanciu #
  10. # ##########################################################
  11. from copy import deepcopy
  12. from io import StringIO
  13. from datetime import datetime
  14. from appEditors.AppTextEditor import AppTextEditor
  15. from appObjects.FlatCAMObj import *
  16. from matplotlib.backend_bases import KeyEvent as mpl_key_event
  17. from camlib import CNCjob
  18. from shapely.ops import unary_union
  19. from shapely.geometry import Point
  20. try:
  21. from shapely.ops import voronoi_diagram
  22. except Exception:
  23. pass
  24. import os
  25. import sys
  26. import serial
  27. import glob
  28. import math
  29. import gettext
  30. import appTranslation as fcTranslate
  31. import builtins
  32. fcTranslate.apply_language('strings')
  33. if '_' not in builtins.__dict__:
  34. _ = gettext.gettext
  35. class CNCJobObject(FlatCAMObj, CNCjob):
  36. """
  37. Represents G-Code.
  38. """
  39. optionChanged = QtCore.pyqtSignal(str)
  40. build_al_table_sig = QtCore.pyqtSignal()
  41. ui_type = CNCObjectUI
  42. def __init__(self, name, units="in", kind="generic", z_move=0.1,
  43. feedrate=3.0, feedrate_rapid=3.0, z_cut=-0.002, tooldia=0.0,
  44. spindlespeed=None):
  45. log.debug("Creating CNCJob object...")
  46. self.decimals = self.app.decimals
  47. CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
  48. feedrate=feedrate, feedrate_rapid=feedrate_rapid, z_cut=z_cut, tooldia=tooldia,
  49. spindlespeed=spindlespeed, steps_per_circle=int(self.app.defaults["cncjob_steps_per_circle"]))
  50. FlatCAMObj.__init__(self, name)
  51. self.kind = "cncjob"
  52. self.options.update({
  53. "plot": True,
  54. "tooldia": 0.03937, # 0.4mm in inches
  55. "append": "",
  56. "prepend": "",
  57. "dwell": False,
  58. "dwelltime": 1,
  59. "type": 'Geometry',
  60. # "toolchange_macro": '',
  61. # "toolchange_macro_enable": False
  62. })
  63. '''
  64. This is a dict of dictionaries. Each dict is associated with a tool present in the file. The key is the
  65. diameter of the tools and the value is another dict that will hold the data under the following form:
  66. {tooldia: {
  67. 'tooluid': 1,
  68. 'offset': 'Path',
  69. 'type_item': 'Rough',
  70. 'tool_type': 'C1',
  71. 'data': {} # a dict to hold the parameters
  72. 'gcode': "" # a string with the actual GCODE
  73. 'gcode_parsed': {} # dictionary holding the CNCJob geometry and type of geometry
  74. (cut or move)
  75. 'solid_geometry': []
  76. },
  77. ...
  78. }
  79. It is populated in the GeometryObject.mtool_gen_cncjob()
  80. BEWARE: I rely on the ordered nature of the Python 3.7 dictionary. Things might change ...
  81. '''
  82. self.cnc_tools = {}
  83. '''
  84. This is a dict of dictionaries. Each dict is associated with a tool present in the file. The key is the
  85. diameter of the tools and the value is another dict that will hold the data under the following form:
  86. {tooldia: {
  87. 'tool': int,
  88. 'nr_drills': int,
  89. 'nr_slots': int,
  90. 'offset': float,
  91. 'data': {}, a dict to hold the parameters
  92. 'gcode': "", a string with the actual GCODE
  93. 'gcode_parsed': [], list of dicts holding the CNCJob geometry and
  94. type of geometry (cut or move)
  95. 'solid_geometry': [],
  96. },
  97. ...
  98. }
  99. It is populated in the ExcellonObject.on_create_cncjob_click() but actually
  100. it's done in camlib.CNCJob.generate_from_excellon_by_tool()
  101. BEWARE: I rely on the ordered nature of the Python 3.7 dictionary. Things might change ...
  102. '''
  103. self.exc_cnc_tools = {}
  104. # flag to store if the CNCJob is part of a special group of CNCJob objects that can't be processed by the
  105. # default engine of FlatCAM. They generated by some of tools and are special cases of CNCJob objects.
  106. self.special_group = None
  107. # for now it show if the plot will be done for multi-tool CNCJob (True) or for single tool
  108. # (like the one in the TCL Command), False
  109. self.multitool = False
  110. # determine if the GCode was generated out of a Excellon object or a Geometry object
  111. self.origin_kind = None
  112. self.coords_decimals = 4
  113. self.fr_decimals = 2
  114. # used for parsing the GCode lines to adjust the GCode when the GCode is offseted or scaled
  115. gcodex_re_string = r'(?=.*(X[-\+]?\d*\.\d*))'
  116. self.g_x_re = re.compile(gcodex_re_string)
  117. gcodey_re_string = r'(?=.*(Y[-\+]?\d*\.\d*))'
  118. self.g_y_re = re.compile(gcodey_re_string)
  119. gcodez_re_string = r'(?=.*(Z[-\+]?\d*\.\d*))'
  120. self.g_z_re = re.compile(gcodez_re_string)
  121. gcodef_re_string = r'(?=.*(F[-\+]?\d*\.\d*))'
  122. self.g_f_re = re.compile(gcodef_re_string)
  123. gcodet_re_string = r'(?=.*(\=\s*[-\+]?\d*\.\d*))'
  124. self.g_t_re = re.compile(gcodet_re_string)
  125. gcodenr_re_string = r'([+-]?\d*\.\d+)'
  126. self.g_nr_re = re.compile(gcodenr_re_string)
  127. if self.app.is_legacy is False:
  128. self.text_col = self.app.plotcanvas.new_text_collection()
  129. self.text_col.enabled = True
  130. self.annotation = self.app.plotcanvas.new_text_group(collection=self.text_col)
  131. self.gcode_editor_tab = None
  132. self.gcode_viewer_tab = None
  133. self.source_file = ''
  134. self.units_found = self.app.defaults['units']
  135. self.probing_gcode_text = ''
  136. # store the current selection shape status to be restored after manual adding test points
  137. self.old_selection_state = self.app.defaults['global_selection_shape']
  138. # if mouse is dragging set the object True
  139. self.mouse_is_dragging = False
  140. # if mouse events are bound to local methods
  141. self.mouse_events_connected = False
  142. # event handlers references
  143. self.kp = None
  144. self.mm = None
  145. self.mr = None
  146. self.append_snippet = ''
  147. self.prepend_snippet = ''
  148. self.gc_header = self.gcode_header()
  149. self.gc_start = ''
  150. self.gc_end = ''
  151. '''
  152. dictionary of dictionaries to store the informations for the autolevelling
  153. format:
  154. {
  155. id: {
  156. 'point': Shapely Point
  157. 'geo': Shapely Polygon from Voronoi diagram,
  158. 'height': float
  159. }
  160. }
  161. '''
  162. self.al_geometry_dict = {}
  163. self.grbl_ser_port = None
  164. # Attributes to be included in serialization
  165. # Always append to it because it carries contents
  166. # from predecessors.
  167. self.ser_attrs += [
  168. 'options', 'kind', 'origin_kind', 'cnc_tools', 'exc_cnc_tools', 'multitool', 'append_snippet',
  169. 'prepend_snippet', 'gc_header'
  170. ]
  171. def build_ui(self):
  172. self.ui_disconnect()
  173. FlatCAMObj.build_ui(self)
  174. self.units = self.app.defaults['units'].upper()
  175. # if the FlatCAM object is Excellon don't build the CNC Tools Table but hide it
  176. self.ui.cnc_tools_table.hide()
  177. if self.cnc_tools:
  178. self.ui.cnc_tools_table.show()
  179. self.build_cnc_tools_table()
  180. self.ui.exc_cnc_tools_table.hide()
  181. if self.exc_cnc_tools:
  182. self.ui.exc_cnc_tools_table.show()
  183. self.build_excellon_cnc_tools()
  184. if self.ui.sal_cb.get_value():
  185. self.build_al_table()
  186. self.ui_connect()
  187. def build_cnc_tools_table(self):
  188. tool_idx = 0
  189. n = len(self.cnc_tools)
  190. self.ui.cnc_tools_table.setRowCount(n)
  191. for dia_key, dia_value in self.cnc_tools.items():
  192. tool_idx += 1
  193. row_no = tool_idx - 1
  194. t_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
  195. # id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
  196. self.ui.cnc_tools_table.setItem(row_no, 0, t_id) # Tool name/id
  197. # Make sure that the tool diameter when in MM is with no more than 2 decimals.
  198. # There are no tool bits in MM with more than 2 decimals diameter.
  199. # For INCH the decimals should be no more than 4. There are no tools under 10mils.
  200. dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(dia_value['tooldia'])))
  201. offset_txt = list(str(dia_value['offset']))
  202. offset_txt[0] = offset_txt[0].upper()
  203. offset_item = QtWidgets.QTableWidgetItem(''.join(offset_txt))
  204. type_item = QtWidgets.QTableWidgetItem(str(dia_value['type']))
  205. tool_type_item = QtWidgets.QTableWidgetItem(str(dia_value['tool_type']))
  206. t_id.setFlags(QtCore.Qt.ItemIsEnabled)
  207. dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
  208. offset_item.setFlags(QtCore.Qt.ItemIsEnabled)
  209. type_item.setFlags(QtCore.Qt.ItemIsEnabled)
  210. tool_type_item.setFlags(QtCore.Qt.ItemIsEnabled)
  211. # hack so the checkbox stay centered in the table cell
  212. # used this:
  213. # https://stackoverflow.com/questions/32458111/pyqt-allign-checkbox-and-put-it-in-every-row
  214. # plot_item = QtWidgets.QWidget()
  215. # checkbox = FCCheckBox()
  216. # checkbox.setCheckState(QtCore.Qt.Checked)
  217. # qhboxlayout = QtWidgets.QHBoxLayout(plot_item)
  218. # qhboxlayout.addWidget(checkbox)
  219. # qhboxlayout.setAlignment(QtCore.Qt.AlignCenter)
  220. # qhboxlayout.setContentsMargins(0, 0, 0, 0)
  221. plot_item = FCCheckBox()
  222. plot_item.setLayoutDirection(QtCore.Qt.RightToLeft)
  223. tool_uid_item = QtWidgets.QTableWidgetItem(str(dia_key))
  224. if self.ui.plot_cb.isChecked():
  225. plot_item.setChecked(True)
  226. self.ui.cnc_tools_table.setItem(row_no, 1, dia_item) # Diameter
  227. self.ui.cnc_tools_table.setItem(row_no, 2, offset_item) # Offset
  228. self.ui.cnc_tools_table.setItem(row_no, 3, type_item) # Toolpath Type
  229. self.ui.cnc_tools_table.setItem(row_no, 4, tool_type_item) # Tool Type
  230. # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY # ##
  231. self.ui.cnc_tools_table.setItem(row_no, 5, tool_uid_item) # Tool unique ID)
  232. self.ui.cnc_tools_table.setCellWidget(row_no, 6, plot_item)
  233. # make the diameter column editable
  234. # for row in range(tool_idx):
  235. # self.ui.cnc_tools_table.item(row, 1).setFlags(QtCore.Qt.ItemIsSelectable |
  236. # QtCore.Qt.ItemIsEnabled)
  237. for row in range(tool_idx):
  238. self.ui.cnc_tools_table.item(row, 0).setFlags(
  239. self.ui.cnc_tools_table.item(row, 0).flags() ^ QtCore.Qt.ItemIsSelectable)
  240. self.ui.cnc_tools_table.resizeColumnsToContents()
  241. self.ui.cnc_tools_table.resizeRowsToContents()
  242. vertical_header = self.ui.cnc_tools_table.verticalHeader()
  243. # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
  244. vertical_header.hide()
  245. self.ui.cnc_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  246. horizontal_header = self.ui.cnc_tools_table.horizontalHeader()
  247. horizontal_header.setMinimumSectionSize(10)
  248. horizontal_header.setDefaultSectionSize(70)
  249. horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
  250. horizontal_header.resizeSection(0, 20)
  251. horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
  252. horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
  253. horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed)
  254. horizontal_header.resizeSection(4, 40)
  255. horizontal_header.setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed)
  256. horizontal_header.resizeSection(4, 17)
  257. # horizontal_header.setStretchLastSection(True)
  258. self.ui.cnc_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  259. self.ui.cnc_tools_table.setColumnWidth(0, 20)
  260. self.ui.cnc_tools_table.setColumnWidth(4, 40)
  261. self.ui.cnc_tools_table.setColumnWidth(6, 17)
  262. # self.ui.geo_tools_table.setSortingEnabled(True)
  263. self.ui.cnc_tools_table.setMinimumHeight(self.ui.cnc_tools_table.getHeight())
  264. self.ui.cnc_tools_table.setMaximumHeight(self.ui.cnc_tools_table.getHeight())
  265. def build_excellon_cnc_tools(self):
  266. tool_idx = 0
  267. n = len(self.exc_cnc_tools)
  268. self.ui.exc_cnc_tools_table.setRowCount(n)
  269. for tooldia_key, dia_value in self.exc_cnc_tools.items():
  270. tool_idx += 1
  271. row_no = tool_idx - 1
  272. t_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
  273. dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(tooldia_key)))
  274. nr_drills_item = QtWidgets.QTableWidgetItem('%d' % int(dia_value['nr_drills']))
  275. nr_slots_item = QtWidgets.QTableWidgetItem('%d' % int(dia_value['nr_slots']))
  276. cutz_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(dia_value['offset']) + self.z_cut))
  277. t_id.setFlags(QtCore.Qt.ItemIsEnabled)
  278. dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
  279. nr_drills_item.setFlags(QtCore.Qt.ItemIsEnabled)
  280. nr_slots_item.setFlags(QtCore.Qt.ItemIsEnabled)
  281. cutz_item.setFlags(QtCore.Qt.ItemIsEnabled)
  282. # hack so the checkbox stay centered in the table cell
  283. # used this:
  284. # https://stackoverflow.com/questions/32458111/pyqt-allign-checkbox-and-put-it-in-every-row
  285. # plot_item = QtWidgets.QWidget()
  286. # checkbox = FCCheckBox()
  287. # checkbox.setCheckState(QtCore.Qt.Checked)
  288. # qhboxlayout = QtWidgets.QHBoxLayout(plot_item)
  289. # qhboxlayout.addWidget(checkbox)
  290. # qhboxlayout.setAlignment(QtCore.Qt.AlignCenter)
  291. # qhboxlayout.setContentsMargins(0, 0, 0, 0)
  292. plot_item = FCCheckBox()
  293. plot_item.setLayoutDirection(QtCore.Qt.RightToLeft)
  294. tool_uid_item = QtWidgets.QTableWidgetItem(str(dia_value['tool']))
  295. if self.ui.plot_cb.isChecked():
  296. plot_item.setChecked(True)
  297. self.ui.exc_cnc_tools_table.setItem(row_no, 0, t_id) # Tool name/id
  298. self.ui.exc_cnc_tools_table.setItem(row_no, 1, dia_item) # Diameter
  299. self.ui.exc_cnc_tools_table.setItem(row_no, 2, nr_drills_item) # Nr of drills
  300. self.ui.exc_cnc_tools_table.setItem(row_no, 3, nr_slots_item) # Nr of slots
  301. # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY # ##
  302. self.ui.exc_cnc_tools_table.setItem(row_no, 4, tool_uid_item) # Tool unique ID)
  303. self.ui.exc_cnc_tools_table.setItem(row_no, 5, cutz_item)
  304. self.ui.exc_cnc_tools_table.setCellWidget(row_no, 6, plot_item)
  305. for row in range(tool_idx):
  306. self.ui.exc_cnc_tools_table.item(row, 0).setFlags(
  307. self.ui.exc_cnc_tools_table.item(row, 0).flags() ^ QtCore.Qt.ItemIsSelectable)
  308. self.ui.exc_cnc_tools_table.resizeColumnsToContents()
  309. self.ui.exc_cnc_tools_table.resizeRowsToContents()
  310. vertical_header = self.ui.exc_cnc_tools_table.verticalHeader()
  311. vertical_header.hide()
  312. self.ui.exc_cnc_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  313. horizontal_header = self.ui.exc_cnc_tools_table.horizontalHeader()
  314. horizontal_header.setMinimumSectionSize(10)
  315. horizontal_header.setDefaultSectionSize(70)
  316. horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
  317. horizontal_header.resizeSection(0, 20)
  318. horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
  319. horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
  320. horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
  321. horizontal_header.setSectionResizeMode(5, QtWidgets.QHeaderView.ResizeToContents)
  322. horizontal_header.setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed)
  323. # horizontal_header.setStretchLastSection(True)
  324. self.ui.exc_cnc_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  325. self.ui.exc_cnc_tools_table.setColumnWidth(0, 20)
  326. self.ui.exc_cnc_tools_table.setColumnWidth(6, 17)
  327. self.ui.exc_cnc_tools_table.setMinimumHeight(self.ui.exc_cnc_tools_table.getHeight())
  328. self.ui.exc_cnc_tools_table.setMaximumHeight(self.ui.exc_cnc_tools_table.getHeight())
  329. def build_al_table(self):
  330. tool_idx = 0
  331. n = len(self.al_geometry_dict)
  332. self.ui.al_probe_points_table.setRowCount(n)
  333. for id_key, value in self.al_geometry_dict.items():
  334. tool_idx += 1
  335. row_no = tool_idx - 1
  336. t_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
  337. x = value['point'].x
  338. y = value['point'].y
  339. xy_coords = self.app.dec_format(x, dec=self.app.decimals), self.app.dec_format(y, dec=self.app.decimals)
  340. coords_item = QtWidgets.QTableWidgetItem(str(xy_coords))
  341. height = self.app.dec_format(value['height'], dec=self.app.decimals)
  342. height_item = QtWidgets.QTableWidgetItem(str(height))
  343. t_id.setFlags(QtCore.Qt.ItemIsEnabled)
  344. coords_item.setFlags(QtCore.Qt.ItemIsEnabled)
  345. height_item.setFlags(QtCore.Qt.ItemIsEnabled)
  346. self.ui.al_probe_points_table.setItem(row_no, 0, t_id) # Tool name/id
  347. self.ui.al_probe_points_table.setItem(row_no, 1, coords_item) # X-Y coords
  348. self.ui.al_probe_points_table.setItem(row_no, 2, height_item) # Determined Height
  349. self.ui.al_probe_points_table.resizeColumnsToContents()
  350. self.ui.al_probe_points_table.resizeRowsToContents()
  351. h_header = self.ui.al_probe_points_table.horizontalHeader()
  352. h_header.setMinimumSectionSize(10)
  353. h_header.setDefaultSectionSize(70)
  354. h_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
  355. h_header.resizeSection(0, 20)
  356. h_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
  357. h_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
  358. self.ui.al_probe_points_table.setMinimumHeight(self.ui.al_probe_points_table.getHeight())
  359. self.ui.al_probe_points_table.setMaximumHeight(self.ui.al_probe_points_table.getHeight())
  360. if self.ui.al_probe_points_table.model().rowCount():
  361. self.ui.voronoi_cb.setDisabled(False)
  362. self.ui.grbl_get_heightmap_button.setDisabled(False)
  363. self.ui.h_gcode_button.setDisabled(False)
  364. self.ui.view_h_gcode_button.setDisabled(False)
  365. else:
  366. self.ui.voronoi_cb.setDisabled(True)
  367. self.ui.grbl_get_heightmap_button.setDisabled(True)
  368. self.ui.h_gcode_button.setDisabled(True)
  369. self.ui.view_h_gcode_button.setDisabled(True)
  370. def set_ui(self, ui):
  371. FlatCAMObj.set_ui(self, ui)
  372. log.debug("FlatCAMCNCJob.set_ui()")
  373. assert isinstance(self.ui, CNCObjectUI), \
  374. "Expected a CNCObjectUI, got %s" % type(self.ui)
  375. self.units = self.app.defaults['units'].upper()
  376. self.units_found = self.app.defaults['units']
  377. # this signal has to be connected to it's slot before the defaults are populated
  378. # the decision done in the slot has to override the default value set below
  379. # self.ui.toolchange_cb.toggled.connect(self.on_toolchange_custom_clicked)
  380. self.form_fields.update({
  381. "plot": self.ui.plot_cb,
  382. "tooldia": self.ui.tooldia_entry,
  383. # "append": self.ui.append_text,
  384. # "prepend": self.ui.prepend_text,
  385. # "toolchange_macro": self.ui.toolchange_text,
  386. # "toolchange_macro_enable": self.ui.toolchange_cb,
  387. "al_travelz": self.ui.ptravelz_entry,
  388. "al_probe_depth": self.ui.pdepth_entry,
  389. "al_probe_fr": self.ui.feedrate_probe_entry,
  390. "al_controller": self.ui.al_controller_combo,
  391. "al_mode": self.ui.al_mode_radio,
  392. "al_rows": self.ui.al_rows_entry,
  393. "al_columns": self.ui.al_columns_entry,
  394. })
  395. self.append_snippet = self.app.defaults['cncjob_append']
  396. self.prepend_snippet = self.app.defaults['cncjob_prepend']
  397. if self.append_snippet != '' or self.prepend_snippet:
  398. self.ui.snippets_cb.set_value(True)
  399. # Fill form fields only on object create
  400. self.to_form()
  401. # this means that the object that created this CNCJob was an Excellon or Geometry
  402. try:
  403. if self.travel_distance:
  404. self.ui.t_distance_label.show()
  405. self.ui.t_distance_entry.setVisible(True)
  406. self.ui.t_distance_entry.setDisabled(True)
  407. self.ui.t_distance_entry.set_value('%.*f' % (self.decimals, float(self.travel_distance)))
  408. self.ui.units_label.setText(str(self.units).lower())
  409. self.ui.units_label.setDisabled(True)
  410. self.ui.t_time_label.show()
  411. self.ui.t_time_entry.setVisible(True)
  412. self.ui.t_time_entry.setDisabled(True)
  413. # if time is more than 1 then we have minutes, else we have seconds
  414. if self.routing_time > 1:
  415. self.ui.t_time_entry.set_value('%.*f' % (self.decimals, math.ceil(float(self.routing_time))))
  416. self.ui.units_time_label.setText('min')
  417. else:
  418. time_r = self.routing_time * 60
  419. self.ui.t_time_entry.set_value('%.*f' % (self.decimals, math.ceil(float(time_r))))
  420. self.ui.units_time_label.setText('sec')
  421. self.ui.units_time_label.setDisabled(True)
  422. except AttributeError:
  423. pass
  424. if self.multitool is False:
  425. self.ui.tooldia_entry.show()
  426. self.ui.updateplot_button.show()
  427. else:
  428. self.ui.tooldia_entry.hide()
  429. self.ui.updateplot_button.hide()
  430. # set the kind of geometries are plotted by default with plot2() from camlib.CNCJob
  431. self.ui.cncplot_method_combo.set_value(self.app.defaults["cncjob_plot_kind"])
  432. try:
  433. self.ui.annotation_cb.stateChanged.disconnect(self.on_annotation_change)
  434. except (TypeError, AttributeError):
  435. pass
  436. self.ui.annotation_cb.stateChanged.connect(self.on_annotation_change)
  437. # set if to display text annotations
  438. self.ui.annotation_cb.set_value(self.app.defaults["cncjob_annotation"])
  439. # Show/Hide Advanced Options
  440. if self.app.defaults["global_app_level"] == 'b':
  441. self.ui.level.setText(_(
  442. '<span style="color:green;"><b>Basic</b></span>'
  443. ))
  444. self.ui.sal_cb.hide()
  445. else:
  446. self.ui.level.setText(_(
  447. '<span style="color:red;"><b>Advanced</b></span>'
  448. ))
  449. self.ui.sal_cb.show()
  450. self.ui.updateplot_button.clicked.connect(self.on_updateplot_button_click)
  451. self.ui.export_gcode_button.clicked.connect(self.on_exportgcode_button_click)
  452. self.ui.review_gcode_button.clicked.connect(self.on_edit_code_click)
  453. self.ui.editor_button.clicked.connect(lambda: self.app.object2editor())
  454. # autolevelling signals
  455. self.ui.al_mode_radio.activated_custom.connect(self.on_mode_radio)
  456. self.ui.al_controller_combo.currentIndexChanged.connect(self.on_controller_change)
  457. self.ui.com_search_button.clicked.connect(self.on_search_ports)
  458. self.ui.add_bd_button.clicked.connect(self.on_add_baudrate_grbl)
  459. self.ui.del_bd_button.clicked.connect(self.on_delete_baudrate_grbl)
  460. self.ui.com_connect_button.clicked.connect(self.on_connect_grbl)
  461. self.ui.view_h_gcode_button.clicked.connect(self.on_view_probing_gcode)
  462. self.ui.h_gcode_button.clicked.connect(self.on_generate_probing_gcode)
  463. self.ui.import_heights_button.clicked.connect(self.on_import_height_map)
  464. self.build_al_table_sig.connect(self.build_al_table)
  465. # self.ui.tc_variable_combo.currentIndexChanged[str].connect(self.on_cnc_custom_parameters)
  466. self.ui.cncplot_method_combo.activated_custom.connect(self.on_plot_kind_change)
  467. preamble = self.append_snippet
  468. postamble = self.prepend_snippet
  469. gc = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True)
  470. self.source_file = gc.getvalue()
  471. self.ui.al_mode_radio.set_value(self.options['al_mode'])
  472. self.on_controller_change()
  473. # def on_cnc_custom_parameters(self, signal_text):
  474. # if signal_text == 'Parameters':
  475. # return
  476. # else:
  477. # self.ui.toolchange_text.insertPlainText('%%%s%%' % signal_text)
  478. def ui_connect(self):
  479. for row in range(self.ui.cnc_tools_table.rowCount()):
  480. self.ui.cnc_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table)
  481. for row in range(self.ui.exc_cnc_tools_table.rowCount()):
  482. self.ui.exc_cnc_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table)
  483. self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
  484. self.ui.al_add_button.clicked.connect(self.on_add_al_probepoints)
  485. self.ui.show_al_table.stateChanged.connect(self.on_show_al_table)
  486. def ui_disconnect(self):
  487. for row in range(self.ui.cnc_tools_table.rowCount()):
  488. try:
  489. self.ui.cnc_tools_table.cellWidget(row, 6).clicked.disconnect(self.on_plot_cb_click_table)
  490. except (TypeError, AttributeError):
  491. pass
  492. for row in range(self.ui.exc_cnc_tools_table.rowCount()):
  493. try:
  494. self.ui.exc_cnc_tools_table.cellWidget(row, 6).clicked.disconnect(self.on_plot_cb_click_table)
  495. except (TypeError, AttributeError):
  496. pass
  497. try:
  498. self.ui.plot_cb.stateChanged.disconnect(self.on_plot_cb_click)
  499. except (TypeError, AttributeError):
  500. pass
  501. try:
  502. self.ui.al_add_button.clicked.disconnect()
  503. except (TypeError, AttributeError):
  504. pass
  505. try:
  506. self.ui.show_al_table.stateChanged.disconnect()
  507. except (TypeError, AttributeError):
  508. pass
  509. def on_add_al_probepoints(self):
  510. # create the solid_geo
  511. solid_geo = [geo['geom'] for geo in self.gcode_parsed if geo['kind'][0] == 'C']
  512. solid_geo = unary_union(solid_geo)
  513. # reset al table
  514. self.ui.al_probe_points_table.setRowCount(0)
  515. # reset the al dict
  516. self.al_geometry_dict.clear()
  517. xmin, ymin, xmax, ymax = solid_geo.bounds
  518. if self.ui.al_mode_radio.get_value() == 'grid':
  519. width = abs(xmax - xmin)
  520. height = abs(ymax - ymin)
  521. cols = self.ui.al_columns_entry.get_value()
  522. rows = self.ui.al_rows_entry.get_value()
  523. dx = width / (cols + 1)
  524. dy = height / (rows + 1)
  525. points = []
  526. new_y = ymin
  527. for x in range(rows):
  528. new_y += dy
  529. new_x = xmin
  530. for x in range(cols):
  531. new_x += dx
  532. points.append((new_x, new_y))
  533. pt_id = 0
  534. for point in points:
  535. pt_id += 1
  536. new_dict = {
  537. 'point': Point(point),
  538. 'geo': None,
  539. 'height': 0.0
  540. }
  541. self.al_geometry_dict[pt_id] = deepcopy(new_dict)
  542. else:
  543. self.app.inform.emit(_("Click on canvas to add a Test Point..."))
  544. if self.app.is_legacy is False:
  545. self.app.plotcanvas.graph_event_disconnect('key_press', self.app.ui.keyPressEvent)
  546. self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
  547. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
  548. else:
  549. self.app.plotcanvas.graph_event_disconnect(self.app.kp)
  550. self.app.plotcanvas.graph_event_disconnect(self.app.mp)
  551. self.app.plotcanvas.graph_event_disconnect(self.app.mr)
  552. self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
  553. self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
  554. self.mouse_events_connected = True
  555. # self.calculate_voronoi_diagram()
  556. self.build_al_table_sig.emit()
  557. # def calculate_voronoi_diagram(self):
  558. # return voronoi_diagram()
  559. # To be called after clicking on the plot.
  560. def on_mouse_click_release(self, event):
  561. if self.app.is_legacy is False:
  562. event_pos = event.pos
  563. # event_is_dragging = event.is_dragging
  564. right_button = 2
  565. else:
  566. event_pos = (event.xdata, event.ydata)
  567. # event_is_dragging = self.app.plotcanvas.is_dragging
  568. right_button = 3
  569. try:
  570. x = float(event_pos[0])
  571. y = float(event_pos[1])
  572. except TypeError:
  573. return
  574. event_pos = (x, y)
  575. # do paint single only for left mouse clicks
  576. if event.button == 1:
  577. pos = self.app.plotcanvas.translate_coords(event_pos)
  578. # use the snapped position as reference
  579. snapped_pos = self.app.geo_editor.snap(pos[0], pos[1])
  580. if not self.al_geometry_dict:
  581. new_dict = {
  582. 'point': Point(snapped_pos),
  583. 'geo': None,
  584. 'height': 0.0
  585. }
  586. self.al_geometry_dict[1] = deepcopy(new_dict)
  587. else:
  588. int_keys = [int(k) for k in self.al_geometry_dict.keys()]
  589. new_id = max(int_keys) + 1
  590. new_dict = {
  591. 'point': Point(snapped_pos),
  592. 'geo': None,
  593. 'height': 0.0
  594. }
  595. self.al_geometry_dict[new_id] = deepcopy(new_dict)
  596. # rebuild the al table
  597. self.build_al_table_sig.emit()
  598. self.app.inform.emit(_("Added Test Point... Click again to add another or right click to finish ..."))
  599. # if RMB then we exit
  600. elif event.button == right_button and self.mouse_is_dragging is False:
  601. if self.app.is_legacy is False:
  602. self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
  603. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
  604. else:
  605. self.app.plotcanvas.graph_event_disconnect(self.kp)
  606. self.app.plotcanvas.graph_event_disconnect(self.mr)
  607. self.app.kp = self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent)
  608. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot)
  609. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  610. self.app.on_mouse_click_release_over_plot)
  611. # signal that the mouse events are disconnected from local methods
  612. self.mouse_events_connected = False
  613. # restore selection
  614. self.app.defaults['global_selection_shape'] = self.old_selection_state
  615. self.app.inform.emit(_("Finished manual adding of Test Point..."))
  616. # rebuild the al table
  617. self.build_al_table_sig.emit()
  618. def on_key_press(self, event):
  619. # events out of the self.app.collection view (it's about Project Tab) are of type int
  620. if type(event) is int:
  621. key = event
  622. # events from the GUI are of type QKeyEvent
  623. elif type(event) == QtGui.QKeyEvent:
  624. key = event.key()
  625. elif isinstance(event, mpl_key_event): # MatPlotLib key events are trickier to interpret than the rest
  626. key = event.key
  627. key = QtGui.QKeySequence(key)
  628. # check for modifiers
  629. key_string = key.toString().lower()
  630. if '+' in key_string:
  631. mod, __, key_text = key_string.rpartition('+')
  632. if mod.lower() == 'ctrl':
  633. # modifiers = QtCore.Qt.ControlModifier
  634. pass
  635. elif mod.lower() == 'alt':
  636. # modifiers = QtCore.Qt.AltModifier
  637. pass
  638. elif mod.lower() == 'shift':
  639. # modifiers = QtCore.Qt.ShiftModifier
  640. pass
  641. else:
  642. # modifiers = QtCore.Qt.NoModifier
  643. pass
  644. key = QtGui.QKeySequence(key_text)
  645. # events from Vispy are of type KeyEvent
  646. else:
  647. key = event.key
  648. # Escape = Deselect All
  649. if key == QtCore.Qt.Key_Escape or key == 'Escape':
  650. if self.mouse_events_connected is True:
  651. self.mouse_events_connected = False
  652. if self.app.is_legacy is False:
  653. self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
  654. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
  655. else:
  656. self.app.plotcanvas.graph_event_disconnect(self.kp)
  657. self.app.plotcanvas.graph_event_disconnect(self.mr)
  658. self.app.kp = self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent)
  659. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot)
  660. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  661. self.app.on_mouse_click_release_over_plot)
  662. if self.ui.big_cursor_cb.get_value():
  663. # restore cursor
  664. self.app.on_cursor_type(val=self.old_cursor_type)
  665. # restore selection
  666. self.app.defaults['global_selection_shape'] = self.old_selection_state
  667. # Grid toggle
  668. if key == QtCore.Qt.Key_G or key == 'G':
  669. self.app.ui.grid_snap_btn.trigger()
  670. # Jump to coords
  671. if key == QtCore.Qt.Key_J or key == 'J':
  672. l_x, l_y = self.app.on_jump_to()
  673. def on_show_al_table(self, state):
  674. self.ui.al_probe_points_table.show() if state else self.ui.al_probe_points_table.hide()
  675. def on_mode_radio(self, val):
  676. # reset al table
  677. self.ui.al_probe_points_table.setRowCount(0)
  678. # reset the al dict
  679. self.al_geometry_dict.clear()
  680. # build AL table
  681. self.build_al_table()
  682. if val == "manual":
  683. self.ui.al_rows_entry.setDisabled(True)
  684. self.ui.al_rows_label.setDisabled(True)
  685. self.ui.al_columns_entry.setDisabled(True)
  686. self.ui.al_columns_label.setDisabled(True)
  687. else:
  688. self.ui.al_rows_entry.setDisabled(False)
  689. self.ui.al_rows_label.setDisabled(False)
  690. self.ui.al_columns_entry.setDisabled(False)
  691. self.ui.al_columns_label.setDisabled(False)
  692. def on_controller_change(self):
  693. if self.ui.al_controller_combo.get_value() == 'GRBL':
  694. self.ui.h_gcode_button.hide()
  695. self.ui.view_h_gcode_button.hide()
  696. self.ui.import_heights_button.hide()
  697. self.ui.grbl_frame.show()
  698. self.on_search_ports(muted=True)
  699. else:
  700. self.ui.h_gcode_button.show()
  701. self.ui.view_h_gcode_button.show()
  702. self.ui.import_heights_button.show()
  703. self.ui.grbl_frame.hide()
  704. def list_serial_ports(self):
  705. """
  706. Lists serial port names.
  707. From here: https://stackoverflow.com/questions/12090503/listing-available-com-ports-with-python
  708. :raises EnvironmentError: On unsupported or unknown platforms
  709. :returns: A list of the serial ports available on the system
  710. """
  711. if sys.platform.startswith('win'):
  712. ports = ['COM%s' % (i + 1) for i in range(256)]
  713. elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'):
  714. # this excludes your current terminal "/dev/tty"
  715. ports = glob.glob('/dev/tty[A-Za-z]*')
  716. elif sys.platform.startswith('darwin'):
  717. ports = glob.glob('/dev/tty.*')
  718. else:
  719. raise EnvironmentError('Unsupported platform')
  720. result = []
  721. s = serial.Serial()
  722. for port in ports:
  723. s.port = port
  724. try:
  725. s.open()
  726. s.close()
  727. result.append(port)
  728. except (OSError, serial.SerialException):
  729. # result.append(port + " (in use)")
  730. pass
  731. return result
  732. def on_search_ports(self, muted=None):
  733. port_list = self.list_serial_ports()
  734. self.ui.com_list_combo.clear()
  735. self.ui.com_list_combo.addItems(port_list)
  736. if muted is not True:
  737. self.app.inform.emit('[WARNING_NOTCL] %s' % _("COM list updated ..."))
  738. def on_connect_grbl(self):
  739. port_name = self.ui.com_list_combo.currentText()
  740. if " (" in port_name:
  741. port_name = port_name.rpartition(" (")[0]
  742. baudrate = int(self.ui.baudrates_list_combo.currentText())
  743. try:
  744. self.grbl_ser_port = serial.serial_for_url(port_name, baudrate,
  745. bytesize=serial.EIGHTBITS,
  746. parity=serial.PARITY_NONE,
  747. stopbits=serial.STOPBITS_ONE,
  748. timeout=0.1,
  749. xonxoff=False,
  750. rtscts=False)
  751. self.app.inform.emit("%s: %s" % (_("Port connected"), port_name))
  752. self.ui.com_connect_button.setStyleSheet("QPushButton {color: seagreen;}")
  753. # Toggle DTR to reset the controller loaded with GRBL (Arduino, ESP32, etc)
  754. try:
  755. self.grbl_ser_port.dtr = False
  756. except IOError:
  757. pass
  758. self.grbl_ser_port.reset_input_buffer()
  759. try:
  760. self.grbl_ser_port.dtr = True
  761. except IOError:
  762. pass
  763. except serial.SerialException:
  764. self.grbl_ser_port = serial.Serial()
  765. self.grbl_ser_port.port = port_name
  766. self.grbl_ser_port.close()
  767. self.ui.com_connect_button.setStyleSheet("")
  768. self.app.inform.emit("%s: %s" % (_("Port is connected. Disconnecting"), port_name))
  769. except Exception:
  770. self.app.inform.emit("[ERROR_NOTCL] %s: %s" % (_("Could not connect to port"), port_name))
  771. def on_add_baudrate_grbl(self):
  772. new_bd = str(self.ui.new_baudrate_entry.get_value())
  773. if int(new_bd) >= 40 and new_bd not in self.ui.baudrates_list_combo.model().stringList():
  774. self.ui.baudrates_list_combo.addItem(new_bd)
  775. self.ui.baudrates_list_combo.setCurrentText(new_bd)
  776. def on_delete_baudrate_grbl(self):
  777. current_idx = self.ui.baudrates_list_combo.currentIndex()
  778. self.ui.baudrates_list_combo.removeItem(current_idx)
  779. def probing_gcode(self, coords, pr_travel, probe_fr, pr_depth, controller):
  780. """
  781. :param coords: a list of (x, y) tuples of probe points coordinates
  782. :type coords: list
  783. :param pr_travel: the height (z) where the probe travel between probe points
  784. :type pr_travel: float
  785. :param probe_fr: feedrate when probing
  786. :type probe_fr: float
  787. :param pr_depth: how much to lower the probe searching for contact
  788. :type pr_depth: float
  789. :param controller: a string with the name of the GCode sender for which to create the probing GCode.
  790. Can be: 'MACH3', 'MACH4', 'LinuxCNC', 'GRBL'
  791. :type controller: str
  792. :return: Probing GCode
  793. :rtype: str
  794. """
  795. p_gcode = ''
  796. header = ''
  797. time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
  798. header += '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \
  799. (str(self.app.version), str(self.app.version_date)) + '\n'
  800. header += '(This is a autolevelling probing GCode.)\n' \
  801. '(Make sure that before you start the job you first do a zero for all axis.)\n\n'
  802. header += '(Name: ' + str(self.options['name']) + ')\n'
  803. header += '(Type: ' + "Autolevelling Probing GCode " + ')\n'
  804. header += '(Units: ' + self.units.upper() + ')\n'
  805. header += '(Created on ' + time_str + ')\n\n'
  806. # commands
  807. if controller == 'MACH3':
  808. probing_command = 'G31'
  809. probing_var = '#2002'
  810. openfile_command = 'M40'
  811. closefile_command = 'M41'
  812. elif controller == 'MACH4':
  813. probing_command = 'G31'
  814. probing_var = '#5063'
  815. openfile_command = 'M40'
  816. closefile_command = 'M41'
  817. elif controller == 'LinuxCNC':
  818. probing_command = 'G38.2'
  819. probing_var = '#5422'
  820. openfile_command = '(PROBEOPEN a_probing_points_file.txt)'
  821. closefile_command = '(PROBECLOSE)'
  822. else:
  823. log.debug("CNCJobObject.probing_gcode() -> controller not supported")
  824. return
  825. # #############################################################################################################
  826. # ########################### GCODE construction ##############################################################
  827. # #############################################################################################################
  828. # header
  829. p_gcode += header + '\n\n'
  830. # supplementary message for LinuxCNC
  831. if controller == 'LinuxCNC':
  832. probing_var += "The file with the stored probing points can be found\n" \
  833. "in the configuration folder for LinuxCNC.\n" \
  834. "The name of the file is: a_probing_points_file.txt.\n"
  835. # units
  836. p_gcode += 'G21\n' if self.units == 'MM' else 'G20\n'
  837. # reference mode = absolute
  838. p_gcode += 'G90\n'
  839. # open a new file
  840. p_gcode += openfile_command + '\n'
  841. # move to safe height (probe travel Z)
  842. p_gcode += 'G0 Z%s\n' % str(self.app.dec_format(pr_travel, self.coords_decimals))
  843. # probing points
  844. for idx, xy_tuple in enumerate(coords, 1): # index starts from 1
  845. x = xy_tuple[0]
  846. y = xy_tuple[1]
  847. # move to probing point
  848. p_gcode += "G0 X%sY%s\n" % (
  849. str(self.app.dec_format(x, self.coords_decimals)),
  850. str(self.app.dec_format(y, self.coords_decimals))
  851. )
  852. # do the probing
  853. p_gcode += "%s Z%s F%s\n" % (
  854. probing_command,
  855. str(self.app.dec_format(pr_depth, self.coords_decimals)),
  856. str(self.app.dec_format(probe_fr, self.fr_decimals)),
  857. )
  858. # store in a global numeric variable the value of the detected probe Z
  859. # I offset the global numeric variable by 500 so it does not conflict with something else
  860. temp_var = int(idx + 500)
  861. p_gcode += "#%d = %s\n" % (temp_var, probing_var)
  862. # move to safe height (probe travel Z)
  863. p_gcode += 'G0 Z%s\n' % str(self.app.dec_format(pr_travel, self.coords_decimals))
  864. # close the file
  865. p_gcode += closefile_command + '\n'
  866. # finish the GCode
  867. p_gcode += 'M2'
  868. return p_gcode
  869. def on_generate_probing_gcode(self):
  870. coords = []
  871. for id_key, value in self.al_geometry_dict.items():
  872. x = value['point'].x
  873. y = value['point'].y
  874. coords.append(
  875. (
  876. self.app.dec_format(x, dec=self.app.decimals),
  877. self.app.dec_format(y, dec=self.app.decimals)
  878. )
  879. )
  880. pr_travel = self.ui.ptravelz_entry.get_value()
  881. probe_fr = self.ui.feedrate_probe_entry.get_value()
  882. pr_depth = self.ui.pdepth_entry.get_value()
  883. controller = self.ui.al_controller_combo.get_value()
  884. self.probing_gcode_text = self.probing_gcode(coords, pr_travel, probe_fr, pr_depth, controller)
  885. def on_view_probing_gcode(self):
  886. self.app.proc_container.view.set_busy(_("Loading..."))
  887. gco = self.probing_gcode_text
  888. if gco is None or gco == '':
  889. self.app.inform.emit('[WARNING_NOTCL] %s...' % _('There is nothing to view'))
  890. return
  891. self.gcode_viewer_tab = AppTextEditor(app=self.app, plain_text=True)
  892. # add the tab if it was closed
  893. self.app.ui.plot_tab_area.addTab(self.gcode_viewer_tab, '%s' % _("Code Viewer"))
  894. self.gcode_viewer_tab.setObjectName('code_viewer_tab')
  895. # delete the absolute and relative position and messages in the infobar
  896. self.app.ui.position_label.setText("")
  897. self.app.ui.rel_position_label.setText("")
  898. self.gcode_viewer_tab.code_editor.completer_enable = False
  899. self.gcode_viewer_tab.buttonRun.hide()
  900. # Switch plot_area to CNCJob tab
  901. self.app.ui.plot_tab_area.setCurrentWidget(self.gcode_viewer_tab)
  902. self.gcode_viewer_tab.t_frame.hide()
  903. # then append the text from GCode to the text editor
  904. try:
  905. self.gcode_viewer_tab.load_text(gco, move_to_start=True, clear_text=True)
  906. except Exception as e:
  907. log.debug('FlatCAMCNCJob.on_edit_code_click() -->%s' % str(e))
  908. return
  909. self.gcode_viewer_tab.t_frame.show()
  910. self.app.proc_container.view.set_idle()
  911. self.gcode_viewer_tab.buttonSave.hide()
  912. self.gcode_viewer_tab.buttonOpen.hide()
  913. self.gcode_viewer_tab.buttonPrint.hide()
  914. self.gcode_viewer_tab.buttonPreview.hide()
  915. self.gcode_viewer_tab.buttonReplace.hide()
  916. self.gcode_viewer_tab.sel_all_cb.hide()
  917. self.gcode_viewer_tab.entryReplace.hide()
  918. self.gcode_viewer_tab.code_editor.setReadOnly(True)
  919. self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Viewer'))
  920. def on_import_height_map(self):
  921. """
  922. Import the height map file into the app
  923. :return:
  924. :rtype:
  925. """
  926. _filter_ = "Text File .txt (*.txt);;All Files (*.*)"
  927. try:
  928. filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import Height Map"),
  929. directory=self.app.get_last_folder(),
  930. filter=_filter_)
  931. except TypeError:
  932. filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import Height Map"),
  933. filter=_filter_)
  934. filename = str(filename)
  935. if filename == '':
  936. self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
  937. else:
  938. self.app.worker_task.emit({'fcn': self.import_height_map, 'params': [filename]})
  939. def import_height_map(self, filename):
  940. """
  941. :param filename:
  942. :type filename:
  943. :return:
  944. :rtype:
  945. """
  946. stream = ''
  947. try:
  948. if filename:
  949. with open(filename, 'r') as f:
  950. stream = f.readlines()
  951. else:
  952. return
  953. except IOError:
  954. log.error("Failed to open height map file: %s" % filename)
  955. self.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open height map file"), filename))
  956. return
  957. idx = 0
  958. for line in stream:
  959. if line != '':
  960. idx += 1
  961. line = line.replace(' ', ',').replace('\n', '').split(',')
  962. if idx not in self.al_geometry_dict:
  963. self.al_geometry_dict[idx] = {}
  964. self.al_geometry_dict[idx]['height'] = float(line[2])
  965. if 'point' not in self.al_geometry_dict[idx]:
  966. x = float(line[0])
  967. y = float(line[1])
  968. self.al_geometry_dict[idx]['point'] = Point((x, y))
  969. self.build_al_table_sig.emit()
  970. def on_updateplot_button_click(self, *args):
  971. """
  972. Callback for the "Updata Plot" button. Reads the form for updates
  973. and plots the object.
  974. """
  975. self.read_form()
  976. self.on_plot_kind_change()
  977. def on_plot_kind_change(self):
  978. kind = self.ui.cncplot_method_combo.get_value()
  979. def worker_task():
  980. with self.app.proc_container.new(_("Plotting...")):
  981. self.plot(kind=kind)
  982. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  983. def on_exportgcode_button_click(self):
  984. """
  985. Handler activated by a button clicked when exporting GCode.
  986. :param args:
  987. :return:
  988. """
  989. self.app.defaults.report_usage("cncjob_on_exportgcode_button")
  990. self.read_form()
  991. name = self.app.collection.get_active().options['name']
  992. save_gcode = False
  993. if 'Roland' in self.pp_excellon_name or 'Roland' in self.pp_geometry_name:
  994. _filter_ = "RML1 Files .rol (*.rol);;All Files (*.*)"
  995. elif 'hpgl' in self.pp_geometry_name:
  996. _filter_ = "HPGL Files .plt (*.plt);;All Files (*.*)"
  997. else:
  998. save_gcode = True
  999. _filter_ = self.app.defaults['cncjob_save_filters']
  1000. try:
  1001. dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name)
  1002. filename, _f = FCFileSaveDialog.get_saved_filename(
  1003. caption=_("Export Code ..."),
  1004. directory=dir_file_to_save,
  1005. ext_filter=_filter_
  1006. )
  1007. except TypeError:
  1008. filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export Code ..."), ext_filter=_filter_)
  1009. self.export_gcode_handler(filename, is_gcode=save_gcode)
  1010. def export_gcode_handler(self, filename, is_gcode=True):
  1011. preamble = ''
  1012. postamble = ''
  1013. filename = str(filename)
  1014. if filename == '':
  1015. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export cancelled ..."))
  1016. return
  1017. else:
  1018. if is_gcode is True:
  1019. used_extension = filename.rpartition('.')[2]
  1020. self.update_filters(last_ext=used_extension, filter_string='cncjob_save_filters')
  1021. new_name = os.path.split(str(filename))[1].rpartition('.')[0]
  1022. self.ui.name_entry.set_value(new_name)
  1023. self.on_name_activate(silent=True)
  1024. try:
  1025. if self.ui.snippets_cb.get_value():
  1026. preamble = self.append_snippet
  1027. postamble = self.prepend_snippet
  1028. gc = self.export_gcode(filename, preamble=preamble, postamble=postamble)
  1029. except Exception as err:
  1030. log.debug("CNCJobObject.export_gcode_handler() --> %s" % str(err))
  1031. gc = self.export_gcode(filename)
  1032. if gc == 'fail':
  1033. return
  1034. if self.app.defaults["global_open_style"] is False:
  1035. self.app.file_opened.emit("gcode", filename)
  1036. self.app.file_saved.emit("gcode", filename)
  1037. self.app.inform.emit('[success] %s: %s' % (_("File saved to"), filename))
  1038. def on_edit_code_click(self, *args):
  1039. """
  1040. Handler activated by a button clicked when reviewing GCode.
  1041. :param args:
  1042. :return:
  1043. """
  1044. self.app.proc_container.view.set_busy(_("Loading..."))
  1045. preamble = self.append_snippet
  1046. postamble = self.prepend_snippet
  1047. gco = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True)
  1048. if gco == 'fail':
  1049. return
  1050. else:
  1051. self.app.gcode_edited = gco
  1052. self.gcode_editor_tab = AppTextEditor(app=self.app, plain_text=True)
  1053. # add the tab if it was closed
  1054. self.app.ui.plot_tab_area.addTab(self.gcode_editor_tab, '%s' % _("Code Review"))
  1055. self.gcode_editor_tab.setObjectName('code_editor_tab')
  1056. # delete the absolute and relative position and messages in the infobar
  1057. self.app.ui.position_label.setText("")
  1058. self.app.ui.rel_position_label.setText("")
  1059. self.gcode_editor_tab.code_editor.completer_enable = False
  1060. self.gcode_editor_tab.buttonRun.hide()
  1061. # Switch plot_area to CNCJob tab
  1062. self.app.ui.plot_tab_area.setCurrentWidget(self.gcode_editor_tab)
  1063. self.gcode_editor_tab.t_frame.hide()
  1064. # then append the text from GCode to the text editor
  1065. try:
  1066. self.gcode_editor_tab.load_text(self.app.gcode_edited.getvalue(), move_to_start=True, clear_text=True)
  1067. except Exception as e:
  1068. log.debug('FlatCAMCNCJob.on_edit_code_click() -->%s' % str(e))
  1069. return
  1070. self.gcode_editor_tab.t_frame.show()
  1071. self.app.proc_container.view.set_idle()
  1072. self.gcode_editor_tab.buttonSave.hide()
  1073. self.gcode_editor_tab.buttonOpen.hide()
  1074. self.gcode_editor_tab.buttonPrint.hide()
  1075. self.gcode_editor_tab.buttonPreview.hide()
  1076. self.gcode_editor_tab.buttonReplace.hide()
  1077. self.gcode_editor_tab.sel_all_cb.hide()
  1078. self.gcode_editor_tab.entryReplace.hide()
  1079. self.gcode_editor_tab.code_editor.setReadOnly(True)
  1080. self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Editor'))
  1081. def on_update_source_file(self):
  1082. self.source_file = self.gcode_editor_tab.code_editor.toPlainText()
  1083. def gcode_header(self, comment_start_symbol=None, comment_stop_symbol=None):
  1084. """
  1085. Will create a header to be added to all GCode files generated by FlatCAM
  1086. :param comment_start_symbol: A symbol to be used as the first symbol in a comment
  1087. :param comment_stop_symbol: A symbol to be used as the last symbol in a comment
  1088. :return: A string with a GCode header
  1089. """
  1090. log.debug("FlatCAMCNCJob.gcode_header()")
  1091. time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
  1092. marlin = False
  1093. hpgl = False
  1094. probe_pp = False
  1095. gcode = ''
  1096. start_comment = comment_start_symbol if comment_start_symbol is not None else '('
  1097. stop_comment = comment_stop_symbol if comment_stop_symbol is not None else ')'
  1098. try:
  1099. for key in self.cnc_tools:
  1100. ppg = self.cnc_tools[key]['data']['ppname_g']
  1101. if 'marlin' in ppg.lower() or 'repetier' in ppg.lower():
  1102. marlin = True
  1103. break
  1104. if ppg == 'hpgl':
  1105. hpgl = True
  1106. break
  1107. if "toolchange_probe" in ppg.lower():
  1108. probe_pp = True
  1109. break
  1110. except KeyError:
  1111. # log.debug("FlatCAMCNCJob.gcode_header() error: --> %s" % str(e))
  1112. pass
  1113. try:
  1114. if 'marlin' in self.options['ppname_e'].lower() or 'repetier' in self.options['ppname_e'].lower():
  1115. marlin = True
  1116. except KeyError:
  1117. # log.debug("FlatCAMCNCJob.gcode_header(): --> There is no such self.option: %s" % str(e))
  1118. pass
  1119. try:
  1120. if "toolchange_probe" in self.options['ppname_e'].lower():
  1121. probe_pp = True
  1122. except KeyError:
  1123. # log.debug("FlatCAMCNCJob.gcode_header(): --> There is no such self.option: %s" % str(e))
  1124. pass
  1125. if marlin is True:
  1126. gcode += ';Marlin(Repetier) G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s\n' % \
  1127. (str(self.app.version), str(self.app.version_date)) + '\n'
  1128. gcode += ';Name: ' + str(self.options['name']) + '\n'
  1129. gcode += ';Type: ' + "G-code from " + str(self.options['type']) + '\n'
  1130. # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
  1131. # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
  1132. gcode += ';Units: ' + self.units.upper() + '\n' + "\n"
  1133. gcode += ';Created on ' + time_str + '\n' + '\n'
  1134. elif hpgl is True:
  1135. gcode += 'CO "HPGL CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s' % \
  1136. (str(self.app.version), str(self.app.version_date)) + '";\n'
  1137. gcode += 'CO "Name: ' + str(self.options['name']) + '";\n'
  1138. gcode += 'CO "Type: ' + "HPGL code from " + str(self.options['type']) + '";\n'
  1139. # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
  1140. # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
  1141. gcode += 'CO "Units: ' + self.units.upper() + '";\n'
  1142. gcode += 'CO "Created on ' + time_str + '";\n'
  1143. elif probe_pp is True:
  1144. gcode += '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \
  1145. (str(self.app.version), str(self.app.version_date)) + '\n'
  1146. gcode += '(This GCode tool change is done by using a Probe.)\n' \
  1147. '(Make sure that before you start the job you first do a rough zero for Z axis.)\n' \
  1148. '(This means that you need to zero the CNC axis and then jog to the toolchange X, Y location,)\n' \
  1149. '(mount the probe and adjust the Z so more or less the probe tip touch the plate. ' \
  1150. 'Then zero the Z axis.)\n' + '\n'
  1151. gcode += '(Name: ' + str(self.options['name']) + ')\n'
  1152. gcode += '(Type: ' + "G-code from " + str(self.options['type']) + ')\n'
  1153. # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
  1154. # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
  1155. gcode += '(Units: ' + self.units.upper() + ')\n' + "\n"
  1156. gcode += '(Created on ' + time_str + ')\n' + '\n'
  1157. else:
  1158. gcode += '%sG-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s%s\n' % \
  1159. (start_comment, str(self.app.version), str(self.app.version_date), stop_comment) + '\n'
  1160. gcode += '%sName: ' % start_comment + str(self.options['name']) + '%s\n' % stop_comment
  1161. gcode += '%sType: ' % start_comment + "G-code from " + str(self.options['type']) + '%s\n' % stop_comment
  1162. # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
  1163. # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
  1164. gcode += '%sUnits: ' % start_comment + self.units.upper() + '%s\n' % stop_comment + "\n"
  1165. gcode += '%sCreated on ' % start_comment + time_str + '%s\n' % stop_comment + '\n'
  1166. return gcode
  1167. @staticmethod
  1168. def gcode_footer(end_command=None):
  1169. """
  1170. Will add the M02 to the end of GCode, if requested.
  1171. :param end_command: 'M02' or 'M30' - String
  1172. :return:
  1173. """
  1174. if end_command:
  1175. return end_command
  1176. else:
  1177. return 'M02'
  1178. def export_gcode(self, filename=None, preamble='', postamble='', to_file=False, from_tcl=False):
  1179. """
  1180. This will save the GCode from the Gcode object to a file on the OS filesystem
  1181. :param filename: filename for the GCode file
  1182. :param preamble: a custom Gcode block to be added at the beginning of the Gcode file
  1183. :param postamble: a custom Gcode block to be added at the end of the Gcode file
  1184. :param to_file: if False then no actual file is saved but the app will know that a file was created
  1185. :param from_tcl: True if run from Tcl Shell
  1186. :return: None
  1187. """
  1188. # gcode = ''
  1189. # roland = False
  1190. # hpgl = False
  1191. # isel_icp = False
  1192. include_header = True
  1193. if preamble == '':
  1194. preamble = self.app.defaults["cncjob_prepend"]
  1195. if postamble == '':
  1196. preamble = self.app.defaults["cncjob_append"]
  1197. try:
  1198. if self.special_group:
  1199. self.app.inform.emit('[WARNING_NOTCL] %s %s %s.' %
  1200. (_("This CNCJob object can't be processed because it is a"),
  1201. str(self.special_group),
  1202. _("CNCJob object")))
  1203. return 'fail'
  1204. except AttributeError:
  1205. pass
  1206. # if this dict is not empty then the object is a Geometry object
  1207. if self.cnc_tools:
  1208. first_key = next(iter(self.cnc_tools))
  1209. include_header = self.app.preprocessors[self.cnc_tools[first_key]['data']['ppname_g']].include_header
  1210. # if this dict is not empty then the object is an Excellon object
  1211. if self.exc_cnc_tools:
  1212. first_key = next(iter(self.exc_cnc_tools))
  1213. include_header = self.app.preprocessors[
  1214. self.exc_cnc_tools[first_key]['data']['tools_drill_ppname_e']
  1215. ].include_header
  1216. gcode = ''
  1217. if include_header is False:
  1218. g = preamble
  1219. # detect if using multi-tool and make the Gcode summation correctly for each case
  1220. if self.multitool is True:
  1221. for tooluid_key in self.cnc_tools:
  1222. for key, value in self.cnc_tools[tooluid_key].items():
  1223. if key == 'gcode':
  1224. gcode += value
  1225. break
  1226. else:
  1227. gcode += self.gcode
  1228. g = g + gcode + postamble
  1229. else:
  1230. # search for the GCode beginning which is usually a G20 or G21
  1231. # fix so the preamble gets inserted in between the comments header and the actual start of GCODE
  1232. # g_idx = gcode.rfind('G20')
  1233. #
  1234. # # if it did not find 'G20' then search for 'G21'
  1235. # if g_idx == -1:
  1236. # g_idx = gcode.rfind('G21')
  1237. #
  1238. # # if it did not find 'G20' and it did not find 'G21' then there is an error and return
  1239. # if g_idx == -1:
  1240. # self.app.inform.emit('[ERROR_NOTCL] %s' % _("G-code does not have a units code: either G20 or G21"))
  1241. # return
  1242. # detect if using multi-tool and make the Gcode summation correctly for each case
  1243. if self.multitool is True:
  1244. if self.origin_kind == 'excellon':
  1245. for tooluid_key in self.exc_cnc_tools:
  1246. for key, value in self.exc_cnc_tools[tooluid_key].items():
  1247. if key == 'gcode' and value:
  1248. gcode += value
  1249. break
  1250. else:
  1251. for tooluid_key in self.cnc_tools:
  1252. for key, value in self.cnc_tools[tooluid_key].items():
  1253. if key == 'gcode' and value:
  1254. gcode += value
  1255. break
  1256. else:
  1257. gcode += self.gcode
  1258. end_gcode = self.gcode_footer() if self.app.defaults['cncjob_footer'] is True else ''
  1259. # detect if using a HPGL preprocessor
  1260. hpgl = False
  1261. if self.cnc_tools:
  1262. for key in self.cnc_tools:
  1263. if 'ppname_g' in self.cnc_tools[key]['data']:
  1264. if 'hpgl' in self.cnc_tools[key]['data']['ppname_g']:
  1265. hpgl = True
  1266. break
  1267. elif self.exc_cnc_tools:
  1268. for key in self.cnc_tools:
  1269. if 'ppname_e' in self.cnc_tools[key]['data']:
  1270. if 'hpgl' in self.cnc_tools[key]['data']['ppname_e']:
  1271. hpgl = True
  1272. break
  1273. if hpgl:
  1274. processed_gcode = ''
  1275. pa_re = re.compile(r"^PA\s*(-?\d+\.\d*),?\s*(-?\d+\.\d*)*;?$")
  1276. for gline in gcode.splitlines():
  1277. match = pa_re.search(gline)
  1278. if match:
  1279. x_int = int(float(match.group(1)))
  1280. y_int = int(float(match.group(2)))
  1281. new_line = 'PA%d,%d;\n' % (x_int, y_int)
  1282. processed_gcode += new_line
  1283. else:
  1284. processed_gcode += gline + '\n'
  1285. gcode = processed_gcode
  1286. g = self.gc_header + '\n' + preamble + '\n' + gcode + postamble + end_gcode
  1287. else:
  1288. try:
  1289. g_idx = gcode.index('G94')
  1290. if preamble != '' and postamble != '':
  1291. g = self.gc_header + gcode[:g_idx + 3] + '\n' + preamble + '\n' + \
  1292. gcode[(g_idx + 3):] + postamble + end_gcode
  1293. elif preamble == '':
  1294. g = self.gc_header + gcode[:g_idx + 3] + '\n' + \
  1295. gcode[(g_idx + 3):] + postamble + end_gcode
  1296. elif postamble == '':
  1297. g = self.gc_header + gcode[:g_idx + 3] + '\n' + preamble + '\n' + \
  1298. gcode[(g_idx + 3):] + end_gcode
  1299. else:
  1300. g = self.gc_header + gcode[:g_idx + 3] + gcode[(g_idx + 3):] + end_gcode
  1301. except ValueError:
  1302. self.app.inform.emit('[ERROR_NOTCL] %s' %
  1303. _("G-code does not have a G94 code.\n"
  1304. "Append Code snippet will not be used.."))
  1305. g = self.gc_header + '\n' + gcode + postamble + end_gcode
  1306. # if toolchange custom is used, replace M6 code with the code from the Toolchange Custom Text box
  1307. # if self.ui.toolchange_cb.get_value() is True:
  1308. # # match = self.re_toolchange.search(g)
  1309. # if 'M6' in g:
  1310. # m6_code = self.parse_custom_toolchange_code(self.ui.toolchange_text.get_value())
  1311. # if m6_code is None or m6_code == '':
  1312. # self.app.inform.emit(
  1313. # '[ERROR_NOTCL] %s' % _("Cancelled. The Toolchange Custom code is enabled but it's empty.")
  1314. # )
  1315. # return 'fail'
  1316. #
  1317. # g = g.replace('M6', m6_code)
  1318. # self.app.inform.emit('[success] %s' % _("Toolchange G-code was replaced by a custom code."))
  1319. lines = StringIO(g)
  1320. # Write
  1321. if filename is not None:
  1322. try:
  1323. force_windows_line_endings = self.app.defaults['cncjob_line_ending']
  1324. if force_windows_line_endings and sys.platform != 'win32':
  1325. with open(filename, 'w', newline='\r\n') as f:
  1326. for line in lines:
  1327. f.write(line)
  1328. else:
  1329. with open(filename, 'w') as f:
  1330. for line in lines:
  1331. f.write(line)
  1332. except FileNotFoundError:
  1333. self.app.inform.emit('[WARNING_NOTCL] %s' % _("No such file or directory"))
  1334. return
  1335. except PermissionError:
  1336. self.app.inform.emit(
  1337. '[WARNING] %s' % _("Permission denied, saving not possible.\n"
  1338. "Most likely another app is holding the file open and not accessible.")
  1339. )
  1340. return 'fail'
  1341. elif to_file is False:
  1342. # Just for adding it to the recent files list.
  1343. if self.app.defaults["global_open_style"] is False:
  1344. self.app.file_opened.emit("cncjob", filename)
  1345. self.app.file_saved.emit("cncjob", filename)
  1346. self.app.inform.emit('[success] %s: %s' % (_("Saved to"), filename))
  1347. else:
  1348. return lines
  1349. # def on_toolchange_custom_clicked(self, signal):
  1350. # """
  1351. # Handler for clicking toolchange custom.
  1352. #
  1353. # :param signal:
  1354. # :return:
  1355. # """
  1356. #
  1357. # try:
  1358. # if 'toolchange_custom' not in str(self.options['ppname_e']).lower():
  1359. # if self.ui.toolchange_cb.get_value():
  1360. # self.ui.toolchange_cb.set_value(False)
  1361. # self.app.inform.emit('[WARNING_NOTCL] %s' %
  1362. # _("The used preprocessor file has to have in it's name: 'toolchange_custom'"))
  1363. # except KeyError:
  1364. # try:
  1365. # for key in self.cnc_tools:
  1366. # ppg = self.cnc_tools[key]['data']['ppname_g']
  1367. # if 'toolchange_custom' not in str(ppg).lower():
  1368. # if self.ui.toolchange_cb.get_value():
  1369. # self.ui.toolchange_cb.set_value(False)
  1370. # self.app.inform.emit('[WARNING_NOTCL] %s' %
  1371. # _("The used preprocessor file has to have in it's name: "
  1372. # "'toolchange_custom'"))
  1373. # except KeyError:
  1374. # self.app.inform.emit('[ERROR] %s' % _("There is no preprocessor file."))
  1375. def get_gcode(self, preamble='', postamble=''):
  1376. """
  1377. We need this to be able to get_gcode separately for shell command export_gcode
  1378. :param preamble: Extra GCode added to the beginning of the GCode
  1379. :param postamble: Extra GCode added at the end of the GCode
  1380. :return: The modified GCode
  1381. """
  1382. return preamble + '\n' + self.gcode + "\n" + postamble
  1383. def get_svg(self):
  1384. # we need this to be able get_svg separately for shell command export_svg
  1385. pass
  1386. def on_plot_cb_click(self, *args):
  1387. """
  1388. Handler for clicking on the Plot checkbox.
  1389. :param args:
  1390. :return:
  1391. """
  1392. if self.muted_ui:
  1393. return
  1394. kind = self.ui.cncplot_method_combo.get_value()
  1395. self.plot(kind=kind)
  1396. self.read_form_item('plot')
  1397. self.ui_disconnect()
  1398. cb_flag = self.ui.plot_cb.isChecked()
  1399. for row in range(self.ui.cnc_tools_table.rowCount()):
  1400. table_cb = self.ui.cnc_tools_table.cellWidget(row, 6)
  1401. if cb_flag:
  1402. table_cb.setChecked(True)
  1403. else:
  1404. table_cb.setChecked(False)
  1405. self.ui_connect()
  1406. def on_plot_cb_click_table(self):
  1407. """
  1408. Handler for clicking the plot checkboxes added into a Table on each row. Purpose: toggle visibility for the
  1409. tool/aperture found on that row.
  1410. :return:
  1411. """
  1412. # self.ui.cnc_tools_table.cellWidget(row, 2).widget().setCheckState(QtCore.Qt.Unchecked)
  1413. self.ui_disconnect()
  1414. # cw = self.sender()
  1415. # cw_index = self.ui.cnc_tools_table.indexAt(cw.pos())
  1416. # cw_row = cw_index.row()
  1417. kind = self.ui.cncplot_method_combo.get_value()
  1418. self.shapes.clear(update=True)
  1419. if self.origin_kind == "excellon":
  1420. for r in range(self.ui.exc_cnc_tools_table.rowCount()):
  1421. row_dia = float('%.*f' % (self.decimals, float(self.ui.exc_cnc_tools_table.item(r, 1).text())))
  1422. for tooluid_key in self.exc_cnc_tools:
  1423. tooldia = float('%.*f' % (self.decimals, float(tooluid_key)))
  1424. if row_dia == tooldia:
  1425. gcode_parsed = self.exc_cnc_tools[tooluid_key]['gcode_parsed']
  1426. if self.ui.exc_cnc_tools_table.cellWidget(r, 6).isChecked():
  1427. self.plot2(tooldia=tooldia, obj=self, visible=True, gcode_parsed=gcode_parsed, kind=kind)
  1428. else:
  1429. for tooluid_key in self.cnc_tools:
  1430. tooldia = float('%.*f' % (self.decimals, float(self.cnc_tools[tooluid_key]['tooldia'])))
  1431. gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
  1432. # tool_uid = int(self.ui.cnc_tools_table.item(cw_row, 3).text())
  1433. for r in range(self.ui.cnc_tools_table.rowCount()):
  1434. if int(self.ui.cnc_tools_table.item(r, 5).text()) == int(tooluid_key):
  1435. if self.ui.cnc_tools_table.cellWidget(r, 6).isChecked():
  1436. self.plot2(tooldia=tooldia, obj=self, visible=True, gcode_parsed=gcode_parsed, kind=kind)
  1437. self.shapes.redraw()
  1438. # make sure that the general plot is disabled if one of the row plot's are disabled and
  1439. # if all the row plot's are enabled also enable the general plot checkbox
  1440. cb_cnt = 0
  1441. total_row = self.ui.cnc_tools_table.rowCount()
  1442. for row in range(total_row):
  1443. if self.ui.cnc_tools_table.cellWidget(row, 6).isChecked():
  1444. cb_cnt += 1
  1445. else:
  1446. cb_cnt -= 1
  1447. if cb_cnt < total_row:
  1448. self.ui.plot_cb.setChecked(False)
  1449. else:
  1450. self.ui.plot_cb.setChecked(True)
  1451. self.ui_connect()
  1452. def plot(self, visible=None, kind='all'):
  1453. """
  1454. # Does all the required setup and returns False
  1455. # if the 'ptint' option is set to False.
  1456. :param visible: Boolean to decide if the object will be plotted as visible or disabled on canvas
  1457. :param kind: String. Can be "all" or "travel" or "cut". For CNCJob plotting
  1458. :return: None
  1459. """
  1460. if not FlatCAMObj.plot(self):
  1461. return
  1462. visible = visible if visible else self.options['plot']
  1463. if self.app.is_legacy is False:
  1464. if self.ui.annotation_cb.get_value() and self.ui.plot_cb.get_value():
  1465. self.text_col.enabled = True
  1466. else:
  1467. self.text_col.enabled = False
  1468. self.annotation.redraw()
  1469. try:
  1470. if self.multitool is False: # single tool usage
  1471. try:
  1472. dia_plot = float(self.options["tooldia"])
  1473. except ValueError:
  1474. # we may have a tuple with only one element and a comma
  1475. dia_plot = [float(el) for el in self.options["tooldia"].split(',') if el != ''][0]
  1476. self.plot2(tooldia=dia_plot, obj=self, visible=visible, kind=kind)
  1477. else:
  1478. # I do this so the travel lines thickness will reflect the tool diameter
  1479. # may work only for objects created within the app and not Gcode imported from elsewhere for which we
  1480. # don't know the origin
  1481. if self.origin_kind == "excellon":
  1482. if self.exc_cnc_tools:
  1483. for tooldia_key in self.exc_cnc_tools:
  1484. tooldia = float('%.*f' % (self.decimals, float(tooldia_key)))
  1485. gcode_parsed = self.exc_cnc_tools[tooldia_key]['gcode_parsed']
  1486. if not gcode_parsed:
  1487. continue
  1488. # gcode_parsed = self.gcode_parsed
  1489. self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
  1490. else:
  1491. # multiple tools usage
  1492. if self.cnc_tools:
  1493. for tooluid_key in self.cnc_tools:
  1494. tooldia = float('%.*f' % (self.decimals, float(self.cnc_tools[tooluid_key]['tooldia'])))
  1495. gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
  1496. self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
  1497. self.shapes.redraw()
  1498. except (ObjectDeleted, AttributeError):
  1499. self.shapes.clear(update=True)
  1500. if self.app.is_legacy is False:
  1501. self.annotation.clear(update=True)
  1502. def on_annotation_change(self):
  1503. """
  1504. Handler for toggling the annotation display by clicking a checkbox.
  1505. :return:
  1506. """
  1507. if self.app.is_legacy is False:
  1508. if self.ui.annotation_cb.get_value():
  1509. self.text_col.enabled = True
  1510. else:
  1511. self.text_col.enabled = False
  1512. # kind = self.ui.cncplot_method_combo.get_value()
  1513. # self.plot(kind=kind)
  1514. self.annotation.redraw()
  1515. else:
  1516. kind = self.ui.cncplot_method_combo.get_value()
  1517. self.plot(kind=kind)
  1518. def convert_units(self, units):
  1519. """
  1520. Units conversion used by the CNCJob objects.
  1521. :param units: Can be "MM" or "IN"
  1522. :return:
  1523. """
  1524. log.debug("FlatCAMObj.FlatCAMECNCjob.convert_units()")
  1525. factor = CNCjob.convert_units(self, units)
  1526. self.options["tooldia"] = float(self.options["tooldia"]) * factor
  1527. param_list = ['cutz', 'depthperpass', 'travelz', 'feedrate', 'feedrate_z', 'feedrate_rapid',
  1528. 'endz', 'toolchangez']
  1529. temp_tools_dict = {}
  1530. tool_dia_copy = {}
  1531. data_copy = {}
  1532. for tooluid_key, tooluid_value in self.cnc_tools.items():
  1533. for dia_key, dia_value in tooluid_value.items():
  1534. if dia_key == 'tooldia':
  1535. dia_value *= factor
  1536. dia_value = float('%.*f' % (self.decimals, dia_value))
  1537. tool_dia_copy[dia_key] = dia_value
  1538. if dia_key == 'offset':
  1539. tool_dia_copy[dia_key] = dia_value
  1540. if dia_key == 'offset_value':
  1541. dia_value *= factor
  1542. tool_dia_copy[dia_key] = dia_value
  1543. if dia_key == 'type':
  1544. tool_dia_copy[dia_key] = dia_value
  1545. if dia_key == 'tool_type':
  1546. tool_dia_copy[dia_key] = dia_value
  1547. if dia_key == 'data':
  1548. for data_key, data_value in dia_value.items():
  1549. # convert the form fields that are convertible
  1550. for param in param_list:
  1551. if data_key == param and data_value is not None:
  1552. data_copy[data_key] = data_value * factor
  1553. # copy the other dict entries that are not convertible
  1554. if data_key not in param_list:
  1555. data_copy[data_key] = data_value
  1556. tool_dia_copy[dia_key] = deepcopy(data_copy)
  1557. data_copy.clear()
  1558. if dia_key == 'gcode':
  1559. tool_dia_copy[dia_key] = dia_value
  1560. if dia_key == 'gcode_parsed':
  1561. tool_dia_copy[dia_key] = dia_value
  1562. if dia_key == 'solid_geometry':
  1563. tool_dia_copy[dia_key] = dia_value
  1564. # if dia_key == 'solid_geometry':
  1565. # tool_dia_copy[dia_key] = affinity.scale(dia_value, xfact=factor, origin=(0, 0))
  1566. # if dia_key == 'gcode_parsed':
  1567. # for g in dia_value:
  1568. # g['geom'] = affinity.scale(g['geom'], factor, factor, origin=(0, 0))
  1569. #
  1570. # tool_dia_copy['gcode_parsed'] = deepcopy(dia_value)
  1571. # tool_dia_copy['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_value])
  1572. temp_tools_dict.update({
  1573. tooluid_key: deepcopy(tool_dia_copy)
  1574. })
  1575. tool_dia_copy.clear()
  1576. self.cnc_tools.clear()
  1577. self.cnc_tools = deepcopy(temp_tools_dict)