FlatCAMCNCJob.py 55 KB

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