FlatCAMCNCJob.py 77 KB

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