ToolCorners.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # File Author: Marius Adrian Stanciu (c) #
  4. # Date: 5/17/2020 #
  5. # MIT Licence #
  6. # ##########################################################
  7. from PyQt5 import QtWidgets, QtCore, QtGui
  8. from appTool import AppTool
  9. from appGUI.GUIElements import FCDoubleSpinner, FCCheckBox, FCComboBox, FCButton, RadioSet, FCLabel
  10. from shapely.geometry import MultiPolygon, LineString
  11. from copy import deepcopy
  12. import logging
  13. import gettext
  14. import appTranslation as fcTranslate
  15. import builtins
  16. fcTranslate.apply_language('strings')
  17. if '_' not in builtins.__dict__:
  18. _ = gettext.gettext
  19. log = logging.getLogger('base')
  20. class ToolCorners(AppTool):
  21. def __init__(self, app):
  22. AppTool.__init__(self, app)
  23. self.app = app
  24. self.canvas = self.app.plotcanvas
  25. self.decimals = self.app.decimals
  26. self.units = ''
  27. # #############################################################################
  28. # ######################### Tool GUI ##########################################
  29. # #############################################################################
  30. self.ui = CornersUI(layout=self.layout, app=self.app)
  31. self.toolName = self.ui.toolName
  32. # Objects involved in Copper thieving
  33. self.grb_object = None
  34. # store the flattened geometry here:
  35. self.flat_geometry = []
  36. # Tool properties
  37. self.fid_dia = None
  38. self.grb_steps_per_circle = self.app.defaults["gerber_circle_steps"]
  39. # SIGNALS
  40. self.ui.add_marker_button.clicked.connect(self.add_markers)
  41. self.ui.toggle_all_cb.toggled.connect(self.on_toggle_all)
  42. def run(self, toggle=True):
  43. self.app.defaults.report_usage("ToolCorners()")
  44. if toggle:
  45. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  46. if self.app.ui.splitter.sizes()[0] == 0:
  47. self.app.ui.splitter.setSizes([1, 1])
  48. else:
  49. try:
  50. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  51. # if tab is populated with the tool but it does not have the focus, focus on it
  52. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  53. # focus on Tool Tab
  54. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  55. else:
  56. self.app.ui.splitter.setSizes([0, 1])
  57. except AttributeError:
  58. pass
  59. else:
  60. if self.app.ui.splitter.sizes()[0] == 0:
  61. self.app.ui.splitter.setSizes([1, 1])
  62. AppTool.run(self)
  63. self.set_tool_ui()
  64. self.app.ui.notebook.setTabText(2, _("Corners Tool"))
  65. def install(self, icon=None, separator=None, **kwargs):
  66. AppTool.install(self, icon, separator, shortcut='Alt+M', **kwargs)
  67. def set_tool_ui(self):
  68. self.units = self.app.defaults['units']
  69. self.ui.thick_entry.set_value(self.app.defaults["tools_corners_thickness"])
  70. self.ui.l_entry.set_value(float(self.app.defaults["tools_corners_length"]))
  71. self.ui.margin_entry.set_value(float(self.app.defaults["tools_corners_margin"]))
  72. self.ui.toggle_all_cb.set_value(False)
  73. self.ui.type_radio.set_value(self.app.defaults["tools_corners_type"])
  74. def on_toggle_all(self, val):
  75. self.ui.bl_cb.set_value(val)
  76. self.ui.br_cb.set_value(val)
  77. self.ui.tl_cb.set_value(val)
  78. self.ui.tr_cb.set_value(val)
  79. def add_markers(self):
  80. self.app.call_source = "corners_tool"
  81. tl_state = self.ui.tl_cb.get_value()
  82. tr_state = self.ui.tr_cb.get_value()
  83. bl_state = self.ui.bl_cb.get_value()
  84. br_state = self.ui.br_cb.get_value()
  85. # get the Gerber object on which the corner marker will be inserted
  86. selection_index = self.ui.object_combo.currentIndex()
  87. model_index = self.app.collection.index(selection_index, 0, self.ui.object_combo.rootModelIndex())
  88. try:
  89. self.grb_object = model_index.internalPointer().obj
  90. except Exception as e:
  91. log.debug("ToolCorners.add_markers() --> %s" % str(e))
  92. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
  93. self.app.call_source = "app"
  94. return
  95. xmin, ymin, xmax, ymax = self.grb_object.bounds()
  96. points = {}
  97. if tl_state:
  98. points['tl'] = (xmin, ymax)
  99. if tr_state:
  100. points['tr'] = (xmax, ymax)
  101. if bl_state:
  102. points['bl'] = (xmin, ymin)
  103. if br_state:
  104. points['br'] = (xmax, ymin)
  105. ret_val = self.add_corners_geo(points, g_obj=self.grb_object)
  106. self.app.call_source = "app"
  107. if ret_val == 'fail':
  108. return
  109. self.grb_object.source_file = self.app.f_handlers.export_gerber(obj_name=self.grb_object.options['name'],
  110. filename=None,
  111. local_use=self.grb_object,
  112. use_thread=False)
  113. self.on_exit()
  114. def add_corners_geo(self, points_storage, g_obj):
  115. """
  116. Add geometry to the solid_geometry of the copper Gerber object
  117. :param points_storage: a dictionary holding the points where to add corners
  118. :param g_obj: the Gerber object where to add the geometry
  119. :return: None
  120. """
  121. marker_type = self.ui.type_radio.get_value()
  122. line_thickness = self.ui.thick_entry.get_value()
  123. line_length = self.ui.l_entry.get_value()
  124. margin = self.ui.margin_entry.get_value()
  125. geo_list = []
  126. if not points_storage:
  127. self.app.inform.emit("[ERROR_NOTCL] %s." % _("Please select at least a location"))
  128. return 'fail'
  129. for key in points_storage:
  130. if key == 'tl':
  131. pt = points_storage[key]
  132. x = pt[0] - margin - line_thickness / 2.0
  133. y = pt[1] + margin + line_thickness / 2.0
  134. if type == 's':
  135. line_geo_hor = LineString([
  136. (x, y), (x + line_length, y)
  137. ])
  138. line_geo_vert = LineString([
  139. (x, y), (x, y - line_length)
  140. ])
  141. else:
  142. line_geo_hor = LineString([
  143. (x - line_length, y), (x + line_length, y)
  144. ])
  145. line_geo_vert = LineString([
  146. (x, y + line_length), (x, y - line_length)
  147. ])
  148. geo_list.append(line_geo_hor)
  149. geo_list.append(line_geo_vert)
  150. if key == 'tr':
  151. pt = points_storage[key]
  152. x = pt[0] + margin + line_thickness / 2.0
  153. y = pt[1] + margin + line_thickness / 2.0
  154. if type == 's':
  155. line_geo_hor = LineString([
  156. (x, y), (x - line_length, y)
  157. ])
  158. line_geo_vert = LineString([
  159. (x, y), (x, y - line_length)
  160. ])
  161. else:
  162. line_geo_hor = LineString([
  163. (x + line_length, y), (x - line_length, y)
  164. ])
  165. line_geo_vert = LineString([
  166. (x, y + line_length), (x, y - line_length)
  167. ])
  168. geo_list.append(line_geo_hor)
  169. geo_list.append(line_geo_vert)
  170. if key == 'bl':
  171. pt = points_storage[key]
  172. x = pt[0] - margin - line_thickness / 2.0
  173. y = pt[1] - margin - line_thickness / 2.0
  174. if type == 's':
  175. line_geo_hor = LineString([
  176. (x, y), (x + line_length, y)
  177. ])
  178. line_geo_vert = LineString([
  179. (x, y), (x, y + line_length)
  180. ])
  181. else:
  182. line_geo_hor = LineString([
  183. (x - line_length, y), (x + line_length, y)
  184. ])
  185. line_geo_vert = LineString([
  186. (x, y - line_length), (x, y + line_length)
  187. ])
  188. geo_list.append(line_geo_hor)
  189. geo_list.append(line_geo_vert)
  190. if key == 'br':
  191. pt = points_storage[key]
  192. x = pt[0] + margin + line_thickness / 2.0
  193. y = pt[1] - margin - line_thickness / 2.0
  194. if type == 's':
  195. line_geo_hor = LineString([
  196. (x, y), (x - line_length, y)
  197. ])
  198. line_geo_vert = LineString([
  199. (x, y), (x, y + line_length)
  200. ])
  201. else:
  202. line_geo_hor = LineString([
  203. (x + line_length, y), (x - line_length, y)
  204. ])
  205. line_geo_vert = LineString([
  206. (x, y - line_length), (x, y + line_length)
  207. ])
  208. geo_list.append(line_geo_hor)
  209. geo_list.append(line_geo_vert)
  210. aperture_found = None
  211. for ap_id, ap_val in g_obj.apertures.items():
  212. if ap_val['type'] == 'C' and ap_val['size'] == line_thickness:
  213. aperture_found = ap_id
  214. break
  215. geo_buff_list = []
  216. if aperture_found:
  217. for geo in geo_list:
  218. geo_buff = geo.buffer(line_thickness / 2.0, resolution=self.grb_steps_per_circle, join_style=2)
  219. geo_buff_list.append(geo_buff)
  220. dict_el = {}
  221. dict_el['follow'] = geo
  222. dict_el['solid'] = geo_buff
  223. g_obj.apertures[aperture_found]['geometry'].append(deepcopy(dict_el))
  224. else:
  225. ap_keys = list(g_obj.apertures.keys())
  226. if ap_keys:
  227. new_apid = str(int(max(ap_keys)) + 1)
  228. else:
  229. new_apid = '10'
  230. g_obj.apertures[new_apid] = {}
  231. g_obj.apertures[new_apid]['type'] = 'C'
  232. g_obj.apertures[new_apid]['size'] = line_thickness
  233. g_obj.apertures[new_apid]['geometry'] = []
  234. for geo in geo_list:
  235. geo_buff = geo.buffer(line_thickness / 2.0, resolution=self.grb_steps_per_circle, join_style=3)
  236. geo_buff_list.append(geo_buff)
  237. dict_el = {}
  238. dict_el['follow'] = geo
  239. dict_el['solid'] = geo_buff
  240. g_obj.apertures[new_apid]['geometry'].append(deepcopy(dict_el))
  241. s_list = []
  242. if g_obj.solid_geometry:
  243. try:
  244. for poly in g_obj.solid_geometry:
  245. s_list.append(poly)
  246. except TypeError:
  247. s_list.append(g_obj.solid_geometry)
  248. geo_buff_list = MultiPolygon(geo_buff_list)
  249. geo_buff_list = geo_buff_list.buffer(0)
  250. try:
  251. for poly in geo_buff_list:
  252. s_list.append(poly)
  253. except TypeError:
  254. s_list.append(geo_buff_list)
  255. g_obj.solid_geometry = MultiPolygon(s_list)
  256. def replot(self, obj, run_thread=True):
  257. def worker_task():
  258. with self.app.proc_container.new('%s...' % _("Plotting")):
  259. obj.plot()
  260. if run_thread:
  261. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  262. else:
  263. worker_task()
  264. def on_exit(self):
  265. # plot the object
  266. try:
  267. self.replot(obj=self.grb_object)
  268. except (AttributeError, TypeError):
  269. return
  270. # update the bounding box values
  271. try:
  272. a, b, c, d = self.grb_object.bounds()
  273. self.grb_object.options['xmin'] = a
  274. self.grb_object.options['ymin'] = b
  275. self.grb_object.options['xmax'] = c
  276. self.grb_object.options['ymax'] = d
  277. except Exception as e:
  278. log.debug("ToolCorners.on_exit() copper_obj bounds error --> %s" % str(e))
  279. # reset the variables
  280. self.grb_object = None
  281. self.app.call_source = "app"
  282. self.app.inform.emit('[success] %s' % _("Corners Tool exit."))
  283. class CornersUI:
  284. toolName = _("Corner Markers Tool")
  285. def __init__(self, layout, app):
  286. self.app = app
  287. self.decimals = self.app.decimals
  288. self.layout = layout
  289. # ## Title
  290. title_label = FCLabel("%s" % self.toolName)
  291. title_label.setStyleSheet("""
  292. QLabel
  293. {
  294. font-size: 16px;
  295. font-weight: bold;
  296. }
  297. """)
  298. self.layout.addWidget(title_label)
  299. self.layout.addWidget(FCLabel(""))
  300. # Gerber object #
  301. self.object_label = FCLabel('<b>%s:</b>' % _("GERBER"))
  302. self.object_label.setToolTip(
  303. _("The Gerber object to which will be added corner markers.")
  304. )
  305. self.object_combo = FCComboBox()
  306. self.object_combo.setModel(self.app.collection)
  307. self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  308. self.object_combo.is_last = True
  309. self.object_combo.obj_type = "Gerber"
  310. self.layout.addWidget(self.object_label)
  311. self.layout.addWidget(self.object_combo)
  312. separator_line = QtWidgets.QFrame()
  313. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  314. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  315. self.layout.addWidget(separator_line)
  316. self.points_label = FCLabel('<b>%s:</b>' % _('Locations'))
  317. self.points_label.setToolTip(
  318. _("Locations where to place corner markers.")
  319. )
  320. self.layout.addWidget(self.points_label)
  321. # BOTTOM LEFT
  322. self.bl_cb = FCCheckBox(_("Bottom Left"))
  323. self.layout.addWidget(self.bl_cb)
  324. # BOTTOM RIGHT
  325. self.br_cb = FCCheckBox(_("Bottom Right"))
  326. self.layout.addWidget(self.br_cb)
  327. # TOP LEFT
  328. self.tl_cb = FCCheckBox(_("Top Left"))
  329. self.layout.addWidget(self.tl_cb)
  330. # TOP RIGHT
  331. self.tr_cb = FCCheckBox(_("Top Right"))
  332. self.layout.addWidget(self.tr_cb)
  333. separator_line = QtWidgets.QFrame()
  334. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  335. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  336. self.layout.addWidget(separator_line)
  337. # Toggle ALL
  338. self.toggle_all_cb = FCCheckBox(_("Toggle ALL"))
  339. self.layout.addWidget(self.toggle_all_cb)
  340. separator_line = QtWidgets.QFrame()
  341. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  342. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  343. self.layout.addWidget(separator_line)
  344. # ## Grid Layout
  345. grid_lay = QtWidgets.QGridLayout()
  346. self.layout.addLayout(grid_lay)
  347. grid_lay.setColumnStretch(0, 0)
  348. grid_lay.setColumnStretch(1, 1)
  349. self.param_label = FCLabel('<b>%s:</b>' % _('Parameters'))
  350. self.param_label.setToolTip(
  351. _("Parameters used for this tool.")
  352. )
  353. grid_lay.addWidget(self.param_label, 0, 0, 1, 2)
  354. # Type of Marker
  355. self.type_label = FCLabel('%s:' % _("Type"))
  356. self.type_label.setToolTip(
  357. _("Shape of the marker.")
  358. )
  359. self.type_radio = RadioSet([
  360. {"label": _("Semi-Cross"), "value": "s"},
  361. {"label": _("Cross"), "value": "c"},
  362. ])
  363. grid_lay.addWidget(self.type_label, 2, 0)
  364. grid_lay.addWidget(self.type_radio, 2, 1)
  365. # Thickness #
  366. self.thick_label = FCLabel('%s:' % _("Thickness"))
  367. self.thick_label.setToolTip(
  368. _("The thickness of the line that makes the corner marker.")
  369. )
  370. self.thick_entry = FCDoubleSpinner(callback=self.confirmation_message)
  371. self.thick_entry.set_range(0.0000, 9.9999)
  372. self.thick_entry.set_precision(self.decimals)
  373. self.thick_entry.setWrapping(True)
  374. self.thick_entry.setSingleStep(10 ** -self.decimals)
  375. grid_lay.addWidget(self.thick_label, 4, 0)
  376. grid_lay.addWidget(self.thick_entry, 4, 1)
  377. # Length #
  378. self.l_label = FCLabel('%s:' % _("Length"))
  379. self.l_label.setToolTip(
  380. _("The length of the line that makes the corner marker.")
  381. )
  382. self.l_entry = FCDoubleSpinner(callback=self.confirmation_message)
  383. self.l_entry.set_range(-9999.9999, 9999.9999)
  384. self.l_entry.set_precision(self.decimals)
  385. self.l_entry.setSingleStep(10 ** -self.decimals)
  386. grid_lay.addWidget(self.l_label, 6, 0)
  387. grid_lay.addWidget(self.l_entry, 6, 1)
  388. # Margin #
  389. self.margin_label = FCLabel('%s:' % _("Margin"))
  390. self.margin_label.setToolTip(
  391. _("Bounding box margin.")
  392. )
  393. self.margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
  394. self.margin_entry.set_range(-9999.9999, 9999.9999)
  395. self.margin_entry.set_precision(self.decimals)
  396. self.margin_entry.setSingleStep(0.1)
  397. grid_lay.addWidget(self.margin_label, 8, 0)
  398. grid_lay.addWidget(self.margin_entry, 8, 1)
  399. separator_line_2 = QtWidgets.QFrame()
  400. separator_line_2.setFrameShape(QtWidgets.QFrame.HLine)
  401. separator_line_2.setFrameShadow(QtWidgets.QFrame.Sunken)
  402. grid_lay.addWidget(separator_line_2, 10, 0, 1, 2)
  403. # ## Insert Corner Marker
  404. self.add_marker_button = FCButton(_("Add Marker"))
  405. self.add_marker_button.setIcon(QtGui.QIcon(self.app.resource_location + '/corners_32.png'))
  406. self.add_marker_button.setToolTip(
  407. _("Will add corner markers to the selected Gerber file.")
  408. )
  409. self.add_marker_button.setStyleSheet("""
  410. QPushButton
  411. {
  412. font-weight: bold;
  413. }
  414. """)
  415. grid_lay.addWidget(self.add_marker_button, 12, 0, 1, 2)
  416. self.layout.addStretch()
  417. # ## Reset Tool
  418. self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
  419. self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
  420. self.reset_button.setToolTip(
  421. _("Will reset the tool parameters.")
  422. )
  423. self.reset_button.setStyleSheet("""
  424. QPushButton
  425. {
  426. font-weight: bold;
  427. }
  428. """)
  429. self.layout.addWidget(self.reset_button)
  430. # #################################### FINSIHED GUI ###########################
  431. # #############################################################################
  432. def confirmation_message(self, accepted, minval, maxval):
  433. if accepted is False:
  434. self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
  435. self.decimals,
  436. minval,
  437. self.decimals,
  438. maxval), False)
  439. else:
  440. self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
  441. def confirmation_message_int(self, accepted, minval, maxval):
  442. if accepted is False:
  443. self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
  444. (_("Edited value is out of range"), minval, maxval), False)
  445. else:
  446. self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)