FlatCAMCNCJob.py 93 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205
  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, MultiPoint
  20. try:
  21. from shapely.ops import voronoi_diagram
  22. except Exception:
  23. pass
  24. import os
  25. import sys
  26. import time
  27. import serial
  28. import glob
  29. import math
  30. import gettext
  31. import appTranslation as fcTranslate
  32. import builtins
  33. fcTranslate.apply_language('strings')
  34. if '_' not in builtins.__dict__:
  35. _ = gettext.gettext
  36. class CNCJobObject(FlatCAMObj, CNCjob):
  37. """
  38. Represents G-Code.
  39. """
  40. optionChanged = QtCore.pyqtSignal(str)
  41. build_al_table_sig = QtCore.pyqtSignal()
  42. ui_type = CNCObjectUI
  43. def __init__(self, name, units="in", kind="generic", z_move=0.1,
  44. feedrate=3.0, feedrate_rapid=3.0, z_cut=-0.002, tooldia=0.0,
  45. spindlespeed=None):
  46. log.debug("Creating CNCJob object...")
  47. self.decimals = self.app.decimals
  48. CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
  49. feedrate=feedrate, feedrate_rapid=feedrate_rapid, z_cut=z_cut, tooldia=tooldia,
  50. spindlespeed=spindlespeed, steps_per_circle=int(self.app.defaults["cncjob_steps_per_circle"]))
  51. FlatCAMObj.__init__(self, name)
  52. self.kind = "cncjob"
  53. self.options.update({
  54. "plot": True,
  55. "tooldia": 0.03937, # 0.4mm in inches
  56. "append": "",
  57. "prepend": "",
  58. "dwell": False,
  59. "dwelltime": 1,
  60. "type": 'Geometry',
  61. # "toolchange_macro": '',
  62. # "toolchange_macro_enable": False
  63. })
  64. '''
  65. This is a dict of dictionaries. Each dict is associated with a tool present in the file. The key is the
  66. diameter of the tools and the value is another dict that will hold the data under the following form:
  67. {tooldia: {
  68. 'tooluid': 1,
  69. 'offset': 'Path',
  70. 'type_item': 'Rough',
  71. 'tool_type': 'C1',
  72. 'data': {} # a dict to hold the parameters
  73. 'gcode': "" # a string with the actual GCODE
  74. 'gcode_parsed': {} # dictionary holding the CNCJob geometry and type of geometry
  75. (cut or move)
  76. 'solid_geometry': []
  77. },
  78. ...
  79. }
  80. It is populated in the GeometryObject.mtool_gen_cncjob()
  81. BEWARE: I rely on the ordered nature of the Python 3.7 dictionary. Things might change ...
  82. '''
  83. self.cnc_tools = {}
  84. '''
  85. This is a dict of dictionaries. Each dict is associated with a tool present in the file. The key is the
  86. diameter of the tools and the value is another dict that will hold the data under the following form:
  87. {tooldia: {
  88. 'tool': int,
  89. 'nr_drills': int,
  90. 'nr_slots': int,
  91. 'offset': float,
  92. 'data': {}, a dict to hold the parameters
  93. 'gcode': "", a string with the actual GCODE
  94. 'gcode_parsed': [], list of dicts holding the CNCJob geometry and
  95. type of geometry (cut or move)
  96. 'solid_geometry': [],
  97. },
  98. ...
  99. }
  100. It is populated in the ExcellonObject.on_create_cncjob_click() but actually
  101. it's done in camlib.CNCJob.generate_from_excellon_by_tool()
  102. BEWARE: I rely on the ordered nature of the Python 3.7 dictionary. Things might change ...
  103. '''
  104. self.exc_cnc_tools = {}
  105. # flag to store if the CNCJob is part of a special group of CNCJob objects that can't be processed by the
  106. # default engine of FlatCAM. They generated by some of tools and are special cases of CNCJob objects.
  107. self.special_group = None
  108. # for now it show if the plot will be done for multi-tool CNCJob (True) or for single tool
  109. # (like the one in the TCL Command), False
  110. self.multitool = False
  111. # determine if the GCode was generated out of a Excellon object or a Geometry object
  112. self.origin_kind = None
  113. self.coords_decimals = 4
  114. self.fr_decimals = 2
  115. # used for parsing the GCode lines to adjust the GCode when the GCode is offseted or scaled
  116. gcodex_re_string = r'(?=.*(X[-\+]?\d*\.\d*))'
  117. self.g_x_re = re.compile(gcodex_re_string)
  118. gcodey_re_string = r'(?=.*(Y[-\+]?\d*\.\d*))'
  119. self.g_y_re = re.compile(gcodey_re_string)
  120. gcodez_re_string = r'(?=.*(Z[-\+]?\d*\.\d*))'
  121. self.g_z_re = re.compile(gcodez_re_string)
  122. gcodef_re_string = r'(?=.*(F[-\+]?\d*\.\d*))'
  123. self.g_f_re = re.compile(gcodef_re_string)
  124. gcodet_re_string = r'(?=.*(\=\s*[-\+]?\d*\.\d*))'
  125. self.g_t_re = re.compile(gcodet_re_string)
  126. gcodenr_re_string = r'([+-]?\d*\.\d+)'
  127. self.g_nr_re = re.compile(gcodenr_re_string)
  128. if self.app.is_legacy is False:
  129. self.text_col = self.app.plotcanvas.new_text_collection()
  130. self.text_col.enabled = True
  131. self.annotation = self.app.plotcanvas.new_text_group(collection=self.text_col)
  132. self.gcode_editor_tab = None
  133. self.gcode_viewer_tab = None
  134. self.source_file = ''
  135. self.units_found = self.app.defaults['units']
  136. self.probing_gcode_text = ''
  137. # store the current selection shape status to be restored after manual adding test points
  138. self.old_selection_state = self.app.defaults['global_selection_shape']
  139. # if mouse is dragging set the object True
  140. self.mouse_is_dragging = False
  141. # if mouse events are bound to local methods
  142. self.mouse_events_connected = False
  143. # event handlers references
  144. self.kp = None
  145. self.mm = None
  146. self.mr = None
  147. self.append_snippet = ''
  148. self.prepend_snippet = ''
  149. self.gc_header = self.gcode_header()
  150. self.gc_start = ''
  151. self.gc_end = ''
  152. '''
  153. dictionary of dictionaries to store the informations for the autolevelling
  154. format:
  155. {
  156. id: {
  157. 'point': Shapely Point
  158. 'geo': Shapely Polygon from Voronoi diagram,
  159. 'height': float
  160. }
  161. }
  162. '''
  163. self.al_geometry_dict = {}
  164. self.solid_geo = None
  165. self.grbl_ser_port = None
  166. self.pressed_button = None
  167. # Attributes to be included in serialization
  168. # Always append to it because it carries contents
  169. # from predecessors.
  170. self.ser_attrs += [
  171. 'options', 'kind', 'origin_kind', 'cnc_tools', 'exc_cnc_tools', 'multitool', 'append_snippet',
  172. 'prepend_snippet', 'gc_header'
  173. ]
  174. def build_ui(self):
  175. self.ui_disconnect()
  176. FlatCAMObj.build_ui(self)
  177. self.units = self.app.defaults['units'].upper()
  178. # if the FlatCAM object is Excellon don't build the CNC Tools Table but hide it
  179. self.ui.cnc_tools_table.hide()
  180. if self.cnc_tools:
  181. self.ui.cnc_tools_table.show()
  182. self.build_cnc_tools_table()
  183. self.ui.exc_cnc_tools_table.hide()
  184. if self.exc_cnc_tools:
  185. self.ui.exc_cnc_tools_table.show()
  186. self.build_excellon_cnc_tools()
  187. if self.ui.sal_cb.get_value():
  188. self.build_al_table()
  189. self.ui_connect()
  190. def build_cnc_tools_table(self):
  191. tool_idx = 0
  192. n = len(self.cnc_tools)
  193. self.ui.cnc_tools_table.setRowCount(n)
  194. for dia_key, dia_value in self.cnc_tools.items():
  195. tool_idx += 1
  196. row_no = tool_idx - 1
  197. t_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
  198. # id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
  199. self.ui.cnc_tools_table.setItem(row_no, 0, t_id) # Tool name/id
  200. # Make sure that the tool diameter when in MM is with no more than 2 decimals.
  201. # There are no tool bits in MM with more than 2 decimals diameter.
  202. # For INCH the decimals should be no more than 4. There are no tools under 10mils.
  203. dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(dia_value['tooldia'])))
  204. offset_txt = list(str(dia_value['offset']))
  205. offset_txt[0] = offset_txt[0].upper()
  206. offset_item = QtWidgets.QTableWidgetItem(''.join(offset_txt))
  207. type_item = QtWidgets.QTableWidgetItem(str(dia_value['type']))
  208. tool_type_item = QtWidgets.QTableWidgetItem(str(dia_value['tool_type']))
  209. t_id.setFlags(QtCore.Qt.ItemIsEnabled)
  210. dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
  211. offset_item.setFlags(QtCore.Qt.ItemIsEnabled)
  212. type_item.setFlags(QtCore.Qt.ItemIsEnabled)
  213. tool_type_item.setFlags(QtCore.Qt.ItemIsEnabled)
  214. # hack so the checkbox stay centered in the table cell
  215. # used this:
  216. # https://stackoverflow.com/questions/32458111/pyqt-allign-checkbox-and-put-it-in-every-row
  217. # plot_item = QtWidgets.QWidget()
  218. # checkbox = FCCheckBox()
  219. # checkbox.setCheckState(QtCore.Qt.Checked)
  220. # qhboxlayout = QtWidgets.QHBoxLayout(plot_item)
  221. # qhboxlayout.addWidget(checkbox)
  222. # qhboxlayout.setAlignment(QtCore.Qt.AlignCenter)
  223. # qhboxlayout.setContentsMargins(0, 0, 0, 0)
  224. plot_item = FCCheckBox()
  225. plot_item.setLayoutDirection(QtCore.Qt.RightToLeft)
  226. tool_uid_item = QtWidgets.QTableWidgetItem(str(dia_key))
  227. if self.ui.plot_cb.isChecked():
  228. plot_item.setChecked(True)
  229. self.ui.cnc_tools_table.setItem(row_no, 1, dia_item) # Diameter
  230. self.ui.cnc_tools_table.setItem(row_no, 2, offset_item) # Offset
  231. self.ui.cnc_tools_table.setItem(row_no, 3, type_item) # Toolpath Type
  232. self.ui.cnc_tools_table.setItem(row_no, 4, tool_type_item) # Tool Type
  233. # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY # ##
  234. self.ui.cnc_tools_table.setItem(row_no, 5, tool_uid_item) # Tool unique ID)
  235. self.ui.cnc_tools_table.setCellWidget(row_no, 6, plot_item)
  236. # make the diameter column editable
  237. # for row in range(tool_idx):
  238. # self.ui.cnc_tools_table.item(row, 1).setFlags(QtCore.Qt.ItemIsSelectable |
  239. # QtCore.Qt.ItemIsEnabled)
  240. for row in range(tool_idx):
  241. self.ui.cnc_tools_table.item(row, 0).setFlags(
  242. self.ui.cnc_tools_table.item(row, 0).flags() ^ QtCore.Qt.ItemIsSelectable)
  243. self.ui.cnc_tools_table.resizeColumnsToContents()
  244. self.ui.cnc_tools_table.resizeRowsToContents()
  245. vertical_header = self.ui.cnc_tools_table.verticalHeader()
  246. # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
  247. vertical_header.hide()
  248. self.ui.cnc_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  249. horizontal_header = self.ui.cnc_tools_table.horizontalHeader()
  250. horizontal_header.setMinimumSectionSize(10)
  251. horizontal_header.setDefaultSectionSize(70)
  252. horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
  253. horizontal_header.resizeSection(0, 20)
  254. horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
  255. horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
  256. horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed)
  257. horizontal_header.resizeSection(4, 40)
  258. horizontal_header.setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed)
  259. horizontal_header.resizeSection(4, 17)
  260. # horizontal_header.setStretchLastSection(True)
  261. self.ui.cnc_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  262. self.ui.cnc_tools_table.setColumnWidth(0, 20)
  263. self.ui.cnc_tools_table.setColumnWidth(4, 40)
  264. self.ui.cnc_tools_table.setColumnWidth(6, 17)
  265. # self.ui.geo_tools_table.setSortingEnabled(True)
  266. self.ui.cnc_tools_table.setMinimumHeight(self.ui.cnc_tools_table.getHeight())
  267. self.ui.cnc_tools_table.setMaximumHeight(self.ui.cnc_tools_table.getHeight())
  268. def build_excellon_cnc_tools(self):
  269. tool_idx = 0
  270. n = len(self.exc_cnc_tools)
  271. self.ui.exc_cnc_tools_table.setRowCount(n)
  272. for tooldia_key, dia_value in self.exc_cnc_tools.items():
  273. tool_idx += 1
  274. row_no = tool_idx - 1
  275. t_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
  276. dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(tooldia_key)))
  277. nr_drills_item = QtWidgets.QTableWidgetItem('%d' % int(dia_value['nr_drills']))
  278. nr_slots_item = QtWidgets.QTableWidgetItem('%d' % int(dia_value['nr_slots']))
  279. cutz_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(dia_value['offset']) + self.z_cut))
  280. t_id.setFlags(QtCore.Qt.ItemIsEnabled)
  281. dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
  282. nr_drills_item.setFlags(QtCore.Qt.ItemIsEnabled)
  283. nr_slots_item.setFlags(QtCore.Qt.ItemIsEnabled)
  284. cutz_item.setFlags(QtCore.Qt.ItemIsEnabled)
  285. # hack so the checkbox stay centered in the table cell
  286. # used this:
  287. # https://stackoverflow.com/questions/32458111/pyqt-allign-checkbox-and-put-it-in-every-row
  288. # plot_item = QtWidgets.QWidget()
  289. # checkbox = FCCheckBox()
  290. # checkbox.setCheckState(QtCore.Qt.Checked)
  291. # qhboxlayout = QtWidgets.QHBoxLayout(plot_item)
  292. # qhboxlayout.addWidget(checkbox)
  293. # qhboxlayout.setAlignment(QtCore.Qt.AlignCenter)
  294. # qhboxlayout.setContentsMargins(0, 0, 0, 0)
  295. plot_item = FCCheckBox()
  296. plot_item.setLayoutDirection(QtCore.Qt.RightToLeft)
  297. tool_uid_item = QtWidgets.QTableWidgetItem(str(dia_value['tool']))
  298. if self.ui.plot_cb.isChecked():
  299. plot_item.setChecked(True)
  300. self.ui.exc_cnc_tools_table.setItem(row_no, 0, t_id) # Tool name/id
  301. self.ui.exc_cnc_tools_table.setItem(row_no, 1, dia_item) # Diameter
  302. self.ui.exc_cnc_tools_table.setItem(row_no, 2, nr_drills_item) # Nr of drills
  303. self.ui.exc_cnc_tools_table.setItem(row_no, 3, nr_slots_item) # Nr of slots
  304. # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY # ##
  305. self.ui.exc_cnc_tools_table.setItem(row_no, 4, tool_uid_item) # Tool unique ID)
  306. self.ui.exc_cnc_tools_table.setItem(row_no, 5, cutz_item)
  307. self.ui.exc_cnc_tools_table.setCellWidget(row_no, 6, plot_item)
  308. for row in range(tool_idx):
  309. self.ui.exc_cnc_tools_table.item(row, 0).setFlags(
  310. self.ui.exc_cnc_tools_table.item(row, 0).flags() ^ QtCore.Qt.ItemIsSelectable)
  311. self.ui.exc_cnc_tools_table.resizeColumnsToContents()
  312. self.ui.exc_cnc_tools_table.resizeRowsToContents()
  313. vertical_header = self.ui.exc_cnc_tools_table.verticalHeader()
  314. vertical_header.hide()
  315. self.ui.exc_cnc_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  316. horizontal_header = self.ui.exc_cnc_tools_table.horizontalHeader()
  317. horizontal_header.setMinimumSectionSize(10)
  318. horizontal_header.setDefaultSectionSize(70)
  319. horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
  320. horizontal_header.resizeSection(0, 20)
  321. horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
  322. horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
  323. horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
  324. horizontal_header.setSectionResizeMode(5, QtWidgets.QHeaderView.ResizeToContents)
  325. horizontal_header.setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed)
  326. # horizontal_header.setStretchLastSection(True)
  327. self.ui.exc_cnc_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  328. self.ui.exc_cnc_tools_table.setColumnWidth(0, 20)
  329. self.ui.exc_cnc_tools_table.setColumnWidth(6, 17)
  330. self.ui.exc_cnc_tools_table.setMinimumHeight(self.ui.exc_cnc_tools_table.getHeight())
  331. self.ui.exc_cnc_tools_table.setMaximumHeight(self.ui.exc_cnc_tools_table.getHeight())
  332. def build_al_table(self):
  333. tool_idx = 0
  334. n = len(self.al_geometry_dict)
  335. self.ui.al_probe_points_table.setRowCount(n)
  336. for id_key, value in self.al_geometry_dict.items():
  337. tool_idx += 1
  338. row_no = tool_idx - 1
  339. t_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
  340. x = value['point'].x
  341. y = value['point'].y
  342. xy_coords = self.app.dec_format(x, dec=self.app.decimals), self.app.dec_format(y, dec=self.app.decimals)
  343. coords_item = QtWidgets.QTableWidgetItem(str(xy_coords))
  344. height = self.app.dec_format(value['height'], dec=self.app.decimals)
  345. height_item = QtWidgets.QTableWidgetItem(str(height))
  346. t_id.setFlags(QtCore.Qt.ItemIsEnabled)
  347. coords_item.setFlags(QtCore.Qt.ItemIsEnabled)
  348. height_item.setFlags(QtCore.Qt.ItemIsEnabled)
  349. self.ui.al_probe_points_table.setItem(row_no, 0, t_id) # Tool name/id
  350. self.ui.al_probe_points_table.setItem(row_no, 1, coords_item) # X-Y coords
  351. self.ui.al_probe_points_table.setItem(row_no, 2, height_item) # Determined Height
  352. self.ui.al_probe_points_table.resizeColumnsToContents()
  353. self.ui.al_probe_points_table.resizeRowsToContents()
  354. h_header = self.ui.al_probe_points_table.horizontalHeader()
  355. h_header.setMinimumSectionSize(10)
  356. h_header.setDefaultSectionSize(70)
  357. h_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
  358. h_header.resizeSection(0, 20)
  359. h_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
  360. h_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
  361. self.ui.al_probe_points_table.setMinimumHeight(self.ui.al_probe_points_table.getHeight())
  362. self.ui.al_probe_points_table.setMaximumHeight(self.ui.al_probe_points_table.getHeight())
  363. if self.ui.al_probe_points_table.model().rowCount():
  364. self.ui.voronoi_cb.setDisabled(False)
  365. self.ui.grbl_get_heightmap_button.setDisabled(False)
  366. self.ui.h_gcode_button.setDisabled(False)
  367. self.ui.view_h_gcode_button.setDisabled(False)
  368. else:
  369. self.ui.voronoi_cb.setDisabled(True)
  370. self.ui.grbl_get_heightmap_button.setDisabled(True)
  371. self.ui.h_gcode_button.setDisabled(True)
  372. self.ui.view_h_gcode_button.setDisabled(True)
  373. def set_ui(self, ui):
  374. FlatCAMObj.set_ui(self, ui)
  375. log.debug("FlatCAMCNCJob.set_ui()")
  376. assert isinstance(self.ui, CNCObjectUI), \
  377. "Expected a CNCObjectUI, got %s" % type(self.ui)
  378. self.units = self.app.defaults['units'].upper()
  379. self.units_found = self.app.defaults['units']
  380. # this signal has to be connected to it's slot before the defaults are populated
  381. # the decision done in the slot has to override the default value set below
  382. # self.ui.toolchange_cb.toggled.connect(self.on_toolchange_custom_clicked)
  383. self.form_fields.update({
  384. "plot": self.ui.plot_cb,
  385. "tooldia": self.ui.tooldia_entry,
  386. # "append": self.ui.append_text,
  387. # "prepend": self.ui.prepend_text,
  388. # "toolchange_macro": self.ui.toolchange_text,
  389. # "toolchange_macro_enable": self.ui.toolchange_cb,
  390. "al_travelz": self.ui.ptravelz_entry,
  391. "al_probe_depth": self.ui.pdepth_entry,
  392. "al_probe_fr": self.ui.feedrate_probe_entry,
  393. "al_controller": self.ui.al_controller_combo,
  394. "al_mode": self.ui.al_mode_radio,
  395. "al_rows": self.ui.al_rows_entry,
  396. "al_columns": self.ui.al_columns_entry,
  397. "al_grbl_jog_step": self.ui.jog_step_entry,
  398. "al_grbl_jog_fr": self.ui.jog_fr_entry,
  399. })
  400. self.append_snippet = self.app.defaults['cncjob_append']
  401. self.prepend_snippet = self.app.defaults['cncjob_prepend']
  402. if self.append_snippet != '' or self.prepend_snippet:
  403. self.ui.snippets_cb.set_value(True)
  404. # Fill form fields only on object create
  405. self.to_form()
  406. # this means that the object that created this CNCJob was an Excellon or Geometry
  407. try:
  408. if self.travel_distance:
  409. self.ui.t_distance_label.show()
  410. self.ui.t_distance_entry.setVisible(True)
  411. self.ui.t_distance_entry.setDisabled(True)
  412. self.ui.t_distance_entry.set_value('%.*f' % (self.decimals, float(self.travel_distance)))
  413. self.ui.units_label.setText(str(self.units).lower())
  414. self.ui.units_label.setDisabled(True)
  415. self.ui.t_time_label.show()
  416. self.ui.t_time_entry.setVisible(True)
  417. self.ui.t_time_entry.setDisabled(True)
  418. # if time is more than 1 then we have minutes, else we have seconds
  419. if self.routing_time > 1:
  420. self.ui.t_time_entry.set_value('%.*f' % (self.decimals, math.ceil(float(self.routing_time))))
  421. self.ui.units_time_label.setText('min')
  422. else:
  423. time_r = self.routing_time * 60
  424. self.ui.t_time_entry.set_value('%.*f' % (self.decimals, math.ceil(float(time_r))))
  425. self.ui.units_time_label.setText('sec')
  426. self.ui.units_time_label.setDisabled(True)
  427. except AttributeError:
  428. pass
  429. if self.multitool is False:
  430. self.ui.tooldia_entry.show()
  431. self.ui.updateplot_button.show()
  432. else:
  433. self.ui.tooldia_entry.hide()
  434. self.ui.updateplot_button.hide()
  435. # set the kind of geometries are plotted by default with plot2() from camlib.CNCJob
  436. self.ui.cncplot_method_combo.set_value(self.app.defaults["cncjob_plot_kind"])
  437. try:
  438. self.ui.annotation_cb.stateChanged.disconnect(self.on_annotation_change)
  439. except (TypeError, AttributeError):
  440. pass
  441. self.ui.annotation_cb.stateChanged.connect(self.on_annotation_change)
  442. # set if to display text annotations
  443. self.ui.annotation_cb.set_value(self.app.defaults["cncjob_annotation"])
  444. self.ui.updateplot_button.clicked.connect(self.on_updateplot_button_click)
  445. self.ui.export_gcode_button.clicked.connect(self.on_exportgcode_button_click)
  446. self.ui.review_gcode_button.clicked.connect(self.on_edit_code_click)
  447. self.ui.editor_button.clicked.connect(lambda: self.app.object2editor())
  448. # autolevelling signals
  449. self.ui.sal_cb.stateChanged.connect(self.on_autolevelling)
  450. self.ui.al_mode_radio.activated_custom.connect(self.on_mode_radio)
  451. self.ui.al_controller_combo.currentIndexChanged.connect(self.on_controller_change)
  452. self.ui.voronoi_cb.stateChanged.connect(self.show_voronoi_diagram)
  453. # GRBL
  454. self.ui.com_search_button.clicked.connect(self.on_search_ports)
  455. self.ui.add_bd_button.clicked.connect(self.on_add_baudrate_grbl)
  456. self.ui.del_bd_button.clicked.connect(self.on_delete_baudrate_grbl)
  457. self.ui.controller_reset_button.clicked.connect(self.on_grbl_reset)
  458. self.ui.com_connect_button.clicked.connect(self.on_connect_grbl)
  459. self.ui.grbl_send_button.clicked.connect(self.on_send_grbl_command)
  460. self.ui.grbl_command_entry.returnPressed.connect(self.on_send_grbl_command)
  461. # Jog
  462. self.ui.jog_wdg.jog_up_button.clicked.connect(lambda: self.on_jog(direction='yplus'))
  463. self.ui.jog_wdg.jog_down_button.clicked.connect(lambda: self.on_jog(direction='yminus'))
  464. self.ui.jog_wdg.jog_right_button.clicked.connect(lambda: self.on_jog(direction='xplus'))
  465. self.ui.jog_wdg.jog_left_button.clicked.connect(lambda: self.on_jog(direction='xminus'))
  466. self.ui.jog_wdg.jog_z_up_button.clicked.connect(lambda: self.on_jog(direction='zplus'))
  467. self.ui.jog_wdg.jog_z_down_button.clicked.connect(lambda: self.on_jog(direction='zminus'))
  468. self.ui.jog_wdg.jog_origin_button.clicked.connect(lambda: self.on_jog(direction='origin'))
  469. # Zero
  470. self.ui.zero_axs_wdg.grbl_zerox_button.clicked.connect(lambda: self.on_grbl_zero(axis='x'))
  471. self.ui.zero_axs_wdg.grbl_zeroy_button.clicked.connect(lambda: self.on_grbl_zero(axis='y'))
  472. self.ui.zero_axs_wdg.grbl_zeroz_button.clicked.connect(lambda: self.on_grbl_zero(axis='z'))
  473. self.ui.zero_axs_wdg.grbl_zero_all_button.clicked.connect(lambda: self.on_grbl_zero(axis='all'))
  474. self.ui.zero_axs_wdg.grbl_homing_button.clicked.connect(self.on_grbl_homing)
  475. # Sender
  476. self.ui.grbl_report_button.clicked.connect(lambda: self.send_grbl_command(command='?'))
  477. self.ui.grbl_get_param_button.clicked.connect(
  478. lambda: self.get_grbl_parameter(param=self.ui.grbl_parameter_entry.get_value()))
  479. self.ui.view_h_gcode_button.clicked.connect(self.on_view_probing_gcode)
  480. self.ui.h_gcode_button.clicked.connect(self.on_generate_probing_gcode)
  481. self.ui.import_heights_button.clicked.connect(self.on_import_height_map)
  482. self.ui.pause_resume_button.clicked.connect(self.on_grbl_pause_resume)
  483. self.build_al_table_sig.connect(self.build_al_table)
  484. # self.ui.tc_variable_combo.currentIndexChanged[str].connect(self.on_cnc_custom_parameters)
  485. self.ui.cncplot_method_combo.activated_custom.connect(self.on_plot_kind_change)
  486. # Show/Hide Advanced Options
  487. if self.app.defaults["global_app_level"] == 'b':
  488. self.ui.level.setText(_(
  489. '<span style="color:green;"><b>Basic</b></span>'
  490. ))
  491. self.ui.sal_cb.hide()
  492. self.ui.sal_cb.set_value(False)
  493. else:
  494. self.ui.level.setText(_(
  495. '<span style="color:red;"><b>Advanced</b></span>'
  496. ))
  497. if 'Roland' in self.pp_excellon_name or 'Roland' in self.pp_geometry_name or 'hpgl' in \
  498. self.pp_geometry_name:
  499. pass
  500. else:
  501. self.ui.sal_cb.show()
  502. self.ui.sal_cb.set_value(self.app.defaults["cncjob_al_status"])
  503. preamble = self.append_snippet
  504. postamble = self.prepend_snippet
  505. gc = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True)
  506. self.source_file = gc.getvalue()
  507. self.ui.al_mode_radio.set_value(self.options['al_mode'])
  508. self.on_controller_change()
  509. # def on_cnc_custom_parameters(self, signal_text):
  510. # if signal_text == 'Parameters':
  511. # return
  512. # else:
  513. # self.ui.toolchange_text.insertPlainText('%%%s%%' % signal_text)
  514. def ui_connect(self):
  515. for row in range(self.ui.cnc_tools_table.rowCount()):
  516. self.ui.cnc_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table)
  517. for row in range(self.ui.exc_cnc_tools_table.rowCount()):
  518. self.ui.exc_cnc_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table)
  519. self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
  520. self.ui.al_add_button.clicked.connect(self.on_add_al_probepoints)
  521. self.ui.show_al_table.stateChanged.connect(self.on_show_al_table)
  522. def ui_disconnect(self):
  523. for row in range(self.ui.cnc_tools_table.rowCount()):
  524. try:
  525. self.ui.cnc_tools_table.cellWidget(row, 6).clicked.disconnect(self.on_plot_cb_click_table)
  526. except (TypeError, AttributeError):
  527. pass
  528. for row in range(self.ui.exc_cnc_tools_table.rowCount()):
  529. try:
  530. self.ui.exc_cnc_tools_table.cellWidget(row, 6).clicked.disconnect(self.on_plot_cb_click_table)
  531. except (TypeError, AttributeError):
  532. pass
  533. try:
  534. self.ui.plot_cb.stateChanged.disconnect(self.on_plot_cb_click)
  535. except (TypeError, AttributeError):
  536. pass
  537. try:
  538. self.ui.al_add_button.clicked.disconnect()
  539. except (TypeError, AttributeError):
  540. pass
  541. try:
  542. self.ui.show_al_table.stateChanged.disconnect()
  543. except (TypeError, AttributeError):
  544. pass
  545. def on_add_al_probepoints(self):
  546. # create the solid_geo
  547. self.solid_geo = unary_union([geo['geom'] for geo in self.gcode_parsed if geo['kind'][0] == 'C'])
  548. # reset al table
  549. self.ui.al_probe_points_table.setRowCount(0)
  550. # reset the al dict
  551. self.al_geometry_dict.clear()
  552. xmin, ymin, xmax, ymax = self.solid_geo.bounds
  553. if self.ui.al_mode_radio.get_value() == 'grid':
  554. width = abs(xmax - xmin)
  555. height = abs(ymax - ymin)
  556. cols = self.ui.al_columns_entry.get_value()
  557. rows = self.ui.al_rows_entry.get_value()
  558. dx = width / (cols + 1)
  559. dy = height / (rows + 1)
  560. points = []
  561. new_y = ymin
  562. for x in range(rows):
  563. new_y += dy
  564. new_x = xmin
  565. for y in range(cols):
  566. new_x += dx
  567. formatted_point = (
  568. self.app.dec_format(new_x, self.app.decimals),
  569. self.app.dec_format(new_y, self.app.decimals)
  570. )
  571. points.append(formatted_point)
  572. pt_id = 0
  573. pts_list = []
  574. for point in points:
  575. pt_id += 1
  576. pt = Point(point)
  577. pts_list.append(pt)
  578. new_dict = {
  579. 'point': pt,
  580. 'geo': None,
  581. 'height': 0.0
  582. }
  583. self.al_geometry_dict[pt_id] = deepcopy(new_dict)
  584. self.calculate_voronoi_diagram(pts=pts_list)
  585. else:
  586. self.app.inform.emit(_("Click on canvas to add a Test Point..."))
  587. if self.app.is_legacy is False:
  588. self.app.plotcanvas.graph_event_disconnect('key_press', self.app.ui.keyPressEvent)
  589. self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
  590. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
  591. else:
  592. self.app.plotcanvas.graph_event_disconnect(self.app.kp)
  593. self.app.plotcanvas.graph_event_disconnect(self.app.mp)
  594. self.app.plotcanvas.graph_event_disconnect(self.app.mr)
  595. self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
  596. self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
  597. self.mouse_events_connected = True
  598. self.build_al_table_sig.emit()
  599. def show_voronoi_diagram(self, state):
  600. if state:
  601. pass
  602. else:
  603. pass
  604. def calculate_voronoi_diagram(self, pts):
  605. pts_union = MultiPoint(pts)
  606. env = self.solid_geo.envelope
  607. print(pts_union.wkt)
  608. try:
  609. voronoi_union = voronoi_diagram(geom=pts_union, envelope=env)
  610. print(voronoi_union)
  611. except Exception as e:
  612. log.debug("CNCJobObject.calculate_voronoi_diagram() --> %s" % str(e))
  613. return
  614. for pt_key in list(self.al_geometry_dict.keys()):
  615. for poly in voronoi_union:
  616. if self.al_geometry_dict[pt_key]['point'].within(poly):
  617. self.al_geometry_dict[pt_key]['geo'] = poly
  618. # To be called after clicking on the plot.
  619. def on_mouse_click_release(self, event):
  620. if self.app.is_legacy is False:
  621. event_pos = event.pos
  622. # event_is_dragging = event.is_dragging
  623. right_button = 2
  624. else:
  625. event_pos = (event.xdata, event.ydata)
  626. # event_is_dragging = self.app.plotcanvas.is_dragging
  627. right_button = 3
  628. try:
  629. x = float(event_pos[0])
  630. y = float(event_pos[1])
  631. except TypeError:
  632. return
  633. event_pos = (x, y)
  634. # do paint single only for left mouse clicks
  635. if event.button == 1:
  636. pos = self.app.plotcanvas.translate_coords(event_pos)
  637. # use the snapped position as reference
  638. snapped_pos = self.app.geo_editor.snap(pos[0], pos[1])
  639. if not self.al_geometry_dict:
  640. new_dict = {
  641. 'point': Point(snapped_pos),
  642. 'geo': None,
  643. 'height': 0.0
  644. }
  645. self.al_geometry_dict[1] = deepcopy(new_dict)
  646. else:
  647. int_keys = [int(k) for k in self.al_geometry_dict.keys()]
  648. new_id = max(int_keys) + 1
  649. new_dict = {
  650. 'point': Point(snapped_pos),
  651. 'geo': None,
  652. 'height': 0.0
  653. }
  654. self.al_geometry_dict[new_id] = deepcopy(new_dict)
  655. # rebuild the al table
  656. self.build_al_table_sig.emit()
  657. self.app.inform.emit(_("Added Test Point... Click again to add another or right click to finish ..."))
  658. # if RMB then we exit
  659. elif event.button == right_button and self.mouse_is_dragging is False:
  660. if self.app.is_legacy is False:
  661. self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
  662. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
  663. else:
  664. self.app.plotcanvas.graph_event_disconnect(self.kp)
  665. self.app.plotcanvas.graph_event_disconnect(self.mr)
  666. self.app.kp = self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent)
  667. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot)
  668. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  669. self.app.on_mouse_click_release_over_plot)
  670. # signal that the mouse events are disconnected from local methods
  671. self.mouse_events_connected = False
  672. # restore selection
  673. self.app.defaults['global_selection_shape'] = self.old_selection_state
  674. self.app.inform.emit(_("Finished manual adding of Test Point..."))
  675. pts_list = []
  676. for k in self.al_geometry_dict:
  677. pts_list.append(self.al_geometry_dict[k]['point'])
  678. self.calculate_voronoi_diagram(pts=pts_list)
  679. # rebuild the al table
  680. self.build_al_table_sig.emit()
  681. def on_key_press(self, event):
  682. # events out of the self.app.collection view (it's about Project Tab) are of type int
  683. if type(event) is int:
  684. key = event
  685. # events from the GUI are of type QKeyEvent
  686. elif type(event) == QtGui.QKeyEvent:
  687. key = event.key()
  688. elif isinstance(event, mpl_key_event): # MatPlotLib key events are trickier to interpret than the rest
  689. key = event.key
  690. key = QtGui.QKeySequence(key)
  691. # check for modifiers
  692. key_string = key.toString().lower()
  693. if '+' in key_string:
  694. mod, __, key_text = key_string.rpartition('+')
  695. if mod.lower() == 'ctrl':
  696. # modifiers = QtCore.Qt.ControlModifier
  697. pass
  698. elif mod.lower() == 'alt':
  699. # modifiers = QtCore.Qt.AltModifier
  700. pass
  701. elif mod.lower() == 'shift':
  702. # modifiers = QtCore.Qt.ShiftModifier
  703. pass
  704. else:
  705. # modifiers = QtCore.Qt.NoModifier
  706. pass
  707. key = QtGui.QKeySequence(key_text)
  708. # events from Vispy are of type KeyEvent
  709. else:
  710. key = event.key
  711. # Escape = Deselect All
  712. if key == QtCore.Qt.Key_Escape or key == 'Escape':
  713. if self.mouse_events_connected is True:
  714. self.mouse_events_connected = False
  715. if self.app.is_legacy is False:
  716. self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
  717. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
  718. else:
  719. self.app.plotcanvas.graph_event_disconnect(self.kp)
  720. self.app.plotcanvas.graph_event_disconnect(self.mr)
  721. self.app.kp = self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent)
  722. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot)
  723. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  724. self.app.on_mouse_click_release_over_plot)
  725. if self.ui.big_cursor_cb.get_value():
  726. # restore cursor
  727. self.app.on_cursor_type(val=self.old_cursor_type)
  728. # restore selection
  729. self.app.defaults['global_selection_shape'] = self.old_selection_state
  730. # Grid toggle
  731. if key == QtCore.Qt.Key_G or key == 'G':
  732. self.app.ui.grid_snap_btn.trigger()
  733. # Jump to coords
  734. if key == QtCore.Qt.Key_J or key == 'J':
  735. l_x, l_y = self.app.on_jump_to()
  736. def on_autolevelling(self, state):
  737. self.ui.al_frame.show() if state else self.ui.al_frame.hide()
  738. self.app.defaults["cncjob_al_status"] = True if state else False
  739. def on_show_al_table(self, state):
  740. self.ui.al_probe_points_table.show() if state else self.ui.al_probe_points_table.hide()
  741. def on_mode_radio(self, val):
  742. # reset al table
  743. self.ui.al_probe_points_table.setRowCount(0)
  744. # reset the al dict
  745. self.al_geometry_dict.clear()
  746. # build AL table
  747. self.build_al_table()
  748. if val == "manual":
  749. self.ui.al_rows_entry.setDisabled(True)
  750. self.ui.al_rows_label.setDisabled(True)
  751. self.ui.al_columns_entry.setDisabled(True)
  752. self.ui.al_columns_label.setDisabled(True)
  753. else:
  754. self.ui.al_rows_entry.setDisabled(False)
  755. self.ui.al_rows_label.setDisabled(False)
  756. self.ui.al_columns_entry.setDisabled(False)
  757. self.ui.al_columns_label.setDisabled(False)
  758. def on_controller_change(self):
  759. if self.ui.al_controller_combo.get_value() == 'GRBL':
  760. self.ui.h_gcode_button.hide()
  761. self.ui.view_h_gcode_button.hide()
  762. self.ui.import_heights_button.hide()
  763. self.ui.grbl_frame.show()
  764. self.on_search_ports(muted=True)
  765. else:
  766. self.ui.h_gcode_button.show()
  767. self.ui.view_h_gcode_button.show()
  768. self.ui.import_heights_button.show()
  769. self.ui.grbl_frame.hide()
  770. def list_serial_ports(self):
  771. """
  772. Lists serial port names.
  773. From here: https://stackoverflow.com/questions/12090503/listing-available-com-ports-with-python
  774. :raises EnvironmentError: On unsupported or unknown platforms
  775. :returns: A list of the serial ports available on the system
  776. """
  777. if sys.platform.startswith('win'):
  778. ports = ['COM%s' % (i + 1) for i in range(256)]
  779. elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'):
  780. # this excludes your current terminal "/dev/tty"
  781. ports = glob.glob('/dev/tty[A-Za-z]*')
  782. elif sys.platform.startswith('darwin'):
  783. ports = glob.glob('/dev/tty.*')
  784. else:
  785. raise EnvironmentError('Unsupported platform')
  786. result = []
  787. s = serial.Serial()
  788. for port in ports:
  789. s.port = port
  790. try:
  791. s.open()
  792. s.close()
  793. result.append(port)
  794. except (OSError, serial.SerialException):
  795. # result.append(port + " (in use)")
  796. pass
  797. return result
  798. def on_search_ports(self, muted=None):
  799. port_list = self.list_serial_ports()
  800. self.ui.com_list_combo.clear()
  801. self.ui.com_list_combo.addItems(port_list)
  802. if muted is not True:
  803. self.app.inform.emit('[WARNING_NOTCL] %s' % _("COM list updated ..."))
  804. def on_connect_grbl(self):
  805. port_name = self.ui.com_list_combo.currentText()
  806. if " (" in port_name:
  807. port_name = port_name.rpartition(" (")[0]
  808. baudrate = int(self.ui.baudrates_list_combo.currentText())
  809. try:
  810. self.grbl_ser_port = serial.serial_for_url(port_name, baudrate,
  811. bytesize=serial.EIGHTBITS,
  812. parity=serial.PARITY_NONE,
  813. stopbits=serial.STOPBITS_ONE,
  814. timeout=0.1,
  815. xonxoff=False,
  816. rtscts=False)
  817. # Toggle DTR to reset the controller loaded with GRBL (Arduino, ESP32, etc)
  818. try:
  819. self.grbl_ser_port.dtr = False
  820. except IOError:
  821. pass
  822. self.grbl_ser_port.reset_input_buffer()
  823. try:
  824. self.grbl_ser_port.dtr = True
  825. except IOError:
  826. pass
  827. answer = self.wake_grbl()
  828. answer = ['ok'] # hack for development without a GRBL controller connected
  829. for line in answer:
  830. if 'ok' in line.lower():
  831. self.ui.com_connect_button.setStyleSheet("QPushButton {background-color: seagreen;}")
  832. self.ui.com_connect_button.setText(_("Connected"))
  833. self.ui.controller_reset_button.setDisabled(False)
  834. for idx in range(self.ui.al_toolbar.count()):
  835. if self.ui.al_toolbar.tabText(idx) == _("Connect"):
  836. self.ui.al_toolbar.tabBar.setTabTextColor(idx, QtGui.QColor('seagreen'))
  837. if self.ui.al_toolbar.tabText(idx) == _("Control"):
  838. self.ui.al_toolbar.tabBar.setTabEnabled(idx, True)
  839. if self.ui.al_toolbar.tabText(idx) == _("Sender"):
  840. self.ui.al_toolbar.tabBar.setTabEnabled(idx, True)
  841. self.app.inform.emit("%s: %s" % (_("Port connected"), port_name))
  842. return
  843. self.grbl_ser_port.close()
  844. self.app.inform.emit("[ERROR_NOTCL] %s: %s" % (_("Could not connect to GRBL on port"), port_name))
  845. except serial.SerialException:
  846. self.grbl_ser_port = serial.Serial()
  847. self.grbl_ser_port.port = port_name
  848. self.grbl_ser_port.close()
  849. self.ui.com_connect_button.setStyleSheet("QPushButton {background-color: red;}")
  850. self.ui.com_connect_button.setText(_("Disconnected"))
  851. self.ui.controller_reset_button.setDisabled(True)
  852. for idx in range(self.ui.al_toolbar.count()):
  853. if self.ui.al_toolbar.tabText(idx) == _("Connect"):
  854. self.ui.al_toolbar.tabBar.setTabTextColor(idx, QtGui.QColor('red'))
  855. if self.ui.al_toolbar.tabText(idx) == _("Control"):
  856. self.ui.al_toolbar.tabBar.setTabEnabled(idx, False)
  857. if self.ui.al_toolbar.tabText(idx) == _("Sender"):
  858. self.ui.al_toolbar.tabBar.setTabEnabled(idx, False)
  859. self.app.inform.emit("%s: %s" % (_("Port is connected. Disconnecting"), port_name))
  860. except Exception:
  861. self.app.inform.emit("[ERROR_NOTCL] %s: %s" % (_("Could not connect to port"), port_name))
  862. def on_add_baudrate_grbl(self):
  863. new_bd = str(self.ui.new_baudrate_entry.get_value())
  864. if int(new_bd) >= 40 and new_bd not in self.ui.baudrates_list_combo.model().stringList():
  865. self.ui.baudrates_list_combo.addItem(new_bd)
  866. self.ui.baudrates_list_combo.setCurrentText(new_bd)
  867. def on_delete_baudrate_grbl(self):
  868. current_idx = self.ui.baudrates_list_combo.currentIndex()
  869. self.ui.baudrates_list_combo.removeItem(current_idx)
  870. def wake_grbl(self):
  871. # Wake up grbl
  872. self.grbl_ser_port.write("\r\n\r\n".encode('utf-8'))
  873. # Wait for GRBL controller to initialize
  874. time.sleep(1)
  875. grbl_out = deepcopy(self.grbl_ser_port.readlines())
  876. self.grbl_ser_port.reset_input_buffer()
  877. return grbl_out
  878. def on_send_grbl_command(self):
  879. cmd = self.ui.grbl_command_entry.get_value()
  880. # show the Shell Dock
  881. self.app.ui.shell_dock.show()
  882. def worker_task():
  883. with self.app.proc_container.new(_("Sending GCode...")):
  884. self.send_grbl_command(command=cmd)
  885. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  886. def send_grbl_command(self, command, echo=True):
  887. stripped_cmd = command.strip()
  888. cmd = stripped_cmd.rpartition('\n')[0]
  889. if echo:
  890. self.app.inform_shell[str, bool].emit(cmd, False)
  891. # Send Gcode command to GRBL
  892. snd = cmd + '\n'
  893. self.grbl_ser_port.write(snd.encode('utf-8'))
  894. grbl_out = self.grbl_ser_port.readlines()
  895. result = False
  896. for line in grbl_out:
  897. if echo:
  898. try:
  899. self.app.inform_shell.emit(' : ' + line.decode('utf-8').strip().upper())
  900. except Exception as e:
  901. log.debug("CNCJobObject.send_grbl_command() --> %s" % str(e))
  902. if 'ok' in line:
  903. result = True
  904. return result
  905. def send_grbl_block(self, command, echo=True):
  906. stripped_cmd = command.strip()
  907. for l in stripped_cmd.split('\n'):
  908. if echo:
  909. self.app.inform_shell[str, bool].emit(l, False)
  910. # Send Gcode block to GRBL
  911. snd = l + '\n'
  912. self.grbl_ser_port.write(snd.encode('utf-8'))
  913. grbl_out = self.grbl_ser_port.readlines()
  914. for line in grbl_out:
  915. if echo:
  916. try:
  917. self.app.inform_shell.emit(' : ' + line.decode('utf-8').strip().upper())
  918. except Exception as e:
  919. log.debug("CNCJobObject.send_grbl_block() --> %s" % str(e))
  920. def get_grbl_parameter(self, param):
  921. if '$' in param:
  922. param = param.replace('$','')
  923. snd = '$$\n'
  924. self.grbl_ser_port.write(snd.encode('utf-8'))
  925. grbl_out = self.grbl_ser_port.readlines()
  926. for line in grbl_out:
  927. decoded_line = line.decode('utf-8')
  928. par = '$%s' % str(param)
  929. if par in decoded_line:
  930. result = float(decoded_line.rpartition('=')[2])
  931. self.app.shell_message("GRBL Parameter: %s = %s" % (str(param), str(result)), show=True)
  932. return result
  933. def on_jog(self, direction=None):
  934. if direction is None:
  935. return
  936. cmd = ''
  937. step = self.ui.jog_step_entry.get_value(),
  938. feedrate = self.ui.jog_fr_entry.get_value()
  939. travelz = float(self.app.defaults["cncjob_al_grbl_travelz"])
  940. if direction == 'xplus':
  941. cmd = "$J=G91 %s X%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(step), str(feedrate))
  942. if direction == 'xminus':
  943. cmd = "$J=G91 %s X-%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(step), str(feedrate))
  944. if direction == 'yplus':
  945. cmd = "$J=G91 %s Y%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(step), str(feedrate))
  946. if direction == 'yminus':
  947. cmd = "$J=G91 %s Y-%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(step), str(feedrate))
  948. if direction == 'zplus':
  949. cmd = "$J=G91 %s Z%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(step), str(feedrate))
  950. if direction == 'zminus':
  951. cmd = "$J=G91 %s Z-%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(step), str(feedrate))
  952. if direction == 'origin':
  953. cmd = "$J=G90 %s Z%s F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(travelz), str(feedrate))
  954. self.send_grbl_command(command=cmd, echo=False)
  955. cmd = "$J=G90 %s X0.0 Y0.0 F%s" % ({'IN': 'G20', 'MM': 'G21'}[self.units], str(feedrate))
  956. self.send_grbl_command(command=cmd, echo=False)
  957. return
  958. self.send_grbl_command(command=cmd, echo=False)
  959. def on_grbl_zero(self, axis):
  960. current_mode = self.get_grbl_parameter('10')
  961. if current_mode is None:
  962. return
  963. cmd = '$10=0'
  964. self.send_grbl_command(command=cmd, echo=False)
  965. if axis == 'x':
  966. cmd = 'G10 L2 P1 X0'
  967. elif axis == 'y':
  968. cmd = 'G10 L2 P1 Y0'
  969. elif axis == 'z':
  970. cmd = 'G10 L2 P1 Z0'
  971. else:
  972. # all
  973. cmd = 'G10 L2 P1 X0 Y0 Z0'
  974. self.send_grbl_command(command=cmd, echo=False)
  975. # restore previous mode
  976. cmd = '$10=%d' % int(current_mode)
  977. self.send_grbl_command(command=cmd, echo=False)
  978. def on_grbl_homing(self):
  979. cmd = '$H'
  980. self.app.inform.emit("%s" % _("GRBL is doing a home cycle."))
  981. self.wake_grbl()
  982. self.send_grbl_command(command=cmd)
  983. def on_grbl_reset(self):
  984. cmd = '\x18'
  985. self.app.inform.emit("%s" % _("GRBL software reset was sent."))
  986. self.wake_grbl()
  987. self.send_grbl_command(command=cmd)
  988. def on_grbl_pause_resume(self, checked):
  989. if checked is False:
  990. cmd = '~'
  991. self.send_grbl_command(command=cmd)
  992. self.app.inform.emit("%s" % _("GRBL resumed."))
  993. else:
  994. cmd = '!'
  995. self.send_grbl_command(command=cmd)
  996. self.app.inform.emit("%s" % _("GRBL paused."))
  997. def probing_gcode(self, coords, pr_travel, probe_fr, pr_depth, controller):
  998. """
  999. :param coords: a list of (x, y) tuples of probe points coordinates
  1000. :type coords: list
  1001. :param pr_travel: the height (z) where the probe travel between probe points
  1002. :type pr_travel: float
  1003. :param probe_fr: feedrate when probing
  1004. :type probe_fr: float
  1005. :param pr_depth: how much to lower the probe searching for contact
  1006. :type pr_depth: float
  1007. :param controller: a string with the name of the GCode sender for which to create the probing GCode.
  1008. Can be: 'MACH3', 'MACH4', 'LinuxCNC', 'GRBL'
  1009. :type controller: str
  1010. :return: Probing GCode
  1011. :rtype: str
  1012. """
  1013. p_gcode = ''
  1014. header = ''
  1015. time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
  1016. header += '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \
  1017. (str(self.app.version), str(self.app.version_date)) + '\n'
  1018. header += '(This is a autolevelling probing GCode.)\n' \
  1019. '(Make sure that before you start the job you first do a zero for all axis.)\n\n'
  1020. header += '(Name: ' + str(self.options['name']) + ')\n'
  1021. header += '(Type: ' + "Autolevelling Probing GCode " + ')\n'
  1022. header += '(Units: ' + self.units.upper() + ')\n'
  1023. header += '(Created on ' + time_str + ')\n'
  1024. # commands
  1025. if controller == 'MACH3':
  1026. probing_command = 'G31'
  1027. # probing_var = '#2002'
  1028. openfile_command = 'M40'
  1029. closefile_command = 'M41'
  1030. elif controller == 'MACH4':
  1031. probing_command = 'G31'
  1032. # probing_var = '#5063'
  1033. openfile_command = 'M40'
  1034. closefile_command = 'M41'
  1035. elif controller == 'LinuxCNC':
  1036. probing_command = 'G38.2'
  1037. # probing_var = '#5422'
  1038. openfile_command = '(PROBEOPEN a_probing_points_file.txt)'
  1039. closefile_command = '(PROBECLOSE)'
  1040. else:
  1041. log.debug("CNCJobObject.probing_gcode() -> controller not supported")
  1042. return
  1043. # #############################################################################################################
  1044. # ########################### GCODE construction ##############################################################
  1045. # #############################################################################################################
  1046. # header
  1047. p_gcode += header + '\n'
  1048. # supplementary message for LinuxCNC
  1049. if controller == 'LinuxCNC':
  1050. p_gcode += "The file with the stored probing points can be found\n" \
  1051. "in the configuration folder for LinuxCNC.\n" \
  1052. "The name of the file is: a_probing_points_file.txt.\n"
  1053. # units
  1054. p_gcode += 'G21\n' if self.units == 'MM' else 'G20\n'
  1055. # reference mode = absolute
  1056. p_gcode += 'G90\n'
  1057. # open a new file
  1058. p_gcode += openfile_command + '\n'
  1059. # move to safe height (probe travel Z)
  1060. p_gcode += 'G0 Z%s\n' % str(self.app.dec_format(pr_travel, self.coords_decimals))
  1061. # probing points
  1062. for idx, xy_tuple in enumerate(coords, 1): # index starts from 1
  1063. x = xy_tuple[0]
  1064. y = xy_tuple[1]
  1065. # move to probing point
  1066. p_gcode += "G0 X%sY%s\n" % (
  1067. str(self.app.dec_format(x, self.coords_decimals)),
  1068. str(self.app.dec_format(y, self.coords_decimals))
  1069. )
  1070. # do the probing
  1071. p_gcode += "%s Z%s F%s\n" % (
  1072. probing_command,
  1073. str(self.app.dec_format(pr_depth, self.coords_decimals)),
  1074. str(self.app.dec_format(probe_fr, self.fr_decimals)),
  1075. )
  1076. # store in a global numeric variable the value of the detected probe Z
  1077. # I offset the global numeric variable by 500 so it does not conflict with something else
  1078. # temp_var = int(idx + 500)
  1079. # p_gcode += "#%d = %s\n" % (temp_var, probing_var)
  1080. # move to safe height (probe travel Z)
  1081. p_gcode += 'G0 Z%s\n' % str(self.app.dec_format(pr_travel, self.coords_decimals))
  1082. # close the file
  1083. p_gcode += closefile_command + '\n'
  1084. # finish the GCode
  1085. p_gcode += 'M2'
  1086. return p_gcode
  1087. def on_generate_probing_gcode(self):
  1088. coords = []
  1089. for id_key, value in self.al_geometry_dict.items():
  1090. x = value['point'].x
  1091. y = value['point'].y
  1092. coords.append(
  1093. (
  1094. self.app.dec_format(x, dec=self.app.decimals),
  1095. self.app.dec_format(y, dec=self.app.decimals)
  1096. )
  1097. )
  1098. pr_travel = self.ui.ptravelz_entry.get_value()
  1099. probe_fr = self.ui.feedrate_probe_entry.get_value()
  1100. pr_depth = self.ui.pdepth_entry.get_value()
  1101. controller = self.ui.al_controller_combo.get_value()
  1102. self.probing_gcode_text = self.probing_gcode(coords, pr_travel, probe_fr, pr_depth, controller)
  1103. lines = StringIO(self.probing_gcode_text)
  1104. _filter_ = self.app.defaults['cncjob_save_filters']
  1105. name = "probing_gcode"
  1106. try:
  1107. dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name)
  1108. filename, _f = FCFileSaveDialog.get_saved_filename(
  1109. caption=_("Export Code ..."),
  1110. directory=dir_file_to_save,
  1111. ext_filter=_filter_
  1112. )
  1113. except TypeError:
  1114. filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export Code ..."), ext_filter=_filter_)
  1115. if filename == '':
  1116. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export cancelled ..."))
  1117. return
  1118. else:
  1119. try:
  1120. force_windows_line_endings = self.app.defaults['cncjob_line_ending']
  1121. if force_windows_line_endings and sys.platform != 'win32':
  1122. with open(filename, 'w', newline='\r\n') as f:
  1123. for line in lines:
  1124. f.write(line)
  1125. else:
  1126. with open(filename, 'w') as f:
  1127. for line in lines:
  1128. f.write(line)
  1129. except FileNotFoundError:
  1130. self.app.inform.emit('[WARNING_NOTCL] %s' % _("No such file or directory"))
  1131. return
  1132. except PermissionError:
  1133. self.app.inform.emit(
  1134. '[WARNING] %s' % _("Permission denied, saving not possible.\n"
  1135. "Most likely another app is holding the file open and not accessible.")
  1136. )
  1137. return 'fail'
  1138. def on_view_probing_gcode(self):
  1139. self.app.proc_container.view.set_busy(_("Loading..."))
  1140. gco = self.probing_gcode_text
  1141. if gco is None or gco == '':
  1142. self.app.inform.emit('[WARNING_NOTCL] %s...' % _('There is nothing to view'))
  1143. return
  1144. self.gcode_viewer_tab = AppTextEditor(app=self.app, plain_text=True)
  1145. # add the tab if it was closed
  1146. self.app.ui.plot_tab_area.addTab(self.gcode_viewer_tab, '%s' % _("Code Viewer"))
  1147. self.gcode_viewer_tab.setObjectName('code_viewer_tab')
  1148. # delete the absolute and relative position and messages in the infobar
  1149. self.app.ui.position_label.setText("")
  1150. self.app.ui.rel_position_label.setText("")
  1151. self.gcode_viewer_tab.code_editor.completer_enable = False
  1152. self.gcode_viewer_tab.buttonRun.hide()
  1153. # Switch plot_area to CNCJob tab
  1154. self.app.ui.plot_tab_area.setCurrentWidget(self.gcode_viewer_tab)
  1155. self.gcode_viewer_tab.t_frame.hide()
  1156. # then append the text from GCode to the text editor
  1157. try:
  1158. self.gcode_viewer_tab.load_text(gco, move_to_start=True, clear_text=True)
  1159. except Exception as e:
  1160. log.debug('FlatCAMCNCJob.on_edit_code_click() -->%s' % str(e))
  1161. return
  1162. self.gcode_viewer_tab.t_frame.show()
  1163. self.app.proc_container.view.set_idle()
  1164. self.gcode_viewer_tab.buttonSave.hide()
  1165. self.gcode_viewer_tab.buttonOpen.hide()
  1166. self.gcode_viewer_tab.buttonPrint.hide()
  1167. self.gcode_viewer_tab.buttonPreview.hide()
  1168. self.gcode_viewer_tab.buttonReplace.hide()
  1169. self.gcode_viewer_tab.sel_all_cb.hide()
  1170. self.gcode_viewer_tab.entryReplace.hide()
  1171. self.gcode_viewer_tab.code_editor.setReadOnly(True)
  1172. self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Viewer'))
  1173. def on_import_height_map(self):
  1174. """
  1175. Import the height map file into the app
  1176. :return:
  1177. :rtype:
  1178. """
  1179. _filter_ = "Text File .txt (*.txt);;All Files (*.*)"
  1180. try:
  1181. filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import Height Map"),
  1182. directory=self.app.get_last_folder(),
  1183. filter=_filter_)
  1184. except TypeError:
  1185. filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import Height Map"),
  1186. filter=_filter_)
  1187. filename = str(filename)
  1188. if filename == '':
  1189. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
  1190. else:
  1191. self.app.worker_task.emit({'fcn': self.import_height_map, 'params': [filename]})
  1192. def import_height_map(self, filename):
  1193. """
  1194. :param filename:
  1195. :type filename:
  1196. :return:
  1197. :rtype:
  1198. """
  1199. stream = ''
  1200. try:
  1201. if filename:
  1202. with open(filename, 'r') as f:
  1203. stream = f.readlines()
  1204. else:
  1205. return
  1206. except IOError:
  1207. log.error("Failed to open height map file: %s" % filename)
  1208. self.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open height map file"), filename))
  1209. return
  1210. idx = 0
  1211. for line in stream:
  1212. if line != '':
  1213. idx += 1
  1214. line = line.replace(' ', ',').replace('\n', '').split(',')
  1215. if idx not in self.al_geometry_dict:
  1216. self.al_geometry_dict[idx] = {}
  1217. self.al_geometry_dict[idx]['height'] = float(line[2])
  1218. if 'point' not in self.al_geometry_dict[idx]:
  1219. x = float(line[0])
  1220. y = float(line[1])
  1221. self.al_geometry_dict[idx]['point'] = Point((x, y))
  1222. self.build_al_table_sig.emit()
  1223. def on_updateplot_button_click(self, *args):
  1224. """
  1225. Callback for the "Updata Plot" button. Reads the form for updates
  1226. and plots the object.
  1227. """
  1228. self.read_form()
  1229. self.on_plot_kind_change()
  1230. def on_plot_kind_change(self):
  1231. kind = self.ui.cncplot_method_combo.get_value()
  1232. def worker_task():
  1233. with self.app.proc_container.new(_("Plotting...")):
  1234. self.plot(kind=kind)
  1235. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  1236. def on_exportgcode_button_click(self):
  1237. """
  1238. Handler activated by a button clicked when exporting GCode.
  1239. :param args:
  1240. :return:
  1241. """
  1242. self.app.defaults.report_usage("cncjob_on_exportgcode_button")
  1243. self.read_form()
  1244. name = self.app.collection.get_active().options['name']
  1245. save_gcode = False
  1246. if 'Roland' in self.pp_excellon_name or 'Roland' in self.pp_geometry_name:
  1247. _filter_ = "RML1 Files .rol (*.rol);;All Files (*.*)"
  1248. elif 'hpgl' in self.pp_geometry_name:
  1249. _filter_ = "HPGL Files .plt (*.plt);;All Files (*.*)"
  1250. else:
  1251. save_gcode = True
  1252. _filter_ = self.app.defaults['cncjob_save_filters']
  1253. try:
  1254. dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name)
  1255. filename, _f = FCFileSaveDialog.get_saved_filename(
  1256. caption=_("Export Code ..."),
  1257. directory=dir_file_to_save,
  1258. ext_filter=_filter_
  1259. )
  1260. except TypeError:
  1261. filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export Code ..."), ext_filter=_filter_)
  1262. self.export_gcode_handler(filename, is_gcode=save_gcode)
  1263. def export_gcode_handler(self, filename, is_gcode=True):
  1264. preamble = ''
  1265. postamble = ''
  1266. filename = str(filename)
  1267. if filename == '':
  1268. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export cancelled ..."))
  1269. return
  1270. else:
  1271. if is_gcode is True:
  1272. used_extension = filename.rpartition('.')[2]
  1273. self.update_filters(last_ext=used_extension, filter_string='cncjob_save_filters')
  1274. new_name = os.path.split(str(filename))[1].rpartition('.')[0]
  1275. self.ui.name_entry.set_value(new_name)
  1276. self.on_name_activate(silent=True)
  1277. try:
  1278. if self.ui.snippets_cb.get_value():
  1279. preamble = self.append_snippet
  1280. postamble = self.prepend_snippet
  1281. gc = self.export_gcode(filename, preamble=preamble, postamble=postamble)
  1282. except Exception as err:
  1283. log.debug("CNCJobObject.export_gcode_handler() --> %s" % str(err))
  1284. gc = self.export_gcode(filename)
  1285. if gc == 'fail':
  1286. return
  1287. if self.app.defaults["global_open_style"] is False:
  1288. self.app.file_opened.emit("gcode", filename)
  1289. self.app.file_saved.emit("gcode", filename)
  1290. self.app.inform.emit('[success] %s: %s' % (_("File saved to"), filename))
  1291. def on_edit_code_click(self, *args):
  1292. """
  1293. Handler activated by a button clicked when reviewing GCode.
  1294. :param args:
  1295. :return:
  1296. """
  1297. self.app.proc_container.view.set_busy(_("Loading..."))
  1298. preamble = self.append_snippet
  1299. postamble = self.prepend_snippet
  1300. gco = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True)
  1301. if gco == 'fail':
  1302. return
  1303. else:
  1304. self.app.gcode_edited = gco
  1305. self.gcode_editor_tab = AppTextEditor(app=self.app, plain_text=True)
  1306. # add the tab if it was closed
  1307. self.app.ui.plot_tab_area.addTab(self.gcode_editor_tab, '%s' % _("Code Review"))
  1308. self.gcode_editor_tab.setObjectName('code_editor_tab')
  1309. # delete the absolute and relative position and messages in the infobar
  1310. self.app.ui.position_label.setText("")
  1311. self.app.ui.rel_position_label.setText("")
  1312. self.gcode_editor_tab.code_editor.completer_enable = False
  1313. self.gcode_editor_tab.buttonRun.hide()
  1314. # Switch plot_area to CNCJob tab
  1315. self.app.ui.plot_tab_area.setCurrentWidget(self.gcode_editor_tab)
  1316. self.gcode_editor_tab.t_frame.hide()
  1317. # then append the text from GCode to the text editor
  1318. try:
  1319. self.gcode_editor_tab.load_text(self.app.gcode_edited.getvalue(), move_to_start=True, clear_text=True)
  1320. except Exception as e:
  1321. log.debug('FlatCAMCNCJob.on_edit_code_click() -->%s' % str(e))
  1322. return
  1323. self.gcode_editor_tab.t_frame.show()
  1324. self.app.proc_container.view.set_idle()
  1325. self.gcode_editor_tab.buttonSave.hide()
  1326. self.gcode_editor_tab.buttonOpen.hide()
  1327. self.gcode_editor_tab.buttonPrint.hide()
  1328. self.gcode_editor_tab.buttonPreview.hide()
  1329. self.gcode_editor_tab.buttonReplace.hide()
  1330. self.gcode_editor_tab.sel_all_cb.hide()
  1331. self.gcode_editor_tab.entryReplace.hide()
  1332. self.gcode_editor_tab.code_editor.setReadOnly(True)
  1333. self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Editor'))
  1334. def on_update_source_file(self):
  1335. self.source_file = self.gcode_editor_tab.code_editor.toPlainText()
  1336. def gcode_header(self, comment_start_symbol=None, comment_stop_symbol=None):
  1337. """
  1338. Will create a header to be added to all GCode files generated by FlatCAM
  1339. :param comment_start_symbol: A symbol to be used as the first symbol in a comment
  1340. :param comment_stop_symbol: A symbol to be used as the last symbol in a comment
  1341. :return: A string with a GCode header
  1342. """
  1343. log.debug("FlatCAMCNCJob.gcode_header()")
  1344. time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
  1345. marlin = False
  1346. hpgl = False
  1347. probe_pp = False
  1348. gcode = ''
  1349. start_comment = comment_start_symbol if comment_start_symbol is not None else '('
  1350. stop_comment = comment_stop_symbol if comment_stop_symbol is not None else ')'
  1351. try:
  1352. for key in self.cnc_tools:
  1353. ppg = self.cnc_tools[key]['data']['ppname_g']
  1354. if 'marlin' in ppg.lower() or 'repetier' in ppg.lower():
  1355. marlin = True
  1356. break
  1357. if ppg == 'hpgl':
  1358. hpgl = True
  1359. break
  1360. if "toolchange_probe" in ppg.lower():
  1361. probe_pp = True
  1362. break
  1363. except KeyError:
  1364. # log.debug("FlatCAMCNCJob.gcode_header() error: --> %s" % str(e))
  1365. pass
  1366. try:
  1367. if 'marlin' in self.options['ppname_e'].lower() or 'repetier' in self.options['ppname_e'].lower():
  1368. marlin = True
  1369. except KeyError:
  1370. # log.debug("FlatCAMCNCJob.gcode_header(): --> There is no such self.option: %s" % str(e))
  1371. pass
  1372. try:
  1373. if "toolchange_probe" in self.options['ppname_e'].lower():
  1374. probe_pp = True
  1375. except KeyError:
  1376. # log.debug("FlatCAMCNCJob.gcode_header(): --> There is no such self.option: %s" % str(e))
  1377. pass
  1378. if marlin is True:
  1379. gcode += ';Marlin(Repetier) G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s\n' % \
  1380. (str(self.app.version), str(self.app.version_date)) + '\n'
  1381. gcode += ';Name: ' + str(self.options['name']) + '\n'
  1382. gcode += ';Type: ' + "G-code from " + str(self.options['type']) + '\n'
  1383. # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
  1384. # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
  1385. gcode += ';Units: ' + self.units.upper() + '\n' + "\n"
  1386. gcode += ';Created on ' + time_str + '\n' + '\n'
  1387. elif hpgl is True:
  1388. gcode += 'CO "HPGL CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s' % \
  1389. (str(self.app.version), str(self.app.version_date)) + '";\n'
  1390. gcode += 'CO "Name: ' + str(self.options['name']) + '";\n'
  1391. gcode += 'CO "Type: ' + "HPGL code from " + str(self.options['type']) + '";\n'
  1392. # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
  1393. # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
  1394. gcode += 'CO "Units: ' + self.units.upper() + '";\n'
  1395. gcode += 'CO "Created on ' + time_str + '";\n'
  1396. elif probe_pp is True:
  1397. gcode += '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \
  1398. (str(self.app.version), str(self.app.version_date)) + '\n'
  1399. gcode += '(This GCode tool change is done by using a Probe.)\n' \
  1400. '(Make sure that before you start the job you first do a rough zero for Z axis.)\n' \
  1401. '(This means that you need to zero the CNC axis and then jog to the toolchange X, Y location,)\n' \
  1402. '(mount the probe and adjust the Z so more or less the probe tip touch the plate. ' \
  1403. 'Then zero the Z axis.)\n' + '\n'
  1404. gcode += '(Name: ' + str(self.options['name']) + ')\n'
  1405. gcode += '(Type: ' + "G-code from " + str(self.options['type']) + ')\n'
  1406. # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
  1407. # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
  1408. gcode += '(Units: ' + self.units.upper() + ')\n' + "\n"
  1409. gcode += '(Created on ' + time_str + ')\n' + '\n'
  1410. else:
  1411. gcode += '%sG-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s%s\n' % \
  1412. (start_comment, str(self.app.version), str(self.app.version_date), stop_comment) + '\n'
  1413. gcode += '%sName: ' % start_comment + str(self.options['name']) + '%s\n' % stop_comment
  1414. gcode += '%sType: ' % start_comment + "G-code from " + str(self.options['type']) + '%s\n' % stop_comment
  1415. # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
  1416. # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
  1417. gcode += '%sUnits: ' % start_comment + self.units.upper() + '%s\n' % stop_comment + "\n"
  1418. gcode += '%sCreated on ' % start_comment + time_str + '%s\n' % stop_comment + '\n'
  1419. return gcode
  1420. @staticmethod
  1421. def gcode_footer(end_command=None):
  1422. """
  1423. Will add the M02 to the end of GCode, if requested.
  1424. :param end_command: 'M02' or 'M30' - String
  1425. :return:
  1426. """
  1427. if end_command:
  1428. return end_command
  1429. else:
  1430. return 'M02'
  1431. def export_gcode(self, filename=None, preamble='', postamble='', to_file=False, from_tcl=False):
  1432. """
  1433. This will save the GCode from the Gcode object to a file on the OS filesystem
  1434. :param filename: filename for the GCode file
  1435. :param preamble: a custom Gcode block to be added at the beginning of the Gcode file
  1436. :param postamble: a custom Gcode block to be added at the end of the Gcode file
  1437. :param to_file: if False then no actual file is saved but the app will know that a file was created
  1438. :param from_tcl: True if run from Tcl Shell
  1439. :return: None
  1440. """
  1441. # gcode = ''
  1442. # roland = False
  1443. # hpgl = False
  1444. # isel_icp = False
  1445. include_header = True
  1446. if preamble == '':
  1447. preamble = self.app.defaults["cncjob_prepend"]
  1448. if postamble == '':
  1449. preamble = self.app.defaults["cncjob_append"]
  1450. try:
  1451. if self.special_group:
  1452. self.app.inform.emit('[WARNING_NOTCL] %s %s %s.' %
  1453. (_("This CNCJob object can't be processed because it is a"),
  1454. str(self.special_group),
  1455. _("CNCJob object")))
  1456. return 'fail'
  1457. except AttributeError:
  1458. pass
  1459. # if this dict is not empty then the object is a Geometry object
  1460. if self.cnc_tools:
  1461. first_key = next(iter(self.cnc_tools))
  1462. include_header = self.app.preprocessors[self.cnc_tools[first_key]['data']['ppname_g']].include_header
  1463. # if this dict is not empty then the object is an Excellon object
  1464. if self.exc_cnc_tools:
  1465. first_key = next(iter(self.exc_cnc_tools))
  1466. include_header = self.app.preprocessors[
  1467. self.exc_cnc_tools[first_key]['data']['tools_drill_ppname_e']
  1468. ].include_header
  1469. gcode = ''
  1470. if include_header is False:
  1471. g = preamble
  1472. # detect if using multi-tool and make the Gcode summation correctly for each case
  1473. if self.multitool is True:
  1474. for tooluid_key in self.cnc_tools:
  1475. for key, value in self.cnc_tools[tooluid_key].items():
  1476. if key == 'gcode':
  1477. gcode += value
  1478. break
  1479. else:
  1480. gcode += self.gcode
  1481. g = g + gcode + postamble
  1482. else:
  1483. # search for the GCode beginning which is usually a G20 or G21
  1484. # fix so the preamble gets inserted in between the comments header and the actual start of GCODE
  1485. # g_idx = gcode.rfind('G20')
  1486. #
  1487. # # if it did not find 'G20' then search for 'G21'
  1488. # if g_idx == -1:
  1489. # g_idx = gcode.rfind('G21')
  1490. #
  1491. # # if it did not find 'G20' and it did not find 'G21' then there is an error and return
  1492. # if g_idx == -1:
  1493. # self.app.inform.emit('[ERROR_NOTCL] %s' % _("G-code does not have a units code: either G20 or G21"))
  1494. # return
  1495. # detect if using multi-tool and make the Gcode summation correctly for each case
  1496. if self.multitool is True:
  1497. if self.origin_kind == 'excellon':
  1498. for tooluid_key in self.exc_cnc_tools:
  1499. for key, value in self.exc_cnc_tools[tooluid_key].items():
  1500. if key == 'gcode' and value:
  1501. gcode += value
  1502. break
  1503. else:
  1504. for tooluid_key in self.cnc_tools:
  1505. for key, value in self.cnc_tools[tooluid_key].items():
  1506. if key == 'gcode' and value:
  1507. gcode += value
  1508. break
  1509. else:
  1510. gcode += self.gcode
  1511. end_gcode = self.gcode_footer() if self.app.defaults['cncjob_footer'] is True else ''
  1512. # detect if using a HPGL preprocessor
  1513. hpgl = False
  1514. if self.cnc_tools:
  1515. for key in self.cnc_tools:
  1516. if 'ppname_g' in self.cnc_tools[key]['data']:
  1517. if 'hpgl' in self.cnc_tools[key]['data']['ppname_g']:
  1518. hpgl = True
  1519. break
  1520. elif self.exc_cnc_tools:
  1521. for key in self.cnc_tools:
  1522. if 'ppname_e' in self.cnc_tools[key]['data']:
  1523. if 'hpgl' in self.cnc_tools[key]['data']['ppname_e']:
  1524. hpgl = True
  1525. break
  1526. if hpgl:
  1527. processed_gcode = ''
  1528. pa_re = re.compile(r"^PA\s*(-?\d+\.\d*),?\s*(-?\d+\.\d*)*;?$")
  1529. for gline in gcode.splitlines():
  1530. match = pa_re.search(gline)
  1531. if match:
  1532. x_int = int(float(match.group(1)))
  1533. y_int = int(float(match.group(2)))
  1534. new_line = 'PA%d,%d;\n' % (x_int, y_int)
  1535. processed_gcode += new_line
  1536. else:
  1537. processed_gcode += gline + '\n'
  1538. gcode = processed_gcode
  1539. g = self.gc_header + '\n' + preamble + '\n' + gcode + postamble + end_gcode
  1540. else:
  1541. try:
  1542. g_idx = gcode.index('G94')
  1543. if preamble != '' and postamble != '':
  1544. g = self.gc_header + gcode[:g_idx + 3] + '\n' + preamble + '\n' + \
  1545. gcode[(g_idx + 3):] + postamble + end_gcode
  1546. elif preamble == '':
  1547. g = self.gc_header + gcode[:g_idx + 3] + '\n' + \
  1548. gcode[(g_idx + 3):] + postamble + end_gcode
  1549. elif postamble == '':
  1550. g = self.gc_header + gcode[:g_idx + 3] + '\n' + preamble + '\n' + \
  1551. gcode[(g_idx + 3):] + end_gcode
  1552. else:
  1553. g = self.gc_header + gcode[:g_idx + 3] + gcode[(g_idx + 3):] + end_gcode
  1554. except ValueError:
  1555. self.app.inform.emit('[ERROR_NOTCL] %s' %
  1556. _("G-code does not have a G94 code.\n"
  1557. "Append Code snippet will not be used.."))
  1558. g = self.gc_header + '\n' + gcode + postamble + end_gcode
  1559. # if toolchange custom is used, replace M6 code with the code from the Toolchange Custom Text box
  1560. # if self.ui.toolchange_cb.get_value() is True:
  1561. # # match = self.re_toolchange.search(g)
  1562. # if 'M6' in g:
  1563. # m6_code = self.parse_custom_toolchange_code(self.ui.toolchange_text.get_value())
  1564. # if m6_code is None or m6_code == '':
  1565. # self.app.inform.emit(
  1566. # '[ERROR_NOTCL] %s' % _("Cancelled. The Toolchange Custom code is enabled but it's empty.")
  1567. # )
  1568. # return 'fail'
  1569. #
  1570. # g = g.replace('M6', m6_code)
  1571. # self.app.inform.emit('[success] %s' % _("Toolchange G-code was replaced by a custom code."))
  1572. lines = StringIO(g)
  1573. # Write
  1574. if filename is not None:
  1575. try:
  1576. force_windows_line_endings = self.app.defaults['cncjob_line_ending']
  1577. if force_windows_line_endings and sys.platform != 'win32':
  1578. with open(filename, 'w', newline='\r\n') as f:
  1579. for line in lines:
  1580. f.write(line)
  1581. else:
  1582. with open(filename, 'w') as f:
  1583. for line in lines:
  1584. f.write(line)
  1585. except FileNotFoundError:
  1586. self.app.inform.emit('[WARNING_NOTCL] %s' % _("No such file or directory"))
  1587. return
  1588. except PermissionError:
  1589. self.app.inform.emit(
  1590. '[WARNING] %s' % _("Permission denied, saving not possible.\n"
  1591. "Most likely another app is holding the file open and not accessible.")
  1592. )
  1593. return 'fail'
  1594. elif to_file is False:
  1595. # Just for adding it to the recent files list.
  1596. if self.app.defaults["global_open_style"] is False:
  1597. self.app.file_opened.emit("cncjob", filename)
  1598. self.app.file_saved.emit("cncjob", filename)
  1599. self.app.inform.emit('[success] %s: %s' % (_("Saved to"), filename))
  1600. else:
  1601. return lines
  1602. # def on_toolchange_custom_clicked(self, signal):
  1603. # """
  1604. # Handler for clicking toolchange custom.
  1605. #
  1606. # :param signal:
  1607. # :return:
  1608. # """
  1609. #
  1610. # try:
  1611. # if 'toolchange_custom' not in str(self.options['ppname_e']).lower():
  1612. # if self.ui.toolchange_cb.get_value():
  1613. # self.ui.toolchange_cb.set_value(False)
  1614. # self.app.inform.emit('[WARNING_NOTCL] %s' %
  1615. # _("The used preprocessor file has to have in it's name: 'toolchange_custom'"))
  1616. # except KeyError:
  1617. # try:
  1618. # for key in self.cnc_tools:
  1619. # ppg = self.cnc_tools[key]['data']['ppname_g']
  1620. # if 'toolchange_custom' not in str(ppg).lower():
  1621. # if self.ui.toolchange_cb.get_value():
  1622. # self.ui.toolchange_cb.set_value(False)
  1623. # self.app.inform.emit('[WARNING_NOTCL] %s' %
  1624. # _("The used preprocessor file has to have in it's name: "
  1625. # "'toolchange_custom'"))
  1626. # except KeyError:
  1627. # self.app.inform.emit('[ERROR] %s' % _("There is no preprocessor file."))
  1628. def get_gcode(self, preamble='', postamble=''):
  1629. """
  1630. We need this to be able to get_gcode separately for shell command export_gcode
  1631. :param preamble: Extra GCode added to the beginning of the GCode
  1632. :param postamble: Extra GCode added at the end of the GCode
  1633. :return: The modified GCode
  1634. """
  1635. return preamble + '\n' + self.gcode + "\n" + postamble
  1636. def get_svg(self):
  1637. # we need this to be able get_svg separately for shell command export_svg
  1638. pass
  1639. def on_plot_cb_click(self, *args):
  1640. """
  1641. Handler for clicking on the Plot checkbox.
  1642. :param args:
  1643. :return:
  1644. """
  1645. if self.muted_ui:
  1646. return
  1647. kind = self.ui.cncplot_method_combo.get_value()
  1648. self.plot(kind=kind)
  1649. self.read_form_item('plot')
  1650. self.ui_disconnect()
  1651. cb_flag = self.ui.plot_cb.isChecked()
  1652. for row in range(self.ui.cnc_tools_table.rowCount()):
  1653. table_cb = self.ui.cnc_tools_table.cellWidget(row, 6)
  1654. if cb_flag:
  1655. table_cb.setChecked(True)
  1656. else:
  1657. table_cb.setChecked(False)
  1658. self.ui_connect()
  1659. def on_plot_cb_click_table(self):
  1660. """
  1661. Handler for clicking the plot checkboxes added into a Table on each row. Purpose: toggle visibility for the
  1662. tool/aperture found on that row.
  1663. :return:
  1664. """
  1665. # self.ui.cnc_tools_table.cellWidget(row, 2).widget().setCheckState(QtCore.Qt.Unchecked)
  1666. self.ui_disconnect()
  1667. # cw = self.sender()
  1668. # cw_index = self.ui.cnc_tools_table.indexAt(cw.pos())
  1669. # cw_row = cw_index.row()
  1670. kind = self.ui.cncplot_method_combo.get_value()
  1671. self.shapes.clear(update=True)
  1672. if self.origin_kind == "excellon":
  1673. for r in range(self.ui.exc_cnc_tools_table.rowCount()):
  1674. row_dia = float('%.*f' % (self.decimals, float(self.ui.exc_cnc_tools_table.item(r, 1).text())))
  1675. for tooluid_key in self.exc_cnc_tools:
  1676. tooldia = float('%.*f' % (self.decimals, float(tooluid_key)))
  1677. if row_dia == tooldia:
  1678. gcode_parsed = self.exc_cnc_tools[tooluid_key]['gcode_parsed']
  1679. if self.ui.exc_cnc_tools_table.cellWidget(r, 6).isChecked():
  1680. self.plot2(tooldia=tooldia, obj=self, visible=True, gcode_parsed=gcode_parsed, kind=kind)
  1681. else:
  1682. for tooluid_key in self.cnc_tools:
  1683. tooldia = float('%.*f' % (self.decimals, float(self.cnc_tools[tooluid_key]['tooldia'])))
  1684. gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
  1685. # tool_uid = int(self.ui.cnc_tools_table.item(cw_row, 3).text())
  1686. for r in range(self.ui.cnc_tools_table.rowCount()):
  1687. if int(self.ui.cnc_tools_table.item(r, 5).text()) == int(tooluid_key):
  1688. if self.ui.cnc_tools_table.cellWidget(r, 6).isChecked():
  1689. self.plot2(tooldia=tooldia, obj=self, visible=True, gcode_parsed=gcode_parsed, kind=kind)
  1690. self.shapes.redraw()
  1691. # make sure that the general plot is disabled if one of the row plot's are disabled and
  1692. # if all the row plot's are enabled also enable the general plot checkbox
  1693. cb_cnt = 0
  1694. total_row = self.ui.cnc_tools_table.rowCount()
  1695. for row in range(total_row):
  1696. if self.ui.cnc_tools_table.cellWidget(row, 6).isChecked():
  1697. cb_cnt += 1
  1698. else:
  1699. cb_cnt -= 1
  1700. if cb_cnt < total_row:
  1701. self.ui.plot_cb.setChecked(False)
  1702. else:
  1703. self.ui.plot_cb.setChecked(True)
  1704. self.ui_connect()
  1705. def plot(self, visible=None, kind='all'):
  1706. """
  1707. # Does all the required setup and returns False
  1708. # if the 'ptint' option is set to False.
  1709. :param visible: Boolean to decide if the object will be plotted as visible or disabled on canvas
  1710. :param kind: String. Can be "all" or "travel" or "cut". For CNCJob plotting
  1711. :return: None
  1712. """
  1713. if not FlatCAMObj.plot(self):
  1714. return
  1715. visible = visible if visible else self.options['plot']
  1716. if self.app.is_legacy is False:
  1717. if self.ui.annotation_cb.get_value() and self.ui.plot_cb.get_value():
  1718. self.text_col.enabled = True
  1719. else:
  1720. self.text_col.enabled = False
  1721. self.annotation.redraw()
  1722. try:
  1723. if self.multitool is False: # single tool usage
  1724. try:
  1725. dia_plot = float(self.options["tooldia"])
  1726. except ValueError:
  1727. # we may have a tuple with only one element and a comma
  1728. dia_plot = [float(el) for el in self.options["tooldia"].split(',') if el != ''][0]
  1729. self.plot2(tooldia=dia_plot, obj=self, visible=visible, kind=kind)
  1730. else:
  1731. # I do this so the travel lines thickness will reflect the tool diameter
  1732. # may work only for objects created within the app and not Gcode imported from elsewhere for which we
  1733. # don't know the origin
  1734. if self.origin_kind == "excellon":
  1735. if self.exc_cnc_tools:
  1736. for tooldia_key in self.exc_cnc_tools:
  1737. tooldia = float('%.*f' % (self.decimals, float(tooldia_key)))
  1738. gcode_parsed = self.exc_cnc_tools[tooldia_key]['gcode_parsed']
  1739. if not gcode_parsed:
  1740. continue
  1741. # gcode_parsed = self.gcode_parsed
  1742. self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
  1743. else:
  1744. # multiple tools usage
  1745. if self.cnc_tools:
  1746. for tooluid_key in self.cnc_tools:
  1747. tooldia = float('%.*f' % (self.decimals, float(self.cnc_tools[tooluid_key]['tooldia'])))
  1748. gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
  1749. self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
  1750. self.shapes.redraw()
  1751. except (ObjectDeleted, AttributeError):
  1752. self.shapes.clear(update=True)
  1753. if self.app.is_legacy is False:
  1754. self.annotation.clear(update=True)
  1755. def on_annotation_change(self):
  1756. """
  1757. Handler for toggling the annotation display by clicking a checkbox.
  1758. :return:
  1759. """
  1760. if self.app.is_legacy is False:
  1761. if self.ui.annotation_cb.get_value():
  1762. self.text_col.enabled = True
  1763. else:
  1764. self.text_col.enabled = False
  1765. # kind = self.ui.cncplot_method_combo.get_value()
  1766. # self.plot(kind=kind)
  1767. self.annotation.redraw()
  1768. else:
  1769. kind = self.ui.cncplot_method_combo.get_value()
  1770. self.plot(kind=kind)
  1771. def convert_units(self, units):
  1772. """
  1773. Units conversion used by the CNCJob objects.
  1774. :param units: Can be "MM" or "IN"
  1775. :return:
  1776. """
  1777. log.debug("FlatCAMObj.FlatCAMECNCjob.convert_units()")
  1778. factor = CNCjob.convert_units(self, units)
  1779. self.options["tooldia"] = float(self.options["tooldia"]) * factor
  1780. param_list = ['cutz', 'depthperpass', 'travelz', 'feedrate', 'feedrate_z', 'feedrate_rapid',
  1781. 'endz', 'toolchangez']
  1782. temp_tools_dict = {}
  1783. tool_dia_copy = {}
  1784. data_copy = {}
  1785. for tooluid_key, tooluid_value in self.cnc_tools.items():
  1786. for dia_key, dia_value in tooluid_value.items():
  1787. if dia_key == 'tooldia':
  1788. dia_value *= factor
  1789. dia_value = float('%.*f' % (self.decimals, dia_value))
  1790. tool_dia_copy[dia_key] = dia_value
  1791. if dia_key == 'offset':
  1792. tool_dia_copy[dia_key] = dia_value
  1793. if dia_key == 'offset_value':
  1794. dia_value *= factor
  1795. tool_dia_copy[dia_key] = dia_value
  1796. if dia_key == 'type':
  1797. tool_dia_copy[dia_key] = dia_value
  1798. if dia_key == 'tool_type':
  1799. tool_dia_copy[dia_key] = dia_value
  1800. if dia_key == 'data':
  1801. for data_key, data_value in dia_value.items():
  1802. # convert the form fields that are convertible
  1803. for param in param_list:
  1804. if data_key == param and data_value is not None:
  1805. data_copy[data_key] = data_value * factor
  1806. # copy the other dict entries that are not convertible
  1807. if data_key not in param_list:
  1808. data_copy[data_key] = data_value
  1809. tool_dia_copy[dia_key] = deepcopy(data_copy)
  1810. data_copy.clear()
  1811. if dia_key == 'gcode':
  1812. tool_dia_copy[dia_key] = dia_value
  1813. if dia_key == 'gcode_parsed':
  1814. tool_dia_copy[dia_key] = dia_value
  1815. if dia_key == 'solid_geometry':
  1816. tool_dia_copy[dia_key] = dia_value
  1817. # if dia_key == 'solid_geometry':
  1818. # tool_dia_copy[dia_key] = affinity.scale(dia_value, xfact=factor, origin=(0, 0))
  1819. # if dia_key == 'gcode_parsed':
  1820. # for g in dia_value:
  1821. # g['geom'] = affinity.scale(g['geom'], factor, factor, origin=(0, 0))
  1822. #
  1823. # tool_dia_copy['gcode_parsed'] = deepcopy(dia_value)
  1824. # tool_dia_copy['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_value])
  1825. temp_tools_dict.update({
  1826. tooluid_key: deepcopy(tool_dia_copy)
  1827. })
  1828. tool_dia_copy.clear()
  1829. self.cnc_tools.clear()
  1830. self.cnc_tools = deepcopy(temp_tools_dict)