FlatCAMCNCJob.py 65 KB

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