ToolCopperFill.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  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
  15. from shapely.geometry import box as box
  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 ToolCopperFill(FlatCAMTool):
  28. toolName = _("Copper Fill Tool")
  29. def __init__(self, app):
  30. FlatCAMTool.__init__(self, app)
  31. self.app = app
  32. self.canvas = self.app.plotcanvas
  33. self.decimals = 4
  34. self.units = ''
  35. # ## Title
  36. title_label = QtWidgets.QLabel("%s" % self.toolName)
  37. title_label.setStyleSheet("""
  38. QLabel
  39. {
  40. font-size: 16px;
  41. font-weight: bold;
  42. }
  43. """)
  44. self.layout.addWidget(title_label)
  45. self.layout.addWidget(QtWidgets.QLabel(''))
  46. # ## Grid Layout
  47. i_grid_lay = QtWidgets.QGridLayout()
  48. self.layout.addLayout(i_grid_lay)
  49. i_grid_lay.setColumnStretch(0, 0)
  50. i_grid_lay.setColumnStretch(1, 1)
  51. self.grb_object_combo = QtWidgets.QComboBox()
  52. self.grb_object_combo.setModel(self.app.collection)
  53. self.grb_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  54. self.grb_object_combo.setCurrentIndex(1)
  55. self.grbobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
  56. self.grbobj_label.setToolTip(
  57. _("Gerber Object to which will be added a copper fill.")
  58. )
  59. i_grid_lay.addWidget(self.grbobj_label, 0, 0)
  60. i_grid_lay.addWidget(self.grb_object_combo, 0, 1, 1, 2)
  61. i_grid_lay.addWidget(QtWidgets.QLabel(''), 1, 0)
  62. # ## Grid Layout
  63. grid_lay = QtWidgets.QGridLayout()
  64. self.layout.addLayout(grid_lay)
  65. grid_lay.setColumnStretch(0, 0)
  66. grid_lay.setColumnStretch(1, 1)
  67. self.copper_fill_label = QtWidgets.QLabel('<b>%s</b>' % _('Parameters'))
  68. self.copper_fill_label.setToolTip(
  69. _("Parameters used for this tool.")
  70. )
  71. grid_lay.addWidget(self.copper_fill_label, 0, 0, 1, 2)
  72. # CLEARANCE #
  73. self.clearance_label = QtWidgets.QLabel('%s:' % _("Clearance"))
  74. self.clearance_label.setToolTip(
  75. _("This set the distance between the copper fill components\n"
  76. "(the polygon fill may be split in multiple polygons)\n"
  77. "and the copper traces in the Gerber file.")
  78. )
  79. self.clearance_entry = FCDoubleSpinner()
  80. self.clearance_entry.setMinimum(0.00001)
  81. self.clearance_entry.set_precision(self.decimals)
  82. self.clearance_entry.setSingleStep(0.1)
  83. grid_lay.addWidget(self.clearance_label, 1, 0)
  84. grid_lay.addWidget(self.clearance_entry, 1, 1)
  85. # MARGIN #
  86. self.margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
  87. self.margin_label.setToolTip(
  88. _("Bounding box margin.")
  89. )
  90. self.margin_entry = FCDoubleSpinner()
  91. self.margin_entry.setMinimum(0.0)
  92. self.margin_entry.set_precision(self.decimals)
  93. self.margin_entry.setSingleStep(0.1)
  94. grid_lay.addWidget(self.margin_label, 2, 0)
  95. grid_lay.addWidget(self.margin_entry, 2, 1)
  96. # Reference #
  97. self.reference_radio = RadioSet([
  98. {'label': _('Itself'), 'value': 'itself'},
  99. {"label": _("Area Selection"), "value": "area"},
  100. {'label': _("Reference Object"), 'value': 'box'}
  101. ], orientation='vertical', stretch=False)
  102. self.reference_label = QtWidgets.QLabel(_("Reference:"))
  103. self.reference_label.setToolTip(
  104. _("- 'Itself' - the copper fill extent is based on the object that is copper cleared.\n "
  105. "- 'Area Selection' - left mouse click to start selection of the area to be filled.\n"
  106. "- 'Reference Object' - will do copper filling within the area specified by another object.")
  107. )
  108. grid_lay.addWidget(self.reference_label, 3, 0)
  109. grid_lay.addWidget(self.reference_radio, 3, 1)
  110. self.box_combo_type_label = QtWidgets.QLabel('%s:' % _("Ref. Type"))
  111. self.box_combo_type_label.setToolTip(
  112. _("The type of FlatCAM object to be used as copper filling reference.\n"
  113. "It can be Gerber, Excellon or Geometry.")
  114. )
  115. self.box_combo_type = QtWidgets.QComboBox()
  116. self.box_combo_type.addItem(_("Reference Gerber"))
  117. self.box_combo_type.addItem(_("Reference Excellon"))
  118. self.box_combo_type.addItem(_("Reference Geometry"))
  119. grid_lay.addWidget(self.box_combo_type_label, 4, 0)
  120. grid_lay.addWidget(self.box_combo_type, 4, 1)
  121. self.box_combo_label = QtWidgets.QLabel('%s:' % _("Ref. Object"))
  122. self.box_combo_label.setToolTip(
  123. _("The FlatCAM object to be used as non copper clearing reference.")
  124. )
  125. self.box_combo = QtWidgets.QComboBox()
  126. self.box_combo.setModel(self.app.collection)
  127. self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  128. self.box_combo.setCurrentIndex(1)
  129. grid_lay.addWidget(self.box_combo_label, 5, 0)
  130. grid_lay.addWidget(self.box_combo, 5, 1)
  131. self.box_combo.hide()
  132. self.box_combo_label.hide()
  133. self.box_combo_type.hide()
  134. self.box_combo_type_label.hide()
  135. # Bounding Box Type #
  136. self.bbox_type_radio = RadioSet([
  137. {'label': _('Rectangular'), 'value': 'rect'},
  138. {"label": _("Minimal"), "value": "min"}
  139. ], stretch=False)
  140. self.bbox_type_label = QtWidgets.QLabel(_("Box Type:"))
  141. self.bbox_type_label.setToolTip(
  142. _("- 'Rectangular' - the bounding box will be of rectangular shape.\n "
  143. "- 'Minimal' - the bounding box will be the convex hull shape.")
  144. )
  145. grid_lay.addWidget(self.bbox_type_label, 6, 0)
  146. grid_lay.addWidget(self.bbox_type_radio, 6, 1)
  147. self.bbox_type_label.hide()
  148. self.bbox_type_radio.hide()
  149. # ## Insert Copper Fill
  150. self.fill_button = QtWidgets.QPushButton(_("Insert Copper Fill"))
  151. self.fill_button.setToolTip(
  152. _("Will add a polygon (may be split in multiple parts)\n"
  153. "that will surround the actual Gerber traces at a certain distance.")
  154. )
  155. self.layout.addWidget(self.fill_button)
  156. self.layout.addStretch()
  157. # Objects involved in Copper filling
  158. self.grb_object = None
  159. self.ref_obj = None
  160. self.sel_rect = list()
  161. # Events ID
  162. self.mr = None
  163. self.mm = None
  164. # Mouse cursor positions
  165. self.mouse_is_dragging = False
  166. self.cursor_pos = (0, 0)
  167. self.first_click = False
  168. self.area_method = False
  169. # Tool properties
  170. self.clearance_val = None
  171. self.margin_val = None
  172. self.geo_steps_per_circle = 128
  173. # SIGNALS
  174. self.fill_button.clicked.connect(self.execute)
  175. self.box_combo_type.currentIndexChanged.connect(self.on_combo_box_type)
  176. self.reference_radio.group_toggle_fn = self.on_toggle_reference
  177. def run(self, toggle=True):
  178. self.app.report_usage("ToolCopperFill()")
  179. if toggle:
  180. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  181. if self.app.ui.splitter.sizes()[0] == 0:
  182. self.app.ui.splitter.setSizes([1, 1])
  183. else:
  184. try:
  185. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  186. # if tab is populated with the tool but it does not have the focus, focus on it
  187. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  188. # focus on Tool Tab
  189. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  190. else:
  191. self.app.ui.splitter.setSizes([0, 1])
  192. except AttributeError:
  193. pass
  194. else:
  195. if self.app.ui.splitter.sizes()[0] == 0:
  196. self.app.ui.splitter.setSizes([1, 1])
  197. FlatCAMTool.run(self)
  198. self.set_tool_ui()
  199. self.app.ui.notebook.setTabText(2, _("Copper Fill Tool"))
  200. def install(self, icon=None, separator=None, **kwargs):
  201. FlatCAMTool.install(self, icon, separator, shortcut='ALT+F', **kwargs)
  202. def set_tool_ui(self):
  203. self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value()
  204. # self.clearance_entry.set_value(float(self.app.defaults["tools_copperfill_clearance"]))
  205. # self.margin_entry.set_value(float(self.app.defaults["tools_copperfill_margin"]))
  206. # self.reference_radio.set_value(self.app.defaults["tools_copperfill_reference"])
  207. # self.geo_steps_per_circle = int(self.app.defaults["tools_copperfill_circle_steps"])
  208. # self.bbox_type_radio.set_value(self.app.defaults["tools_copperfill_box_type"])
  209. self.clearance_entry.set_value(0.5)
  210. self.margin_entry.set_value(1.0)
  211. self.reference_radio.set_value('itself')
  212. self.bbox_type_radio.set_value('rect')
  213. self.area_method = False
  214. def on_combo_box_type(self):
  215. obj_type = self.box_combo_type.currentIndex()
  216. self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
  217. self.box_combo.setCurrentIndex(0)
  218. def on_toggle_reference(self):
  219. if self.reference_radio.get_value() == "itself" or self.reference_radio.get_value() == "area":
  220. self.box_combo.hide()
  221. self.box_combo_label.hide()
  222. self.box_combo_type.hide()
  223. self.box_combo_type_label.hide()
  224. else:
  225. self.box_combo.show()
  226. self.box_combo_label.show()
  227. self.box_combo_type.show()
  228. self.box_combo_type_label.show()
  229. if self.reference_radio.get_value() == "itself":
  230. self.bbox_type_label.show()
  231. self.bbox_type_radio.show()
  232. else:
  233. self.bbox_type_label.hide()
  234. self.bbox_type_radio.hide()
  235. def execute(self):
  236. self.app.call_source = "copperfill_tool"
  237. self.clearance_val = self.clearance_entry.get_value()
  238. self.margin_val = self.margin_entry.get_value()
  239. reference_method = self.reference_radio.get_value()
  240. # get the Gerber object on which the Copper fill will be inserted
  241. selection_index = self.grb_object_combo.currentIndex()
  242. model_index = self.app.collection.index(selection_index, 0, self.grb_object_combo.rootModelIndex())
  243. try:
  244. self.grb_object = model_index.internalPointer().obj
  245. except Exception as e:
  246. log.debug("ToolCopperFill.execute() --> %s" % str(e))
  247. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
  248. return 'fail'
  249. if reference_method == 'itself':
  250. bound_obj_name = self.grb_object_combo.currentText()
  251. # Get reference object.
  252. try:
  253. self.ref_obj = self.app.collection.get_by_name(bound_obj_name)
  254. except Exception as e:
  255. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(e)))
  256. return "Could not retrieve object: %s" % self.obj_name
  257. self.on_copper_fill(
  258. fill_obj=self.grb_object,
  259. c_val=self.clearance_val,
  260. margin=self.margin_val
  261. )
  262. elif reference_method == 'area':
  263. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area."))
  264. self.area_method = True
  265. if self.app.is_legacy is False:
  266. self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
  267. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
  268. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
  269. else:
  270. self.app.plotcanvas.graph_event_disconnect(self.app.mp)
  271. self.app.plotcanvas.graph_event_disconnect(self.app.mm)
  272. self.app.plotcanvas.graph_event_disconnect(self.app.mr)
  273. self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
  274. self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
  275. elif reference_method == 'box':
  276. bound_obj_name = self.box_combo.currentText()
  277. # Get reference object.
  278. try:
  279. self.ref_obj = self.app.collection.get_by_name(bound_obj_name)
  280. except Exception as e:
  281. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), bound_obj_name))
  282. return "Could not retrieve object: %s. Error: %s" % (bound_obj_name, str(e))
  283. self.on_copper_fill(
  284. fill_obj=self.grb_object,
  285. ref_obj=self.ref_obj,
  286. c_val=self.clearance_val,
  287. margin=self.margin_val
  288. )
  289. # To be called after clicking on the plot.
  290. def on_mouse_release(self, event):
  291. if self.app.is_legacy is False:
  292. event_pos = event.pos
  293. event_is_dragging = event.is_dragging
  294. right_button = 2
  295. else:
  296. event_pos = (event.xdata, event.ydata)
  297. event_is_dragging = self.app.plotcanvas.is_dragging
  298. right_button = 3
  299. event_pos = self.app.plotcanvas.translate_coords(event_pos)
  300. # do clear area only for left mouse clicks
  301. if event.button == 1:
  302. if self.first_click is False:
  303. self.first_click = True
  304. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the end point of the filling area."))
  305. self.cursor_pos = self.app.plotcanvas.translate_coords(event_pos)
  306. if self.app.grid_status() == True:
  307. self.cursor_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
  308. else:
  309. self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish."))
  310. if self.app.grid_status() == True:
  311. curr_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
  312. else:
  313. curr_pos = (event_pos[0], event_pos[1])
  314. x0, y0 = self.cursor_pos[0], self.cursor_pos[1]
  315. x1, y1 = curr_pos[0], curr_pos[1]
  316. pt1 = (x0, y0)
  317. pt2 = (x1, y0)
  318. pt3 = (x1, y1)
  319. pt4 = (x0, y1)
  320. self.sel_rect.append(Polygon([pt1, pt2, pt3, pt4]))
  321. self.first_click = False
  322. return
  323. elif event.button == right_button and self.mouse_is_dragging == False:
  324. self.app.delete_selection_shape()
  325. self.area_method = False
  326. self.first_click = False
  327. if self.app.is_legacy is False:
  328. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
  329. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
  330. else:
  331. self.app.plotcanvas.graph_event_disconnect(self.mr)
  332. self.app.plotcanvas.graph_event_disconnect(self.mm)
  333. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
  334. self.app.on_mouse_click_over_plot)
  335. self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
  336. self.app.on_mouse_move_over_plot)
  337. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  338. self.app.on_mouse_click_release_over_plot)
  339. if len(self.sel_rect) == 0:
  340. return
  341. self.sel_rect = cascaded_union(self.sel_rect)
  342. if not isinstance(self.sel_rect, Iterable):
  343. self.sel_rect = [self.sel_rect]
  344. self.on_copper_fill(
  345. fill_obj=self.grb_object,
  346. ref_obj=self.sel_rect,
  347. c_val=self.clearance_val,
  348. margin=self.margin_val
  349. )
  350. # called on mouse move
  351. def on_mouse_move(self, event):
  352. if self.app.is_legacy is False:
  353. event_pos = event.pos
  354. event_is_dragging = event.is_dragging
  355. right_button = 2
  356. else:
  357. event_pos = (event.xdata, event.ydata)
  358. event_is_dragging = self.app.plotcanvas.is_dragging
  359. right_button = 3
  360. curr_pos = self.app.plotcanvas.translate_coords(event_pos)
  361. # detect mouse dragging motion
  362. if event_is_dragging is True:
  363. self.mouse_is_dragging = True
  364. else:
  365. self.mouse_is_dragging = False
  366. # update the cursor position
  367. if self.app.grid_status() == True:
  368. # Update cursor
  369. curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
  370. self.app.app_cursor.set_data(np.asarray([(curr_pos[0], curr_pos[1])]),
  371. symbol='++', edge_color=self.app.cursor_color_3D,
  372. size=self.app.defaults["global_cursor_size"])
  373. # update the positions on status bar
  374. self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp; "
  375. "<b>Y</b>: %.4f" % (curr_pos[0], curr_pos[1]))
  376. if self.cursor_pos is None:
  377. self.cursor_pos = (0, 0)
  378. dx = curr_pos[0] - float(self.cursor_pos[0])
  379. dy = curr_pos[1] - float(self.cursor_pos[1])
  380. self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp; <b>Dy</b>: "
  381. "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (dx, dy))
  382. # draw the utility geometry
  383. if self.first_click:
  384. self.app.delete_selection_shape()
  385. self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]),
  386. coords=(curr_pos[0], curr_pos[1]))
  387. def on_copper_fill(self, fill_obj, ref_obj=None, c_val=None, margin=None, run_threaded=True):
  388. """
  389. :param fill_obj:
  390. :param ref_obj:
  391. :param c_val:
  392. :param margin:
  393. :param run_threaded:
  394. :return:
  395. """
  396. if run_threaded:
  397. proc = self.app.proc_container.new('%s ...' % _("Copper filling"))
  398. else:
  399. self.app.proc_container.view.set_busy('%s ...' % _("Copper filling"))
  400. QtWidgets.QApplication.processEvents()
  401. # #####################################################################
  402. # ####### Read the parameters #########################################
  403. # #####################################################################
  404. log.debug("Copper Filling Tool started. Reading parameters.")
  405. self.app.inform.emit(_("Copper Filling Tool started. Reading parameters."))
  406. ref_selected = self.reference_radio.get_value()
  407. if c_val is None:
  408. c_val = float(self.app.defaults["tools_copperfill_clearance"])
  409. if margin is None:
  410. margin = float(self.app.defaults["tools_copperfill_margin"])
  411. # make sure that the source object solid geometry is an Iterable
  412. if not isinstance(self.grb_object.solid_geometry, Iterable):
  413. self.grb_object.solid_geometry = [self.grb_object.solid_geometry]
  414. # #########################################################################################
  415. # Prepare isolation polygon. This will create the clearance over the Gerber features ######
  416. # #########################################################################################
  417. log.debug("Copper Filling Tool. Preparing isolation polygons.")
  418. self.app.inform.emit(_("Copper Filling Tool. Preparing isolation polygons."))
  419. # variables to display the percentage of work done
  420. geo_len = 0
  421. try:
  422. for pol in self.grb_object.solid_geometry:
  423. geo_len += 1
  424. except TypeError:
  425. geo_len = 1
  426. old_disp_number = 0
  427. pol_nr = 0
  428. clearance_geometry = []
  429. try:
  430. for pol in self.grb_object.solid_geometry:
  431. if self.app.abort_flag:
  432. # graceful abort requested by the user
  433. raise FlatCAMApp.GracefulException
  434. clearance_geometry.append(
  435. pol.buffer(c_val, int(int(self.geo_steps_per_circle) / 4))
  436. )
  437. pol_nr += 1
  438. disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
  439. if old_disp_number < disp_number <= 100:
  440. self.app.proc_container.update_view_text(' %s ... %d%%' %
  441. (_("Buffering"), int(disp_number)))
  442. old_disp_number = disp_number
  443. except TypeError:
  444. # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a
  445. # MultiPolygon (not an iterable)
  446. clearance_geometry.append(
  447. self.grb_object.solid_geometry.buffer(c_val, int(int(self.geo_steps_per_circle) / 4))
  448. )
  449. self.app.proc_container.update_view_text(' %s' % _("Buffering"))
  450. clearance_geometry = unary_union(clearance_geometry)
  451. # #########################################################################################
  452. # Prepare the area to fill with copper. ###################################################
  453. # #########################################################################################
  454. log.debug("Copper Filling Tool. Preparing areas to fill with copper.")
  455. self.app.inform.emit(_("Copper Filling Tool. Preparing areas to fill with copper."))
  456. try:
  457. if ref_obj is None or ref_obj == 'itself':
  458. working_obj = fill_obj
  459. else:
  460. working_obj = ref_obj
  461. except Exception as e:
  462. log.debug("ToolCopperFIll.on_copper_fill() --> %s" % str(e))
  463. return 'fail'
  464. bounding_box = None
  465. if ref_selected == 'itself':
  466. geo_n = working_obj.solid_geometry
  467. try:
  468. if self.bbox_type_radio.get_value() == 'min':
  469. if isinstance(geo_n, MultiPolygon):
  470. env_obj = geo_n.convex_hull
  471. elif (isinstance(geo_n, MultiPolygon) and len(geo_n) == 1) or \
  472. (isinstance(geo_n, list) and len(geo_n) == 1) and isinstance(geo_n[0], Polygon):
  473. env_obj = cascaded_union(geo_n)
  474. else:
  475. env_obj = cascaded_union(geo_n)
  476. env_obj = env_obj.convex_hull
  477. else:
  478. if isinstance(geo_n, Polygon) or \
  479. (isinstance(geo_n, list) and len(geo_n) == 1) or \
  480. (isinstance(geo_n, MultiPolygon) and len(geo_n) == 1):
  481. env_obj = geo_n.buffer(0, join_style=base.JOIN_STYLE.mitre).exterior
  482. elif isinstance(geo_n, MultiPolygon):
  483. x0, y0, x1, y1 = geo_n.bounds
  484. geo = box(x0, y0, x1, y1)
  485. env_obj = geo.buffer(0, join_style=base.JOIN_STYLE.mitre)
  486. else:
  487. self.app.inform.emit(
  488. '[ERROR_NOTCL] %s: %s' % (_("Geometry not supported for bounding box"), type(geo_n))
  489. )
  490. return 'fail'
  491. bounding_box = env_obj.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
  492. except Exception as e:
  493. log.debug("ToolCopperFIll.on_copper_fill() 'itself' --> %s" % str(e))
  494. self.app.inform.emit('[ERROR_NOTCL] %s' % _("No object available."))
  495. return 'fail'
  496. elif ref_selected == 'area':
  497. geo_n = cascaded_union(working_obj)
  498. try:
  499. __ = iter(geo_n)
  500. except Exception as e:
  501. log.debug("ToolCopperFIll.on_copper_fill() 'area' --> %s" % str(e))
  502. geo_n = [geo_n]
  503. geo_buff_list = []
  504. for poly in geo_n:
  505. if self.app.abort_flag:
  506. # graceful abort requested by the user
  507. raise FlatCAMApp.GracefulException
  508. geo_buff_list.append(poly.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
  509. bounding_box = cascaded_union(geo_buff_list)
  510. elif ref_selected == 'box':
  511. geo_n = working_obj.solid_geometry
  512. if isinstance(working_obj, FlatCAMGeometry):
  513. try:
  514. __ = iter(geo_n)
  515. except Exception as e:
  516. log.debug("ToolCopperFIll.on_copper_fill() 'box' --> %s" % str(e))
  517. geo_n = [geo_n]
  518. geo_buff_list = []
  519. for poly in geo_n:
  520. if self.app.abort_flag:
  521. # graceful abort requested by the user
  522. raise FlatCAMApp.GracefulException
  523. geo_buff_list.append(poly.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
  524. bounding_box = cascaded_union(geo_buff_list)
  525. elif isinstance(working_obj, FlatCAMGerber):
  526. geo_n = cascaded_union(geo_n).convex_hull
  527. bounding_box = cascaded_union(self.ncc_obj.solid_geometry).convex_hull.intersection(geo_n)
  528. bounding_box = bounding_box.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
  529. else:
  530. self.app.inform.emit('[ERROR_NOTCL] %s' % _("The reference object type is not supported."))
  531. return 'fail'
  532. log.debug("Copper Filling Tool. Finished creating areas to fill with copper.")
  533. self.app.inform.emit(_("Copper Filling Tool. Appending new geometry and buffering."))
  534. new_solid_geometry = bounding_box.difference(clearance_geometry)
  535. geo_list = self.grb_object.solid_geometry
  536. if isinstance(self.grb_object.solid_geometry, MultiPolygon):
  537. geo_list = list(self.grb_object.solid_geometry.geoms)
  538. if '0' not in self.grb_object.apertures:
  539. self.grb_object.apertures['0'] = dict()
  540. self.grb_object.apertures['0']['geometry'] = list()
  541. self.grb_object.apertures['0']['type'] = 'REG'
  542. self.grb_object.apertures['0']['size'] = 0.0
  543. try:
  544. for poly in new_solid_geometry:
  545. # append to the new solid geometry
  546. geo_list.append(poly)
  547. # append into the '0' aperture
  548. geo_elem = dict()
  549. geo_elem['solid'] = poly
  550. geo_elem['follow'] = poly.exterior
  551. self.grb_object.apertures['0']['geometry'].append(deepcopy(geo_elem))
  552. except TypeError:
  553. # append to the new solid geometry
  554. geo_list.append(new_solid_geometry)
  555. # append into the '0' aperture
  556. geo_elem = dict()
  557. geo_elem['solid'] = new_solid_geometry
  558. geo_elem['follow'] = new_solid_geometry.exterior
  559. self.grb_object.apertures['0']['geometry'].append(deepcopy(geo_elem))
  560. self.grb_object.solid_geometry = MultiPolygon(geo_list).buffer(0.0000001).buffer(-0.0000001)
  561. # update the source file with the new geometry:
  562. self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'], filename=None,
  563. local_use=self.grb_object, use_thread=False)
  564. self.on_exit()
  565. self.app.inform.emit('[success] %s' % _("Copper Fill Tool done."))
  566. def replot(self, obj):
  567. def worker_task():
  568. with self.app.proc_container.new('%s...' % _("Plotting")):
  569. obj.plot()
  570. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  571. def on_exit(self):
  572. # plot the object
  573. self.replot(obj=self.grb_object)
  574. # update the bounding box values
  575. try:
  576. a, b, c, d = self.grb_object.bounds()
  577. self.grb_object.options['xmin'] = a
  578. self.grb_object.options['ymin'] = b
  579. self.grb_object.options['xmax'] = c
  580. self.grb_object.options['ymax'] = d
  581. except Exception as e:
  582. log.debug("ToolCopperFill.on_exit() bounds error --> %s" % str(e))
  583. # reset the variables
  584. self.grb_object = None
  585. self.ref_obj = None
  586. self.sel_rect = list()
  587. # Events ID
  588. self.mr = None
  589. self.mm = None
  590. # Mouse cursor positions
  591. self.mouse_is_dragging = False
  592. self.cursor_pos = (0, 0)
  593. self.first_click = False
  594. # if True it means we exited from tool in the middle of area adding therefore disconnect the events
  595. if self.area_method is True:
  596. self.app.delete_selection_shape()
  597. self.area_method = False
  598. if self.app.is_legacy is False:
  599. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
  600. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
  601. else:
  602. self.app.plotcanvas.graph_event_disconnect(self.mr)
  603. self.app.plotcanvas.graph_event_disconnect(self.mm)
  604. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
  605. self.app.on_mouse_click_over_plot)
  606. self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
  607. self.app.on_mouse_move_over_plot)
  608. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  609. self.app.on_mouse_click_release_over_plot)
  610. self.app.call_source = "app"
  611. self.app.inform.emit('[success] %s' % _("Copper Fill Tool exit."))