ToolFiducials.py 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968
  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 ToolFiducials(FlatCAMTool):
  29. toolName = _("Fiducials 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. grid_lay = QtWidgets.QGridLayout()
  49. self.layout.addLayout(grid_lay)
  50. grid_lay.setColumnStretch(0, 0)
  51. grid_lay.setColumnStretch(1, 1)
  52. self.copper_fill_label = QtWidgets.QLabel('<b>%s:</b>' % _('Parameters'))
  53. self.copper_fill_label.setToolTip(
  54. _("Parameters used for this tool.")
  55. )
  56. grid_lay.addWidget(self.copper_fill_label, 0, 0, 1, 2)
  57. # DIAMETER #
  58. self.dia_label = QtWidgets.QLabel('%s:' % _("Diameter"))
  59. self.dia_label.setToolTip(
  60. _("This set the fiducial diameter.\n"
  61. "The soldermask opening is double than that.")
  62. )
  63. self.dia_entry = FCDoubleSpinner()
  64. self.dia_entry.set_range(1.0000, 3.0000)
  65. self.dia_entry.set_precision(self.decimals)
  66. self.dia_entry.setWrapping(True)
  67. self.dia_entry.setSingleStep(0.1)
  68. grid_lay.addWidget(self.dia_label, 1, 0)
  69. grid_lay.addWidget(self.dia_entry, 1, 1)
  70. # MARGIN #
  71. self.margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
  72. self.margin_label.setToolTip(
  73. _("Bounding box margin.")
  74. )
  75. self.margin_entry = FCDoubleSpinner()
  76. self.margin_entry.set_range(0.0, 9999.9999)
  77. self.margin_entry.set_precision(self.decimals)
  78. self.margin_entry.setSingleStep(0.1)
  79. grid_lay.addWidget(self.margin_label, 2, 0)
  80. grid_lay.addWidget(self.margin_entry, 2, 1)
  81. # Mode #
  82. self.mode_radio = RadioSet([
  83. {'label': _('Auto'), 'value': 'auto'},
  84. {"label": _("Manual"), "value": "manual"}
  85. ], stretch=False)
  86. self.mode_label = QtWidgets.QLabel(_("Mode:"))
  87. self.mode_label.setToolTip(
  88. _("- 'Auto' - automatic placement of fiducials in the corners of the bounding box.\n "
  89. "- 'Manual' - manual placement of fiducials.")
  90. )
  91. grid_lay.addWidget(self.mode_label, 3, 0)
  92. grid_lay.addWidget(self.mode_radio, 3, 1)
  93. # Position for second fiducial #
  94. self.pos_radio = RadioSet([
  95. {'label': _('Up'), 'value': 'up'},
  96. {"label": _("Down"), "value": "down"},
  97. {"label": _("None"), "value": "no"}
  98. ], stretch=False)
  99. self.pos_label = QtWidgets.QLabel('%s:' % _("Second fiducial"))
  100. self.pos_label.setToolTip(
  101. _("The position for the second fiducial.\n"
  102. "- 'Up' - the order is: bottom-left, top-left, top-right.\n "
  103. "- 'Down' - the order is: bottom-left, bottom-right, top-right.\n"
  104. "- 'None' - there is no second fiducial. The order is: bottom-left, top-right.")
  105. )
  106. grid_lay.addWidget(self.pos_label, 4, 0)
  107. grid_lay.addWidget(self.pos_radio, 4, 1)
  108. separator_line = QtWidgets.QFrame()
  109. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  110. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  111. grid_lay.addWidget(separator_line, 5, 0, 1, 2)
  112. # Copper Gerber object
  113. self.grb_object_combo = QtWidgets.QComboBox()
  114. self.grb_object_combo.setModel(self.app.collection)
  115. self.grb_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  116. self.grb_object_combo.setCurrentIndex(1)
  117. self.grbobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("Copper Gerber"))
  118. self.grbobj_label.setToolTip(
  119. _("Gerber Object to which will be added a copper thieving.")
  120. )
  121. grid_lay.addWidget(self.grbobj_label, 6, 0, 1, 2)
  122. grid_lay.addWidget(self.grb_object_combo, 7, 0, 1, 2)
  123. # ## Insert Copper Fiducial
  124. self.add_cfid_button = QtWidgets.QPushButton(_("Add Fiducial"))
  125. self.add_cfid_button.setToolTip(
  126. _("Will add a polygon on the copper layer to serve as fiducial.")
  127. )
  128. grid_lay.addWidget(self.add_cfid_button, 8, 0, 1, 2)
  129. separator_line_1 = QtWidgets.QFrame()
  130. separator_line_1.setFrameShape(QtWidgets.QFrame.HLine)
  131. separator_line_1.setFrameShadow(QtWidgets.QFrame.Sunken)
  132. grid_lay.addWidget(separator_line_1, 9, 0, 1, 2)
  133. # Soldermask Gerber object #
  134. self.sm_object_label = QtWidgets.QLabel('<b>%s:</b>' % _("Soldermask Gerber"))
  135. self.sm_object_label.setToolTip(
  136. _("The Soldermask Gerber object.")
  137. )
  138. self.sm_object_combo = QtWidgets.QComboBox()
  139. self.sm_object_combo.setModel(self.app.collection)
  140. self.sm_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  141. self.sm_object_combo.setCurrentIndex(1)
  142. grid_lay.addWidget(self.sm_object_label, 10, 0, 1, 2)
  143. grid_lay.addWidget(self.sm_object_combo, 11, 0, 1, 2)
  144. # ## Insert Soldermask opening for Fiducial
  145. self.add_sm_opening_button = QtWidgets.QPushButton(_("Add SM Opening"))
  146. self.add_sm_opening_button.setToolTip(
  147. _("Will add a polygon on the soldermask layer\n"
  148. "to serve as fiducial opening.\n"
  149. "The diameter is always double of the diameter\n"
  150. "for the copper fiducial.")
  151. )
  152. grid_lay.addWidget(self.add_sm_opening_button, 12, 0, 1, 2)
  153. self.layout.addStretch()
  154. # Objects involved in Copper thieving
  155. self.grb_object = None
  156. self.sm_obj = None
  157. self.sel_rect = list()
  158. # store the flattened geometry here:
  159. self.flat_geometry = list()
  160. # Events ID
  161. self.mr = None
  162. self.mm = None
  163. # Mouse cursor positions
  164. self.mouse_is_dragging = False
  165. self.cursor_pos = (0, 0)
  166. self.first_click = False
  167. self.area_method = False
  168. # Tool properties
  169. self.clearance_val = None
  170. self.margin_val = None
  171. self.geo_steps_per_circle = 128
  172. # SIGNALS
  173. # self.fill_button.clicked.connect(self.execute)
  174. # self.box_combo_type.currentIndexChanged.connect(self.on_combo_box_type)
  175. # self.reference_radio.group_toggle_fn = self.on_toggle_reference
  176. # self.fill_type_radio.activated_custom.connect(self.on_thieving_type)
  177. def run(self, toggle=True):
  178. self.app.report_usage("ToolFiducials()")
  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, _("Fiducials Tool"))
  200. def install(self, icon=None, separator=None, **kwargs):
  201. FlatCAMTool.install(self, icon, separator, shortcut='ALT+J', **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_copper_thieving_clearance"]))
  205. # self.margin_entry.set_value(float(self.app.defaults["tools_copper_thieving_margin"]))
  206. # self.reference_radio.set_value(self.app.defaults["tools_copper_thieving_reference"])
  207. def on_combo_box_type(self):
  208. obj_type = self.box_combo_type.currentIndex()
  209. self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
  210. self.box_combo.setCurrentIndex(0)
  211. def on_toggle_reference(self):
  212. if self.reference_radio.get_value() == "itself" or self.reference_radio.get_value() == "area":
  213. self.box_combo.hide()
  214. self.box_combo_label.hide()
  215. self.box_combo_type.hide()
  216. self.box_combo_type_label.hide()
  217. else:
  218. self.box_combo.show()
  219. self.box_combo_label.show()
  220. self.box_combo_type.show()
  221. self.box_combo_type_label.show()
  222. if self.reference_radio.get_value() == "itself":
  223. self.bbox_type_label.show()
  224. self.bbox_type_radio.show()
  225. else:
  226. if self.fill_type_radio.get_value() == 'line':
  227. self.reference_radio.set_value('itself')
  228. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Lines Grid works only for 'itself' reference ..."))
  229. return
  230. self.bbox_type_label.hide()
  231. self.bbox_type_radio.hide()
  232. def on_thieving_type(self, choice):
  233. if choice == 'solid':
  234. self.dots_frame.hide()
  235. self.squares_frame.hide()
  236. self.lines_frame.hide()
  237. self.app.inform.emit(_("Solid fill selected."))
  238. elif choice == 'dot':
  239. self.dots_frame.show()
  240. self.squares_frame.hide()
  241. self.lines_frame.hide()
  242. self.app.inform.emit(_("Dots grid fill selected."))
  243. elif choice == 'square':
  244. self.dots_frame.hide()
  245. self.squares_frame.show()
  246. self.lines_frame.hide()
  247. self.app.inform.emit(_("Squares grid fill selected."))
  248. else:
  249. if self.reference_radio.get_value() != 'itself':
  250. self.reference_radio.set_value('itself')
  251. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Lines Grid works only for 'itself' reference ..."))
  252. self.dots_frame.hide()
  253. self.squares_frame.hide()
  254. self.lines_frame.show()
  255. def execute(self):
  256. self.app.call_source = "copper_thieving_tool"
  257. self.clearance_val = self.clearance_entry.get_value()
  258. self.margin_val = self.margin_entry.get_value()
  259. reference_method = self.reference_radio.get_value()
  260. # get the Gerber object on which the Copper thieving will be inserted
  261. selection_index = self.grb_object_combo.currentIndex()
  262. model_index = self.app.collection.index(selection_index, 0, self.grb_object_combo.rootModelIndex())
  263. try:
  264. self.grb_object = model_index.internalPointer().obj
  265. except Exception as e:
  266. log.debug("ToolCopperThieving.execute() --> %s" % str(e))
  267. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
  268. return 'fail'
  269. if reference_method == 'itself':
  270. bound_obj_name = self.grb_object_combo.currentText()
  271. # Get reference object.
  272. try:
  273. self.ref_obj = self.app.collection.get_by_name(bound_obj_name)
  274. except Exception as e:
  275. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(e)))
  276. return "Could not retrieve object: %s" % self.obj_name
  277. self.on_copper_thieving(
  278. thieving_obj=self.grb_object,
  279. c_val=self.clearance_val,
  280. margin=self.margin_val
  281. )
  282. elif reference_method == 'area':
  283. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area."))
  284. self.area_method = True
  285. if self.app.is_legacy is False:
  286. self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
  287. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
  288. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
  289. else:
  290. self.app.plotcanvas.graph_event_disconnect(self.app.mp)
  291. self.app.plotcanvas.graph_event_disconnect(self.app.mm)
  292. self.app.plotcanvas.graph_event_disconnect(self.app.mr)
  293. self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
  294. self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
  295. elif reference_method == 'box':
  296. bound_obj_name = self.box_combo.currentText()
  297. # Get reference object.
  298. try:
  299. self.ref_obj = self.app.collection.get_by_name(bound_obj_name)
  300. except Exception as e:
  301. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), bound_obj_name))
  302. return "Could not retrieve object: %s. Error: %s" % (bound_obj_name, str(e))
  303. self.on_copper_thieving(
  304. thieving_obj=self.grb_object,
  305. ref_obj=self.ref_obj,
  306. c_val=self.clearance_val,
  307. margin=self.margin_val
  308. )
  309. # To be called after clicking on the plot.
  310. def on_mouse_release(self, event):
  311. if self.app.is_legacy is False:
  312. event_pos = event.pos
  313. # event_is_dragging = event.is_dragging
  314. right_button = 2
  315. else:
  316. event_pos = (event.xdata, event.ydata)
  317. # event_is_dragging = self.app.plotcanvas.is_dragging
  318. right_button = 3
  319. event_pos = self.app.plotcanvas.translate_coords(event_pos)
  320. # do clear area only for left mouse clicks
  321. if event.button == 1:
  322. if self.first_click is False:
  323. self.first_click = True
  324. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the end point of the filling area."))
  325. self.cursor_pos = self.app.plotcanvas.translate_coords(event_pos)
  326. if self.app.grid_status() is True:
  327. self.cursor_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
  328. else:
  329. self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish."))
  330. self.app.delete_selection_shape()
  331. if self.app.grid_status() is True:
  332. curr_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
  333. else:
  334. curr_pos = (event_pos[0], event_pos[1])
  335. x0, y0 = self.cursor_pos[0], self.cursor_pos[1]
  336. x1, y1 = curr_pos[0], curr_pos[1]
  337. pt1 = (x0, y0)
  338. pt2 = (x1, y0)
  339. pt3 = (x1, y1)
  340. pt4 = (x0, y1)
  341. new_rectangle = Polygon([pt1, pt2, pt3, pt4])
  342. self.sel_rect.append(new_rectangle)
  343. # add a temporary shape on canvas
  344. self.draw_tool_selection_shape(old_coords=(x0, y0), coords=(x1, y1))
  345. self.first_click = False
  346. return
  347. elif event.button == right_button and self.mouse_is_dragging is False:
  348. self.area_method = False
  349. self.first_click = False
  350. self.delete_tool_selection_shape()
  351. if self.app.is_legacy is False:
  352. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
  353. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
  354. else:
  355. self.app.plotcanvas.graph_event_disconnect(self.mr)
  356. self.app.plotcanvas.graph_event_disconnect(self.mm)
  357. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
  358. self.app.on_mouse_click_over_plot)
  359. self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
  360. self.app.on_mouse_move_over_plot)
  361. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  362. self.app.on_mouse_click_release_over_plot)
  363. if len(self.sel_rect) == 0:
  364. return
  365. self.sel_rect = cascaded_union(self.sel_rect)
  366. if not isinstance(self.sel_rect, Iterable):
  367. self.sel_rect = [self.sel_rect]
  368. self.on_copper_thieving(
  369. thieving_obj=self.grb_object,
  370. ref_obj=self.sel_rect,
  371. c_val=self.clearance_val,
  372. margin=self.margin_val
  373. )
  374. # called on mouse move
  375. def on_mouse_move(self, event):
  376. if self.app.is_legacy is False:
  377. event_pos = event.pos
  378. event_is_dragging = event.is_dragging
  379. # right_button = 2
  380. else:
  381. event_pos = (event.xdata, event.ydata)
  382. event_is_dragging = self.app.plotcanvas.is_dragging
  383. # right_button = 3
  384. curr_pos = self.app.plotcanvas.translate_coords(event_pos)
  385. # detect mouse dragging motion
  386. if event_is_dragging is True:
  387. self.mouse_is_dragging = True
  388. else:
  389. self.mouse_is_dragging = False
  390. # update the cursor position
  391. if self.app.grid_status() is True:
  392. # Update cursor
  393. curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
  394. self.app.app_cursor.set_data(np.asarray([(curr_pos[0], curr_pos[1])]),
  395. symbol='++', edge_color=self.app.cursor_color_3D,
  396. size=self.app.defaults["global_cursor_size"])
  397. # update the positions on status bar
  398. self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp; "
  399. "<b>Y</b>: %.4f" % (curr_pos[0], curr_pos[1]))
  400. if self.cursor_pos is None:
  401. self.cursor_pos = (0, 0)
  402. dx = curr_pos[0] - float(self.cursor_pos[0])
  403. dy = curr_pos[1] - float(self.cursor_pos[1])
  404. self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp; <b>Dy</b>: "
  405. "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (dx, dy))
  406. # draw the utility geometry
  407. if self.first_click:
  408. self.app.delete_selection_shape()
  409. self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]),
  410. coords=(curr_pos[0], curr_pos[1]))
  411. def on_copper_thieving(self, thieving_obj, ref_obj=None, c_val=None, margin=None, run_threaded=True):
  412. """
  413. :param thieving_obj:
  414. :param ref_obj:
  415. :param c_val:
  416. :param margin:
  417. :param run_threaded:
  418. :return:
  419. """
  420. if run_threaded:
  421. proc = self.app.proc_container.new('%s ...' % _("Thieving"))
  422. else:
  423. QtWidgets.QApplication.processEvents()
  424. self.app.proc_container.view.set_busy('%s ...' % _("Thieving"))
  425. # #####################################################################
  426. # ####### Read the parameters #########################################
  427. # #####################################################################
  428. log.debug("Copper Thieving Tool started. Reading parameters.")
  429. self.app.inform.emit(_("Copper Thieving Tool started. Reading parameters."))
  430. ref_selected = self.reference_radio.get_value()
  431. if c_val is None:
  432. c_val = float(self.app.defaults["tools_copperfill_clearance"])
  433. if margin is None:
  434. margin = float(self.app.defaults["tools_copperfill_margin"])
  435. fill_type = self.fill_type_radio.get_value()
  436. dot_dia = self.dot_dia_entry.get_value()
  437. dot_spacing = self.dot_spacing_entry.get_value()
  438. square_size = self.square_size_entry.get_value()
  439. square_spacing = self.squares_spacing_entry.get_value()
  440. line_size = self.line_size_entry.get_value()
  441. line_spacing = self.lines_spacing_entry.get_value()
  442. # make sure that the source object solid geometry is an Iterable
  443. if not isinstance(self.grb_object.solid_geometry, Iterable):
  444. self.grb_object.solid_geometry = [self.grb_object.solid_geometry]
  445. def job_thread_thieving(app_obj):
  446. # #########################################################################################
  447. # Prepare isolation polygon. This will create the clearance over the Gerber features ######
  448. # #########################################################################################
  449. log.debug("Copper Thieving Tool. Preparing isolation polygons.")
  450. app_obj.app.inform.emit(_("Copper Thieving Tool. Preparing isolation polygons."))
  451. # variables to display the percentage of work done
  452. geo_len = 0
  453. try:
  454. for pol in app_obj.grb_object.solid_geometry:
  455. geo_len += 1
  456. except TypeError:
  457. geo_len = 1
  458. old_disp_number = 0
  459. pol_nr = 0
  460. clearance_geometry = []
  461. try:
  462. for pol in app_obj.grb_object.solid_geometry:
  463. if app_obj.app.abort_flag:
  464. # graceful abort requested by the user
  465. raise FlatCAMApp.GracefulException
  466. clearance_geometry.append(
  467. pol.buffer(c_val, int(int(app_obj.geo_steps_per_circle) / 4))
  468. )
  469. pol_nr += 1
  470. disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
  471. if old_disp_number < disp_number <= 100:
  472. app_obj.app.proc_container.update_view_text(' %s ... %d%%' %
  473. (_("Thieving"), int(disp_number)))
  474. old_disp_number = disp_number
  475. except TypeError:
  476. # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a
  477. # MultiPolygon (not an iterable)
  478. clearance_geometry.append(
  479. app_obj.grb_object.solid_geometry.buffer(c_val, int(int(app_obj.geo_steps_per_circle) / 4))
  480. )
  481. app_obj.app.proc_container.update_view_text(' %s ...' % _("Buffering"))
  482. clearance_geometry = unary_union(clearance_geometry)
  483. # #########################################################################################
  484. # Prepare the area to fill with copper. ###################################################
  485. # #########################################################################################
  486. log.debug("Copper Thieving Tool. Preparing areas to fill with copper.")
  487. app_obj.app.inform.emit(_("Copper Thieving Tool. Preparing areas to fill with copper."))
  488. try:
  489. if ref_obj is None or ref_obj == 'itself':
  490. working_obj = thieving_obj
  491. else:
  492. working_obj = ref_obj
  493. except Exception as e:
  494. log.debug("ToolCopperThieving.on_copper_thieving() --> %s" % str(e))
  495. return 'fail'
  496. app_obj.app.proc_container.update_view_text(' %s' % _("Working..."))
  497. if ref_selected == 'itself':
  498. geo_n = working_obj.solid_geometry
  499. try:
  500. if app_obj.bbox_type_radio.get_value() == 'min':
  501. if isinstance(geo_n, MultiPolygon):
  502. env_obj = geo_n.convex_hull
  503. elif (isinstance(geo_n, MultiPolygon) and len(geo_n) == 1) or \
  504. (isinstance(geo_n, list) and len(geo_n) == 1) and isinstance(geo_n[0], Polygon):
  505. env_obj = cascaded_union(geo_n)
  506. else:
  507. env_obj = cascaded_union(geo_n)
  508. env_obj = env_obj.convex_hull
  509. bounding_box = env_obj.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
  510. else:
  511. if isinstance(geo_n, Polygon):
  512. bounding_box = geo_n.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre).exterior
  513. elif isinstance(geo_n, list):
  514. geo_n = unary_union(geo_n)
  515. bounding_box = geo_n.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre).exterior
  516. elif isinstance(geo_n, MultiPolygon):
  517. x0, y0, x1, y1 = geo_n.bounds
  518. geo = box(x0, y0, x1, y1)
  519. bounding_box = geo.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
  520. else:
  521. app_obj.app.inform.emit(
  522. '[ERROR_NOTCL] %s: %s' % (_("Geometry not supported for bounding box"), type(geo_n))
  523. )
  524. return 'fail'
  525. except Exception as e:
  526. log.debug("ToolCopperFIll.on_copper_thieving() 'itself' --> %s" % str(e))
  527. app_obj.app.inform.emit('[ERROR_NOTCL] %s' % _("No object available."))
  528. return 'fail'
  529. elif ref_selected == 'area':
  530. geo_buff_list = []
  531. try:
  532. for poly in working_obj:
  533. if app_obj.app.abort_flag:
  534. # graceful abort requested by the user
  535. raise FlatCAMApp.GracefulException
  536. geo_buff_list.append(poly.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
  537. except TypeError:
  538. geo_buff_list.append(working_obj.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
  539. bounding_box = MultiPolygon(geo_buff_list)
  540. else: # ref_selected == 'box'
  541. geo_n = working_obj.solid_geometry
  542. if isinstance(working_obj, FlatCAMGeometry):
  543. try:
  544. __ = iter(geo_n)
  545. except Exception as e:
  546. log.debug("ToolCopperFIll.on_copper_thieving() 'box' --> %s" % str(e))
  547. geo_n = [geo_n]
  548. geo_buff_list = []
  549. for poly in geo_n:
  550. if app_obj.app.abort_flag:
  551. # graceful abort requested by the user
  552. raise FlatCAMApp.GracefulException
  553. geo_buff_list.append(poly.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre))
  554. bounding_box = cascaded_union(geo_buff_list)
  555. elif isinstance(working_obj, FlatCAMGerber):
  556. geo_n = cascaded_union(geo_n).convex_hull
  557. bounding_box = cascaded_union(thieving_obj.solid_geometry).convex_hull.intersection(geo_n)
  558. bounding_box = bounding_box.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
  559. else:
  560. app_obj.app.inform.emit('[ERROR_NOTCL] %s' % _("The reference object type is not supported."))
  561. return 'fail'
  562. log.debug("Copper Thieving Tool. Finished creating areas to fill with copper.")
  563. app_obj.app.inform.emit(_("Copper Thieving Tool. Appending new geometry and buffering."))
  564. # #########################################################################################
  565. # ########## Generate filling geometry. ###################################################
  566. # #########################################################################################
  567. new_solid_geometry = bounding_box.difference(clearance_geometry)
  568. # determine the bounding box polygon for the entire Gerber object to which we add copper thieving
  569. # if isinstance(geo_n, list):
  570. # env_obj = unary_union(geo_n).buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
  571. # else:
  572. # env_obj = geo_n.buffer(distance=margin, join_style=base.JOIN_STYLE.mitre)
  573. #
  574. # x0, y0, x1, y1 = env_obj.bounds
  575. # bounding_box = box(x0, y0, x1, y1)
  576. app_obj.app.proc_container.update_view_text(' %s' % _("Create geometry"))
  577. bounding_box = thieving_obj.solid_geometry.envelope.buffer(
  578. distance=margin,
  579. join_style=base.JOIN_STYLE.mitre
  580. )
  581. x0, y0, x1, y1 = bounding_box.bounds
  582. if fill_type == 'dot' or fill_type == 'square':
  583. # build the MultiPolygon of dots/squares that will fill the entire bounding box
  584. thieving_list = list()
  585. if fill_type == 'dot':
  586. radius = dot_dia / 2.0
  587. new_x = x0 + radius
  588. new_y = y0 + radius
  589. while new_x <= x1 - radius:
  590. while new_y <= y1 - radius:
  591. dot_geo = Point((new_x, new_y)).buffer(radius, resolution=64)
  592. thieving_list.append(dot_geo)
  593. new_y += dot_dia + dot_spacing
  594. new_x += dot_dia + dot_spacing
  595. new_y = y0 + radius
  596. else:
  597. h_size = square_size / 2.0
  598. new_x = x0 + h_size
  599. new_y = y0 + h_size
  600. while new_x <= x1 - h_size:
  601. while new_y <= y1 - h_size:
  602. a, b, c, d = (Point((new_x, new_y)).buffer(h_size)).bounds
  603. square_geo = box(a, b, c, d)
  604. thieving_list.append(square_geo)
  605. new_y += square_size + square_spacing
  606. new_x += square_size + square_spacing
  607. new_y = y0 + h_size
  608. thieving_box_geo = MultiPolygon(thieving_list)
  609. dx = bounding_box.centroid.x - thieving_box_geo.centroid.x
  610. dy = bounding_box.centroid.y - thieving_box_geo.centroid.y
  611. thieving_box_geo = affinity.translate(thieving_box_geo, xoff=dx, yoff=dy)
  612. try:
  613. _it = iter(new_solid_geometry)
  614. except TypeError:
  615. new_solid_geometry = [new_solid_geometry]
  616. try:
  617. _it = iter(thieving_box_geo)
  618. except TypeError:
  619. thieving_box_geo = [thieving_box_geo]
  620. thieving_geo = list()
  621. for dot_geo in thieving_box_geo:
  622. for geo_t in new_solid_geometry:
  623. if dot_geo.within(geo_t):
  624. thieving_geo.append(dot_geo)
  625. new_solid_geometry = thieving_geo
  626. if fill_type == 'line':
  627. half_thick_line = line_size / 2.0
  628. # create a thick polygon-line that surrounds the copper features
  629. outline_geometry = []
  630. try:
  631. for pol in app_obj.grb_object.solid_geometry:
  632. if app_obj.app.abort_flag:
  633. # graceful abort requested by the user
  634. raise FlatCAMApp.GracefulException
  635. outline_geometry.append(
  636. pol.buffer(c_val+half_thick_line, int(int(app_obj.geo_steps_per_circle) / 4))
  637. )
  638. pol_nr += 1
  639. disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
  640. if old_disp_number < disp_number <= 100:
  641. app_obj.app.proc_container.update_view_text(' %s ... %d%%' %
  642. (_("Buffering"), int(disp_number)))
  643. old_disp_number = disp_number
  644. except TypeError:
  645. # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a
  646. # MultiPolygon (not an iterable)
  647. outline_geometry.append(
  648. app_obj.grb_object.solid_geometry.buffer(
  649. c_val+half_thick_line,
  650. int(int(app_obj.geo_steps_per_circle) / 4)
  651. )
  652. )
  653. app_obj.app.proc_container.update_view_text(' %s' % _("Buffering"))
  654. outline_geometry = unary_union(outline_geometry)
  655. outline_line = list()
  656. try:
  657. for geo_o in outline_geometry:
  658. outline_line.append(
  659. geo_o.exterior.buffer(
  660. half_thick_line, resolution=int(int(app_obj.geo_steps_per_circle) / 4)
  661. )
  662. )
  663. except TypeError:
  664. outline_line.append(
  665. outline_geometry.exterior.buffer(
  666. half_thick_line, resolution=int(int(app_obj.geo_steps_per_circle) / 4)
  667. )
  668. )
  669. outline_geometry = unary_union(outline_line)
  670. # create a polygon-line that surrounds in the inside the bounding box polygon of the target Gerber
  671. box_outline_geo = box(x0, y0, x1, y1).buffer(-half_thick_line)
  672. box_outline_geo_exterior = box_outline_geo.exterior
  673. box_outline_geometry = box_outline_geo_exterior.buffer(
  674. half_thick_line,
  675. resolution=int(int(app_obj.geo_steps_per_circle) / 4)
  676. )
  677. bx0, by0, bx1, by1 = box_outline_geo.bounds
  678. thieving_lines_geo = list()
  679. new_x = bx0
  680. new_y = by0
  681. while new_x <= x1 - half_thick_line:
  682. line_geo = LineString([(new_x, by0), (new_x, by1)]).buffer(
  683. half_thick_line,
  684. resolution=int(int(app_obj.geo_steps_per_circle) / 4)
  685. )
  686. thieving_lines_geo.append(line_geo)
  687. new_x += line_size + line_spacing
  688. while new_y <= y1 - half_thick_line:
  689. line_geo = LineString([(bx0, new_y), (bx1, new_y)]).buffer(
  690. half_thick_line,
  691. resolution=int(int(app_obj.geo_steps_per_circle) / 4)
  692. )
  693. thieving_lines_geo.append(line_geo)
  694. new_y += line_size + line_spacing
  695. # merge everything together
  696. diff_lines_geo = list()
  697. for line_poly in thieving_lines_geo:
  698. rest_line = line_poly.difference(clearance_geometry)
  699. diff_lines_geo.append(rest_line)
  700. app_obj.flatten([outline_geometry, box_outline_geometry, diff_lines_geo])
  701. new_solid_geometry = app_obj.flat_geometry
  702. app_obj.app.proc_container.update_view_text(' %s' % _("Append geometry"))
  703. geo_list = app_obj.grb_object.solid_geometry
  704. if isinstance(app_obj.grb_object.solid_geometry, MultiPolygon):
  705. geo_list = list(app_obj.grb_object.solid_geometry.geoms)
  706. if '0' not in app_obj.grb_object.apertures:
  707. app_obj.grb_object.apertures['0'] = dict()
  708. app_obj.grb_object.apertures['0']['geometry'] = list()
  709. app_obj.grb_object.apertures['0']['type'] = 'REG'
  710. app_obj.grb_object.apertures['0']['size'] = 0.0
  711. try:
  712. for poly in new_solid_geometry:
  713. # append to the new solid geometry
  714. geo_list.append(poly)
  715. # append into the '0' aperture
  716. geo_elem = dict()
  717. geo_elem['solid'] = poly
  718. geo_elem['follow'] = poly.exterior
  719. app_obj.grb_object.apertures['0']['geometry'].append(deepcopy(geo_elem))
  720. except TypeError:
  721. # append to the new solid geometry
  722. geo_list.append(new_solid_geometry)
  723. # append into the '0' aperture
  724. geo_elem = dict()
  725. geo_elem['solid'] = new_solid_geometry
  726. geo_elem['follow'] = new_solid_geometry.exterior
  727. app_obj.grb_object.apertures['0']['geometry'].append(deepcopy(geo_elem))
  728. app_obj.grb_object.solid_geometry = MultiPolygon(geo_list).buffer(0.0000001).buffer(-0.0000001)
  729. app_obj.app.proc_container.update_view_text(' %s' % _("Append source file"))
  730. # update the source file with the new geometry:
  731. app_obj.grb_object.source_file = app_obj.app.export_gerber(obj_name=app_obj.grb_object.options['name'],
  732. filename=None,
  733. local_use=app_obj.grb_object,
  734. use_thread=False)
  735. app_obj.app.proc_container.update_view_text(' %s' % '')
  736. app_obj.on_exit()
  737. app_obj.app.inform.emit('[success] %s' % _("Copper Thieving Tool done."))
  738. if run_threaded:
  739. self.app.worker_task.emit({'fcn': job_thread_thieving, 'params': [self]})
  740. else:
  741. job_thread_thieving(self)
  742. def replot(self, obj):
  743. def worker_task():
  744. with self.app.proc_container.new('%s...' % _("Plotting")):
  745. obj.plot()
  746. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  747. def on_exit(self):
  748. # plot the object
  749. self.replot(obj=self.grb_object)
  750. # update the bounding box values
  751. try:
  752. a, b, c, d = self.grb_object.bounds()
  753. self.grb_object.options['xmin'] = a
  754. self.grb_object.options['ymin'] = b
  755. self.grb_object.options['xmax'] = c
  756. self.grb_object.options['ymax'] = d
  757. except Exception as e:
  758. log.debug("ToolCopperThieving.on_exit() bounds error --> %s" % str(e))
  759. # reset the variables
  760. self.grb_object = None
  761. self.ref_obj = None
  762. self.sel_rect = list()
  763. # Events ID
  764. self.mr = None
  765. self.mm = None
  766. # Mouse cursor positions
  767. self.mouse_is_dragging = False
  768. self.cursor_pos = (0, 0)
  769. self.first_click = False
  770. # if True it means we exited from tool in the middle of area adding therefore disconnect the events
  771. if self.area_method is True:
  772. self.app.delete_selection_shape()
  773. self.area_method = False
  774. if self.app.is_legacy is False:
  775. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
  776. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
  777. else:
  778. self.app.plotcanvas.graph_event_disconnect(self.mr)
  779. self.app.plotcanvas.graph_event_disconnect(self.mm)
  780. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
  781. self.app.on_mouse_click_over_plot)
  782. self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
  783. self.app.on_mouse_move_over_plot)
  784. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  785. self.app.on_mouse_click_release_over_plot)
  786. self.app.call_source = "app"
  787. self.app.inform.emit('[success] %s' % _("Copper Thieving Tool exit."))
  788. def flatten(self, geometry):
  789. """
  790. Creates a list of non-iterable linear geometry objects.
  791. :param geometry: Shapely type or list or list of list of such.
  792. Results are placed in self.flat_geometry
  793. """
  794. # ## If iterable, expand recursively.
  795. try:
  796. for geo in geometry:
  797. if geo is not None:
  798. self.flatten(geometry=geo)
  799. # ## Not iterable, do the actual indexing and add.
  800. except TypeError:
  801. self.flat_geometry.append(geometry)
  802. return self.flat_geometry