ToolCopperThieving.py 65 KB

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