ToolEtchCompensation.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  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
  8. from AppTool import AppTool
  9. from AppGUI.GUIElements import FCButton, FCDoubleSpinner, RadioSet, FCComboBox
  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 ToolEtchCompensation(AppTool):
  21. toolName = _("Etch Compensation Tool")
  22. def __init__(self, app):
  23. self.app = app
  24. self.decimals = self.app.decimals
  25. AppTool.__init__(self, app)
  26. self.tools_frame = QtWidgets.QFrame()
  27. self.tools_frame.setContentsMargins(0, 0, 0, 0)
  28. self.layout.addWidget(self.tools_frame)
  29. self.tools_box = QtWidgets.QVBoxLayout()
  30. self.tools_box.setContentsMargins(0, 0, 0, 0)
  31. self.tools_frame.setLayout(self.tools_box)
  32. # Title
  33. title_label = QtWidgets.QLabel("%s" % self.toolName)
  34. title_label.setStyleSheet("""
  35. QLabel
  36. {
  37. font-size: 16px;
  38. font-weight: bold;
  39. }
  40. """)
  41. self.tools_box.addWidget(title_label)
  42. # Grid Layout
  43. grid0 = QtWidgets.QGridLayout()
  44. grid0.setColumnStretch(0, 0)
  45. grid0.setColumnStretch(1, 1)
  46. self.tools_box.addLayout(grid0)
  47. grid0.addWidget(QtWidgets.QLabel(''), 0, 0, 1, 2)
  48. # Target Gerber Object
  49. self.gerber_combo = FCComboBox()
  50. self.gerber_combo.setModel(self.app.collection)
  51. self.gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  52. self.gerber_combo.is_last = True
  53. self.gerber_combo.obj_type = "Gerber"
  54. self.gerber_label = QtWidgets.QLabel('<b>%s:</b>' % _("GERBER"))
  55. self.gerber_label.setToolTip(
  56. _("Gerber object that will be inverted.")
  57. )
  58. grid0.addWidget(self.gerber_label, 1, 0, 1, 2)
  59. grid0.addWidget(self.gerber_combo, 2, 0, 1, 2)
  60. grid0.addWidget(QtWidgets.QLabel(""), 3, 0, 1, 2)
  61. self.param_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
  62. self.param_label.setToolTip('%s.' % _("Parameters for this tool"))
  63. grid0.addWidget(self.param_label, 4, 0, 1, 2)
  64. # Thickness
  65. self.thick_label = QtWidgets.QLabel('%s:' % _('Copper Thickness'))
  66. self.thick_label.setToolTip(
  67. _("The thickness of the copper foil.\n"
  68. "In microns [um].")
  69. )
  70. self.thick_entry = FCDoubleSpinner(callback=self.confirmation_message)
  71. self.thick_entry.set_precision(self.decimals)
  72. self.thick_entry.set_range(0.0000, 9999.9999)
  73. self.thick_entry.setObjectName(_("Thickness"))
  74. grid0.addWidget(self.thick_label, 5, 0, 1, 2)
  75. grid0.addWidget(self.thick_entry, 6, 0, 1, 2)
  76. self.ratio_label = QtWidgets.QLabel('%s:' % _("Ratio"))
  77. self.ratio_label.setToolTip(
  78. _("The ratio of lateral etch versus depth etch.\n"
  79. "Can be:\n"
  80. "- custom -> the user will enter a custom value\n"
  81. "- preselection -> value which depends on a selection of etchants")
  82. )
  83. self.ratio_radio = RadioSet([
  84. {'label': _('PreSelection'), 'value': 'p'},
  85. {'label': _('Custom'), 'value': 'c'}
  86. ])
  87. grid0.addWidget(self.ratio_label, 7, 0, 1, 2)
  88. grid0.addWidget(self.ratio_radio, 8, 0, 1, 2)
  89. separator_line = QtWidgets.QFrame()
  90. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  91. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  92. grid0.addWidget(separator_line, 9, 0, 1, 2)
  93. self.compensate_btn = FCButton(_('Compensate'))
  94. self.compensate_btn.setToolTip(
  95. _("Will increase the copper features thickness to compensate the lateral etch.")
  96. )
  97. self.compensate_btn.setStyleSheet("""
  98. QPushButton
  99. {
  100. font-weight: bold;
  101. }
  102. """)
  103. grid0.addWidget(self.compensate_btn, 10, 0, 1, 2)
  104. self.tools_box.addStretch()
  105. # ## Reset Tool
  106. self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
  107. self.reset_button.setToolTip(
  108. _("Will reset the tool parameters.")
  109. )
  110. self.reset_button.setStyleSheet("""
  111. QPushButton
  112. {
  113. font-weight: bold;
  114. }
  115. """)
  116. self.tools_box.addWidget(self.reset_button)
  117. self.compensate_btn.clicked.connect(self.on_grb_invert)
  118. self.reset_button.clicked.connect(self.set_tool_ui)
  119. self.ratio_radio.activated_custom.connect(self.on_ratio_change)
  120. def install(self, icon=None, separator=None, **kwargs):
  121. AppTool.install(self, icon, separator, shortcut='', **kwargs)
  122. def run(self, toggle=True):
  123. self.app.defaults.report_usage("ToolInvertGerber()")
  124. log.debug("ToolInvertGerber() is running ...")
  125. if toggle:
  126. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  127. if self.app.ui.splitter.sizes()[0] == 0:
  128. self.app.ui.splitter.setSizes([1, 1])
  129. else:
  130. try:
  131. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  132. # if tab is populated with the tool but it does not have the focus, focus on it
  133. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  134. # focus on Tool Tab
  135. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  136. else:
  137. self.app.ui.splitter.setSizes([0, 1])
  138. except AttributeError:
  139. pass
  140. else:
  141. if self.app.ui.splitter.sizes()[0] == 0:
  142. self.app.ui.splitter.setSizes([1, 1])
  143. AppTool.run(self)
  144. self.set_tool_ui()
  145. self.app.ui.notebook.setTabText(2, _("Invert Tool"))
  146. def set_tool_ui(self):
  147. self.thick_entry.set_value(18)
  148. self.ratio_radio.set_value('p')
  149. def on_ratio_change(self, val):
  150. pass
  151. def on_grb_invert(self):
  152. margin = self.margin_entry.get_value()
  153. if round(margin, self.decimals) == 0.0:
  154. margin = 1E-10
  155. join_style = {'r': 1, 'b': 3, 's': 2}[self.join_radio.get_value()]
  156. if join_style is None:
  157. join_style = 'r'
  158. grb_circle_steps = int(self.app.defaults["gerber_circle_steps"])
  159. obj_name = self.gerber_combo.currentText()
  160. outname = obj_name + "_inverted"
  161. # Get source object.
  162. try:
  163. grb_obj = self.app.collection.get_by_name(obj_name)
  164. except Exception as e:
  165. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(obj_name)))
  166. return "Could not retrieve object: %s with error: %s" % (obj_name, str(e))
  167. if grb_obj is None:
  168. if obj_name == '':
  169. obj_name = 'None'
  170. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(obj_name)))
  171. return
  172. xmin, ymin, xmax, ymax = grb_obj.bounds()
  173. grb_box = box(xmin, ymin, xmax, ymax).buffer(margin, resolution=grb_circle_steps, join_style=join_style)
  174. try:
  175. __ = iter(grb_obj.solid_geometry)
  176. except TypeError:
  177. grb_obj.solid_geometry = list(grb_obj.solid_geometry)
  178. new_solid_geometry = deepcopy(grb_box)
  179. for poly in grb_obj.solid_geometry:
  180. new_solid_geometry = new_solid_geometry.difference(poly)
  181. new_options = {}
  182. for opt in grb_obj.options:
  183. new_options[opt] = deepcopy(grb_obj.options[opt])
  184. new_apertures = {}
  185. # for apid, val in grb_obj.apertures.items():
  186. # new_apertures[apid] = {}
  187. # for key in val:
  188. # if key == 'geometry':
  189. # new_apertures[apid]['geometry'] = []
  190. # for elem in val['geometry']:
  191. # geo_elem = {}
  192. # if 'follow' in elem:
  193. # try:
  194. # geo_elem['clear'] = elem['follow'].buffer(val['size'] / 2.0).exterior
  195. # except AttributeError:
  196. # # TODO should test if width or height is bigger
  197. # geo_elem['clear'] = elem['follow'].buffer(val['width'] / 2.0).exterior
  198. # if 'clear' in elem:
  199. # if isinstance(elem['clear'], Polygon):
  200. # try:
  201. # geo_elem['solid'] = elem['clear'].buffer(val['size'] / 2.0, grb_circle_steps)
  202. # except AttributeError:
  203. # # TODO should test if width or height is bigger
  204. # geo_elem['solid'] = elem['clear'].buffer(val['width'] / 2.0, grb_circle_steps)
  205. # else:
  206. # geo_elem['follow'] = elem['clear']
  207. # new_apertures[apid]['geometry'].append(deepcopy(geo_elem))
  208. # else:
  209. # new_apertures[apid][key] = deepcopy(val[key])
  210. if '0' not in new_apertures:
  211. new_apertures['0'] = {}
  212. new_apertures['0']['type'] = 'C'
  213. new_apertures['0']['size'] = 0.0
  214. new_apertures['0']['geometry'] = []
  215. try:
  216. for poly in new_solid_geometry:
  217. new_el = {}
  218. new_el['solid'] = poly
  219. new_el['follow'] = poly.exterior
  220. new_apertures['0']['geometry'].append(new_el)
  221. except TypeError:
  222. new_el = {}
  223. new_el['solid'] = new_solid_geometry
  224. new_el['follow'] = new_solid_geometry.exterior
  225. new_apertures['0']['geometry'].append(new_el)
  226. for td in new_apertures:
  227. print(td, new_apertures[td])
  228. def init_func(new_obj, app_obj):
  229. new_obj.options.update(new_options)
  230. new_obj.options['name'] = outname
  231. new_obj.fill_color = deepcopy(grb_obj.fill_color)
  232. new_obj.outline_color = deepcopy(grb_obj.outline_color)
  233. new_obj.apertures = deepcopy(new_apertures)
  234. new_obj.solid_geometry = deepcopy(new_solid_geometry)
  235. new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None,
  236. local_use=new_obj, use_thread=False)
  237. self.app.app_obj.new_object('gerber', outname, init_func)
  238. def reset_fields(self):
  239. self.gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  240. @staticmethod
  241. def poly2rings(poly):
  242. return [poly.exterior] + [interior for interior in poly.interiors]
  243. # end of file