ToolInvertGerber.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # File Author: Marius Adrian Stanciu (c) #
  4. # Date: 2/14/2020 #
  5. # MIT Licence #
  6. # ##########################################################
  7. from PyQt5 import QtWidgets, QtCore, QtGui
  8. from appTool import AppTool
  9. from appGUI.GUIElements import FCButton, FCDoubleSpinner, RadioSet, FCComboBox, FCLabel
  10. from shapely.geometry import box
  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 ToolInvertGerber(AppTool):
  21. def __init__(self, app):
  22. self.app = app
  23. self.decimals = self.app.decimals
  24. AppTool.__init__(self, app)
  25. # #############################################################################
  26. # ######################### Tool GUI ##########################################
  27. # #############################################################################
  28. self.ui = InvertUI(layout=self.layout, app=self.app)
  29. self.toolName = self.ui.toolName
  30. self.ui.invert_btn.clicked.connect(self.on_grb_invert)
  31. self.ui.reset_button.clicked.connect(self.set_tool_ui)
  32. def install(self, icon=None, separator=None, **kwargs):
  33. AppTool.install(self, icon, separator, shortcut='', **kwargs)
  34. def run(self, toggle=True):
  35. self.app.defaults.report_usage("ToolInvertGerber()")
  36. log.debug("ToolInvertGerber() is running ...")
  37. if toggle:
  38. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  39. if self.app.ui.splitter.sizes()[0] == 0:
  40. self.app.ui.splitter.setSizes([1, 1])
  41. else:
  42. try:
  43. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  44. # if tab is populated with the tool but it does not have the focus, focus on it
  45. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  46. # focus on Tool Tab
  47. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  48. else:
  49. self.app.ui.splitter.setSizes([0, 1])
  50. except AttributeError:
  51. pass
  52. else:
  53. if self.app.ui.splitter.sizes()[0] == 0:
  54. self.app.ui.splitter.setSizes([1, 1])
  55. AppTool.run(self)
  56. self.set_tool_ui()
  57. self.app.ui.notebook.setTabText(2, _("Invert Tool"))
  58. def set_tool_ui(self):
  59. self.ui.margin_entry.set_value(float(self.app.defaults["tools_invert_margin"]))
  60. self.ui.join_radio.set_value(self.app.defaults["tools_invert_join_style"])
  61. def on_grb_invert(self):
  62. margin = self.ui.margin_entry.get_value()
  63. if round(margin, self.decimals) == 0.0:
  64. margin = 1E-10
  65. join_style = {'r': 1, 'b': 3, 's': 2}[self.ui.join_radio.get_value()]
  66. if join_style is None:
  67. join_style = 'r'
  68. grb_circle_steps = int(self.app.defaults["gerber_circle_steps"])
  69. obj_name = self.ui.gerber_combo.currentText()
  70. outname = obj_name + "_inverted"
  71. # Get source object.
  72. try:
  73. grb_obj = self.app.collection.get_by_name(obj_name)
  74. except Exception as e:
  75. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(obj_name)))
  76. return "Could not retrieve object: %s with error: %s" % (obj_name, str(e))
  77. if grb_obj is None:
  78. if obj_name == '':
  79. obj_name = 'None'
  80. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(obj_name)))
  81. return
  82. xmin, ymin, xmax, ymax = grb_obj.bounds()
  83. grb_box = box(xmin, ymin, xmax, ymax).buffer(margin, resolution=grb_circle_steps, join_style=join_style)
  84. try:
  85. __ = iter(grb_obj.solid_geometry)
  86. except TypeError:
  87. grb_obj.solid_geometry = list(grb_obj.solid_geometry)
  88. new_solid_geometry = deepcopy(grb_box)
  89. for poly in grb_obj.solid_geometry:
  90. new_solid_geometry = new_solid_geometry.difference(poly)
  91. try:
  92. __ = iter(new_solid_geometry)
  93. except TypeError:
  94. new_solid_geometry = [new_solid_geometry]
  95. new_options = {}
  96. for opt in grb_obj.options:
  97. new_options[opt] = deepcopy(grb_obj.options[opt])
  98. new_apertures = {}
  99. if '0' not in new_apertures:
  100. new_apertures['0'] = {}
  101. new_apertures['0']['type'] = 'C'
  102. new_apertures['0']['size'] = 0.0
  103. new_apertures['0']['geometry'] = []
  104. try:
  105. for poly in new_solid_geometry:
  106. new_el = {}
  107. new_el['solid'] = poly
  108. new_el['follow'] = poly.exterior
  109. new_apertures['0']['geometry'].append(new_el)
  110. except TypeError:
  111. new_el = {}
  112. new_el['solid'] = new_solid_geometry
  113. new_el['follow'] = new_solid_geometry.exterior
  114. new_apertures['0']['geometry'].append(new_el)
  115. def init_func(new_obj, app_obj):
  116. new_obj.options.update(new_options)
  117. new_obj.options['name'] = outname
  118. new_obj.fill_color = deepcopy(grb_obj.fill_color)
  119. new_obj.outline_color = deepcopy(grb_obj.outline_color)
  120. new_obj.apertures = deepcopy(new_apertures)
  121. new_obj.solid_geometry = deepcopy(new_solid_geometry)
  122. new_obj.source_file = self.app.f_handlers.export_gerber(obj_name=outname, filename=None,
  123. local_use=new_obj, use_thread=False)
  124. self.app.app_obj.new_object('gerber', outname, init_func)
  125. def reset_fields(self):
  126. self.ui.gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  127. @staticmethod
  128. def poly2rings(poly):
  129. return [poly.exterior] + [interior for interior in poly.interiors]
  130. class InvertUI:
  131. toolName = _("Invert Gerber Tool")
  132. def __init__(self, layout, app):
  133. self.app = app
  134. self.decimals = self.app.decimals
  135. self.layout = layout
  136. # ## Title
  137. title_label = FCLabel("%s" % self.toolName)
  138. title_label.setStyleSheet("""
  139. QLabel
  140. {
  141. font-size: 16px;
  142. font-weight: bold;
  143. }
  144. """)
  145. self.layout.addWidget(title_label)
  146. self.layout.addWidget(FCLabel(""))
  147. self.tools_frame = QtWidgets.QFrame()
  148. self.tools_frame.setContentsMargins(0, 0, 0, 0)
  149. self.layout.addWidget(self.tools_frame)
  150. self.tools_box = QtWidgets.QVBoxLayout()
  151. self.tools_box.setContentsMargins(0, 0, 0, 0)
  152. self.tools_frame.setLayout(self.tools_box)
  153. # Grid Layout
  154. grid0 = QtWidgets.QGridLayout()
  155. grid0.setColumnStretch(0, 0)
  156. grid0.setColumnStretch(1, 1)
  157. self.tools_box.addLayout(grid0)
  158. # Target Gerber Object
  159. self.gerber_combo = FCComboBox()
  160. self.gerber_combo.setModel(self.app.collection)
  161. self.gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  162. self.gerber_combo.is_last = True
  163. self.gerber_combo.obj_type = "Gerber"
  164. self.gerber_label = FCLabel('<b>%s:</b>' % _("GERBER"))
  165. self.gerber_label.setToolTip(
  166. _("Gerber object that will be inverted.")
  167. )
  168. grid0.addWidget(self.gerber_label, 1, 0, 1, 2)
  169. grid0.addWidget(self.gerber_combo, 2, 0, 1, 2)
  170. separator_line = QtWidgets.QFrame()
  171. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  172. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  173. grid0.addWidget(separator_line, 3, 0, 1, 2)
  174. self.param_label = FCLabel("<b>%s:</b>" % _("Parameters"))
  175. self.param_label.setToolTip('%s.' % _("Parameters for this tool"))
  176. grid0.addWidget(self.param_label, 4, 0, 1, 2)
  177. # Margin
  178. self.margin_label = FCLabel('%s:' % _('Margin'))
  179. self.margin_label.setToolTip(
  180. _("Distance by which to avoid\n"
  181. "the edges of the Gerber object.")
  182. )
  183. self.margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
  184. self.margin_entry.set_precision(self.decimals)
  185. self.margin_entry.set_range(0.0000, 9999.9999)
  186. self.margin_entry.setObjectName(_("Margin"))
  187. grid0.addWidget(self.margin_label, 5, 0, 1, 2)
  188. grid0.addWidget(self.margin_entry, 6, 0, 1, 2)
  189. self.join_label = FCLabel('%s:' % _("Lines Join Style"))
  190. self.join_label.setToolTip(
  191. _("The way that the lines in the object outline will be joined.\n"
  192. "Can be:\n"
  193. "- rounded -> an arc is added between two joining lines\n"
  194. "- square -> the lines meet in 90 degrees angle\n"
  195. "- bevel -> the lines are joined by a third line")
  196. )
  197. self.join_radio = RadioSet([
  198. {'label': _('Rounded'), 'value': 'r'},
  199. {'label': _('Square'), 'value': 's'},
  200. {'label': _('Bevel'), 'value': 'b'}
  201. ], orientation='vertical', stretch=False)
  202. grid0.addWidget(self.join_label, 7, 0, 1, 2)
  203. grid0.addWidget(self.join_radio, 8, 0, 1, 2)
  204. separator_line = QtWidgets.QFrame()
  205. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  206. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  207. grid0.addWidget(separator_line, 9, 0, 1, 2)
  208. self.invert_btn = FCButton(_('Invert Gerber'))
  209. self.invert_btn.setToolTip(
  210. _("Will invert the Gerber object: areas that have copper\n"
  211. "will be empty of copper and previous empty area will be\n"
  212. "filled with copper.")
  213. )
  214. self.invert_btn.setStyleSheet("""
  215. QPushButton
  216. {
  217. font-weight: bold;
  218. }
  219. """)
  220. grid0.addWidget(self.invert_btn, 10, 0, 1, 2)
  221. self.tools_box.addStretch()
  222. # ## Reset Tool
  223. self.reset_button = FCButton(_("Reset Tool"))
  224. self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
  225. self.reset_button.setToolTip(
  226. _("Will reset the tool parameters.")
  227. )
  228. self.reset_button.setStyleSheet("""
  229. QPushButton
  230. {
  231. font-weight: bold;
  232. }
  233. """)
  234. self.tools_box.addWidget(self.reset_button)
  235. # #################################### FINSIHED GUI ###########################
  236. # #############################################################################
  237. def confirmation_message(self, accepted, minval, maxval):
  238. if accepted is False:
  239. self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
  240. self.decimals,
  241. minval,
  242. self.decimals,
  243. maxval), False)
  244. else:
  245. self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
  246. def confirmation_message_int(self, accepted, minval, maxval):
  247. if accepted is False:
  248. self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
  249. (_("Edited value is out of range"), minval, maxval), False)
  250. else:
  251. self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)