ToolCopperThieving.py 75 KB

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