FlatCAMGeometry.py 127 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947
  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 shapely.geometry import Polygon, MultiPolygon, MultiLineString, LineString, LinearRing
  12. import shapely.affinity as affinity
  13. from camlib import Geometry
  14. from flatcamObjects.FlatCAMObj import *
  15. from flatcamGUI.VisPyVisuals import ShapeCollection
  16. import FlatCAMTool
  17. import ezdxf
  18. import math
  19. import numpy as np
  20. from copy import deepcopy
  21. import traceback
  22. import gettext
  23. import FlatCAMTranslation as fcTranslate
  24. import builtins
  25. fcTranslate.apply_language('strings')
  26. if '_' not in builtins.__dict__:
  27. _ = gettext.gettext
  28. class GeometryObject(FlatCAMObj, Geometry):
  29. """
  30. Geometric object not associated with a specific
  31. format.
  32. """
  33. optionChanged = QtCore.pyqtSignal(str)
  34. ui_type = GeometryObjectUI
  35. def __init__(self, name):
  36. self.decimals = self.app.decimals
  37. self.circle_steps = int(self.app.defaults["geometry_circle_steps"])
  38. FlatCAMObj.__init__(self, name)
  39. Geometry.__init__(self, geo_steps_per_circle=self.circle_steps)
  40. self.kind = "geometry"
  41. self.options.update({
  42. "plot": True,
  43. "cutz": -0.002,
  44. "vtipdia": 0.1,
  45. "vtipangle": 30,
  46. "travelz": 0.1,
  47. "feedrate": 5.0,
  48. "feedrate_z": 5.0,
  49. "feedrate_rapid": 5.0,
  50. "spindlespeed": 0,
  51. "dwell": True,
  52. "dwelltime": 1000,
  53. "multidepth": False,
  54. "depthperpass": 0.002,
  55. "extracut": False,
  56. "extracut_length": 0.1,
  57. "endz": 2.0,
  58. "endxy": '',
  59. "area_exclusion": False,
  60. "area_shape": "polygon",
  61. "area_strategy": "over",
  62. "area_overz": 1.0,
  63. "startz": None,
  64. "toolchange": False,
  65. "toolchangez": 1.0,
  66. "toolchangexy": "0.0, 0.0",
  67. "ppname_g": 'default',
  68. "z_pdepth": -0.02,
  69. "feedrate_probe": 3.0,
  70. })
  71. if "cnctooldia" not in self.options:
  72. if type(self.app.defaults["geometry_cnctooldia"]) == float:
  73. self.options["cnctooldia"] = self.app.defaults["geometry_cnctooldia"]
  74. else:
  75. try:
  76. tools_string = self.app.defaults["geometry_cnctooldia"].split(",")
  77. tools_diameters = [eval(a) for a in tools_string if a != '']
  78. self.options["cnctooldia"] = tools_diameters[0] if tools_diameters else 0.0
  79. except Exception as e:
  80. log.debug("FlatCAMObj.GeometryObject.init() --> %s" % str(e))
  81. self.options["startz"] = self.app.defaults["geometry_startz"]
  82. # this will hold the tool unique ID that is useful when having multiple tools with same diameter
  83. self.tooluid = 0
  84. '''
  85. self.tools = {}
  86. This is a dictionary. Each dict key is associated with a tool used in geo_tools_table. The key is the
  87. tool_id of the tools and the value is another dict that will hold the data under the following form:
  88. {tooluid: {
  89. 'tooldia': 1,
  90. 'offset': 'Path',
  91. 'offset_value': 0.0
  92. 'type': 'Rough',
  93. 'tool_type': 'C1',
  94. 'data': self.default_tool_data
  95. 'solid_geometry': []
  96. }
  97. }
  98. '''
  99. self.tools = {}
  100. # this dict is to store those elements (tools) of self.tools that are selected in the self.geo_tools_table
  101. # those elements are the ones used for generating GCode
  102. self.sel_tools = {}
  103. self.offset_item_options = ["Path", "In", "Out", "Custom"]
  104. self.type_item_options = [_("Iso"), _("Rough"), _("Finish")]
  105. self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
  106. # flag to store if the V-Shape tool is selected in self.ui.geo_tools_table
  107. self.v_tool_type = None
  108. # flag to store if the Geometry is type 'multi-geometry' meaning that each tool has it's own geometry
  109. # the default value is False
  110. self.multigeo = False
  111. # flag to store if the geometry is part of a special group of geometries that can't be processed by the default
  112. # engine of FlatCAM. Most likely are generated by some of tools and are special cases of geometries.
  113. self.special_group = None
  114. self.old_pp_state = self.app.defaults["geometry_multidepth"]
  115. self.old_toolchangeg_state = self.app.defaults["geometry_toolchange"]
  116. self.units_found = self.app.defaults['units']
  117. # this variable can be updated by the Object that generates the geometry
  118. self.tool_type = 'C1'
  119. # save here the old value for the Cut Z before it is changed by selecting a V-shape type tool in the tool table
  120. self.old_cutz = self.app.defaults["geometry_cutz"]
  121. self.fill_color = self.app.defaults['geometry_plot_line']
  122. self.outline_color = self.app.defaults['geometry_plot_line']
  123. self.alpha_level = 'FF'
  124. self.param_fields = {}
  125. # Event signals disconnect id holders
  126. self.mr = None
  127. self.mm = None
  128. self.kp = None
  129. # variables to be used in area exclusion
  130. self.cursor_pos = (0, 0)
  131. self.exclusion_areas_list = []
  132. self.first_click = False
  133. self.points = []
  134. self.poly_drawn = False
  135. # Storage for shapes, storage that can be used by FlatCAm tools for utility geometry
  136. # VisPy visuals
  137. if self.app.is_legacy is False:
  138. try:
  139. self.exclusion_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1)
  140. except AttributeError:
  141. self.exclusion_shapes = None
  142. else:
  143. from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
  144. self.exclusion_shapes = ShapeCollectionLegacy(
  145. obj=self, app=self.app, name="exclusion" + self.options['name'])
  146. # Attributes to be included in serialization
  147. # Always append to it because it carries contents
  148. # from predecessors.
  149. self.ser_attrs += ['options', 'kind', 'tools', 'multigeo']
  150. def build_ui(self):
  151. self.ui_disconnect()
  152. FlatCAMObj.build_ui(self)
  153. self.units = self.app.defaults['units']
  154. tool_idx = 0
  155. n = len(self.tools)
  156. self.ui.geo_tools_table.setRowCount(n)
  157. for tooluid_key, tooluid_value in self.tools.items():
  158. tool_idx += 1
  159. row_no = tool_idx - 1
  160. tool_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
  161. tool_id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
  162. self.ui.geo_tools_table.setItem(row_no, 0, tool_id) # Tool name/id
  163. # Make sure that the tool diameter when in MM is with no more than 2 decimals.
  164. # There are no tool bits in MM with more than 3 decimals diameter.
  165. # For INCH the decimals should be no more than 3. There are no tools under 10mils.
  166. dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(tooluid_value['tooldia'])))
  167. dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
  168. offset_item = FCComboBox()
  169. for item in self.offset_item_options:
  170. offset_item.addItem(item)
  171. # offset_item.setStyleSheet('background-color: rgb(255,255,255)')
  172. idx = offset_item.findText(tooluid_value['offset'])
  173. offset_item.setCurrentIndex(idx)
  174. type_item = FCComboBox()
  175. for item in self.type_item_options:
  176. type_item.addItem(item)
  177. # type_item.setStyleSheet('background-color: rgb(255,255,255)')
  178. idx = type_item.findText(tooluid_value['type'])
  179. type_item.setCurrentIndex(idx)
  180. tool_type_item = FCComboBox()
  181. for item in self.tool_type_item_options:
  182. tool_type_item.addItem(item)
  183. # tool_type_item.setStyleSheet('background-color: rgb(255,255,255)')
  184. idx = tool_type_item.findText(tooluid_value['tool_type'])
  185. tool_type_item.setCurrentIndex(idx)
  186. tool_uid_item = QtWidgets.QTableWidgetItem(str(tooluid_key))
  187. plot_item = FCCheckBox()
  188. plot_item.setLayoutDirection(QtCore.Qt.RightToLeft)
  189. if self.ui.plot_cb.isChecked():
  190. plot_item.setChecked(True)
  191. self.ui.geo_tools_table.setItem(row_no, 1, dia_item) # Diameter
  192. self.ui.geo_tools_table.setCellWidget(row_no, 2, offset_item)
  193. self.ui.geo_tools_table.setCellWidget(row_no, 3, type_item)
  194. self.ui.geo_tools_table.setCellWidget(row_no, 4, tool_type_item)
  195. # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY ###
  196. self.ui.geo_tools_table.setItem(row_no, 5, tool_uid_item) # Tool unique ID
  197. self.ui.geo_tools_table.setCellWidget(row_no, 6, plot_item)
  198. try:
  199. self.ui.tool_offset_entry.set_value(tooluid_value['offset_value'])
  200. except Exception as e:
  201. log.debug("build_ui() --> Could not set the 'offset_value' key in self.tools. Error: %s" % str(e))
  202. # make the diameter column editable
  203. for row in range(tool_idx):
  204. self.ui.geo_tools_table.item(row, 1).setFlags(QtCore.Qt.ItemIsSelectable |
  205. QtCore.Qt.ItemIsEditable |
  206. QtCore.Qt.ItemIsEnabled)
  207. # sort the tool diameter column
  208. # self.ui.geo_tools_table.sortItems(1)
  209. # all the tools are selected by default
  210. # self.ui.geo_tools_table.selectColumn(0)
  211. self.ui.geo_tools_table.resizeColumnsToContents()
  212. self.ui.geo_tools_table.resizeRowsToContents()
  213. vertical_header = self.ui.geo_tools_table.verticalHeader()
  214. # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
  215. vertical_header.hide()
  216. self.ui.geo_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  217. horizontal_header = self.ui.geo_tools_table.horizontalHeader()
  218. horizontal_header.setMinimumSectionSize(10)
  219. horizontal_header.setDefaultSectionSize(70)
  220. horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
  221. horizontal_header.resizeSection(0, 20)
  222. horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
  223. # horizontal_header.setColumnWidth(2, QtWidgets.QHeaderView.ResizeToContents)
  224. horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
  225. horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed)
  226. horizontal_header.resizeSection(4, 40)
  227. horizontal_header.setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed)
  228. horizontal_header.resizeSection(4, 17)
  229. # horizontal_header.setStretchLastSection(True)
  230. self.ui.geo_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  231. self.ui.geo_tools_table.setColumnWidth(0, 20)
  232. self.ui.geo_tools_table.setColumnWidth(4, 40)
  233. self.ui.geo_tools_table.setColumnWidth(6, 17)
  234. # self.ui.geo_tools_table.setSortingEnabled(True)
  235. self.ui.geo_tools_table.setMinimumHeight(self.ui.geo_tools_table.getHeight())
  236. self.ui.geo_tools_table.setMaximumHeight(self.ui.geo_tools_table.getHeight())
  237. # update UI for all rows - useful after units conversion but only if there is at least one row
  238. row_cnt = self.ui.geo_tools_table.rowCount()
  239. if row_cnt > 0:
  240. for r in range(row_cnt):
  241. self.update_ui(r)
  242. # select only the first tool / row
  243. selected_row = 0
  244. try:
  245. self.select_tools_table_row(selected_row, clearsel=True)
  246. # update the Geometry UI
  247. self.update_ui()
  248. except Exception as e:
  249. # when the tools table is empty there will be this error but once the table is populated it will go away
  250. log.debug(str(e))
  251. # disable the Plot column in Tool Table if the geometry is SingleGeo as it is not needed
  252. # and can create some problems
  253. if self.multigeo is False:
  254. self.ui.geo_tools_table.setColumnHidden(6, True)
  255. else:
  256. self.ui.geo_tools_table.setColumnHidden(6, False)
  257. self.set_tool_offset_visibility(selected_row)
  258. # HACK: for whatever reasons the name in Selected tab is reverted to the original one after a successful rename
  259. # done in the collection view but only for Geometry objects. Perhaps some references remains. Should be fixed.
  260. self.ui.name_entry.set_value(self.options['name'])
  261. self.ui_connect()
  262. self.ui.e_cut_entry.setDisabled(False) if self.ui.extracut_cb.get_value() else \
  263. self.ui.e_cut_entry.setDisabled(True)
  264. # set the text on tool_data_label after loading the object
  265. sel_rows = []
  266. sel_items = self.ui.geo_tools_table.selectedItems()
  267. for it in sel_items:
  268. sel_rows.append(it.row())
  269. if len(sel_rows) > 1:
  270. self.ui.tool_data_label.setText(
  271. "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("Multiple Tools"))
  272. )
  273. def set_ui(self, ui):
  274. FlatCAMObj.set_ui(self, ui)
  275. log.debug("GeometryObject.set_ui()")
  276. assert isinstance(self.ui, GeometryObjectUI), \
  277. "Expected a GeometryObjectUI, got %s" % type(self.ui)
  278. self.units = self.app.defaults['units'].upper()
  279. self.units_found = self.app.defaults['units']
  280. # populate preprocessor names in the combobox
  281. for name in list(self.app.preprocessors.keys()):
  282. self.ui.pp_geometry_name_cb.addItem(name)
  283. self.form_fields.update({
  284. "plot": self.ui.plot_cb,
  285. "cutz": self.ui.cutz_entry,
  286. "vtipdia": self.ui.tipdia_entry,
  287. "vtipangle": self.ui.tipangle_entry,
  288. "travelz": self.ui.travelz_entry,
  289. "feedrate": self.ui.cncfeedrate_entry,
  290. "feedrate_z": self.ui.feedrate_z_entry,
  291. "feedrate_rapid": self.ui.feedrate_rapid_entry,
  292. "spindlespeed": self.ui.cncspindlespeed_entry,
  293. "dwell": self.ui.dwell_cb,
  294. "dwelltime": self.ui.dwelltime_entry,
  295. "multidepth": self.ui.mpass_cb,
  296. "ppname_g": self.ui.pp_geometry_name_cb,
  297. "z_pdepth": self.ui.pdepth_entry,
  298. "feedrate_probe": self.ui.feedrate_probe_entry,
  299. "depthperpass": self.ui.maxdepth_entry,
  300. "extracut": self.ui.extracut_cb,
  301. "extracut_length": self.ui.e_cut_entry,
  302. "toolchange": self.ui.toolchangeg_cb,
  303. "toolchangez": self.ui.toolchangez_entry,
  304. "endz": self.ui.endz_entry,
  305. "endxy": self.ui.endxy_entry,
  306. "cnctooldia": self.ui.addtool_entry,
  307. "area_exclusion": self.ui.exclusion_cb,
  308. "area_shape": self.ui.area_shape_radio,
  309. "area_strategy": self.ui.strategy_radio,
  310. "area_overz": self.ui.over_z_entry,
  311. })
  312. self.param_fields.update({
  313. "vtipdia": self.ui.tipdia_entry,
  314. "vtipangle": self.ui.tipangle_entry,
  315. "cutz": self.ui.cutz_entry,
  316. "depthperpass": self.ui.maxdepth_entry,
  317. "multidepth": self.ui.mpass_cb,
  318. "travelz": self.ui.travelz_entry,
  319. "feedrate": self.ui.cncfeedrate_entry,
  320. "feedrate_z": self.ui.feedrate_z_entry,
  321. "feedrate_rapid": self.ui.feedrate_rapid_entry,
  322. "extracut": self.ui.extracut_cb,
  323. "extracut_length": self.ui.e_cut_entry,
  324. "spindlespeed": self.ui.cncspindlespeed_entry,
  325. "dwelltime": self.ui.dwelltime_entry,
  326. "dwell": self.ui.dwell_cb,
  327. "pdepth": self.ui.pdepth_entry,
  328. "pfeedrate": self.ui.feedrate_probe_entry,
  329. })
  330. # Fill form fields only on object create
  331. self.to_form()
  332. # update the changes in UI depending on the selected preprocessor in Preferences
  333. # after this moment all the changes in the Posprocessor combo will be handled by the activated signal of the
  334. # self.ui.pp_geometry_name_cb combobox
  335. self.on_pp_changed()
  336. self.ui.tipdialabel.hide()
  337. self.ui.tipdia_entry.hide()
  338. self.ui.tipanglelabel.hide()
  339. self.ui.tipangle_entry.hide()
  340. self.ui.cutz_entry.setDisabled(False)
  341. # store here the default data for Geometry Data
  342. self.default_data = {}
  343. self.default_data.update({
  344. "name": None,
  345. "plot": None,
  346. "cutz": None,
  347. "vtipdia": None,
  348. "vtipangle": None,
  349. "travelz": None,
  350. "feedrate": None,
  351. "feedrate_z": None,
  352. "feedrate_rapid": None,
  353. "dwell": None,
  354. "dwelltime": None,
  355. "multidepth": None,
  356. "ppname_g": None,
  357. "depthperpass": None,
  358. "extracut": None,
  359. "extracut_length": None,
  360. "toolchange": None,
  361. "toolchangez": None,
  362. "endz": None,
  363. "endxy": '',
  364. "area_exclusion": None,
  365. "area_shape": None,
  366. "area_strategy": None,
  367. "area_overz": None,
  368. "spindlespeed": 0,
  369. "toolchangexy": None,
  370. "startz": None
  371. })
  372. # fill in self.default_data values from self.options
  373. for def_key in self.default_data:
  374. for opt_key, opt_val in self.options.items():
  375. if def_key == opt_key:
  376. self.default_data[def_key] = deepcopy(opt_val)
  377. if type(self.options["cnctooldia"]) == float:
  378. tools_list = [self.options["cnctooldia"]]
  379. else:
  380. try:
  381. temp_tools = self.options["cnctooldia"].split(",")
  382. tools_list = [
  383. float(eval(dia)) for dia in temp_tools if dia != ''
  384. ]
  385. except Exception as e:
  386. log.error("GeometryObject.set_ui() -> At least one tool diameter needed. "
  387. "Verify in Edit -> Preferences -> Geometry General -> Tool dia. %s" % str(e))
  388. return
  389. self.tooluid += 1
  390. if not self.tools:
  391. for toold in tools_list:
  392. new_data = deepcopy(self.default_data)
  393. self.tools.update({
  394. self.tooluid: {
  395. 'tooldia': float('%.*f' % (self.decimals, float(toold))),
  396. 'offset': 'Path',
  397. 'offset_value': 0.0,
  398. 'type': _('Rough'),
  399. 'tool_type': self.tool_type,
  400. 'data': new_data,
  401. 'solid_geometry': self.solid_geometry
  402. }
  403. })
  404. self.tooluid += 1
  405. else:
  406. # if self.tools is not empty then it can safely be assumed that it comes from an opened project.
  407. # Because of the serialization the self.tools list on project save, the dict keys (members of self.tools
  408. # are each a dict) are turned into strings so we rebuild the self.tools elements so the keys are
  409. # again float type; dict's don't like having keys changed when iterated through therefore the need for the
  410. # following convoluted way of changing the keys from string to float type
  411. temp_tools = {}
  412. for tooluid_key in self.tools:
  413. val = deepcopy(self.tools[tooluid_key])
  414. new_key = deepcopy(int(tooluid_key))
  415. temp_tools[new_key] = val
  416. self.tools.clear()
  417. self.tools = deepcopy(temp_tools)
  418. self.ui.tool_offset_entry.hide()
  419. self.ui.tool_offset_lbl.hide()
  420. # used to store the state of the mpass_cb if the selected preprocessor for geometry is hpgl
  421. self.old_pp_state = self.default_data['multidepth']
  422. self.old_toolchangeg_state = self.default_data['toolchange']
  423. if not isinstance(self.ui, GeometryObjectUI):
  424. log.debug("Expected a GeometryObjectUI, got %s" % type(self.ui))
  425. return
  426. self.ui.geo_tools_table.setupContextMenu()
  427. self.ui.geo_tools_table.addContextMenu(
  428. _("Add from Tool DB"), self.on_tool_add_from_db_clicked,
  429. icon=QtGui.QIcon(self.app.resource_location + "/plus16.png"))
  430. self.ui.geo_tools_table.addContextMenu(
  431. _("Copy"), self.on_tool_copy,
  432. icon=QtGui.QIcon(self.app.resource_location + "/copy16.png"))
  433. self.ui.geo_tools_table.addContextMenu(
  434. _("Delete"), lambda: self.on_tool_delete(all_tools=None),
  435. icon=QtGui.QIcon(self.app.resource_location + "/delete32.png"))
  436. # Show/Hide Advanced Options
  437. if self.app.defaults["global_app_level"] == 'b':
  438. self.ui.level.setText('<span style="color:green;"><b>%s</b></span>' % _('Basic'))
  439. self.ui.geo_tools_table.setColumnHidden(2, True)
  440. self.ui.geo_tools_table.setColumnHidden(3, True)
  441. # self.ui.geo_tools_table.setColumnHidden(4, True)
  442. self.ui.addtool_entry_lbl.hide()
  443. self.ui.addtool_entry.hide()
  444. self.ui.addtool_btn.hide()
  445. self.ui.copytool_btn.hide()
  446. self.ui.deltool_btn.hide()
  447. # self.ui.endz_label.hide()
  448. # self.ui.endz_entry.hide()
  449. self.ui.fr_rapidlabel.hide()
  450. self.ui.feedrate_rapid_entry.hide()
  451. self.ui.extracut_cb.hide()
  452. self.ui.e_cut_entry.hide()
  453. self.ui.pdepth_label.hide()
  454. self.ui.pdepth_entry.hide()
  455. self.ui.feedrate_probe_label.hide()
  456. self.ui.feedrate_probe_entry.hide()
  457. else:
  458. self.ui.level.setText('<span style="color:red;"><b>%s</b></span>' % _('Advanced'))
  459. self.ui.e_cut_entry.setDisabled(False) if self.app.defaults['geometry_extracut'] else \
  460. self.ui.e_cut_entry.setDisabled(True)
  461. self.ui.extracut_cb.toggled.connect(lambda state: self.ui.e_cut_entry.setDisabled(not state))
  462. self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
  463. self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click)
  464. self.ui.paint_tool_button.clicked.connect(lambda: self.app.paint_tool.run(toggle=False))
  465. self.ui.generate_ncc_button.clicked.connect(lambda: self.app.ncclear_tool.run(toggle=False))
  466. self.ui.pp_geometry_name_cb.activated.connect(self.on_pp_changed)
  467. self.ui.tipdia_entry.valueChanged.connect(self.update_cutz)
  468. self.ui.tipangle_entry.valueChanged.connect(self.update_cutz)
  469. self.ui.addtool_from_db_btn.clicked.connect(self.on_tool_add_from_db_clicked)
  470. self.ui.apply_param_to_all.clicked.connect(self.on_apply_param_to_all_clicked)
  471. self.ui.cutz_entry.returnPressed.connect(self.on_cut_z_changed)
  472. self.ui.add_area_button.clicked.connect(self.on_add_area_click)
  473. self.ui.delete_area_button.clicked.connect(self.on_clear_area_click)
  474. def on_cut_z_changed(self):
  475. self.old_cutz = self.ui.cutz_entry.get_value()
  476. def set_tool_offset_visibility(self, current_row):
  477. if current_row is None:
  478. return
  479. try:
  480. tool_offset = self.ui.geo_tools_table.cellWidget(current_row, 2)
  481. if tool_offset is not None:
  482. tool_offset_txt = tool_offset.currentText()
  483. if tool_offset_txt == 'Custom':
  484. self.ui.tool_offset_entry.show()
  485. self.ui.tool_offset_lbl.show()
  486. else:
  487. self.ui.tool_offset_entry.hide()
  488. self.ui.tool_offset_lbl.hide()
  489. except Exception as e:
  490. log.debug("set_tool_offset_visibility() --> " + str(e))
  491. return
  492. def on_offset_value_edited(self):
  493. """
  494. This will save the offset_value into self.tools storage whenever the offset value is edited
  495. :return:
  496. """
  497. for current_row in self.ui.geo_tools_table.selectedItems():
  498. # sometime the header get selected and it has row number -1
  499. # we don't want to do anything with the header :)
  500. if current_row.row() < 0:
  501. continue
  502. tool_uid = int(self.ui.geo_tools_table.item(current_row.row(), 5).text())
  503. self.set_tool_offset_visibility(current_row.row())
  504. for tooluid_key, tooluid_value in self.tools.items():
  505. if int(tooluid_key) == tool_uid:
  506. try:
  507. tooluid_value['offset_value'] = float(self.ui.tool_offset_entry.get_value())
  508. except ValueError:
  509. # try to convert comma to decimal point. if it's still not working error message and return
  510. try:
  511. tooluid_value['offset_value'] = float(
  512. self.ui.tool_offset_entry.get_value().replace(',', '.')
  513. )
  514. except ValueError:
  515. self.app.inform.emit('[ERROR_NOTCL] %s' %
  516. _("Wrong value format entered, use a number."))
  517. return
  518. def ui_connect(self):
  519. # on any change to the widgets that matter it will be called self.gui_form_to_storage which will save the
  520. # changes in geometry UI
  521. for i in self.param_fields:
  522. current_widget = self.param_fields[i]
  523. if isinstance(current_widget, FCCheckBox):
  524. current_widget.stateChanged.connect(self.gui_form_to_storage)
  525. elif isinstance(current_widget, FCComboBox):
  526. current_widget.currentIndexChanged.connect(self.gui_form_to_storage)
  527. elif isinstance(current_widget, FloatEntry) or isinstance(current_widget, LengthEntry) or \
  528. isinstance(current_widget, FCEntry) or isinstance(current_widget, IntEntry):
  529. current_widget.editingFinished.connect(self.gui_form_to_storage)
  530. elif isinstance(current_widget, FCSpinner) or isinstance(current_widget, FCDoubleSpinner):
  531. current_widget.returnPressed.connect(self.gui_form_to_storage)
  532. for row in range(self.ui.geo_tools_table.rowCount()):
  533. for col in [2, 3, 4]:
  534. self.ui.geo_tools_table.cellWidget(row, col).currentIndexChanged.connect(
  535. self.on_tooltable_cellwidget_change)
  536. # I use lambda's because the connected functions have parameters that could be used in certain scenarios
  537. self.ui.addtool_btn.clicked.connect(lambda: self.on_tool_add())
  538. self.ui.copytool_btn.clicked.connect(lambda: self.on_tool_copy())
  539. self.ui.deltool_btn.clicked.connect(lambda: self.on_tool_delete())
  540. # self.ui.geo_tools_table.currentItemChanged.connect(self.on_row_selection_change)
  541. self.ui.geo_tools_table.clicked.connect(self.on_row_selection_change)
  542. self.ui.geo_tools_table.horizontalHeader().sectionClicked.connect(self.on_row_selection_change)
  543. self.ui.geo_tools_table.itemChanged.connect(self.on_tool_edit)
  544. self.ui.tool_offset_entry.returnPressed.connect(self.on_offset_value_edited)
  545. for row in range(self.ui.geo_tools_table.rowCount()):
  546. self.ui.geo_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table)
  547. self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
  548. # common parameters update
  549. self.ui.pp_geometry_name_cb.currentIndexChanged.connect(self.update_common_param_in_storage)
  550. def ui_disconnect(self):
  551. # on any change to the widgets that matter it will be called self.gui_form_to_storage which will save the
  552. # changes in geometry UI
  553. for i in self.param_fields:
  554. # current_widget = self.ui.grid3.itemAt(i).widget()
  555. current_widget = self.param_fields[i]
  556. if isinstance(current_widget, FCCheckBox):
  557. try:
  558. current_widget.stateChanged.disconnect(self.gui_form_to_storage)
  559. except (TypeError, AttributeError):
  560. pass
  561. elif isinstance(current_widget, FCComboBox):
  562. try:
  563. current_widget.currentIndexChanged.disconnect(self.gui_form_to_storage)
  564. except (TypeError, AttributeError):
  565. pass
  566. elif isinstance(current_widget, LengthEntry) or isinstance(current_widget, IntEntry) or \
  567. isinstance(current_widget, FCEntry) or isinstance(current_widget, FloatEntry):
  568. try:
  569. current_widget.editingFinished.disconnect(self.gui_form_to_storage)
  570. except (TypeError, AttributeError):
  571. pass
  572. elif isinstance(current_widget, FCSpinner) or isinstance(current_widget, FCDoubleSpinner):
  573. try:
  574. current_widget.returnPressed.disconnect(self.gui_form_to_storage)
  575. except TypeError:
  576. pass
  577. for row in range(self.ui.geo_tools_table.rowCount()):
  578. for col in [2, 3, 4]:
  579. try:
  580. self.ui.geo_tools_table.cellWidget(row, col).currentIndexChanged.disconnect()
  581. except (TypeError, AttributeError):
  582. pass
  583. try:
  584. self.ui.addtool_btn.clicked.disconnect()
  585. except (TypeError, AttributeError):
  586. pass
  587. try:
  588. self.ui.copytool_btn.clicked.disconnect()
  589. except (TypeError, AttributeError):
  590. pass
  591. try:
  592. self.ui.deltool_btn.clicked.disconnect()
  593. except (TypeError, AttributeError):
  594. pass
  595. try:
  596. self.ui.geo_tools_table.clicked.disconnect()
  597. except (TypeError, AttributeError):
  598. pass
  599. try:
  600. self.ui.geo_tools_table.horizontalHeader().sectionClicked.disconnect()
  601. except (TypeError, AttributeError):
  602. pass
  603. try:
  604. self.ui.geo_tools_table.itemChanged.disconnect()
  605. except (TypeError, AttributeError):
  606. pass
  607. try:
  608. self.ui.tool_offset_entry.returnPressed.disconnect()
  609. except (TypeError, AttributeError):
  610. pass
  611. for row in range(self.ui.geo_tools_table.rowCount()):
  612. try:
  613. self.ui.geo_tools_table.cellWidget(row, 6).clicked.disconnect()
  614. except (TypeError, AttributeError):
  615. pass
  616. try:
  617. self.ui.plot_cb.stateChanged.disconnect()
  618. except (TypeError, AttributeError):
  619. pass
  620. def on_row_selection_change(self):
  621. self.update_ui()
  622. def update_ui(self, row=None):
  623. self.ui_disconnect()
  624. if row is None:
  625. sel_rows = []
  626. sel_items = self.ui.geo_tools_table.selectedItems()
  627. for it in sel_items:
  628. sel_rows.append(it.row())
  629. else:
  630. sel_rows = row if type(row) == list else [row]
  631. if not sel_rows:
  632. sel_rows = [0]
  633. for current_row in sel_rows:
  634. self.set_tool_offset_visibility(current_row)
  635. # populate the form with the data from the tool associated with the row parameter
  636. try:
  637. item = self.ui.geo_tools_table.item(current_row, 5)
  638. if type(item) is not None:
  639. tooluid = int(item.text())
  640. else:
  641. self.ui_connect()
  642. return
  643. except Exception as e:
  644. log.debug("Tool missing. Add a tool in Geo Tool Table. %s" % str(e))
  645. self.ui_connect()
  646. return
  647. # update the QLabel that shows for which Tool we have the parameters in the UI form
  648. if len(sel_rows) == 1:
  649. self.ui.tool_data_label.setText(
  650. "<b>%s: <font color='#0000FF'>%s %d</font></b>" % (_('Parameters for'), _("Tool"), tooluid)
  651. )
  652. # update the form with the V-Shape fields if V-Shape selected in the geo_tool_table
  653. # also modify the Cut Z form entry to reflect the calculated Cut Z from values got from V-Shape Fields
  654. try:
  655. item = self.ui.geo_tools_table.cellWidget(current_row, 4)
  656. if item is not None:
  657. tool_type_txt = item.currentText()
  658. self.ui_update_v_shape(tool_type_txt=tool_type_txt)
  659. else:
  660. self.ui_connect()
  661. return
  662. except Exception as e:
  663. log.debug("Tool missing in ui_update_v_shape(). Add a tool in Geo Tool Table. %s" % str(e))
  664. return
  665. try:
  666. # set the form with data from the newly selected tool
  667. for tooluid_key, tooluid_value in list(self.tools.items()):
  668. if int(tooluid_key) == tooluid:
  669. for key, value in list(tooluid_value.items()):
  670. if key == 'data':
  671. form_value_storage = tooluid_value['data']
  672. self.update_form(form_value_storage)
  673. if key == 'offset_value':
  674. # update the offset value in the entry even if the entry is hidden
  675. self.ui.tool_offset_entry.set_value(tooluid_value['offset_value'])
  676. if key == 'tool_type' and value == 'V':
  677. self.update_cutz()
  678. except Exception as e:
  679. log.debug("GeometryObject.update_ui() -> %s " % str(e))
  680. else:
  681. self.ui.tool_data_label.setText(
  682. "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("Multiple Tools"))
  683. )
  684. self.ui_connect()
  685. def on_tool_add(self, dia=None):
  686. self.ui_disconnect()
  687. self.units = self.app.defaults['units'].upper()
  688. if dia is not None:
  689. tooldia = dia
  690. else:
  691. tooldia = float(self.ui.addtool_entry.get_value())
  692. # construct a list of all 'tooluid' in the self.tools
  693. # tool_uid_list = []
  694. # for tooluid_key in self.tools:
  695. # tool_uid_list.append(int(tooluid_key))
  696. tool_uid_list = [int(tooluid_key) for tooluid_key in self.tools]
  697. # find maximum from the temp_uid, add 1 and this is the new 'tooluid'
  698. max_uid = max(tool_uid_list) if tool_uid_list else 0
  699. self.tooluid = max_uid + 1
  700. tooldia = float('%.*f' % (self.decimals, tooldia))
  701. # here we actually add the new tool; if there is no tool in the tool table we add a tool with default data
  702. # otherwise we add a tool with data copied from last tool
  703. if self.tools:
  704. last_data = self.tools[max_uid]['data']
  705. last_offset = self.tools[max_uid]['offset']
  706. last_offset_value = self.tools[max_uid]['offset_value']
  707. last_type = self.tools[max_uid]['type']
  708. last_tool_type = self.tools[max_uid]['tool_type']
  709. last_solid_geometry = self.tools[max_uid]['solid_geometry']
  710. # if previous geometry was empty (it may happen for the first tool added)
  711. # then copy the object.solid_geometry
  712. if not last_solid_geometry:
  713. last_solid_geometry = self.solid_geometry
  714. self.tools.update({
  715. self.tooluid: {
  716. 'tooldia': tooldia,
  717. 'offset': last_offset,
  718. 'offset_value': last_offset_value,
  719. 'type': last_type,
  720. 'tool_type': last_tool_type,
  721. 'data': deepcopy(last_data),
  722. 'solid_geometry': deepcopy(last_solid_geometry)
  723. }
  724. })
  725. else:
  726. self.tools.update({
  727. self.tooluid: {
  728. 'tooldia': tooldia,
  729. 'offset': 'Path',
  730. 'offset_value': 0.0,
  731. 'type': _('Rough'),
  732. 'tool_type': 'C1',
  733. 'data': deepcopy(self.default_data),
  734. 'solid_geometry': self.solid_geometry
  735. }
  736. })
  737. self.tools[self.tooluid]['data']['name'] = self.options['name']
  738. self.ui.tool_offset_entry.hide()
  739. self.ui.tool_offset_lbl.hide()
  740. # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list
  741. try:
  742. self.ser_attrs.remove('tools')
  743. except TypeError:
  744. pass
  745. self.ser_attrs.append('tools')
  746. self.app.inform.emit('[success] %s' % _("Tool added in Tool Table."))
  747. self.ui_connect()
  748. self.build_ui()
  749. # if there is no tool left in the Tools Table, enable the parameters GUI
  750. if self.ui.geo_tools_table.rowCount() != 0:
  751. self.ui.geo_param_frame.setDisabled(False)
  752. def on_tool_add_from_db_clicked(self):
  753. """
  754. Called when the user wants to add a new tool from Tools Database. It will create the Tools Database object
  755. and display the Tools Database tab in the form needed for the Tool adding
  756. :return: None
  757. """
  758. # if the Tools Database is already opened focus on it
  759. for idx in range(self.app.ui.plot_tab_area.count()):
  760. if self.app.ui.plot_tab_area.tabText(idx) == _("Tools Database"):
  761. self.app.ui.plot_tab_area.setCurrentWidget(self.app.tools_db_tab)
  762. break
  763. self.app.on_tools_database()
  764. self.app.tools_db_tab.ok_to_add = True
  765. self.app.tools_db_tab.buttons_frame.hide()
  766. self.app.tools_db_tab.add_tool_from_db.show()
  767. self.app.tools_db_tab.cancel_tool_from_db.show()
  768. def on_tool_from_db_inserted(self, tool):
  769. """
  770. Called from the Tools DB object through a App method when adding a tool from Tools Database
  771. :param tool: a dict with the tool data
  772. :return: None
  773. """
  774. self.ui_disconnect()
  775. self.units = self.app.defaults['units'].upper()
  776. tooldia = float(tool['tooldia'])
  777. # construct a list of all 'tooluid' in the self.tools
  778. tool_uid_list = []
  779. for tooluid_key in self.tools:
  780. tool_uid_item = int(tooluid_key)
  781. tool_uid_list.append(tool_uid_item)
  782. # find maximum from the temp_uid, add 1 and this is the new 'tooluid'
  783. if not tool_uid_list:
  784. max_uid = 0
  785. else:
  786. max_uid = max(tool_uid_list)
  787. self.tooluid = max_uid + 1
  788. tooldia = float('%.*f' % (self.decimals, tooldia))
  789. self.tools.update({
  790. self.tooluid: {
  791. 'tooldia': tooldia,
  792. 'offset': tool['offset'],
  793. 'offset_value': float(tool['offset_value']),
  794. 'type': tool['type'],
  795. 'tool_type': tool['tool_type'],
  796. 'data': deepcopy(tool['data']),
  797. 'solid_geometry': self.solid_geometry
  798. }
  799. })
  800. self.tools[self.tooluid]['data']['name'] = self.options['name']
  801. self.ui.tool_offset_entry.hide()
  802. self.ui.tool_offset_lbl.hide()
  803. # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list
  804. try:
  805. self.ser_attrs.remove('tools')
  806. except TypeError:
  807. pass
  808. self.ser_attrs.append('tools')
  809. self.ui_connect()
  810. self.build_ui()
  811. # if there is no tool left in the Tools Table, enable the parameters GUI
  812. if self.ui.geo_tools_table.rowCount() != 0:
  813. self.ui.geo_param_frame.setDisabled(False)
  814. def on_tool_copy(self, all_tools=None):
  815. self.ui_disconnect()
  816. # find the tool_uid maximum value in the self.tools
  817. uid_list = []
  818. for key in self.tools:
  819. uid_list.append(int(key))
  820. try:
  821. max_uid = max(uid_list, key=int)
  822. except ValueError:
  823. max_uid = 0
  824. if all_tools is None:
  825. if self.ui.geo_tools_table.selectedItems():
  826. for current_row in self.ui.geo_tools_table.selectedItems():
  827. # sometime the header get selected and it has row number -1
  828. # we don't want to do anything with the header :)
  829. if current_row.row() < 0:
  830. continue
  831. try:
  832. tooluid_copy = int(self.ui.geo_tools_table.item(current_row.row(), 5).text())
  833. self.set_tool_offset_visibility(current_row.row())
  834. max_uid += 1
  835. self.tools[int(max_uid)] = deepcopy(self.tools[tooluid_copy])
  836. except AttributeError:
  837. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Failed. Select a tool to copy."))
  838. self.ui_connect()
  839. self.build_ui()
  840. return
  841. except Exception as e:
  842. log.debug("on_tool_copy() --> " + str(e))
  843. # deselect the table
  844. # self.ui.geo_tools_table.clearSelection()
  845. else:
  846. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Failed. Select a tool to copy."))
  847. self.ui_connect()
  848. self.build_ui()
  849. return
  850. else:
  851. # we copy all tools in geo_tools_table
  852. try:
  853. temp_tools = deepcopy(self.tools)
  854. max_uid += 1
  855. for tooluid in temp_tools:
  856. self.tools[int(max_uid)] = deepcopy(temp_tools[tooluid])
  857. temp_tools.clear()
  858. except Exception as e:
  859. log.debug("on_tool_copy() --> " + str(e))
  860. # if there are no more tools in geo tools table then hide the tool offset
  861. if not self.tools:
  862. self.ui.tool_offset_entry.hide()
  863. self.ui.tool_offset_lbl.hide()
  864. # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list
  865. try:
  866. self.ser_attrs.remove('tools')
  867. except ValueError:
  868. pass
  869. self.ser_attrs.append('tools')
  870. self.ui_connect()
  871. self.build_ui()
  872. self.app.inform.emit('[success] %s' % _("Tool was copied in Tool Table."))
  873. def on_tool_edit(self, current_item):
  874. self.ui_disconnect()
  875. current_row = current_item.row()
  876. try:
  877. d = float(self.ui.geo_tools_table.item(current_row, 1).text())
  878. except ValueError:
  879. # try to convert comma to decimal point. if it's still not working error message and return
  880. try:
  881. d = float(self.ui.geo_tools_table.item(current_row, 1).text().replace(',', '.'))
  882. except ValueError:
  883. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number."))
  884. return
  885. tool_dia = float('%.*f' % (self.decimals, d))
  886. tooluid = int(self.ui.geo_tools_table.item(current_row, 5).text())
  887. self.tools[tooluid]['tooldia'] = tool_dia
  888. try:
  889. self.ser_attrs.remove('tools')
  890. self.ser_attrs.append('tools')
  891. except (TypeError, ValueError):
  892. pass
  893. self.app.inform.emit('[success] %s' % _("Tool was edited in Tool Table."))
  894. self.ui_connect()
  895. self.build_ui()
  896. def on_tool_delete(self, all_tools=None):
  897. self.ui_disconnect()
  898. if all_tools is None:
  899. if self.ui.geo_tools_table.selectedItems():
  900. for current_row in self.ui.geo_tools_table.selectedItems():
  901. # sometime the header get selected and it has row number -1
  902. # we don't want to do anything with the header :)
  903. if current_row.row() < 0:
  904. continue
  905. try:
  906. tooluid_del = int(self.ui.geo_tools_table.item(current_row.row(), 5).text())
  907. self.set_tool_offset_visibility(current_row.row())
  908. temp_tools = deepcopy(self.tools)
  909. for tooluid_key in self.tools:
  910. if int(tooluid_key) == tooluid_del:
  911. # if the self.tools has only one tool and we delete it then we move the solid_geometry
  912. # as a property of the object otherwise there will be nothing to hold it
  913. if len(self.tools) == 1:
  914. self.solid_geometry = deepcopy(self.tools[tooluid_key]['solid_geometry'])
  915. temp_tools.pop(tooluid_del, None)
  916. self.tools = deepcopy(temp_tools)
  917. temp_tools.clear()
  918. except AttributeError:
  919. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Failed. Select a tool to delete."))
  920. self.ui_connect()
  921. self.build_ui()
  922. return
  923. except Exception as e:
  924. log.debug("on_tool_delete() --> " + str(e))
  925. # deselect the table
  926. # self.ui.geo_tools_table.clearSelection()
  927. else:
  928. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Failed. Select a tool to delete."))
  929. self.ui_connect()
  930. self.build_ui()
  931. return
  932. else:
  933. # we delete all tools in geo_tools_table
  934. self.tools.clear()
  935. self.app.plot_all()
  936. # if there are no more tools in geo tools table then hide the tool offset
  937. if not self.tools:
  938. self.ui.tool_offset_entry.hide()
  939. self.ui.tool_offset_lbl.hide()
  940. # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list
  941. try:
  942. self.ser_attrs.remove('tools')
  943. except TypeError:
  944. pass
  945. self.ser_attrs.append('tools')
  946. self.ui_connect()
  947. self.build_ui()
  948. self.app.inform.emit('[success] %s' % _("Tool was deleted in Tool Table."))
  949. obj_active = self.app.collection.get_active()
  950. # if the object was MultiGeo and now it has no tool at all (therefore no geometry)
  951. # we make it back SingleGeo
  952. if self.ui.geo_tools_table.rowCount() <= 0:
  953. obj_active.multigeo = False
  954. obj_active.options['xmin'] = 0
  955. obj_active.options['ymin'] = 0
  956. obj_active.options['xmax'] = 0
  957. obj_active.options['ymax'] = 0
  958. if obj_active.multigeo is True:
  959. try:
  960. xmin, ymin, xmax, ymax = obj_active.bounds()
  961. obj_active.options['xmin'] = xmin
  962. obj_active.options['ymin'] = ymin
  963. obj_active.options['xmax'] = xmax
  964. obj_active.options['ymax'] = ymax
  965. except Exception:
  966. obj_active.options['xmin'] = 0
  967. obj_active.options['ymin'] = 0
  968. obj_active.options['xmax'] = 0
  969. obj_active.options['ymax'] = 0
  970. # if there is no tool left in the Tools Table, disable the parameters GUI
  971. if self.ui.geo_tools_table.rowCount() == 0:
  972. self.ui.geo_param_frame.setDisabled(True)
  973. def ui_update_v_shape(self, tool_type_txt):
  974. if tool_type_txt == 'V':
  975. self.ui.tipdialabel.show()
  976. self.ui.tipdia_entry.show()
  977. self.ui.tipanglelabel.show()
  978. self.ui.tipangle_entry.show()
  979. self.ui.cutz_entry.setDisabled(True)
  980. self.ui.cutzlabel.setToolTip(
  981. _("Disabled because the tool is V-shape.\n"
  982. "For V-shape tools the depth of cut is\n"
  983. "calculated from other parameters like:\n"
  984. "- 'V-tip Angle' -> angle at the tip of the tool\n"
  985. "- 'V-tip Dia' -> diameter at the tip of the tool \n"
  986. "- Tool Dia -> 'Dia' column found in the Tool Table\n"
  987. "NB: a value of zero means that Tool Dia = 'V-tip Dia'")
  988. )
  989. self.ui.cutz_entry.setToolTip(
  990. _("Disabled because the tool is V-shape.\n"
  991. "For V-shape tools the depth of cut is\n"
  992. "calculated from other parameters like:\n"
  993. "- 'V-tip Angle' -> angle at the tip of the tool\n"
  994. "- 'V-tip Dia' -> diameter at the tip of the tool \n"
  995. "- Tool Dia -> 'Dia' column found in the Tool Table\n"
  996. "NB: a value of zero means that Tool Dia = 'V-tip Dia'")
  997. )
  998. self.update_cutz()
  999. else:
  1000. self.ui.tipdialabel.hide()
  1001. self.ui.tipdia_entry.hide()
  1002. self.ui.tipanglelabel.hide()
  1003. self.ui.tipangle_entry.hide()
  1004. self.ui.cutz_entry.setDisabled(False)
  1005. self.ui.cutzlabel.setToolTip(
  1006. _("Cutting depth (negative)\n"
  1007. "below the copper surface.")
  1008. )
  1009. self.ui.cutz_entry.setToolTip('')
  1010. def update_cutz(self):
  1011. vdia = float(self.ui.tipdia_entry.get_value())
  1012. half_vangle = float(self.ui.tipangle_entry.get_value()) / 2
  1013. row = self.ui.geo_tools_table.currentRow()
  1014. tool_uid_item = self.ui.geo_tools_table.item(row, 5)
  1015. if tool_uid_item is None:
  1016. return
  1017. tool_uid = int(tool_uid_item.text())
  1018. tool_dia_item = self.ui.geo_tools_table.item(row, 1)
  1019. if tool_dia_item is None:
  1020. return
  1021. tooldia = float(tool_dia_item.text())
  1022. try:
  1023. new_cutz = (tooldia - vdia) / (2 * math.tan(math.radians(half_vangle)))
  1024. except ZeroDivisionError:
  1025. new_cutz = self.old_cutz
  1026. new_cutz = float('%.*f' % (self.decimals, new_cutz)) * -1.0 # this value has to be negative
  1027. self.ui.cutz_entry.set_value(new_cutz)
  1028. # store the new CutZ value into storage (self.tools)
  1029. for tooluid_key, tooluid_value in self.tools.items():
  1030. if int(tooluid_key) == tool_uid:
  1031. tooluid_value['data']['cutz'] = new_cutz
  1032. def on_tooltable_cellwidget_change(self):
  1033. cw = self.sender()
  1034. # assert isinstance(cw, FCComboBox) or isinstance(cw, FCCheckBox),\
  1035. # "Expected a FCCombobox or a FCCheckbox got %s" % type(cw)
  1036. cw_index = self.ui.geo_tools_table.indexAt(cw.pos())
  1037. cw_row = cw_index.row()
  1038. cw_col = cw_index.column()
  1039. current_uid = int(self.ui.geo_tools_table.item(cw_row, 5).text())
  1040. # store the text of the cellWidget that changed it's index in the self.tools
  1041. for tooluid_key, tooluid_value in self.tools.items():
  1042. if int(tooluid_key) == current_uid:
  1043. cb_txt = cw.currentText()
  1044. if cw_col == 2:
  1045. tooluid_value['offset'] = cb_txt
  1046. if cb_txt == 'Custom':
  1047. self.ui.tool_offset_entry.show()
  1048. self.ui.tool_offset_lbl.show()
  1049. else:
  1050. self.ui.tool_offset_entry.hide()
  1051. self.ui.tool_offset_lbl.hide()
  1052. # reset the offset_value in storage self.tools
  1053. tooluid_value['offset_value'] = 0.0
  1054. elif cw_col == 3:
  1055. # force toolpath type as 'Iso' if the tool type is V-Shape
  1056. if self.ui.geo_tools_table.cellWidget(cw_row, 4).currentText() == 'V':
  1057. tooluid_value['type'] = _('Iso')
  1058. idx = self.ui.geo_tools_table.cellWidget(cw_row, 3).findText(_('Iso'))
  1059. self.ui.geo_tools_table.cellWidget(cw_row, 3).setCurrentIndex(idx)
  1060. else:
  1061. tooluid_value['type'] = cb_txt
  1062. elif cw_col == 4:
  1063. tooluid_value['tool_type'] = cb_txt
  1064. # if the tool_type selected is V-Shape then autoselect the toolpath type as Iso
  1065. if cb_txt == 'V':
  1066. idx = self.ui.geo_tools_table.cellWidget(cw_row, 3).findText(_('Iso'))
  1067. self.ui.geo_tools_table.cellWidget(cw_row, 3).setCurrentIndex(idx)
  1068. else:
  1069. self.ui.cutz_entry.set_value(self.old_cutz)
  1070. self.ui_update_v_shape(tool_type_txt=self.ui.geo_tools_table.cellWidget(cw_row, 4).currentText())
  1071. def update_form(self, dict_storage):
  1072. for form_key in self.form_fields:
  1073. for storage_key in dict_storage:
  1074. if form_key == storage_key:
  1075. try:
  1076. self.form_fields[form_key].set_value(dict_storage[form_key])
  1077. except Exception as e:
  1078. log.debug(str(e))
  1079. # this is done here because those buttons control through OptionalInputSelection if some entry's are Enabled
  1080. # or not. But due of using the ui_disconnect() status is no longer updated and I had to do it here
  1081. self.ui.ois_dwell_geo.on_cb_change()
  1082. self.ui.ois_mpass_geo.on_cb_change()
  1083. self.ui.ois_tcz_geo.on_cb_change()
  1084. def on_apply_param_to_all_clicked(self):
  1085. if self.ui.geo_tools_table.rowCount() == 0:
  1086. # there is no tool in tool table so we can't save the GUI elements values to storage
  1087. log.debug("GeometryObject.gui_form_to_storage() --> no tool in Tools Table, aborting.")
  1088. return
  1089. self.ui_disconnect()
  1090. row = self.ui.geo_tools_table.currentRow()
  1091. if row < 0:
  1092. row = 0
  1093. # store all the data associated with the row parameter to the self.tools storage
  1094. tooldia_item = float(self.ui.geo_tools_table.item(row, 1).text())
  1095. offset_item = self.ui.geo_tools_table.cellWidget(row, 2).currentText()
  1096. type_item = self.ui.geo_tools_table.cellWidget(row, 3).currentText()
  1097. tool_type_item = self.ui.geo_tools_table.cellWidget(row, 4).currentText()
  1098. offset_value_item = float(self.ui.tool_offset_entry.get_value())
  1099. # this new dict will hold the actual useful data, another dict that is the value of key 'data'
  1100. temp_tools = {}
  1101. temp_dia = {}
  1102. temp_data = {}
  1103. for tooluid_key, tooluid_value in self.tools.items():
  1104. for key, value in tooluid_value.items():
  1105. if key == 'tooldia':
  1106. temp_dia[key] = tooldia_item
  1107. # update the 'offset', 'type' and 'tool_type' sections
  1108. if key == 'offset':
  1109. temp_dia[key] = offset_item
  1110. if key == 'type':
  1111. temp_dia[key] = type_item
  1112. if key == 'tool_type':
  1113. temp_dia[key] = tool_type_item
  1114. if key == 'offset_value':
  1115. temp_dia[key] = offset_value_item
  1116. if key == 'data':
  1117. # update the 'data' section
  1118. for data_key in tooluid_value[key].keys():
  1119. for form_key, form_value in self.form_fields.items():
  1120. if form_key == data_key:
  1121. temp_data[data_key] = form_value.get_value()
  1122. # make sure we make a copy of the keys not in the form (we may use 'data' keys that are
  1123. # updated from self.app.defaults
  1124. if data_key not in self.form_fields:
  1125. temp_data[data_key] = value[data_key]
  1126. temp_dia[key] = deepcopy(temp_data)
  1127. temp_data.clear()
  1128. if key == 'solid_geometry':
  1129. temp_dia[key] = deepcopy(self.tools[tooluid_key]['solid_geometry'])
  1130. temp_tools[tooluid_key] = deepcopy(temp_dia)
  1131. self.tools.clear()
  1132. self.tools = deepcopy(temp_tools)
  1133. temp_tools.clear()
  1134. self.ui_connect()
  1135. def gui_form_to_storage(self):
  1136. if self.ui.geo_tools_table.rowCount() == 0:
  1137. # there is no tool in tool table so we can't save the GUI elements values to storage
  1138. log.debug("GeometryObject.gui_form_to_storage() --> no tool in Tools Table, aborting.")
  1139. return
  1140. self.ui_disconnect()
  1141. widget_changed = self.sender()
  1142. try:
  1143. widget_idx = self.ui.grid3.indexOf(widget_changed)
  1144. except Exception:
  1145. return
  1146. # those are the indexes for the V-Tip Dia and V-Tip Angle, if edited calculate the new Cut Z
  1147. if widget_idx == 1 or widget_idx == 3:
  1148. self.update_cutz()
  1149. # the original connect() function of the OptionalInputSelection is no longer working because of the
  1150. # ui_diconnect() so I use this 'hack'
  1151. if isinstance(widget_changed, FCCheckBox):
  1152. if widget_changed.text() == 'Multi-Depth:':
  1153. self.ui.ois_mpass_geo.on_cb_change()
  1154. if widget_changed.text() == 'Tool change':
  1155. self.ui.ois_tcz_geo.on_cb_change()
  1156. if widget_changed.text() == 'Dwell:':
  1157. self.ui.ois_dwell_geo.on_cb_change()
  1158. row = self.ui.geo_tools_table.currentRow()
  1159. if row < 0:
  1160. row = 0
  1161. # store all the data associated with the row parameter to the self.tools storage
  1162. tooldia_item = float(self.ui.geo_tools_table.item(row, 1).text())
  1163. offset_item = self.ui.geo_tools_table.cellWidget(row, 2).currentText()
  1164. type_item = self.ui.geo_tools_table.cellWidget(row, 3).currentText()
  1165. tool_type_item = self.ui.geo_tools_table.cellWidget(row, 4).currentText()
  1166. tooluid_item = int(self.ui.geo_tools_table.item(row, 5).text())
  1167. offset_value_item = float(self.ui.tool_offset_entry.get_value())
  1168. # this new dict will hold the actual useful data, another dict that is the value of key 'data'
  1169. temp_tools = {}
  1170. temp_dia = {}
  1171. temp_data = {}
  1172. for tooluid_key, tooluid_value in self.tools.items():
  1173. if int(tooluid_key) == tooluid_item:
  1174. for key, value in tooluid_value.items():
  1175. if key == 'tooldia':
  1176. temp_dia[key] = tooldia_item
  1177. # update the 'offset', 'type' and 'tool_type' sections
  1178. if key == 'offset':
  1179. temp_dia[key] = offset_item
  1180. if key == 'type':
  1181. temp_dia[key] = type_item
  1182. if key == 'tool_type':
  1183. temp_dia[key] = tool_type_item
  1184. if key == 'offset_value':
  1185. temp_dia[key] = offset_value_item
  1186. if key == 'data':
  1187. # update the 'data' section
  1188. for data_key in tooluid_value[key].keys():
  1189. for form_key, form_value in self.form_fields.items():
  1190. if form_key == data_key:
  1191. temp_data[data_key] = form_value.get_value()
  1192. # make sure we make a copy of the keys not in the form (we may use 'data' keys that are
  1193. # updated from self.app.defaults
  1194. if data_key not in self.form_fields:
  1195. temp_data[data_key] = value[data_key]
  1196. temp_dia[key] = deepcopy(temp_data)
  1197. temp_data.clear()
  1198. if key == 'solid_geometry':
  1199. temp_dia[key] = deepcopy(self.tools[tooluid_key]['solid_geometry'])
  1200. temp_tools[tooluid_key] = deepcopy(temp_dia)
  1201. else:
  1202. temp_tools[tooluid_key] = deepcopy(tooluid_value)
  1203. self.tools.clear()
  1204. self.tools = deepcopy(temp_tools)
  1205. temp_tools.clear()
  1206. self.ui_connect()
  1207. def update_common_param_in_storage(self):
  1208. for tooluid_value in self.tools.values():
  1209. tooluid_value['data']['ppname_g'] = self.ui.pp_geometry_name_cb.get_value()
  1210. def select_tools_table_row(self, row, clearsel=None):
  1211. if clearsel:
  1212. self.ui.geo_tools_table.clearSelection()
  1213. if self.ui.geo_tools_table.rowCount() > 0:
  1214. # self.ui.geo_tools_table.item(row, 0).setSelected(True)
  1215. self.ui.geo_tools_table.setCurrentItem(self.ui.geo_tools_table.item(row, 0))
  1216. def export_dxf(self):
  1217. dwg = None
  1218. try:
  1219. dwg = ezdxf.new('R2010')
  1220. msp = dwg.modelspace()
  1221. def g2dxf(dxf_space, geo_obj):
  1222. if isinstance(geo_obj, MultiPolygon):
  1223. for poly in geo_obj:
  1224. ext_points = list(poly.exterior.coords)
  1225. dxf_space.add_lwpolyline(ext_points)
  1226. for interior in poly.interiors:
  1227. dxf_space.add_lwpolyline(list(interior.coords))
  1228. if isinstance(geo_obj, Polygon):
  1229. ext_points = list(geo_obj.exterior.coords)
  1230. dxf_space.add_lwpolyline(ext_points)
  1231. for interior in geo_obj.interiors:
  1232. dxf_space.add_lwpolyline(list(interior.coords))
  1233. if isinstance(geo_obj, MultiLineString):
  1234. for line in geo_obj:
  1235. dxf_space.add_lwpolyline(list(line.coords))
  1236. if isinstance(geo_obj, LineString) or isinstance(geo_obj, LinearRing):
  1237. dxf_space.add_lwpolyline(list(geo_obj.coords))
  1238. multigeo_solid_geometry = []
  1239. if self.multigeo:
  1240. for tool in self.tools:
  1241. multigeo_solid_geometry += self.tools[tool]['solid_geometry']
  1242. else:
  1243. multigeo_solid_geometry = self.solid_geometry
  1244. for geo in multigeo_solid_geometry:
  1245. if type(geo) == list:
  1246. for g in geo:
  1247. g2dxf(msp, g)
  1248. else:
  1249. g2dxf(msp, geo)
  1250. # points = GeometryObject.get_pts(geo)
  1251. # msp.add_lwpolyline(points)
  1252. except Exception as e:
  1253. log.debug(str(e))
  1254. return dwg
  1255. def get_selected_tools_table_items(self):
  1256. """
  1257. Returns a list of lists, each list in the list is made out of row elements
  1258. :return: List of table_tools items.
  1259. :rtype: list
  1260. """
  1261. table_tools_items = []
  1262. if self.multigeo:
  1263. for x in self.ui.geo_tools_table.selectedItems():
  1264. elem = []
  1265. txt = ''
  1266. for column in range(0, self.ui.geo_tools_table.columnCount()):
  1267. try:
  1268. txt = self.ui.geo_tools_table.item(x.row(), column).text()
  1269. except AttributeError:
  1270. try:
  1271. txt = self.ui.geo_tools_table.cellWidget(x.row(), column).currentText()
  1272. except AttributeError:
  1273. pass
  1274. elem.append(txt)
  1275. table_tools_items.append(deepcopy(elem))
  1276. # table_tools_items.append([self.ui.geo_tools_table.item(x.row(), column).text()
  1277. # for column in range(0, self.ui.geo_tools_table.columnCount())])
  1278. else:
  1279. for x in self.ui.geo_tools_table.selectedItems():
  1280. r = []
  1281. txt = ''
  1282. # the last 2 columns for single-geo geometry are irrelevant and create problems reading
  1283. # so we don't read them
  1284. for column in range(0, self.ui.geo_tools_table.columnCount() - 2):
  1285. # the columns have items that have text but also have items that are widgets
  1286. # for which the text they hold has to be read differently
  1287. try:
  1288. txt = self.ui.geo_tools_table.item(x.row(), column).text()
  1289. except AttributeError:
  1290. try:
  1291. txt = self.ui.geo_tools_table.cellWidget(x.row(), column).currentText()
  1292. except AttributeError:
  1293. pass
  1294. r.append(txt)
  1295. table_tools_items.append(r)
  1296. for item in table_tools_items:
  1297. item[0] = str(item[0])
  1298. return table_tools_items
  1299. def on_pp_changed(self):
  1300. current_pp = self.ui.pp_geometry_name_cb.get_value()
  1301. if current_pp == 'hpgl':
  1302. self.old_pp_state = self.ui.mpass_cb.get_value()
  1303. self.old_toolchangeg_state = self.ui.toolchangeg_cb.get_value()
  1304. self.ui.mpass_cb.set_value(False)
  1305. self.ui.mpass_cb.setDisabled(True)
  1306. self.ui.toolchangeg_cb.set_value(True)
  1307. self.ui.toolchangeg_cb.setDisabled(True)
  1308. else:
  1309. self.ui.mpass_cb.set_value(self.old_pp_state)
  1310. self.ui.mpass_cb.setDisabled(False)
  1311. self.ui.toolchangeg_cb.set_value(self.old_toolchangeg_state)
  1312. self.ui.toolchangeg_cb.setDisabled(False)
  1313. if "toolchange_probe" in current_pp.lower():
  1314. self.ui.pdepth_entry.setVisible(True)
  1315. self.ui.pdepth_label.show()
  1316. self.ui.feedrate_probe_entry.setVisible(True)
  1317. self.ui.feedrate_probe_label.show()
  1318. else:
  1319. self.ui.pdepth_entry.setVisible(False)
  1320. self.ui.pdepth_label.hide()
  1321. self.ui.feedrate_probe_entry.setVisible(False)
  1322. self.ui.feedrate_probe_label.hide()
  1323. if 'marlin' in current_pp.lower() or 'custom' in current_pp.lower():
  1324. self.ui.fr_rapidlabel.show()
  1325. self.ui.feedrate_rapid_entry.show()
  1326. else:
  1327. self.ui.fr_rapidlabel.hide()
  1328. self.ui.feedrate_rapid_entry.hide()
  1329. if 'laser' in current_pp.lower():
  1330. self.ui.cutzlabel.hide()
  1331. self.ui.cutz_entry.hide()
  1332. try:
  1333. self.ui.mpass_cb.hide()
  1334. self.ui.maxdepth_entry.hide()
  1335. except AttributeError:
  1336. pass
  1337. if 'marlin' in current_pp.lower():
  1338. self.ui.travelzlabel.setText('%s:' % _("Focus Z"))
  1339. self.ui.endz_label.show()
  1340. self.ui.endz_entry.show()
  1341. else:
  1342. self.ui.travelzlabel.hide()
  1343. self.ui.travelz_entry.hide()
  1344. self.ui.endz_label.hide()
  1345. self.ui.endz_entry.hide()
  1346. try:
  1347. self.ui.frzlabel.hide()
  1348. self.ui.feedrate_z_entry.hide()
  1349. except AttributeError:
  1350. pass
  1351. self.ui.dwell_cb.hide()
  1352. self.ui.dwelltime_entry.hide()
  1353. self.ui.spindle_label.setText('%s:' % _("Laser Power"))
  1354. try:
  1355. self.ui.tool_offset_label.hide()
  1356. self.ui.offset_entry.hide()
  1357. except AttributeError:
  1358. pass
  1359. else:
  1360. self.ui.cutzlabel.show()
  1361. self.ui.cutz_entry.show()
  1362. try:
  1363. self.ui.mpass_cb.show()
  1364. self.ui.maxdepth_entry.show()
  1365. except AttributeError:
  1366. pass
  1367. self.ui.travelzlabel.setText('%s:' % _('Travel Z'))
  1368. self.ui.travelzlabel.show()
  1369. self.ui.travelz_entry.show()
  1370. self.ui.endz_label.show()
  1371. self.ui.endz_entry.show()
  1372. try:
  1373. self.ui.frzlabel.show()
  1374. self.ui.feedrate_z_entry.show()
  1375. except AttributeError:
  1376. pass
  1377. self.ui.dwell_cb.show()
  1378. self.ui.dwelltime_entry.show()
  1379. self.ui.spindle_label.setText('%s:' % _('Spindle speed'))
  1380. try:
  1381. self.ui.tool_offset_lbl.show()
  1382. self.ui.offset_entry.show()
  1383. except AttributeError:
  1384. pass
  1385. def on_generatecnc_button_click(self, *args):
  1386. log.debug("Generating CNCJob from Geometry ...")
  1387. self.app.defaults.report_usage("geometry_on_generatecnc_button")
  1388. # this reads the values in the UI form to the self.options dictionary
  1389. self.read_form()
  1390. self.sel_tools = {}
  1391. try:
  1392. if self.special_group:
  1393. self.app.inform.emit(
  1394. '[WARNING_NOTCL] %s %s %s.' %
  1395. (_("This Geometry can't be processed because it is"), str(self.special_group), _("geometry"))
  1396. )
  1397. return
  1398. except AttributeError:
  1399. pass
  1400. # test to see if we have tools available in the tool table
  1401. if self.ui.geo_tools_table.selectedItems():
  1402. for x in self.ui.geo_tools_table.selectedItems():
  1403. # try:
  1404. # tooldia = float(self.ui.geo_tools_table.item(x.row(), 1).text())
  1405. # except ValueError:
  1406. # # try to convert comma to decimal point. if it's still not working error message and return
  1407. # try:
  1408. # tooldia = float(self.ui.geo_tools_table.item(x.row(), 1).text().replace(',', '.'))
  1409. # except ValueError:
  1410. # self.app.inform.emit('[ERROR_NOTCL] %s' %
  1411. # _("Wrong value format entered, use a number."))
  1412. # return
  1413. tooluid = int(self.ui.geo_tools_table.item(x.row(), 5).text())
  1414. for tooluid_key, tooluid_value in self.tools.items():
  1415. if int(tooluid_key) == tooluid:
  1416. self.sel_tools.update({
  1417. tooluid: deepcopy(tooluid_value)
  1418. })
  1419. self.mtool_gen_cncjob()
  1420. self.ui.geo_tools_table.clearSelection()
  1421. elif self.ui.geo_tools_table.rowCount() == 1:
  1422. tooluid = int(self.ui.geo_tools_table.item(0, 5).text())
  1423. for tooluid_key, tooluid_value in self.tools.items():
  1424. if int(tooluid_key) == tooluid:
  1425. self.sel_tools.update({
  1426. tooluid: deepcopy(tooluid_value)
  1427. })
  1428. self.mtool_gen_cncjob()
  1429. self.ui.geo_tools_table.clearSelection()
  1430. else:
  1431. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed. No tool selected in the tool table ..."))
  1432. def mtool_gen_cncjob(self, outname=None, tools_dict=None, tools_in_use=None, segx=None, segy=None,
  1433. plot=True, use_thread=True):
  1434. """
  1435. Creates a multi-tool CNCJob out of this Geometry object.
  1436. The actual work is done by the target CNCJobObject object's
  1437. `generate_from_geometry_2()` method.
  1438. :param tools_dict: a dictionary that holds the whole data needed to create the Gcode
  1439. (including the solid_geometry)
  1440. :param tools_in_use: the tools that are used, needed by some preprocessors
  1441. :type list of lists, each list in the list is made out of row elements of tools table from GUI
  1442. :param outname:
  1443. :param tools_dict:
  1444. :param tools_in_use:
  1445. :param segx: number of segments on the X axis, for auto-levelling
  1446. :param segy: number of segments on the Y axis, for auto-levelling
  1447. :param plot: if True the generated object will be plotted; if False will not be plotted
  1448. :param use_thread: if True use threading
  1449. :return: None
  1450. """
  1451. # use the name of the first tool selected in self.geo_tools_table which has the diameter passed as tool_dia
  1452. outname = "%s_%s" % (self.options["name"], 'cnc') if outname is None else outname
  1453. tools_dict = self.sel_tools if tools_dict is None else tools_dict
  1454. tools_in_use = tools_in_use if tools_in_use is not None else self.get_selected_tools_table_items()
  1455. segx = segx if segx is not None else float(self.app.defaults['geometry_segx'])
  1456. segy = segy if segy is not None else float(self.app.defaults['geometry_segy'])
  1457. try:
  1458. xmin = self.options['xmin']
  1459. ymin = self.options['ymin']
  1460. xmax = self.options['xmax']
  1461. ymax = self.options['ymax']
  1462. except Exception as e:
  1463. log.debug("FlatCAMObj.GeometryObject.mtool_gen_cncjob() --> %s\n" % str(e))
  1464. msg = '[ERROR] %s' % _("An internal error has occurred. See shell.\n")
  1465. msg += '%s %s' % ('FlatCAMObj.GeometryObject.mtool_gen_cncjob() -->', str(e))
  1466. msg += traceback.format_exc()
  1467. self.app.inform.emit(msg)
  1468. return
  1469. # Object initialization function for app.new_object()
  1470. # RUNNING ON SEPARATE THREAD!
  1471. def job_init_single_geometry(job_obj, app_obj):
  1472. log.debug("Creating a CNCJob out of a single-geometry")
  1473. assert job_obj.kind == 'cncjob', "Initializer expected a CNCJobObject, got %s" % type(job_obj)
  1474. job_obj.options['xmin'] = xmin
  1475. job_obj.options['ymin'] = ymin
  1476. job_obj.options['xmax'] = xmax
  1477. job_obj.options['ymax'] = ymax
  1478. # count the tools
  1479. tool_cnt = 0
  1480. dia_cnc_dict = {}
  1481. # this turn on the FlatCAMCNCJob plot for multiple tools
  1482. job_obj.multitool = True
  1483. job_obj.multigeo = False
  1484. job_obj.cnc_tools.clear()
  1485. job_obj.options['Tools_in_use'] = tools_in_use
  1486. job_obj.segx = segx if segx else float(self.app.defaults["geometry_segx"])
  1487. job_obj.segy = segy if segy else float(self.app.defaults["geometry_segy"])
  1488. job_obj.z_pdepth = float(self.app.defaults["geometry_z_pdepth"])
  1489. job_obj.feedrate_probe = float(self.app.defaults["geometry_feedrate_probe"])
  1490. for tooluid_key in list(tools_dict.keys()):
  1491. tool_cnt += 1
  1492. dia_cnc_dict = deepcopy(tools_dict[tooluid_key])
  1493. tooldia_val = float('%.*f' % (self.decimals, float(tools_dict[tooluid_key]['tooldia'])))
  1494. dia_cnc_dict.update({
  1495. 'tooldia': tooldia_val
  1496. })
  1497. if dia_cnc_dict['offset'] == 'in':
  1498. tool_offset = -dia_cnc_dict['tooldia'] / 2
  1499. elif dia_cnc_dict['offset'].lower() == 'out':
  1500. tool_offset = dia_cnc_dict['tooldia'] / 2
  1501. elif dia_cnc_dict['offset'].lower() == 'custom':
  1502. try:
  1503. offset_value = float(self.ui.tool_offset_entry.get_value())
  1504. except ValueError:
  1505. # try to convert comma to decimal point. if it's still not working error message and return
  1506. try:
  1507. offset_value = float(self.ui.tool_offset_entry.get_value().replace(',', '.'))
  1508. except ValueError:
  1509. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number."))
  1510. return
  1511. if offset_value:
  1512. tool_offset = float(offset_value)
  1513. else:
  1514. self.app.inform.emit(
  1515. '[WARNING] %s' % _("Tool Offset is selected in Tool Table but no value is provided.\n"
  1516. "Add a Tool Offset or change the Offset Type.")
  1517. )
  1518. return
  1519. else:
  1520. tool_offset = 0.0
  1521. dia_cnc_dict.update({
  1522. 'offset_value': tool_offset
  1523. })
  1524. z_cut = tools_dict[tooluid_key]['data']["cutz"]
  1525. z_move = tools_dict[tooluid_key]['data']["travelz"]
  1526. feedrate = tools_dict[tooluid_key]['data']["feedrate"]
  1527. feedrate_z = tools_dict[tooluid_key]['data']["feedrate_z"]
  1528. feedrate_rapid = tools_dict[tooluid_key]['data']["feedrate_rapid"]
  1529. multidepth = tools_dict[tooluid_key]['data']["multidepth"]
  1530. extracut = tools_dict[tooluid_key]['data']["extracut"]
  1531. extracut_length = tools_dict[tooluid_key]['data']["extracut_length"]
  1532. depthpercut = tools_dict[tooluid_key]['data']["depthperpass"]
  1533. toolchange = tools_dict[tooluid_key]['data']["toolchange"]
  1534. toolchangez = tools_dict[tooluid_key]['data']["toolchangez"]
  1535. toolchangexy = tools_dict[tooluid_key]['data']["toolchangexy"]
  1536. startz = tools_dict[tooluid_key]['data']["startz"]
  1537. endz = tools_dict[tooluid_key]['data']["endz"]
  1538. endxy = self.options["endxy"]
  1539. spindlespeed = tools_dict[tooluid_key]['data']["spindlespeed"]
  1540. dwell = tools_dict[tooluid_key]['data']["dwell"]
  1541. dwelltime = tools_dict[tooluid_key]['data']["dwelltime"]
  1542. pp_geometry_name = tools_dict[tooluid_key]['data']["ppname_g"]
  1543. spindledir = self.app.defaults['geometry_spindledir']
  1544. tool_solid_geometry = self.solid_geometry
  1545. job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"]
  1546. job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"]
  1547. # Propagate options
  1548. job_obj.options["tooldia"] = tooldia_val
  1549. job_obj.options['type'] = 'Geometry'
  1550. job_obj.options['tool_dia'] = tooldia_val
  1551. # it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially
  1552. # to a value of 0.0005 which is 20 times less than 0.01
  1553. tol = float(self.app.defaults['global_tolerance']) / 20
  1554. res = job_obj.generate_from_geometry_2(
  1555. self, tooldia=tooldia_val, offset=tool_offset, tolerance=tol,
  1556. z_cut=z_cut, z_move=z_move,
  1557. feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid,
  1558. spindlespeed=spindlespeed, spindledir=spindledir, dwell=dwell, dwelltime=dwelltime,
  1559. multidepth=multidepth, depthpercut=depthpercut,
  1560. extracut=extracut, extracut_length=extracut_length, startz=startz, endz=endz, endxy=endxy,
  1561. toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy,
  1562. pp_geometry_name=pp_geometry_name,
  1563. tool_no=tool_cnt)
  1564. if res == 'fail':
  1565. log.debug("GeometryObject.mtool_gen_cncjob() --> generate_from_geometry2() failed")
  1566. return 'fail'
  1567. else:
  1568. dia_cnc_dict['gcode'] = res
  1569. # tell gcode_parse from which point to start drawing the lines depending on what kind of
  1570. # object is the source of gcode
  1571. job_obj.toolchange_xy_type = "geometry"
  1572. self.app.inform.emit('[success] %s' % _("G-Code parsing in progress..."))
  1573. dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse()
  1574. self.app.inform.emit('[success] %s' % _("G-Code parsing finished..."))
  1575. # TODO this serve for bounding box creation only; should be optimized
  1576. # commented this; there is no need for the actual GCode geometry - the original one will serve as well
  1577. # for bounding box values
  1578. # dia_cnc_dict['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_cnc_dict['gcode_parsed']])
  1579. try:
  1580. dia_cnc_dict['solid_geometry'] = tool_solid_geometry
  1581. self.app.inform.emit('[success] %s...' % _("Finished G-Code processing"))
  1582. except Exception as er:
  1583. self.app.inform.emit('[ERROR] %s: %s' % (_("G-Code processing failed with error"), str(er)))
  1584. job_obj.cnc_tools.update({
  1585. tooluid_key: deepcopy(dia_cnc_dict)
  1586. })
  1587. dia_cnc_dict.clear()
  1588. # Object initialization function for app.new_object()
  1589. # RUNNING ON SEPARATE THREAD!
  1590. def job_init_multi_geometry(job_obj, app_obj):
  1591. log.debug("Creating a CNCJob out of a multi-geometry")
  1592. assert job_obj.kind == 'cncjob', "Initializer expected a CNCJobObject, got %s" % type(job_obj)
  1593. job_obj.options['xmin'] = xmin
  1594. job_obj.options['ymin'] = ymin
  1595. job_obj.options['xmax'] = xmax
  1596. job_obj.options['ymax'] = ymax
  1597. # count the tools
  1598. tool_cnt = 0
  1599. dia_cnc_dict = {}
  1600. # this turn on the FlatCAMCNCJob plot for multiple tools
  1601. job_obj.multitool = True
  1602. job_obj.multigeo = True
  1603. job_obj.cnc_tools.clear()
  1604. job_obj.options['Tools_in_use'] = tools_in_use
  1605. job_obj.segx = segx if segx else float(self.app.defaults["geometry_segx"])
  1606. job_obj.segy = segy if segy else float(self.app.defaults["geometry_segy"])
  1607. job_obj.z_pdepth = float(self.app.defaults["geometry_z_pdepth"])
  1608. job_obj.feedrate_probe = float(self.app.defaults["geometry_feedrate_probe"])
  1609. # make sure that trying to make a CNCJob from an empty file is not creating an app crash
  1610. if not self.solid_geometry:
  1611. a = 0
  1612. for tooluid_key in self.tools:
  1613. if self.tools[tooluid_key]['solid_geometry'] is None:
  1614. a += 1
  1615. if a == len(self.tools):
  1616. self.app.inform.emit('[ERROR_NOTCL] %s...' % _('Cancelled. Empty file, it has no geometry'))
  1617. return 'fail'
  1618. for tooluid_key in list(tools_dict.keys()):
  1619. tool_cnt += 1
  1620. dia_cnc_dict = deepcopy(tools_dict[tooluid_key])
  1621. tooldia_val = float('%.*f' % (self.decimals, float(tools_dict[tooluid_key]['tooldia'])))
  1622. dia_cnc_dict.update({
  1623. 'tooldia': tooldia_val
  1624. })
  1625. # find the tool_dia associated with the tooluid_key
  1626. # search in the self.tools for the sel_tool_dia and when found see what tooluid has
  1627. # on the found tooluid in self.tools we also have the solid_geometry that interest us
  1628. # for k, v in self.tools.items():
  1629. # if float('%.*f' % (self.decimals, float(v['tooldia']))) == tooldia_val:
  1630. # current_uid = int(k)
  1631. # break
  1632. if dia_cnc_dict['offset'] == 'in':
  1633. tool_offset = -tooldia_val / 2
  1634. elif dia_cnc_dict['offset'].lower() == 'out':
  1635. tool_offset = tooldia_val / 2
  1636. elif dia_cnc_dict['offset'].lower() == 'custom':
  1637. offset_value = float(self.ui.tool_offset_entry.get_value())
  1638. if offset_value:
  1639. tool_offset = float(offset_value)
  1640. else:
  1641. self.app.inform.emit('[WARNING] %s' %
  1642. _("Tool Offset is selected in Tool Table but "
  1643. "no value is provided.\n"
  1644. "Add a Tool Offset or change the Offset Type."))
  1645. return
  1646. else:
  1647. tool_offset = 0.0
  1648. dia_cnc_dict.update({
  1649. 'offset_value': tool_offset
  1650. })
  1651. z_cut = tools_dict[tooluid_key]['data']["cutz"]
  1652. z_move = tools_dict[tooluid_key]['data']["travelz"]
  1653. feedrate = tools_dict[tooluid_key]['data']["feedrate"]
  1654. feedrate_z = tools_dict[tooluid_key]['data']["feedrate_z"]
  1655. feedrate_rapid = tools_dict[tooluid_key]['data']["feedrate_rapid"]
  1656. multidepth = tools_dict[tooluid_key]['data']["multidepth"]
  1657. extracut = tools_dict[tooluid_key]['data']["extracut"]
  1658. extracut_length = tools_dict[tooluid_key]['data']["extracut_length"]
  1659. depthpercut = tools_dict[tooluid_key]['data']["depthperpass"]
  1660. toolchange = tools_dict[tooluid_key]['data']["toolchange"]
  1661. toolchangez = tools_dict[tooluid_key]['data']["toolchangez"]
  1662. toolchangexy = tools_dict[tooluid_key]['data']["toolchangexy"]
  1663. startz = tools_dict[tooluid_key]['data']["startz"]
  1664. endz = tools_dict[tooluid_key]['data']["endz"]
  1665. endxy = self.options["endxy"]
  1666. spindlespeed = tools_dict[tooluid_key]['data']["spindlespeed"]
  1667. dwell = tools_dict[tooluid_key]['data']["dwell"]
  1668. dwelltime = tools_dict[tooluid_key]['data']["dwelltime"]
  1669. pp_geometry_name = tools_dict[tooluid_key]['data']["ppname_g"]
  1670. spindledir = self.app.defaults['geometry_spindledir']
  1671. tool_solid_geometry = self.tools[tooluid_key]['solid_geometry']
  1672. job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"]
  1673. job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"]
  1674. # Propagate options
  1675. job_obj.options["tooldia"] = tooldia_val
  1676. job_obj.options['type'] = 'Geometry'
  1677. job_obj.options['tool_dia'] = tooldia_val
  1678. # it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially
  1679. # to a value of 0.0005 which is 20 times less than 0.01
  1680. tol = float(self.app.defaults['global_tolerance']) / 20
  1681. res = job_obj.generate_from_multitool_geometry(
  1682. tool_solid_geometry, tooldia=tooldia_val, offset=tool_offset,
  1683. tolerance=tol, z_cut=z_cut, z_move=z_move,
  1684. feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid,
  1685. spindlespeed=spindlespeed, spindledir=spindledir, dwell=dwell, dwelltime=dwelltime,
  1686. multidepth=multidepth, depthpercut=depthpercut,
  1687. extracut=extracut, extracut_length=extracut_length, startz=startz, endz=endz, endxy=endxy,
  1688. toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy,
  1689. pp_geometry_name=pp_geometry_name,
  1690. tool_no=tool_cnt)
  1691. if res == 'fail':
  1692. log.debug("GeometryObject.mtool_gen_cncjob() --> generate_from_geometry2() failed")
  1693. return 'fail'
  1694. else:
  1695. dia_cnc_dict['gcode'] = res
  1696. self.app.inform.emit('[success] %s' % _("G-Code parsing in progress..."))
  1697. dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse()
  1698. self.app.inform.emit('[success] %s' % _("G-Code parsing finished..."))
  1699. # TODO this serve for bounding box creation only; should be optimized
  1700. # commented this; there is no need for the actual GCode geometry - the original one will serve as well
  1701. # for bounding box values
  1702. # geo_for_bound_values = cascaded_union([
  1703. # geo['geom'] for geo in dia_cnc_dict['gcode_parsed'] if geo['geom'].is_valid is True
  1704. # ])
  1705. try:
  1706. dia_cnc_dict['solid_geometry'] = deepcopy(tool_solid_geometry)
  1707. self.app.inform.emit('[success] %s' % _("Finished G-Code processing..."))
  1708. except Exception as ee:
  1709. self.app.inform.emit('[ERROR] %s: %s' % (_("G-Code processing failed with error"), str(ee)))
  1710. # tell gcode_parse from which point to start drawing the lines depending on what kind of
  1711. # object is the source of gcode
  1712. job_obj.toolchange_xy_type = "geometry"
  1713. job_obj.cnc_tools.update({
  1714. tooluid_key: deepcopy(dia_cnc_dict)
  1715. })
  1716. dia_cnc_dict.clear()
  1717. if use_thread:
  1718. # To be run in separate thread
  1719. def job_thread(app_obj):
  1720. if self.multigeo is False:
  1721. with self.app.proc_container.new(_("Generating CNC Code")):
  1722. if app_obj.new_object("cncjob", outname, job_init_single_geometry, plot=plot) != 'fail':
  1723. app_obj.inform.emit('[success] %s: %s' % (_("CNCjob created"), outname))
  1724. else:
  1725. with self.app.proc_container.new(_("Generating CNC Code")):
  1726. if app_obj.new_object("cncjob", outname, job_init_multi_geometry) != 'fail':
  1727. app_obj.inform.emit('[success] %s: %s' % (_("CNCjob created"), outname))
  1728. # Create a promise with the name
  1729. self.app.collection.promise(outname)
  1730. # Send to worker
  1731. self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
  1732. else:
  1733. if self.solid_geometry:
  1734. self.app.new_object("cncjob", outname, job_init_single_geometry, plot=plot)
  1735. else:
  1736. self.app.new_object("cncjob", outname, job_init_multi_geometry, plot=plot)
  1737. def generatecncjob(
  1738. self, outname=None,
  1739. dia=None, offset=None,
  1740. z_cut=None, z_move=None,
  1741. feedrate=None, feedrate_z=None, feedrate_rapid=None,
  1742. spindlespeed=None, dwell=None, dwelltime=None,
  1743. multidepth=None, depthperpass=None,
  1744. toolchange=None, toolchangez=None, toolchangexy=None,
  1745. extracut=None, extracut_length=None, startz=None, endz=None,
  1746. pp=None,
  1747. segx=None, segy=None,
  1748. use_thread=True,
  1749. plot=True):
  1750. """
  1751. Only used for TCL Command.
  1752. Creates a CNCJob out of this Geometry object. The actual
  1753. work is done by the target camlib.CNCjob
  1754. `generate_from_geometry_2()` method.
  1755. :param outname: Name of the new object
  1756. :param dia: Tool diameter
  1757. :param offset:
  1758. :param z_cut: Cut depth (negative value)
  1759. :param z_move: Height of the tool when travelling (not cutting)
  1760. :param feedrate: Feed rate while cutting on X - Y plane
  1761. :param feedrate_z: Feed rate while cutting on Z plane
  1762. :param feedrate_rapid: Feed rate while moving with rapids
  1763. :param spindlespeed: Spindle speed (RPM)
  1764. :param dwell:
  1765. :param dwelltime:
  1766. :param multidepth:
  1767. :param depthperpass:
  1768. :param toolchange:
  1769. :param toolchangez:
  1770. :param toolchangexy:
  1771. :param extracut:
  1772. :param extracut_length:
  1773. :param startz:
  1774. :param endz:
  1775. :param pp: Name of the preprocessor
  1776. :param segx:
  1777. :param segy:
  1778. :param use_thread:
  1779. :param plot:
  1780. :return: None
  1781. """
  1782. tooldia = dia if dia else float(self.options["cnctooldia"])
  1783. outname = outname if outname is not None else self.options["name"]
  1784. z_cut = z_cut if z_cut is not None else float(self.options["cutz"])
  1785. z_move = z_move if z_move is not None else float(self.options["travelz"])
  1786. feedrate = feedrate if feedrate is not None else float(self.options["feedrate"])
  1787. feedrate_z = feedrate_z if feedrate_z is not None else float(self.options["feedrate_z"])
  1788. feedrate_rapid = feedrate_rapid if feedrate_rapid is not None else float(self.options["feedrate_rapid"])
  1789. multidepth = multidepth if multidepth is not None else self.options["multidepth"]
  1790. depthperpass = depthperpass if depthperpass is not None else float(self.options["depthperpass"])
  1791. segx = segx if segx is not None else float(self.app.defaults['geometry_segx'])
  1792. segy = segy if segy is not None else float(self.app.defaults['geometry_segy'])
  1793. extracut = extracut if extracut is not None else float(self.options["extracut"])
  1794. extracut_length = extracut_length if extracut_length is not None else float(self.options["extracut_length"])
  1795. startz = startz if startz is not None else self.options["startz"]
  1796. endz = endz if endz is not None else float(self.options["endz"])
  1797. endxy = self.options["endxy"]
  1798. toolchangez = toolchangez if toolchangez else float(self.options["toolchangez"])
  1799. toolchangexy = toolchangexy if toolchangexy else self.options["toolchangexy"]
  1800. toolchange = toolchange if toolchange else self.options["toolchange"]
  1801. offset = offset if offset else 0.0
  1802. # int or None.
  1803. spindlespeed = spindlespeed if spindlespeed else self.options['spindlespeed']
  1804. dwell = dwell if dwell else self.options["dwell"]
  1805. dwelltime = dwelltime if dwelltime else float(self.options["dwelltime"])
  1806. ppname_g = pp if pp else self.options["ppname_g"]
  1807. # Object initialization function for app.new_object()
  1808. # RUNNING ON SEPARATE THREAD!
  1809. def job_init(job_obj, app_obj):
  1810. assert job_obj.kind == 'cncjob', "Initializer expected a CNCJobObject, got %s" % type(job_obj)
  1811. # Propagate options
  1812. job_obj.options["tooldia"] = tooldia
  1813. job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"]
  1814. job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"]
  1815. job_obj.options['type'] = 'Geometry'
  1816. job_obj.options['tool_dia'] = tooldia
  1817. job_obj.segx = segx
  1818. job_obj.segy = segy
  1819. job_obj.z_pdepth = float(self.options["z_pdepth"])
  1820. job_obj.feedrate_probe = float(self.options["feedrate_probe"])
  1821. job_obj.options['xmin'] = self.options['xmin']
  1822. job_obj.options['ymin'] = self.options['ymin']
  1823. job_obj.options['xmax'] = self.options['xmax']
  1824. job_obj.options['ymax'] = self.options['ymax']
  1825. # it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially
  1826. # to a value of 0.0005 which is 20 times less than 0.01
  1827. tol = float(self.app.defaults['global_tolerance']) / 20
  1828. job_obj.generate_from_geometry_2(
  1829. self, tooldia=tooldia, offset=offset, tolerance=tol,
  1830. z_cut=z_cut, z_move=z_move,
  1831. feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid,
  1832. spindlespeed=spindlespeed, dwell=dwell, dwelltime=dwelltime,
  1833. multidepth=multidepth, depthpercut=depthperpass,
  1834. toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy,
  1835. extracut=extracut, extracut_length=extracut_length, startz=startz, endz=endz, endxy=endxy,
  1836. pp_geometry_name=ppname_g
  1837. )
  1838. # tell gcode_parse from which point to start drawing the lines depending on what kind of object is the
  1839. # source of gcode
  1840. job_obj.toolchange_xy_type = "geometry"
  1841. job_obj.gcode_parse()
  1842. self.app.inform.emit('[success] %s' % _("Finished G-Code processing..."))
  1843. if use_thread:
  1844. # To be run in separate thread
  1845. def job_thread(app_obj):
  1846. with self.app.proc_container.new(_("Generating CNC Code")):
  1847. app_obj.new_object("cncjob", outname, job_init, plot=plot)
  1848. app_obj.inform.emit('[success] %s: %s' % (_("CNCjob created")), outname)
  1849. # Create a promise with the name
  1850. self.app.collection.promise(outname)
  1851. # Send to worker
  1852. self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
  1853. else:
  1854. self.app.new_object("cncjob", outname, job_init, plot=plot)
  1855. # def on_plot_cb_click(self, *args):
  1856. # if self.muted_ui:
  1857. # return
  1858. # self.read_form_item('plot')
  1859. def scale(self, xfactor, yfactor=None, point=None):
  1860. """
  1861. Scales all geometry by a given factor.
  1862. :param xfactor: Factor by which to scale the object's geometry/
  1863. :type xfactor: float
  1864. :param yfactor: Factor by which to scale the object's geometry/
  1865. :type yfactor: float
  1866. :param point: Point around which to scale
  1867. :return: None
  1868. :rtype: None
  1869. """
  1870. log.debug("FlatCAMObj.GeometryObject.scale()")
  1871. try:
  1872. xfactor = float(xfactor)
  1873. except Exception:
  1874. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Scale factor has to be a number: integer or float."))
  1875. return
  1876. if yfactor is None:
  1877. yfactor = xfactor
  1878. else:
  1879. try:
  1880. yfactor = float(yfactor)
  1881. except Exception:
  1882. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Scale factor has to be a number: integer or float."))
  1883. return
  1884. if xfactor == 1 and yfactor == 1:
  1885. return
  1886. if point is None:
  1887. px = 0
  1888. py = 0
  1889. else:
  1890. px, py = point
  1891. self.geo_len = 0
  1892. self.old_disp_number = 0
  1893. self.el_count = 0
  1894. def scale_recursion(geom):
  1895. if type(geom) is list:
  1896. geoms = []
  1897. for local_geom in geom:
  1898. geoms.append(scale_recursion(local_geom))
  1899. return geoms
  1900. else:
  1901. try:
  1902. self.el_count += 1
  1903. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  1904. if self.old_disp_number < disp_number <= 100:
  1905. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  1906. self.old_disp_number = disp_number
  1907. return affinity.scale(geom, xfactor, yfactor, origin=(px, py))
  1908. except AttributeError:
  1909. return geom
  1910. if self.multigeo is True:
  1911. for tool in self.tools:
  1912. # variables to display the percentage of work done
  1913. self.geo_len = 0
  1914. try:
  1915. self.geo_len = len(self.tools[tool]['solid_geometry'])
  1916. except TypeError:
  1917. self.geo_len = 1
  1918. self.old_disp_number = 0
  1919. self.el_count = 0
  1920. self.tools[tool]['solid_geometry'] = scale_recursion(self.tools[tool]['solid_geometry'])
  1921. try:
  1922. # variables to display the percentage of work done
  1923. self.geo_len = 0
  1924. try:
  1925. self.geo_len = len(self.solid_geometry)
  1926. except TypeError:
  1927. self.geo_len = 1
  1928. self.old_disp_number = 0
  1929. self.el_count = 0
  1930. self.solid_geometry = scale_recursion(self.solid_geometry)
  1931. except AttributeError:
  1932. self.solid_geometry = []
  1933. return
  1934. self.app.proc_container.new_text = ''
  1935. self.app.inform.emit('[success] %s' % _("Geometry Scale done."))
  1936. def offset(self, vect):
  1937. """
  1938. Offsets all geometry by a given vector/
  1939. :param vect: (x, y) vector by which to offset the object's geometry.
  1940. :type vect: tuple
  1941. :return: None
  1942. :rtype: None
  1943. """
  1944. log.debug("FlatCAMObj.GeometryObject.offset()")
  1945. try:
  1946. dx, dy = vect
  1947. except TypeError:
  1948. self.app.inform.emit('[ERROR_NOTCL] %s' %
  1949. _("An (x,y) pair of values are needed. "
  1950. "Probable you entered only one value in the Offset field.")
  1951. )
  1952. return
  1953. if dx == 0 and dy == 0:
  1954. return
  1955. self.geo_len = 0
  1956. self.old_disp_number = 0
  1957. self.el_count = 0
  1958. def translate_recursion(geom):
  1959. if type(geom) is list:
  1960. geoms = []
  1961. for local_geom in geom:
  1962. geoms.append(translate_recursion(local_geom))
  1963. return geoms
  1964. else:
  1965. try:
  1966. self.el_count += 1
  1967. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  1968. if self.old_disp_number < disp_number <= 100:
  1969. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  1970. self.old_disp_number = disp_number
  1971. return affinity.translate(geom, xoff=dx, yoff=dy)
  1972. except AttributeError:
  1973. return geom
  1974. if self.multigeo is True:
  1975. for tool in self.tools:
  1976. # variables to display the percentage of work done
  1977. self.geo_len = 0
  1978. try:
  1979. self.geo_len = len(self.tools[tool]['solid_geometry'])
  1980. except TypeError:
  1981. self.geo_len = 1
  1982. self.old_disp_number = 0
  1983. self.el_count = 0
  1984. self.tools[tool]['solid_geometry'] = translate_recursion(self.tools[tool]['solid_geometry'])
  1985. # variables to display the percentage of work done
  1986. self.geo_len = 0
  1987. try:
  1988. self.geo_len = len(self.solid_geometry)
  1989. except TypeError:
  1990. self.geo_len = 1
  1991. self.old_disp_number = 0
  1992. self.el_count = 0
  1993. self.solid_geometry = translate_recursion(self.solid_geometry)
  1994. self.app.proc_container.new_text = ''
  1995. self.app.inform.emit('[success] %s' % _("Geometry Offset done."))
  1996. def convert_units(self, units):
  1997. log.debug("FlatCAMObj.GeometryObject.convert_units()")
  1998. self.ui_disconnect()
  1999. factor = Geometry.convert_units(self, units)
  2000. self.options['cutz'] = float(self.options['cutz']) * factor
  2001. self.options['depthperpass'] = float(self.options['depthperpass']) * factor
  2002. self.options['travelz'] = float(self.options['travelz']) * factor
  2003. self.options['feedrate'] = float(self.options['feedrate']) * factor
  2004. self.options['feedrate_z'] = float(self.options['feedrate_z']) * factor
  2005. self.options['feedrate_rapid'] = float(self.options['feedrate_rapid']) * factor
  2006. self.options['endz'] = float(self.options['endz']) * factor
  2007. # self.options['cnctooldia'] *= factor
  2008. # self.options['painttooldia'] *= factor
  2009. # self.options['paintmargin'] *= factor
  2010. # self.options['paintoverlap'] *= factor
  2011. self.options["toolchangez"] = float(self.options["toolchangez"]) * factor
  2012. if self.app.defaults["geometry_toolchangexy"] == '':
  2013. self.options['toolchangexy'] = "0.0, 0.0"
  2014. else:
  2015. coords_xy = [float(eval(coord)) for coord in self.app.defaults["geometry_toolchangexy"].split(",")]
  2016. if len(coords_xy) < 2:
  2017. self.app.inform.emit('[ERROR] %s' %
  2018. _("The Toolchange X,Y field in Edit -> Preferences "
  2019. "has to be in the format (x, y)\n"
  2020. "but now there is only one value, not two.")
  2021. )
  2022. return 'fail'
  2023. coords_xy[0] *= factor
  2024. coords_xy[1] *= factor
  2025. self.options['toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1])
  2026. if self.options['startz'] is not None:
  2027. self.options['startz'] = float(self.options['startz']) * factor
  2028. param_list = ['cutz', 'depthperpass', 'travelz', 'feedrate', 'feedrate_z', 'feedrate_rapid',
  2029. 'endz', 'toolchangez']
  2030. if isinstance(self, GeometryObject):
  2031. temp_tools_dict = {}
  2032. tool_dia_copy = {}
  2033. data_copy = {}
  2034. for tooluid_key, tooluid_value in self.tools.items():
  2035. for dia_key, dia_value in tooluid_value.items():
  2036. if dia_key == 'tooldia':
  2037. dia_value *= factor
  2038. dia_value = float('%.*f' % (self.decimals, dia_value))
  2039. tool_dia_copy[dia_key] = dia_value
  2040. if dia_key == 'offset':
  2041. tool_dia_copy[dia_key] = dia_value
  2042. if dia_key == 'offset_value':
  2043. dia_value *= factor
  2044. tool_dia_copy[dia_key] = dia_value
  2045. # convert the value in the Custom Tool Offset entry in UI
  2046. custom_offset = None
  2047. try:
  2048. custom_offset = float(self.ui.tool_offset_entry.get_value())
  2049. except ValueError:
  2050. # try to convert comma to decimal point. if it's still not working error message and return
  2051. try:
  2052. custom_offset = float(self.ui.tool_offset_entry.get_value().replace(',', '.'))
  2053. except ValueError:
  2054. self.app.inform.emit('[ERROR_NOTCL] %s' %
  2055. _("Wrong value format entered, use a number."))
  2056. return
  2057. except TypeError:
  2058. pass
  2059. if custom_offset:
  2060. custom_offset *= factor
  2061. self.ui.tool_offset_entry.set_value(custom_offset)
  2062. if dia_key == 'type':
  2063. tool_dia_copy[dia_key] = dia_value
  2064. if dia_key == 'tool_type':
  2065. tool_dia_copy[dia_key] = dia_value
  2066. if dia_key == 'data':
  2067. for data_key, data_value in dia_value.items():
  2068. # convert the form fields that are convertible
  2069. for param in param_list:
  2070. if data_key == param and data_value is not None:
  2071. data_copy[data_key] = data_value * factor
  2072. # copy the other dict entries that are not convertible
  2073. if data_key not in param_list:
  2074. data_copy[data_key] = data_value
  2075. tool_dia_copy[dia_key] = deepcopy(data_copy)
  2076. data_copy.clear()
  2077. temp_tools_dict.update({
  2078. tooluid_key: deepcopy(tool_dia_copy)
  2079. })
  2080. tool_dia_copy.clear()
  2081. self.tools.clear()
  2082. self.tools = deepcopy(temp_tools_dict)
  2083. # if there is a value in the new tool field then convert that one too
  2084. try:
  2085. self.ui.addtool_entry.returnPressed.disconnect()
  2086. except TypeError:
  2087. pass
  2088. tooldia = self.ui.addtool_entry.get_value()
  2089. if tooldia:
  2090. tooldia *= factor
  2091. tooldia = float('%.*f' % (self.decimals, tooldia))
  2092. self.ui.addtool_entry.set_value(tooldia)
  2093. self.ui.addtool_entry.returnPressed.connect(self.on_tool_add)
  2094. return factor
  2095. def plot_element(self, element, color=None, visible=None):
  2096. if color is None:
  2097. color = '#FF0000FF'
  2098. visible = visible if visible else self.options['plot']
  2099. try:
  2100. for sub_el in element:
  2101. self.plot_element(sub_el, color=color)
  2102. except TypeError: # Element is not iterable...
  2103. # if self.app.is_legacy is False:
  2104. self.add_shape(shape=element, color=color, visible=visible, layer=0)
  2105. def plot(self, visible=None, kind=None):
  2106. """
  2107. Plot the object.
  2108. :param visible: Controls if the added shape is visible of not
  2109. :param kind: added so there is no error when a project is loaded and it has both geometry and CNCJob, because
  2110. CNCJob require the 'kind' parameter. Perhaps the FlatCAMObj.plot() has to be rewrited
  2111. :return:
  2112. """
  2113. # Does all the required setup and returns False
  2114. # if the 'ptint' option is set to False.
  2115. if not FlatCAMObj.plot(self):
  2116. return
  2117. try:
  2118. # plot solid geometries found as members of self.tools attribute dict
  2119. # for MultiGeo
  2120. if self.multigeo is True: # geo multi tool usage
  2121. for tooluid_key in self.tools:
  2122. solid_geometry = self.tools[tooluid_key]['solid_geometry']
  2123. self.plot_element(solid_geometry, visible=visible,
  2124. color=self.app.defaults["geometry_plot_line"])
  2125. else:
  2126. # plot solid geometry that may be an direct attribute of the geometry object
  2127. # for SingleGeo
  2128. if self.solid_geometry:
  2129. self.plot_element(self.solid_geometry, visible=visible,
  2130. color=self.app.defaults["geometry_plot_line"])
  2131. # self.plot_element(self.solid_geometry, visible=self.options['plot'])
  2132. self.shapes.redraw()
  2133. except (ObjectDeleted, AttributeError):
  2134. self.shapes.clear(update=True)
  2135. def on_plot_cb_click(self, *args):
  2136. if self.muted_ui:
  2137. return
  2138. self.read_form_item('plot')
  2139. self.plot()
  2140. self.ui_disconnect()
  2141. cb_flag = self.ui.plot_cb.isChecked()
  2142. for row in range(self.ui.geo_tools_table.rowCount()):
  2143. table_cb = self.ui.geo_tools_table.cellWidget(row, 6)
  2144. if cb_flag:
  2145. table_cb.setChecked(True)
  2146. else:
  2147. table_cb.setChecked(False)
  2148. self.ui_connect()
  2149. def on_plot_cb_click_table(self):
  2150. # self.ui.cnc_tools_table.cellWidget(row, 2).widget().setCheckState(QtCore.Qt.Unchecked)
  2151. self.ui_disconnect()
  2152. # cw = self.sender()
  2153. # cw_index = self.ui.geo_tools_table.indexAt(cw.pos())
  2154. # cw_row = cw_index.row()
  2155. check_row = 0
  2156. self.shapes.clear(update=True)
  2157. for tooluid_key in self.tools:
  2158. solid_geometry = self.tools[tooluid_key]['solid_geometry']
  2159. # find the geo_tool_table row associated with the tooluid_key
  2160. for row in range(self.ui.geo_tools_table.rowCount()):
  2161. tooluid_item = int(self.ui.geo_tools_table.item(row, 5).text())
  2162. if tooluid_item == int(tooluid_key):
  2163. check_row = row
  2164. break
  2165. if self.ui.geo_tools_table.cellWidget(check_row, 6).isChecked():
  2166. self.plot_element(element=solid_geometry, visible=True)
  2167. self.shapes.redraw()
  2168. # make sure that the general plot is disabled if one of the row plot's are disabled and
  2169. # if all the row plot's are enabled also enable the general plot checkbox
  2170. cb_cnt = 0
  2171. total_row = self.ui.geo_tools_table.rowCount()
  2172. for row in range(total_row):
  2173. if self.ui.geo_tools_table.cellWidget(row, 6).isChecked():
  2174. cb_cnt += 1
  2175. else:
  2176. cb_cnt -= 1
  2177. if cb_cnt < total_row:
  2178. self.ui.plot_cb.setChecked(False)
  2179. else:
  2180. self.ui.plot_cb.setChecked(True)
  2181. self.ui_connect()
  2182. def on_add_area_click(self):
  2183. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area."))
  2184. self.app.call_source = 'geometry'
  2185. if self.app.is_legacy is False:
  2186. self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
  2187. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
  2188. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
  2189. else:
  2190. self.app.plotcanvas.graph_event_disconnect(self.app.mp)
  2191. self.app.plotcanvas.graph_event_disconnect(self.app.mm)
  2192. self.app.plotcanvas.graph_event_disconnect(self.app.mr)
  2193. self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
  2194. self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
  2195. # self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
  2196. # To be called after clicking on the plot.
  2197. def on_mouse_release(self, event):
  2198. if self.app.is_legacy is False:
  2199. event_pos = event.pos
  2200. # event_is_dragging = event.is_dragging
  2201. right_button = 2
  2202. else:
  2203. event_pos = (event.xdata, event.ydata)
  2204. # event_is_dragging = self.app.plotcanvas.is_dragging
  2205. right_button = 3
  2206. event_pos = self.app.plotcanvas.translate_coords(event_pos)
  2207. if self.app.grid_status():
  2208. curr_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
  2209. else:
  2210. curr_pos = (event_pos[0], event_pos[1])
  2211. x1, y1 = curr_pos[0], curr_pos[1]
  2212. shape_type = self.ui.area_shape_radio.get_value()
  2213. # do clear area only for left mouse clicks
  2214. if event.button == 1:
  2215. if shape_type == "square":
  2216. if self.first_click is False:
  2217. self.first_click = True
  2218. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the end point of the area."))
  2219. self.cursor_pos = self.app.plotcanvas.translate_coords(event_pos)
  2220. if self.app.grid_status():
  2221. self.cursor_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
  2222. else:
  2223. self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish."))
  2224. self.app.delete_selection_shape()
  2225. x0, y0 = self.cursor_pos[0], self.cursor_pos[1]
  2226. pt1 = (x0, y0)
  2227. pt2 = (x1, y0)
  2228. pt3 = (x1, y1)
  2229. pt4 = (x0, y1)
  2230. new_rectangle = Polygon([pt1, pt2, pt3, pt4])
  2231. self.exclusion_areas_list.append(new_rectangle)
  2232. # add a temporary shape on canvas
  2233. FlatCAMTool.FlatCAMTool.draw_tool_selection_shape(
  2234. self, old_coords=(x0, y0), coords=(x1, y1),
  2235. color="#FF7400",
  2236. face_color="#FF7400BF",
  2237. shapes_storage=self.exclusion_shapes)
  2238. self.first_click = False
  2239. return
  2240. else:
  2241. self.points.append((x1, y1))
  2242. if len(self.points) > 1:
  2243. self.poly_drawn = True
  2244. self.app.inform.emit(_("Click on next Point or click right mouse button to complete ..."))
  2245. return ""
  2246. elif event.button == right_button and self.mouse_is_dragging is False:
  2247. shape_type = self.ui.area_shape_radio.get_value()
  2248. if shape_type == "square":
  2249. self.first_click = False
  2250. else:
  2251. # if we finish to add a polygon
  2252. if self.poly_drawn is True:
  2253. try:
  2254. # try to add the point where we last clicked if it is not already in the self.points
  2255. last_pt = (x1, y1)
  2256. if last_pt != self.points[-1]:
  2257. self.points.append(last_pt)
  2258. except IndexError:
  2259. pass
  2260. # we need to add a Polygon and a Polygon can be made only from at least 3 points
  2261. if len(self.points) > 2:
  2262. FlatCAMTool.FlatCAMTool.delete_moving_selection_shape(self)
  2263. pol = Polygon(self.points)
  2264. # do not add invalid polygons even if they are drawn by utility geometry
  2265. if pol.is_valid:
  2266. self.exclusion_areas_list.append(pol)
  2267. FlatCAMTool.FlatCAMTool.draw_selection_shape_polygon(
  2268. self, points=self.points,
  2269. color="#FF7400",
  2270. face_color="#FF7400BF",
  2271. shapes_storage=self.exclusion_shapes)
  2272. self.app.inform.emit(
  2273. _("Zone added. Click to start adding next zone or right click to finish."))
  2274. self.points = []
  2275. self.poly_drawn = False
  2276. return
  2277. # FlatCAMTool.FlatCAMTool.delete_tool_selection_shape(self, shapes_storage=self.exclusion_shapes)
  2278. if self.app.is_legacy is False:
  2279. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
  2280. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
  2281. # self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
  2282. else:
  2283. self.app.plotcanvas.graph_event_disconnect(self.mr)
  2284. self.app.plotcanvas.graph_event_disconnect(self.mm)
  2285. # self.app.plotcanvas.graph_event_disconnect(self.kp)
  2286. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
  2287. self.app.on_mouse_click_over_plot)
  2288. self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
  2289. self.app.on_mouse_move_over_plot)
  2290. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  2291. self.app.on_mouse_click_release_over_plot)
  2292. self.app.call_source = 'app'
  2293. if len(self.exclusion_areas_list) == 0:
  2294. return
  2295. else:
  2296. self.exclusion_areas_list = MultiPolygon(self.exclusion_areas_list)
  2297. self.app.inform.emit(
  2298. "[success] %s" % _("Exclusion areas added. Checking overlap with the object geometry ..."))
  2299. if self.exclusion_areas_list.intersects(MultiPolygon(self.solid_geometry)):
  2300. self.exclusion_areas_list = []
  2301. self.app.inform.emit(
  2302. "[ERROR_NOTCL] %s" % _("Failed. Exclusion areas intersects the object geometry ..."))
  2303. return
  2304. else:
  2305. self.app.inform.emit(
  2306. "[success] %s" % _("Exclusion areas added."))
  2307. self.ui.generate_cnc_button.setStyleSheet("""
  2308. QPushButton
  2309. {
  2310. font-weight: bold;
  2311. color: orange;
  2312. }
  2313. """)
  2314. self.ui.generate_cnc_button.setToolTip(
  2315. '%s %s' % (_("Generate the CNC Job object."), _("With Exclusion areas."))
  2316. )
  2317. def area_disconnect(self):
  2318. if self.app.is_legacy is False:
  2319. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
  2320. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
  2321. else:
  2322. self.app.plotcanvas.graph_event_disconnect(self.mr)
  2323. self.app.plotcanvas.graph_event_disconnect(self.mm)
  2324. self.app.plotcanvas.graph_event_disconnect(self.kp)
  2325. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
  2326. self.app.on_mouse_click_over_plot)
  2327. self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
  2328. self.app.on_mouse_move_over_plot)
  2329. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  2330. self.app.on_mouse_click_release_over_plot)
  2331. self.points = []
  2332. self.poly_drawn = False
  2333. self.exclusion_areas_list = []
  2334. FlatCAMTool.FlatCAMTool.delete_moving_selection_shape(self)
  2335. # FlatCAMTool.FlatCAMTool.delete_tool_selection_shape(self, shapes_storage=self.exclusion_shapes)
  2336. self.app.call_source = "app"
  2337. self.app.inform.emit("[WARNING_NOTCL] %s" % _("Cancelled. Area exclusion drawing was interrupted."))
  2338. # called on mouse move
  2339. def on_mouse_move(self, event):
  2340. shape_type = self.ui.area_shape_radio.get_value()
  2341. if self.app.is_legacy is False:
  2342. event_pos = event.pos
  2343. event_is_dragging = event.is_dragging
  2344. # right_button = 2
  2345. else:
  2346. event_pos = (event.xdata, event.ydata)
  2347. event_is_dragging = self.app.plotcanvas.is_dragging
  2348. # right_button = 3
  2349. curr_pos = self.app.plotcanvas.translate_coords(event_pos)
  2350. # detect mouse dragging motion
  2351. if event_is_dragging is True:
  2352. self.mouse_is_dragging = True
  2353. else:
  2354. self.mouse_is_dragging = False
  2355. # update the cursor position
  2356. if self.app.grid_status():
  2357. # Update cursor
  2358. curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
  2359. self.app.app_cursor.set_data(np.asarray([(curr_pos[0], curr_pos[1])]),
  2360. symbol='++', edge_color=self.app.cursor_color_3D,
  2361. edge_width=self.app.defaults["global_cursor_width"],
  2362. size=self.app.defaults["global_cursor_size"])
  2363. # update the positions on status bar
  2364. self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp; "
  2365. "<b>Y</b>: %.4f" % (curr_pos[0], curr_pos[1]))
  2366. if self.cursor_pos is None:
  2367. self.cursor_pos = (0, 0)
  2368. self.app.dx = curr_pos[0] - float(self.cursor_pos[0])
  2369. self.app.dy = curr_pos[1] - float(self.cursor_pos[1])
  2370. self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp; <b>Dy</b>: "
  2371. "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (self.app.dx, self.app.dy))
  2372. # draw the utility geometry
  2373. if shape_type == "square":
  2374. if self.first_click:
  2375. self.app.delete_selection_shape()
  2376. self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]),
  2377. color="#FF7400",
  2378. face_color="#FF7400BF",
  2379. coords=(curr_pos[0], curr_pos[1]))
  2380. else:
  2381. FlatCAMTool.FlatCAMTool.delete_moving_selection_shape(self)
  2382. FlatCAMTool.FlatCAMTool.draw_moving_selection_shape_poly(
  2383. self, points=self.points,
  2384. color="#FF7400",
  2385. face_color="#FF7400BF",
  2386. data=(curr_pos[0], curr_pos[1]))
  2387. def on_clear_area_click(self):
  2388. self.exclusion_areas_list = []
  2389. FlatCAMTool.FlatCAMTool.delete_moving_selection_shape(self)
  2390. self.app.delete_selection_shape()
  2391. FlatCAMTool.FlatCAMTool.delete_tool_selection_shape(self, shapes_storage=self.exclusion_shapes)
  2392. # restore the default StyleSheet
  2393. self.ui.generate_cnc_button.setStyleSheet("")
  2394. # update the StyleSheet
  2395. self.ui.generate_cnc_button.setStyleSheet("""
  2396. QPushButton
  2397. {
  2398. font-weight: bold;
  2399. }
  2400. """)
  2401. self.ui.generate_cnc_button.setToolTip('%s' % _("Generate the CNC Job object."))
  2402. @staticmethod
  2403. def merge(geo_list, geo_final, multigeo=None):
  2404. """
  2405. Merges the geometry of objects in grb_list into the geometry of geo_final.
  2406. :param geo_list: List of GerberObject Objects to join.
  2407. :param geo_final: Destination GerberObject object.
  2408. :param multigeo: if the merged geometry objects are of type MultiGeo
  2409. :return: None
  2410. """
  2411. if geo_final.solid_geometry is None:
  2412. geo_final.solid_geometry = []
  2413. try:
  2414. __ = iter(geo_final.solid_geometry)
  2415. except TypeError:
  2416. geo_final.solid_geometry = [geo_final.solid_geometry]
  2417. new_solid_geometry = []
  2418. new_options = {}
  2419. new_tools = {}
  2420. for geo_obj in geo_list:
  2421. for option in geo_obj.options:
  2422. if option != 'name':
  2423. try:
  2424. new_options[option] = deepcopy(geo_obj.options[option])
  2425. except Exception as e:
  2426. log.warning("Failed to copy option %s. Error: %s" % (str(option), str(e)))
  2427. # Expand lists
  2428. if type(geo_obj) is list:
  2429. GeometryObject.merge(geo_list=geo_obj, geo_final=geo_final)
  2430. # If not list, just append
  2431. else:
  2432. if multigeo is None or multigeo is False:
  2433. geo_final.multigeo = False
  2434. else:
  2435. geo_final.multigeo = True
  2436. try:
  2437. new_solid_geometry += deepcopy(geo_obj.solid_geometry)
  2438. except Exception as e:
  2439. log.debug("GeometryObject.merge() --> %s" % str(e))
  2440. # find the tool_uid maximum value in the geo_final
  2441. try:
  2442. max_uid = max([int(i) for i in new_tools.keys()])
  2443. except ValueError:
  2444. max_uid = 0
  2445. # add and merge tools. If what we try to merge as Geometry is Excellon's and/or Gerber's then don't try
  2446. # to merge the obj.tools as it is likely there is none to merge.
  2447. if geo_obj.kind != 'gerber' and geo_obj.kind != 'excellon':
  2448. for tool_uid in geo_obj.tools:
  2449. max_uid += 1
  2450. new_tools[max_uid] = deepcopy(geo_obj.tools[tool_uid])
  2451. geo_final.options.update(new_options)
  2452. geo_final.solid_geometry = new_solid_geometry
  2453. geo_final.tools = new_tools
  2454. @staticmethod
  2455. def get_pts(o):
  2456. """
  2457. Returns a list of all points in the object, where
  2458. the object can be a MultiPolygon, Polygon, Not a polygon, or a list
  2459. of such. Search is done recursively.
  2460. :param: geometric object
  2461. :return: List of points
  2462. :rtype: list
  2463. """
  2464. pts = []
  2465. # Iterable: descend into each item.
  2466. try:
  2467. for subo in o:
  2468. pts += GeometryObject.get_pts(subo)
  2469. # Non-iterable
  2470. except TypeError:
  2471. if o is not None:
  2472. if type(o) == MultiPolygon:
  2473. for poly in o:
  2474. pts += GeometryObject.get_pts(poly)
  2475. # ## Descend into .exerior and .interiors
  2476. elif type(o) == Polygon:
  2477. pts += GeometryObject.get_pts(o.exterior)
  2478. for i in o.interiors:
  2479. pts += GeometryObject.get_pts(i)
  2480. elif type(o) == MultiLineString:
  2481. for line in o:
  2482. pts += GeometryObject.get_pts(line)
  2483. # ## Has .coords: list them.
  2484. else:
  2485. pts += list(o.coords)
  2486. else:
  2487. return
  2488. return pts