ToolCopperThieving.py 75 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # File Author: Marius Adrian Stanciu (c) #
  4. # Date: 10/25/2019 #
  5. # MIT Licence #
  6. # ##########################################################
  7. from PyQt5 import QtWidgets, QtCore, QtGui
  8. from camlib import grace
  9. from appTool import AppTool
  10. from appGUI.GUIElements import FCDoubleSpinner, RadioSet, FCEntry, FCComboBox, FCLabel
  11. import shapely.geometry.base as base
  12. from shapely.ops import unary_union
  13. from shapely.geometry import Polygon, MultiPolygon, Point, LineString
  14. from shapely.geometry import box as box
  15. import shapely.affinity as affinity
  16. import logging
  17. from copy import deepcopy
  18. import numpy as np
  19. from collections import Iterable
  20. import gettext
  21. import appTranslation as fcTranslate
  22. import builtins
  23. fcTranslate.apply_language('strings')
  24. if '_' not in builtins.__dict__:
  25. _ = gettext.gettext
  26. log = logging.getLogger('base')
  27. class ToolCopperThieving(AppTool):
  28. work_finished = QtCore.pyqtSignal()
  29. def __init__(self, app):
  30. AppTool.__init__(self, app)
  31. self.app = app
  32. self.canvas = self.app.plotcanvas
  33. self.decimals = self.app.decimals
  34. self.units = self.app.defaults['units']
  35. # #############################################################################
  36. # ######################### Tool GUI ##########################################
  37. # #############################################################################
  38. self.ui = ThievingUI(layout=self.layout, app=self.app)
  39. self.toolName = self.ui.toolName
  40. # Objects involved in Copper thieving
  41. self.grb_object = None
  42. self.ref_obj = None
  43. self.sel_rect = []
  44. self.sm_object = None
  45. # store the flattened geometry here:
  46. self.flat_geometry = []
  47. # Events ID
  48. self.mr = None
  49. self.mm = None
  50. # Mouse cursor positions
  51. self.mouse_is_dragging = False
  52. self.cursor_pos = (0, 0)
  53. self.first_click = False
  54. self.handlers_connected = False
  55. # Tool properties
  56. self.clearance_val = None
  57. self.margin_val = None
  58. self.geo_steps_per_circle = 128
  59. # Thieving geometry storage
  60. self.thief_solid_geometry = []
  61. # Robber bar geometry storage
  62. self.robber_geo = None
  63. self.robber_line = None
  64. self.rb_thickness = None
  65. # SIGNALS
  66. self.ui.ref_combo_type.currentIndexChanged.connect(self.on_ref_combo_type_change)
  67. self.ui.reference_radio.group_toggle_fn = self.on_toggle_reference
  68. self.ui.fill_type_radio.activated_custom.connect(self.on_thieving_type)
  69. self.ui.fill_button.clicked.connect(self.on_add_copper_thieving_click)
  70. self.ui.rb_button.clicked.connect(self.on_add_robber_bar_click)
  71. self.ui.ppm_button.clicked.connect(self.on_add_ppm_click)
  72. self.ui.reset_button.clicked.connect(self.set_tool_ui)
  73. self.work_finished.connect(self.on_new_pattern_plating_object)
  74. def run(self, toggle=True):
  75. self.app.defaults.report_usage("ToolCopperThieving()")
  76. if toggle:
  77. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  78. if self.app.ui.splitter.sizes()[0] == 0:
  79. self.app.ui.splitter.setSizes([1, 1])
  80. else:
  81. try:
  82. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  83. # if tab is populated with the tool but it does not have the focus, focus on it
  84. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  85. # focus on Tool Tab
  86. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  87. else:
  88. self.app.ui.splitter.setSizes([0, 1])
  89. except AttributeError:
  90. pass
  91. else:
  92. if self.app.ui.splitter.sizes()[0] == 0:
  93. self.app.ui.splitter.setSizes([1, 1])
  94. AppTool.run(self)
  95. self.set_tool_ui()
  96. self.app.ui.notebook.setTabText(2, _("Copper Thieving Tool"))
  97. def install(self, icon=None, separator=None, **kwargs):
  98. AppTool.install(self, icon, separator, shortcut='Alt+J', **kwargs)
  99. def set_tool_ui(self):
  100. self.units = self.app.defaults['units']
  101. self.geo_steps_per_circle = int(self.app.defaults["tools_copper_thieving_circle_steps"])
  102. self.ui.clearance_entry.set_value(float(self.app.defaults["tools_copper_thieving_clearance"]))
  103. self.ui.margin_entry.set_value(float(self.app.defaults["tools_copper_thieving_margin"]))
  104. self.ui.reference_radio.set_value(self.app.defaults["tools_copper_thieving_reference"])
  105. self.ui.bbox_type_radio.set_value(self.app.defaults["tools_copper_thieving_box_type"])
  106. self.ui.fill_type_radio.set_value(self.app.defaults["tools_copper_thieving_fill_type"])
  107. self.ui.area_entry.set_value(self.app.defaults["tools_copper_thieving_area"])
  108. self.ui.dot_dia_entry.set_value(self.app.defaults["tools_copper_thieving_dots_dia"])
  109. self.ui.dot_spacing_entry.set_value(self.app.defaults["tools_copper_thieving_dots_spacing"])
  110. self.ui.square_size_entry.set_value(self.app.defaults["tools_copper_thieving_squares_size"])
  111. self.ui.squares_spacing_entry.set_value(self.app.defaults["tools_copper_thieving_squares_spacing"])
  112. self.ui.line_size_entry.set_value(self.app.defaults["tools_copper_thieving_lines_size"])
  113. self.ui.lines_spacing_entry.set_value(self.app.defaults["tools_copper_thieving_lines_spacing"])
  114. self.ui.rb_margin_entry.set_value(self.app.defaults["tools_copper_thieving_rb_margin"])
  115. self.ui.rb_thickness_entry.set_value(self.app.defaults["tools_copper_thieving_rb_thickness"])
  116. self.ui.clearance_ppm_entry.set_value(self.app.defaults["tools_copper_thieving_mask_clearance"])
  117. self.ui.ppm_choice_radio.set_value(self.app.defaults["tools_copper_thieving_geo_choice"])
  118. # INIT SECTION
  119. self.handlers_connected = False
  120. self.robber_geo = None
  121. self.robber_line = None
  122. self.thief_solid_geometry = []
  123. def on_ref_combo_type_change(self):
  124. obj_type = self.ui.ref_combo_type.currentIndex()
  125. self.ui.ref_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
  126. self.ui.ref_combo.setCurrentIndex(0)
  127. self.ui.ref_combo.obj_type = {
  128. _("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry"
  129. }[self.ui.ref_combo_type.get_value()]
  130. def on_toggle_reference(self):
  131. if self.ui.reference_radio.get_value() == "itself" or self.ui.reference_radio.get_value() == "area":
  132. self.ui.ref_combo.hide()
  133. self.ui.ref_combo_label.hide()
  134. self.ui.ref_combo_type.hide()
  135. self.ui.ref_combo_type_label.hide()
  136. else:
  137. self.ui.ref_combo.show()
  138. self.ui.ref_combo_label.show()
  139. self.ui.ref_combo_type.show()
  140. self.ui.ref_combo_type_label.show()
  141. if self.ui.reference_radio.get_value() == "itself":
  142. self.ui.bbox_type_label.show()
  143. self.ui.bbox_type_radio.show()
  144. else:
  145. if self.ui.fill_type_radio.get_value() == 'line':
  146. self.ui.reference_radio.set_value('itself')
  147. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Lines Grid works only for 'itself' reference ..."))
  148. return
  149. self.ui.bbox_type_label.hide()
  150. self.ui.bbox_type_radio.hide()
  151. def on_thieving_type(self, choice):
  152. if choice == 'solid':
  153. self.ui.dots_frame.hide()
  154. self.ui.squares_frame.hide()
  155. self.ui.lines_frame.hide()
  156. self.app.inform.emit(_("Solid fill selected."))
  157. elif choice == 'dot':
  158. self.ui.dots_frame.show()
  159. self.ui.squares_frame.hide()
  160. self.ui.lines_frame.hide()
  161. self.app.inform.emit(_("Dots grid fill selected."))
  162. elif choice == 'square':
  163. self.ui.dots_frame.hide()
  164. self.ui.squares_frame.show()
  165. self.ui.lines_frame.hide()
  166. self.app.inform.emit(_("Squares grid fill selected."))
  167. else:
  168. if self.ui.reference_radio.get_value() != 'itself':
  169. self.ui.reference_radio.set_value('itself')
  170. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Lines Grid works only for 'itself' reference ..."))
  171. self.ui.dots_frame.hide()
  172. self.ui.squares_frame.hide()
  173. self.ui.lines_frame.show()
  174. def on_add_robber_bar_click(self):
  175. rb_margin = self.ui.rb_margin_entry.get_value()
  176. self.rb_thickness = self.ui.rb_thickness_entry.get_value()
  177. # get the Gerber object on which the Robber bar will be inserted
  178. selection_index = self.ui.grb_object_combo.currentIndex()
  179. model_index = self.app.collection.index(selection_index, 0, self.ui.grb_object_combo.rootModelIndex())
  180. try:
  181. self.grb_object = model_index.internalPointer().obj
  182. except Exception as e:
  183. log.debug("ToolCopperThieving.on_add_robber_bar_click() --> %s" % str(e))
  184. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
  185. return
  186. try:
  187. outline_pol = self.grb_object.solid_geometry.envelope
  188. except (TypeError, AttributeError):
  189. outline_pol = MultiPolygon(self.grb_object.solid_geometry).envelope
  190. rb_distance = rb_margin + (self.rb_thickness / 2.0)
  191. self.robber_line = outline_pol.buffer(rb_distance).exterior
  192. self.robber_geo = self.robber_line.buffer(self.rb_thickness / 2.0)
  193. self.app.proc_container.update_view_text(' %s' % _("Append geometry"))
  194. new_apertures = deepcopy(self.grb_object.apertures)
  195. aperture_found = None
  196. for ap_id, ap_val in self.grb_object.apertures.items():
  197. if ap_val['type'] == 'C' and ap_val['size'] == self.rb_thickness:
  198. aperture_found = ap_id
  199. break
  200. if aperture_found:
  201. geo_elem = {'solid': self.robber_geo, 'follow': self.robber_line}
  202. new_apertures[aperture_found]['geometry'].append(deepcopy(geo_elem))
  203. else:
  204. ap_keys = list(new_apertures.keys())
  205. if ap_keys:
  206. new_apid = str(int(max(ap_keys)) + 1)
  207. else:
  208. new_apid = '10'
  209. new_apertures[new_apid] = {
  210. 'type': 'C',
  211. 'size': deepcopy(self.rb_thickness),
  212. 'geometry': []
  213. }
  214. geo_elem = {'solid': self.robber_geo, 'follow': self.robber_line}
  215. new_apertures[new_apid]['geometry'].append(deepcopy(geo_elem))
  216. geo_obj = deepcopy(self.grb_object.solid_geometry)
  217. if isinstance(geo_obj, MultiPolygon):
  218. s_list = []
  219. for pol in geo_obj.geoms:
  220. s_list.append(pol)
  221. s_list.append(deepcopy(self.robber_geo))
  222. geo_obj = MultiPolygon(s_list)
  223. elif isinstance(geo_obj, list):
  224. geo_obj.append(deepcopy(self.robber_geo))
  225. elif isinstance(geo_obj, Polygon):
  226. geo_obj = MultiPolygon([geo_obj, deepcopy(self.robber_geo)])
  227. outname = '%s_%s' % (str(self.grb_object.options['name']), 'robber')
  228. def initialize(grb_obj, app_obj):
  229. grb_obj.options = {}
  230. for opt in self.grb_object.options:
  231. if opt != 'name':
  232. grb_obj.options[opt] = deepcopy(self.grb_object.options[opt])
  233. grb_obj.options['name'] = outname
  234. grb_obj.multitool = False
  235. grb_obj.multigeo = False
  236. grb_obj.follow = deepcopy(self.grb_object.follow)
  237. grb_obj.apertures = new_apertures
  238. grb_obj.solid_geometry = unary_union(geo_obj)
  239. grb_obj.follow_geometry = deepcopy(self.grb_object.follow_geometry) + [deepcopy(self.robber_line)]
  240. app_obj.proc_container.update_view_text(' %s' % _("Append source file"))
  241. # update the source file with the new geometry:
  242. grb_obj.source_file = app_obj.f_handlers.export_gerber(obj_name=outname, filename=None, local_use=grb_obj,
  243. use_thread=False)
  244. ret_val = self.app.app_obj.new_object('gerber', outname, initialize, plot=True)
  245. self.app.proc_container.update_view_text(' %s' % '')
  246. if ret_val == 'fail':
  247. self.app.call_source = "app"
  248. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
  249. return
  250. self.on_exit()
  251. self.app.inform.emit('[success] %s' % _("Copper Thieving Tool done."))
  252. def on_add_copper_thieving_click(self):
  253. self.app.call_source = "copper_thieving_tool"
  254. self.clearance_val = self.ui.clearance_entry.get_value()
  255. self.margin_val = self.ui.margin_entry.get_value()
  256. reference_method = self.ui.reference_radio.get_value()
  257. # get the Gerber object on which the Copper thieving will be inserted
  258. selection_index = self.ui.grb_object_combo.currentIndex()
  259. model_index = self.app.collection.index(selection_index, 0, self.ui.grb_object_combo.rootModelIndex())
  260. try:
  261. self.grb_object = model_index.internalPointer().obj
  262. except Exception as e:
  263. log.debug("ToolCopperThieving.on_add_copper_thieving_click() --> %s" % str(e))
  264. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
  265. return
  266. if reference_method == 'itself':
  267. bound_obj_name = self.ui.grb_object_combo.currentText()
  268. # Get reference object.
  269. try:
  270. self.ref_obj = self.app.collection.get_by_name(bound_obj_name)
  271. except Exception as e:
  272. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(e)))
  273. return "Could not retrieve object: %s" % self.obj_name
  274. self.copper_thieving(
  275. thieving_obj=self.grb_object,
  276. c_val=self.clearance_val,
  277. margin=self.margin_val
  278. )
  279. elif reference_method == 'area':
  280. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area."))
  281. self.connect_event_handlers()
  282. elif reference_method == 'box':
  283. bound_obj_name = self.ui.ref_combo.currentText()
  284. # Get reference object.
  285. try:
  286. self.ref_obj = self.app.collection.get_by_name(bound_obj_name)
  287. except Exception:
  288. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), bound_obj_name))
  289. return
  290. self.copper_thieving(
  291. thieving_obj=self.grb_object,
  292. ref_obj=self.ref_obj,
  293. c_val=self.clearance_val,
  294. margin=self.margin_val
  295. )
  296. # To be called after clicking on the plot.
  297. def on_mouse_release(self, event):
  298. if self.app.is_legacy is False:
  299. event_pos = event.pos
  300. # event_is_dragging = event.is_dragging
  301. right_button = 2
  302. else:
  303. event_pos = (event.xdata, event.ydata)
  304. # event_is_dragging = self.app.plotcanvas.is_dragging
  305. right_button = 3
  306. event_pos = self.app.plotcanvas.translate_coords(event_pos)
  307. # do clear area only for left mouse clicks
  308. if event.button == 1:
  309. if self.first_click is False:
  310. self.first_click = True
  311. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the end point of the filling area."))
  312. self.cursor_pos = self.app.plotcanvas.translate_coords(event_pos)
  313. if self.app.grid_status() is True:
  314. self.cursor_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
  315. else:
  316. self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish."))
  317. self.app.delete_selection_shape()
  318. if self.app.grid_status() is True:
  319. curr_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
  320. else:
  321. curr_pos = (event_pos[0], event_pos[1])
  322. x0, y0 = self.cursor_pos[0], self.cursor_pos[1]
  323. x1, y1 = curr_pos[0], curr_pos[1]
  324. pt1 = (x0, y0)
  325. pt2 = (x1, y0)
  326. pt3 = (x1, y1)
  327. pt4 = (x0, y1)
  328. new_rectangle = Polygon([pt1, pt2, pt3, pt4])
  329. self.sel_rect.append(new_rectangle)
  330. # add a temporary shape on canvas
  331. self.draw_tool_selection_shape(old_coords=(x0, y0), coords=(x1, y1))
  332. self.first_click = False
  333. return
  334. elif event.button == right_button and self.mouse_is_dragging is False:
  335. self.first_click = False
  336. self.delete_tool_selection_shape()
  337. self.disconnect_event_handlers()
  338. if len(self.sel_rect) == 0:
  339. return
  340. self.sel_rect = unary_union(self.sel_rect)
  341. if not isinstance(self.sel_rect, Iterable):
  342. self.sel_rect = [self.sel_rect]
  343. self.copper_thieving(
  344. thieving_obj=self.grb_object,
  345. ref_obj=self.sel_rect,
  346. c_val=self.clearance_val,
  347. margin=self.margin_val
  348. )
  349. # called on mouse move
  350. def on_mouse_move(self, event):
  351. if self.app.is_legacy is False:
  352. event_pos = event.pos
  353. event_is_dragging = event.is_dragging
  354. # right_button = 2
  355. else:
  356. event_pos = (event.xdata, event.ydata)
  357. event_is_dragging = self.app.plotcanvas.is_dragging
  358. # right_button = 3
  359. curr_pos = self.app.plotcanvas.translate_coords(event_pos)
  360. # detect mouse dragging motion
  361. if event_is_dragging is True:
  362. self.mouse_is_dragging = True
  363. else:
  364. self.mouse_is_dragging = False
  365. # update the cursor position
  366. if self.app.grid_status() is True:
  367. # Update cursor
  368. curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
  369. self.app.app_cursor.set_data(np.asarray([(curr_pos[0], curr_pos[1])]),
  370. symbol='++', edge_color=self.app.cursor_color_3D,
  371. edge_width=self.app.defaults["global_cursor_width"],
  372. size=self.app.defaults["global_cursor_size"])
  373. if self.cursor_pos is None:
  374. self.cursor_pos = (0, 0)
  375. self.app.dx = curr_pos[0] - float(self.cursor_pos[0])
  376. self.app.dy = curr_pos[1] - float(self.cursor_pos[1])
  377. # # update the positions on status bar
  378. self.app.ui.position_label.setText("&nbsp;<b>X</b>: %.4f&nbsp;&nbsp; "
  379. "<b>Y</b>: %.4f&nbsp;" % (curr_pos[0], curr_pos[1]))
  380. self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp; <b>Dy</b>: "
  381. "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (self.app.dx, self.app.dy))
  382. units = self.app.defaults["units"].lower()
  383. self.app.plotcanvas.text_hud.text = \
  384. 'Dx:\t{:<.4f} [{:s}]\nDy:\t{:<.4f} [{:s}]\n\nX: \t{:<.4f} [{:s}]\nY: \t{:<.4f} [{:s}]'.format(
  385. self.app.dx, units, self.app.dy, units, curr_pos[0], units, curr_pos[1], units)
  386. # draw the utility geometry
  387. if self.first_click:
  388. self.app.delete_selection_shape()
  389. self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]),
  390. coords=(curr_pos[0], curr_pos[1]))
  391. def copper_thieving(self, thieving_obj, ref_obj=None, c_val=None, margin=None, run_threaded=True):
  392. """
  393. :param thieving_obj:
  394. :param ref_obj:
  395. :param c_val:
  396. :param margin:
  397. :param run_threaded:
  398. :return:
  399. """
  400. if run_threaded:
  401. self.app.proc_container.new('%s ...' % _("Thieving"))
  402. else:
  403. QtWidgets.QApplication.processEvents()
  404. self.app.proc_container.view.set_busy('%s ...' % _("Thieving"))
  405. # #####################################################################
  406. # ####### Read the parameters #########################################
  407. # #####################################################################
  408. log.debug("Copper Thieving Tool started. Reading parameters.")
  409. self.app.inform.emit(_("Copper Thieving Tool started. Reading parameters."))
  410. ref_selected = self.ui.reference_radio.get_value()
  411. if c_val is None:
  412. c_val = float(self.app.defaults["tools_copper_thieving_clearance"])
  413. if margin is None:
  414. margin = float(self.app.defaults["tools_copper_thieving_margin"])
  415. min_area = self.ui.area_entry.get_value()
  416. fill_type = self.ui.fill_type_radio.get_value()
  417. dot_dia = self.ui.dot_dia_entry.get_value()
  418. dot_spacing = self.ui.dot_spacing_entry.get_value()
  419. square_size = self.ui.square_size_entry.get_value()
  420. square_spacing = self.ui.squares_spacing_entry.get_value()
  421. line_size = self.ui.line_size_entry.get_value()
  422. line_spacing = self.ui.lines_spacing_entry.get_value()
  423. # make sure that the source object solid geometry is an Iterable
  424. if not isinstance(self.grb_object.solid_geometry, Iterable):
  425. self.grb_object.solid_geometry = [self.grb_object.solid_geometry]
  426. def job_thread_thieving(tool_obj):
  427. # #########################################################################################################
  428. # Prepare isolation polygon. This will create the clearance over the Gerber features
  429. # #########################################################################################################
  430. log.debug("Copper Thieving Tool. Preparing isolation polygons.")
  431. tool_obj.app.inform.emit(_("Copper Thieving Tool. Preparing isolation polygons."))
  432. # variables to display the percentage of work done
  433. try:
  434. geo_len = len(tool_obj.grb_object.solid_geometry)
  435. except TypeError:
  436. geo_len = 1
  437. old_disp_number = 0
  438. pol_nr = 0
  439. # #########################################################################################################
  440. # apply the clearance value to the geometry
  441. # #########################################################################################################
  442. clearance_geometry = []
  443. try:
  444. for pol in tool_obj.grb_object.solid_geometry:
  445. if tool_obj.app.abort_flag:
  446. # graceful abort requested by the user
  447. raise grace
  448. clearance_geometry.append(
  449. pol.buffer(c_val, int(int(tool_obj.geo_steps_per_circle) / 4))
  450. )
  451. pol_nr += 1
  452. disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
  453. if old_disp_number < disp_number <= 100:
  454. msg = ' %s ... %d%%' % (_("Thieving"), int(disp_number))
  455. tool_obj.app.proc_container.update_view_text(msg)
  456. old_disp_number = disp_number
  457. except TypeError:
  458. # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a
  459. # MultiPolygon (not an iterable)
  460. clearance_geometry.append(
  461. tool_obj.grb_object.solid_geometry.buffer(c_val, int(int(tool_obj.geo_steps_per_circle) / 4))
  462. )
  463. tool_obj.app.proc_container.update_view_text(' %s ...' % _("Buffering"))
  464. clearance_geometry = unary_union(clearance_geometry)
  465. # #########################################################################################################
  466. # Prepare the area to fill with copper.
  467. # #########################################################################################################
  468. log.debug("Copper Thieving Tool. Preparing areas to fill with copper.")
  469. tool_obj.app.inform.emit(_("Copper Thieving Tool. Preparing areas to fill with copper."))
  470. try:
  471. if ref_obj is None or ref_obj == 'itself':
  472. working_obj = thieving_obj
  473. else:
  474. working_obj = ref_obj
  475. except Exception as e:
  476. log.debug("ToolCopperThieving.copper_thieving() --> %s" % str(e))
  477. return 'fail'
  478. tool_obj.app.proc_container.update_view_text(' %s' % _("Working..."))
  479. # #########################################################################################################
  480. # generate the bounding box geometry
  481. # #########################################################################################################
  482. if ref_selected == 'itself':
  483. geo_n = deepcopy(working_obj.solid_geometry)
  484. try:
  485. if tool_obj.ui.bbox_type_radio.get_value() == 'min':
  486. if isinstance(geo_n, MultiPolygon):
  487. env_obj = geo_n.convex_hull
  488. elif (isinstance(geo_n, MultiPolygon) and len(geo_n) == 1) or \
  489. (isinstance(geo_n, list) and len(geo_n) == 1) and isinstance(geo_n[0], Polygon):
  490. env_obj = unary_union(geo_n)
  491. else:
  492. env_obj = unary_union(geo_n)
  493. env_obj = env_obj.convex_hull
  494. bounding_box = env_obj.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
  495. else:
  496. if isinstance(geo_n, Polygon):
  497. bounding_box = geo_n.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
  498. elif isinstance(geo_n, list):
  499. geo_n = MultiPolygon(geo_n)
  500. x0, y0, x1, y1 = geo_n.bounds
  501. geo = box(x0, y0, x1, y1)
  502. bounding_box = geo.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
  503. elif isinstance(geo_n, MultiPolygon):
  504. x0, y0, x1, y1 = geo_n.bounds
  505. geo = box(x0, y0, x1, y1)
  506. bounding_box = geo.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
  507. else:
  508. tool_obj.app.inform.emit(
  509. '[ERROR_NOTCL] %s: %s' % (_("Geometry not supported for bounding box"), type(geo_n))
  510. )
  511. return 'fail'
  512. except Exception as e:
  513. log.debug("ToolCopperFIll.copper_thieving() 'itself' --> %s" % str(e))
  514. tool_obj.app.inform.emit('[ERROR_NOTCL] %s' % _("No object available."))
  515. return 'fail'
  516. elif ref_selected == 'area':
  517. geo_buff_list = []
  518. try:
  519. for poly in working_obj:
  520. if tool_obj.app.abort_flag:
  521. # graceful abort requested by the user
  522. raise grace
  523. geo_buff_list.append(poly.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
  524. except TypeError:
  525. geo_buff_list.append(working_obj.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
  526. bounding_box = MultiPolygon(geo_buff_list)
  527. else: # ref_selected == 'box'
  528. geo_n = working_obj.solid_geometry
  529. if working_obj.kind == 'geometry':
  530. try:
  531. __ = iter(geo_n)
  532. except Exception as e:
  533. log.debug("ToolCopperFIll.copper_thieving() 'box' --> %s" % str(e))
  534. geo_n = [geo_n]
  535. geo_buff_list = []
  536. for poly in geo_n:
  537. if tool_obj.app.abort_flag:
  538. # graceful abort requested by the user
  539. raise grace
  540. geo_buff_list.append(poly.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
  541. bounding_box = unary_union(geo_buff_list)
  542. elif working_obj.kind == 'gerber':
  543. geo_n = unary_union(geo_n).convex_hull
  544. bounding_box = unary_union(thieving_obj.solid_geometry).convex_hull.intersection(geo_n)
  545. bounding_box = bounding_box.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
  546. else:
  547. tool_obj.app.inform.emit('[ERROR_NOTCL] %s' % _("The reference object type is not supported."))
  548. return 'fail'
  549. log.debug("Copper Thieving Tool. Finished creating areas to fill with copper.")
  550. tool_obj.app.inform.emit(_("Copper Thieving Tool. Appending new geometry and buffering."))
  551. # #########################################################################################################
  552. # Generate solid filling geometry. Effectively it's a NEGATIVE of the source object
  553. # #########################################################################################################
  554. tool_obj.thief_solid_geometry = bounding_box.difference(clearance_geometry)
  555. temp_geo = []
  556. try:
  557. for s_geo in tool_obj.thief_solid_geometry:
  558. if s_geo.area >= min_area:
  559. temp_geo.append(s_geo)
  560. except TypeError:
  561. if tool_obj.thief_solid_geometry.area >= min_area:
  562. temp_geo.append(tool_obj.thief_solid_geometry)
  563. tool_obj.thief_solid_geometry = temp_geo
  564. # #########################################################################################################
  565. # apply the 'margin' to the bounding box geometry
  566. # #########################################################################################################
  567. try:
  568. bounding_box = thieving_obj.solid_geometry.envelope.buffer(
  569. distance=margin,
  570. join_style=base.JOIN_STYLE.mitre
  571. )
  572. except AttributeError:
  573. bounding_box = MultiPolygon(thieving_obj.solid_geometry).envelope.buffer(
  574. distance=margin,
  575. join_style=base.JOIN_STYLE.mitre
  576. )
  577. x0, y0, x1, y1 = bounding_box.bounds
  578. # #########################################################################################################
  579. # add Thieving geometry
  580. # #########################################################################################################
  581. tool_obj.app.proc_container.update_view_text(' %s' % _("Create geometry"))
  582. if fill_type == 'dot' or fill_type == 'square':
  583. # build the MultiPolygon of dots/squares that will fill the entire bounding box
  584. thieving_list = []
  585. if fill_type == 'dot':
  586. radius = dot_dia / 2.0
  587. new_x = x0 + radius
  588. new_y = y0 + radius
  589. while new_x <= x1 - radius:
  590. while new_y <= y1 - radius:
  591. dot_geo = Point((new_x, new_y)).buffer(radius, resolution=64)
  592. thieving_list.append(dot_geo)
  593. new_y += dot_dia + dot_spacing
  594. new_x += dot_dia + dot_spacing
  595. new_y = y0 + radius
  596. else:
  597. h_size = square_size / 2.0
  598. new_x = x0 + h_size
  599. new_y = y0 + h_size
  600. while new_x <= x1 - h_size:
  601. while new_y <= y1 - h_size:
  602. a, b, c, d = (Point((new_x, new_y)).buffer(h_size)).bounds
  603. square_geo = box(a, b, c, d)
  604. thieving_list.append(square_geo)
  605. new_y += square_size + square_spacing
  606. new_x += square_size + square_spacing
  607. new_y = y0 + h_size
  608. thieving_box_geo = MultiPolygon(thieving_list)
  609. dx = bounding_box.centroid.x - thieving_box_geo.centroid.x
  610. dy = bounding_box.centroid.y - thieving_box_geo.centroid.y
  611. thieving_box_geo = affinity.translate(thieving_box_geo, xoff=dx, yoff=dy)
  612. try:
  613. _it = iter(thieving_box_geo)
  614. except TypeError:
  615. thieving_box_geo = [thieving_box_geo]
  616. thieving_geo = []
  617. for dot_geo in thieving_box_geo:
  618. for geo_t in tool_obj.thief_solid_geometry:
  619. if dot_geo.within(geo_t):
  620. thieving_geo.append(dot_geo)
  621. tool_obj.thief_solid_geometry = thieving_geo
  622. if fill_type == 'line':
  623. half_thick_line = line_size / 2.0
  624. # create a thick polygon-line that surrounds the copper features
  625. outline_geometry = []
  626. try:
  627. for pol in tool_obj.grb_object.solid_geometry:
  628. if tool_obj.app.abort_flag:
  629. # graceful abort requested by the user
  630. raise grace
  631. outline_geometry.append(
  632. pol.buffer(c_val+half_thick_line, int(int(tool_obj.geo_steps_per_circle) / 4))
  633. )
  634. pol_nr += 1
  635. disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
  636. if old_disp_number < disp_number <= 100:
  637. msg = ' %s ... %d%%' % (_("Buffering"), int(disp_number))
  638. tool_obj.app.proc_container.update_view_text(msg)
  639. old_disp_number = disp_number
  640. except TypeError:
  641. # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a
  642. # MultiPolygon (not an iterable)
  643. outline_geometry.append(
  644. tool_obj.grb_object.solid_geometry.buffer(
  645. c_val+half_thick_line,
  646. int(int(tool_obj.geo_steps_per_circle) / 4)
  647. )
  648. )
  649. tool_obj.app.proc_container.update_view_text(' %s' % _("Buffering"))
  650. outline_geometry = unary_union(outline_geometry)
  651. outline_line = []
  652. try:
  653. for geo_o in outline_geometry:
  654. outline_line.append(
  655. geo_o.exterior.buffer(
  656. half_thick_line, resolution=int(int(tool_obj.geo_steps_per_circle) / 4)
  657. )
  658. )
  659. except TypeError:
  660. outline_line.append(
  661. outline_geometry.exterior.buffer(
  662. half_thick_line, resolution=int(int(tool_obj.geo_steps_per_circle) / 4)
  663. )
  664. )
  665. outline_geometry = unary_union(outline_line)
  666. # create a polygon-line that surrounds in the inside the bounding box polygon of the target Gerber
  667. box_outline_geo = box(x0, y0, x1, y1).buffer(-half_thick_line)
  668. box_outline_geo_exterior = box_outline_geo.exterior
  669. box_outline_geometry = box_outline_geo_exterior.buffer(
  670. half_thick_line,
  671. resolution=int(int(tool_obj.geo_steps_per_circle) / 4)
  672. )
  673. bx0, by0, bx1, by1 = box_outline_geo.bounds
  674. thieving_lines_geo = []
  675. new_x = bx0
  676. new_y = by0
  677. while new_x <= x1 - half_thick_line:
  678. line_geo = LineString([(new_x, by0), (new_x, by1)]).buffer(
  679. half_thick_line,
  680. resolution=int(int(tool_obj.geo_steps_per_circle) / 4)
  681. )
  682. thieving_lines_geo.append(line_geo)
  683. new_x += line_size + line_spacing
  684. while new_y <= y1 - half_thick_line:
  685. line_geo = LineString([(bx0, new_y), (bx1, new_y)]).buffer(
  686. half_thick_line,
  687. resolution=int(int(tool_obj.geo_steps_per_circle) / 4)
  688. )
  689. thieving_lines_geo.append(line_geo)
  690. new_y += line_size + line_spacing
  691. # merge everything together
  692. diff_lines_geo = []
  693. for line_poly in thieving_lines_geo:
  694. rest_line = line_poly.difference(clearance_geometry)
  695. diff_lines_geo.append(rest_line)
  696. tool_obj.flatten([outline_geometry, box_outline_geometry, diff_lines_geo])
  697. tool_obj.thief_solid_geometry = tool_obj.flat_geometry
  698. tool_obj.app.proc_container.update_view_text(' %s' % _("Append geometry"))
  699. # create a list of the source geometry
  700. geo_list = deepcopy(tool_obj.grb_object.solid_geometry)
  701. if isinstance(tool_obj.grb_object.solid_geometry, MultiPolygon):
  702. geo_list = list(geo_list.geoms)
  703. # create a new dictionary to hold the source object apertures allowing us to tamper with without altering
  704. # the original source object's apertures
  705. new_apertures = deepcopy(tool_obj.grb_object.apertures)
  706. if '0' not in new_apertures:
  707. new_apertures['0'] = {
  708. 'type': 'REG',
  709. 'size': 0.0,
  710. 'geometry': []
  711. }
  712. # add the thieving geometry in the '0' aperture of the new_apertures dict
  713. try:
  714. for poly in tool_obj.thief_solid_geometry:
  715. # append to the new solid geometry
  716. geo_list.append(poly)
  717. # append into the '0' aperture
  718. geo_elem = {'solid': poly, 'follow': poly.exterior}
  719. new_apertures['0']['geometry'].append(deepcopy(geo_elem))
  720. except TypeError:
  721. # append to the new solid geometry
  722. geo_list.append(tool_obj.thief_solid_geometry)
  723. # append into the '0' aperture
  724. geo_elem = {'solid': tool_obj.new_solid_geometry, 'follow': tool_obj.new_solid_geometry.exterior}
  725. new_apertures['0']['geometry'].append(deepcopy(geo_elem))
  726. # prepare also the solid_geometry for the new object having the thieving geometry
  727. new_solid_geo = MultiPolygon(geo_list).buffer(0.0000001).buffer(-0.0000001)
  728. outname = '%s_%s' % (str(self.grb_object.options['name']), 'thief')
  729. def initialize(grb_obj, app_obj):
  730. grb_obj.options = {}
  731. for opt in self.grb_object.options:
  732. if opt != 'name':
  733. grb_obj.options[opt] = deepcopy(self.grb_object.options[opt])
  734. grb_obj.options['name'] = outname
  735. grb_obj.multitool = False
  736. grb_obj.multigeo = False
  737. grb_obj.follow = deepcopy(self.grb_object.follow)
  738. grb_obj.apertures = new_apertures
  739. grb_obj.solid_geometry = deepcopy(new_solid_geo)
  740. grb_obj.follow_geometry = deepcopy(self.grb_object.follow_geometry)
  741. app_obj.proc_container.update_view_text(' %s' % _("Append source file"))
  742. grb_obj.source_file = app_obj.f_handlers.export_gerber(obj_name=outname, filename=None,
  743. local_use=grb_obj,
  744. use_thread=False)
  745. ret_val = self.app.app_obj.new_object('gerber', outname, initialize, plot=True)
  746. tool_obj.app.proc_container.update_view_text(' %s' % '')
  747. if ret_val == 'fail':
  748. self.app.call_source = "app"
  749. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
  750. return
  751. tool_obj.on_exit()
  752. tool_obj.app.inform.emit('[success] %s' % _("Copper Thieving Tool done."))
  753. if run_threaded:
  754. self.app.worker_task.emit({'fcn': job_thread_thieving, 'params': [self]})
  755. else:
  756. job_thread_thieving(self)
  757. def on_add_ppm_click(self):
  758. run_threaded = True
  759. if run_threaded:
  760. self.app.proc_container.new('%s ...' % _("P-Plating Mask"))
  761. else:
  762. QtWidgets.QApplication.processEvents()
  763. self.app.proc_container.view.set_busy('%s ...' % _("P-Plating Mask"))
  764. if run_threaded:
  765. self.app.worker_task.emit({'fcn': self.on_new_pattern_plating_object, 'params': []})
  766. else:
  767. self.on_new_pattern_plating_object()
  768. def on_new_pattern_plating_object(self):
  769. ppm_clearance = self.ui.clearance_ppm_entry.get_value()
  770. geo_choice = self.ui.ppm_choice_radio.get_value()
  771. rb_thickness = self.rb_thickness
  772. # get the Gerber object on which the Copper thieving will be inserted
  773. selection_index = self.ui.sm_object_combo.currentIndex()
  774. model_index = self.app.collection.index(selection_index, 0, self.ui.sm_object_combo.rootModelIndex())
  775. try:
  776. self.sm_object = model_index.internalPointer().obj
  777. except Exception as e:
  778. log.debug("ToolCopperThieving.on_add_ppm_click() --> %s" % str(e))
  779. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
  780. return
  781. self.app.proc_container.update_view_text(' %s' % _("Append PP-M geometry"))
  782. geo_list = deepcopy(self.sm_object.solid_geometry)
  783. if isinstance(geo_list, MultiPolygon):
  784. geo_list = list(geo_list.geoms)
  785. # create a copy of the source apertures so we can manipulate them without altering the source object
  786. new_apertures = deepcopy(self.sm_object.apertures)
  787. # if the clearance is negative apply it to the original soldermask geometry too
  788. if ppm_clearance < 0:
  789. temp_geo_list = []
  790. for geo in geo_list:
  791. temp_geo_list.append(geo.buffer(ppm_clearance))
  792. geo_list = temp_geo_list
  793. # squash former geometry in apertures
  794. for ap_id in new_apertures:
  795. for k in new_apertures[ap_id]:
  796. if k == 'geometry':
  797. new_apertures[ap_id]['geometry'] = []
  798. # then add a buffered geometry
  799. for ap_id in new_apertures:
  800. if 'geometry' in self.sm_object.apertures[ap_id]:
  801. new_geo_list = []
  802. for geo_el in self.sm_object.apertures[ap_id]['geometry']:
  803. new_el = {
  804. 'solid': geo_el['solid'].buffer(ppm_clearance) if 'solid' in geo_el else [],
  805. 'follow': geo_el['follow'] if 'follow' in geo_el else [],
  806. 'clear': geo_el['clear'] if 'clear' in geo_el else []
  807. }
  808. new_geo_list.append(deepcopy(new_el))
  809. new_apertures[ap_id]['geometry'] = deepcopy(new_geo_list)
  810. # calculate its own plated area (from the solder mask object)
  811. plated_area = 0.0
  812. for geo in geo_list:
  813. plated_area += geo.area
  814. thieving_solid_geo = deepcopy(self.thief_solid_geometry)
  815. robber_solid_geo = deepcopy(self.robber_geo)
  816. robber_line = deepcopy(self.robber_line)
  817. # store here the chosen follow geometry
  818. new_follow_geo = deepcopy(self.sm_object.follow_geometry)
  819. # if we have copper thieving geometry, add it
  820. if thieving_solid_geo and geo_choice in ['b', 't']:
  821. # add to the total the thieving geometry area, if chosen
  822. for geo in thieving_solid_geo:
  823. plated_area += geo.area
  824. if '0' not in new_apertures:
  825. new_apertures['0'] = {
  826. 'type': 'REG',
  827. 'size': 0.0,
  828. 'geometry': []
  829. }
  830. try:
  831. for poly in thieving_solid_geo:
  832. poly_b = poly.buffer(ppm_clearance)
  833. # append to the new solid geometry
  834. geo_list.append(poly_b)
  835. # append into the '0' aperture
  836. geo_elem = {
  837. 'solid': poly_b,
  838. 'follow': poly_b.exterior
  839. }
  840. new_apertures['0']['geometry'].append(deepcopy(geo_elem))
  841. except TypeError:
  842. # append to the new solid geometry
  843. assert isinstance(thieving_solid_geo, Polygon)
  844. geo_list.append(thieving_solid_geo.buffer(ppm_clearance))
  845. # append into the '0' aperture
  846. geo_elem = {
  847. 'solid': thieving_solid_geo.buffer(ppm_clearance),
  848. 'follow': thieving_solid_geo.buffer(ppm_clearance).exterior
  849. }
  850. new_apertures['0']['geometry'].append(deepcopy(geo_elem))
  851. # if we have robber bar geometry, add it
  852. if robber_solid_geo and geo_choice in ['b', 'r']:
  853. # add to the total the robber bar geometry are, if chose
  854. plated_area += robber_solid_geo.area
  855. # add to the follow_geomery
  856. new_follow_geo.append(robber_line)
  857. aperture_found = None
  858. for ap_id, ap_val in new_apertures.items():
  859. if ap_val['type'] == 'C' and ap_val['size'] == self.rb_thickness + ppm_clearance:
  860. aperture_found = ap_id
  861. break
  862. if aperture_found:
  863. geo_elem = {'solid': robber_solid_geo, 'follow': robber_line}
  864. new_apertures[aperture_found]['geometry'].append(deepcopy(geo_elem))
  865. else:
  866. ap_keys = list(new_apertures.keys())
  867. max_apid = int(max(ap_keys))
  868. if ap_keys and max_apid != 0:
  869. new_apid = str(max_apid + 1)
  870. else:
  871. new_apid = '10'
  872. new_apertures[new_apid] = {
  873. 'type': 'C',
  874. 'size': rb_thickness + ppm_clearance,
  875. 'geometry': []
  876. }
  877. geo_elem = {
  878. 'solid': robber_solid_geo.buffer(ppm_clearance),
  879. 'follow': deepcopy(robber_line)
  880. }
  881. new_apertures[new_apid]['geometry'].append(deepcopy(geo_elem))
  882. geo_list.append(robber_solid_geo.buffer(ppm_clearance))
  883. # and then set the total plated area value to the GUI element
  884. self.ui.plated_area_entry.set_value(plated_area)
  885. new_solid_geometry = MultiPolygon(geo_list).buffer(0.0000001).buffer(-0.0000001)
  886. def obj_init(grb_obj, app_obj):
  887. grb_obj.options = {}
  888. for opt in self.sm_object.options:
  889. if opt != 'name':
  890. grb_obj.options[opt] = deepcopy(self.sm_object.options[opt])
  891. grb_obj.options['name'] = outname
  892. grb_obj.multitool = False
  893. grb_obj.source_file = []
  894. grb_obj.multigeo = False
  895. grb_obj.follow = False
  896. grb_obj.follow_geometry = deepcopy(new_follow_geo)
  897. grb_obj.apertures = deepcopy(new_apertures)
  898. grb_obj.solid_geometry = deepcopy(new_solid_geometry)
  899. app_obj.proc_container.update_view_text(' %s' % _("Append source file"))
  900. # update the source file with the new geometry:
  901. grb_obj.source_file = app_obj.f_handlers.export_gerber(obj_name=outname, filename=None, local_use=grb_obj,
  902. use_thread=False)
  903. app_obj.proc_container.update_view_text(' %s' % '')
  904. # Object name
  905. obj_name, separatpr, obj_extension = self.sm_object.options['name'].rpartition('.')
  906. outname = '%s_%s.%s' % (obj_name, 'plating_mask', obj_extension)
  907. ret_val = self.app.app_obj.new_object('gerber', outname, obj_init, autoselected=False)
  908. if ret_val == 'fail':
  909. self.app.call_source = "app"
  910. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
  911. return
  912. # Register recent file
  913. self.app.file_opened.emit("gerber", outname)
  914. self.on_exit()
  915. self.app.inform.emit('[success] %s' % _("Generating Pattern Plating Mask done."))
  916. def replot(self, obj, run_thread=True):
  917. def worker_task():
  918. with self.app.proc_container.new('%s...' % _("Plotting")):
  919. obj.plot()
  920. self.app.app_obj.object_plotted.emit(obj)
  921. if run_thread:
  922. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  923. else:
  924. worker_task()
  925. def on_exit(self, obj=None):
  926. # plot the objects
  927. if obj:
  928. try:
  929. for ob in obj:
  930. self.replot(obj=ob)
  931. except (AttributeError, TypeError):
  932. self.replot(obj=obj)
  933. except Exception:
  934. return
  935. # reset the variables
  936. self.sel_rect = []
  937. # Events ID
  938. self.mr = None
  939. self.mm = None
  940. # Mouse cursor positions
  941. self.mouse_is_dragging = False
  942. self.cursor_pos = (0, 0)
  943. self.first_click = False
  944. # if True it means we exited from tool in the middle of area adding therefore disconnect the events
  945. if self.handlers_connected is True:
  946. self.app.delete_selection_shape()
  947. self.disconnect_event_handlers()
  948. self.app.call_source = "app"
  949. self.app.inform.emit('[success] %s' % _("Copper Thieving Tool exit."))
  950. def connect_event_handlers(self):
  951. if self.handlers_connected is False:
  952. if self.app.is_legacy is False:
  953. self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
  954. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
  955. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
  956. else:
  957. self.app.plotcanvas.graph_event_disconnect(self.app.mp)
  958. self.app.plotcanvas.graph_event_disconnect(self.app.mm)
  959. self.app.plotcanvas.graph_event_disconnect(self.app.mr)
  960. self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
  961. self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
  962. self.handlers_connected = True
  963. def disconnect_event_handlers(self):
  964. if self.handlers_connected is True:
  965. if self.app.is_legacy is False:
  966. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
  967. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
  968. else:
  969. self.app.plotcanvas.graph_event_disconnect(self.mr)
  970. self.app.plotcanvas.graph_event_disconnect(self.mm)
  971. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
  972. self.app.on_mouse_click_over_plot)
  973. self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
  974. self.app.on_mouse_move_over_plot)
  975. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  976. self.app.on_mouse_click_release_over_plot)
  977. self.handlers_connected = False
  978. def flatten(self, geometry):
  979. """
  980. Creates a list of non-iterable linear geometry objects.
  981. :param geometry: Shapely type or list or list of list of such.
  982. Results are placed in self.flat_geometry
  983. """
  984. # ## If iterable, expand recursively.
  985. try:
  986. for geo in geometry:
  987. if geo is not None:
  988. self.flatten(geometry=geo)
  989. # ## Not iterable, do the actual indexing and add.
  990. except TypeError:
  991. self.flat_geometry.append(geometry)
  992. return self.flat_geometry
  993. class ThievingUI:
  994. toolName = _("Copper Thieving Tool")
  995. def __init__(self, layout, app):
  996. self.app = app
  997. self.decimals = self.app.decimals
  998. self.units = self.app.defaults['units']
  999. self.layout = layout
  1000. # ## Title
  1001. title_label = FCLabel("%s" % self.toolName)
  1002. title_label.setStyleSheet("""
  1003. QLabel
  1004. {
  1005. font-size: 16px;
  1006. font-weight: bold;
  1007. }
  1008. """)
  1009. self.layout.addWidget(title_label)
  1010. self.layout.addWidget(FCLabel(""))
  1011. # ## Grid Layout
  1012. i_grid_lay = QtWidgets.QGridLayout()
  1013. self.layout.addLayout(i_grid_lay)
  1014. i_grid_lay.setColumnStretch(0, 0)
  1015. i_grid_lay.setColumnStretch(1, 1)
  1016. self.grb_object_combo = FCComboBox()
  1017. self.grb_object_combo.setModel(self.app.collection)
  1018. self.grb_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  1019. self.grb_object_combo.is_last = True
  1020. self.grb_object_combo.obj_type = 'Gerber'
  1021. self.grbobj_label = FCLabel("<b>%s:</b>" % _("GERBER"))
  1022. self.grbobj_label.setToolTip(
  1023. _("Gerber Object to which will be added a copper thieving.")
  1024. )
  1025. i_grid_lay.addWidget(self.grbobj_label, 0, 0)
  1026. i_grid_lay.addWidget(self.grb_object_combo, 1, 0, 1, 2)
  1027. separator_line = QtWidgets.QFrame()
  1028. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  1029. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  1030. i_grid_lay.addWidget(separator_line, 2, 0, 1, 2)
  1031. # ## Grid Layout
  1032. grid_lay = QtWidgets.QGridLayout()
  1033. self.layout.addLayout(grid_lay)
  1034. grid_lay.setColumnStretch(0, 0)
  1035. grid_lay.setColumnStretch(1, 1)
  1036. self.copper_fill_label = FCLabel('<b>%s</b>' % _('Parameters'))
  1037. self.copper_fill_label.setToolTip(
  1038. _("Parameters used for this tool.")
  1039. )
  1040. grid_lay.addWidget(self.copper_fill_label, 0, 0, 1, 2)
  1041. # CLEARANCE #
  1042. self.clearance_label = FCLabel('%s:' % _("Clearance"))
  1043. self.clearance_label.setToolTip(
  1044. _("This set the distance between the copper thieving components\n"
  1045. "(the polygon fill may be split in multiple polygons)\n"
  1046. "and the copper traces in the Gerber file.")
  1047. )
  1048. self.clearance_entry = FCDoubleSpinner(callback=self.confirmation_message)
  1049. self.clearance_entry.set_range(0.00001, 10000.0000)
  1050. self.clearance_entry.set_precision(self.decimals)
  1051. self.clearance_entry.setSingleStep(0.1)
  1052. grid_lay.addWidget(self.clearance_label, 2, 0)
  1053. grid_lay.addWidget(self.clearance_entry, 2, 1)
  1054. # MARGIN #
  1055. self.margin_label = FCLabel('%s:' % _("Margin"))
  1056. self.margin_label.setToolTip(
  1057. _("Bounding box margin.")
  1058. )
  1059. self.margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
  1060. self.margin_entry.set_range(0.0, 10000.0000)
  1061. self.margin_entry.set_precision(self.decimals)
  1062. self.margin_entry.setSingleStep(0.1)
  1063. grid_lay.addWidget(self.margin_label, 4, 0)
  1064. grid_lay.addWidget(self.margin_entry, 4, 1)
  1065. # Area #
  1066. area_hlay = QtWidgets.QHBoxLayout()
  1067. self.area_label = FCLabel('%s:' % _("Area"))
  1068. self.area_label.setToolTip(
  1069. _("Thieving areas with area less then this value will not be added.")
  1070. )
  1071. self.area_entry = FCDoubleSpinner(callback=self.confirmation_message)
  1072. self.area_entry.set_range(0.0, 10000.0000)
  1073. self.area_entry.set_precision(self.decimals)
  1074. self.area_entry.setSingleStep(0.1)
  1075. self.area_entry.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred)
  1076. if self.units.upper() == 'MM':
  1077. units_area_label = FCLabel('%s<sup>2</sup>' % _("mm"))
  1078. else:
  1079. units_area_label = FCLabel('%s<sup>2</sup>' % _("in"))
  1080. area_hlay.addWidget(self.area_entry)
  1081. area_hlay.addWidget(units_area_label)
  1082. grid_lay.addWidget(self.area_label, 6, 0)
  1083. grid_lay.addLayout(area_hlay, 6, 1)
  1084. # Reference #
  1085. self.reference_radio = RadioSet([
  1086. {'label': _('Itself'), 'value': 'itself'},
  1087. {"label": _("Area Selection"), "value": "area"},
  1088. {'label': _("Reference Object"), 'value': 'box'}
  1089. ], orientation='vertical', stretch=False)
  1090. self.reference_label = FCLabel(_("Reference:"))
  1091. self.reference_label.setToolTip(
  1092. _("- 'Itself' - the copper thieving extent is based on the object extent.\n"
  1093. "- 'Area Selection' - left mouse click to start selection of the area to be filled.\n"
  1094. "- 'Reference Object' - will do copper thieving within the area specified by another object.")
  1095. )
  1096. grid_lay.addWidget(self.reference_label, 8, 0)
  1097. grid_lay.addWidget(self.reference_radio, 8, 1)
  1098. self.ref_combo_type_label = FCLabel('%s:' % _("Ref. Type"))
  1099. self.ref_combo_type_label.setToolTip(
  1100. _("The type of FlatCAM object to be used as copper thieving reference.\n"
  1101. "It can be Gerber, Excellon or Geometry.")
  1102. )
  1103. self.ref_combo_type = FCComboBox()
  1104. self.ref_combo_type.addItems([_("Gerber"), _("Excellon"), _("Geometry")])
  1105. grid_lay.addWidget(self.ref_combo_type_label, 10, 0)
  1106. grid_lay.addWidget(self.ref_combo_type, 10, 1)
  1107. self.ref_combo_label = FCLabel('%s:' % _("Ref. Object"))
  1108. self.ref_combo_label.setToolTip(
  1109. _("The FlatCAM object to be used as non copper clearing reference.")
  1110. )
  1111. self.ref_combo = FCComboBox()
  1112. self.ref_combo.setModel(self.app.collection)
  1113. self.ref_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  1114. self.ref_combo.is_last = True
  1115. self.ref_combo.obj_type = {
  1116. _("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry"
  1117. }[self.ref_combo_type.get_value()]
  1118. grid_lay.addWidget(self.ref_combo_label, 12, 0)
  1119. grid_lay.addWidget(self.ref_combo, 12, 1)
  1120. self.ref_combo.hide()
  1121. self.ref_combo_label.hide()
  1122. self.ref_combo_type.hide()
  1123. self.ref_combo_type_label.hide()
  1124. # Bounding Box Type #
  1125. self.bbox_type_label = FCLabel(_("Box Type:"))
  1126. self.bbox_type_label.setToolTip(
  1127. _("- 'Rectangular' - the bounding box will be of rectangular shape.\n"
  1128. "- 'Minimal' - the bounding box will be the convex hull shape.")
  1129. )
  1130. self.bbox_type_radio = RadioSet([
  1131. {'label': _('Rectangular'), 'value': 'rect'},
  1132. {"label": _("Minimal"), "value": "min"}
  1133. ], stretch=False)
  1134. grid_lay.addWidget(self.bbox_type_label, 14, 0)
  1135. grid_lay.addWidget(self.bbox_type_radio, 14, 1)
  1136. self.bbox_type_label.hide()
  1137. self.bbox_type_radio.hide()
  1138. separator_line = QtWidgets.QFrame()
  1139. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  1140. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  1141. grid_lay.addWidget(separator_line, 16, 0, 1, 2)
  1142. # Fill Type
  1143. self.fill_type_radio = RadioSet([
  1144. {'label': _('Solid'), 'value': 'solid'},
  1145. {"label": _("Dots Grid"), "value": "dot"},
  1146. {"label": _("Squares Grid"), "value": "square"},
  1147. {"label": _("Lines Grid"), "value": "line"}
  1148. ], orientation='vertical', stretch=False)
  1149. self.fill_type_label = FCLabel(_("Fill Type:"))
  1150. self.fill_type_label.setToolTip(
  1151. _("- 'Solid' - copper thieving will be a solid polygon.\n"
  1152. "- 'Dots Grid' - the empty area will be filled with a pattern of dots.\n"
  1153. "- 'Squares Grid' - the empty area will be filled with a pattern of squares.\n"
  1154. "- 'Lines Grid' - the empty area will be filled with a pattern of lines.")
  1155. )
  1156. grid_lay.addWidget(self.fill_type_label, 18, 0)
  1157. grid_lay.addWidget(self.fill_type_radio, 18, 1)
  1158. # DOTS FRAME
  1159. self.dots_frame = QtWidgets.QFrame()
  1160. self.dots_frame.setContentsMargins(0, 0, 0, 0)
  1161. self.layout.addWidget(self.dots_frame)
  1162. dots_grid = QtWidgets.QGridLayout()
  1163. dots_grid.setColumnStretch(0, 0)
  1164. dots_grid.setColumnStretch(1, 1)
  1165. dots_grid.setContentsMargins(0, 0, 0, 0)
  1166. self.dots_frame.setLayout(dots_grid)
  1167. self.dots_frame.hide()
  1168. self.dots_label = FCLabel('<b>%s</b>:' % _("Dots Grid Parameters"))
  1169. dots_grid.addWidget(self.dots_label, 0, 0, 1, 2)
  1170. # Dot diameter #
  1171. self.dotdia_label = FCLabel('%s:' % _("Dia"))
  1172. self.dotdia_label.setToolTip(
  1173. _("Dot diameter in Dots Grid.")
  1174. )
  1175. self.dot_dia_entry = FCDoubleSpinner(callback=self.confirmation_message)
  1176. self.dot_dia_entry.set_range(0.0, 10000.0000)
  1177. self.dot_dia_entry.set_precision(self.decimals)
  1178. self.dot_dia_entry.setSingleStep(0.1)
  1179. dots_grid.addWidget(self.dotdia_label, 1, 0)
  1180. dots_grid.addWidget(self.dot_dia_entry, 1, 1)
  1181. # Dot spacing #
  1182. self.dotspacing_label = FCLabel('%s:' % _("Spacing"))
  1183. self.dotspacing_label.setToolTip(
  1184. _("Distance between each two dots in Dots Grid.")
  1185. )
  1186. self.dot_spacing_entry = FCDoubleSpinner(callback=self.confirmation_message)
  1187. self.dot_spacing_entry.set_range(0.0, 10000.0000)
  1188. self.dot_spacing_entry.set_precision(self.decimals)
  1189. self.dot_spacing_entry.setSingleStep(0.1)
  1190. dots_grid.addWidget(self.dotspacing_label, 2, 0)
  1191. dots_grid.addWidget(self.dot_spacing_entry, 2, 1)
  1192. # SQUARES FRAME
  1193. self.squares_frame = QtWidgets.QFrame()
  1194. self.squares_frame.setContentsMargins(0, 0, 0, 0)
  1195. self.layout.addWidget(self.squares_frame)
  1196. squares_grid = QtWidgets.QGridLayout()
  1197. squares_grid.setColumnStretch(0, 0)
  1198. squares_grid.setColumnStretch(1, 1)
  1199. squares_grid.setContentsMargins(0, 0, 0, 0)
  1200. self.squares_frame.setLayout(squares_grid)
  1201. self.squares_frame.hide()
  1202. self.squares_label = FCLabel('<b>%s</b>:' % _("Squares Grid Parameters"))
  1203. squares_grid.addWidget(self.squares_label, 0, 0, 1, 2)
  1204. # Square Size #
  1205. self.square_size_label = FCLabel('%s:' % _("Size"))
  1206. self.square_size_label.setToolTip(
  1207. _("Square side size in Squares Grid.")
  1208. )
  1209. self.square_size_entry = FCDoubleSpinner(callback=self.confirmation_message)
  1210. self.square_size_entry.set_range(0.0, 10000.0000)
  1211. self.square_size_entry.set_precision(self.decimals)
  1212. self.square_size_entry.setSingleStep(0.1)
  1213. squares_grid.addWidget(self.square_size_label, 1, 0)
  1214. squares_grid.addWidget(self.square_size_entry, 1, 1)
  1215. # Squares spacing #
  1216. self.squares_spacing_label = FCLabel('%s:' % _("Spacing"))
  1217. self.squares_spacing_label.setToolTip(
  1218. _("Distance between each two squares in Squares Grid.")
  1219. )
  1220. self.squares_spacing_entry = FCDoubleSpinner(callback=self.confirmation_message)
  1221. self.squares_spacing_entry.set_range(0.0, 10000.0000)
  1222. self.squares_spacing_entry.set_precision(self.decimals)
  1223. self.squares_spacing_entry.setSingleStep(0.1)
  1224. squares_grid.addWidget(self.squares_spacing_label, 2, 0)
  1225. squares_grid.addWidget(self.squares_spacing_entry, 2, 1)
  1226. # LINES FRAME
  1227. self.lines_frame = QtWidgets.QFrame()
  1228. self.lines_frame.setContentsMargins(0, 0, 0, 0)
  1229. self.layout.addWidget(self.lines_frame)
  1230. lines_grid = QtWidgets.QGridLayout()
  1231. lines_grid.setColumnStretch(0, 0)
  1232. lines_grid.setColumnStretch(1, 1)
  1233. lines_grid.setContentsMargins(0, 0, 0, 0)
  1234. self.lines_frame.setLayout(lines_grid)
  1235. self.lines_frame.hide()
  1236. self.lines_label = FCLabel('<b>%s</b>:' % _("Lines Grid Parameters"))
  1237. lines_grid.addWidget(self.lines_label, 0, 0, 1, 2)
  1238. # Square Size #
  1239. self.line_size_label = FCLabel('%s:' % _("Size"))
  1240. self.line_size_label.setToolTip(
  1241. _("Line thickness size in Lines Grid.")
  1242. )
  1243. self.line_size_entry = FCDoubleSpinner(callback=self.confirmation_message)
  1244. self.line_size_entry.set_range(0.0, 10000.0000)
  1245. self.line_size_entry.set_precision(self.decimals)
  1246. self.line_size_entry.setSingleStep(0.1)
  1247. lines_grid.addWidget(self.line_size_label, 1, 0)
  1248. lines_grid.addWidget(self.line_size_entry, 1, 1)
  1249. # Lines spacing #
  1250. self.lines_spacing_label = FCLabel('%s:' % _("Spacing"))
  1251. self.lines_spacing_label.setToolTip(
  1252. _("Distance between each two lines in Lines Grid.")
  1253. )
  1254. self.lines_spacing_entry = FCDoubleSpinner(callback=self.confirmation_message)
  1255. self.lines_spacing_entry.set_range(0.0, 10000.0000)
  1256. self.lines_spacing_entry.set_precision(self.decimals)
  1257. self.lines_spacing_entry.setSingleStep(0.1)
  1258. lines_grid.addWidget(self.lines_spacing_label, 2, 0)
  1259. lines_grid.addWidget(self.lines_spacing_entry, 2, 1)
  1260. # ## Insert Copper Thieving
  1261. self.fill_button = QtWidgets.QPushButton(_("Insert Copper thieving"))
  1262. self.fill_button.setIcon(QtGui.QIcon(self.app.resource_location + '/copperfill32.png'))
  1263. self.fill_button.setToolTip(
  1264. _("Will add a polygon (may be split in multiple parts)\n"
  1265. "that will surround the actual Gerber traces at a certain distance.")
  1266. )
  1267. self.fill_button.setStyleSheet("""
  1268. QPushButton
  1269. {
  1270. font-weight: bold;
  1271. }
  1272. """)
  1273. self.layout.addWidget(self.fill_button)
  1274. # ## Grid Layout
  1275. grid_lay_1 = QtWidgets.QGridLayout()
  1276. self.layout.addLayout(grid_lay_1)
  1277. grid_lay_1.setColumnStretch(0, 0)
  1278. grid_lay_1.setColumnStretch(1, 1)
  1279. grid_lay_1.setColumnStretch(2, 0)
  1280. separator_line_1 = QtWidgets.QFrame()
  1281. separator_line_1.setFrameShape(QtWidgets.QFrame.HLine)
  1282. separator_line_1.setFrameShadow(QtWidgets.QFrame.Sunken)
  1283. grid_lay_1.addWidget(separator_line_1, 0, 0, 1, 3)
  1284. grid_lay_1.addWidget(FCLabel(''))
  1285. self.robber_bar_label = FCLabel('<b>%s</b>' % _('Robber Bar Parameters'))
  1286. self.robber_bar_label.setToolTip(
  1287. _("Parameters used for the robber bar.\n"
  1288. "Robber bar = copper border to help in pattern hole plating.")
  1289. )
  1290. grid_lay_1.addWidget(self.robber_bar_label, 2, 0, 1, 3)
  1291. # ROBBER BAR MARGIN #
  1292. self.rb_margin_label = FCLabel('%s:' % _("Margin"))
  1293. self.rb_margin_label.setToolTip(
  1294. _("Bounding box margin for robber bar.")
  1295. )
  1296. self.rb_margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
  1297. self.rb_margin_entry.set_range(-10000.0000, 10000.0000)
  1298. self.rb_margin_entry.set_precision(self.decimals)
  1299. self.rb_margin_entry.setSingleStep(0.1)
  1300. grid_lay_1.addWidget(self.rb_margin_label, 4, 0)
  1301. grid_lay_1.addWidget(self.rb_margin_entry, 4, 1, 1, 2)
  1302. # THICKNESS #
  1303. self.rb_thickness_label = FCLabel('%s:' % _("Thickness"))
  1304. self.rb_thickness_label.setToolTip(
  1305. _("The robber bar thickness.")
  1306. )
  1307. self.rb_thickness_entry = FCDoubleSpinner(callback=self.confirmation_message)
  1308. self.rb_thickness_entry.set_range(0.0000, 10000.0000)
  1309. self.rb_thickness_entry.set_precision(self.decimals)
  1310. self.rb_thickness_entry.setSingleStep(0.1)
  1311. grid_lay_1.addWidget(self.rb_thickness_label, 6, 0)
  1312. grid_lay_1.addWidget(self.rb_thickness_entry, 6, 1, 1, 2)
  1313. # ## Insert Robber Bar
  1314. self.rb_button = QtWidgets.QPushButton(_("Insert Robber Bar"))
  1315. self.rb_button.setIcon(QtGui.QIcon(self.app.resource_location + '/robber32.png'))
  1316. self.rb_button.setToolTip(
  1317. _("Will add a polygon with a defined thickness\n"
  1318. "that will surround the actual Gerber object\n"
  1319. "at a certain distance.\n"
  1320. "Required when doing holes pattern plating.")
  1321. )
  1322. self.rb_button.setStyleSheet("""
  1323. QPushButton
  1324. {
  1325. font-weight: bold;
  1326. }
  1327. """)
  1328. grid_lay_1.addWidget(self.rb_button, 8, 0, 1, 3)
  1329. separator_line_2 = QtWidgets.QFrame()
  1330. separator_line_2.setFrameShape(QtWidgets.QFrame.HLine)
  1331. separator_line_2.setFrameShadow(QtWidgets.QFrame.Sunken)
  1332. grid_lay_1.addWidget(separator_line_2, 10, 0, 1, 3)
  1333. self.patern_mask_label = FCLabel('<b>%s</b>' % _('Pattern Plating Mask'))
  1334. self.patern_mask_label.setToolTip(
  1335. _("Generate a mask for pattern plating.")
  1336. )
  1337. grid_lay_1.addWidget(self.patern_mask_label, 12, 0, 1, 3)
  1338. self.sm_obj_label = FCLabel("%s:" % _("Select Soldermask object"))
  1339. self.sm_obj_label.setToolTip(
  1340. _("Gerber Object with the soldermask.\n"
  1341. "It will be used as a base for\n"
  1342. "the pattern plating mask.")
  1343. )
  1344. self.sm_object_combo = FCComboBox()
  1345. self.sm_object_combo.setModel(self.app.collection)
  1346. self.sm_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  1347. self.sm_object_combo.is_last = True
  1348. self.sm_object_combo.obj_type = 'Gerber'
  1349. grid_lay_1.addWidget(self.sm_obj_label, 14, 0, 1, 3)
  1350. grid_lay_1.addWidget(self.sm_object_combo, 16, 0, 1, 3)
  1351. # Openings CLEARANCE #
  1352. self.clearance_ppm_label = FCLabel('%s:' % _("Clearance"))
  1353. self.clearance_ppm_label.setToolTip(
  1354. _("The distance between the possible copper thieving elements\n"
  1355. "and/or robber bar and the actual openings in the mask.")
  1356. )
  1357. self.clearance_ppm_entry = FCDoubleSpinner(callback=self.confirmation_message)
  1358. self.clearance_ppm_entry.set_range(-10000.0000, 10000.0000)
  1359. self.clearance_ppm_entry.set_precision(self.decimals)
  1360. self.clearance_ppm_entry.setSingleStep(0.1)
  1361. grid_lay_1.addWidget(self.clearance_ppm_label, 18, 0)
  1362. grid_lay_1.addWidget(self.clearance_ppm_entry, 18, 1, 1, 2)
  1363. # Plated area
  1364. self.plated_area_label = FCLabel('%s:' % _("Plated area"))
  1365. self.plated_area_label.setToolTip(
  1366. _("The area to be plated by pattern plating.\n"
  1367. "Basically is made from the openings in the plating mask.\n\n"
  1368. "<<WARNING>> - the calculated area is actually a bit larger\n"
  1369. "due of the fact that the soldermask openings are by design\n"
  1370. "a bit larger than the copper pads, and this area is\n"
  1371. "calculated from the soldermask openings.")
  1372. )
  1373. self.plated_area_entry = FCEntry()
  1374. self.plated_area_entry.setDisabled(True)
  1375. if self.units.upper() == 'MM':
  1376. self.units_area_label = FCLabel('%s<sup>2</sup>' % _("mm"))
  1377. else:
  1378. self.units_area_label = FCLabel('%s<sup>2</sup>' % _("in"))
  1379. grid_lay_1.addWidget(self.plated_area_label, 20, 0)
  1380. grid_lay_1.addWidget(self.plated_area_entry, 20, 1)
  1381. grid_lay_1.addWidget(self.units_area_label, 20, 2)
  1382. # Include geometry
  1383. self.ppm_choice_label = FCLabel('%s:' % _("Add"))
  1384. self.ppm_choice_label.setToolTip(
  1385. _("Choose which additional geometry to include, if available.")
  1386. )
  1387. self.ppm_choice_radio = RadioSet([
  1388. {"label": _("Both"), "value": "b"},
  1389. {'label': _('Thieving'), 'value': 't'},
  1390. {"label": _("Robber bar"), "value": "r"},
  1391. {"label": _("None"), "value": "n"}
  1392. ], orientation='vertical', stretch=False)
  1393. grid_lay_1.addWidget(self.ppm_choice_label, 22, 0)
  1394. grid_lay_1.addWidget(self.ppm_choice_radio, 22, 1, 1, 2)
  1395. # ## Pattern Plating Mask
  1396. self.ppm_button = QtWidgets.QPushButton(_("Generate pattern plating mask"))
  1397. self.ppm_button.setIcon(QtGui.QIcon(self.app.resource_location + '/pattern32.png'))
  1398. self.ppm_button.setToolTip(
  1399. _("Will add to the soldermask gerber geometry\n"
  1400. "the geometries of the copper thieving and/or\n"
  1401. "the robber bar if those were generated.")
  1402. )
  1403. self.ppm_button.setStyleSheet("""
  1404. QPushButton
  1405. {
  1406. font-weight: bold;
  1407. }
  1408. """)
  1409. grid_lay_1.addWidget(self.ppm_button, 24, 0, 1, 3)
  1410. self.layout.addStretch()
  1411. # ## Reset Tool
  1412. self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
  1413. self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
  1414. self.reset_button.setToolTip(
  1415. _("Will reset the tool parameters.")
  1416. )
  1417. self.reset_button.setStyleSheet("""
  1418. QPushButton
  1419. {
  1420. font-weight: bold;
  1421. }
  1422. """)
  1423. self.layout.addWidget(self.reset_button)
  1424. # #################################### FINSIHED GUI ###########################
  1425. # #############################################################################
  1426. def confirmation_message(self, accepted, minval, maxval):
  1427. if accepted is False:
  1428. self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
  1429. self.decimals,
  1430. minval,
  1431. self.decimals,
  1432. maxval), False)
  1433. else:
  1434. self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
  1435. def confirmation_message_int(self, accepted, minval, maxval):
  1436. if accepted is False:
  1437. self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
  1438. (_("Edited value is out of range"), minval, maxval), False)
  1439. else:
  1440. self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)