FlatCAMCNCJob.py 50 KB

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