FlatCAMCNCJob.py 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363
  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 camlib import CNCjob
  17. from shapely.ops import unary_union
  18. from shapely.geometry import Point
  19. try:
  20. from shapely.ops import voronoi_diagram
  21. except Exception:
  22. pass
  23. import os
  24. import sys
  25. import math
  26. import gettext
  27. import appTranslation as fcTranslate
  28. import builtins
  29. fcTranslate.apply_language('strings')
  30. if '_' not in builtins.__dict__:
  31. _ = gettext.gettext
  32. class CNCJobObject(FlatCAMObj, CNCjob):
  33. """
  34. Represents G-Code.
  35. """
  36. optionChanged = QtCore.pyqtSignal(str)
  37. ui_type = CNCObjectUI
  38. def __init__(self, name, units="in", kind="generic", z_move=0.1,
  39. feedrate=3.0, feedrate_rapid=3.0, z_cut=-0.002, tooldia=0.0,
  40. spindlespeed=None):
  41. log.debug("Creating CNCJob object...")
  42. self.decimals = self.app.decimals
  43. CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
  44. feedrate=feedrate, feedrate_rapid=feedrate_rapid, z_cut=z_cut, tooldia=tooldia,
  45. spindlespeed=spindlespeed, steps_per_circle=int(self.app.defaults["cncjob_steps_per_circle"]))
  46. FlatCAMObj.__init__(self, name)
  47. self.kind = "cncjob"
  48. self.options.update({
  49. "plot": True,
  50. "tooldia": 0.03937, # 0.4mm in inches
  51. "append": "",
  52. "prepend": "",
  53. "dwell": False,
  54. "dwelltime": 1,
  55. "type": 'Geometry',
  56. # "toolchange_macro": '',
  57. # "toolchange_macro_enable": False
  58. })
  59. '''
  60. This is a dict of dictionaries. Each dict is associated with a tool present in the file. The key is the
  61. diameter of the tools and the value is another dict that will hold the data under the following form:
  62. {tooldia: {
  63. 'tooluid': 1,
  64. 'offset': 'Path',
  65. 'type_item': 'Rough',
  66. 'tool_type': 'C1',
  67. 'data': {} # a dict to hold the parameters
  68. 'gcode': "" # a string with the actual GCODE
  69. 'gcode_parsed': {} # dictionary holding the CNCJob geometry and type of geometry
  70. (cut or move)
  71. 'solid_geometry': []
  72. },
  73. ...
  74. }
  75. It is populated in the GeometryObject.mtool_gen_cncjob()
  76. BEWARE: I rely on the ordered nature of the Python 3.7 dictionary. Things might change ...
  77. '''
  78. self.cnc_tools = {}
  79. '''
  80. This is a dict of dictionaries. Each dict is associated with a tool present in the file. The key is the
  81. diameter of the tools and the value is another dict that will hold the data under the following form:
  82. {tooldia: {
  83. 'tool': int,
  84. 'nr_drills': int,
  85. 'nr_slots': int,
  86. 'offset': float,
  87. 'data': {}, a dict to hold the parameters
  88. 'gcode': "", a string with the actual GCODE
  89. 'gcode_parsed': [], list of dicts holding the CNCJob geometry and
  90. type of geometry (cut or move)
  91. 'solid_geometry': [],
  92. },
  93. ...
  94. }
  95. It is populated in the ExcellonObject.on_create_cncjob_click() but actually
  96. it's done in camlib.CNCJob.generate_from_excellon_by_tool()
  97. BEWARE: I rely on the ordered nature of the Python 3.7 dictionary. Things might change ...
  98. '''
  99. self.exc_cnc_tools = {}
  100. # flag to store if the CNCJob is part of a special group of CNCJob objects that can't be processed by the
  101. # default engine of FlatCAM. They generated by some of tools and are special cases of CNCJob objects.
  102. self.special_group = None
  103. # for now it show if the plot will be done for multi-tool CNCJob (True) or for single tool
  104. # (like the one in the TCL Command), False
  105. self.multitool = False
  106. # determine if the GCode was generated out of a Excellon object or a Geometry object
  107. self.origin_kind = None
  108. # used for parsing the GCode lines to adjust the GCode when the GCode is offseted or scaled
  109. gcodex_re_string = r'(?=.*(X[-\+]?\d*\.\d*))'
  110. self.g_x_re = re.compile(gcodex_re_string)
  111. gcodey_re_string = r'(?=.*(Y[-\+]?\d*\.\d*))'
  112. self.g_y_re = re.compile(gcodey_re_string)
  113. gcodez_re_string = r'(?=.*(Z[-\+]?\d*\.\d*))'
  114. self.g_z_re = re.compile(gcodez_re_string)
  115. gcodef_re_string = r'(?=.*(F[-\+]?\d*\.\d*))'
  116. self.g_f_re = re.compile(gcodef_re_string)
  117. gcodet_re_string = r'(?=.*(\=\s*[-\+]?\d*\.\d*))'
  118. self.g_t_re = re.compile(gcodet_re_string)
  119. gcodenr_re_string = r'([+-]?\d*\.\d+)'
  120. self.g_nr_re = re.compile(gcodenr_re_string)
  121. if self.app.is_legacy is False:
  122. self.text_col = self.app.plotcanvas.new_text_collection()
  123. self.text_col.enabled = True
  124. self.annotation = self.app.plotcanvas.new_text_group(collection=self.text_col)
  125. self.gcode_editor_tab = None
  126. self.source_file = ''
  127. self.units_found = self.app.defaults['units']
  128. self.append_snippet = ''
  129. self.prepend_snippet = ''
  130. self.gc_header = self.gcode_header()
  131. self.gc_start = ''
  132. self.gc_end = ''
  133. '''
  134. dictionary of dictionaries to store the informations for the autolevelling
  135. format:
  136. {
  137. id: {
  138. 'point': Shapely Point
  139. 'geo': Shapely Polygon from Voronoi diagram,
  140. 'height': float
  141. }
  142. }
  143. '''
  144. self.al_geometry_dict = {}
  145. # Attributes to be included in serialization
  146. # Always append to it because it carries contents
  147. # from predecessors.
  148. self.ser_attrs += [
  149. 'options', 'kind', 'origin_kind', 'cnc_tools', 'exc_cnc_tools', 'multitool', 'append_snippet',
  150. 'prepend_snippet', 'gc_header'
  151. ]
  152. def build_ui(self):
  153. self.ui_disconnect()
  154. FlatCAMObj.build_ui(self)
  155. self.units = self.app.defaults['units'].upper()
  156. # if the FlatCAM object is Excellon don't build the CNC Tools Table but hide it
  157. self.ui.cnc_tools_table.hide()
  158. if self.cnc_tools:
  159. self.ui.cnc_tools_table.show()
  160. self.build_cnc_tools_table()
  161. self.ui.exc_cnc_tools_table.hide()
  162. if self.exc_cnc_tools:
  163. self.ui.exc_cnc_tools_table.show()
  164. self.build_excellon_cnc_tools()
  165. if self.ui.sal_cb.get_value():
  166. self.build_al_table()
  167. self.ui_connect()
  168. def build_cnc_tools_table(self):
  169. tool_idx = 0
  170. n = len(self.cnc_tools)
  171. self.ui.cnc_tools_table.setRowCount(n)
  172. for dia_key, dia_value in self.cnc_tools.items():
  173. tool_idx += 1
  174. row_no = tool_idx - 1
  175. t_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
  176. # id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
  177. self.ui.cnc_tools_table.setItem(row_no, 0, t_id) # Tool name/id
  178. # Make sure that the tool diameter when in MM is with no more than 2 decimals.
  179. # There are no tool bits in MM with more than 2 decimals diameter.
  180. # For INCH the decimals should be no more than 4. There are no tools under 10mils.
  181. dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(dia_value['tooldia'])))
  182. offset_txt = list(str(dia_value['offset']))
  183. offset_txt[0] = offset_txt[0].upper()
  184. offset_item = QtWidgets.QTableWidgetItem(''.join(offset_txt))
  185. type_item = QtWidgets.QTableWidgetItem(str(dia_value['type']))
  186. tool_type_item = QtWidgets.QTableWidgetItem(str(dia_value['tool_type']))
  187. t_id.setFlags(QtCore.Qt.ItemIsEnabled)
  188. dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
  189. offset_item.setFlags(QtCore.Qt.ItemIsEnabled)
  190. type_item.setFlags(QtCore.Qt.ItemIsEnabled)
  191. tool_type_item.setFlags(QtCore.Qt.ItemIsEnabled)
  192. # hack so the checkbox stay centered in the table cell
  193. # used this:
  194. # https://stackoverflow.com/questions/32458111/pyqt-allign-checkbox-and-put-it-in-every-row
  195. # plot_item = QtWidgets.QWidget()
  196. # checkbox = FCCheckBox()
  197. # checkbox.setCheckState(QtCore.Qt.Checked)
  198. # qhboxlayout = QtWidgets.QHBoxLayout(plot_item)
  199. # qhboxlayout.addWidget(checkbox)
  200. # qhboxlayout.setAlignment(QtCore.Qt.AlignCenter)
  201. # qhboxlayout.setContentsMargins(0, 0, 0, 0)
  202. plot_item = FCCheckBox()
  203. plot_item.setLayoutDirection(QtCore.Qt.RightToLeft)
  204. tool_uid_item = QtWidgets.QTableWidgetItem(str(dia_key))
  205. if self.ui.plot_cb.isChecked():
  206. plot_item.setChecked(True)
  207. self.ui.cnc_tools_table.setItem(row_no, 1, dia_item) # Diameter
  208. self.ui.cnc_tools_table.setItem(row_no, 2, offset_item) # Offset
  209. self.ui.cnc_tools_table.setItem(row_no, 3, type_item) # Toolpath Type
  210. self.ui.cnc_tools_table.setItem(row_no, 4, tool_type_item) # Tool Type
  211. # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY # ##
  212. self.ui.cnc_tools_table.setItem(row_no, 5, tool_uid_item) # Tool unique ID)
  213. self.ui.cnc_tools_table.setCellWidget(row_no, 6, plot_item)
  214. # make the diameter column editable
  215. # for row in range(tool_idx):
  216. # self.ui.cnc_tools_table.item(row, 1).setFlags(QtCore.Qt.ItemIsSelectable |
  217. # QtCore.Qt.ItemIsEnabled)
  218. for row in range(tool_idx):
  219. self.ui.cnc_tools_table.item(row, 0).setFlags(
  220. self.ui.cnc_tools_table.item(row, 0).flags() ^ QtCore.Qt.ItemIsSelectable)
  221. self.ui.cnc_tools_table.resizeColumnsToContents()
  222. self.ui.cnc_tools_table.resizeRowsToContents()
  223. vertical_header = self.ui.cnc_tools_table.verticalHeader()
  224. # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
  225. vertical_header.hide()
  226. self.ui.cnc_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  227. horizontal_header = self.ui.cnc_tools_table.horizontalHeader()
  228. horizontal_header.setMinimumSectionSize(10)
  229. horizontal_header.setDefaultSectionSize(70)
  230. horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
  231. horizontal_header.resizeSection(0, 20)
  232. horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
  233. horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
  234. horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed)
  235. horizontal_header.resizeSection(4, 40)
  236. horizontal_header.setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed)
  237. horizontal_header.resizeSection(4, 17)
  238. # horizontal_header.setStretchLastSection(True)
  239. self.ui.cnc_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  240. self.ui.cnc_tools_table.setColumnWidth(0, 20)
  241. self.ui.cnc_tools_table.setColumnWidth(4, 40)
  242. self.ui.cnc_tools_table.setColumnWidth(6, 17)
  243. # self.ui.geo_tools_table.setSortingEnabled(True)
  244. self.ui.cnc_tools_table.setMinimumHeight(self.ui.cnc_tools_table.getHeight())
  245. self.ui.cnc_tools_table.setMaximumHeight(self.ui.cnc_tools_table.getHeight())
  246. def build_excellon_cnc_tools(self):
  247. tool_idx = 0
  248. n = len(self.exc_cnc_tools)
  249. self.ui.exc_cnc_tools_table.setRowCount(n)
  250. for tooldia_key, dia_value in self.exc_cnc_tools.items():
  251. tool_idx += 1
  252. row_no = tool_idx - 1
  253. t_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
  254. dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(tooldia_key)))
  255. nr_drills_item = QtWidgets.QTableWidgetItem('%d' % int(dia_value['nr_drills']))
  256. nr_slots_item = QtWidgets.QTableWidgetItem('%d' % int(dia_value['nr_slots']))
  257. cutz_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(dia_value['offset']) + self.z_cut))
  258. t_id.setFlags(QtCore.Qt.ItemIsEnabled)
  259. dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
  260. nr_drills_item.setFlags(QtCore.Qt.ItemIsEnabled)
  261. nr_slots_item.setFlags(QtCore.Qt.ItemIsEnabled)
  262. cutz_item.setFlags(QtCore.Qt.ItemIsEnabled)
  263. # hack so the checkbox stay centered in the table cell
  264. # used this:
  265. # https://stackoverflow.com/questions/32458111/pyqt-allign-checkbox-and-put-it-in-every-row
  266. # plot_item = QtWidgets.QWidget()
  267. # checkbox = FCCheckBox()
  268. # checkbox.setCheckState(QtCore.Qt.Checked)
  269. # qhboxlayout = QtWidgets.QHBoxLayout(plot_item)
  270. # qhboxlayout.addWidget(checkbox)
  271. # qhboxlayout.setAlignment(QtCore.Qt.AlignCenter)
  272. # qhboxlayout.setContentsMargins(0, 0, 0, 0)
  273. plot_item = FCCheckBox()
  274. plot_item.setLayoutDirection(QtCore.Qt.RightToLeft)
  275. tool_uid_item = QtWidgets.QTableWidgetItem(str(dia_value['tool']))
  276. if self.ui.plot_cb.isChecked():
  277. plot_item.setChecked(True)
  278. self.ui.exc_cnc_tools_table.setItem(row_no, 0, t_id) # Tool name/id
  279. self.ui.exc_cnc_tools_table.setItem(row_no, 1, dia_item) # Diameter
  280. self.ui.exc_cnc_tools_table.setItem(row_no, 2, nr_drills_item) # Nr of drills
  281. self.ui.exc_cnc_tools_table.setItem(row_no, 3, nr_slots_item) # Nr of slots
  282. # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY # ##
  283. self.ui.exc_cnc_tools_table.setItem(row_no, 4, tool_uid_item) # Tool unique ID)
  284. self.ui.exc_cnc_tools_table.setItem(row_no, 5, cutz_item)
  285. self.ui.exc_cnc_tools_table.setCellWidget(row_no, 6, plot_item)
  286. for row in range(tool_idx):
  287. self.ui.exc_cnc_tools_table.item(row, 0).setFlags(
  288. self.ui.exc_cnc_tools_table.item(row, 0).flags() ^ QtCore.Qt.ItemIsSelectable)
  289. self.ui.exc_cnc_tools_table.resizeColumnsToContents()
  290. self.ui.exc_cnc_tools_table.resizeRowsToContents()
  291. vertical_header = self.ui.exc_cnc_tools_table.verticalHeader()
  292. vertical_header.hide()
  293. self.ui.exc_cnc_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  294. horizontal_header = self.ui.exc_cnc_tools_table.horizontalHeader()
  295. horizontal_header.setMinimumSectionSize(10)
  296. horizontal_header.setDefaultSectionSize(70)
  297. horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
  298. horizontal_header.resizeSection(0, 20)
  299. horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
  300. horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
  301. horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
  302. horizontal_header.setSectionResizeMode(5, QtWidgets.QHeaderView.ResizeToContents)
  303. horizontal_header.setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed)
  304. # horizontal_header.setStretchLastSection(True)
  305. self.ui.exc_cnc_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  306. self.ui.exc_cnc_tools_table.setColumnWidth(0, 20)
  307. self.ui.exc_cnc_tools_table.setColumnWidth(6, 17)
  308. self.ui.exc_cnc_tools_table.setMinimumHeight(self.ui.exc_cnc_tools_table.getHeight())
  309. self.ui.exc_cnc_tools_table.setMaximumHeight(self.ui.exc_cnc_tools_table.getHeight())
  310. def build_al_table(self):
  311. tool_idx = 0
  312. n = len(self.al_geometry_dict)
  313. self.ui.al_testpoints_table.setRowCount(n)
  314. for id_key, value in self.al_geometry_dict.items():
  315. tool_idx += 1
  316. row_no = tool_idx - 1
  317. t_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
  318. x = value['point'].x
  319. y = value['point'].y
  320. xy_coords = self.app.dec_format(x, dec=self.app.decimals), self.app.dec_format(y, dec=self.app.decimals)
  321. coords_item = QtWidgets.QTableWidgetItem(str(xy_coords))
  322. height = self.app.dec_format(value['height'], dec=self.app.decimals)
  323. height_item = QtWidgets.QTableWidgetItem(str(height))
  324. t_id.setFlags(QtCore.Qt.ItemIsEnabled)
  325. coords_item.setFlags(QtCore.Qt.ItemIsEnabled)
  326. height_item.setFlags(QtCore.Qt.ItemIsEnabled)
  327. self.ui.al_testpoints_table.setItem(row_no, 0, t_id) # Tool name/id
  328. self.ui.al_testpoints_table.setItem(row_no, 1, coords_item) # X-Y coords
  329. self.ui.al_testpoints_table.setItem(row_no, 2, height_item) # Determined Height
  330. self.ui.al_testpoints_table.resizeColumnsToContents()
  331. self.ui.al_testpoints_table.resizeRowsToContents()
  332. h_header = self.ui.al_testpoints_table.horizontalHeader()
  333. h_header.setMinimumSectionSize(10)
  334. h_header.setDefaultSectionSize(70)
  335. h_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
  336. h_header.resizeSection(0, 20)
  337. h_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
  338. h_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
  339. self.ui.al_testpoints_table.setMinimumHeight(self.ui.al_testpoints_table.getHeight())
  340. self.ui.al_testpoints_table.setMaximumHeight(self.ui.al_testpoints_table.getHeight())
  341. if self.ui.al_testpoints_table.model().rowCount():
  342. self.ui.voronoi_cb.setDisabled(False)
  343. else:
  344. self.ui.voronoi_cb.setDisabled(True)
  345. def set_ui(self, ui):
  346. FlatCAMObj.set_ui(self, ui)
  347. log.debug("FlatCAMCNCJob.set_ui()")
  348. assert isinstance(self.ui, CNCObjectUI), \
  349. "Expected a CNCObjectUI, got %s" % type(self.ui)
  350. self.units = self.app.defaults['units'].upper()
  351. self.units_found = self.app.defaults['units']
  352. # this signal has to be connected to it's slot before the defaults are populated
  353. # the decision done in the slot has to override the default value set below
  354. # self.ui.toolchange_cb.toggled.connect(self.on_toolchange_custom_clicked)
  355. self.form_fields.update({
  356. "plot": self.ui.plot_cb,
  357. "tooldia": self.ui.tooldia_entry,
  358. # "append": self.ui.append_text,
  359. # "prepend": self.ui.prepend_text,
  360. # "toolchange_macro": self.ui.toolchange_text,
  361. # "toolchange_macro_enable": self.ui.toolchange_cb
  362. })
  363. self.append_snippet = self.app.defaults['cncjob_append']
  364. self.prepend_snippet = self.app.defaults['cncjob_prepend']
  365. if self.append_snippet != '' or self.prepend_snippet:
  366. self.ui.snippets_cb.set_value(True)
  367. # Fill form fields only on object create
  368. self.to_form()
  369. # this means that the object that created this CNCJob was an Excellon or Geometry
  370. try:
  371. if self.travel_distance:
  372. self.ui.t_distance_label.show()
  373. self.ui.t_distance_entry.setVisible(True)
  374. self.ui.t_distance_entry.setDisabled(True)
  375. self.ui.t_distance_entry.set_value('%.*f' % (self.decimals, float(self.travel_distance)))
  376. self.ui.units_label.setText(str(self.units).lower())
  377. self.ui.units_label.setDisabled(True)
  378. self.ui.t_time_label.show()
  379. self.ui.t_time_entry.setVisible(True)
  380. self.ui.t_time_entry.setDisabled(True)
  381. # if time is more than 1 then we have minutes, else we have seconds
  382. if self.routing_time > 1:
  383. self.ui.t_time_entry.set_value('%.*f' % (self.decimals, math.ceil(float(self.routing_time))))
  384. self.ui.units_time_label.setText('min')
  385. else:
  386. time_r = self.routing_time * 60
  387. self.ui.t_time_entry.set_value('%.*f' % (self.decimals, math.ceil(float(time_r))))
  388. self.ui.units_time_label.setText('sec')
  389. self.ui.units_time_label.setDisabled(True)
  390. except AttributeError:
  391. pass
  392. if self.multitool is False:
  393. self.ui.tooldia_entry.show()
  394. self.ui.updateplot_button.show()
  395. else:
  396. self.ui.tooldia_entry.hide()
  397. self.ui.updateplot_button.hide()
  398. # set the kind of geometries are plotted by default with plot2() from camlib.CNCJob
  399. self.ui.cncplot_method_combo.set_value(self.app.defaults["cncjob_plot_kind"])
  400. try:
  401. self.ui.annotation_cb.stateChanged.disconnect(self.on_annotation_change)
  402. except (TypeError, AttributeError):
  403. pass
  404. self.ui.annotation_cb.stateChanged.connect(self.on_annotation_change)
  405. # set if to display text annotations
  406. self.ui.annotation_cb.set_value(self.app.defaults["cncjob_annotation"])
  407. # Show/Hide Advanced Options
  408. if self.app.defaults["global_app_level"] == 'b':
  409. self.ui.level.setText(_(
  410. '<span style="color:green;"><b>Basic</b></span>'
  411. ))
  412. # self.ui.cnc_frame.hide()
  413. else:
  414. self.ui.level.setText(_(
  415. '<span style="color:red;"><b>Advanced</b></span>'
  416. ))
  417. # self.ui.cnc_frame.show()
  418. self.ui.updateplot_button.clicked.connect(self.on_updateplot_button_click)
  419. self.ui.export_gcode_button.clicked.connect(self.on_exportgcode_button_click)
  420. self.ui.review_gcode_button.clicked.connect(self.on_edit_code_click)
  421. self.ui.editor_button.clicked.connect(lambda: self.app.object2editor())
  422. self.ui.al_mode_radio.activated_custom.connect(self.on_mode_radio)
  423. # self.ui.tc_variable_combo.currentIndexChanged[str].connect(self.on_cnc_custom_parameters)
  424. self.ui.cncplot_method_combo.activated_custom.connect(self.on_plot_kind_change)
  425. preamble = self.append_snippet
  426. postamble = self.prepend_snippet
  427. gc = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True)
  428. self.source_file = gc.getvalue()
  429. self.ui.al_mode_radio.set_value('grid')
  430. # def on_cnc_custom_parameters(self, signal_text):
  431. # if signal_text == 'Parameters':
  432. # return
  433. # else:
  434. # self.ui.toolchange_text.insertPlainText('%%%s%%' % signal_text)
  435. def ui_connect(self):
  436. for row in range(self.ui.cnc_tools_table.rowCount()):
  437. self.ui.cnc_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table)
  438. for row in range(self.ui.exc_cnc_tools_table.rowCount()):
  439. self.ui.exc_cnc_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table)
  440. self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
  441. self.ui.al_add_button.clicked.connect(self.on_add_al_testpoints)
  442. self.ui.show_al_table.stateChanged.connect(self.on_show_al_table)
  443. def ui_disconnect(self):
  444. for row in range(self.ui.cnc_tools_table.rowCount()):
  445. try:
  446. self.ui.cnc_tools_table.cellWidget(row, 6).clicked.disconnect(self.on_plot_cb_click_table)
  447. except (TypeError, AttributeError):
  448. pass
  449. for row in range(self.ui.exc_cnc_tools_table.rowCount()):
  450. try:
  451. self.ui.exc_cnc_tools_table.cellWidget(row, 6).clicked.disconnect(self.on_plot_cb_click_table)
  452. except (TypeError, AttributeError):
  453. pass
  454. try:
  455. self.ui.plot_cb.stateChanged.disconnect(self.on_plot_cb_click)
  456. except (TypeError, AttributeError):
  457. pass
  458. try:
  459. self.ui.al_add_button.clicked.disconnect()
  460. except (TypeError, AttributeError):
  461. pass
  462. try:
  463. self.ui.show_al_table.stateChanged.disconnect()
  464. except (TypeError, AttributeError):
  465. pass
  466. def on_add_al_testpoints(self):
  467. # create the solid_geo
  468. solid_geo = [geo['geom'] for geo in self.gcode_parsed if geo['kind'][0] == 'C']
  469. solid_geo = unary_union(solid_geo)
  470. # reset al table
  471. self.ui.al_testpoints_table.setRowCount(0)
  472. # reset the al dict
  473. self.al_geometry_dict.clear()
  474. xmin, ymin, xmax, ymax = solid_geo.bounds
  475. if self.ui.al_mode_radio.get_value() == 'grid':
  476. width = abs(xmax - xmin)
  477. height = abs(ymax - ymin)
  478. cols = self.ui.al_columns_entry.get_value()
  479. rows = self.ui.al_rows_entry.get_value()
  480. dx = width / (cols + 1)
  481. dy = height / (rows + 1)
  482. points = []
  483. new_y = ymin
  484. for x in range(rows):
  485. new_y += dy
  486. new_x = xmin
  487. for x in range(cols):
  488. new_x += dx
  489. points.append((new_x, new_y))
  490. pt_id = 0
  491. for point in points:
  492. pt_id += 1
  493. new_dict = {
  494. 'point': Point(point),
  495. 'geo': None,
  496. 'height': 0.0
  497. }
  498. self.al_geometry_dict[pt_id] = deepcopy(new_dict)
  499. # self.calculate_voronoi_diagram()
  500. self.build_al_table()
  501. # def calculate_voronoi_diagram(self):
  502. # return voronoi_diagram()
  503. def on_show_al_table(self, state):
  504. self.ui.al_testpoints_table.show() if state else self.ui.al_testpoints_table.hide()
  505. def on_mode_radio(self, val):
  506. # reset al table
  507. self.ui.al_testpoints_table.setRowCount(0)
  508. # reset the al dict
  509. self.al_geometry_dict.clear()
  510. # build AL table
  511. self.build_al_table()
  512. if val == "manual":
  513. self.ui.al_rows_entry.setDisabled(True)
  514. self.ui.al_rows_label.setDisabled(True)
  515. self.ui.al_columns_entry.setDisabled(True)
  516. self.ui.al_columns_label.setDisabled(True)
  517. else:
  518. self.ui.al_rows_entry.setDisabled(False)
  519. self.ui.al_rows_label.setDisabled(False)
  520. self.ui.al_columns_entry.setDisabled(False)
  521. self.ui.al_columns_label.setDisabled(False)
  522. def on_updateplot_button_click(self, *args):
  523. """
  524. Callback for the "Updata Plot" button. Reads the form for updates
  525. and plots the object.
  526. """
  527. self.read_form()
  528. self.on_plot_kind_change()
  529. def on_plot_kind_change(self):
  530. kind = self.ui.cncplot_method_combo.get_value()
  531. def worker_task():
  532. with self.app.proc_container.new(_("Plotting...")):
  533. self.plot(kind=kind)
  534. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  535. def on_exportgcode_button_click(self):
  536. """
  537. Handler activated by a button clicked when exporting GCode.
  538. :param args:
  539. :return:
  540. """
  541. self.app.defaults.report_usage("cncjob_on_exportgcode_button")
  542. self.read_form()
  543. name = self.app.collection.get_active().options['name']
  544. save_gcode = False
  545. if 'Roland' in self.pp_excellon_name or 'Roland' in self.pp_geometry_name:
  546. _filter_ = "RML1 Files .rol (*.rol);;All Files (*.*)"
  547. elif 'hpgl' in self.pp_geometry_name:
  548. _filter_ = "HPGL Files .plt (*.plt);;All Files (*.*)"
  549. else:
  550. save_gcode = True
  551. _filter_ = self.app.defaults['cncjob_save_filters']
  552. try:
  553. dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name)
  554. filename, _f = FCFileSaveDialog.get_saved_filename(
  555. caption=_("Export Code ..."),
  556. directory=dir_file_to_save,
  557. ext_filter=_filter_
  558. )
  559. except TypeError:
  560. filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export Code ..."), ext_filter=_filter_)
  561. self.export_gcode_handler(filename, is_gcode=save_gcode)
  562. def export_gcode_handler(self, filename, is_gcode=True):
  563. preamble = ''
  564. postamble = ''
  565. filename = str(filename)
  566. if filename == '':
  567. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export cancelled ..."))
  568. return
  569. else:
  570. if is_gcode is True:
  571. used_extension = filename.rpartition('.')[2]
  572. self.update_filters(last_ext=used_extension, filter_string='cncjob_save_filters')
  573. new_name = os.path.split(str(filename))[1].rpartition('.')[0]
  574. self.ui.name_entry.set_value(new_name)
  575. self.on_name_activate(silent=True)
  576. try:
  577. if self.ui.snippets_cb.get_value():
  578. preamble = self.append_snippet
  579. postamble = self.prepend_snippet
  580. gc = self.export_gcode(filename, preamble=preamble, postamble=postamble)
  581. except Exception as err:
  582. log.debug("CNCJobObject.export_gcode_handler() --> %s" % str(err))
  583. gc = self.export_gcode(filename)
  584. if gc == 'fail':
  585. return
  586. if self.app.defaults["global_open_style"] is False:
  587. self.app.file_opened.emit("gcode", filename)
  588. self.app.file_saved.emit("gcode", filename)
  589. self.app.inform.emit('[success] %s: %s' % (_("File saved to"), filename))
  590. def on_edit_code_click(self, *args):
  591. """
  592. Handler activated by a button clicked when editing GCode.
  593. :param args:
  594. :return:
  595. """
  596. self.app.proc_container.view.set_busy(_("Loading..."))
  597. preamble = self.append_snippet
  598. postamble = self.prepend_snippet
  599. gco = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True)
  600. if gco == 'fail':
  601. return
  602. else:
  603. self.app.gcode_edited = gco
  604. self.gcode_editor_tab = AppTextEditor(app=self.app, plain_text=True)
  605. # add the tab if it was closed
  606. self.app.ui.plot_tab_area.addTab(self.gcode_editor_tab, '%s' % _("Code Editor"))
  607. self.gcode_editor_tab.setObjectName('code_editor_tab')
  608. # delete the absolute and relative position and messages in the infobar
  609. self.app.ui.position_label.setText("")
  610. self.app.ui.rel_position_label.setText("")
  611. self.gcode_editor_tab.code_editor.completer_enable = False
  612. self.gcode_editor_tab.buttonRun.hide()
  613. # Switch plot_area to CNCJob tab
  614. self.app.ui.plot_tab_area.setCurrentWidget(self.gcode_editor_tab)
  615. self.gcode_editor_tab.t_frame.hide()
  616. # then append the text from GCode to the text editor
  617. try:
  618. self.gcode_editor_tab.load_text(self.app.gcode_edited.getvalue(), move_to_start=True, clear_text=True)
  619. except Exception as e:
  620. log.debug('FlatCAMCNCJob.on_edit_code_click() -->%s' % str(e))
  621. return
  622. self.gcode_editor_tab.t_frame.show()
  623. self.app.proc_container.view.set_idle()
  624. self.gcode_editor_tab.buttonSave.hide()
  625. self.gcode_editor_tab.buttonOpen.hide()
  626. self.gcode_editor_tab.code_editor.setReadOnly(True)
  627. self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Editor'))
  628. def on_update_source_file(self):
  629. self.source_file = self.gcode_editor_tab.code_editor.toPlainText()
  630. def gcode_header(self, comment_start_symbol=None, comment_stop_symbol=None):
  631. """
  632. Will create a header to be added to all GCode files generated by FlatCAM
  633. :param comment_start_symbol: A symbol to be used as the first symbol in a comment
  634. :param comment_stop_symbol: A symbol to be used as the last symbol in a comment
  635. :return: A string with a GCode header
  636. """
  637. log.debug("FlatCAMCNCJob.gcode_header()")
  638. time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
  639. marlin = False
  640. hpgl = False
  641. probe_pp = False
  642. gcode = ''
  643. start_comment = comment_start_symbol if comment_start_symbol is not None else '('
  644. stop_comment = comment_stop_symbol if comment_stop_symbol is not None else ')'
  645. try:
  646. for key in self.cnc_tools:
  647. ppg = self.cnc_tools[key]['data']['ppname_g']
  648. if 'marlin' in ppg.lower() or 'repetier' in ppg.lower():
  649. marlin = True
  650. break
  651. if ppg == 'hpgl':
  652. hpgl = True
  653. break
  654. if "toolchange_probe" in ppg.lower():
  655. probe_pp = True
  656. break
  657. except KeyError:
  658. # log.debug("FlatCAMCNCJob.gcode_header() error: --> %s" % str(e))
  659. pass
  660. try:
  661. if 'marlin' in self.options['ppname_e'].lower() or 'repetier' in self.options['ppname_e'].lower():
  662. marlin = True
  663. except KeyError:
  664. # log.debug("FlatCAMCNCJob.gcode_header(): --> There is no such self.option: %s" % str(e))
  665. pass
  666. try:
  667. if "toolchange_probe" in self.options['ppname_e'].lower():
  668. probe_pp = True
  669. except KeyError:
  670. # log.debug("FlatCAMCNCJob.gcode_header(): --> There is no such self.option: %s" % str(e))
  671. pass
  672. if marlin is True:
  673. gcode += ';Marlin(Repetier) G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s\n' % \
  674. (str(self.app.version), str(self.app.version_date)) + '\n'
  675. gcode += ';Name: ' + str(self.options['name']) + '\n'
  676. gcode += ';Type: ' + "G-code from " + str(self.options['type']) + '\n'
  677. # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
  678. # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
  679. gcode += ';Units: ' + self.units.upper() + '\n' + "\n"
  680. gcode += ';Created on ' + time_str + '\n' + '\n'
  681. elif hpgl is True:
  682. gcode += 'CO "HPGL CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s' % \
  683. (str(self.app.version), str(self.app.version_date)) + '";\n'
  684. gcode += 'CO "Name: ' + str(self.options['name']) + '";\n'
  685. gcode += 'CO "Type: ' + "HPGL code from " + str(self.options['type']) + '";\n'
  686. # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
  687. # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
  688. gcode += 'CO "Units: ' + self.units.upper() + '";\n'
  689. gcode += 'CO "Created on ' + time_str + '";\n'
  690. elif probe_pp is True:
  691. gcode += '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \
  692. (str(self.app.version), str(self.app.version_date)) + '\n'
  693. gcode += '(This GCode tool change is done by using a Probe.)\n' \
  694. '(Make sure that before you start the job you first do a rough zero for Z axis.)\n' \
  695. '(This means that you need to zero the CNC axis and then jog to the toolchange X, Y location,)\n' \
  696. '(mount the probe and adjust the Z so more or less the probe tip touch the plate. ' \
  697. 'Then zero the Z axis.)\n' + '\n'
  698. gcode += '(Name: ' + str(self.options['name']) + ')\n'
  699. gcode += '(Type: ' + "G-code from " + str(self.options['type']) + ')\n'
  700. # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
  701. # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
  702. gcode += '(Units: ' + self.units.upper() + ')\n' + "\n"
  703. gcode += '(Created on ' + time_str + ')\n' + '\n'
  704. else:
  705. gcode += '%sG-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s%s\n' % \
  706. (start_comment, str(self.app.version), str(self.app.version_date), stop_comment) + '\n'
  707. gcode += '%sName: ' % start_comment + str(self.options['name']) + '%s\n' % stop_comment
  708. gcode += '%sType: ' % start_comment + "G-code from " + str(self.options['type']) + '%s\n' % stop_comment
  709. # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
  710. # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
  711. gcode += '%sUnits: ' % start_comment + self.units.upper() + '%s\n' % stop_comment + "\n"
  712. gcode += '%sCreated on ' % start_comment + time_str + '%s\n' % stop_comment + '\n'
  713. return gcode
  714. @staticmethod
  715. def gcode_footer(end_command=None):
  716. """
  717. Will add the M02 to the end of GCode, if requested.
  718. :param end_command: 'M02' or 'M30' - String
  719. :return:
  720. """
  721. if end_command:
  722. return end_command
  723. else:
  724. return 'M02'
  725. def export_gcode(self, filename=None, preamble='', postamble='', to_file=False, from_tcl=False):
  726. """
  727. This will save the GCode from the Gcode object to a file on the OS filesystem
  728. :param filename: filename for the GCode file
  729. :param preamble: a custom Gcode block to be added at the beginning of the Gcode file
  730. :param postamble: a custom Gcode block to be added at the end of the Gcode file
  731. :param to_file: if False then no actual file is saved but the app will know that a file was created
  732. :param from_tcl: True if run from Tcl Shell
  733. :return: None
  734. """
  735. # gcode = ''
  736. # roland = False
  737. # hpgl = False
  738. # isel_icp = False
  739. include_header = True
  740. if preamble == '':
  741. preamble = self.app.defaults["cncjob_prepend"]
  742. if postamble == '':
  743. preamble = self.app.defaults["cncjob_append"]
  744. try:
  745. if self.special_group:
  746. self.app.inform.emit('[WARNING_NOTCL] %s %s %s.' %
  747. (_("This CNCJob object can't be processed because it is a"),
  748. str(self.special_group),
  749. _("CNCJob object")))
  750. return 'fail'
  751. except AttributeError:
  752. pass
  753. # if this dict is not empty then the object is a Geometry object
  754. if self.cnc_tools:
  755. first_key = next(iter(self.cnc_tools))
  756. include_header = self.app.preprocessors[self.cnc_tools[first_key]['data']['ppname_g']].include_header
  757. # if this dict is not empty then the object is an Excellon object
  758. if self.exc_cnc_tools:
  759. first_key = next(iter(self.exc_cnc_tools))
  760. include_header = self.app.preprocessors[
  761. self.exc_cnc_tools[first_key]['data']['tools_drill_ppname_e']
  762. ].include_header
  763. gcode = ''
  764. if include_header is False:
  765. g = preamble
  766. # detect if using multi-tool and make the Gcode summation correctly for each case
  767. if self.multitool is True:
  768. for tooluid_key in self.cnc_tools:
  769. for key, value in self.cnc_tools[tooluid_key].items():
  770. if key == 'gcode':
  771. gcode += value
  772. break
  773. else:
  774. gcode += self.gcode
  775. g = g + gcode + postamble
  776. else:
  777. # search for the GCode beginning which is usually a G20 or G21
  778. # fix so the preamble gets inserted in between the comments header and the actual start of GCODE
  779. # g_idx = gcode.rfind('G20')
  780. #
  781. # # if it did not find 'G20' then search for 'G21'
  782. # if g_idx == -1:
  783. # g_idx = gcode.rfind('G21')
  784. #
  785. # # if it did not find 'G20' and it did not find 'G21' then there is an error and return
  786. # if g_idx == -1:
  787. # self.app.inform.emit('[ERROR_NOTCL] %s' % _("G-code does not have a units code: either G20 or G21"))
  788. # return
  789. # detect if using multi-tool and make the Gcode summation correctly for each case
  790. if self.multitool is True:
  791. if self.origin_kind == 'excellon':
  792. for tooluid_key in self.exc_cnc_tools:
  793. for key, value in self.exc_cnc_tools[tooluid_key].items():
  794. if key == 'gcode' and value:
  795. gcode += value
  796. break
  797. else:
  798. for tooluid_key in self.cnc_tools:
  799. for key, value in self.cnc_tools[tooluid_key].items():
  800. if key == 'gcode' and value:
  801. gcode += value
  802. break
  803. else:
  804. gcode += self.gcode
  805. end_gcode = self.gcode_footer() if self.app.defaults['cncjob_footer'] is True else ''
  806. # detect if using a HPGL preprocessor
  807. hpgl = False
  808. if self.cnc_tools:
  809. for key in self.cnc_tools:
  810. if 'ppname_g' in self.cnc_tools[key]['data']:
  811. if 'hpgl' in self.cnc_tools[key]['data']['ppname_g']:
  812. hpgl = True
  813. break
  814. elif self.exc_cnc_tools:
  815. for key in self.cnc_tools:
  816. if 'ppname_e' in self.cnc_tools[key]['data']:
  817. if 'hpgl' in self.cnc_tools[key]['data']['ppname_e']:
  818. hpgl = True
  819. break
  820. if hpgl:
  821. processed_gcode = ''
  822. pa_re = re.compile(r"^PA\s*(-?\d+\.\d*),?\s*(-?\d+\.\d*)*;?$")
  823. for gline in gcode.splitlines():
  824. match = pa_re.search(gline)
  825. if match:
  826. x_int = int(float(match.group(1)))
  827. y_int = int(float(match.group(2)))
  828. new_line = 'PA%d,%d;\n' % (x_int, y_int)
  829. processed_gcode += new_line
  830. else:
  831. processed_gcode += gline + '\n'
  832. gcode = processed_gcode
  833. g = self.gc_header + '\n' + preamble + '\n' + gcode + postamble + end_gcode
  834. else:
  835. try:
  836. g_idx = gcode.index('G94')
  837. if preamble != '' and postamble != '':
  838. g = self.gc_header + gcode[:g_idx + 3] + '\n' + preamble + '\n' + \
  839. gcode[(g_idx + 3):] + postamble + end_gcode
  840. elif preamble == '':
  841. g = self.gc_header + gcode[:g_idx + 3] + '\n' + \
  842. gcode[(g_idx + 3):] + postamble + end_gcode
  843. elif postamble == '':
  844. g = self.gc_header + gcode[:g_idx + 3] + '\n' + preamble + '\n' + \
  845. gcode[(g_idx + 3):] + end_gcode
  846. else:
  847. g = self.gc_header + gcode[:g_idx + 3] + gcode[(g_idx + 3):] + end_gcode
  848. except ValueError:
  849. self.app.inform.emit('[ERROR_NOTCL] %s' %
  850. _("G-code does not have a G94 code.\n"
  851. "Append Code snippet will not be used.."))
  852. g = self.gc_header + '\n' + gcode + postamble + end_gcode
  853. # if toolchange custom is used, replace M6 code with the code from the Toolchange Custom Text box
  854. # if self.ui.toolchange_cb.get_value() is True:
  855. # # match = self.re_toolchange.search(g)
  856. # if 'M6' in g:
  857. # m6_code = self.parse_custom_toolchange_code(self.ui.toolchange_text.get_value())
  858. # if m6_code is None or m6_code == '':
  859. # self.app.inform.emit(
  860. # '[ERROR_NOTCL] %s' % _("Cancelled. The Toolchange Custom code is enabled but it's empty.")
  861. # )
  862. # return 'fail'
  863. #
  864. # g = g.replace('M6', m6_code)
  865. # self.app.inform.emit('[success] %s' % _("Toolchange G-code was replaced by a custom code."))
  866. lines = StringIO(g)
  867. # Write
  868. if filename is not None:
  869. try:
  870. force_windows_line_endings = self.app.defaults['cncjob_line_ending']
  871. if force_windows_line_endings and sys.platform != 'win32':
  872. with open(filename, 'w', newline='\r\n') as f:
  873. for line in lines:
  874. f.write(line)
  875. else:
  876. with open(filename, 'w') as f:
  877. for line in lines:
  878. f.write(line)
  879. except FileNotFoundError:
  880. self.app.inform.emit('[WARNING_NOTCL] %s' % _("No such file or directory"))
  881. return
  882. except PermissionError:
  883. self.app.inform.emit(
  884. '[WARNING] %s' % _("Permission denied, saving not possible.\n"
  885. "Most likely another app is holding the file open and not accessible.")
  886. )
  887. return 'fail'
  888. elif to_file is False:
  889. # Just for adding it to the recent files list.
  890. if self.app.defaults["global_open_style"] is False:
  891. self.app.file_opened.emit("cncjob", filename)
  892. self.app.file_saved.emit("cncjob", filename)
  893. self.app.inform.emit('[success] %s: %s' % (_("Saved to"), filename))
  894. else:
  895. return lines
  896. # def on_toolchange_custom_clicked(self, signal):
  897. # """
  898. # Handler for clicking toolchange custom.
  899. #
  900. # :param signal:
  901. # :return:
  902. # """
  903. #
  904. # try:
  905. # if 'toolchange_custom' not in str(self.options['ppname_e']).lower():
  906. # if self.ui.toolchange_cb.get_value():
  907. # self.ui.toolchange_cb.set_value(False)
  908. # self.app.inform.emit('[WARNING_NOTCL] %s' %
  909. # _("The used preprocessor file has to have in it's name: 'toolchange_custom'"))
  910. # except KeyError:
  911. # try:
  912. # for key in self.cnc_tools:
  913. # ppg = self.cnc_tools[key]['data']['ppname_g']
  914. # if 'toolchange_custom' not in str(ppg).lower():
  915. # if self.ui.toolchange_cb.get_value():
  916. # self.ui.toolchange_cb.set_value(False)
  917. # self.app.inform.emit('[WARNING_NOTCL] %s' %
  918. # _("The used preprocessor file has to have in it's name: "
  919. # "'toolchange_custom'"))
  920. # except KeyError:
  921. # self.app.inform.emit('[ERROR] %s' % _("There is no preprocessor file."))
  922. def get_gcode(self, preamble='', postamble=''):
  923. """
  924. We need this to be able to get_gcode separately for shell command export_gcode
  925. :param preamble: Extra GCode added to the beginning of the GCode
  926. :param postamble: Extra GCode added at the end of the GCode
  927. :return: The modified GCode
  928. """
  929. return preamble + '\n' + self.gcode + "\n" + postamble
  930. def get_svg(self):
  931. # we need this to be able get_svg separately for shell command export_svg
  932. pass
  933. def on_plot_cb_click(self, *args):
  934. """
  935. Handler for clicking on the Plot checkbox.
  936. :param args:
  937. :return:
  938. """
  939. if self.muted_ui:
  940. return
  941. kind = self.ui.cncplot_method_combo.get_value()
  942. self.plot(kind=kind)
  943. self.read_form_item('plot')
  944. self.ui_disconnect()
  945. cb_flag = self.ui.plot_cb.isChecked()
  946. for row in range(self.ui.cnc_tools_table.rowCount()):
  947. table_cb = self.ui.cnc_tools_table.cellWidget(row, 6)
  948. if cb_flag:
  949. table_cb.setChecked(True)
  950. else:
  951. table_cb.setChecked(False)
  952. self.ui_connect()
  953. def on_plot_cb_click_table(self):
  954. """
  955. Handler for clicking the plot checkboxes added into a Table on each row. Purpose: toggle visibility for the
  956. tool/aperture found on that row.
  957. :return:
  958. """
  959. # self.ui.cnc_tools_table.cellWidget(row, 2).widget().setCheckState(QtCore.Qt.Unchecked)
  960. self.ui_disconnect()
  961. # cw = self.sender()
  962. # cw_index = self.ui.cnc_tools_table.indexAt(cw.pos())
  963. # cw_row = cw_index.row()
  964. kind = self.ui.cncplot_method_combo.get_value()
  965. self.shapes.clear(update=True)
  966. if self.origin_kind == "excellon":
  967. for r in range(self.ui.exc_cnc_tools_table.rowCount()):
  968. row_dia = float('%.*f' % (self.decimals, float(self.ui.exc_cnc_tools_table.item(r, 1).text())))
  969. for tooluid_key in self.exc_cnc_tools:
  970. tooldia = float('%.*f' % (self.decimals, float(tooluid_key)))
  971. if row_dia == tooldia:
  972. gcode_parsed = self.exc_cnc_tools[tooluid_key]['gcode_parsed']
  973. if self.ui.exc_cnc_tools_table.cellWidget(r, 6).isChecked():
  974. self.plot2(tooldia=tooldia, obj=self, visible=True, gcode_parsed=gcode_parsed, kind=kind)
  975. else:
  976. for tooluid_key in self.cnc_tools:
  977. tooldia = float('%.*f' % (self.decimals, float(self.cnc_tools[tooluid_key]['tooldia'])))
  978. gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
  979. # tool_uid = int(self.ui.cnc_tools_table.item(cw_row, 3).text())
  980. for r in range(self.ui.cnc_tools_table.rowCount()):
  981. if int(self.ui.cnc_tools_table.item(r, 5).text()) == int(tooluid_key):
  982. if self.ui.cnc_tools_table.cellWidget(r, 6).isChecked():
  983. self.plot2(tooldia=tooldia, obj=self, visible=True, gcode_parsed=gcode_parsed, kind=kind)
  984. self.shapes.redraw()
  985. # make sure that the general plot is disabled if one of the row plot's are disabled and
  986. # if all the row plot's are enabled also enable the general plot checkbox
  987. cb_cnt = 0
  988. total_row = self.ui.cnc_tools_table.rowCount()
  989. for row in range(total_row):
  990. if self.ui.cnc_tools_table.cellWidget(row, 6).isChecked():
  991. cb_cnt += 1
  992. else:
  993. cb_cnt -= 1
  994. if cb_cnt < total_row:
  995. self.ui.plot_cb.setChecked(False)
  996. else:
  997. self.ui.plot_cb.setChecked(True)
  998. self.ui_connect()
  999. def plot(self, visible=None, kind='all'):
  1000. """
  1001. # Does all the required setup and returns False
  1002. # if the 'ptint' option is set to False.
  1003. :param visible: Boolean to decide if the object will be plotted as visible or disabled on canvas
  1004. :param kind: String. Can be "all" or "travel" or "cut". For CNCJob plotting
  1005. :return: None
  1006. """
  1007. if not FlatCAMObj.plot(self):
  1008. return
  1009. visible = visible if visible else self.options['plot']
  1010. if self.app.is_legacy is False:
  1011. if self.ui.annotation_cb.get_value() and self.ui.plot_cb.get_value():
  1012. self.text_col.enabled = True
  1013. else:
  1014. self.text_col.enabled = False
  1015. self.annotation.redraw()
  1016. try:
  1017. if self.multitool is False: # single tool usage
  1018. try:
  1019. dia_plot = float(self.options["tooldia"])
  1020. except ValueError:
  1021. # we may have a tuple with only one element and a comma
  1022. dia_plot = [float(el) for el in self.options["tooldia"].split(',') if el != ''][0]
  1023. self.plot2(tooldia=dia_plot, obj=self, visible=visible, kind=kind)
  1024. else:
  1025. # I do this so the travel lines thickness will reflect the tool diameter
  1026. # may work only for objects created within the app and not Gcode imported from elsewhere for which we
  1027. # don't know the origin
  1028. if self.origin_kind == "excellon":
  1029. if self.exc_cnc_tools:
  1030. for tooldia_key in self.exc_cnc_tools:
  1031. tooldia = float('%.*f' % (self.decimals, float(tooldia_key)))
  1032. gcode_parsed = self.exc_cnc_tools[tooldia_key]['gcode_parsed']
  1033. if not gcode_parsed:
  1034. continue
  1035. # gcode_parsed = self.gcode_parsed
  1036. self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
  1037. else:
  1038. # multiple tools usage
  1039. if self.cnc_tools:
  1040. for tooluid_key in self.cnc_tools:
  1041. tooldia = float('%.*f' % (self.decimals, float(self.cnc_tools[tooluid_key]['tooldia'])))
  1042. gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
  1043. self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
  1044. self.shapes.redraw()
  1045. except (ObjectDeleted, AttributeError):
  1046. self.shapes.clear(update=True)
  1047. if self.app.is_legacy is False:
  1048. self.annotation.clear(update=True)
  1049. def on_annotation_change(self):
  1050. """
  1051. Handler for toggling the annotation display by clicking a checkbox.
  1052. :return:
  1053. """
  1054. if self.app.is_legacy is False:
  1055. if self.ui.annotation_cb.get_value():
  1056. self.text_col.enabled = True
  1057. else:
  1058. self.text_col.enabled = False
  1059. # kind = self.ui.cncplot_method_combo.get_value()
  1060. # self.plot(kind=kind)
  1061. self.annotation.redraw()
  1062. else:
  1063. kind = self.ui.cncplot_method_combo.get_value()
  1064. self.plot(kind=kind)
  1065. def convert_units(self, units):
  1066. """
  1067. Units conversion used by the CNCJob objects.
  1068. :param units: Can be "MM" or "IN"
  1069. :return:
  1070. """
  1071. log.debug("FlatCAMObj.FlatCAMECNCjob.convert_units()")
  1072. factor = CNCjob.convert_units(self, units)
  1073. self.options["tooldia"] = float(self.options["tooldia"]) * factor
  1074. param_list = ['cutz', 'depthperpass', 'travelz', 'feedrate', 'feedrate_z', 'feedrate_rapid',
  1075. 'endz', 'toolchangez']
  1076. temp_tools_dict = {}
  1077. tool_dia_copy = {}
  1078. data_copy = {}
  1079. for tooluid_key, tooluid_value in self.cnc_tools.items():
  1080. for dia_key, dia_value in tooluid_value.items():
  1081. if dia_key == 'tooldia':
  1082. dia_value *= factor
  1083. dia_value = float('%.*f' % (self.decimals, dia_value))
  1084. tool_dia_copy[dia_key] = dia_value
  1085. if dia_key == 'offset':
  1086. tool_dia_copy[dia_key] = dia_value
  1087. if dia_key == 'offset_value':
  1088. dia_value *= factor
  1089. tool_dia_copy[dia_key] = dia_value
  1090. if dia_key == 'type':
  1091. tool_dia_copy[dia_key] = dia_value
  1092. if dia_key == 'tool_type':
  1093. tool_dia_copy[dia_key] = dia_value
  1094. if dia_key == 'data':
  1095. for data_key, data_value in dia_value.items():
  1096. # convert the form fields that are convertible
  1097. for param in param_list:
  1098. if data_key == param and data_value is not None:
  1099. data_copy[data_key] = data_value * factor
  1100. # copy the other dict entries that are not convertible
  1101. if data_key not in param_list:
  1102. data_copy[data_key] = data_value
  1103. tool_dia_copy[dia_key] = deepcopy(data_copy)
  1104. data_copy.clear()
  1105. if dia_key == 'gcode':
  1106. tool_dia_copy[dia_key] = dia_value
  1107. if dia_key == 'gcode_parsed':
  1108. tool_dia_copy[dia_key] = dia_value
  1109. if dia_key == 'solid_geometry':
  1110. tool_dia_copy[dia_key] = dia_value
  1111. # if dia_key == 'solid_geometry':
  1112. # tool_dia_copy[dia_key] = affinity.scale(dia_value, xfact=factor, origin=(0, 0))
  1113. # if dia_key == 'gcode_parsed':
  1114. # for g in dia_value:
  1115. # g['geom'] = affinity.scale(g['geom'], factor, factor, origin=(0, 0))
  1116. #
  1117. # tool_dia_copy['gcode_parsed'] = deepcopy(dia_value)
  1118. # tool_dia_copy['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_value])
  1119. temp_tools_dict.update({
  1120. tooluid_key: deepcopy(tool_dia_copy)
  1121. })
  1122. tool_dia_copy.clear()
  1123. self.cnc_tools.clear()
  1124. self.cnc_tools = deepcopy(temp_tools_dict)