ToolPunchGerber.py 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994
  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 QtCore, QtWidgets
  8. from FlatCAMTool import FlatCAMTool
  9. from flatcamGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, FCComboBox
  10. from copy import deepcopy
  11. import logging
  12. from shapely.geometry import MultiPolygon, Point
  13. import gettext
  14. import FlatCAMTranslation 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 ToolPunchGerber(FlatCAMTool):
  21. toolName = _("Punch Gerber")
  22. def __init__(self, app):
  23. FlatCAMTool.__init__(self, app)
  24. self.decimals = self.app.decimals
  25. # Title
  26. title_label = QtWidgets.QLabel("%s" % self.toolName)
  27. title_label.setStyleSheet("""
  28. QLabel
  29. {
  30. font-size: 16px;
  31. font-weight: bold;
  32. }
  33. """)
  34. self.layout.addWidget(title_label)
  35. # Punch Drill holes
  36. self.layout.addWidget(QtWidgets.QLabel(""))
  37. # ## Grid Layout
  38. grid_lay = QtWidgets.QGridLayout()
  39. self.layout.addLayout(grid_lay)
  40. grid_lay.setColumnStretch(0, 1)
  41. grid_lay.setColumnStretch(1, 0)
  42. # ## Gerber Object
  43. self.gerber_object_combo = FCComboBox()
  44. self.gerber_object_combo.setModel(self.app.collection)
  45. self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  46. self.gerber_object_combo.is_last = True
  47. self.gerber_object_combo.obj_type = "Gerber"
  48. self.grb_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
  49. self.grb_label.setToolTip('%s.' % _("Gerber into which to punch holes"))
  50. grid_lay.addWidget(self.grb_label, 0, 0, 1, 2)
  51. grid_lay.addWidget(self.gerber_object_combo, 1, 0, 1, 2)
  52. separator_line = QtWidgets.QFrame()
  53. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  54. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  55. grid_lay.addWidget(separator_line, 2, 0, 1, 2)
  56. self.padt_label = QtWidgets.QLabel("<b>%s</b>" % _("Processed Pads Type"))
  57. self.padt_label.setToolTip(
  58. _("The type of pads shape to be processed.\n"
  59. "If the PCB has many SMD pads with rectangular pads,\n"
  60. "disable the Rectangular aperture.")
  61. )
  62. grid_lay.addWidget(self.padt_label, 3, 0, 1, 2)
  63. # Select all
  64. self.select_all_cb = FCCheckBox('%s' % _("ALL"))
  65. grid_lay.addWidget(self.select_all_cb)
  66. # Circular Aperture Selection
  67. self.circular_cb = FCCheckBox('%s' % _("Circular"))
  68. self.circular_cb.setToolTip(
  69. _("Process Circular Pads.")
  70. )
  71. grid_lay.addWidget(self.circular_cb, 5, 0, 1, 2)
  72. # Oblong Aperture Selection
  73. self.oblong_cb = FCCheckBox('%s' % _("Oblong"))
  74. self.oblong_cb.setToolTip(
  75. _("Process Oblong Pads.")
  76. )
  77. grid_lay.addWidget(self.oblong_cb, 6, 0, 1, 2)
  78. # Square Aperture Selection
  79. self.square_cb = FCCheckBox('%s' % _("Square"))
  80. self.square_cb.setToolTip(
  81. _("Process Square Pads.")
  82. )
  83. grid_lay.addWidget(self.square_cb, 7, 0, 1, 2)
  84. # Rectangular Aperture Selection
  85. self.rectangular_cb = FCCheckBox('%s' % _("Rectangular"))
  86. self.rectangular_cb.setToolTip(
  87. _("Process Rectangular Pads.")
  88. )
  89. grid_lay.addWidget(self.rectangular_cb, 8, 0, 1, 2)
  90. # Others type of Apertures Selection
  91. self.other_cb = FCCheckBox('%s' % _("Others"))
  92. self.other_cb.setToolTip(
  93. _("Process pads not in the categories above.")
  94. )
  95. grid_lay.addWidget(self.other_cb, 9, 0, 1, 2)
  96. separator_line = QtWidgets.QFrame()
  97. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  98. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  99. grid_lay.addWidget(separator_line, 10, 0, 1, 2)
  100. # Grid Layout
  101. grid0 = QtWidgets.QGridLayout()
  102. self.layout.addLayout(grid0)
  103. grid0.setColumnStretch(0, 0)
  104. grid0.setColumnStretch(1, 1)
  105. self.method_label = QtWidgets.QLabel('<b>%s:</b>' % _("Method"))
  106. self.method_label.setToolTip(
  107. _("The punch hole source can be:\n"
  108. "- Excellon Object-> the Excellon object drills center will serve as reference.\n"
  109. "- Fixed Diameter -> will try to use the pads center as reference adding fixed diameter holes.\n"
  110. "- Fixed Annular Ring -> will try to keep a set annular ring.\n"
  111. "- Proportional -> will make a Gerber punch hole having the diameter a percentage of the pad diameter.")
  112. )
  113. self.method_punch = RadioSet(
  114. [
  115. {'label': _('Excellon'), 'value': 'exc'},
  116. {'label': _("Fixed Diameter"), 'value': 'fixed'},
  117. {'label': _("Fixed Annular Ring"), 'value': 'ring'},
  118. {'label': _("Proportional"), 'value': 'prop'}
  119. ],
  120. orientation='vertical',
  121. stretch=False)
  122. grid0.addWidget(self.method_label, 0, 0, 1, 2)
  123. grid0.addWidget(self.method_punch, 1, 0, 1, 2)
  124. separator_line = QtWidgets.QFrame()
  125. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  126. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  127. grid0.addWidget(separator_line, 2, 0, 1, 2)
  128. self.exc_label = QtWidgets.QLabel('<b>%s</b>' % _("Excellon"))
  129. self.exc_label.setToolTip(
  130. _("Remove the geometry of Excellon from the Gerber to create the holes in pads.")
  131. )
  132. self.exc_combo = FCComboBox()
  133. self.exc_combo.setModel(self.app.collection)
  134. self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
  135. self.exc_combo.is_last = True
  136. self.exc_combo.obj_type = "Excellon"
  137. grid0.addWidget(self.exc_label, 3, 0, 1, 2)
  138. grid0.addWidget(self.exc_combo, 4, 0, 1, 2)
  139. separator_line = QtWidgets.QFrame()
  140. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  141. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  142. grid0.addWidget(separator_line, 5, 0, 1, 2)
  143. # Fixed Dia
  144. self.fixed_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Diameter"))
  145. grid0.addWidget(self.fixed_label, 6, 0, 1, 2)
  146. # Diameter value
  147. self.dia_entry = FCDoubleSpinner(callback=self.confirmation_message)
  148. self.dia_entry.set_precision(self.decimals)
  149. self.dia_entry.set_range(0.0000, 9999.9999)
  150. self.dia_label = QtWidgets.QLabel('%s:' % _("Value"))
  151. self.dia_label.setToolTip(
  152. _("Fixed hole diameter.")
  153. )
  154. grid0.addWidget(self.dia_label, 8, 0)
  155. grid0.addWidget(self.dia_entry, 8, 1)
  156. separator_line = QtWidgets.QFrame()
  157. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  158. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  159. grid0.addWidget(separator_line, 9, 0, 1, 2)
  160. self.ring_frame = QtWidgets.QFrame()
  161. self.ring_frame.setContentsMargins(0, 0, 0, 0)
  162. grid0.addWidget(self.ring_frame, 10, 0, 1, 2)
  163. self.ring_box = QtWidgets.QVBoxLayout()
  164. self.ring_box.setContentsMargins(0, 0, 0, 0)
  165. self.ring_frame.setLayout(self.ring_box)
  166. # Annular Ring value
  167. self.ring_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Annular Ring"))
  168. self.ring_label.setToolTip(
  169. _("The size of annular ring.\n"
  170. "The copper sliver between the hole exterior\n"
  171. "and the margin of the copper pad.")
  172. )
  173. self.ring_box.addWidget(self.ring_label)
  174. # ## Grid Layout
  175. self.grid1 = QtWidgets.QGridLayout()
  176. self.grid1.setColumnStretch(0, 0)
  177. self.grid1.setColumnStretch(1, 1)
  178. self.ring_box.addLayout(self.grid1)
  179. # Circular Annular Ring Value
  180. self.circular_ring_label = QtWidgets.QLabel('%s:' % _("Circular"))
  181. self.circular_ring_label.setToolTip(
  182. _("The size of annular ring for circular pads.")
  183. )
  184. self.circular_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
  185. self.circular_ring_entry.set_precision(self.decimals)
  186. self.circular_ring_entry.set_range(0.0000, 9999.9999)
  187. self.grid1.addWidget(self.circular_ring_label, 3, 0)
  188. self.grid1.addWidget(self.circular_ring_entry, 3, 1)
  189. # Oblong Annular Ring Value
  190. self.oblong_ring_label = QtWidgets.QLabel('%s:' % _("Oblong"))
  191. self.oblong_ring_label.setToolTip(
  192. _("The size of annular ring for oblong pads.")
  193. )
  194. self.oblong_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
  195. self.oblong_ring_entry.set_precision(self.decimals)
  196. self.oblong_ring_entry.set_range(0.0000, 9999.9999)
  197. self.grid1.addWidget(self.oblong_ring_label, 4, 0)
  198. self.grid1.addWidget(self.oblong_ring_entry, 4, 1)
  199. # Square Annular Ring Value
  200. self.square_ring_label = QtWidgets.QLabel('%s:' % _("Square"))
  201. self.square_ring_label.setToolTip(
  202. _("The size of annular ring for square pads.")
  203. )
  204. self.square_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
  205. self.square_ring_entry.set_precision(self.decimals)
  206. self.square_ring_entry.set_range(0.0000, 9999.9999)
  207. self.grid1.addWidget(self.square_ring_label, 5, 0)
  208. self.grid1.addWidget(self.square_ring_entry, 5, 1)
  209. # Rectangular Annular Ring Value
  210. self.rectangular_ring_label = QtWidgets.QLabel('%s:' % _("Rectangular"))
  211. self.rectangular_ring_label.setToolTip(
  212. _("The size of annular ring for rectangular pads.")
  213. )
  214. self.rectangular_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
  215. self.rectangular_ring_entry.set_precision(self.decimals)
  216. self.rectangular_ring_entry.set_range(0.0000, 9999.9999)
  217. self.grid1.addWidget(self.rectangular_ring_label, 6, 0)
  218. self.grid1.addWidget(self.rectangular_ring_entry, 6, 1)
  219. # Others Annular Ring Value
  220. self.other_ring_label = QtWidgets.QLabel('%s:' % _("Others"))
  221. self.other_ring_label.setToolTip(
  222. _("The size of annular ring for other pads.")
  223. )
  224. self.other_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
  225. self.other_ring_entry.set_precision(self.decimals)
  226. self.other_ring_entry.set_range(0.0000, 9999.9999)
  227. self.grid1.addWidget(self.other_ring_label, 7, 0)
  228. self.grid1.addWidget(self.other_ring_entry, 7, 1)
  229. separator_line = QtWidgets.QFrame()
  230. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  231. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  232. grid0.addWidget(separator_line, 11, 0, 1, 2)
  233. # Proportional value
  234. self.prop_label = QtWidgets.QLabel('<b>%s</b>' % _("Proportional Diameter"))
  235. grid0.addWidget(self.prop_label, 12, 0, 1, 2)
  236. # Diameter value
  237. self.factor_entry = FCDoubleSpinner(callback=self.confirmation_message, suffix='%')
  238. self.factor_entry.set_precision(self.decimals)
  239. self.factor_entry.set_range(0.0000, 100.0000)
  240. self.factor_entry.setSingleStep(0.1)
  241. self.factor_label = QtWidgets.QLabel('%s:' % _("Value"))
  242. self.factor_label.setToolTip(
  243. _("Proportional Diameter.\n"
  244. "The hole diameter will be a fraction of the pad size.")
  245. )
  246. grid0.addWidget(self.factor_label, 13, 0)
  247. grid0.addWidget(self.factor_entry, 13, 1)
  248. separator_line3 = QtWidgets.QFrame()
  249. separator_line3.setFrameShape(QtWidgets.QFrame.HLine)
  250. separator_line3.setFrameShadow(QtWidgets.QFrame.Sunken)
  251. grid0.addWidget(separator_line3, 14, 0, 1, 2)
  252. # Buttons
  253. self.punch_object_button = QtWidgets.QPushButton(_("Punch Gerber"))
  254. self.punch_object_button.setToolTip(
  255. _("Create a Gerber object from the selected object, within\n"
  256. "the specified box.")
  257. )
  258. self.punch_object_button.setStyleSheet("""
  259. QPushButton
  260. {
  261. font-weight: bold;
  262. }
  263. """)
  264. self.layout.addWidget(self.punch_object_button)
  265. self.layout.addStretch()
  266. # ## Reset Tool
  267. self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
  268. self.reset_button.setToolTip(
  269. _("Will reset the tool parameters.")
  270. )
  271. self.reset_button.setStyleSheet("""
  272. QPushButton
  273. {
  274. font-weight: bold;
  275. }
  276. """)
  277. self.layout.addWidget(self.reset_button)
  278. self.units = self.app.defaults['units']
  279. # self.cb_items = [
  280. # self.grid1.itemAt(w).widget() for w in range(self.grid1.count())
  281. # if isinstance(self.grid1.itemAt(w).widget(), FCCheckBox)
  282. # ]
  283. self.circular_ring_entry.setEnabled(False)
  284. self.oblong_ring_entry.setEnabled(False)
  285. self.square_ring_entry.setEnabled(False)
  286. self.rectangular_ring_entry.setEnabled(False)
  287. self.other_ring_entry.setEnabled(False)
  288. self.dia_entry.setDisabled(True)
  289. self.dia_label.setDisabled(True)
  290. self.factor_label.setDisabled(True)
  291. self.factor_entry.setDisabled(True)
  292. # ## Signals
  293. self.method_punch.activated_custom.connect(self.on_method)
  294. self.reset_button.clicked.connect(self.set_tool_ui)
  295. self.punch_object_button.clicked.connect(self.on_generate_object)
  296. self.circular_cb.stateChanged.connect(
  297. lambda state:
  298. self.circular_ring_entry.setDisabled(False) if state else self.circular_ring_entry.setDisabled(True)
  299. )
  300. self.oblong_cb.stateChanged.connect(
  301. lambda state:
  302. self.oblong_ring_entry.setDisabled(False) if state else self.oblong_ring_entry.setDisabled(True)
  303. )
  304. self.square_cb.stateChanged.connect(
  305. lambda state:
  306. self.square_ring_entry.setDisabled(False) if state else self.square_ring_entry.setDisabled(True)
  307. )
  308. self.rectangular_cb.stateChanged.connect(
  309. lambda state:
  310. self.rectangular_ring_entry.setDisabled(False) if state else self.rectangular_ring_entry.setDisabled(True)
  311. )
  312. self.other_cb.stateChanged.connect(
  313. lambda state:
  314. self.other_ring_entry.setDisabled(False) if state else self.other_ring_entry.setDisabled(True)
  315. )
  316. def run(self, toggle=True):
  317. self.app.report_usage("ToolPunchGerber()")
  318. if toggle:
  319. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  320. if self.app.ui.splitter.sizes()[0] == 0:
  321. self.app.ui.splitter.setSizes([1, 1])
  322. else:
  323. try:
  324. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  325. # if tab is populated with the tool but it does not have the focus, focus on it
  326. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  327. # focus on Tool Tab
  328. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  329. else:
  330. self.app.ui.splitter.setSizes([0, 1])
  331. except AttributeError:
  332. pass
  333. else:
  334. if self.app.ui.splitter.sizes()[0] == 0:
  335. self.app.ui.splitter.setSizes([1, 1])
  336. FlatCAMTool.run(self)
  337. self.set_tool_ui()
  338. self.app.ui.notebook.setTabText(2, _("Punch Tool"))
  339. def install(self, icon=None, separator=None, **kwargs):
  340. FlatCAMTool.install(self, icon, separator, shortcut='Alt+H', **kwargs)
  341. def set_tool_ui(self):
  342. self.reset_fields()
  343. self.ui_connect()
  344. self.method_punch.set_value(self.app.defaults["tools_punch_hole_type"])
  345. self.select_all_cb.set_value(False)
  346. self.dia_entry.set_value(float(self.app.defaults["tools_punch_hole_fixed_dia"]))
  347. self.circular_ring_entry.set_value(float(self.app.defaults["tools_punch_circular_ring"]))
  348. self.oblong_ring_entry.set_value(float(self.app.defaults["tools_punch_oblong_ring"]))
  349. self.square_ring_entry.set_value(float(self.app.defaults["tools_punch_square_ring"]))
  350. self.rectangular_ring_entry.set_value(float(self.app.defaults["tools_punch_rectangular_ring"]))
  351. self.other_ring_entry.set_value(float(self.app.defaults["tools_punch_others_ring"]))
  352. self.circular_cb.set_value(self.app.defaults["tools_punch_circular"])
  353. self.oblong_cb.set_value(self.app.defaults["tools_punch_oblong"])
  354. self.square_cb.set_value(self.app.defaults["tools_punch_square"])
  355. self.rectangular_cb.set_value(self.app.defaults["tools_punch_rectangular"])
  356. self.other_cb.set_value(self.app.defaults["tools_punch_others"])
  357. self.factor_entry.set_value(float(self.app.defaults["tools_punch_hole_prop_factor"]))
  358. def on_select_all(self, state):
  359. self.ui_disconnect()
  360. if state:
  361. self.circular_cb.setChecked(True)
  362. self.oblong_cb.setChecked(True)
  363. self.square_cb.setChecked(True)
  364. self.rectangular_cb.setChecked(True)
  365. self.other_cb.setChecked(True)
  366. else:
  367. self.circular_cb.setChecked(False)
  368. self.oblong_cb.setChecked(False)
  369. self.square_cb.setChecked(False)
  370. self.rectangular_cb.setChecked(False)
  371. self.other_cb.setChecked(False)
  372. self.ui_connect()
  373. def on_method(self, val):
  374. self.exc_label.setEnabled(False)
  375. self.exc_combo.setEnabled(False)
  376. self.fixed_label.setEnabled(False)
  377. self.dia_label.setEnabled(False)
  378. self.dia_entry.setEnabled(False)
  379. self.ring_frame.setEnabled(False)
  380. self.prop_label.setEnabled(False)
  381. self.factor_label.setEnabled(False)
  382. self.factor_entry.setEnabled(False)
  383. if val == 'exc':
  384. self.exc_label.setEnabled(True)
  385. self.exc_combo.setEnabled(True)
  386. elif val == 'fixed':
  387. self.fixed_label.setEnabled(True)
  388. self.dia_label.setEnabled(True)
  389. self.dia_entry.setEnabled(True)
  390. elif val == 'ring':
  391. self.ring_frame.setEnabled(True)
  392. elif val == 'prop':
  393. self.prop_label.setEnabled(True)
  394. self.factor_label.setEnabled(True)
  395. self.factor_entry.setEnabled(True)
  396. def ui_connect(self):
  397. self.select_all_cb.stateChanged.connect(self.on_select_all)
  398. def ui_disconnect(self):
  399. try:
  400. self.select_all_cb.stateChanged.disconnect()
  401. except (AttributeError, TypeError):
  402. pass
  403. def on_generate_object(self):
  404. # get the Gerber file who is the source of the punched Gerber
  405. selection_index = self.gerber_object_combo.currentIndex()
  406. model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
  407. try:
  408. grb_obj = model_index.internalPointer().obj
  409. except Exception:
  410. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
  411. return
  412. name = grb_obj.options['name'].rpartition('.')[0]
  413. outname = name + "_punched"
  414. punch_method = self.method_punch.get_value()
  415. new_options = {}
  416. for opt in grb_obj.options:
  417. new_options[opt] = deepcopy(grb_obj.options[opt])
  418. if punch_method == 'exc':
  419. # get the Excellon file whose geometry will create the punch holes
  420. selection_index = self.exc_combo.currentIndex()
  421. model_index = self.app.collection.index(selection_index, 0, self.exc_combo.rootModelIndex())
  422. try:
  423. exc_obj = model_index.internalPointer().obj
  424. except Exception:
  425. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Excellon object loaded ..."))
  426. return
  427. # this is the punching geometry
  428. exc_solid_geometry = MultiPolygon(exc_obj.solid_geometry)
  429. if isinstance(grb_obj.solid_geometry, list):
  430. grb_solid_geometry = MultiPolygon(grb_obj.solid_geometry)
  431. else:
  432. grb_solid_geometry = grb_obj.solid_geometry
  433. # create the punched Gerber solid_geometry
  434. punched_solid_geometry = grb_solid_geometry.difference(exc_solid_geometry)
  435. # update the gerber apertures to include the clear geometry so it can be exported successfully
  436. new_apertures = deepcopy(grb_obj.apertures)
  437. new_apertures_items = new_apertures.items()
  438. # find maximum aperture id
  439. new_apid = max([int(x) for x, __ in new_apertures_items])
  440. # store here the clear geometry, the key is the drill size
  441. holes_apertures = {}
  442. for apid, val in new_apertures_items:
  443. for elem in val['geometry']:
  444. # make it work only for Gerber Flashes who are Points in 'follow'
  445. if 'solid' in elem and isinstance(elem['follow'], Point):
  446. for drill in exc_obj.drills:
  447. clear_apid_size = exc_obj.tools[drill['tool']]['C']
  448. # since there may be drills that do not drill into a pad we test only for drills in a pad
  449. if drill['point'].within(elem['solid']):
  450. geo_elem = {}
  451. geo_elem['clear'] = drill['point']
  452. if clear_apid_size not in holes_apertures:
  453. holes_apertures[clear_apid_size] = {}
  454. holes_apertures[clear_apid_size]['type'] = 'C'
  455. holes_apertures[clear_apid_size]['size'] = clear_apid_size
  456. holes_apertures[clear_apid_size]['geometry'] = []
  457. holes_apertures[clear_apid_size]['geometry'].append(deepcopy(geo_elem))
  458. # add the clear geometry to new apertures; it's easier than to test if there are apertures with the same
  459. # size and add there the clear geometry
  460. for hole_size, ap_val in holes_apertures.items():
  461. new_apid += 1
  462. new_apertures[str(new_apid)] = deepcopy(ap_val)
  463. def init_func(new_obj, app_obj):
  464. new_obj.options.update(new_options)
  465. new_obj.options['name'] = outname
  466. new_obj.fill_color = deepcopy(grb_obj.fill_color)
  467. new_obj.outline_color = deepcopy(grb_obj.outline_color)
  468. new_obj.apertures = deepcopy(new_apertures)
  469. new_obj.solid_geometry = deepcopy(punched_solid_geometry)
  470. new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None,
  471. local_use=new_obj, use_thread=False)
  472. self.app.new_object('gerber', outname, init_func)
  473. elif punch_method == 'fixed':
  474. punch_size = float(self.dia_entry.get_value())
  475. if punch_size == 0.0:
  476. self.app.inform.emit('[WARNING_NOTCL] %s' % _("The value of the fixed diameter is 0.0. Aborting."))
  477. return 'fail'
  478. punching_geo = []
  479. for apid in grb_obj.apertures:
  480. if grb_obj.apertures[apid]['type'] == 'C' and self.circular_cb.get_value():
  481. if punch_size >= float(grb_obj.apertures[apid]['size']):
  482. self.app.inform.emit('[ERROR_NOTCL] %s' %
  483. _("Could not generate punched hole Gerber because the punch hole size"
  484. " is bigger than some of the apertures in the Gerber object."))
  485. return 'fail'
  486. else:
  487. for elem in grb_obj.apertures[apid]['geometry']:
  488. if 'follow' in elem:
  489. if isinstance(elem['follow'], Point):
  490. punching_geo.append(elem['follow'].buffer(punch_size / 2))
  491. elif grb_obj.apertures[apid]['type'] == 'R':
  492. if punch_size >= float(grb_obj.apertures[apid]['width']) or \
  493. punch_size >= float(grb_obj.apertures[apid]['height']):
  494. self.app.inform.emit('[ERROR_NOTCL] %s' %
  495. _("Could not generate punched hole Gerber because the punch hole size"
  496. " is bigger than some of the apertures in the Gerber object."))
  497. return 'fail'
  498. elif round(float(grb_obj.apertures[apid]['width']), self.decimals) == \
  499. round(float(grb_obj.apertures[apid]['height']), self.decimals) and \
  500. self.square_cb.get_value():
  501. for elem in grb_obj.apertures[apid]['geometry']:
  502. if 'follow' in elem:
  503. if isinstance(elem['follow'], Point):
  504. punching_geo.append(elem['follow'].buffer(punch_size / 2))
  505. elif round(float(grb_obj.apertures[apid]['width']), self.decimals) != \
  506. round(float(grb_obj.apertures[apid]['height']), self.decimals) and \
  507. self.rectangular_cb.get_value():
  508. for elem in grb_obj.apertures[apid]['geometry']:
  509. if 'follow' in elem:
  510. if isinstance(elem['follow'], Point):
  511. punching_geo.append(elem['follow'].buffer(punch_size / 2))
  512. elif grb_obj.apertures[apid]['type'] == 'O' and self.oblong_cb.get_value():
  513. for elem in grb_obj.apertures[apid]['geometry']:
  514. if 'follow' in elem:
  515. if isinstance(elem['follow'], Point):
  516. punching_geo.append(elem['follow'].buffer(punch_size / 2))
  517. elif grb_obj.apertures[apid]['type'] not in ['C', 'R', 'O'] and self.other_cb.get_value():
  518. for elem in grb_obj.apertures[apid]['geometry']:
  519. if 'follow' in elem:
  520. if isinstance(elem['follow'], Point):
  521. punching_geo.append(elem['follow'].buffer(punch_size / 2))
  522. punching_geo = MultiPolygon(punching_geo)
  523. if isinstance(grb_obj.solid_geometry, list):
  524. temp_solid_geometry = MultiPolygon(grb_obj.solid_geometry)
  525. else:
  526. temp_solid_geometry = grb_obj.solid_geometry
  527. punched_solid_geometry = temp_solid_geometry.difference(punching_geo)
  528. if punched_solid_geometry == temp_solid_geometry:
  529. self.app.inform.emit('[WARNING_NOTCL] %s' %
  530. _("Could not generate punched hole Gerber because the newly created object "
  531. "geometry is the same as the one in the source object geometry..."))
  532. return 'fail'
  533. # update the gerber apertures to include the clear geometry so it can be exported successfully
  534. new_apertures = deepcopy(grb_obj.apertures)
  535. new_apertures_items = new_apertures.items()
  536. # find maximum aperture id
  537. new_apid = max([int(x) for x, __ in new_apertures_items])
  538. # store here the clear geometry, the key is the drill size
  539. holes_apertures = {}
  540. for apid, val in new_apertures_items:
  541. for elem in val['geometry']:
  542. # make it work only for Gerber Flashes who are Points in 'follow'
  543. if 'solid' in elem and isinstance(elem['follow'], Point):
  544. for geo in punching_geo:
  545. clear_apid_size = punch_size
  546. # since there may be drills that do not drill into a pad we test only for drills in a pad
  547. if geo.within(elem['solid']):
  548. geo_elem = {}
  549. geo_elem['clear'] = geo.centroid
  550. if clear_apid_size not in holes_apertures:
  551. holes_apertures[clear_apid_size] = {}
  552. holes_apertures[clear_apid_size]['type'] = 'C'
  553. holes_apertures[clear_apid_size]['size'] = clear_apid_size
  554. holes_apertures[clear_apid_size]['geometry'] = []
  555. holes_apertures[clear_apid_size]['geometry'].append(deepcopy(geo_elem))
  556. # add the clear geometry to new apertures; it's easier than to test if there are apertures with the same
  557. # size and add there the clear geometry
  558. for hole_size, ap_val in holes_apertures.items():
  559. new_apid += 1
  560. new_apertures[str(new_apid)] = deepcopy(ap_val)
  561. def init_func(new_obj, app_obj):
  562. new_obj.options.update(new_options)
  563. new_obj.options['name'] = outname
  564. new_obj.fill_color = deepcopy(grb_obj.fill_color)
  565. new_obj.outline_color = deepcopy(grb_obj.outline_color)
  566. new_obj.apertures = deepcopy(new_apertures)
  567. new_obj.solid_geometry = deepcopy(punched_solid_geometry)
  568. new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None,
  569. local_use=new_obj, use_thread=False)
  570. self.app.new_object('gerber', outname, init_func)
  571. elif punch_method == 'ring':
  572. circ_r_val = self.circular_ring_entry.get_value()
  573. oblong_r_val = self.oblong_ring_entry.get_value()
  574. square_r_val = self.square_ring_entry.get_value()
  575. rect_r_val = self.rectangular_ring_entry.get_value()
  576. other_r_val = self.other_ring_entry.get_value()
  577. dia = None
  578. if isinstance(grb_obj.solid_geometry, list):
  579. temp_solid_geometry = MultiPolygon(grb_obj.solid_geometry)
  580. else:
  581. temp_solid_geometry = grb_obj.solid_geometry
  582. punched_solid_geometry = temp_solid_geometry
  583. new_apertures = deepcopy(grb_obj.apertures)
  584. new_apertures_items = new_apertures.items()
  585. # find maximum aperture id
  586. new_apid = max([int(x) for x, __ in new_apertures_items])
  587. # store here the clear geometry, the key is the new aperture size
  588. holes_apertures = {}
  589. for apid, apid_value in grb_obj.apertures.items():
  590. ap_type = apid_value['type']
  591. punching_geo = []
  592. if ap_type == 'C' and self.circular_cb.get_value():
  593. dia = float(apid_value['size']) - (2 * circ_r_val)
  594. for elem in apid_value['geometry']:
  595. if 'follow' in elem and isinstance(elem['follow'], Point):
  596. punching_geo.append(elem['follow'].buffer(dia / 2))
  597. elif ap_type == 'O' and self.oblong_cb.get_value():
  598. width = float(apid_value['width'])
  599. height = float(apid_value['height'])
  600. if width > height:
  601. dia = float(apid_value['height']) - (2 * oblong_r_val)
  602. else:
  603. dia = float(apid_value['width']) - (2 * oblong_r_val)
  604. for elem in grb_obj.apertures[apid]['geometry']:
  605. if 'follow' in elem:
  606. if isinstance(elem['follow'], Point):
  607. punching_geo.append(elem['follow'].buffer(dia / 2))
  608. elif ap_type == 'R':
  609. width = float(apid_value['width'])
  610. height = float(apid_value['height'])
  611. # if the height == width (float numbers so the reason for the following)
  612. if round(width, self.decimals) == round(height, self.decimals):
  613. if self.square_cb.get_value():
  614. dia = float(apid_value['height']) - (2 * square_r_val)
  615. for elem in grb_obj.apertures[apid]['geometry']:
  616. if 'follow' in elem:
  617. if isinstance(elem['follow'], Point):
  618. punching_geo.append(elem['follow'].buffer(dia / 2))
  619. elif self.rectangular_cb.get_value():
  620. if width > height:
  621. dia = float(apid_value['height']) - (2 * rect_r_val)
  622. else:
  623. dia = float(apid_value['width']) - (2 * rect_r_val)
  624. for elem in grb_obj.apertures[apid]['geometry']:
  625. if 'follow' in elem:
  626. if isinstance(elem['follow'], Point):
  627. punching_geo.append(elem['follow'].buffer(dia / 2))
  628. elif self.other_cb.get_value():
  629. try:
  630. dia = float(apid_value['size']) - (2 * other_r_val)
  631. except KeyError:
  632. if ap_type == 'AM':
  633. pol = apid_value['geometry'][0]['solid']
  634. x0, y0, x1, y1 = pol.bounds
  635. dx = x1 - x0
  636. dy = y1 - y0
  637. if dx <= dy:
  638. dia = dx - (2 * other_r_val)
  639. else:
  640. dia = dy - (2 * other_r_val)
  641. for elem in grb_obj.apertures[apid]['geometry']:
  642. if 'follow' in elem:
  643. if isinstance(elem['follow'], Point):
  644. punching_geo.append(elem['follow'].buffer(dia / 2))
  645. # if dia is None then none of the above applied so we skip the following
  646. if dia is None:
  647. continue
  648. punching_geo = MultiPolygon(punching_geo)
  649. if punching_geo is None or punching_geo.is_empty:
  650. continue
  651. punched_solid_geometry = punched_solid_geometry.difference(punching_geo)
  652. # update the gerber apertures to include the clear geometry so it can be exported successfully
  653. for elem in apid_value['geometry']:
  654. # make it work only for Gerber Flashes who are Points in 'follow'
  655. if 'solid' in elem and isinstance(elem['follow'], Point):
  656. clear_apid_size = dia
  657. for geo in punching_geo:
  658. # since there may be drills that do not drill into a pad we test only for geos in a pad
  659. if geo.within(elem['solid']):
  660. geo_elem = {}
  661. geo_elem['clear'] = geo.centroid
  662. if clear_apid_size not in holes_apertures:
  663. holes_apertures[clear_apid_size] = {}
  664. holes_apertures[clear_apid_size]['type'] = 'C'
  665. holes_apertures[clear_apid_size]['size'] = clear_apid_size
  666. holes_apertures[clear_apid_size]['geometry'] = []
  667. holes_apertures[clear_apid_size]['geometry'].append(deepcopy(geo_elem))
  668. # add the clear geometry to new apertures; it's easier than to test if there are apertures with the same
  669. # size and add there the clear geometry
  670. for hole_size, ap_val in holes_apertures.items():
  671. new_apid += 1
  672. new_apertures[str(new_apid)] = deepcopy(ap_val)
  673. def init_func(new_obj, app_obj):
  674. new_obj.options.update(new_options)
  675. new_obj.options['name'] = outname
  676. new_obj.fill_color = deepcopy(grb_obj.fill_color)
  677. new_obj.outline_color = deepcopy(grb_obj.outline_color)
  678. new_obj.apertures = deepcopy(new_apertures)
  679. new_obj.solid_geometry = deepcopy(punched_solid_geometry)
  680. new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None,
  681. local_use=new_obj, use_thread=False)
  682. self.app.new_object('gerber', outname, init_func)
  683. elif punch_method == 'prop':
  684. prop_factor = self.factor_entry.get_value() / 100.0
  685. dia = None
  686. if isinstance(grb_obj.solid_geometry, list):
  687. temp_solid_geometry = MultiPolygon(grb_obj.solid_geometry)
  688. else:
  689. temp_solid_geometry = grb_obj.solid_geometry
  690. punched_solid_geometry = temp_solid_geometry
  691. new_apertures = deepcopy(grb_obj.apertures)
  692. new_apertures_items = new_apertures.items()
  693. # find maximum aperture id
  694. new_apid = max([int(x) for x, __ in new_apertures_items])
  695. # store here the clear geometry, the key is the new aperture size
  696. holes_apertures = {}
  697. for apid, apid_value in grb_obj.apertures.items():
  698. ap_type = apid_value['type']
  699. punching_geo = []
  700. if ap_type == 'C' and self.circular_cb.get_value():
  701. dia = float(apid_value['size']) * prop_factor
  702. for elem in apid_value['geometry']:
  703. if 'follow' in elem and isinstance(elem['follow'], Point):
  704. punching_geo.append(elem['follow'].buffer(dia / 2))
  705. elif ap_type == 'O' and self.oblong_cb.get_value():
  706. width = float(apid_value['width'])
  707. height = float(apid_value['height'])
  708. if width > height:
  709. dia = float(apid_value['height']) * prop_factor
  710. else:
  711. dia = float(apid_value['width']) * prop_factor
  712. for elem in grb_obj.apertures[apid]['geometry']:
  713. if 'follow' in elem:
  714. if isinstance(elem['follow'], Point):
  715. punching_geo.append(elem['follow'].buffer(dia / 2))
  716. elif ap_type == 'R':
  717. width = float(apid_value['width'])
  718. height = float(apid_value['height'])
  719. # if the height == width (float numbers so the reason for the following)
  720. if round(width, self.decimals) == round(height, self.decimals):
  721. if self.square_cb.get_value():
  722. dia = float(apid_value['height']) * prop_factor
  723. for elem in grb_obj.apertures[apid]['geometry']:
  724. if 'follow' in elem:
  725. if isinstance(elem['follow'], Point):
  726. punching_geo.append(elem['follow'].buffer(dia / 2))
  727. elif self.rectangular_cb.get_value():
  728. if width > height:
  729. dia = float(apid_value['height']) * prop_factor
  730. else:
  731. dia = float(apid_value['width']) * prop_factor
  732. for elem in grb_obj.apertures[apid]['geometry']:
  733. if 'follow' in elem:
  734. if isinstance(elem['follow'], Point):
  735. punching_geo.append(elem['follow'].buffer(dia / 2))
  736. elif self.other_cb.get_value():
  737. try:
  738. dia = float(apid_value['size']) * prop_factor
  739. except KeyError:
  740. if ap_type == 'AM':
  741. pol = apid_value['geometry'][0]['solid']
  742. x0, y0, x1, y1 = pol.bounds
  743. dx = x1 - x0
  744. dy = y1 - y0
  745. if dx <= dy:
  746. dia = dx * prop_factor
  747. else:
  748. dia = dy * prop_factor
  749. for elem in grb_obj.apertures[apid]['geometry']:
  750. if 'follow' in elem:
  751. if isinstance(elem['follow'], Point):
  752. punching_geo.append(elem['follow'].buffer(dia / 2))
  753. # if dia is None then none of the above applied so we skip the following
  754. if dia is None:
  755. continue
  756. punching_geo = MultiPolygon(punching_geo)
  757. if punching_geo is None or punching_geo.is_empty:
  758. continue
  759. punched_solid_geometry = punched_solid_geometry.difference(punching_geo)
  760. # update the gerber apertures to include the clear geometry so it can be exported successfully
  761. for elem in apid_value['geometry']:
  762. # make it work only for Gerber Flashes who are Points in 'follow'
  763. if 'solid' in elem and isinstance(elem['follow'], Point):
  764. clear_apid_size = dia
  765. for geo in punching_geo:
  766. # since there may be drills that do not drill into a pad we test only for geos in a pad
  767. if geo.within(elem['solid']):
  768. geo_elem = {}
  769. geo_elem['clear'] = geo.centroid
  770. if clear_apid_size not in holes_apertures:
  771. holes_apertures[clear_apid_size] = {}
  772. holes_apertures[clear_apid_size]['type'] = 'C'
  773. holes_apertures[clear_apid_size]['size'] = clear_apid_size
  774. holes_apertures[clear_apid_size]['geometry'] = []
  775. holes_apertures[clear_apid_size]['geometry'].append(deepcopy(geo_elem))
  776. # add the clear geometry to new apertures; it's easier than to test if there are apertures with the same
  777. # size and add there the clear geometry
  778. for hole_size, ap_val in holes_apertures.items():
  779. new_apid += 1
  780. new_apertures[str(new_apid)] = deepcopy(ap_val)
  781. def init_func(new_obj, app_obj):
  782. new_obj.options.update(new_options)
  783. new_obj.options['name'] = outname
  784. new_obj.fill_color = deepcopy(grb_obj.fill_color)
  785. new_obj.outline_color = deepcopy(grb_obj.outline_color)
  786. new_obj.apertures = deepcopy(new_apertures)
  787. new_obj.solid_geometry = deepcopy(punched_solid_geometry)
  788. new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None,
  789. local_use=new_obj, use_thread=False)
  790. self.app.new_object('gerber', outname, init_func)
  791. def reset_fields(self):
  792. self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  793. self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
  794. self.ui_disconnect()