FlatCAMCNCJob.py 99 KB

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