ToolPunchGerber.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # File Author: Marius Adrian Stanciu (c) #
  4. # Date: 1/24/2020 #
  5. # MIT Licence #
  6. # ##########################################################
  7. from PyQt5 import QtGui, QtCore, QtWidgets
  8. from FlatCAMTool import FlatCAMTool
  9. from flatcamGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, \
  10. OptionalHideInputSection, OptionalInputSection, FCComboBox
  11. from copy import deepcopy
  12. import logging
  13. from shapely.geometry import Polygon, MultiPolygon, Point
  14. from reportlab.graphics import renderPDF
  15. from reportlab.pdfgen import canvas
  16. from reportlab.graphics import renderPM
  17. from reportlab.lib.units import inch, mm
  18. from reportlab.lib.pagesizes import landscape, portrait
  19. from svglib.svglib import svg2rlg
  20. from xml.dom.minidom import parseString as parse_xml_string
  21. from lxml import etree as ET
  22. from io import StringIO
  23. import gettext
  24. import FlatCAMTranslation as fcTranslate
  25. import builtins
  26. fcTranslate.apply_language('strings')
  27. if '_' not in builtins.__dict__:
  28. _ = gettext.gettext
  29. log = logging.getLogger('base')
  30. class ToolPunchGerber(FlatCAMTool):
  31. toolName = _("Punch Gerber")
  32. def __init__(self, app):
  33. FlatCAMTool.__init__(self, app)
  34. self.decimals = self.app.decimals
  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. # Punch Drill holes
  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, 1)
  51. grid_lay.setColumnStretch(1, 0)
  52. # ## Gerber Object
  53. self.gerber_object_combo = QtWidgets.QComboBox()
  54. self.gerber_object_combo.setModel(self.app.collection)
  55. self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  56. self.gerber_object_combo.setCurrentIndex(1)
  57. self.grb_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
  58. self.grb_label.setToolTip('%s.' % _("Gerber into which to punch holes"))
  59. grid_lay.addWidget(self.grb_label, 0, 0, 1, 2)
  60. grid_lay.addWidget(self.gerber_object_combo, 1, 0, 1, 2)
  61. separator_line = QtWidgets.QFrame()
  62. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  63. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  64. grid_lay.addWidget(separator_line, 2, 0, 1, 2)
  65. # Grid Layout
  66. grid0 = QtWidgets.QGridLayout()
  67. self.layout.addLayout(grid0)
  68. grid0.setColumnStretch(0, 0)
  69. grid0.setColumnStretch(1, 1)
  70. self.method_label = QtWidgets.QLabel('%s:' % _("Method"))
  71. self.method_label.setToolTip(
  72. _("The punch hole source can be:\n"
  73. "- Excellon Object-> the Excellon object drills center will serve as reference.\n"
  74. "- Fixed Diameter -> will try to use the pads center as reference adding fixed diameter holes.\n"
  75. "- Fixed Annular Ring -> will try to keep a set annular ring.\n"
  76. "- Proportional -> will make a Gerber punch hole having the diameter a percentage of the pad diameter.\n")
  77. )
  78. self.method_punch = RadioSet(
  79. [
  80. {'label': _('Excellon'), 'value': 'exc'},
  81. {'label': _("Fixed Diameter"), 'value': 'fixed'},
  82. {'label': _("Fixed Annular Ring"), 'value': 'ring'},
  83. {'label': _("Proportional"), 'value': 'prop'}
  84. ],
  85. orientation='vertical',
  86. stretch=False)
  87. grid0.addWidget(self.method_label, 0, 0)
  88. grid0.addWidget(self.method_punch, 0, 1)
  89. separator_line = QtWidgets.QFrame()
  90. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  91. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  92. grid0.addWidget(separator_line, 1, 0, 1, 2)
  93. self.exc_label = QtWidgets.QLabel('<b>%s</b>' % _("Excellon"))
  94. self.exc_label.setToolTip(
  95. _("Remove the geometry of Excellon from the Gerber to create the holes in pads.")
  96. )
  97. self.exc_combo = QtWidgets.QComboBox()
  98. self.exc_combo.setModel(self.app.collection)
  99. self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
  100. self.exc_combo.setCurrentIndex(1)
  101. grid0.addWidget(self.exc_label, 2, 0, 1, 2)
  102. grid0.addWidget(self.exc_combo, 3, 0, 1, 2)
  103. separator_line = QtWidgets.QFrame()
  104. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  105. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  106. grid0.addWidget(separator_line, 5, 0, 1, 2)
  107. # Fixed Dia
  108. self.fixed_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Diameter"))
  109. grid0.addWidget(self.fixed_label, 6, 0, 1, 2)
  110. # Diameter value
  111. self.dia_entry = FCDoubleSpinner()
  112. self.dia_entry.set_precision(self.decimals)
  113. self.dia_entry.set_range(0.0000, 9999.9999)
  114. self.dia_label = QtWidgets.QLabel('%s:' % _("Value"))
  115. self.dia_label.setToolTip(
  116. _("Fixed hole diameter.")
  117. )
  118. grid0.addWidget(self.dia_label, 8, 0)
  119. grid0.addWidget(self.dia_entry, 8, 1)
  120. separator_line = QtWidgets.QFrame()
  121. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  122. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  123. grid0.addWidget(separator_line, 9, 0, 1, 2)
  124. self.ring_frame = QtWidgets.QFrame()
  125. self.ring_frame.setContentsMargins(0, 0, 0, 0)
  126. grid0.addWidget(self.ring_frame, 10, 0, 1, 2)
  127. self.ring_box = QtWidgets.QVBoxLayout()
  128. self.ring_box.setContentsMargins(0, 0, 0, 0)
  129. self.ring_frame.setLayout(self.ring_box)
  130. # Annular Ring value
  131. self.ring_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Annular Ring"))
  132. self.ring_label.setToolTip(
  133. _("The size of annular ring.\n"
  134. "The copper sliver between the drill hole exterior\n"
  135. "and the margin of the copper pad.")
  136. )
  137. self.ring_box.addWidget(self.ring_label)
  138. # Select all
  139. self.select_all_cb = FCCheckBox('%s' % _("ALL"))
  140. self.ring_box.addWidget(self.select_all_cb)
  141. # ## Grid Layout
  142. self.grid1 = QtWidgets.QGridLayout()
  143. self.grid1.setColumnStretch(0, 0)
  144. self.grid1.setColumnStretch(1, 1)
  145. self.ring_box.addLayout(self.grid1)
  146. # Circular Annular Ring Value
  147. self.circular_ring_cb = FCCheckBox('%s:' % _("Circular"))
  148. self.circular_ring_cb.setToolTip(
  149. _("The size of annular ring for circular pads.")
  150. )
  151. self.circular_ring_entry = FCDoubleSpinner()
  152. self.circular_ring_entry.set_precision(self.decimals)
  153. self.circular_ring_entry.set_range(0.0000, 9999.9999)
  154. self.c_ois = OptionalInputSection(self.circular_ring_cb, [self.circular_ring_entry])
  155. self.grid1.addWidget(self.circular_ring_cb, 3, 0)
  156. self.grid1.addWidget(self.circular_ring_entry, 3, 1)
  157. # Oblong Annular Ring Value
  158. self.oblong_ring_cb= FCCheckBox('%s:' % _("Oblong"))
  159. self.oblong_ring_cb.setToolTip(
  160. _("The size of annular ring for oblong pads.")
  161. )
  162. self.oblong_ring_entry = FCDoubleSpinner()
  163. self.oblong_ring_entry.set_precision(self.decimals)
  164. self.oblong_ring_entry.set_range(0.0000, 9999.9999)
  165. self.o_ois = OptionalInputSection(self.oblong_ring_cb, [self.oblong_ring_entry])
  166. self.grid1.addWidget(self.oblong_ring_cb, 4, 0)
  167. self.grid1.addWidget(self.oblong_ring_entry, 4, 1)
  168. # Square Annular Ring Value
  169. self.square_ring_cb = FCCheckBox('%s:' % _("Square"))
  170. self.square_ring_cb.setToolTip(
  171. _("The size of annular ring for square pads.")
  172. )
  173. self.square_ring_entry = FCDoubleSpinner()
  174. self.square_ring_entry.set_precision(self.decimals)
  175. self.square_ring_entry.set_range(0.0000, 9999.9999)
  176. self.s_ois = OptionalInputSection(self.square_ring_cb, [self.square_ring_entry])
  177. self.grid1.addWidget(self.square_ring_cb, 5, 0)
  178. self.grid1.addWidget(self.square_ring_entry, 5, 1)
  179. # Rectangular Annular Ring Value
  180. self.rectangular_ring_cb = FCCheckBox('%s:' % _("Rectangular"))
  181. self.rectangular_ring_cb.setToolTip(
  182. _("The size of annular ring for rectangular pads.")
  183. )
  184. self.rectangular_ring_entry = FCDoubleSpinner()
  185. self.rectangular_ring_entry.set_precision(self.decimals)
  186. self.rectangular_ring_entry.set_range(0.0000, 9999.9999)
  187. self.r_ois = OptionalInputSection(self.rectangular_ring_cb, [self.rectangular_ring_entry])
  188. self.grid1.addWidget(self.rectangular_ring_cb, 6, 0)
  189. self.grid1.addWidget(self.rectangular_ring_entry, 6, 1)
  190. # Others Annular Ring Value
  191. self.other_ring_cb = FCCheckBox('%s:' % _("Others"))
  192. self.other_ring_cb.setToolTip(
  193. _("The size of annular ring for other pads.")
  194. )
  195. self.other_ring_entry = FCDoubleSpinner()
  196. self.other_ring_entry.set_precision(self.decimals)
  197. self.other_ring_entry.set_range(0.0000, 9999.9999)
  198. self.ot_ois = OptionalInputSection(self.other_ring_cb, [self.other_ring_entry])
  199. self.grid1.addWidget(self.other_ring_cb, 7, 0)
  200. self.grid1.addWidget(self.other_ring_entry, 7, 1)
  201. separator_line = QtWidgets.QFrame()
  202. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  203. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  204. grid0.addWidget(separator_line, 11, 0, 1, 2)
  205. # Proportional value
  206. self.prop_label = QtWidgets.QLabel('<b>%s</b>' % _("Proportional Diameter"))
  207. grid0.addWidget(self.prop_label, 12, 0, 1, 2)
  208. # Diameter value
  209. self.factor_entry = FCDoubleSpinner(suffix='%')
  210. self.factor_entry.set_precision(self.decimals)
  211. self.factor_entry.set_range(0.0000, 100.0000)
  212. self.factor_entry.setSingleStep(0.1)
  213. self.factor_label = QtWidgets.QLabel('%s:' % _("Value"))
  214. self.factor_label.setToolTip(
  215. _("Proportional Diameter.\n"
  216. "The drill diameter will be a fraction of the pad size.")
  217. )
  218. grid0.addWidget(self.factor_label, 13, 0)
  219. grid0.addWidget(self.factor_entry, 13, 1)
  220. separator_line3 = QtWidgets.QFrame()
  221. separator_line3.setFrameShape(QtWidgets.QFrame.HLine)
  222. separator_line3.setFrameShadow(QtWidgets.QFrame.Sunken)
  223. grid0.addWidget(separator_line3, 14, 0, 1, 2)
  224. # Buttons
  225. self.punch_object_button = QtWidgets.QPushButton(_("Punch Gerber"))
  226. self.punch_object_button.setToolTip(
  227. _("Create a Gerber object from the selected object, within\n"
  228. "the specified box.")
  229. )
  230. self.punch_object_button.setStyleSheet("""
  231. QPushButton
  232. {
  233. font-weight: bold;
  234. }
  235. """)
  236. self.layout.addWidget(self.punch_object_button)
  237. self.layout.addStretch()
  238. # ## Reset Tool
  239. self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
  240. self.reset_button.setToolTip(
  241. _("Will reset the tool parameters.")
  242. )
  243. self.reset_button.setStyleSheet("""
  244. QPushButton
  245. {
  246. font-weight: bold;
  247. }
  248. """)
  249. self.layout.addWidget(self.reset_button)
  250. self.units = self.app.defaults['units']
  251. self.cb_items = [
  252. self.grid1.itemAt(w).widget() for w in range(self.grid1.count())
  253. if isinstance(self.grid1.itemAt(w).widget(), FCCheckBox)
  254. ]
  255. # ## Signals
  256. self.method_punch.activated_custom.connect(self.on_method)
  257. self.reset_button.clicked.connect(self.set_tool_ui)
  258. def run(self, toggle=True):
  259. self.app.report_usage("ToolPunchGerber()")
  260. if toggle:
  261. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  262. if self.app.ui.splitter.sizes()[0] == 0:
  263. self.app.ui.splitter.setSizes([1, 1])
  264. else:
  265. try:
  266. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  267. # if tab is populated with the tool but it does not have the focus, focus on it
  268. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  269. # focus on Tool Tab
  270. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  271. else:
  272. self.app.ui.splitter.setSizes([0, 1])
  273. except AttributeError:
  274. pass
  275. else:
  276. if self.app.ui.splitter.sizes()[0] == 0:
  277. self.app.ui.splitter.setSizes([1, 1])
  278. FlatCAMTool.run(self)
  279. self.set_tool_ui()
  280. self.app.ui.notebook.setTabText(2, _("Punch Tool"))
  281. def install(self, icon=None, separator=None, **kwargs):
  282. FlatCAMTool.install(self, icon, separator, shortcut='ALT+H', **kwargs)
  283. def set_tool_ui(self):
  284. self.reset_fields()
  285. self.ui_connect()
  286. self.method_punch.set_value('exc')
  287. self.select_all_cb.set_value(True)
  288. def on_select_all(self, state):
  289. self.ui_disconnect()
  290. if state:
  291. self.circular_ring_cb.setChecked(True)
  292. self.oblong_ring_cb.setChecked(True)
  293. self.square_ring_cb.setChecked(True)
  294. self.rectangular_ring_cb.setChecked(True)
  295. self.other_ring_cb.setChecked(True)
  296. else:
  297. self.circular_ring_cb.setChecked(False)
  298. self.oblong_ring_cb.setChecked(False)
  299. self.square_ring_cb.setChecked(False)
  300. self.rectangular_ring_cb.setChecked(False)
  301. self.other_ring_cb.setChecked(False)
  302. self.ui_connect()
  303. def on_method(self, val):
  304. self.exc_label.setEnabled(False)
  305. self.exc_combo.setEnabled(False)
  306. self.fixed_label.setEnabled(False)
  307. self.dia_label.setEnabled(False)
  308. self.dia_entry.setEnabled(False)
  309. self.ring_frame.setEnabled(False)
  310. self.prop_label.setEnabled(False)
  311. self.factor_label.setEnabled(False)
  312. self.factor_entry.setEnabled(False)
  313. if val == 'exc':
  314. self.exc_label.setEnabled(True)
  315. self.exc_combo.setEnabled(True)
  316. elif val == 'fixed':
  317. self.fixed_label.setEnabled(True)
  318. self.dia_label.setEnabled(True)
  319. self.dia_entry.setEnabled(True)
  320. elif val == 'ring':
  321. self.ring_frame.setEnabled(True)
  322. elif val == 'prop':
  323. self.prop_label.setEnabled(True)
  324. self.factor_label.setEnabled(True)
  325. self.factor_entry.setEnabled(True)
  326. def on_ring_cb_toggled(self):
  327. sum_cb = 0
  328. for it in self.cb_items:
  329. if it.get_value():
  330. sum_cb += 1
  331. self.ui_disconnect()
  332. if sum_cb == 5:
  333. self.select_all_cb.set_value(True)
  334. else:
  335. self.select_all_cb.set_value(False)
  336. self.ui_connect()
  337. def ui_connect(self):
  338. self.select_all_cb.stateChanged.connect(self.on_select_all)
  339. for it in self.cb_items:
  340. try:
  341. it.stateChanged.connect(self.on_ring_cb_toggled)
  342. except (AttributeError, TypeError):
  343. pass
  344. def ui_disconnect(self):
  345. try:
  346. self.select_all_cb.stateChanged.disconnect()
  347. except (AttributeError, TypeError):
  348. pass
  349. for it in self.cb_items:
  350. try:
  351. it.stateChanged.disconnect()
  352. except (AttributeError, TypeError):
  353. pass
  354. def reset_fields(self):
  355. self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  356. self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
  357. self.ui_disconnect()