FlatCAMCNCJob.py 110 KB

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