ToolCutOut.py 106 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # File Author: Marius Adrian Stanciu (c) #
  4. # Date: 3/10/2019 #
  5. # MIT Licence #
  6. # ##########################################################
  7. # ##########################################################
  8. # File Modified : Marcos Dumay de Medeiros #
  9. # Modifications under GPLv3 #
  10. # ##########################################################
  11. from PyQt5 import QtWidgets, QtGui, QtCore
  12. from appTool import AppTool
  13. from appGUI.GUIElements import FCDoubleSpinner, FCCheckBox, RadioSet, FCComboBox, OptionalInputSection, FCButton, \
  14. FCLabel
  15. from shapely.geometry import box, MultiPolygon, Polygon, LineString, LinearRing, MultiLineString
  16. from shapely.ops import unary_union, linemerge
  17. import shapely.affinity as affinity
  18. from matplotlib.backend_bases import KeyEvent as mpl_key_event
  19. from numpy import inf
  20. from copy import deepcopy
  21. import math
  22. import logging
  23. import gettext
  24. import sys
  25. import simplejson as json
  26. import appTranslation as fcTranslate
  27. import builtins
  28. fcTranslate.apply_language('strings')
  29. if '_' not in builtins.__dict__:
  30. _ = gettext.gettext
  31. log = logging.getLogger('base')
  32. settings = QtCore.QSettings("Open Source", "FlatCAM")
  33. if settings.contains("machinist"):
  34. machinist_setting = settings.value('machinist', type=int)
  35. else:
  36. machinist_setting = 0
  37. class CutOut(AppTool):
  38. def __init__(self, app):
  39. AppTool.__init__(self, app)
  40. self.app = app
  41. self.canvas = app.plotcanvas
  42. self.decimals = self.app.decimals
  43. # #############################################################################
  44. # ######################### Tool GUI ##########################################
  45. # #############################################################################
  46. self.ui = CutoutUI(layout=self.layout, app=self.app)
  47. self.toolName = self.ui.toolName
  48. self.cutting_gapsize = 0.0
  49. self.cutting_dia = 0.0
  50. # true if we want to repeat the gap without clicking again on the button
  51. self.repeat_gap = False
  52. self.flat_geometry = []
  53. # this is the Geometry object generated in this class to be used for adding manual gaps
  54. self.man_cutout_obj = None
  55. # if mouse is dragging set the object True
  56. self.mouse_is_dragging = False
  57. # if mouse events are bound to local methods
  58. self.mouse_events_connected = False
  59. # event handlers references
  60. self.kp = None
  61. self.mm = None
  62. self.mr = None
  63. # hold the mouse position here
  64. self.x_pos = None
  65. self.y_pos = None
  66. # store the default data for the resulting Geometry Object
  67. self.default_data = {}
  68. # store the current cursor type to be restored after manual geo
  69. self.old_cursor_type = self.app.defaults["global_cursor_type"]
  70. # store the current selection shape status to be restored after manual geo
  71. self.old_selection_state = self.app.defaults['global_selection_shape']
  72. # store original geometry for manual cutout
  73. self.manual_solid_geo = None
  74. # here will store the original geometry for manual cutout with mouse bytes
  75. self.mb_manual_solid_geo = None
  76. # here will store the geo rests when doing manual cutouts with mouse bites
  77. self.mb_manual_cuts = []
  78. # here store the tool data for the Cutout Tool
  79. self.cut_tool_dict = {}
  80. # Signals
  81. self.ui.ff_cutout_object_btn.clicked.connect(self.on_freeform_cutout)
  82. self.ui.rect_cutout_object_btn.clicked.connect(self.on_rectangular_cutout)
  83. # adding tools
  84. self.ui.add_newtool_button.clicked.connect(lambda: self.on_tool_add())
  85. self.ui.addtool_from_db_btn.clicked.connect(self.on_tool_add_from_db_clicked)
  86. self.ui.type_obj_radio.activated_custom.connect(self.on_type_obj_changed)
  87. self.ui.man_geo_creation_btn.clicked.connect(self.on_manual_geo)
  88. self.ui.man_gaps_creation_btn.clicked.connect(self.on_manual_gap_click)
  89. self.ui.reset_button.clicked.connect(self.set_tool_ui)
  90. def on_type_obj_changed(self, val):
  91. obj_type = {'grb': 0, 'geo': 2}[val]
  92. self.ui.obj_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
  93. self.ui.obj_combo.setCurrentIndex(0)
  94. self.ui.obj_combo.obj_type = {"grb": "Gerber", "geo": "Geometry"}[val]
  95. if val == 'grb':
  96. self.ui.convex_box_label.setDisabled(False)
  97. self.ui.convex_box_cb.setDisabled(False)
  98. else:
  99. self.ui.convex_box_label.setDisabled(True)
  100. self.ui.convex_box_cb.setDisabled(True)
  101. def run(self, toggle=True):
  102. self.app.defaults.report_usage("ToolCutOut()")
  103. if toggle:
  104. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  105. if self.app.ui.splitter.sizes()[0] == 0:
  106. self.app.ui.splitter.setSizes([1, 1])
  107. else:
  108. try:
  109. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  110. # if tab is populated with the tool but it does not have the focus, focus on it
  111. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  112. # focus on Tool Tab
  113. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  114. else:
  115. self.app.ui.splitter.setSizes([0, 1])
  116. except AttributeError:
  117. pass
  118. else:
  119. if self.app.ui.splitter.sizes()[0] == 0:
  120. self.app.ui.splitter.setSizes([1, 1])
  121. AppTool.run(self)
  122. self.set_tool_ui()
  123. self.app.ui.notebook.setTabText(2, _("Cutout Tool"))
  124. def install(self, icon=None, separator=None, **kwargs):
  125. AppTool.install(self, icon, separator, shortcut='Alt+X', **kwargs)
  126. def set_tool_ui(self):
  127. self.reset_fields()
  128. # use the current selected object and make it visible in the object combobox
  129. sel_list = self.app.collection.get_selected()
  130. if len(sel_list) == 1:
  131. active = self.app.collection.get_active()
  132. kind = active.kind
  133. if kind == 'gerber':
  134. self.ui.type_obj_radio.set_value('grb')
  135. else:
  136. self.ui.type_obj_radio.set_value('geo')
  137. # run those once so the obj_type attribute is updated for the FCComboboxes
  138. # so the last loaded object is displayed
  139. if kind == 'gerber':
  140. self.on_type_obj_changed(val='grb')
  141. else:
  142. self.on_type_obj_changed(val='geo')
  143. self.ui.obj_combo.set_value(active.options['name'])
  144. else:
  145. kind = 'gerber'
  146. self.ui.type_obj_radio.set_value('grb')
  147. # run those once so the obj_type attribute is updated for the FCComboboxes
  148. # so the last loaded object is displayed
  149. if kind == 'gerber':
  150. self.on_type_obj_changed(val='grb')
  151. else:
  152. self.on_type_obj_changed(val='geo')
  153. self.ui.dia.set_value(float(self.app.defaults["tools_cutout_tooldia"]))
  154. self.default_data.update({
  155. "plot": True,
  156. "cutz": float(self.app.defaults["geometry_cutz"]),
  157. "multidepth": self.app.defaults["geometry_multidepth"],
  158. "depthperpass": float(self.app.defaults["geometry_depthperpass"]),
  159. "vtipdia": float(self.app.defaults["geometry_vtipdia"]),
  160. "vtipangle": float(self.app.defaults["geometry_vtipangle"]),
  161. "travelz": float(self.app.defaults["geometry_travelz"]),
  162. "feedrate": float(self.app.defaults["geometry_feedrate"]),
  163. "feedrate_z": float(self.app.defaults["geometry_feedrate_z"]),
  164. "feedrate_rapid": float(self.app.defaults["geometry_feedrate_rapid"]),
  165. "spindlespeed": self.app.defaults["geometry_spindlespeed"],
  166. "dwell": self.app.defaults["geometry_dwell"],
  167. "dwelltime": float(self.app.defaults["geometry_dwelltime"]),
  168. "spindledir": self.app.defaults["geometry_spindledir"],
  169. "ppname_g": self.app.defaults["geometry_ppname_g"],
  170. "extracut": self.app.defaults["geometry_extracut"],
  171. "extracut_length": float(self.app.defaults["geometry_extracut_length"]),
  172. "toolchange": self.app.defaults["geometry_toolchange"],
  173. "toolchangexy": self.app.defaults["geometry_toolchangexy"],
  174. "toolchangez": float(self.app.defaults["geometry_toolchangez"]),
  175. "startz": self.app.defaults["geometry_startz"],
  176. "endz": float(self.app.defaults["geometry_endz"]),
  177. "endxy": self.app.defaults["geometry_endxy"],
  178. "area_exclusion": self.app.defaults["geometry_area_exclusion"],
  179. "area_shape": self.app.defaults["geometry_area_shape"],
  180. "area_strategy": self.app.defaults["geometry_area_strategy"],
  181. "area_overz": float(self.app.defaults["geometry_area_overz"]),
  182. "optimization_type": self.app.defaults["geometry_optimization_type"],
  183. # Cutout
  184. "tools_cutout_tooldia": self.app.defaults["tools_cutout_tooldia"],
  185. "tools_cutout_kind": self.app.defaults["tools_cutout_kind"],
  186. "tools_cutout_margin": float(self.app.defaults["tools_cutout_margin"]),
  187. "tools_cutout_z": float(self.app.defaults["tools_cutout_z"]),
  188. "tools_cutout_depthperpass": float(self.app.defaults["tools_cutout_depthperpass"]),
  189. "tools_cutout_mdepth": self.app.defaults["tools_cutout_mdepth"],
  190. "tools_cutout_gapsize": float(self.app.defaults["tools_cutout_gapsize"]),
  191. "tools_cutout_gaps_ff": self.app.defaults["tools_cutout_gaps_ff"],
  192. "tools_cutout_convexshape": self.app.defaults["tools_cutout_convexshape"],
  193. "tools_cutout_big_cursor": self.app.defaults["tools_cutout_big_cursor"],
  194. "tools_cutout_gap_type": self.app.defaults["tools_cutout_gap_type"],
  195. "tools_cutout_gap_depth": float(self.app.defaults["tools_cutout_gap_depth"]),
  196. "tools_cutout_mb_dia": float(self.app.defaults["tools_cutout_mb_dia"]),
  197. "tools_cutout_mb_spacing": float(self.app.defaults["tools_cutout_mb_spacing"]),
  198. })
  199. tool_dia = float(self.app.defaults["tools_cutout_tooldia"])
  200. self.on_tool_add(custom_dia=tool_dia)
  201. def update_ui(self, tool_dict):
  202. self.ui.obj_kind_combo.set_value(self.default_data["tools_cutout_kind"])
  203. self.ui.big_cursor_cb.set_value(self.default_data['tools_cutout_big_cursor'])
  204. # Entries that may be updated from database
  205. self.ui.margin.set_value(float(tool_dict["tools_cutout_margin"]))
  206. self.ui.gapsize.set_value(float(tool_dict["tools_cutout_gapsize"]))
  207. self.ui.gaptype_radio.set_value(tool_dict["tools_cutout_gap_type"])
  208. self.ui.thin_depth_entry.set_value(float(tool_dict["tools_cutout_gap_depth"]))
  209. self.ui.mb_dia_entry.set_value(float(tool_dict["tools_cutout_mb_dia"]))
  210. self.ui.mb_spacing_entry.set_value(float(tool_dict["tools_cutout_mb_spacing"]))
  211. self.ui.convex_box_cb.set_value(tool_dict['tools_cutout_convexshape'])
  212. self.ui.gaps.set_value(tool_dict["tools_cutout_gaps_ff"])
  213. self.ui.cutz_entry.set_value(float(tool_dict["tools_cutout_z"]))
  214. self.ui.mpass_cb.set_value(float(tool_dict["tools_cutout_mdepth"]))
  215. self.ui.maxdepth_entry.set_value(float(tool_dict["tools_cutout_depthperpass"]))
  216. def on_tool_add(self, custom_dia=None):
  217. self.blockSignals(True)
  218. filename = self.app.tools_database_path()
  219. new_tools_dict = deepcopy(self.default_data)
  220. updated_tooldia = None
  221. # determine the new tool diameter
  222. if custom_dia is None:
  223. tool_dia = self.ui.dia.get_value()
  224. else:
  225. tool_dia = custom_dia
  226. if tool_dia is None or tool_dia == 0:
  227. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Please enter a tool diameter with non-zero value, "
  228. "in Float format."))
  229. self.blockSignals(False)
  230. return
  231. truncated_tooldia = self.app.dec_format(tool_dia, self.decimals)
  232. # load the database tools from the file
  233. try:
  234. with open(filename) as f:
  235. tools = f.read()
  236. except IOError:
  237. self.app.log.error("Could not load tools DB file.")
  238. self.app.inform.emit('[ERROR] %s' % _("Could not load Tools DB file."))
  239. self.blockSignals(False)
  240. self.on_tool_default_add(dia=tool_dia)
  241. return
  242. try:
  243. # store here the tools from Tools Database when searching in Tools Database
  244. tools_db_dict = json.loads(tools)
  245. except Exception:
  246. e = sys.exc_info()[0]
  247. self.app.log.error(str(e))
  248. self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
  249. self.blockSignals(False)
  250. self.on_tool_default_add(dia=tool_dia)
  251. return
  252. tool_found = 0
  253. offset = 'Path'
  254. offset_val = 0.0
  255. typ = 'Rough'
  256. tool_type = 'V'
  257. # look in database tools
  258. for db_tool, db_tool_val in tools_db_dict.items():
  259. offset = db_tool_val['offset']
  260. offset_val = db_tool_val['offset_value']
  261. typ = db_tool_val['type']
  262. tool_type = db_tool_val['tool_type']
  263. db_tooldia = db_tool_val['tooldia']
  264. low_limit = float(db_tool_val['data']['tol_min'])
  265. high_limit = float(db_tool_val['data']['tol_max'])
  266. # we need only tool marked for Cutout Tool
  267. if db_tool_val['data']['tool_target'] != _('Cutout'):
  268. continue
  269. # if we find a tool with the same diameter in the Tools DB just update it's data
  270. if truncated_tooldia == db_tooldia:
  271. tool_found += 1
  272. for d in db_tool_val['data']:
  273. if d.find('tools_cutout') == 0:
  274. new_tools_dict[d] = db_tool_val['data'][d]
  275. elif d.find('tools_') == 0:
  276. # don't need data for other App Tools; this tests after 'tools_drill_'
  277. continue
  278. else:
  279. new_tools_dict[d] = db_tool_val['data'][d]
  280. # search for a tool that has a tolerance that the tool fits in
  281. elif high_limit >= truncated_tooldia >= low_limit:
  282. tool_found += 1
  283. updated_tooldia = db_tooldia
  284. for d in db_tool_val['data']:
  285. if d.find('tools_cutout') == 0:
  286. new_tools_dict[d] = db_tool_val['data'][d]
  287. elif d.find('tools_') == 0:
  288. # don't need data for other App Tools; this tests after 'tools_drill_'
  289. continue
  290. else:
  291. new_tools_dict[d] = db_tool_val['data'][d]
  292. # test we found a suitable tool in Tools Database or if multiple ones
  293. if tool_found == 0:
  294. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Tool not in Tools Database. Adding a default tool."))
  295. self.on_tool_default_add()
  296. self.blockSignals(False)
  297. return
  298. if tool_found > 1:
  299. self.app.inform.emit(
  300. '[WARNING_NOTCL] %s' % _("Cancelled.\n"
  301. "Multiple tools for one tool diameter found in Tools Database."))
  302. self.blockSignals(False)
  303. return
  304. # FIXME when the Geometry UI milling functionality will be transferred in the Milling Tool this needs changes
  305. new_tools_dict["tools_cutout_z"] = deepcopy(new_tools_dict["cutz"])
  306. new_tools_dict["tools_cutout_mdepth"] = deepcopy(new_tools_dict["multidepth"])
  307. new_tools_dict["tools_cutout_depthperpass"] = deepcopy(new_tools_dict["depthperpass"])
  308. new_tdia = deepcopy(updated_tooldia) if updated_tooldia is not None else deepcopy(truncated_tooldia)
  309. self.cut_tool_dict.update({
  310. 'tooldia': new_tdia,
  311. 'offset': deepcopy(offset),
  312. 'offset_value': deepcopy(offset_val),
  313. 'type': deepcopy(typ),
  314. 'tool_type': deepcopy(tool_type),
  315. 'data': deepcopy(new_tools_dict),
  316. 'solid_geometry': []
  317. })
  318. self.update_ui(new_tools_dict)
  319. self.blockSignals(False)
  320. self.app.inform.emit('[success] %s' % _("Updated tool from Tools Database."))
  321. def on_tool_default_add(self, dia=None, muted=None):
  322. dia = dia if dia else str(self.app.defaults["tools_cutout_tooldia"])
  323. self.default_data.update({
  324. "plot": True,
  325. "cutz": float(self.app.defaults["geometry_cutz"]),
  326. "multidepth": self.app.defaults["geometry_multidepth"],
  327. "depthperpass": float(self.app.defaults["geometry_depthperpass"]),
  328. "vtipdia": float(self.app.defaults["geometry_vtipdia"]),
  329. "vtipangle": float(self.app.defaults["geometry_vtipangle"]),
  330. "travelz": float(self.app.defaults["geometry_travelz"]),
  331. "feedrate": float(self.app.defaults["geometry_feedrate"]),
  332. "feedrate_z": float(self.app.defaults["geometry_feedrate_z"]),
  333. "feedrate_rapid": float(self.app.defaults["geometry_feedrate_rapid"]),
  334. "spindlespeed": self.app.defaults["geometry_spindlespeed"],
  335. "dwell": self.app.defaults["geometry_dwell"],
  336. "dwelltime": float(self.app.defaults["geometry_dwelltime"]),
  337. "spindledir": self.app.defaults["geometry_spindledir"],
  338. "ppname_g": self.app.defaults["geometry_ppname_g"],
  339. "extracut": self.app.defaults["geometry_extracut"],
  340. "extracut_length": float(self.app.defaults["geometry_extracut_length"]),
  341. "toolchange": self.app.defaults["geometry_toolchange"],
  342. "toolchangexy": self.app.defaults["geometry_toolchangexy"],
  343. "toolchangez": float(self.app.defaults["geometry_toolchangez"]),
  344. "startz": self.app.defaults["geometry_startz"],
  345. "endz": float(self.app.defaults["geometry_endz"]),
  346. "endxy": self.app.defaults["geometry_endxy"],
  347. "area_exclusion": self.app.defaults["geometry_area_exclusion"],
  348. "area_shape": self.app.defaults["geometry_area_shape"],
  349. "area_strategy": self.app.defaults["geometry_area_strategy"],
  350. "area_overz": float(self.app.defaults["geometry_area_overz"]),
  351. "optimization_type": self.app.defaults["geometry_optimization_type"],
  352. # Cutout
  353. "tools_cutout_tooldia": self.app.defaults["tools_cutout_tooldia"],
  354. "tools_cutout_kind": self.app.defaults["tools_cutout_kind"],
  355. "tools_cutout_margin": float(self.app.defaults["tools_cutout_margin"]),
  356. "tools_cutout_z": float(self.app.defaults["tools_cutout_z"]),
  357. "tools_cutout_depthperpass": float(self.app.defaults["tools_cutout_depthperpass"]),
  358. "tools_cutout_mdepth": self.app.defaults["tools_cutout_mdepth"],
  359. "tools_cutout_gapsize": float(self.app.defaults["tools_cutout_gapsize"]),
  360. "tools_cutout_gaps_ff": self.app.defaults["tools_cutout_gaps_ff"],
  361. "tools_cutout_convexshape": self.app.defaults["tools_cutout_convexshape"],
  362. "tools_cutout_big_cursor": self.app.defaults["tools_cutout_big_cursor"],
  363. "tools_cutout_gap_type": self.app.defaults["tools_cutout_gap_type"],
  364. "tools_cutout_gap_depth": float(self.app.defaults["tools_cutout_gap_depth"]),
  365. "tools_cutout_mb_dia": float(self.app.defaults["tools_cutout_mb_dia"]),
  366. "tools_cutout_mb_spacing": float(self.app.defaults["tools_cutout_mb_spacing"]),
  367. })
  368. self.cut_tool_dict.update({
  369. 'tooldia': dia,
  370. 'offset': 'Path',
  371. 'offset_value': 0.0,
  372. 'type': 'Rough',
  373. 'tool_type': 'C1',
  374. 'data': deepcopy(self.default_data),
  375. 'solid_geometry': []
  376. })
  377. self.update_ui(self.default_data)
  378. if muted is None:
  379. self.app.inform.emit('[success] %s' % _("Default tool added."))
  380. def on_cutout_tool_add_from_db_executed(self, tool):
  381. """
  382. Here add the tool from DB in the selected geometry object
  383. :return:
  384. """
  385. if tool['data']['tool_target'] not in [0, 6]: # [General, Cutout Tool]
  386. for idx in range(self.app.ui.plot_tab_area.count()):
  387. if self.app.ui.plot_tab_area.tabText(idx) == _("Tools Database"):
  388. wdg = self.app.ui.plot_tab_area.widget(idx)
  389. wdg.deleteLater()
  390. self.app.ui.plot_tab_area.removeTab(idx)
  391. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Selected tool can't be used here. Pick another."))
  392. return
  393. tool_from_db = deepcopy(self.default_data)
  394. tool_from_db.update(tool)
  395. # FIXME when the Geometry UI milling functionality will be transferred in the Milling Tool this needs changes
  396. tool_from_db['data']["tools_cutout_tooldia"] = deepcopy(tool["tooldia"])
  397. tool_from_db['data']["tools_cutout_z"] = deepcopy(tool_from_db['data']["cutz"])
  398. tool_from_db['data']["tools_cutout_mdepth"] = deepcopy(tool_from_db['data']["multidepth"])
  399. tool_from_db['data']["tools_cutout_depthperpass"] = deepcopy(tool_from_db['data']["depthperpass"])
  400. self.cut_tool_dict.update(tool_from_db)
  401. self.cut_tool_dict['solid_geometry'] = []
  402. self.update_ui(tool_from_db['data'])
  403. self.ui.dia.set_value(float(tool_from_db['data']["tools_cutout_tooldia"]))
  404. for idx in range(self.app.ui.plot_tab_area.count()):
  405. if self.app.ui.plot_tab_area.tabText(idx) == _("Tools Database"):
  406. wdg = self.app.ui.plot_tab_area.widget(idx)
  407. wdg.deleteLater()
  408. self.app.ui.plot_tab_area.removeTab(idx)
  409. self.app.inform.emit('[success] %s' % _("Tool updated from Tools Database."))
  410. def on_tool_from_db_inserted(self, tool):
  411. """
  412. Called from the Tools DB object through a App method when adding a tool from Tools Database
  413. :param tool: a dict with the tool data
  414. :return: None
  415. """
  416. tooldia = float(tool['tooldia'])
  417. truncated_tooldia = self.app.dec_format(tooldia, self.decimals)
  418. self.cutout_tools.update({
  419. 1: {
  420. 'tooldia': truncated_tooldia,
  421. 'offset': tool['offset'],
  422. 'offset_value': tool['offset_value'],
  423. 'type': tool['type'],
  424. 'tool_type': tool['tool_type'],
  425. 'data': deepcopy(tool['data']),
  426. 'solid_geometry': []
  427. }
  428. })
  429. self.cutout_tools[1]['data']['name'] = '_cutout'
  430. return 1
  431. def on_tool_add_from_db_clicked(self):
  432. """
  433. Called when the user wants to add a new tool from Tools Database. It will create the Tools Database object
  434. and display the Tools Database tab in the form needed for the Tool adding
  435. :return: None
  436. """
  437. # if the Tools Database is already opened focus on it
  438. for idx in range(self.app.ui.plot_tab_area.count()):
  439. if self.app.ui.plot_tab_area.tabText(idx) == _("Tools Database"):
  440. self.app.ui.plot_tab_area.setCurrentWidget(self.app.tools_db_tab)
  441. break
  442. ret_val = self.app.on_tools_database(source='cutout')
  443. if ret_val == 'fail':
  444. return
  445. self.app.tools_db_tab.ok_to_add = True
  446. self.app.tools_db_tab.ui.buttons_frame.hide()
  447. self.app.tools_db_tab.ui.add_tool_from_db.show()
  448. self.app.tools_db_tab.ui.cancel_tool_from_db.show()
  449. def on_freeform_cutout(self):
  450. log.debug("Cutout.on_freeform_cutout() was launched ...")
  451. name = self.ui.obj_combo.currentText()
  452. # Get source object.
  453. try:
  454. cutout_obj = self.app.collection.get_by_name(str(name))
  455. except Exception as e:
  456. log.debug("CutOut.on_freeform_cutout() --> %s" % str(e))
  457. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), name))
  458. return "Could not retrieve object: %s" % name
  459. if cutout_obj is None:
  460. self.app.inform.emit('[ERROR_NOTCL] %s' %
  461. _("There is no object selected for Cutout.\nSelect one and try again."))
  462. return
  463. dia = self.ui.dia.get_value()
  464. if 0 in {dia}:
  465. self.app.inform.emit('[WARNING_NOTCL] %s' %
  466. _("Tool Diameter is zero value. Change it to a positive real number."))
  467. return "Tool Diameter is zero value. Change it to a positive real number."
  468. try:
  469. kind = self.ui.obj_kind_combo.get_value()
  470. except ValueError:
  471. return
  472. margin = self.ui.margin.get_value()
  473. try:
  474. gaps = self.ui.gaps.get_value()
  475. except TypeError:
  476. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Number of gaps value is missing. Add it and retry."))
  477. return
  478. if gaps not in ['None', 'LR', 'TB', '2LR', '2TB', '4', '8']:
  479. self.app.inform.emit('[WARNING_NOTCL] %s' %
  480. _("Gaps value can be only one of: 'None', 'lr', 'tb', '2lr', '2tb', 4 or 8.\n"
  481. "Fill in a correct value and retry."))
  482. return
  483. # if cutout_obj.multigeo is True:
  484. # self.app.inform.emit('[ERROR] %s' % _("Cutout operation cannot be done on a multi-geo Geometry.\n"
  485. # "Optionally, this Multi-geo Geometry can be converted to "
  486. # "Single-geo Geometry,\n"
  487. # "and after that perform Cutout."))
  488. # return
  489. def cutout_handler(geom, gapsize):
  490. proc_geometry = []
  491. rest_geometry = []
  492. r_temp_geo = []
  493. initial_geo = deepcopy(geom)
  494. # Get min and max data for each object as we just cut rectangles across X or Y
  495. xxmin, yymin, xxmax, yymax = CutOut.recursive_bounds(geom)
  496. px = 0.5 * (xxmin + xxmax) + margin
  497. py = 0.5 * (yymin + yymax) + margin
  498. lenx = (xxmax - xxmin) + (margin * 2)
  499. leny = (yymax - yymin) + (margin * 2)
  500. if gaps == 'None':
  501. pass
  502. else:
  503. if gaps == '8' or gaps == '2LR':
  504. points = (
  505. xxmin - gapsize, # botleft_x
  506. py - gapsize + leny / 4, # botleft_y
  507. xxmax + gapsize, # topright_x
  508. py + gapsize + leny / 4 # topright_y
  509. )
  510. geom = self.subtract_poly_from_geo(geom, points)
  511. r_temp_geo.append(
  512. self.intersect_geo(initial_geo, box(points[0], points[1], points[2], points[3]))
  513. )
  514. points = (
  515. xxmin - gapsize,
  516. py - gapsize - leny / 4,
  517. xxmax + gapsize,
  518. py + gapsize - leny / 4
  519. )
  520. geom = self.subtract_poly_from_geo(geom, points)
  521. r_temp_geo.append(
  522. self.intersect_geo(initial_geo, box(points[0], points[1], points[2], points[3]))
  523. )
  524. if gaps == '8' or gaps == '2TB':
  525. points = (
  526. px - gapsize + lenx / 4,
  527. yymin - gapsize,
  528. px + gapsize + lenx / 4,
  529. yymax + gapsize
  530. )
  531. geom = self.subtract_poly_from_geo(geom, points)
  532. r_temp_geo.append(
  533. self.intersect_geo(initial_geo, box(points[0], points[1], points[2], points[3]))
  534. )
  535. points = (
  536. px - gapsize - lenx / 4,
  537. yymin - gapsize,
  538. px + gapsize - lenx / 4,
  539. yymax + gapsize
  540. )
  541. geom = self.subtract_poly_from_geo(geom, points)
  542. r_temp_geo.append(
  543. self.intersect_geo(initial_geo, box(points[0], points[1], points[2], points[3]))
  544. )
  545. if gaps == '4' or gaps == 'LR':
  546. points = (
  547. xxmin - gapsize,
  548. py - gapsize,
  549. xxmax + gapsize,
  550. py + gapsize
  551. )
  552. geom = self.subtract_poly_from_geo(geom, points)
  553. r_temp_geo.append(
  554. self.intersect_geo(initial_geo, box(points[0], points[1], points[2], points[3]))
  555. )
  556. if gaps == '4' or gaps == 'TB':
  557. points = (
  558. px - gapsize,
  559. yymin - gapsize,
  560. px + gapsize,
  561. yymax + gapsize
  562. )
  563. geom = self.subtract_poly_from_geo(geom, points)
  564. r_temp_geo.append(
  565. self.intersect_geo(initial_geo, box(points[0], points[1], points[2], points[3]))
  566. )
  567. try:
  568. for g in geom:
  569. if g and not g.is_empty:
  570. proc_geometry.append(g)
  571. except TypeError:
  572. if geom and not geom.is_empty:
  573. proc_geometry.append(geom)
  574. r_temp_geo = CutOut.flatten(r_temp_geo)
  575. for g in r_temp_geo:
  576. if g and not g.is_empty:
  577. rest_geometry.append(g)
  578. return proc_geometry, rest_geometry
  579. with self.app.proc_container.new("Generating Cutout ..."):
  580. outname = cutout_obj.options["name"] + "_cutout"
  581. self.app.collection.promise(outname)
  582. has_mouse_bites = True if self.ui.gaptype_radio.get_value() == 'mb' else False
  583. outname_exc = cutout_obj.options["name"] + "_mouse_bites"
  584. if has_mouse_bites is True:
  585. self.app.collection.promise(outname_exc)
  586. def job_thread(app_obj):
  587. solid_geo = []
  588. gaps_solid_geo = []
  589. mouse_bites_geo = []
  590. convex_box = self.ui.convex_box_cb.get_value()
  591. gapsize = self.ui.gapsize.get_value()
  592. gapsize = gapsize / 2 + (dia / 2)
  593. mb_dia = self.ui.mb_dia_entry.get_value()
  594. mb_buff_val = mb_dia / 2.0
  595. mb_spacing = self.ui.mb_spacing_entry.get_value()
  596. gap_type = self.ui.gaptype_radio.get_value()
  597. thin_entry = self.ui.thin_depth_entry.get_value()
  598. if cutout_obj.kind == 'gerber':
  599. if isinstance(cutout_obj.solid_geometry, list):
  600. cutout_obj.solid_geometry = MultiPolygon(cutout_obj.solid_geometry)
  601. try:
  602. if convex_box:
  603. object_geo = cutout_obj.solid_geometry.convex_hull
  604. else:
  605. object_geo = cutout_obj.solid_geometry
  606. except Exception as err:
  607. log.debug("CutOut.on_freeform_cutout().geo_init() --> %s" % str(err))
  608. object_geo = cutout_obj.solid_geometry
  609. else:
  610. if cutout_obj.multigeo is False:
  611. object_geo = cutout_obj.solid_geometry
  612. else:
  613. # first tool in the tools dict
  614. t_first = list(cutout_obj.tools.keys())[0]
  615. object_geo = cutout_obj.tools[t_first]['solid_geometry']
  616. if kind == 'single':
  617. object_geo = unary_union(object_geo)
  618. # for geo in object_geo:
  619. if cutout_obj.kind == 'gerber':
  620. if isinstance(object_geo, MultiPolygon):
  621. x0, y0, x1, y1 = object_geo.bounds
  622. object_geo = box(x0, y0, x1, y1)
  623. if margin >= 0:
  624. geo_buf = object_geo.buffer(margin + abs(dia / 2))
  625. else:
  626. geo_buf = object_geo.buffer(margin - abs(dia / 2))
  627. geo = geo_buf.exterior
  628. else:
  629. if isinstance(object_geo, MultiPolygon):
  630. x0, y0, x1, y1 = object_geo.bounds
  631. object_geo = box(x0, y0, x1, y1)
  632. geo_buf = object_geo.buffer(0)
  633. geo = geo_buf.exterior
  634. solid_geo, rest_geo = cutout_handler(geom=geo, gapsize=gapsize)
  635. if gap_type == 'bt' and thin_entry != 0:
  636. gaps_solid_geo = rest_geo
  637. else:
  638. try:
  639. __ = iter(object_geo)
  640. except TypeError:
  641. object_geo = [object_geo]
  642. for geom_struct in object_geo:
  643. if cutout_obj.kind == 'gerber':
  644. if margin >= 0:
  645. geom_struct = (geom_struct.buffer(margin + abs(dia / 2))).exterior
  646. else:
  647. geom_struct_buff = geom_struct.buffer(-margin + abs(dia / 2))
  648. geom_struct = geom_struct_buff.interiors
  649. c_geo, r_geo = cutout_handler(geom=geom_struct, gapsize=gapsize)
  650. solid_geo += c_geo
  651. if gap_type == 'bt' and thin_entry != 0:
  652. gaps_solid_geo += r_geo
  653. if not solid_geo:
  654. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
  655. return "fail"
  656. solid_geo = linemerge(solid_geo)
  657. if has_mouse_bites is True:
  658. gapsize -= dia / 2
  659. mb_object_geo = deepcopy(object_geo)
  660. if kind == 'single':
  661. mb_object_geo = unary_union(mb_object_geo)
  662. # for geo in object_geo:
  663. if cutout_obj.kind == 'gerber':
  664. if isinstance(mb_object_geo, MultiPolygon):
  665. x0, y0, x1, y1 = mb_object_geo.bounds
  666. mb_object_geo = box(x0, y0, x1, y1)
  667. if margin >= 0:
  668. geo_buf = mb_object_geo.buffer(margin + mb_buff_val)
  669. else:
  670. geo_buf = mb_object_geo.buffer(margin - mb_buff_val)
  671. mb_geo = geo_buf.exterior
  672. else:
  673. if isinstance(mb_object_geo, MultiPolygon):
  674. x0, y0, x1, y1 = mb_object_geo.bounds
  675. mb_object_geo = box(x0, y0, x1, y1)
  676. geo_buf = mb_object_geo.buffer(0)
  677. mb_geo = geo_buf.exterior
  678. __, rest_geo = cutout_handler(geom=mb_geo, gapsize=gapsize)
  679. mouse_bites_geo = rest_geo
  680. else:
  681. try:
  682. __ = iter(mb_object_geo)
  683. except TypeError:
  684. mb_object_geo = [mb_object_geo]
  685. for mb_geom_struct in mb_object_geo:
  686. if cutout_obj.kind == 'gerber':
  687. if margin >= 0:
  688. mb_geom_struct = mb_geom_struct.buffer(margin + mb_buff_val)
  689. mb_geom_struct = mb_geom_struct.exterior
  690. else:
  691. mb_geom_struct = mb_geom_struct.buffer(-margin + mb_buff_val)
  692. mb_geom_struct = mb_geom_struct.interiors
  693. __, mb_r_geo = cutout_handler(geom=mb_geom_struct, gapsize=gapsize)
  694. mouse_bites_geo += mb_r_geo
  695. # list of Shapely Points to mark the drill points centers
  696. holes = []
  697. for line in mouse_bites_geo:
  698. calc_len = 0
  699. while calc_len < line.length:
  700. holes.append(line.interpolate(calc_len))
  701. calc_len += mb_dia + mb_spacing
  702. def geo_init(geo_obj, app_object):
  703. geo_obj.multigeo = True
  704. geo_obj.solid_geometry = deepcopy(solid_geo)
  705. xmin, ymin, xmax, ymax = CutOut.recursive_bounds(geo_obj.solid_geometry)
  706. geo_obj.options['xmin'] = xmin
  707. geo_obj.options['ymin'] = ymin
  708. geo_obj.options['xmax'] = xmax
  709. geo_obj.options['ymax'] = ymax
  710. geo_obj.options['cnctooldia'] = str(dia)
  711. geo_obj.options['cutz'] = self.ui.cutz_entry.get_value()
  712. geo_obj.options['multidepth'] = self.ui.mpass_cb.get_value()
  713. geo_obj.options['depthperpass'] = self.ui.maxdepth_entry.get_value()
  714. geo_obj.tools[1] = deepcopy(self.cut_tool_dict)
  715. geo_obj.tools[1]['tooldia'] = str(dia)
  716. geo_obj.tools[1]['solid_geometry'] = geo_obj.solid_geometry
  717. geo_obj.tools[1]['data']['name'] = outname
  718. geo_obj.tools[1]['data']['cutz'] = self.ui.cutz_entry.get_value()
  719. geo_obj.tools[1]['data']['multidepth'] = self.ui.mpass_cb.get_value()
  720. geo_obj.tools[1]['data']['depthperpass'] = self.ui.maxdepth_entry.get_value()
  721. if not gaps_solid_geo:
  722. pass
  723. else:
  724. geo_obj.tools[9999] = deepcopy(self.cut_tool_dict)
  725. geo_obj.tools[9999]['tooldia'] = str(dia)
  726. geo_obj.tools[9999]['solid_geometry'] = gaps_solid_geo
  727. geo_obj.tools[9999]['data']['name'] = outname
  728. geo_obj.tools[9999]['data']['cutz'] = self.ui.thin_depth_entry.get_value()
  729. geo_obj.tools[9999]['data']['multidepth'] = self.ui.mpass_cb.get_value()
  730. geo_obj.tools[9999]['data']['depthperpass'] = self.ui.maxdepth_entry.get_value()
  731. # plot this tool in a different color
  732. geo_obj.tools[9999]['data']['override_color'] = "#29a3a3fa"
  733. def excellon_init(exc_obj, app_o):
  734. if not holes:
  735. return 'fail'
  736. tools = {
  737. 1: {
  738. "tooldia": mb_dia,
  739. "drills": holes,
  740. "solid_geometry": []
  741. }
  742. }
  743. exc_obj.tools = tools
  744. exc_obj.create_geometry()
  745. exc_obj.source_file = app_o.f_handlers.export_excellon(obj_name=exc_obj.options['name'],
  746. local_use=exc_obj, filename=None,
  747. use_thread=False)
  748. # calculate the bounds
  749. xmin, ymin, xmax, ymax = CutOut.recursive_bounds(exc_obj.solid_geometry)
  750. exc_obj.options['xmin'] = xmin
  751. exc_obj.options['ymin'] = ymin
  752. exc_obj.options['xmax'] = xmax
  753. exc_obj.options['ymax'] = ymax
  754. try:
  755. if self.ui.gaptype_radio.get_value() == 'mb':
  756. ret = app_obj.app_obj.new_object('excellon', outname_exc, excellon_init)
  757. if ret == 'fail':
  758. app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Mouse bites failed."))
  759. ret = app_obj.app_obj.new_object('geometry', outname, geo_init)
  760. if ret == 'fail':
  761. app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
  762. return
  763. # cutout_obj.plot(plot_tool=1)
  764. app_obj.inform.emit('[success] %s' % _("Any-form Cutout operation finished."))
  765. # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
  766. app_obj.should_we_save = True
  767. except Exception as ee:
  768. log.debug(str(ee))
  769. self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
  770. def on_rectangular_cutout(self):
  771. log.debug("Cutout.on_rectangular_cutout() was launched ...")
  772. name = self.ui.obj_combo.currentText()
  773. # Get source object.
  774. try:
  775. cutout_obj = self.app.collection.get_by_name(str(name))
  776. except Exception as e:
  777. log.debug("CutOut.on_rectangular_cutout() --> %s" % str(e))
  778. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), name))
  779. return "Could not retrieve object: %s" % name
  780. if cutout_obj is None:
  781. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(name)))
  782. dia = float(self.ui.dia.get_value())
  783. if 0 in {dia}:
  784. self.app.inform.emit('[ERROR_NOTCL] %s' %
  785. _("Tool Diameter is zero value. Change it to a positive real number."))
  786. return "Tool Diameter is zero value. Change it to a positive real number."
  787. try:
  788. kind = self.ui.obj_kind_combo.get_value()
  789. except ValueError:
  790. return
  791. margin = self.ui.margin.get_value()
  792. try:
  793. gaps = self.ui.gaps.get_value()
  794. except TypeError:
  795. self.app.inform.emit('[WARNING_NOTCL] %s' %
  796. _("Number of gaps value is missing. Add it and retry."))
  797. return
  798. if gaps not in ['None', 'LR', 'TB', '2LR', '2TB', '4', '8']:
  799. msg = '[WARNING_NOTCL] %s' % _("Gaps value can be only one of: 'None', 'lr', 'tb', '2lr', '2tb', 4 or 8.\n"
  800. "Fill in a correct value and retry.")
  801. self.app.inform.emit(msg)
  802. return
  803. # if cutout_obj.multigeo is True:
  804. # self.app.inform.emit('[ERROR] %s' % _("Cutout operation cannot be done on a multi-geo Geometry.\n"
  805. # "Optionally, this Multi-geo Geometry can be converted to "
  806. # "Single-geo Geometry,\n"
  807. # "and after that perform Cutout."))
  808. # return
  809. def cutout_rect_handler(geom, gapsize, xmin, ymin, xmax, ymax):
  810. proc_geometry = []
  811. px = 0.5 * (xmin + xmax) + margin
  812. py = 0.5 * (ymin + ymax) + margin
  813. lenx = (xmax - xmin) + (margin * 2)
  814. leny = (ymax - ymin) + (margin * 2)
  815. if gaps == 'None':
  816. pass
  817. else:
  818. if gaps == '8' or gaps == '2LR':
  819. points = (
  820. xmin - gapsize, # botleft_x
  821. py - gapsize + leny / 4, # botleft_y
  822. xmax + gapsize, # topright_x
  823. py + gapsize + leny / 4 # topright_y
  824. )
  825. geom = self.subtract_poly_from_geo(geom, points)
  826. points = (
  827. xmin - gapsize,
  828. py - gapsize - leny / 4,
  829. xmax + gapsize,
  830. py + gapsize - leny / 4
  831. )
  832. geom = self.subtract_poly_from_geo(geom, points)
  833. if gaps == '8' or gaps == '2TB':
  834. points = (
  835. px - gapsize + lenx / 4,
  836. ymin - gapsize,
  837. px + gapsize + lenx / 4,
  838. ymax + gapsize
  839. )
  840. geom = self.subtract_poly_from_geo(geom, points)
  841. points = (
  842. px - gapsize - lenx / 4,
  843. ymin - gapsize,
  844. px + gapsize - lenx / 4,
  845. ymax + gapsize
  846. )
  847. geom = self.subtract_poly_from_geo(geom, points)
  848. if gaps == '4' or gaps == 'LR':
  849. points = (
  850. xmin - gapsize,
  851. py - gapsize,
  852. xmax + gapsize,
  853. py + gapsize
  854. )
  855. geom = self.subtract_poly_from_geo(geom, points)
  856. if gaps == '4' or gaps == 'TB':
  857. points = (
  858. px - gapsize,
  859. ymin - gapsize,
  860. px + gapsize,
  861. ymax + gapsize
  862. )
  863. geom = self.subtract_poly_from_geo(geom, points)
  864. try:
  865. for g in geom:
  866. proc_geometry.append(g)
  867. except TypeError:
  868. proc_geometry.append(geom)
  869. return proc_geometry
  870. with self.app.proc_container.new("Generating Cutout ..."):
  871. outname = cutout_obj.options["name"] + "_cutout"
  872. self.app.collection.promise(outname)
  873. has_mouse_bites = True if self.ui.gaptype_radio.get_value() == 'mb' else False
  874. outname_exc = cutout_obj.options["name"] + "_mouse_bites"
  875. if has_mouse_bites is True:
  876. self.app.collection.promise(outname_exc)
  877. def job_thread(app_obj):
  878. solid_geo = []
  879. gaps_solid_geo = []
  880. mouse_bites_geo = []
  881. gapsize = self.ui.gapsize.get_value()
  882. gapsize = gapsize / 2 + (dia / 2)
  883. mb_dia = self.ui.mb_dia_entry.get_value()
  884. mb_buff_val = mb_dia / 2.0
  885. mb_spacing = self.ui.mb_spacing_entry.get_value()
  886. gap_type = self.ui.gaptype_radio.get_value()
  887. thin_entry = self.ui.thin_depth_entry.get_value()
  888. if cutout_obj.multigeo is False:
  889. object_geo = cutout_obj.solid_geometry
  890. else:
  891. # first tool in the tools dict
  892. t_first = list(cutout_obj.tools.keys())[0]
  893. object_geo = cutout_obj.tools[t_first]['solid_geometry']
  894. if kind == 'single':
  895. # fuse the lines
  896. object_geo = unary_union(object_geo)
  897. xmin, ymin, xmax, ymax = object_geo.bounds
  898. geo = box(xmin, ymin, xmax, ymax)
  899. # if Gerber create a buffer at a distance
  900. # if Geometry then cut through the geometry
  901. if cutout_obj.kind == 'gerber':
  902. if margin >= 0:
  903. geo = geo.buffer(margin + abs(dia / 2))
  904. else:
  905. geo = geo.buffer(margin - abs(dia / 2))
  906. solid_geo = cutout_rect_handler(geo, gapsize, xmin, ymin, xmax, ymax)
  907. if gap_type == 'bt' and thin_entry != 0:
  908. gaps_solid_geo = self.subtract_geo(geo, deepcopy(solid_geo))
  909. else:
  910. if cutout_obj.kind == 'geometry':
  911. try:
  912. __ = iter(object_geo)
  913. except TypeError:
  914. object_geo = [object_geo]
  915. for geom_struct in object_geo:
  916. geom_struct = unary_union(geom_struct)
  917. xmin, ymin, xmax, ymax = geom_struct.bounds
  918. geom_struct = box(xmin, ymin, xmax, ymax)
  919. c_geo = cutout_rect_handler(geom_struct, gapsize, xmin, ymin, xmax, ymax)
  920. solid_geo += c_geo
  921. if gap_type == 'bt' and thin_entry != 0:
  922. try:
  923. gaps_solid_geo += self.subtract_geo(geom_struct, c_geo)
  924. except TypeError:
  925. gaps_solid_geo.append(self.subtract_geo(geom_struct, c_geo))
  926. elif cutout_obj.kind == 'gerber' and margin >= 0:
  927. try:
  928. __ = iter(object_geo)
  929. except TypeError:
  930. object_geo = [object_geo]
  931. for geom_struct in object_geo:
  932. geom_struct = unary_union(geom_struct)
  933. xmin, ymin, xmax, ymax = geom_struct.bounds
  934. geom_struct = box(xmin, ymin, xmax, ymax)
  935. geom_struct = geom_struct.buffer(margin + abs(dia / 2))
  936. c_geo = cutout_rect_handler(geom_struct, gapsize, xmin, ymin, xmax, ymax)
  937. solid_geo += c_geo
  938. if gap_type == 'bt' and thin_entry != 0:
  939. try:
  940. gaps_solid_geo += self.subtract_geo(geom_struct, c_geo)
  941. except TypeError:
  942. gaps_solid_geo.append(self.subtract_geo(geom_struct, c_geo))
  943. elif cutout_obj.kind == 'gerber' and margin < 0:
  944. app_obj.inform.emit(
  945. '[WARNING_NOTCL] %s' % _("Rectangular cutout with negative margin is not possible."))
  946. return "fail"
  947. if not solid_geo:
  948. app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
  949. return "fail"
  950. solid_geo = linemerge(solid_geo)
  951. if has_mouse_bites is True:
  952. gapsize -= dia / 2
  953. mb_object_geo = deepcopy(object_geo)
  954. if kind == 'single':
  955. # fuse the lines
  956. mb_object_geo = unary_union(mb_object_geo)
  957. xmin, ymin, xmax, ymax = mb_object_geo.bounds
  958. mb_geo = box(xmin, ymin, xmax, ymax)
  959. # if Gerber create a buffer at a distance
  960. # if Geometry then cut through the geometry
  961. if cutout_obj.kind == 'gerber':
  962. if margin >= 0:
  963. mb_geo = mb_geo.buffer(margin + mb_buff_val)
  964. else:
  965. mb_geo = mb_geo.buffer(margin - mb_buff_val)
  966. else:
  967. mb_geo = mb_geo.buffer(0)
  968. mb_solid_geo = cutout_rect_handler(mb_geo, gapsize, xmin, ymin, xmax, ymax)
  969. mouse_bites_geo = self.subtract_geo(mb_geo, mb_solid_geo)
  970. else:
  971. if cutout_obj.kind == 'geometry':
  972. try:
  973. __ = iter(mb_object_geo)
  974. except TypeError:
  975. mb_object_geo = [mb_object_geo]
  976. for mb_geom_struct in mb_object_geo:
  977. mb_geom_struct = unary_union(mb_geom_struct)
  978. xmin, ymin, xmax, ymax = mb_geom_struct.bounds
  979. mb_geom_struct = box(xmin, ymin, xmax, ymax)
  980. c_geo = cutout_rect_handler(mb_geom_struct, gapsize, xmin, ymin, xmax, ymax)
  981. solid_geo += c_geo
  982. try:
  983. mouse_bites_geo += self.subtract_geo(mb_geom_struct, c_geo)
  984. except TypeError:
  985. mouse_bites_geo.append(self.subtract_geo(mb_geom_struct, c_geo))
  986. elif cutout_obj.kind == 'gerber' and margin >= 0:
  987. try:
  988. __ = iter(mb_object_geo)
  989. except TypeError:
  990. mb_object_geo = [mb_object_geo]
  991. for mb_geom_struct in mb_object_geo:
  992. mb_geom_struct = unary_union(mb_geom_struct)
  993. xmin, ymin, xmax, ymax = mb_geom_struct.bounds
  994. mb_geom_struct = box(xmin, ymin, xmax, ymax)
  995. mb_geom_struct = mb_geom_struct.buffer(margin + mb_buff_val)
  996. c_geo = cutout_rect_handler(mb_geom_struct, gapsize, xmin, ymin, xmax, ymax)
  997. solid_geo += c_geo
  998. try:
  999. mouse_bites_geo += self.subtract_geo(mb_geom_struct, c_geo)
  1000. except TypeError:
  1001. mouse_bites_geo.append(self.subtract_geo(mb_geom_struct, c_geo))
  1002. elif cutout_obj.kind == 'gerber' and margin < 0:
  1003. msg = '[WARNING_NOTCL] %s' % \
  1004. _("Rectangular cutout with negative margin is not possible.")
  1005. app_obj.inform.emit(msg)
  1006. return "fail"
  1007. # list of Shapely Points to mark the drill points centers
  1008. holes = []
  1009. for line in mouse_bites_geo:
  1010. calc_len = 0
  1011. while calc_len < line.length:
  1012. holes.append(line.interpolate(calc_len))
  1013. calc_len += mb_dia + mb_spacing
  1014. def geo_init(geo_obj, application_obj):
  1015. geo_obj.multigeo = True
  1016. geo_obj.solid_geometry = deepcopy(solid_geo)
  1017. geo_obj.options['xmin'] = xmin
  1018. geo_obj.options['ymin'] = ymin
  1019. geo_obj.options['xmax'] = xmax
  1020. geo_obj.options['ymax'] = ymax
  1021. geo_obj.options['cnctooldia'] = str(dia)
  1022. geo_obj.options['cutz'] = self.ui.cutz_entry.get_value()
  1023. geo_obj.options['multidepth'] = self.ui.mpass_cb.get_value()
  1024. geo_obj.options['depthperpass'] = self.ui.maxdepth_entry.get_value()
  1025. geo_obj.tools[1] = deepcopy(self.cut_tool_dict)
  1026. geo_obj.tools[1]['tooldia'] = str(dia)
  1027. geo_obj.tools[1]['solid_geometry'] = geo_obj.solid_geometry
  1028. geo_obj.tools[1]['data']['name'] = outname
  1029. geo_obj.tools[1]['data']['cutz'] = self.ui.cutz_entry.get_value()
  1030. geo_obj.tools[1]['data']['multidepth'] = self.ui.mpass_cb.get_value()
  1031. geo_obj.tools[1]['data']['depthperpass'] = self.ui.maxdepth_entry.get_value()
  1032. if not gaps_solid_geo:
  1033. pass
  1034. else:
  1035. geo_obj.tools[9999] = deepcopy(self.cut_tool_dict)
  1036. geo_obj.tools[9999]['tooldia'] = str(dia)
  1037. geo_obj.tools[9999]['solid_geometry'] = gaps_solid_geo
  1038. geo_obj.tools[9999]['data']['name'] = outname
  1039. geo_obj.tools[9999]['data']['cutz'] = self.ui.thin_depth_entry.get_value()
  1040. geo_obj.tools[9999]['data']['multidepth'] = self.ui.mpass_cb.get_value()
  1041. geo_obj.tools[9999]['data']['depthperpass'] = self.ui.maxdepth_entry.get_value()
  1042. geo_obj.tools[9999]['data']['override_color'] = "#29a3a3fa"
  1043. def excellon_init(exc_obj, app_o):
  1044. if not holes:
  1045. return 'fail'
  1046. tools = {
  1047. 1: {
  1048. "tooldia": mb_dia,
  1049. "drills": holes,
  1050. "solid_geometry": []
  1051. }
  1052. }
  1053. exc_obj.tools = tools
  1054. exc_obj.create_geometry()
  1055. exc_obj.source_file = app_o.f_handlers.export_excellon(obj_name=exc_obj.options['name'],
  1056. local_use=exc_obj,
  1057. filename=None,
  1058. use_thread=False)
  1059. # calculate the bounds
  1060. e_xmin, e_ymin, e_xmax, e_ymax = CutOut.recursive_bounds(exc_obj.solid_geometry)
  1061. exc_obj.options['xmin'] = e_xmin
  1062. exc_obj.options['ymin'] = e_ymin
  1063. exc_obj.options['xmax'] = e_xmax
  1064. exc_obj.options['ymax'] = e_ymax
  1065. try:
  1066. if self.ui.gaptype_radio.get_value() == 'mb':
  1067. ret = app_obj.app_obj.new_object('excellon', outname_exc, excellon_init)
  1068. if ret == 'fail':
  1069. app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Mouse bites failed."))
  1070. ret = app_obj.app_obj.new_object('geometry', outname, geo_init)
  1071. if ret == 'fail':
  1072. app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
  1073. return
  1074. # cutout_obj.plot(plot_tool=1)
  1075. app_obj.inform.emit('[success] %s' % _("Rectangular CutOut operation finished."))
  1076. # self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
  1077. app_obj.should_we_save = True
  1078. except Exception as ee:
  1079. log.debug(str(ee))
  1080. self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
  1081. def on_manual_gap_click(self):
  1082. name = self.ui.man_object_combo.currentText()
  1083. # Get source object.
  1084. try:
  1085. self.man_cutout_obj = self.app.collection.get_by_name(str(name))
  1086. except Exception as e:
  1087. log.debug("CutOut.on_manual_cutout() --> %s" % str(e))
  1088. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), name))
  1089. return
  1090. if self.man_cutout_obj is None:
  1091. self.app.inform.emit('[ERROR_NOTCL] %s: %s' %
  1092. (_("Geometry object for manual cutout not found"), self.man_cutout_obj))
  1093. return
  1094. self.app.inform.emit(_("Click on the selected geometry object perimeter to create a bridge gap ..."))
  1095. self.app.geo_editor.tool_shape.enabled = True
  1096. self.manual_solid_geo = deepcopy(self.flatten(self.man_cutout_obj.solid_geometry))
  1097. self.cutting_dia = self.ui.dia.get_value()
  1098. if 0 in {self.cutting_dia}:
  1099. self.app.inform.emit('[ERROR_NOTCL] %s' %
  1100. _("Tool Diameter is zero value. Change it to a positive real number."))
  1101. return
  1102. if self.ui.gaptype_radio.get_value() == 'mb':
  1103. mb_dia = self.ui.mb_dia_entry.get_value()
  1104. b_dia = (self.cutting_dia / 2.0) - (mb_dia / 2.0)
  1105. self.mb_manual_solid_geo = self.flatten(unary_union(self.manual_solid_geo).buffer(b_dia).interiors)
  1106. self.cutting_gapsize = self.ui.gapsize.get_value()
  1107. name = self.ui.man_object_combo.currentText()
  1108. # Get Geometry source object to be used as target for Manual adding Gaps
  1109. try:
  1110. self.man_cutout_obj = self.app.collection.get_by_name(str(name))
  1111. except Exception as e:
  1112. log.debug("CutOut.on_manual_cutout() --> %s" % str(e))
  1113. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), name))
  1114. return
  1115. if self.app.is_legacy is False:
  1116. self.app.plotcanvas.graph_event_disconnect('key_press', self.app.ui.keyPressEvent)
  1117. self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
  1118. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
  1119. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
  1120. else:
  1121. self.app.plotcanvas.graph_event_disconnect(self.app.kp)
  1122. self.app.plotcanvas.graph_event_disconnect(self.app.mp)
  1123. self.app.plotcanvas.graph_event_disconnect(self.app.mr)
  1124. self.app.plotcanvas.graph_event_disconnect(self.app.mm)
  1125. self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
  1126. self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
  1127. self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
  1128. self.mouse_events_connected = True
  1129. if self.ui.big_cursor_cb.get_value():
  1130. self.old_cursor_type = self.app.defaults["global_cursor_type"]
  1131. self.app.on_cursor_type(val="big")
  1132. self.app.defaults['global_selection_shape'] = False
  1133. def on_manual_cutout(self, click_pos):
  1134. if self.man_cutout_obj is None:
  1135. msg = '[ERROR_NOTCL] %s: %s' % (_("Geometry object for manual cutout not found"), self.man_cutout_obj)
  1136. self.app.inform.emit(msg)
  1137. return
  1138. # use the snapped position as reference
  1139. snapped_pos = self.app.geo_editor.snap(click_pos[0], click_pos[1])
  1140. cut_poly = self.cutting_geo(pos=(snapped_pos[0], snapped_pos[1]))
  1141. gap_type = self.ui.gaptype_radio.get_value()
  1142. gaps_solid_geo = None
  1143. if gap_type == 'bt' and self.ui.thin_depth_entry.get_value() != 0:
  1144. gaps_solid_geo = self.intersect_geo(self.manual_solid_geo, cut_poly)
  1145. if gap_type == 'mb':
  1146. rests_geo = self.intersect_geo(self.mb_manual_solid_geo, cut_poly)
  1147. if isinstance(rests_geo, list):
  1148. self.mb_manual_cuts += rests_geo
  1149. else:
  1150. self.mb_manual_cuts.append(rests_geo)
  1151. # first subtract geometry for the total solid_geometry
  1152. new_solid_geometry = CutOut.subtract_geo(self.man_cutout_obj.solid_geometry, cut_poly)
  1153. new_solid_geometry = linemerge(new_solid_geometry)
  1154. self.man_cutout_obj.solid_geometry = new_solid_geometry
  1155. # then do it on each tool in the manual cutout Geometry object
  1156. try:
  1157. self.man_cutout_obj.multigeo = True
  1158. self.man_cutout_obj.tools[1]['solid_geometry'] = new_solid_geometry
  1159. self.man_cutout_obj.tools[1]['data']['name'] = self.man_cutout_obj.options['name'] + '_cutout'
  1160. self.man_cutout_obj.tools[1]['data']['cutz'] = self.ui.cutz_entry.get_value()
  1161. self.man_cutout_obj.tools[1]['data']['multidepth'] = self.ui.mpass_cb.get_value()
  1162. self.man_cutout_obj.tools[1]['data']['depthperpass'] = self.ui.maxdepth_entry.get_value()
  1163. except KeyError:
  1164. self.app.inform.emit('[ERROR_NOTCL] %s' % _("No tool in the Geometry object."))
  1165. return
  1166. dia = self.ui.dia.get_value()
  1167. if gaps_solid_geo:
  1168. if 9999 not in self.man_cutout_obj.tools:
  1169. self.man_cutout_obj.tools.update({
  1170. 9999: self.cut_tool_dict
  1171. })
  1172. self.man_cutout_obj.tools[9999]['tooldia'] = str(dia)
  1173. self.man_cutout_obj.tools[9999]['solid_geometry'] = [gaps_solid_geo]
  1174. self.man_cutout_obj.tools[9999]['data']['name'] = self.man_cutout_obj.options['name'] + '_cutout'
  1175. self.man_cutout_obj.tools[9999]['data']['cutz'] = self.ui.thin_depth_entry.get_value()
  1176. self.man_cutout_obj.tools[9999]['data']['multidepth'] = self.ui.mpass_cb.get_value()
  1177. self.man_cutout_obj.tools[9999]['data']['depthperpass'] = self.ui.maxdepth_entry.get_value()
  1178. self.man_cutout_obj.tools[9999]['data']['override_color'] = "#29a3a3fa"
  1179. else:
  1180. self.man_cutout_obj.tools[9999]['solid_geometry'].append(gaps_solid_geo)
  1181. self.man_cutout_obj.plot(plot_tool=1)
  1182. self.app.inform.emit('%s' % _("Added manual Bridge Gap. Left click to add another or right click to finish."))
  1183. self.app.should_we_save = True
  1184. def on_manual_geo(self):
  1185. name = self.ui.obj_combo.currentText()
  1186. # Get source object.
  1187. try:
  1188. cutout_obj = self.app.collection.get_by_name(str(name))
  1189. except Exception as e:
  1190. log.debug("CutOut.on_manual_geo() --> %s" % str(e))
  1191. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), name))
  1192. return "Could not retrieve object: %s" % name
  1193. if cutout_obj is None:
  1194. self.app.inform.emit('[ERROR_NOTCL] %s' %
  1195. _("There is no Gerber object selected for Cutout.\n"
  1196. "Select one and try again."))
  1197. return
  1198. if cutout_obj.kind != 'gerber':
  1199. self.app.inform.emit('[ERROR_NOTCL] %s' %
  1200. _("The selected object has to be of Gerber type.\n"
  1201. "Select a Gerber file and try again."))
  1202. return
  1203. dia = float(self.ui.dia.get_value())
  1204. if 0 in {dia}:
  1205. self.app.inform.emit('[ERROR_NOTCL] %s' %
  1206. _("Tool Diameter is zero value. Change it to a positive real number."))
  1207. return
  1208. try:
  1209. kind = self.ui.obj_kind_combo.get_value()
  1210. except ValueError:
  1211. return
  1212. margin = float(self.ui.margin.get_value())
  1213. convex_box = self.ui.convex_box_cb.get_value()
  1214. def geo_init(geo_obj, app_obj):
  1215. geo_union = unary_union(cutout_obj.solid_geometry)
  1216. if convex_box:
  1217. geo = geo_union.convex_hull
  1218. geo_obj.solid_geometry = geo.buffer(margin + abs(dia / 2))
  1219. elif kind == 'single':
  1220. if isinstance(geo_union, Polygon) or \
  1221. (isinstance(geo_union, list) and len(geo_union) == 1) or \
  1222. (isinstance(geo_union, MultiPolygon) and len(geo_union) == 1):
  1223. geo_obj.solid_geometry = geo_union.buffer(margin + abs(dia / 2)).exterior
  1224. elif isinstance(geo_union, MultiPolygon):
  1225. x0, y0, x1, y1 = geo_union.bounds
  1226. geo = box(x0, y0, x1, y1)
  1227. geo_obj.solid_geometry = geo.buffer(margin + abs(dia / 2))
  1228. else:
  1229. app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (
  1230. _("Geometry not supported"), type(geo_union)))
  1231. return 'fail'
  1232. else:
  1233. geo = geo_union
  1234. geo = geo.buffer(margin + abs(dia / 2))
  1235. if isinstance(geo, Polygon):
  1236. geo_obj.solid_geometry = geo.exterior
  1237. elif isinstance(geo, MultiPolygon):
  1238. solid_geo = []
  1239. for poly in geo:
  1240. solid_geo.append(poly.exterior)
  1241. geo_obj.solid_geometry = deepcopy(solid_geo)
  1242. geo_obj.options['cnctooldia'] = str(dia)
  1243. geo_obj.options['cutz'] = self.ui.cutz_entry.get_value()
  1244. geo_obj.options['multidepth'] = self.ui.mpass_cb.get_value()
  1245. geo_obj.options['depthperpass'] = self.ui.maxdepth_entry.get_value()
  1246. geo_obj.multigeo = True
  1247. geo_obj.tools.update({
  1248. 1: self.cut_tool_dict
  1249. })
  1250. geo_obj.tools[1]['tooldia'] = str(dia)
  1251. geo_obj.tools[1]['solid_geometry'] = geo_obj.solid_geometry
  1252. geo_obj.tools[1]['data']['name'] = outname
  1253. geo_obj.tools[1]['data']['cutz'] = self.ui.cutz_entry.get_value()
  1254. geo_obj.tools[1]['data']['multidepth'] = self.ui.mpass_cb.get_value()
  1255. geo_obj.tools[1]['data']['depthperpass'] = self.ui.maxdepth_entry.get_value()
  1256. outname = cutout_obj.options["name"] + "_cutout"
  1257. self.app.app_obj.new_object('geometry', outname, geo_init)
  1258. def cutting_geo(self, pos):
  1259. self.cutting_dia = float(self.ui.dia.get_value())
  1260. self.cutting_gapsize = float(self.ui.gapsize.get_value())
  1261. offset = self.cutting_dia / 2 + self.cutting_gapsize / 2
  1262. # cutting area definition
  1263. orig_x = pos[0]
  1264. orig_y = pos[1]
  1265. xmin = orig_x - offset
  1266. ymin = orig_y - offset
  1267. xmax = orig_x + offset
  1268. ymax = orig_y + offset
  1269. cut_poly = box(xmin, ymin, xmax, ymax)
  1270. return cut_poly
  1271. # To be called after clicking on the plot.
  1272. def on_mouse_click_release(self, event):
  1273. if self.app.is_legacy is False:
  1274. event_pos = event.pos
  1275. # event_is_dragging = event.is_dragging
  1276. right_button = 2
  1277. else:
  1278. event_pos = (event.xdata, event.ydata)
  1279. # event_is_dragging = self.app.plotcanvas.is_dragging
  1280. right_button = 3
  1281. try:
  1282. x = float(event_pos[0])
  1283. y = float(event_pos[1])
  1284. except TypeError:
  1285. return
  1286. event_pos = (x, y)
  1287. # do paint single only for left mouse clicks
  1288. if event.button == 1:
  1289. self.app.inform.emit(_("Making manual bridge gap..."))
  1290. pos = self.app.plotcanvas.translate_coords(event_pos)
  1291. self.on_manual_cutout(click_pos=pos)
  1292. # if RMB then we exit
  1293. elif event.button == right_button and self.mouse_is_dragging is False:
  1294. if self.app.is_legacy is False:
  1295. self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
  1296. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
  1297. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
  1298. else:
  1299. self.app.plotcanvas.graph_event_disconnect(self.kp)
  1300. self.app.plotcanvas.graph_event_disconnect(self.mm)
  1301. self.app.plotcanvas.graph_event_disconnect(self.mr)
  1302. self.app.kp = self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent)
  1303. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot)
  1304. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  1305. self.app.on_mouse_click_release_over_plot)
  1306. self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.app.on_mouse_move_over_plot)
  1307. # Remove any previous utility shape
  1308. self.app.geo_editor.tool_shape.clear(update=True)
  1309. self.app.geo_editor.tool_shape.enabled = False
  1310. # signal that the mouse events are disconnected from local methods
  1311. self.mouse_events_connected = False
  1312. if self.ui.big_cursor_cb.get_value():
  1313. # restore cursor
  1314. self.app.on_cursor_type(val=self.old_cursor_type)
  1315. # restore selection
  1316. self.app.defaults['global_selection_shape'] = self.old_selection_state
  1317. # rebuild the manual Geometry object
  1318. self.man_cutout_obj.build_ui()
  1319. # plot the final object
  1320. self.man_cutout_obj.plot()
  1321. # mouse bytes
  1322. if self.ui.gaptype_radio.get_value() == 'mb':
  1323. with self.app.proc_container.new("Generating Excellon ..."):
  1324. outname_exc = self.man_cutout_obj.options["name"] + "_mouse_bites"
  1325. self.app.collection.promise(outname_exc)
  1326. def job_thread(app_obj):
  1327. # list of Shapely Points to mark the drill points centers
  1328. holes = []
  1329. mb_dia = self.ui.mb_dia_entry.get_value()
  1330. mb_spacing = self.ui.mb_spacing_entry.get_value()
  1331. for line in self.mb_manual_cuts:
  1332. calc_len = 0
  1333. while calc_len < line.length:
  1334. holes.append(line.interpolate(calc_len))
  1335. calc_len += mb_dia + mb_spacing
  1336. self.mb_manual_cuts[:] = []
  1337. def excellon_init(exc_obj, app_o):
  1338. if not holes:
  1339. return 'fail'
  1340. tools = {
  1341. 1: {
  1342. "tooldia": mb_dia,
  1343. "drills": holes,
  1344. "solid_geometry": []
  1345. }
  1346. }
  1347. exc_obj.tools = tools
  1348. exc_obj.create_geometry()
  1349. exc_obj.source_file = app_o.f_handlers.export_excellon(obj_name=exc_obj.options['name'],
  1350. local_use=exc_obj,
  1351. filename=None,
  1352. use_thread=False)
  1353. # calculate the bounds
  1354. xmin, ymin, xmax, ymax = CutOut.recursive_bounds(exc_obj.solid_geometry)
  1355. exc_obj.options['xmin'] = xmin
  1356. exc_obj.options['ymin'] = ymin
  1357. exc_obj.options['xmax'] = xmax
  1358. exc_obj.options['ymax'] = ymax
  1359. ret = app_obj.app_obj.new_object('excellon', outname_exc, excellon_init)
  1360. if ret == 'fail':
  1361. app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Mouse bites failed."))
  1362. self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
  1363. self.app.inform.emit('[success] %s' % _("Finished manual adding of gaps."))
  1364. def on_mouse_move(self, event):
  1365. self.app.on_mouse_move_over_plot(event=event)
  1366. if self.app.is_legacy is False:
  1367. event_pos = event.pos
  1368. event_is_dragging = event.is_dragging
  1369. # right_button = 2
  1370. else:
  1371. event_pos = (event.xdata, event.ydata)
  1372. event_is_dragging = self.app.plotcanvas.is_dragging
  1373. # right_button = 3
  1374. try:
  1375. x = float(event_pos[0])
  1376. y = float(event_pos[1])
  1377. except TypeError:
  1378. return
  1379. event_pos = (x, y)
  1380. pos = self.canvas.translate_coords(event_pos)
  1381. event.xdata, event.ydata = pos[0], pos[1]
  1382. if event_is_dragging is True:
  1383. self.mouse_is_dragging = True
  1384. else:
  1385. self.mouse_is_dragging = False
  1386. try:
  1387. x = float(event.xdata)
  1388. y = float(event.ydata)
  1389. except TypeError:
  1390. return
  1391. if self.app.grid_status():
  1392. snap_x, snap_y = self.app.geo_editor.snap(x, y)
  1393. else:
  1394. snap_x, snap_y = x, y
  1395. self.x_pos, self.y_pos = snap_x, snap_y
  1396. # #################################################
  1397. # ### This section makes the cutting geo to #######
  1398. # ### rotate if it intersects the target geo ######
  1399. # #################################################
  1400. cut_geo = self.cutting_geo(pos=(snap_x, snap_y))
  1401. man_geo = self.man_cutout_obj.solid_geometry
  1402. def get_angle(geo):
  1403. line = cut_geo.intersection(geo)
  1404. try:
  1405. pt1_x = line.coords[0][0]
  1406. pt1_y = line.coords[0][1]
  1407. pt2_x = line.coords[1][0]
  1408. pt2_y = line.coords[1][1]
  1409. dx = pt1_x - pt2_x
  1410. dy = pt1_y - pt2_y
  1411. if dx == 0 or dy == 0:
  1412. angle = 0
  1413. else:
  1414. radian = math.atan(dx / dy)
  1415. angle = radian * 180 / math.pi
  1416. except Exception:
  1417. angle = 0
  1418. return angle
  1419. try:
  1420. rot_angle = 0
  1421. for geo_el in man_geo:
  1422. if isinstance(geo_el, Polygon):
  1423. work_geo = geo_el.exterior
  1424. if cut_geo.intersects(work_geo):
  1425. rot_angle = get_angle(geo=work_geo)
  1426. else:
  1427. rot_angle = 0
  1428. else:
  1429. rot_angle = 0
  1430. if cut_geo.intersects(geo_el):
  1431. rot_angle = get_angle(geo=geo_el)
  1432. if rot_angle != 0:
  1433. break
  1434. except TypeError:
  1435. if isinstance(man_geo, Polygon):
  1436. work_geo = man_geo.exterior
  1437. if cut_geo.intersects(work_geo):
  1438. rot_angle = get_angle(geo=work_geo)
  1439. else:
  1440. rot_angle = 0
  1441. else:
  1442. rot_angle = 0
  1443. if cut_geo.intersects(man_geo):
  1444. rot_angle = get_angle(geo=man_geo)
  1445. # rotate only if there is an angle to rotate to
  1446. if rot_angle != 0:
  1447. cut_geo = affinity.rotate(cut_geo, -rot_angle)
  1448. # Remove any previous utility shape
  1449. self.app.geo_editor.tool_shape.clear(update=True)
  1450. self.draw_utility_geometry(geo=cut_geo)
  1451. def draw_utility_geometry(self, geo):
  1452. self.app.geo_editor.tool_shape.add(
  1453. shape=geo,
  1454. color=(self.app.defaults["global_draw_color"] + '80'),
  1455. update=False,
  1456. layer=0,
  1457. tolerance=None)
  1458. self.app.geo_editor.tool_shape.redraw()
  1459. def on_key_press(self, event):
  1460. # events out of the self.app.collection view (it's about Project Tab) are of type int
  1461. if type(event) is int:
  1462. key = event
  1463. # events from the GUI are of type QKeyEvent
  1464. elif type(event) == QtGui.QKeyEvent:
  1465. key = event.key()
  1466. elif isinstance(event, mpl_key_event): # MatPlotLib key events are trickier to interpret than the rest
  1467. key = event.key
  1468. key = QtGui.QKeySequence(key)
  1469. # check for modifiers
  1470. key_string = key.toString().lower()
  1471. if '+' in key_string:
  1472. mod, __, key_text = key_string.rpartition('+')
  1473. if mod.lower() == 'ctrl':
  1474. # modifiers = QtCore.Qt.ControlModifier
  1475. pass
  1476. elif mod.lower() == 'alt':
  1477. # modifiers = QtCore.Qt.AltModifier
  1478. pass
  1479. elif mod.lower() == 'shift':
  1480. # modifiers = QtCore.Qt.ShiftModifier
  1481. pass
  1482. else:
  1483. # modifiers = QtCore.Qt.NoModifier
  1484. pass
  1485. key = QtGui.QKeySequence(key_text)
  1486. # events from Vispy are of type KeyEvent
  1487. else:
  1488. key = event.key
  1489. # Escape = Deselect All
  1490. if key == QtCore.Qt.Key_Escape or key == 'Escape':
  1491. if self.mouse_events_connected is True:
  1492. self.mouse_events_connected = False
  1493. if self.app.is_legacy is False:
  1494. self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
  1495. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
  1496. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
  1497. else:
  1498. self.app.plotcanvas.graph_event_disconnect(self.kp)
  1499. self.app.plotcanvas.graph_event_disconnect(self.mm)
  1500. self.app.plotcanvas.graph_event_disconnect(self.mr)
  1501. self.app.kp = self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent)
  1502. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot)
  1503. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  1504. self.app.on_mouse_click_release_over_plot)
  1505. self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.app.on_mouse_move_over_plot)
  1506. if self.ui.big_cursor_cb.get_value():
  1507. # restore cursor
  1508. self.app.on_cursor_type(val=self.old_cursor_type)
  1509. # restore selection
  1510. self.app.defaults['global_selection_shape'] = self.old_selection_state
  1511. # Remove any previous utility shape
  1512. self.app.geo_editor.tool_shape.clear(update=True)
  1513. self.app.geo_editor.tool_shape.enabled = False
  1514. # Grid toggle
  1515. if key == QtCore.Qt.Key_G or key == 'G':
  1516. self.app.ui.grid_snap_btn.trigger()
  1517. # Jump to coords
  1518. if key == QtCore.Qt.Key_J or key == 'J':
  1519. l_x, l_y = self.app.on_jump_to()
  1520. self.app.geo_editor.tool_shape.clear(update=True)
  1521. geo = self.cutting_geo(pos=(l_x, l_y))
  1522. self.draw_utility_geometry(geo=geo)
  1523. @staticmethod
  1524. def subtract_poly_from_geo(solid_geo, pts):
  1525. """
  1526. Subtract polygon made from points from the given object.
  1527. This only operates on the paths in the original geometry,
  1528. i.e. it converts polygons into paths.
  1529. :param solid_geo: Geometry from which to subtract.
  1530. :param pts: a tuple of coordinates in format (x0, y0, x1, y1)
  1531. :type pts: tuple
  1532. x0: x coord for lower left vertex of the polygon.
  1533. y0: y coord for lower left vertex of the polygon.
  1534. x1: x coord for upper right vertex of the polygon.
  1535. y1: y coord for upper right vertex of the polygon.
  1536. :return: none
  1537. """
  1538. x0 = pts[0]
  1539. y0 = pts[1]
  1540. x1 = pts[2]
  1541. y1 = pts[3]
  1542. points = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
  1543. # pathonly should be always True, otherwise polygons are not subtracted
  1544. flat_geometry = CutOut.flatten(geometry=solid_geo)
  1545. log.debug("%d paths" % len(flat_geometry))
  1546. polygon = Polygon(points)
  1547. toolgeo = unary_union(polygon)
  1548. diffs = []
  1549. for target in flat_geometry:
  1550. if type(target) == LineString or type(target) == LinearRing:
  1551. diffs.append(target.difference(toolgeo))
  1552. else:
  1553. log.warning("Not implemented.")
  1554. return unary_union(diffs)
  1555. @staticmethod
  1556. def flatten(geometry):
  1557. """
  1558. Creates a list of non-iterable linear geometry objects.
  1559. Polygons are expanded into its exterior and interiors.
  1560. Results are placed in self.flat_geometry
  1561. :param geometry: Shapely type or list or list of list of such.
  1562. """
  1563. flat_geo = []
  1564. try:
  1565. for geo in geometry:
  1566. if geo:
  1567. flat_geo += CutOut.flatten(geometry=geo)
  1568. except TypeError:
  1569. if isinstance(geometry, Polygon) and not geometry.is_empty:
  1570. flat_geo.append(geometry.exterior)
  1571. CutOut.flatten(geometry=geometry.interiors)
  1572. elif not geometry.is_empty:
  1573. flat_geo.append(geometry)
  1574. return flat_geo
  1575. @staticmethod
  1576. def recursive_bounds(geometry):
  1577. """
  1578. Return the bounds of the biggest bounding box in geometry, one that include all.
  1579. :param geometry: a iterable object that holds geometry
  1580. :return: Returns coordinates of rectangular bounds of geometry: (xmin, ymin, xmax, ymax).
  1581. """
  1582. # now it can get bounds for nested lists of objects
  1583. def bounds_rec(obj):
  1584. try:
  1585. minx = inf
  1586. miny = inf
  1587. maxx = -inf
  1588. maxy = -inf
  1589. for k in obj:
  1590. minx_, miny_, maxx_, maxy_ = bounds_rec(k)
  1591. minx = min(minx, minx_)
  1592. miny = min(miny, miny_)
  1593. maxx = max(maxx, maxx_)
  1594. maxy = max(maxy, maxy_)
  1595. return minx, miny, maxx, maxy
  1596. except TypeError:
  1597. # it's a Shapely object, return it's bounds
  1598. if obj:
  1599. return obj.bounds
  1600. return bounds_rec(geometry)
  1601. @staticmethod
  1602. def subtract_geo(target_geo, subtractor):
  1603. """
  1604. Subtract subtractor polygon from the target_geo. This only operates on the paths in the target_geo,
  1605. i.e. it converts polygons into paths.
  1606. :param target_geo: geometry from which to subtract
  1607. :param subtractor: a list of Points, a LinearRing or a Polygon that will be subtracted from target_geo
  1608. :return: a unary_union of the resulting geometry
  1609. """
  1610. if target_geo is None:
  1611. target_geo = []
  1612. # flatten() takes care of possible empty geometry making sure that is filtered
  1613. flat_geometry = CutOut.flatten(target_geo)
  1614. log.debug("%d paths" % len(flat_geometry))
  1615. toolgeo = unary_union(subtractor)
  1616. diffs = []
  1617. for target in flat_geometry:
  1618. if isinstance(target, LineString) or isinstance(target, LinearRing) or isinstance(target, MultiLineString):
  1619. diffs.append(target.difference(toolgeo))
  1620. else:
  1621. log.warning("Not implemented.")
  1622. return unary_union(diffs)
  1623. @staticmethod
  1624. def intersect_geo(target_geo, second_geo):
  1625. """
  1626. :param target_geo:
  1627. :type target_geo:
  1628. :param second_geo:
  1629. :type second_geo:
  1630. :return:
  1631. :rtype:
  1632. """
  1633. results = []
  1634. try:
  1635. __ = iter(target_geo)
  1636. except TypeError:
  1637. target_geo = [target_geo]
  1638. for geo in target_geo:
  1639. if second_geo.intersects(geo):
  1640. results.append(second_geo.intersection(geo))
  1641. return CutOut.flatten(results)
  1642. def reset_fields(self):
  1643. self.ui.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  1644. class CutoutUI:
  1645. toolName = _("Cutout PCB")
  1646. def __init__(self, layout, app):
  1647. self.app = app
  1648. self.decimals = self.app.decimals
  1649. self.layout = layout
  1650. # Title
  1651. title_label = QtWidgets.QLabel("%s" % self.toolName)
  1652. title_label.setStyleSheet("""
  1653. QLabel
  1654. {
  1655. font-size: 16px;
  1656. font-weight: bold;
  1657. }
  1658. """)
  1659. self.layout.addWidget(title_label)
  1660. self.layout.addWidget(QtWidgets.QLabel(''))
  1661. # Form Layout
  1662. grid0 = QtWidgets.QGridLayout()
  1663. grid0.setColumnStretch(0, 0)
  1664. grid0.setColumnStretch(1, 1)
  1665. self.layout.addLayout(grid0)
  1666. self.object_label = QtWidgets.QLabel('<b>%s:</b>' % _("Source Object"))
  1667. self.object_label.setToolTip('%s.' % _("Object to be cutout"))
  1668. grid0.addWidget(self.object_label, 0, 0, 1, 2)
  1669. # Object kind
  1670. self.kindlabel = QtWidgets.QLabel('%s:' % _('Kind'))
  1671. self.kindlabel.setToolTip(
  1672. _("Choice of what kind the object we want to cutout is.\n"
  1673. "- Single: contain a single PCB Gerber outline object.\n"
  1674. "- Panel: a panel PCB Gerber object, which is made\n"
  1675. "out of many individual PCB outlines.")
  1676. )
  1677. self.obj_kind_combo = RadioSet([
  1678. {"label": _("Single"), "value": "single"},
  1679. {"label": _("Panel"), "value": "panel"},
  1680. ])
  1681. grid0.addWidget(self.kindlabel, 2, 0)
  1682. grid0.addWidget(self.obj_kind_combo, 2, 1)
  1683. # Type of object to be cutout
  1684. self.type_obj_radio = RadioSet([
  1685. {"label": _("Gerber"), "value": "grb"},
  1686. {"label": _("Geometry"), "value": "geo"},
  1687. ])
  1688. self.type_obj_combo_label = QtWidgets.QLabel('%s:' % _("Type"))
  1689. self.type_obj_combo_label.setToolTip(
  1690. _("Specify the type of object to be cutout.\n"
  1691. "It can be of type: Gerber or Geometry.\n"
  1692. "What is selected here will dictate the kind\n"
  1693. "of objects that will populate the 'Object' combobox.")
  1694. )
  1695. grid0.addWidget(self.type_obj_combo_label, 4, 0)
  1696. grid0.addWidget(self.type_obj_radio, 4, 1)
  1697. # Object to be cutout
  1698. self.obj_combo = FCComboBox()
  1699. self.obj_combo.setModel(self.app.collection)
  1700. self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  1701. self.obj_combo.is_last = True
  1702. grid0.addWidget(self.obj_combo, 6, 0, 1, 2)
  1703. # Convex Shape
  1704. # Surrounding convex box shape
  1705. self.convex_box_label = QtWidgets.QLabel('%s:' % _("Convex Shape"))
  1706. self.convex_box_label.setToolTip(
  1707. _("Create a convex shape surrounding the entire PCB.\n"
  1708. "Used only if the source object type is Gerber.")
  1709. )
  1710. self.convex_box_cb = FCCheckBox()
  1711. self.convex_box_cb.setToolTip(
  1712. _("Create a convex shape surrounding the entire PCB.\n"
  1713. "Used only if the source object type is Gerber.")
  1714. )
  1715. grid0.addWidget(self.convex_box_label, 8, 0)
  1716. grid0.addWidget(self.convex_box_cb, 8, 1)
  1717. separator_line = QtWidgets.QFrame()
  1718. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  1719. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  1720. grid0.addWidget(separator_line, 10, 0, 1, 2)
  1721. self.tool_sel_label = FCLabel('<b>%s</b>' % _('Cutout Tool'))
  1722. grid0.addWidget(self.tool_sel_label, 12, 0, 1, 2)
  1723. # Tool Diameter
  1724. self.dia = FCDoubleSpinner(callback=self.confirmation_message)
  1725. self.dia.set_precision(self.decimals)
  1726. self.dia.set_range(0.0000, 10000.0000)
  1727. self.dia_label = QtWidgets.QLabel('%s:' % _("Tool Dia"))
  1728. self.dia_label.setToolTip(
  1729. _("Diameter of the tool used to cutout\n"
  1730. "the PCB shape out of the surrounding material.")
  1731. )
  1732. grid0.addWidget(self.dia_label, 14, 0)
  1733. grid0.addWidget(self.dia, 14, 1)
  1734. hlay = QtWidgets.QHBoxLayout()
  1735. # Search and Add new Tool
  1736. self.add_newtool_button = FCButton(_('Search and Add'))
  1737. self.add_newtool_button.setIcon(QtGui.QIcon(self.app.resource_location + '/plus16.png'))
  1738. self.add_newtool_button.setToolTip(
  1739. _("Add a new tool to the Tool Table\n"
  1740. "with the diameter specified above.\n"
  1741. "This is done by a background search\n"
  1742. "in the Tools Database. If nothing is found\n"
  1743. "in the Tools DB then a default tool is added.")
  1744. )
  1745. hlay.addWidget(self.add_newtool_button)
  1746. # Pick from DB new Tool
  1747. self.addtool_from_db_btn = FCButton(_('Pick from DB'))
  1748. self.addtool_from_db_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/search_db32.png'))
  1749. self.addtool_from_db_btn.setToolTip(
  1750. _("Add a new tool to the Tool Table\n"
  1751. "from the Tools Database.\n"
  1752. "Tools database administration in in:\n"
  1753. "Menu: Options -> Tools Database")
  1754. )
  1755. hlay.addWidget(self.addtool_from_db_btn)
  1756. grid0.addLayout(hlay, 16, 0, 1, 2)
  1757. separator_line = QtWidgets.QFrame()
  1758. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  1759. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  1760. grid0.addWidget(separator_line, 18, 0, 1, 2)
  1761. self.param_label = QtWidgets.QLabel('<b>%s:</b>' % _("Tool Parameters"))
  1762. grid0.addWidget(self.param_label, 20, 0, 1, 2)
  1763. # Cut Z
  1764. cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
  1765. cutzlabel.setToolTip(
  1766. _(
  1767. "Cutting depth (negative)\n"
  1768. "below the copper surface."
  1769. )
  1770. )
  1771. self.cutz_entry = FCDoubleSpinner(callback=self.confirmation_message)
  1772. self.cutz_entry.set_precision(self.decimals)
  1773. if machinist_setting == 0:
  1774. self.cutz_entry.setRange(-10000.0000, -0.00001)
  1775. else:
  1776. self.cutz_entry.setRange(-10000.0000, 10000.0000)
  1777. self.cutz_entry.setSingleStep(0.1)
  1778. grid0.addWidget(cutzlabel, 22, 0)
  1779. grid0.addWidget(self.cutz_entry, 22, 1)
  1780. # Multi-pass
  1781. self.mpass_cb = FCCheckBox('%s:' % _("Multi-Depth"))
  1782. self.mpass_cb.setToolTip(
  1783. _(
  1784. "Use multiple passes to limit\n"
  1785. "the cut depth in each pass. Will\n"
  1786. "cut multiple times until Cut Z is\n"
  1787. "reached."
  1788. )
  1789. )
  1790. self.maxdepth_entry = FCDoubleSpinner(callback=self.confirmation_message)
  1791. self.maxdepth_entry.set_precision(self.decimals)
  1792. self.maxdepth_entry.setRange(0, 10000.0000)
  1793. self.maxdepth_entry.setSingleStep(0.1)
  1794. self.maxdepth_entry.setToolTip(
  1795. _(
  1796. "Depth of each pass (positive)."
  1797. )
  1798. )
  1799. grid0.addWidget(self.mpass_cb, 24, 0)
  1800. grid0.addWidget(self.maxdepth_entry, 24, 1)
  1801. self.ois_mpass_geo = OptionalInputSection(self.mpass_cb, [self.maxdepth_entry])
  1802. # Margin
  1803. self.margin = FCDoubleSpinner(callback=self.confirmation_message)
  1804. self.margin.set_range(-10000.0000, 10000.0000)
  1805. self.margin.setSingleStep(0.1)
  1806. self.margin.set_precision(self.decimals)
  1807. self.margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
  1808. self.margin_label.setToolTip(
  1809. _("Margin over bounds. A positive value here\n"
  1810. "will make the cutout of the PCB further from\n"
  1811. "the actual PCB border")
  1812. )
  1813. grid0.addWidget(self.margin_label, 26, 0)
  1814. grid0.addWidget(self.margin, 26, 1)
  1815. # Gapsize
  1816. self.gapsize_label = QtWidgets.QLabel('%s:' % _("Gap size"))
  1817. self.gapsize_label.setToolTip(
  1818. _("The size of the bridge gaps in the cutout\n"
  1819. "used to keep the board connected to\n"
  1820. "the surrounding material (the one \n"
  1821. "from which the PCB is cutout).")
  1822. )
  1823. self.gapsize = FCDoubleSpinner(callback=self.confirmation_message)
  1824. self.gapsize.set_precision(self.decimals)
  1825. grid0.addWidget(self.gapsize_label, 28, 0)
  1826. grid0.addWidget(self.gapsize, 28, 1)
  1827. # Gap Type
  1828. self.gaptype_label = FCLabel('%s:' % _("Gap type"))
  1829. self.gaptype_label.setToolTip(
  1830. _("The type of gap:\n"
  1831. "- Bridge -> the cutout will be interrupted by bridges\n"
  1832. "- Thin -> same as 'bridge' but it will be thinner by partially milling the gap\n"
  1833. "- M-Bites -> 'Mouse Bites' - same as 'bridge' but covered with drill holes")
  1834. )
  1835. self.gaptype_radio = RadioSet(
  1836. [
  1837. {'label': _('Bridge'), 'value': 'b'},
  1838. {'label': _('Thin'), 'value': 'bt'},
  1839. {'label': "M-Bites", 'value': 'mb'}
  1840. ],
  1841. stretch=True
  1842. )
  1843. grid0.addWidget(self.gaptype_label, 30, 0)
  1844. grid0.addWidget(self.gaptype_radio, 30, 1)
  1845. # Thin gaps Depth
  1846. self.thin_depth_label = FCLabel('%s:' % _("Depth"))
  1847. self.thin_depth_label.setToolTip(
  1848. _("The depth until the milling is done\n"
  1849. "in order to thin the gaps.")
  1850. )
  1851. self.thin_depth_entry = FCDoubleSpinner(callback=self.confirmation_message)
  1852. self.thin_depth_entry.set_precision(self.decimals)
  1853. if machinist_setting == 0:
  1854. self.thin_depth_entry.setRange(-10000.0000, -0.00001)
  1855. else:
  1856. self.thin_depth_entry.setRange(-10000.0000, 10000.0000)
  1857. self.thin_depth_entry.setSingleStep(0.1)
  1858. grid0.addWidget(self.thin_depth_label, 32, 0)
  1859. grid0.addWidget(self.thin_depth_entry, 32, 1)
  1860. # Mouse Bites Tool Diameter
  1861. self.mb_dia_label = FCLabel('%s:' % _("Tool Diameter"))
  1862. self.mb_dia_label.setToolTip(
  1863. _("The drill hole diameter when doing mouse bites.")
  1864. )
  1865. self.mb_dia_entry = FCDoubleSpinner(callback=self.confirmation_message)
  1866. self.mb_dia_entry.set_precision(self.decimals)
  1867. self.mb_dia_entry.setRange(0, 100.0000)
  1868. grid0.addWidget(self.mb_dia_label, 34, 0)
  1869. grid0.addWidget(self.mb_dia_entry, 34, 1)
  1870. # Mouse Bites Holes Spacing
  1871. self.mb_spacing_label = FCLabel('%s:' % _("Spacing"))
  1872. self.mb_spacing_label.setToolTip(
  1873. _("The spacing between drill holes when doing mouse bites.")
  1874. )
  1875. self.mb_spacing_entry = FCDoubleSpinner(callback=self.confirmation_message)
  1876. self.mb_spacing_entry.set_precision(self.decimals)
  1877. self.mb_spacing_entry.setRange(0, 100.0000)
  1878. grid0.addWidget(self.mb_spacing_label, 36, 0)
  1879. grid0.addWidget(self.mb_spacing_entry, 36, 1)
  1880. separator_line = QtWidgets.QFrame()
  1881. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  1882. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  1883. grid0.addWidget(separator_line, 38, 0, 1, 2)
  1884. # Title2
  1885. title_param_label = QtWidgets.QLabel("<b>%s %s</b>:" % (_('Automatic'), _("Bridge Gaps")))
  1886. title_param_label.setToolTip(
  1887. _("This section handle creation of automatic bridge gaps.")
  1888. )
  1889. grid0.addWidget(title_param_label, 40, 0, 1, 2)
  1890. # Gaps
  1891. # How gaps wil be rendered:
  1892. # lr - left + right
  1893. # tb - top + bottom
  1894. # 4 - left + right +top + bottom
  1895. # 2lr - 2*left + 2*right
  1896. # 2tb - 2*top + 2*bottom
  1897. # 8 - 2*left + 2*right +2*top + 2*bottom
  1898. gaps_label = QtWidgets.QLabel('%s:' % _('Gaps'))
  1899. gaps_label.setToolTip(
  1900. _("Number of gaps used for the Automatic cutout.\n"
  1901. "There can be maximum 8 bridges/gaps.\n"
  1902. "The choices are:\n"
  1903. "- None - no gaps\n"
  1904. "- lr - left + right\n"
  1905. "- tb - top + bottom\n"
  1906. "- 4 - left + right +top + bottom\n"
  1907. "- 2lr - 2*left + 2*right\n"
  1908. "- 2tb - 2*top + 2*bottom\n"
  1909. "- 8 - 2*left + 2*right +2*top + 2*bottom")
  1910. )
  1911. # gaps_label.setMinimumWidth(60)
  1912. self.gaps = FCComboBox()
  1913. gaps_items = ['None', 'LR', 'TB', '4', '2LR', '2TB', '8']
  1914. for it in gaps_items:
  1915. self.gaps.addItem(it)
  1916. # self.gaps.setStyleSheet('background-color: rgb(255,255,255)')
  1917. grid0.addWidget(gaps_label, 42, 0)
  1918. grid0.addWidget(self.gaps, 42, 1)
  1919. # Buttons
  1920. self.ff_cutout_object_btn = FCButton(_("Generate Geometry"))
  1921. self.ff_cutout_object_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/irregular32.png'))
  1922. self.ff_cutout_object_btn.setToolTip(
  1923. _("Cutout the selected object.\n"
  1924. "The cutout shape can be of any shape.\n"
  1925. "Useful when the PCB has a non-rectangular shape.")
  1926. )
  1927. self.ff_cutout_object_btn.setStyleSheet("""
  1928. QPushButton
  1929. {
  1930. font-weight: bold;
  1931. }
  1932. """)
  1933. grid0.addWidget(self.ff_cutout_object_btn, 44, 0, 1, 2)
  1934. self.rect_cutout_object_btn = FCButton(_("Generate Geometry"))
  1935. self.rect_cutout_object_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/rectangle32.png'))
  1936. self.rect_cutout_object_btn.setToolTip(
  1937. _("Cutout the selected object.\n"
  1938. "The resulting cutout shape is\n"
  1939. "always a rectangle shape and it will be\n"
  1940. "the bounding box of the Object.")
  1941. )
  1942. self.rect_cutout_object_btn.setStyleSheet("""
  1943. QPushButton
  1944. {
  1945. font-weight: bold;
  1946. }
  1947. """)
  1948. grid0.addWidget(self.rect_cutout_object_btn, 46, 0, 1, 2)
  1949. separator_line = QtWidgets.QFrame()
  1950. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  1951. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  1952. grid0.addWidget(separator_line, 48, 0, 1, 2)
  1953. # MANUAL BRIDGE GAPS
  1954. title_manual_label = QtWidgets.QLabel("<b>%s %s</b>:" % (_('Manual'), _("Bridge Gaps")))
  1955. title_manual_label.setToolTip(
  1956. _("This section handle creation of manual bridge gaps.\n"
  1957. "This is done by mouse clicking on the perimeter of the\n"
  1958. "Geometry object that is used as a cutout object. ")
  1959. )
  1960. grid0.addWidget(title_manual_label, 50, 0, 1, 2)
  1961. # Big Cursor
  1962. big_cursor_label = QtWidgets.QLabel('%s:' % _("Big cursor"))
  1963. big_cursor_label.setToolTip(
  1964. _("Use a big cursor when adding manual gaps."))
  1965. self.big_cursor_cb = FCCheckBox()
  1966. grid0.addWidget(big_cursor_label, 52, 0)
  1967. grid0.addWidget(self.big_cursor_cb, 52, 1)
  1968. # Generate a surrounding Geometry object
  1969. self.man_geo_creation_btn = FCButton(_("Generate Manual Geometry"))
  1970. self.man_geo_creation_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/rectangle32.png'))
  1971. self.man_geo_creation_btn.setToolTip(
  1972. _("If the object to be cutout is a Gerber\n"
  1973. "first create a Geometry that surrounds it,\n"
  1974. "to be used as the cutout, if one doesn't exist yet.\n"
  1975. "Select the source Gerber file in the top object combobox.")
  1976. )
  1977. # self.man_geo_creation_btn.setStyleSheet("""
  1978. # QPushButton
  1979. # {
  1980. # font-weight: bold;
  1981. # }
  1982. # """)
  1983. grid0.addWidget(self.man_geo_creation_btn, 54, 0, 1, 2)
  1984. # Manual Geo Object
  1985. self.man_object_combo = FCComboBox()
  1986. self.man_object_combo.setModel(self.app.collection)
  1987. self.man_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
  1988. self.man_object_combo.is_last = True
  1989. self.man_object_combo.obj_type = "Geometry"
  1990. self.man_object_label = QtWidgets.QLabel('%s:' % _("Manual cutout Geometry"))
  1991. self.man_object_label.setToolTip(
  1992. _("Geometry object used to create the manual cutout.")
  1993. )
  1994. # self.man_object_label.setMinimumWidth(60)
  1995. grid0.addWidget(self.man_object_label, 56, 0, 1, 2)
  1996. grid0.addWidget(self.man_object_combo, 56, 0, 1, 2)
  1997. self.man_gaps_creation_btn = FCButton(_("Manual Add Bridge Gaps"))
  1998. self.man_gaps_creation_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/gaps32.png'))
  1999. self.man_gaps_creation_btn.setToolTip(
  2000. _("Use the left mouse button (LMB) click\n"
  2001. "to create a bridge gap to separate the PCB from\n"
  2002. "the surrounding material.\n"
  2003. "The LMB click has to be done on the perimeter of\n"
  2004. "the Geometry object used as a cutout geometry.")
  2005. )
  2006. self.man_gaps_creation_btn.setStyleSheet("""
  2007. QPushButton
  2008. {
  2009. font-weight: bold;
  2010. }
  2011. """)
  2012. grid0.addWidget(self.man_gaps_creation_btn, 58, 0, 1, 2)
  2013. self.layout.addStretch()
  2014. # ## Reset Tool
  2015. self.reset_button = FCButton(_("Reset Tool"))
  2016. self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
  2017. self.reset_button.setToolTip(
  2018. _("Will reset the tool parameters.")
  2019. )
  2020. self.reset_button.setStyleSheet("""
  2021. QPushButton
  2022. {
  2023. font-weight: bold;
  2024. }
  2025. """)
  2026. self.layout.addWidget(self.reset_button)
  2027. self.gaptype_radio.activated_custom.connect(self.on_gap_type_radio)
  2028. # ############################ FINSIHED GUI ###################################
  2029. # #############################################################################
  2030. def on_gap_type_radio(self, val):
  2031. if val == 'b':
  2032. self.thin_depth_label.hide()
  2033. self.thin_depth_entry.hide()
  2034. self.mb_dia_label.hide()
  2035. self.mb_dia_entry.hide()
  2036. self.mb_spacing_label.hide()
  2037. self.mb_spacing_entry.hide()
  2038. elif val == 'bt':
  2039. self.thin_depth_label.show()
  2040. self.thin_depth_entry.show()
  2041. self.mb_dia_label.hide()
  2042. self.mb_dia_entry.hide()
  2043. self.mb_spacing_label.hide()
  2044. self.mb_spacing_entry.hide()
  2045. elif val == 'mb':
  2046. self.thin_depth_label.hide()
  2047. self.thin_depth_entry.hide()
  2048. self.mb_dia_label.show()
  2049. self.mb_dia_entry.show()
  2050. self.mb_spacing_label.show()
  2051. self.mb_spacing_entry.show()
  2052. def confirmation_message(self, accepted, minval, maxval):
  2053. if accepted is False:
  2054. self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
  2055. self.decimals,
  2056. minval,
  2057. self.decimals,
  2058. maxval), False)
  2059. else:
  2060. self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
  2061. def confirmation_message_int(self, accepted, minval, maxval):
  2062. if accepted is False:
  2063. self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
  2064. (_("Edited value is out of range"), minval, maxval), False)
  2065. else:
  2066. self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)