ToolCopperThieving.py 68 KB

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