FlatCAMGeometry.py 144 KB

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