ToolCopperThieving.py 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079
  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
  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. self.layout.addStretch()
  280. # Objects involved in Copper thieving
  281. self.grb_object = None
  282. self.ref_obj = None
  283. self.sel_rect = list()
  284. # Events ID
  285. self.mr = None
  286. self.mm = None
  287. # Mouse cursor positions
  288. self.mouse_is_dragging = False
  289. self.cursor_pos = (0, 0)
  290. self.first_click = False
  291. self.area_method = False
  292. # Tool properties
  293. self.clearance_val = None
  294. self.margin_val = None
  295. self.geo_steps_per_circle = 128
  296. # SIGNALS
  297. self.fill_button.clicked.connect(self.execute)
  298. self.box_combo_type.currentIndexChanged.connect(self.on_combo_box_type)
  299. self.reference_radio.group_toggle_fn = self.on_toggle_reference
  300. self.fill_type_radio.activated_custom.connect(self.on_thieving_type)
  301. def run(self, toggle=True):
  302. self.app.report_usage("ToolCopperThieving()")
  303. if toggle:
  304. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  305. if self.app.ui.splitter.sizes()[0] == 0:
  306. self.app.ui.splitter.setSizes([1, 1])
  307. else:
  308. try:
  309. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  310. # if tab is populated with the tool but it does not have the focus, focus on it
  311. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  312. # focus on Tool Tab
  313. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  314. else:
  315. self.app.ui.splitter.setSizes([0, 1])
  316. except AttributeError:
  317. pass
  318. else:
  319. if self.app.ui.splitter.sizes()[0] == 0:
  320. self.app.ui.splitter.setSizes([1, 1])
  321. FlatCAMTool.run(self)
  322. self.set_tool_ui()
  323. self.app.ui.notebook.setTabText(2, _("Copper Thieving Tool"))
  324. def install(self, icon=None, separator=None, **kwargs):
  325. FlatCAMTool.install(self, icon, separator, shortcut='ALT+F', **kwargs)
  326. def set_tool_ui(self):
  327. self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value()
  328. self.clearance_entry.set_value(float(self.app.defaults["tools_copper_thieving_clearance"]))
  329. self.margin_entry.set_value(float(self.app.defaults["tools_copper_thieving_margin"]))
  330. self.reference_radio.set_value(self.app.defaults["tools_copper_thieving_reference"])
  331. self.bbox_type_radio.set_value(self.app.defaults["tools_copper_thieving_box_type"])
  332. self.fill_type_radio.set_value(self.app.defaults["tools_copper_thieving_fill_type"])
  333. self.geo_steps_per_circle = int(self.app.defaults["tools_copper_thieving_circle_steps"])
  334. self.dot_dia_entry.set_value(self.app.defaults["tools_copper_thieving_dots_dia"])
  335. self.dot_spacing_entry.set_value(self.app.defaults["tools_copper_thieving_dots_spacing"])
  336. self.square_size_entry.set_value(self.app.defaults["tools_copper_thieving_squares_size"])
  337. self.squares_spacing_entry.set_value(self.app.defaults["tools_copper_thieving_squares_spacing"])
  338. self.line_size_entry.set_value(self.app.defaults["tools_copper_thieving_lines_size"])
  339. self.lines_spacing_entry.set_value(self.app.defaults["tools_copper_thieving_lines_spacing"])
  340. self.area_method = False
  341. def on_combo_box_type(self):
  342. obj_type = self.box_combo_type.currentIndex()
  343. self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
  344. self.box_combo.setCurrentIndex(0)
  345. def on_toggle_reference(self):
  346. if self.reference_radio.get_value() == "itself" or self.reference_radio.get_value() == "area":
  347. self.box_combo.hide()
  348. self.box_combo_label.hide()
  349. self.box_combo_type.hide()
  350. self.box_combo_type_label.hide()
  351. else:
  352. self.box_combo.show()
  353. self.box_combo_label.show()
  354. self.box_combo_type.show()
  355. self.box_combo_type_label.show()
  356. if self.reference_radio.get_value() == "itself":
  357. self.bbox_type_label.show()
  358. self.bbox_type_radio.show()
  359. else:
  360. self.bbox_type_label.hide()
  361. self.bbox_type_radio.hide()
  362. def on_thieving_type(self, choice):
  363. if choice == 'solid':
  364. self.dots_frame.hide()
  365. self.squares_frame.hide()
  366. self.lines_frame.hide()
  367. elif choice == 'dot':
  368. self.dots_frame.show()
  369. self.squares_frame.hide()
  370. self.lines_frame.hide()
  371. elif choice == 'square':
  372. self.dots_frame.hide()
  373. self.squares_frame.show()
  374. self.lines_frame.hide()
  375. else:
  376. self.dots_frame.hide()
  377. self.squares_frame.hide()
  378. self.lines_frame.show()
  379. def execute(self):
  380. self.app.call_source = "copper_thieving_tool"
  381. self.clearance_val = self.clearance_entry.get_value()
  382. self.margin_val = self.margin_entry.get_value()
  383. reference_method = self.reference_radio.get_value()
  384. # get the Gerber object on which the Copper thieving will be inserted
  385. selection_index = self.grb_object_combo.currentIndex()
  386. model_index = self.app.collection.index(selection_index, 0, self.grb_object_combo.rootModelIndex())
  387. try:
  388. self.grb_object = model_index.internalPointer().obj
  389. except Exception as e:
  390. log.debug("ToolCopperThieving.execute() --> %s" % str(e))
  391. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
  392. return 'fail'
  393. if reference_method == 'itself':
  394. bound_obj_name = self.grb_object_combo.currentText()
  395. # Get reference object.
  396. try:
  397. self.ref_obj = self.app.collection.get_by_name(bound_obj_name)
  398. except Exception as e:
  399. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(e)))
  400. return "Could not retrieve object: %s" % self.obj_name
  401. self.on_copper_thieving(
  402. thieving_obj=self.grb_object,
  403. c_val=self.clearance_val,
  404. margin=self.margin_val
  405. )
  406. elif reference_method == 'area':
  407. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area."))
  408. self.area_method = True
  409. if self.app.is_legacy is False:
  410. self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
  411. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
  412. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
  413. else:
  414. self.app.plotcanvas.graph_event_disconnect(self.app.mp)
  415. self.app.plotcanvas.graph_event_disconnect(self.app.mm)
  416. self.app.plotcanvas.graph_event_disconnect(self.app.mr)
  417. self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
  418. self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
  419. elif reference_method == 'box':
  420. bound_obj_name = self.box_combo.currentText()
  421. # Get reference object.
  422. try:
  423. self.ref_obj = self.app.collection.get_by_name(bound_obj_name)
  424. except Exception as e:
  425. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), bound_obj_name))
  426. return "Could not retrieve object: %s. Error: %s" % (bound_obj_name, str(e))
  427. self.on_copper_thieving(
  428. thieving_obj=self.grb_object,
  429. ref_obj=self.ref_obj,
  430. c_val=self.clearance_val,
  431. margin=self.margin_val
  432. )
  433. # To be called after clicking on the plot.
  434. def on_mouse_release(self, event):
  435. if self.app.is_legacy is False:
  436. event_pos = event.pos
  437. # event_is_dragging = event.is_dragging
  438. right_button = 2
  439. else:
  440. event_pos = (event.xdata, event.ydata)
  441. # event_is_dragging = self.app.plotcanvas.is_dragging
  442. right_button = 3
  443. event_pos = self.app.plotcanvas.translate_coords(event_pos)
  444. # do clear area only for left mouse clicks
  445. if event.button == 1:
  446. if self.first_click is False:
  447. self.first_click = True
  448. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the end point of the filling area."))
  449. self.cursor_pos = self.app.plotcanvas.translate_coords(event_pos)
  450. if self.app.grid_status() is True:
  451. self.cursor_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
  452. else:
  453. self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish."))
  454. self.app.delete_selection_shape()
  455. if self.app.grid_status() is True:
  456. curr_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
  457. else:
  458. curr_pos = (event_pos[0], event_pos[1])
  459. x0, y0 = self.cursor_pos[0], self.cursor_pos[1]
  460. x1, y1 = curr_pos[0], curr_pos[1]
  461. pt1 = (x0, y0)
  462. pt2 = (x1, y0)
  463. pt3 = (x1, y1)
  464. pt4 = (x0, y1)
  465. new_rectangle = Polygon([pt1, pt2, pt3, pt4])
  466. self.sel_rect.append(new_rectangle)
  467. # add a temporary shape on canvas
  468. self.draw_tool_selection_shape(old_coords=(x0, y0), coords=(x1, y1))
  469. self.first_click = False
  470. return
  471. elif event.button == right_button and self.mouse_is_dragging is False:
  472. self.area_method = False
  473. self.first_click = False
  474. self.delete_tool_selection_shape()
  475. if self.app.is_legacy is False:
  476. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
  477. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
  478. else:
  479. self.app.plotcanvas.graph_event_disconnect(self.mr)
  480. self.app.plotcanvas.graph_event_disconnect(self.mm)
  481. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
  482. self.app.on_mouse_click_over_plot)
  483. self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
  484. self.app.on_mouse_move_over_plot)
  485. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  486. self.app.on_mouse_click_release_over_plot)
  487. if len(self.sel_rect) == 0:
  488. return
  489. self.sel_rect = cascaded_union(self.sel_rect)
  490. if not isinstance(self.sel_rect, Iterable):
  491. self.sel_rect = [self.sel_rect]
  492. self.on_copper_thieving(
  493. thieving_obj=self.grb_object,
  494. ref_obj=self.sel_rect,
  495. c_val=self.clearance_val,
  496. margin=self.margin_val
  497. )
  498. # called on mouse move
  499. def on_mouse_move(self, event):
  500. if self.app.is_legacy is False:
  501. event_pos = event.pos
  502. event_is_dragging = event.is_dragging
  503. # right_button = 2
  504. else:
  505. event_pos = (event.xdata, event.ydata)
  506. event_is_dragging = self.app.plotcanvas.is_dragging
  507. # right_button = 3
  508. curr_pos = self.app.plotcanvas.translate_coords(event_pos)
  509. # detect mouse dragging motion
  510. if event_is_dragging is True:
  511. self.mouse_is_dragging = True
  512. else:
  513. self.mouse_is_dragging = False
  514. # update the cursor position
  515. if self.app.grid_status() is True:
  516. # Update cursor
  517. curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
  518. self.app.app_cursor.set_data(np.asarray([(curr_pos[0], curr_pos[1])]),
  519. symbol='++', edge_color=self.app.cursor_color_3D,
  520. size=self.app.defaults["global_cursor_size"])
  521. # update the positions on status bar
  522. self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp; "
  523. "<b>Y</b>: %.4f" % (curr_pos[0], curr_pos[1]))
  524. if self.cursor_pos is None:
  525. self.cursor_pos = (0, 0)
  526. dx = curr_pos[0] - float(self.cursor_pos[0])
  527. dy = curr_pos[1] - float(self.cursor_pos[1])
  528. self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp; <b>Dy</b>: "
  529. "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (dx, dy))
  530. # draw the utility geometry
  531. if self.first_click:
  532. self.app.delete_selection_shape()
  533. self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]),
  534. coords=(curr_pos[0], curr_pos[1]))
  535. def on_copper_thieving(self, thieving_obj, ref_obj=None, c_val=None, margin=None, run_threaded=True):
  536. """
  537. :param thieving_obj:
  538. :param ref_obj:
  539. :param c_val:
  540. :param margin:
  541. :param run_threaded:
  542. :return:
  543. """
  544. if run_threaded:
  545. proc = self.app.proc_container.new('%s ...' % _("Copper thieving"))
  546. else:
  547. self.app.proc_container.view.set_busy('%s ...' % _("Copper thieving"))
  548. QtWidgets.QApplication.processEvents()
  549. # #####################################################################
  550. # ####### Read the parameters #########################################
  551. # #####################################################################
  552. log.debug("Copper Thieving Tool started. Reading parameters.")
  553. self.app.inform.emit(_("Copper Thieving Tool started. Reading parameters."))
  554. ref_selected = self.reference_radio.get_value()
  555. if c_val is None:
  556. c_val = float(self.app.defaults["tools_copperfill_clearance"])
  557. if margin is None:
  558. margin = float(self.app.defaults["tools_copperfill_margin"])
  559. fill_type = self.fill_type_radio.get_value()
  560. dot_dia = self.dot_dia_entry.get_value()
  561. dot_spacing = self.dot_spacing_entry.get_value()
  562. square_size = self.square_size_entry.get_value()
  563. square_spacing = self.squares_spacing_entry.get_value()
  564. line_size = self.line_size_entry.get_value()
  565. line_spacing = self.lines_spacing_entry.get_value()
  566. # make sure that the source object solid geometry is an Iterable
  567. if not isinstance(self.grb_object.solid_geometry, Iterable):
  568. self.grb_object.solid_geometry = [self.grb_object.solid_geometry]
  569. # #########################################################################################
  570. # Prepare isolation polygon. This will create the clearance over the Gerber features ######
  571. # #########################################################################################
  572. log.debug("Copper Thieving Tool. Preparing isolation polygons.")
  573. self.app.inform.emit(_("Copper Thieving Tool. Preparing isolation polygons."))
  574. # variables to display the percentage of work done
  575. geo_len = 0
  576. try:
  577. for pol in self.grb_object.solid_geometry:
  578. geo_len += 1
  579. except TypeError:
  580. geo_len = 1
  581. old_disp_number = 0
  582. pol_nr = 0
  583. clearance_geometry = []
  584. try:
  585. for pol in self.grb_object.solid_geometry:
  586. if self.app.abort_flag:
  587. # graceful abort requested by the user
  588. raise FlatCAMApp.GracefulException
  589. clearance_geometry.append(
  590. pol.buffer(c_val, int(int(self.geo_steps_per_circle) / 4))
  591. )
  592. pol_nr += 1
  593. disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
  594. if old_disp_number < disp_number <= 100:
  595. self.app.proc_container.update_view_text(' %s ... %d%%' %
  596. (_("Buffering"), int(disp_number)))
  597. old_disp_number = disp_number
  598. except TypeError:
  599. # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a
  600. # MultiPolygon (not an iterable)
  601. clearance_geometry.append(
  602. self.grb_object.solid_geometry.buffer(c_val, int(int(self.geo_steps_per_circle) / 4))
  603. )
  604. self.app.proc_container.update_view_text(' %s' % _("Buffering"))
  605. clearance_geometry = unary_union(clearance_geometry)
  606. # #########################################################################################
  607. # Prepare the area to fill with copper. ###################################################
  608. # #########################################################################################
  609. log.debug("Copper Thieving Tool. Preparing areas to fill with copper.")
  610. self.app.inform.emit(_("Copper Thieving Tool. Preparing areas to fill with copper."))
  611. try:
  612. if ref_obj is None or ref_obj == 'itself':
  613. working_obj = thieving_obj
  614. else:
  615. working_obj = ref_obj
  616. except Exception as e:
  617. log.debug("ToolCopperThieving.on_copper_thieving() --> %s" % str(e))
  618. return 'fail'
  619. bounding_box = None
  620. if ref_selected == 'itself':
  621. geo_n = working_obj.solid_geometry
  622. try:
  623. if self.bbox_type_radio.get_value() == 'min':
  624. if isinstance(geo_n, MultiPolygon):
  625. env_obj = geo_n.convex_hull
  626. elif (isinstance(geo_n, MultiPolygon) and len(geo_n) == 1) or \
  627. (isinstance(geo_n, list) and len(geo_n) == 1) and isinstance(geo_n[0], Polygon):
  628. env_obj = cascaded_union(geo_n)
  629. else:
  630. env_obj = cascaded_union(geo_n)
  631. env_obj = env_obj.convex_hull
  632. else:
  633. if isinstance(geo_n, Polygon):
  634. env_obj = geo_n.buffer(0, join_style=base.JOIN_STYLE.mitre).exterior
  635. elif isinstance(geo_n, list):
  636. geo_n = unary_union(geo_n)
  637. env_obj = geo_n.buffer(0, join_style=base.JOIN_STYLE.mitre).exterior
  638. elif isinstance(geo_n, MultiPolygon):
  639. x0, y0, x1, y1 = geo_n.bounds
  640. geo = box(x0, y0, x1, y1)
  641. env_obj = geo.buffer(0, join_style=base.JOIN_STYLE.mitre)
  642. else:
  643. self.app.inform.emit(
  644. '[ERROR_NOTCL] %s: %s' % (_("Geometry not supported for bounding box"), type(geo_n))
  645. )
  646. return 'fail'
  647. bounding_box = env_obj.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
  648. except Exception as e:
  649. log.debug("ToolCopperFIll.on_copper_thieving() 'itself' --> %s" % str(e))
  650. self.app.inform.emit('[ERROR_NOTCL] %s' % _("No object available."))
  651. return 'fail'
  652. elif ref_selected == 'area':
  653. geo_n = cascaded_union(working_obj)
  654. try:
  655. __ = iter(geo_n)
  656. except Exception as e:
  657. log.debug("ToolCopperFIll.on_copper_thieving() 'area' --> %s" % str(e))
  658. geo_n = [geo_n]
  659. geo_buff_list = []
  660. for poly in geo_n:
  661. if self.app.abort_flag:
  662. # graceful abort requested by the user
  663. raise FlatCAMApp.GracefulException
  664. geo_buff_list.append(poly.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
  665. bounding_box = cascaded_union(geo_buff_list)
  666. else: # ref_selected == 'box'
  667. geo_n = working_obj.solid_geometry
  668. if isinstance(working_obj, FlatCAMGeometry):
  669. try:
  670. __ = iter(geo_n)
  671. except Exception as e:
  672. log.debug("ToolCopperFIll.on_copper_thieving() 'box' --> %s" % str(e))
  673. geo_n = [geo_n]
  674. geo_buff_list = []
  675. for poly in geo_n:
  676. if self.app.abort_flag:
  677. # graceful abort requested by the user
  678. raise FlatCAMApp.GracefulException
  679. geo_buff_list.append(poly.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
  680. bounding_box = cascaded_union(geo_buff_list)
  681. elif isinstance(working_obj, FlatCAMGerber):
  682. geo_n = cascaded_union(geo_n).convex_hull
  683. bounding_box = cascaded_union(thieving_obj.solid_geometry).convex_hull.intersection(geo_n)
  684. bounding_box = bounding_box.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
  685. else:
  686. self.app.inform.emit('[ERROR_NOTCL] %s' % _("The reference object type is not supported."))
  687. return 'fail'
  688. log.debug("Copper Thieving Tool. Finished creating areas to fill with copper.")
  689. self.app.inform.emit(_("Copper Thieving Tool. Appending new geometry and buffering."))
  690. # #########################################################################################
  691. # ########## Generate filling geometry. ###################################################
  692. # #########################################################################################
  693. new_solid_geometry = bounding_box.difference(clearance_geometry)
  694. # determine the bounding box polygon for the entire Gerber object to which we add copper thieving
  695. if isinstance(geo_n, list):
  696. env_obj = unary_union(geo_n).buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
  697. else:
  698. env_obj = geo_n.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
  699. x0, y0, x1, y1 = env_obj.bounds
  700. bounding_box = box(x0, y0, x1, y1)
  701. if fill_type == 'dot' or fill_type == 'square':
  702. # build the MultiPolygon of dots/squares that will fill the entire bounding box
  703. thieving_list = list()
  704. if fill_type == 'dot':
  705. radius = dot_dia / 2.0
  706. new_x = x0 + radius
  707. new_y = y0 + radius
  708. while new_x <= x1 - radius:
  709. while new_y <= y1 - radius:
  710. dot_geo = Point((new_x, new_y)).buffer(radius, resolution=64)
  711. thieving_list.append(dot_geo)
  712. new_y += dot_dia + dot_spacing
  713. new_x += dot_dia + dot_spacing
  714. new_y = y0 + radius
  715. else:
  716. h_size = square_size / 2.0
  717. new_x = x0 + h_size
  718. new_y = y0 + h_size
  719. while new_x <= x1 - h_size:
  720. while new_y <= y1 - h_size:
  721. a, b, c, d = (Point((new_x, new_y)).buffer(h_size)).bounds
  722. square_geo = box(a, b, c, d)
  723. thieving_list.append(square_geo)
  724. new_y += square_size + square_spacing
  725. new_x += square_size + square_spacing
  726. new_y = y0 + h_size
  727. thieving_box_geo = MultiPolygon(thieving_list)
  728. dx = bounding_box.centroid.x - thieving_box_geo.centroid.x
  729. dy = bounding_box.centroid.y - thieving_box_geo.centroid.y
  730. thieving_box_geo = affinity.translate(thieving_box_geo, xoff=dx, yoff=dy)
  731. try:
  732. _it = iter(new_solid_geometry)
  733. except TypeError:
  734. new_solid_geometry = [new_solid_geometry]
  735. try:
  736. _it = iter(thieving_box_geo)
  737. except TypeError:
  738. thieving_box_geo = [thieving_box_geo]
  739. thieving_geo = list()
  740. for dot_geo in thieving_box_geo:
  741. for geo_t in new_solid_geometry:
  742. if dot_geo.within(geo_t):
  743. thieving_geo.append(dot_geo)
  744. new_solid_geometry = thieving_geo
  745. if fill_type == 'line':
  746. half_thick_line = line_size / 2.0
  747. # create a thick polygon-line that surrounds the copper features
  748. outline_geometry = []
  749. try:
  750. for pol in self.grb_object.solid_geometry:
  751. if self.app.abort_flag:
  752. # graceful abort requested by the user
  753. raise FlatCAMApp.GracefulException
  754. outline_geometry.append(
  755. pol.buffer(c_val+half_thick_line, int(int(self.geo_steps_per_circle) / 4))
  756. )
  757. pol_nr += 1
  758. disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
  759. if old_disp_number < disp_number <= 100:
  760. self.app.proc_container.update_view_text(' %s ... %d%%' %
  761. (_("Buffering"), int(disp_number)))
  762. old_disp_number = disp_number
  763. except TypeError:
  764. # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a
  765. # MultiPolygon (not an iterable)
  766. outline_geometry.append(
  767. self.grb_object.solid_geometry.buffer(
  768. c_val+half_thick_line,
  769. int(int(self.geo_steps_per_circle) / 4)
  770. )
  771. )
  772. self.app.proc_container.update_view_text(' %s' % _("Buffering"))
  773. outline_geometry = unary_union(outline_geometry)
  774. outline_line = list()
  775. try:
  776. for geo_o in outline_geometry:
  777. outline_line.append(
  778. geo_o.exterior.buffer(
  779. half_thick_line, resolution=int(int(self.geo_steps_per_circle) / 4)
  780. )
  781. )
  782. except TypeError:
  783. outline_line.append(
  784. outline_geometry.exterior.buffer(
  785. half_thick_line, resolution=int(int(self.geo_steps_per_circle) / 4)
  786. )
  787. )
  788. outline_geometry = unary_union(outline_line)
  789. # create a polygon-line that surrounds in the inside the bounding box polygon of the target Gerber
  790. box_outline_geo = box(x0, y0, x1, y1).buffer(-half_thick_line)
  791. box_outline_geo_exterior = box_outline_geo.exterior
  792. box_outline_geometry = box_outline_geo_exterior.buffer(
  793. half_thick_line,
  794. resolution=int(int(self.geo_steps_per_circle) / 4)
  795. )
  796. bx0, by0, bx1, by1 = box_outline_geo.bounds
  797. thieving_lines_geo = list()
  798. new_x = bx0
  799. new_y = by0
  800. while new_x <= x1 - half_thick_line:
  801. line_geo = LineString([(new_x, by0), (new_x, by1)]).buffer(
  802. half_thick_line,
  803. resolution=int(int(self.geo_steps_per_circle) / 4)
  804. )
  805. thieving_lines_geo.append(line_geo)
  806. new_x += line_size + line_spacing
  807. while new_y <= y1 - half_thick_line:
  808. line_geo = LineString([(bx0, new_y), (bx1, new_y)]).buffer(
  809. half_thick_line,
  810. resolution=int(int(self.geo_steps_per_circle) / 4)
  811. )
  812. thieving_lines_geo.append(line_geo)
  813. new_y += line_size + line_spacing
  814. thieving_box_geo = cascaded_union(thieving_lines_geo)
  815. # merge everything together
  816. diff_lines_geo = thieving_box_geo.difference(clearance_geometry)
  817. new_solid_geometry = unary_union([outline_geometry, box_outline_geometry, diff_lines_geo])
  818. geo_list = self.grb_object.solid_geometry
  819. if isinstance(self.grb_object.solid_geometry, MultiPolygon):
  820. geo_list = list(self.grb_object.solid_geometry.geoms)
  821. if '0' not in self.grb_object.apertures:
  822. self.grb_object.apertures['0'] = dict()
  823. self.grb_object.apertures['0']['geometry'] = list()
  824. self.grb_object.apertures['0']['type'] = 'REG'
  825. self.grb_object.apertures['0']['size'] = 0.0
  826. try:
  827. for poly in new_solid_geometry:
  828. # append to the new solid geometry
  829. geo_list.append(poly)
  830. # append into the '0' aperture
  831. geo_elem = dict()
  832. geo_elem['solid'] = poly
  833. geo_elem['follow'] = poly.exterior
  834. self.grb_object.apertures['0']['geometry'].append(deepcopy(geo_elem))
  835. except TypeError:
  836. # append to the new solid geometry
  837. geo_list.append(new_solid_geometry)
  838. # append into the '0' aperture
  839. geo_elem = dict()
  840. geo_elem['solid'] = new_solid_geometry
  841. geo_elem['follow'] = new_solid_geometry.exterior
  842. self.grb_object.apertures['0']['geometry'].append(deepcopy(geo_elem))
  843. self.grb_object.solid_geometry = MultiPolygon(geo_list).buffer(0.0000001).buffer(-0.0000001)
  844. # update the source file with the new geometry:
  845. self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'], filename=None,
  846. local_use=self.grb_object, use_thread=False)
  847. self.on_exit()
  848. self.app.inform.emit('[success] %s' % _("Copper Thieving Tool done."))
  849. def replot(self, obj):
  850. def worker_task():
  851. with self.app.proc_container.new('%s...' % _("Plotting")):
  852. obj.plot()
  853. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  854. def on_exit(self):
  855. # plot the object
  856. self.replot(obj=self.grb_object)
  857. # update the bounding box values
  858. try:
  859. a, b, c, d = self.grb_object.bounds()
  860. self.grb_object.options['xmin'] = a
  861. self.grb_object.options['ymin'] = b
  862. self.grb_object.options['xmax'] = c
  863. self.grb_object.options['ymax'] = d
  864. except Exception as e:
  865. log.debug("ToolCopperThieving.on_exit() bounds error --> %s" % str(e))
  866. # reset the variables
  867. self.grb_object = None
  868. self.ref_obj = None
  869. self.sel_rect = list()
  870. # Events ID
  871. self.mr = None
  872. self.mm = None
  873. # Mouse cursor positions
  874. self.mouse_is_dragging = False
  875. self.cursor_pos = (0, 0)
  876. self.first_click = False
  877. # if True it means we exited from tool in the middle of area adding therefore disconnect the events
  878. if self.area_method is True:
  879. self.app.delete_selection_shape()
  880. self.area_method = False
  881. if self.app.is_legacy is False:
  882. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
  883. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
  884. else:
  885. self.app.plotcanvas.graph_event_disconnect(self.mr)
  886. self.app.plotcanvas.graph_event_disconnect(self.mm)
  887. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
  888. self.app.on_mouse_click_over_plot)
  889. self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
  890. self.app.on_mouse_move_over_plot)
  891. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  892. self.app.on_mouse_click_release_over_plot)
  893. self.app.call_source = "app"
  894. self.app.inform.emit('[success] %s' % _("Copper Thieving Tool exit."))