FlatCAMCNCJob.py 80 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911
  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. self.ui.updateplot_button.clicked.connect(self.on_updateplot_button_click)
  440. self.ui.export_gcode_button.clicked.connect(self.on_exportgcode_button_click)
  441. self.ui.review_gcode_button.clicked.connect(self.on_edit_code_click)
  442. self.ui.editor_button.clicked.connect(lambda: self.app.object2editor())
  443. # autolevelling signals
  444. self.ui.sal_cb.stateChanged.connect(self.on_autolevelling)
  445. self.ui.al_mode_radio.activated_custom.connect(self.on_mode_radio)
  446. self.ui.al_controller_combo.currentIndexChanged.connect(self.on_controller_change)
  447. self.ui.com_search_button.clicked.connect(self.on_search_ports)
  448. self.ui.add_bd_button.clicked.connect(self.on_add_baudrate_grbl)
  449. self.ui.del_bd_button.clicked.connect(self.on_delete_baudrate_grbl)
  450. self.ui.com_connect_button.clicked.connect(self.on_connect_grbl)
  451. self.ui.view_h_gcode_button.clicked.connect(self.on_view_probing_gcode)
  452. self.ui.h_gcode_button.clicked.connect(self.on_generate_probing_gcode)
  453. self.ui.import_heights_button.clicked.connect(self.on_import_height_map)
  454. self.build_al_table_sig.connect(self.build_al_table)
  455. # self.ui.tc_variable_combo.currentIndexChanged[str].connect(self.on_cnc_custom_parameters)
  456. self.ui.cncplot_method_combo.activated_custom.connect(self.on_plot_kind_change)
  457. # Show/Hide Advanced Options
  458. if self.app.defaults["global_app_level"] == 'b':
  459. self.ui.level.setText(_(
  460. '<span style="color:green;"><b>Basic</b></span>'
  461. ))
  462. self.ui.sal_cb.hide()
  463. self.ui.sal_cb.set_value(False)
  464. else:
  465. self.ui.level.setText(_(
  466. '<span style="color:red;"><b>Advanced</b></span>'
  467. ))
  468. self.ui.sal_cb.show()
  469. self.ui.sal_cb.set_value(self.app.defaults["cncjob_al_status"])
  470. preamble = self.append_snippet
  471. postamble = self.prepend_snippet
  472. gc = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True)
  473. self.source_file = gc.getvalue()
  474. self.ui.al_mode_radio.set_value(self.options['al_mode'])
  475. self.on_controller_change()
  476. # def on_cnc_custom_parameters(self, signal_text):
  477. # if signal_text == 'Parameters':
  478. # return
  479. # else:
  480. # self.ui.toolchange_text.insertPlainText('%%%s%%' % signal_text)
  481. def ui_connect(self):
  482. for row in range(self.ui.cnc_tools_table.rowCount()):
  483. self.ui.cnc_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table)
  484. for row in range(self.ui.exc_cnc_tools_table.rowCount()):
  485. self.ui.exc_cnc_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table)
  486. self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
  487. self.ui.al_add_button.clicked.connect(self.on_add_al_probepoints)
  488. self.ui.show_al_table.stateChanged.connect(self.on_show_al_table)
  489. def ui_disconnect(self):
  490. for row in range(self.ui.cnc_tools_table.rowCount()):
  491. try:
  492. self.ui.cnc_tools_table.cellWidget(row, 6).clicked.disconnect(self.on_plot_cb_click_table)
  493. except (TypeError, AttributeError):
  494. pass
  495. for row in range(self.ui.exc_cnc_tools_table.rowCount()):
  496. try:
  497. self.ui.exc_cnc_tools_table.cellWidget(row, 6).clicked.disconnect(self.on_plot_cb_click_table)
  498. except (TypeError, AttributeError):
  499. pass
  500. try:
  501. self.ui.plot_cb.stateChanged.disconnect(self.on_plot_cb_click)
  502. except (TypeError, AttributeError):
  503. pass
  504. try:
  505. self.ui.al_add_button.clicked.disconnect()
  506. except (TypeError, AttributeError):
  507. pass
  508. try:
  509. self.ui.show_al_table.stateChanged.disconnect()
  510. except (TypeError, AttributeError):
  511. pass
  512. def on_add_al_probepoints(self):
  513. # create the solid_geo
  514. solid_geo = [geo['geom'] for geo in self.gcode_parsed if geo['kind'][0] == 'C']
  515. solid_geo = unary_union(solid_geo)
  516. # reset al table
  517. self.ui.al_probe_points_table.setRowCount(0)
  518. # reset the al dict
  519. self.al_geometry_dict.clear()
  520. xmin, ymin, xmax, ymax = solid_geo.bounds
  521. if self.ui.al_mode_radio.get_value() == 'grid':
  522. width = abs(xmax - xmin)
  523. height = abs(ymax - ymin)
  524. cols = self.ui.al_columns_entry.get_value()
  525. rows = self.ui.al_rows_entry.get_value()
  526. dx = width / (cols + 1)
  527. dy = height / (rows + 1)
  528. points = []
  529. new_y = ymin
  530. for x in range(rows):
  531. new_y += dy
  532. new_x = xmin
  533. for x in range(cols):
  534. new_x += dx
  535. points.append((new_x, new_y))
  536. pt_id = 0
  537. for point in points:
  538. pt_id += 1
  539. new_dict = {
  540. 'point': Point(point),
  541. 'geo': None,
  542. 'height': 0.0
  543. }
  544. self.al_geometry_dict[pt_id] = deepcopy(new_dict)
  545. else:
  546. self.app.inform.emit(_("Click on canvas to add a Test Point..."))
  547. if self.app.is_legacy is False:
  548. self.app.plotcanvas.graph_event_disconnect('key_press', self.app.ui.keyPressEvent)
  549. self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
  550. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
  551. else:
  552. self.app.plotcanvas.graph_event_disconnect(self.app.kp)
  553. self.app.plotcanvas.graph_event_disconnect(self.app.mp)
  554. self.app.plotcanvas.graph_event_disconnect(self.app.mr)
  555. self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
  556. self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
  557. self.mouse_events_connected = True
  558. # self.calculate_voronoi_diagram()
  559. self.build_al_table_sig.emit()
  560. # def calculate_voronoi_diagram(self):
  561. # return voronoi_diagram()
  562. # To be called after clicking on the plot.
  563. def on_mouse_click_release(self, event):
  564. if self.app.is_legacy is False:
  565. event_pos = event.pos
  566. # event_is_dragging = event.is_dragging
  567. right_button = 2
  568. else:
  569. event_pos = (event.xdata, event.ydata)
  570. # event_is_dragging = self.app.plotcanvas.is_dragging
  571. right_button = 3
  572. try:
  573. x = float(event_pos[0])
  574. y = float(event_pos[1])
  575. except TypeError:
  576. return
  577. event_pos = (x, y)
  578. # do paint single only for left mouse clicks
  579. if event.button == 1:
  580. pos = self.app.plotcanvas.translate_coords(event_pos)
  581. # use the snapped position as reference
  582. snapped_pos = self.app.geo_editor.snap(pos[0], pos[1])
  583. if not self.al_geometry_dict:
  584. new_dict = {
  585. 'point': Point(snapped_pos),
  586. 'geo': None,
  587. 'height': 0.0
  588. }
  589. self.al_geometry_dict[1] = deepcopy(new_dict)
  590. else:
  591. int_keys = [int(k) for k in self.al_geometry_dict.keys()]
  592. new_id = max(int_keys) + 1
  593. new_dict = {
  594. 'point': Point(snapped_pos),
  595. 'geo': None,
  596. 'height': 0.0
  597. }
  598. self.al_geometry_dict[new_id] = deepcopy(new_dict)
  599. # rebuild the al table
  600. self.build_al_table_sig.emit()
  601. self.app.inform.emit(_("Added Test Point... Click again to add another or right click to finish ..."))
  602. # if RMB then we exit
  603. elif event.button == right_button and self.mouse_is_dragging is False:
  604. if self.app.is_legacy is False:
  605. self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
  606. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
  607. else:
  608. self.app.plotcanvas.graph_event_disconnect(self.kp)
  609. self.app.plotcanvas.graph_event_disconnect(self.mr)
  610. self.app.kp = self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent)
  611. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot)
  612. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  613. self.app.on_mouse_click_release_over_plot)
  614. # signal that the mouse events are disconnected from local methods
  615. self.mouse_events_connected = False
  616. # restore selection
  617. self.app.defaults['global_selection_shape'] = self.old_selection_state
  618. self.app.inform.emit(_("Finished manual adding of Test Point..."))
  619. # rebuild the al table
  620. self.build_al_table_sig.emit()
  621. def on_key_press(self, event):
  622. # events out of the self.app.collection view (it's about Project Tab) are of type int
  623. if type(event) is int:
  624. key = event
  625. # events from the GUI are of type QKeyEvent
  626. elif type(event) == QtGui.QKeyEvent:
  627. key = event.key()
  628. elif isinstance(event, mpl_key_event): # MatPlotLib key events are trickier to interpret than the rest
  629. key = event.key
  630. key = QtGui.QKeySequence(key)
  631. # check for modifiers
  632. key_string = key.toString().lower()
  633. if '+' in key_string:
  634. mod, __, key_text = key_string.rpartition('+')
  635. if mod.lower() == 'ctrl':
  636. # modifiers = QtCore.Qt.ControlModifier
  637. pass
  638. elif mod.lower() == 'alt':
  639. # modifiers = QtCore.Qt.AltModifier
  640. pass
  641. elif mod.lower() == 'shift':
  642. # modifiers = QtCore.Qt.ShiftModifier
  643. pass
  644. else:
  645. # modifiers = QtCore.Qt.NoModifier
  646. pass
  647. key = QtGui.QKeySequence(key_text)
  648. # events from Vispy are of type KeyEvent
  649. else:
  650. key = event.key
  651. # Escape = Deselect All
  652. if key == QtCore.Qt.Key_Escape or key == 'Escape':
  653. if self.mouse_events_connected is True:
  654. self.mouse_events_connected = False
  655. if self.app.is_legacy is False:
  656. self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
  657. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
  658. else:
  659. self.app.plotcanvas.graph_event_disconnect(self.kp)
  660. self.app.plotcanvas.graph_event_disconnect(self.mr)
  661. self.app.kp = self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent)
  662. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot)
  663. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  664. self.app.on_mouse_click_release_over_plot)
  665. if self.ui.big_cursor_cb.get_value():
  666. # restore cursor
  667. self.app.on_cursor_type(val=self.old_cursor_type)
  668. # restore selection
  669. self.app.defaults['global_selection_shape'] = self.old_selection_state
  670. # Grid toggle
  671. if key == QtCore.Qt.Key_G or key == 'G':
  672. self.app.ui.grid_snap_btn.trigger()
  673. # Jump to coords
  674. if key == QtCore.Qt.Key_J or key == 'J':
  675. l_x, l_y = self.app.on_jump_to()
  676. def on_autolevelling(self, state):
  677. self.ui.al_frame.show() if state else self.ui.al_frame.hide()
  678. self.app.defaults["cncjob_al_status"] = True if state else False
  679. def on_show_al_table(self, state):
  680. self.ui.al_probe_points_table.show() if state else self.ui.al_probe_points_table.hide()
  681. def on_mode_radio(self, val):
  682. # reset al table
  683. self.ui.al_probe_points_table.setRowCount(0)
  684. # reset the al dict
  685. self.al_geometry_dict.clear()
  686. # build AL table
  687. self.build_al_table()
  688. if val == "manual":
  689. self.ui.al_rows_entry.setDisabled(True)
  690. self.ui.al_rows_label.setDisabled(True)
  691. self.ui.al_columns_entry.setDisabled(True)
  692. self.ui.al_columns_label.setDisabled(True)
  693. else:
  694. self.ui.al_rows_entry.setDisabled(False)
  695. self.ui.al_rows_label.setDisabled(False)
  696. self.ui.al_columns_entry.setDisabled(False)
  697. self.ui.al_columns_label.setDisabled(False)
  698. def on_controller_change(self):
  699. if self.ui.al_controller_combo.get_value() == 'GRBL':
  700. self.ui.h_gcode_button.hide()
  701. self.ui.view_h_gcode_button.hide()
  702. self.ui.import_heights_button.hide()
  703. self.ui.grbl_frame.show()
  704. self.on_search_ports(muted=True)
  705. else:
  706. self.ui.h_gcode_button.show()
  707. self.ui.view_h_gcode_button.show()
  708. self.ui.import_heights_button.show()
  709. self.ui.grbl_frame.hide()
  710. def list_serial_ports(self):
  711. """
  712. Lists serial port names.
  713. From here: https://stackoverflow.com/questions/12090503/listing-available-com-ports-with-python
  714. :raises EnvironmentError: On unsupported or unknown platforms
  715. :returns: A list of the serial ports available on the system
  716. """
  717. if sys.platform.startswith('win'):
  718. ports = ['COM%s' % (i + 1) for i in range(256)]
  719. elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'):
  720. # this excludes your current terminal "/dev/tty"
  721. ports = glob.glob('/dev/tty[A-Za-z]*')
  722. elif sys.platform.startswith('darwin'):
  723. ports = glob.glob('/dev/tty.*')
  724. else:
  725. raise EnvironmentError('Unsupported platform')
  726. result = []
  727. s = serial.Serial()
  728. for port in ports:
  729. s.port = port
  730. try:
  731. s.open()
  732. s.close()
  733. result.append(port)
  734. except (OSError, serial.SerialException):
  735. # result.append(port + " (in use)")
  736. pass
  737. return result
  738. def on_search_ports(self, muted=None):
  739. port_list = self.list_serial_ports()
  740. self.ui.com_list_combo.clear()
  741. self.ui.com_list_combo.addItems(port_list)
  742. if muted is not True:
  743. self.app.inform.emit('[WARNING_NOTCL] %s' % _("COM list updated ..."))
  744. def on_connect_grbl(self):
  745. port_name = self.ui.com_list_combo.currentText()
  746. if " (" in port_name:
  747. port_name = port_name.rpartition(" (")[0]
  748. baudrate = int(self.ui.baudrates_list_combo.currentText())
  749. try:
  750. self.grbl_ser_port = serial.serial_for_url(port_name, baudrate,
  751. bytesize=serial.EIGHTBITS,
  752. parity=serial.PARITY_NONE,
  753. stopbits=serial.STOPBITS_ONE,
  754. timeout=0.1,
  755. xonxoff=False,
  756. rtscts=False)
  757. self.app.inform.emit("%s: %s" % (_("Port connected"), port_name))
  758. self.ui.com_connect_button.setStyleSheet("QPushButton {color: seagreen;}")
  759. # Toggle DTR to reset the controller loaded with GRBL (Arduino, ESP32, etc)
  760. try:
  761. self.grbl_ser_port.dtr = False
  762. except IOError:
  763. pass
  764. self.grbl_ser_port.reset_input_buffer()
  765. try:
  766. self.grbl_ser_port.dtr = True
  767. except IOError:
  768. pass
  769. except serial.SerialException:
  770. self.grbl_ser_port = serial.Serial()
  771. self.grbl_ser_port.port = port_name
  772. self.grbl_ser_port.close()
  773. self.ui.com_connect_button.setStyleSheet("")
  774. self.app.inform.emit("%s: %s" % (_("Port is connected. Disconnecting"), port_name))
  775. except Exception:
  776. self.app.inform.emit("[ERROR_NOTCL] %s: %s" % (_("Could not connect to port"), port_name))
  777. def on_add_baudrate_grbl(self):
  778. new_bd = str(self.ui.new_baudrate_entry.get_value())
  779. if int(new_bd) >= 40 and new_bd not in self.ui.baudrates_list_combo.model().stringList():
  780. self.ui.baudrates_list_combo.addItem(new_bd)
  781. self.ui.baudrates_list_combo.setCurrentText(new_bd)
  782. def on_delete_baudrate_grbl(self):
  783. current_idx = self.ui.baudrates_list_combo.currentIndex()
  784. self.ui.baudrates_list_combo.removeItem(current_idx)
  785. def probing_gcode(self, coords, pr_travel, probe_fr, pr_depth, controller):
  786. """
  787. :param coords: a list of (x, y) tuples of probe points coordinates
  788. :type coords: list
  789. :param pr_travel: the height (z) where the probe travel between probe points
  790. :type pr_travel: float
  791. :param probe_fr: feedrate when probing
  792. :type probe_fr: float
  793. :param pr_depth: how much to lower the probe searching for contact
  794. :type pr_depth: float
  795. :param controller: a string with the name of the GCode sender for which to create the probing GCode.
  796. Can be: 'MACH3', 'MACH4', 'LinuxCNC', 'GRBL'
  797. :type controller: str
  798. :return: Probing GCode
  799. :rtype: str
  800. """
  801. p_gcode = ''
  802. header = ''
  803. time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
  804. header += '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \
  805. (str(self.app.version), str(self.app.version_date)) + '\n'
  806. header += '(This is a autolevelling probing GCode.)\n' \
  807. '(Make sure that before you start the job you first do a zero for all axis.)\n\n'
  808. header += '(Name: ' + str(self.options['name']) + ')\n'
  809. header += '(Type: ' + "Autolevelling Probing GCode " + ')\n'
  810. header += '(Units: ' + self.units.upper() + ')\n'
  811. header += '(Created on ' + time_str + ')\n'
  812. # commands
  813. if controller == 'MACH3':
  814. probing_command = 'G31'
  815. probing_var = '#2002'
  816. openfile_command = 'M40'
  817. closefile_command = 'M41'
  818. elif controller == 'MACH4':
  819. probing_command = 'G31'
  820. probing_var = '#5063'
  821. openfile_command = 'M40'
  822. closefile_command = 'M41'
  823. elif controller == 'LinuxCNC':
  824. probing_command = 'G38.2'
  825. probing_var = '#5422'
  826. openfile_command = '(PROBEOPEN a_probing_points_file.txt)'
  827. closefile_command = '(PROBECLOSE)'
  828. else:
  829. log.debug("CNCJobObject.probing_gcode() -> controller not supported")
  830. return
  831. # #############################################################################################################
  832. # ########################### GCODE construction ##############################################################
  833. # #############################################################################################################
  834. # header
  835. p_gcode += header + '\n'
  836. # supplementary message for LinuxCNC
  837. if controller == 'LinuxCNC':
  838. probing_var += "The file with the stored probing points can be found\n" \
  839. "in the configuration folder for LinuxCNC.\n" \
  840. "The name of the file is: a_probing_points_file.txt.\n"
  841. # units
  842. p_gcode += 'G21\n' if self.units == 'MM' else 'G20\n'
  843. # reference mode = absolute
  844. p_gcode += 'G90\n'
  845. # open a new file
  846. p_gcode += openfile_command + '\n'
  847. # move to safe height (probe travel Z)
  848. p_gcode += 'G0 Z%s\n' % str(self.app.dec_format(pr_travel, self.coords_decimals))
  849. # probing points
  850. for idx, xy_tuple in enumerate(coords, 1): # index starts from 1
  851. x = xy_tuple[0]
  852. y = xy_tuple[1]
  853. # move to probing point
  854. p_gcode += "G0 X%sY%s\n" % (
  855. str(self.app.dec_format(x, self.coords_decimals)),
  856. str(self.app.dec_format(y, self.coords_decimals))
  857. )
  858. # do the probing
  859. p_gcode += "%s Z%s F%s\n" % (
  860. probing_command,
  861. str(self.app.dec_format(pr_depth, self.coords_decimals)),
  862. str(self.app.dec_format(probe_fr, self.fr_decimals)),
  863. )
  864. # store in a global numeric variable the value of the detected probe Z
  865. # I offset the global numeric variable by 500 so it does not conflict with something else
  866. temp_var = int(idx + 500)
  867. p_gcode += "#%d = %s\n" % (temp_var, probing_var)
  868. # move to safe height (probe travel Z)
  869. p_gcode += 'G0 Z%s\n' % str(self.app.dec_format(pr_travel, self.coords_decimals))
  870. # close the file
  871. p_gcode += closefile_command + '\n'
  872. # finish the GCode
  873. p_gcode += 'M2'
  874. return p_gcode
  875. def on_generate_probing_gcode(self):
  876. coords = []
  877. for id_key, value in self.al_geometry_dict.items():
  878. x = value['point'].x
  879. y = value['point'].y
  880. coords.append(
  881. (
  882. self.app.dec_format(x, dec=self.app.decimals),
  883. self.app.dec_format(y, dec=self.app.decimals)
  884. )
  885. )
  886. pr_travel = self.ui.ptravelz_entry.get_value()
  887. probe_fr = self.ui.feedrate_probe_entry.get_value()
  888. pr_depth = self.ui.pdepth_entry.get_value()
  889. controller = self.ui.al_controller_combo.get_value()
  890. self.probing_gcode_text = self.probing_gcode(coords, pr_travel, probe_fr, pr_depth, controller)
  891. def on_view_probing_gcode(self):
  892. self.app.proc_container.view.set_busy(_("Loading..."))
  893. gco = self.probing_gcode_text
  894. if gco is None or gco == '':
  895. self.app.inform.emit('[WARNING_NOTCL] %s...' % _('There is nothing to view'))
  896. return
  897. self.gcode_viewer_tab = AppTextEditor(app=self.app, plain_text=True)
  898. # add the tab if it was closed
  899. self.app.ui.plot_tab_area.addTab(self.gcode_viewer_tab, '%s' % _("Code Viewer"))
  900. self.gcode_viewer_tab.setObjectName('code_viewer_tab')
  901. # delete the absolute and relative position and messages in the infobar
  902. self.app.ui.position_label.setText("")
  903. self.app.ui.rel_position_label.setText("")
  904. self.gcode_viewer_tab.code_editor.completer_enable = False
  905. self.gcode_viewer_tab.buttonRun.hide()
  906. # Switch plot_area to CNCJob tab
  907. self.app.ui.plot_tab_area.setCurrentWidget(self.gcode_viewer_tab)
  908. self.gcode_viewer_tab.t_frame.hide()
  909. # then append the text from GCode to the text editor
  910. try:
  911. self.gcode_viewer_tab.load_text(gco, move_to_start=True, clear_text=True)
  912. except Exception as e:
  913. log.debug('FlatCAMCNCJob.on_edit_code_click() -->%s' % str(e))
  914. return
  915. self.gcode_viewer_tab.t_frame.show()
  916. self.app.proc_container.view.set_idle()
  917. self.gcode_viewer_tab.buttonSave.hide()
  918. self.gcode_viewer_tab.buttonOpen.hide()
  919. self.gcode_viewer_tab.buttonPrint.hide()
  920. self.gcode_viewer_tab.buttonPreview.hide()
  921. self.gcode_viewer_tab.buttonReplace.hide()
  922. self.gcode_viewer_tab.sel_all_cb.hide()
  923. self.gcode_viewer_tab.entryReplace.hide()
  924. self.gcode_viewer_tab.code_editor.setReadOnly(True)
  925. self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Viewer'))
  926. def on_import_height_map(self):
  927. """
  928. Import the height map file into the app
  929. :return:
  930. :rtype:
  931. """
  932. _filter_ = "Text File .txt (*.txt);;All Files (*.*)"
  933. try:
  934. filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import Height Map"),
  935. directory=self.app.get_last_folder(),
  936. filter=_filter_)
  937. except TypeError:
  938. filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import Height Map"),
  939. filter=_filter_)
  940. filename = str(filename)
  941. if filename == '':
  942. self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
  943. else:
  944. self.app.worker_task.emit({'fcn': self.import_height_map, 'params': [filename]})
  945. def import_height_map(self, filename):
  946. """
  947. :param filename:
  948. :type filename:
  949. :return:
  950. :rtype:
  951. """
  952. stream = ''
  953. try:
  954. if filename:
  955. with open(filename, 'r') as f:
  956. stream = f.readlines()
  957. else:
  958. return
  959. except IOError:
  960. log.error("Failed to open height map file: %s" % filename)
  961. self.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open height map file"), filename))
  962. return
  963. idx = 0
  964. for line in stream:
  965. if line != '':
  966. idx += 1
  967. line = line.replace(' ', ',').replace('\n', '').split(',')
  968. if idx not in self.al_geometry_dict:
  969. self.al_geometry_dict[idx] = {}
  970. self.al_geometry_dict[idx]['height'] = float(line[2])
  971. if 'point' not in self.al_geometry_dict[idx]:
  972. x = float(line[0])
  973. y = float(line[1])
  974. self.al_geometry_dict[idx]['point'] = Point((x, y))
  975. self.build_al_table_sig.emit()
  976. def on_updateplot_button_click(self, *args):
  977. """
  978. Callback for the "Updata Plot" button. Reads the form for updates
  979. and plots the object.
  980. """
  981. self.read_form()
  982. self.on_plot_kind_change()
  983. def on_plot_kind_change(self):
  984. kind = self.ui.cncplot_method_combo.get_value()
  985. def worker_task():
  986. with self.app.proc_container.new(_("Plotting...")):
  987. self.plot(kind=kind)
  988. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  989. def on_exportgcode_button_click(self):
  990. """
  991. Handler activated by a button clicked when exporting GCode.
  992. :param args:
  993. :return:
  994. """
  995. self.app.defaults.report_usage("cncjob_on_exportgcode_button")
  996. self.read_form()
  997. name = self.app.collection.get_active().options['name']
  998. save_gcode = False
  999. if 'Roland' in self.pp_excellon_name or 'Roland' in self.pp_geometry_name:
  1000. _filter_ = "RML1 Files .rol (*.rol);;All Files (*.*)"
  1001. elif 'hpgl' in self.pp_geometry_name:
  1002. _filter_ = "HPGL Files .plt (*.plt);;All Files (*.*)"
  1003. else:
  1004. save_gcode = True
  1005. _filter_ = self.app.defaults['cncjob_save_filters']
  1006. try:
  1007. dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name)
  1008. filename, _f = FCFileSaveDialog.get_saved_filename(
  1009. caption=_("Export Code ..."),
  1010. directory=dir_file_to_save,
  1011. ext_filter=_filter_
  1012. )
  1013. except TypeError:
  1014. filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export Code ..."), ext_filter=_filter_)
  1015. self.export_gcode_handler(filename, is_gcode=save_gcode)
  1016. def export_gcode_handler(self, filename, is_gcode=True):
  1017. preamble = ''
  1018. postamble = ''
  1019. filename = str(filename)
  1020. if filename == '':
  1021. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export cancelled ..."))
  1022. return
  1023. else:
  1024. if is_gcode is True:
  1025. used_extension = filename.rpartition('.')[2]
  1026. self.update_filters(last_ext=used_extension, filter_string='cncjob_save_filters')
  1027. new_name = os.path.split(str(filename))[1].rpartition('.')[0]
  1028. self.ui.name_entry.set_value(new_name)
  1029. self.on_name_activate(silent=True)
  1030. try:
  1031. if self.ui.snippets_cb.get_value():
  1032. preamble = self.append_snippet
  1033. postamble = self.prepend_snippet
  1034. gc = self.export_gcode(filename, preamble=preamble, postamble=postamble)
  1035. except Exception as err:
  1036. log.debug("CNCJobObject.export_gcode_handler() --> %s" % str(err))
  1037. gc = self.export_gcode(filename)
  1038. if gc == 'fail':
  1039. return
  1040. if self.app.defaults["global_open_style"] is False:
  1041. self.app.file_opened.emit("gcode", filename)
  1042. self.app.file_saved.emit("gcode", filename)
  1043. self.app.inform.emit('[success] %s: %s' % (_("File saved to"), filename))
  1044. def on_edit_code_click(self, *args):
  1045. """
  1046. Handler activated by a button clicked when reviewing GCode.
  1047. :param args:
  1048. :return:
  1049. """
  1050. self.app.proc_container.view.set_busy(_("Loading..."))
  1051. preamble = self.append_snippet
  1052. postamble = self.prepend_snippet
  1053. gco = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True)
  1054. if gco == 'fail':
  1055. return
  1056. else:
  1057. self.app.gcode_edited = gco
  1058. self.gcode_editor_tab = AppTextEditor(app=self.app, plain_text=True)
  1059. # add the tab if it was closed
  1060. self.app.ui.plot_tab_area.addTab(self.gcode_editor_tab, '%s' % _("Code Review"))
  1061. self.gcode_editor_tab.setObjectName('code_editor_tab')
  1062. # delete the absolute and relative position and messages in the infobar
  1063. self.app.ui.position_label.setText("")
  1064. self.app.ui.rel_position_label.setText("")
  1065. self.gcode_editor_tab.code_editor.completer_enable = False
  1066. self.gcode_editor_tab.buttonRun.hide()
  1067. # Switch plot_area to CNCJob tab
  1068. self.app.ui.plot_tab_area.setCurrentWidget(self.gcode_editor_tab)
  1069. self.gcode_editor_tab.t_frame.hide()
  1070. # then append the text from GCode to the text editor
  1071. try:
  1072. self.gcode_editor_tab.load_text(self.app.gcode_edited.getvalue(), move_to_start=True, clear_text=True)
  1073. except Exception as e:
  1074. log.debug('FlatCAMCNCJob.on_edit_code_click() -->%s' % str(e))
  1075. return
  1076. self.gcode_editor_tab.t_frame.show()
  1077. self.app.proc_container.view.set_idle()
  1078. self.gcode_editor_tab.buttonSave.hide()
  1079. self.gcode_editor_tab.buttonOpen.hide()
  1080. self.gcode_editor_tab.buttonPrint.hide()
  1081. self.gcode_editor_tab.buttonPreview.hide()
  1082. self.gcode_editor_tab.buttonReplace.hide()
  1083. self.gcode_editor_tab.sel_all_cb.hide()
  1084. self.gcode_editor_tab.entryReplace.hide()
  1085. self.gcode_editor_tab.code_editor.setReadOnly(True)
  1086. self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Editor'))
  1087. def on_update_source_file(self):
  1088. self.source_file = self.gcode_editor_tab.code_editor.toPlainText()
  1089. def gcode_header(self, comment_start_symbol=None, comment_stop_symbol=None):
  1090. """
  1091. Will create a header to be added to all GCode files generated by FlatCAM
  1092. :param comment_start_symbol: A symbol to be used as the first symbol in a comment
  1093. :param comment_stop_symbol: A symbol to be used as the last symbol in a comment
  1094. :return: A string with a GCode header
  1095. """
  1096. log.debug("FlatCAMCNCJob.gcode_header()")
  1097. time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
  1098. marlin = False
  1099. hpgl = False
  1100. probe_pp = False
  1101. gcode = ''
  1102. start_comment = comment_start_symbol if comment_start_symbol is not None else '('
  1103. stop_comment = comment_stop_symbol if comment_stop_symbol is not None else ')'
  1104. try:
  1105. for key in self.cnc_tools:
  1106. ppg = self.cnc_tools[key]['data']['ppname_g']
  1107. if 'marlin' in ppg.lower() or 'repetier' in ppg.lower():
  1108. marlin = True
  1109. break
  1110. if ppg == 'hpgl':
  1111. hpgl = True
  1112. break
  1113. if "toolchange_probe" in ppg.lower():
  1114. probe_pp = True
  1115. break
  1116. except KeyError:
  1117. # log.debug("FlatCAMCNCJob.gcode_header() error: --> %s" % str(e))
  1118. pass
  1119. try:
  1120. if 'marlin' in self.options['ppname_e'].lower() or 'repetier' in self.options['ppname_e'].lower():
  1121. marlin = True
  1122. except KeyError:
  1123. # log.debug("FlatCAMCNCJob.gcode_header(): --> There is no such self.option: %s" % str(e))
  1124. pass
  1125. try:
  1126. if "toolchange_probe" in self.options['ppname_e'].lower():
  1127. probe_pp = True
  1128. except KeyError:
  1129. # log.debug("FlatCAMCNCJob.gcode_header(): --> There is no such self.option: %s" % str(e))
  1130. pass
  1131. if marlin is True:
  1132. gcode += ';Marlin(Repetier) G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s\n' % \
  1133. (str(self.app.version), str(self.app.version_date)) + '\n'
  1134. gcode += ';Name: ' + str(self.options['name']) + '\n'
  1135. gcode += ';Type: ' + "G-code from " + str(self.options['type']) + '\n'
  1136. # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
  1137. # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
  1138. gcode += ';Units: ' + self.units.upper() + '\n' + "\n"
  1139. gcode += ';Created on ' + time_str + '\n' + '\n'
  1140. elif hpgl is True:
  1141. gcode += 'CO "HPGL CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s' % \
  1142. (str(self.app.version), str(self.app.version_date)) + '";\n'
  1143. gcode += 'CO "Name: ' + str(self.options['name']) + '";\n'
  1144. gcode += 'CO "Type: ' + "HPGL code from " + str(self.options['type']) + '";\n'
  1145. # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
  1146. # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
  1147. gcode += 'CO "Units: ' + self.units.upper() + '";\n'
  1148. gcode += 'CO "Created on ' + time_str + '";\n'
  1149. elif probe_pp is True:
  1150. gcode += '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \
  1151. (str(self.app.version), str(self.app.version_date)) + '\n'
  1152. gcode += '(This GCode tool change is done by using a Probe.)\n' \
  1153. '(Make sure that before you start the job you first do a rough zero for Z axis.)\n' \
  1154. '(This means that you need to zero the CNC axis and then jog to the toolchange X, Y location,)\n' \
  1155. '(mount the probe and adjust the Z so more or less the probe tip touch the plate. ' \
  1156. 'Then zero the Z axis.)\n' + '\n'
  1157. gcode += '(Name: ' + str(self.options['name']) + ')\n'
  1158. gcode += '(Type: ' + "G-code from " + str(self.options['type']) + ')\n'
  1159. # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
  1160. # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
  1161. gcode += '(Units: ' + self.units.upper() + ')\n' + "\n"
  1162. gcode += '(Created on ' + time_str + ')\n' + '\n'
  1163. else:
  1164. gcode += '%sG-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s%s\n' % \
  1165. (start_comment, str(self.app.version), str(self.app.version_date), stop_comment) + '\n'
  1166. gcode += '%sName: ' % start_comment + str(self.options['name']) + '%s\n' % stop_comment
  1167. gcode += '%sType: ' % start_comment + "G-code from " + str(self.options['type']) + '%s\n' % stop_comment
  1168. # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
  1169. # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
  1170. gcode += '%sUnits: ' % start_comment + self.units.upper() + '%s\n' % stop_comment + "\n"
  1171. gcode += '%sCreated on ' % start_comment + time_str + '%s\n' % stop_comment + '\n'
  1172. return gcode
  1173. @staticmethod
  1174. def gcode_footer(end_command=None):
  1175. """
  1176. Will add the M02 to the end of GCode, if requested.
  1177. :param end_command: 'M02' or 'M30' - String
  1178. :return:
  1179. """
  1180. if end_command:
  1181. return end_command
  1182. else:
  1183. return 'M02'
  1184. def export_gcode(self, filename=None, preamble='', postamble='', to_file=False, from_tcl=False):
  1185. """
  1186. This will save the GCode from the Gcode object to a file on the OS filesystem
  1187. :param filename: filename for the GCode file
  1188. :param preamble: a custom Gcode block to be added at the beginning of the Gcode file
  1189. :param postamble: a custom Gcode block to be added at the end of the Gcode file
  1190. :param to_file: if False then no actual file is saved but the app will know that a file was created
  1191. :param from_tcl: True if run from Tcl Shell
  1192. :return: None
  1193. """
  1194. # gcode = ''
  1195. # roland = False
  1196. # hpgl = False
  1197. # isel_icp = False
  1198. include_header = True
  1199. if preamble == '':
  1200. preamble = self.app.defaults["cncjob_prepend"]
  1201. if postamble == '':
  1202. preamble = self.app.defaults["cncjob_append"]
  1203. try:
  1204. if self.special_group:
  1205. self.app.inform.emit('[WARNING_NOTCL] %s %s %s.' %
  1206. (_("This CNCJob object can't be processed because it is a"),
  1207. str(self.special_group),
  1208. _("CNCJob object")))
  1209. return 'fail'
  1210. except AttributeError:
  1211. pass
  1212. # if this dict is not empty then the object is a Geometry object
  1213. if self.cnc_tools:
  1214. first_key = next(iter(self.cnc_tools))
  1215. include_header = self.app.preprocessors[self.cnc_tools[first_key]['data']['ppname_g']].include_header
  1216. # if this dict is not empty then the object is an Excellon object
  1217. if self.exc_cnc_tools:
  1218. first_key = next(iter(self.exc_cnc_tools))
  1219. include_header = self.app.preprocessors[
  1220. self.exc_cnc_tools[first_key]['data']['tools_drill_ppname_e']
  1221. ].include_header
  1222. gcode = ''
  1223. if include_header is False:
  1224. g = preamble
  1225. # detect if using multi-tool and make the Gcode summation correctly for each case
  1226. if self.multitool is True:
  1227. for tooluid_key in self.cnc_tools:
  1228. for key, value in self.cnc_tools[tooluid_key].items():
  1229. if key == 'gcode':
  1230. gcode += value
  1231. break
  1232. else:
  1233. gcode += self.gcode
  1234. g = g + gcode + postamble
  1235. else:
  1236. # search for the GCode beginning which is usually a G20 or G21
  1237. # fix so the preamble gets inserted in between the comments header and the actual start of GCODE
  1238. # g_idx = gcode.rfind('G20')
  1239. #
  1240. # # if it did not find 'G20' then search for 'G21'
  1241. # if g_idx == -1:
  1242. # g_idx = gcode.rfind('G21')
  1243. #
  1244. # # if it did not find 'G20' and it did not find 'G21' then there is an error and return
  1245. # if g_idx == -1:
  1246. # self.app.inform.emit('[ERROR_NOTCL] %s' % _("G-code does not have a units code: either G20 or G21"))
  1247. # return
  1248. # detect if using multi-tool and make the Gcode summation correctly for each case
  1249. if self.multitool is True:
  1250. if self.origin_kind == 'excellon':
  1251. for tooluid_key in self.exc_cnc_tools:
  1252. for key, value in self.exc_cnc_tools[tooluid_key].items():
  1253. if key == 'gcode' and value:
  1254. gcode += value
  1255. break
  1256. else:
  1257. for tooluid_key in self.cnc_tools:
  1258. for key, value in self.cnc_tools[tooluid_key].items():
  1259. if key == 'gcode' and value:
  1260. gcode += value
  1261. break
  1262. else:
  1263. gcode += self.gcode
  1264. end_gcode = self.gcode_footer() if self.app.defaults['cncjob_footer'] is True else ''
  1265. # detect if using a HPGL preprocessor
  1266. hpgl = False
  1267. if self.cnc_tools:
  1268. for key in self.cnc_tools:
  1269. if 'ppname_g' in self.cnc_tools[key]['data']:
  1270. if 'hpgl' in self.cnc_tools[key]['data']['ppname_g']:
  1271. hpgl = True
  1272. break
  1273. elif self.exc_cnc_tools:
  1274. for key in self.cnc_tools:
  1275. if 'ppname_e' in self.cnc_tools[key]['data']:
  1276. if 'hpgl' in self.cnc_tools[key]['data']['ppname_e']:
  1277. hpgl = True
  1278. break
  1279. if hpgl:
  1280. processed_gcode = ''
  1281. pa_re = re.compile(r"^PA\s*(-?\d+\.\d*),?\s*(-?\d+\.\d*)*;?$")
  1282. for gline in gcode.splitlines():
  1283. match = pa_re.search(gline)
  1284. if match:
  1285. x_int = int(float(match.group(1)))
  1286. y_int = int(float(match.group(2)))
  1287. new_line = 'PA%d,%d;\n' % (x_int, y_int)
  1288. processed_gcode += new_line
  1289. else:
  1290. processed_gcode += gline + '\n'
  1291. gcode = processed_gcode
  1292. g = self.gc_header + '\n' + preamble + '\n' + gcode + postamble + end_gcode
  1293. else:
  1294. try:
  1295. g_idx = gcode.index('G94')
  1296. if preamble != '' and postamble != '':
  1297. g = self.gc_header + gcode[:g_idx + 3] + '\n' + preamble + '\n' + \
  1298. gcode[(g_idx + 3):] + postamble + end_gcode
  1299. elif preamble == '':
  1300. g = self.gc_header + gcode[:g_idx + 3] + '\n' + \
  1301. gcode[(g_idx + 3):] + postamble + end_gcode
  1302. elif postamble == '':
  1303. g = self.gc_header + gcode[:g_idx + 3] + '\n' + preamble + '\n' + \
  1304. gcode[(g_idx + 3):] + end_gcode
  1305. else:
  1306. g = self.gc_header + gcode[:g_idx + 3] + gcode[(g_idx + 3):] + end_gcode
  1307. except ValueError:
  1308. self.app.inform.emit('[ERROR_NOTCL] %s' %
  1309. _("G-code does not have a G94 code.\n"
  1310. "Append Code snippet will not be used.."))
  1311. g = self.gc_header + '\n' + gcode + postamble + end_gcode
  1312. # if toolchange custom is used, replace M6 code with the code from the Toolchange Custom Text box
  1313. # if self.ui.toolchange_cb.get_value() is True:
  1314. # # match = self.re_toolchange.search(g)
  1315. # if 'M6' in g:
  1316. # m6_code = self.parse_custom_toolchange_code(self.ui.toolchange_text.get_value())
  1317. # if m6_code is None or m6_code == '':
  1318. # self.app.inform.emit(
  1319. # '[ERROR_NOTCL] %s' % _("Cancelled. The Toolchange Custom code is enabled but it's empty.")
  1320. # )
  1321. # return 'fail'
  1322. #
  1323. # g = g.replace('M6', m6_code)
  1324. # self.app.inform.emit('[success] %s' % _("Toolchange G-code was replaced by a custom code."))
  1325. lines = StringIO(g)
  1326. # Write
  1327. if filename is not None:
  1328. try:
  1329. force_windows_line_endings = self.app.defaults['cncjob_line_ending']
  1330. if force_windows_line_endings and sys.platform != 'win32':
  1331. with open(filename, 'w', newline='\r\n') as f:
  1332. for line in lines:
  1333. f.write(line)
  1334. else:
  1335. with open(filename, 'w') as f:
  1336. for line in lines:
  1337. f.write(line)
  1338. except FileNotFoundError:
  1339. self.app.inform.emit('[WARNING_NOTCL] %s' % _("No such file or directory"))
  1340. return
  1341. except PermissionError:
  1342. self.app.inform.emit(
  1343. '[WARNING] %s' % _("Permission denied, saving not possible.\n"
  1344. "Most likely another app is holding the file open and not accessible.")
  1345. )
  1346. return 'fail'
  1347. elif to_file is False:
  1348. # Just for adding it to the recent files list.
  1349. if self.app.defaults["global_open_style"] is False:
  1350. self.app.file_opened.emit("cncjob", filename)
  1351. self.app.file_saved.emit("cncjob", filename)
  1352. self.app.inform.emit('[success] %s: %s' % (_("Saved to"), filename))
  1353. else:
  1354. return lines
  1355. # def on_toolchange_custom_clicked(self, signal):
  1356. # """
  1357. # Handler for clicking toolchange custom.
  1358. #
  1359. # :param signal:
  1360. # :return:
  1361. # """
  1362. #
  1363. # try:
  1364. # if 'toolchange_custom' not in str(self.options['ppname_e']).lower():
  1365. # if self.ui.toolchange_cb.get_value():
  1366. # self.ui.toolchange_cb.set_value(False)
  1367. # self.app.inform.emit('[WARNING_NOTCL] %s' %
  1368. # _("The used preprocessor file has to have in it's name: 'toolchange_custom'"))
  1369. # except KeyError:
  1370. # try:
  1371. # for key in self.cnc_tools:
  1372. # ppg = self.cnc_tools[key]['data']['ppname_g']
  1373. # if 'toolchange_custom' not in str(ppg).lower():
  1374. # if self.ui.toolchange_cb.get_value():
  1375. # self.ui.toolchange_cb.set_value(False)
  1376. # self.app.inform.emit('[WARNING_NOTCL] %s' %
  1377. # _("The used preprocessor file has to have in it's name: "
  1378. # "'toolchange_custom'"))
  1379. # except KeyError:
  1380. # self.app.inform.emit('[ERROR] %s' % _("There is no preprocessor file."))
  1381. def get_gcode(self, preamble='', postamble=''):
  1382. """
  1383. We need this to be able to get_gcode separately for shell command export_gcode
  1384. :param preamble: Extra GCode added to the beginning of the GCode
  1385. :param postamble: Extra GCode added at the end of the GCode
  1386. :return: The modified GCode
  1387. """
  1388. return preamble + '\n' + self.gcode + "\n" + postamble
  1389. def get_svg(self):
  1390. # we need this to be able get_svg separately for shell command export_svg
  1391. pass
  1392. def on_plot_cb_click(self, *args):
  1393. """
  1394. Handler for clicking on the Plot checkbox.
  1395. :param args:
  1396. :return:
  1397. """
  1398. if self.muted_ui:
  1399. return
  1400. kind = self.ui.cncplot_method_combo.get_value()
  1401. self.plot(kind=kind)
  1402. self.read_form_item('plot')
  1403. self.ui_disconnect()
  1404. cb_flag = self.ui.plot_cb.isChecked()
  1405. for row in range(self.ui.cnc_tools_table.rowCount()):
  1406. table_cb = self.ui.cnc_tools_table.cellWidget(row, 6)
  1407. if cb_flag:
  1408. table_cb.setChecked(True)
  1409. else:
  1410. table_cb.setChecked(False)
  1411. self.ui_connect()
  1412. def on_plot_cb_click_table(self):
  1413. """
  1414. Handler for clicking the plot checkboxes added into a Table on each row. Purpose: toggle visibility for the
  1415. tool/aperture found on that row.
  1416. :return:
  1417. """
  1418. # self.ui.cnc_tools_table.cellWidget(row, 2).widget().setCheckState(QtCore.Qt.Unchecked)
  1419. self.ui_disconnect()
  1420. # cw = self.sender()
  1421. # cw_index = self.ui.cnc_tools_table.indexAt(cw.pos())
  1422. # cw_row = cw_index.row()
  1423. kind = self.ui.cncplot_method_combo.get_value()
  1424. self.shapes.clear(update=True)
  1425. if self.origin_kind == "excellon":
  1426. for r in range(self.ui.exc_cnc_tools_table.rowCount()):
  1427. row_dia = float('%.*f' % (self.decimals, float(self.ui.exc_cnc_tools_table.item(r, 1).text())))
  1428. for tooluid_key in self.exc_cnc_tools:
  1429. tooldia = float('%.*f' % (self.decimals, float(tooluid_key)))
  1430. if row_dia == tooldia:
  1431. gcode_parsed = self.exc_cnc_tools[tooluid_key]['gcode_parsed']
  1432. if self.ui.exc_cnc_tools_table.cellWidget(r, 6).isChecked():
  1433. self.plot2(tooldia=tooldia, obj=self, visible=True, gcode_parsed=gcode_parsed, kind=kind)
  1434. else:
  1435. for tooluid_key in self.cnc_tools:
  1436. tooldia = float('%.*f' % (self.decimals, float(self.cnc_tools[tooluid_key]['tooldia'])))
  1437. gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
  1438. # tool_uid = int(self.ui.cnc_tools_table.item(cw_row, 3).text())
  1439. for r in range(self.ui.cnc_tools_table.rowCount()):
  1440. if int(self.ui.cnc_tools_table.item(r, 5).text()) == int(tooluid_key):
  1441. if self.ui.cnc_tools_table.cellWidget(r, 6).isChecked():
  1442. self.plot2(tooldia=tooldia, obj=self, visible=True, gcode_parsed=gcode_parsed, kind=kind)
  1443. self.shapes.redraw()
  1444. # make sure that the general plot is disabled if one of the row plot's are disabled and
  1445. # if all the row plot's are enabled also enable the general plot checkbox
  1446. cb_cnt = 0
  1447. total_row = self.ui.cnc_tools_table.rowCount()
  1448. for row in range(total_row):
  1449. if self.ui.cnc_tools_table.cellWidget(row, 6).isChecked():
  1450. cb_cnt += 1
  1451. else:
  1452. cb_cnt -= 1
  1453. if cb_cnt < total_row:
  1454. self.ui.plot_cb.setChecked(False)
  1455. else:
  1456. self.ui.plot_cb.setChecked(True)
  1457. self.ui_connect()
  1458. def plot(self, visible=None, kind='all'):
  1459. """
  1460. # Does all the required setup and returns False
  1461. # if the 'ptint' option is set to False.
  1462. :param visible: Boolean to decide if the object will be plotted as visible or disabled on canvas
  1463. :param kind: String. Can be "all" or "travel" or "cut". For CNCJob plotting
  1464. :return: None
  1465. """
  1466. if not FlatCAMObj.plot(self):
  1467. return
  1468. visible = visible if visible else self.options['plot']
  1469. if self.app.is_legacy is False:
  1470. if self.ui.annotation_cb.get_value() and self.ui.plot_cb.get_value():
  1471. self.text_col.enabled = True
  1472. else:
  1473. self.text_col.enabled = False
  1474. self.annotation.redraw()
  1475. try:
  1476. if self.multitool is False: # single tool usage
  1477. try:
  1478. dia_plot = float(self.options["tooldia"])
  1479. except ValueError:
  1480. # we may have a tuple with only one element and a comma
  1481. dia_plot = [float(el) for el in self.options["tooldia"].split(',') if el != ''][0]
  1482. self.plot2(tooldia=dia_plot, obj=self, visible=visible, kind=kind)
  1483. else:
  1484. # I do this so the travel lines thickness will reflect the tool diameter
  1485. # may work only for objects created within the app and not Gcode imported from elsewhere for which we
  1486. # don't know the origin
  1487. if self.origin_kind == "excellon":
  1488. if self.exc_cnc_tools:
  1489. for tooldia_key in self.exc_cnc_tools:
  1490. tooldia = float('%.*f' % (self.decimals, float(tooldia_key)))
  1491. gcode_parsed = self.exc_cnc_tools[tooldia_key]['gcode_parsed']
  1492. if not gcode_parsed:
  1493. continue
  1494. # gcode_parsed = self.gcode_parsed
  1495. self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
  1496. else:
  1497. # multiple tools usage
  1498. if self.cnc_tools:
  1499. for tooluid_key in self.cnc_tools:
  1500. tooldia = float('%.*f' % (self.decimals, float(self.cnc_tools[tooluid_key]['tooldia'])))
  1501. gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
  1502. self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
  1503. self.shapes.redraw()
  1504. except (ObjectDeleted, AttributeError):
  1505. self.shapes.clear(update=True)
  1506. if self.app.is_legacy is False:
  1507. self.annotation.clear(update=True)
  1508. def on_annotation_change(self):
  1509. """
  1510. Handler for toggling the annotation display by clicking a checkbox.
  1511. :return:
  1512. """
  1513. if self.app.is_legacy is False:
  1514. if self.ui.annotation_cb.get_value():
  1515. self.text_col.enabled = True
  1516. else:
  1517. self.text_col.enabled = False
  1518. # kind = self.ui.cncplot_method_combo.get_value()
  1519. # self.plot(kind=kind)
  1520. self.annotation.redraw()
  1521. else:
  1522. kind = self.ui.cncplot_method_combo.get_value()
  1523. self.plot(kind=kind)
  1524. def convert_units(self, units):
  1525. """
  1526. Units conversion used by the CNCJob objects.
  1527. :param units: Can be "MM" or "IN"
  1528. :return:
  1529. """
  1530. log.debug("FlatCAMObj.FlatCAMECNCjob.convert_units()")
  1531. factor = CNCjob.convert_units(self, units)
  1532. self.options["tooldia"] = float(self.options["tooldia"]) * factor
  1533. param_list = ['cutz', 'depthperpass', 'travelz', 'feedrate', 'feedrate_z', 'feedrate_rapid',
  1534. 'endz', 'toolchangez']
  1535. temp_tools_dict = {}
  1536. tool_dia_copy = {}
  1537. data_copy = {}
  1538. for tooluid_key, tooluid_value in self.cnc_tools.items():
  1539. for dia_key, dia_value in tooluid_value.items():
  1540. if dia_key == 'tooldia':
  1541. dia_value *= factor
  1542. dia_value = float('%.*f' % (self.decimals, dia_value))
  1543. tool_dia_copy[dia_key] = dia_value
  1544. if dia_key == 'offset':
  1545. tool_dia_copy[dia_key] = dia_value
  1546. if dia_key == 'offset_value':
  1547. dia_value *= factor
  1548. tool_dia_copy[dia_key] = dia_value
  1549. if dia_key == 'type':
  1550. tool_dia_copy[dia_key] = dia_value
  1551. if dia_key == 'tool_type':
  1552. tool_dia_copy[dia_key] = dia_value
  1553. if dia_key == 'data':
  1554. for data_key, data_value in dia_value.items():
  1555. # convert the form fields that are convertible
  1556. for param in param_list:
  1557. if data_key == param and data_value is not None:
  1558. data_copy[data_key] = data_value * factor
  1559. # copy the other dict entries that are not convertible
  1560. if data_key not in param_list:
  1561. data_copy[data_key] = data_value
  1562. tool_dia_copy[dia_key] = deepcopy(data_copy)
  1563. data_copy.clear()
  1564. if dia_key == 'gcode':
  1565. tool_dia_copy[dia_key] = dia_value
  1566. if dia_key == 'gcode_parsed':
  1567. tool_dia_copy[dia_key] = dia_value
  1568. if dia_key == 'solid_geometry':
  1569. tool_dia_copy[dia_key] = dia_value
  1570. # if dia_key == 'solid_geometry':
  1571. # tool_dia_copy[dia_key] = affinity.scale(dia_value, xfact=factor, origin=(0, 0))
  1572. # if dia_key == 'gcode_parsed':
  1573. # for g in dia_value:
  1574. # g['geom'] = affinity.scale(g['geom'], factor, factor, origin=(0, 0))
  1575. #
  1576. # tool_dia_copy['gcode_parsed'] = deepcopy(dia_value)
  1577. # tool_dia_copy['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_value])
  1578. temp_tools_dict.update({
  1579. tooluid_key: deepcopy(tool_dia_copy)
  1580. })
  1581. tool_dia_copy.clear()
  1582. self.cnc_tools.clear()
  1583. self.cnc_tools = deepcopy(temp_tools_dict)