FlatCAMCNCJob.py 52 KB

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