ToolPunchGerber.py 59 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348
  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, QtGui
  8. from appTool import AppTool
  9. from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, FCComboBox, FCTable
  10. from copy import deepcopy
  11. import logging
  12. from shapely.geometry import MultiPolygon, Point
  13. from shapely.ops import unary_union
  14. import gettext
  15. import appTranslation as fcTranslate
  16. import builtins
  17. fcTranslate.apply_language('strings')
  18. if '_' not in builtins.__dict__:
  19. _ = gettext.gettext
  20. log = logging.getLogger('base')
  21. class ToolPunchGerber(AppTool):
  22. def __init__(self, app):
  23. AppTool.__init__(self, app)
  24. self.app = app
  25. self.decimals = self.app.decimals
  26. self.units = self.app.defaults['units']
  27. # store here the old object name
  28. self.old_name = ''
  29. # #############################################################################
  30. # ######################### Tool GUI ##########################################
  31. # #############################################################################
  32. self.ui = PunchUI(layout=self.layout, app=self.app)
  33. self.toolName = self.ui.toolName
  34. # ## Signals
  35. self.ui.method_punch.activated_custom.connect(self.on_method)
  36. self.ui.reset_button.clicked.connect(self.set_tool_ui)
  37. self.ui.punch_object_button.clicked.connect(self.on_generate_object)
  38. self.ui.gerber_object_combo.currentIndexChanged.connect(self.build_tool_ui)
  39. self.ui.circular_cb.stateChanged.connect(
  40. lambda state:
  41. self.ui.circular_ring_entry.setDisabled(False) if state else
  42. self.ui.circular_ring_entry.setDisabled(True)
  43. )
  44. self.ui.oblong_cb.stateChanged.connect(
  45. lambda state:
  46. self.ui.oblong_ring_entry.setDisabled(False) if state else self.ui.oblong_ring_entry.setDisabled(True)
  47. )
  48. self.ui.square_cb.stateChanged.connect(
  49. lambda state:
  50. self.ui.square_ring_entry.setDisabled(False) if state else self.ui.square_ring_entry.setDisabled(True)
  51. )
  52. self.ui.rectangular_cb.stateChanged.connect(
  53. lambda state:
  54. self.ui.rectangular_ring_entry.setDisabled(False) if state else
  55. self.ui.rectangular_ring_entry.setDisabled(True)
  56. )
  57. self.ui.other_cb.stateChanged.connect(
  58. lambda state:
  59. self.ui.other_ring_entry.setDisabled(False) if state else self.ui.other_ring_entry.setDisabled(True)
  60. )
  61. self.ui.circular_cb.stateChanged.connect(self.build_tool_ui)
  62. self.ui.oblong_cb.stateChanged.connect(self.build_tool_ui)
  63. self.ui.square_cb.stateChanged.connect(self.build_tool_ui)
  64. self.ui.rectangular_cb.stateChanged.connect(self.build_tool_ui)
  65. self.ui.other_cb.stateChanged.connect(self.build_tool_ui)
  66. self.ui.gerber_object_combo.currentIndexChanged.connect(self.on_object_combo_changed)
  67. def on_object_combo_changed(self):
  68. # get the Gerber file who is the source of the punched Gerber
  69. selection_index = self.ui.gerber_object_combo.currentIndex()
  70. model_index = self.app.collection.index(selection_index, 0, self.ui.gerber_object_combo.rootModelIndex())
  71. try:
  72. grb_obj = model_index.internalPointer().obj
  73. except Exception:
  74. return
  75. if self.old_name != '':
  76. old_obj = self.app.collection.get_by_name(self.old_name)
  77. old_obj.clear_plot_apertures()
  78. old_obj.mark_shapes.enabled = False
  79. # enable mark shapes
  80. grb_obj.mark_shapes.enabled = True
  81. # create storage for shapes
  82. for ap_code in grb_obj.apertures:
  83. grb_obj.mark_shapes_storage[ap_code] = []
  84. self.old_name = grb_obj.options['name']
  85. def run(self, toggle=True):
  86. self.app.defaults.report_usage("ToolPunchGerber()")
  87. if toggle:
  88. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  89. if self.app.ui.splitter.sizes()[0] == 0:
  90. self.app.ui.splitter.setSizes([1, 1])
  91. else:
  92. try:
  93. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  94. # if tab is populated with the tool but it does not have the focus, focus on it
  95. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  96. # focus on Tool Tab
  97. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  98. else:
  99. self.app.ui.splitter.setSizes([0, 1])
  100. except AttributeError:
  101. pass
  102. else:
  103. if self.app.ui.splitter.sizes()[0] == 0:
  104. self.app.ui.splitter.setSizes([1, 1])
  105. AppTool.run(self)
  106. self.set_tool_ui()
  107. self.build_tool_ui()
  108. self.app.ui.notebook.setTabText(2, _("Punch Tool"))
  109. def install(self, icon=None, separator=None, **kwargs):
  110. AppTool.install(self, icon, separator, shortcut='Alt+H', **kwargs)
  111. def set_tool_ui(self):
  112. self.reset_fields()
  113. self.ui_disconnect()
  114. self.ui_connect()
  115. self.ui.method_punch.set_value(self.app.defaults["tools_punch_hole_type"])
  116. self.ui.select_all_cb.set_value(False)
  117. self.ui.dia_entry.set_value(float(self.app.defaults["tools_punch_hole_fixed_dia"]))
  118. self.ui.circular_ring_entry.set_value(float(self.app.defaults["tools_punch_circular_ring"]))
  119. self.ui.oblong_ring_entry.set_value(float(self.app.defaults["tools_punch_oblong_ring"]))
  120. self.ui.square_ring_entry.set_value(float(self.app.defaults["tools_punch_square_ring"]))
  121. self.ui.rectangular_ring_entry.set_value(float(self.app.defaults["tools_punch_rectangular_ring"]))
  122. self.ui.other_ring_entry.set_value(float(self.app.defaults["tools_punch_others_ring"]))
  123. self.ui.circular_cb.set_value(self.app.defaults["tools_punch_circular"])
  124. self.ui.oblong_cb.set_value(self.app.defaults["tools_punch_oblong"])
  125. self.ui.square_cb.set_value(self.app.defaults["tools_punch_square"])
  126. self.ui.rectangular_cb.set_value(self.app.defaults["tools_punch_rectangular"])
  127. self.ui.other_cb.set_value(self.app.defaults["tools_punch_others"])
  128. self.ui.factor_entry.set_value(float(self.app.defaults["tools_punch_hole_prop_factor"]))
  129. def build_tool_ui(self):
  130. self.ui_disconnect()
  131. # reset table
  132. # self.ui.apertures_table.clear() # this deletes the headers/tooltips too ... not nice!
  133. self.ui.apertures_table.setRowCount(0)
  134. # get the Gerber file who is the source of the punched Gerber
  135. selection_index = self.ui.gerber_object_combo.currentIndex()
  136. model_index = self.app.collection.index(selection_index, 0, self.ui.gerber_object_combo.rootModelIndex())
  137. obj = None
  138. try:
  139. obj = model_index.internalPointer().obj
  140. sort = [int(k) for k in obj.apertures.keys()]
  141. sorted_apertures = sorted(sort)
  142. except Exception:
  143. # no object loaded
  144. sorted_apertures = []
  145. # n = len(sorted_apertures)
  146. # calculate how many rows to add
  147. n = 0
  148. for ap_code in sorted_apertures:
  149. ap_code = str(ap_code)
  150. ap_type = obj.apertures[ap_code]['type']
  151. if ap_type == 'C' and self.ui.circular_cb.get_value() is True:
  152. n += 1
  153. if ap_type == 'R':
  154. if self.ui.square_cb.get_value() is True:
  155. n += 1
  156. elif self.ui.rectangular_cb.get_value() is True:
  157. n += 1
  158. if ap_type == 'O' and self.ui.oblong_cb.get_value() is True:
  159. n += 1
  160. if ap_type not in ['C', 'R', 'O'] and self.ui.other_cb.get_value() is True:
  161. n += 1
  162. self.ui.apertures_table.setRowCount(n)
  163. row = 0
  164. for ap_code in sorted_apertures:
  165. ap_code = str(ap_code)
  166. ap_type = obj.apertures[ap_code]['type']
  167. if ap_type == 'C':
  168. if self.ui.circular_cb.get_value() is False:
  169. continue
  170. elif ap_type == 'R':
  171. if self.ui.square_cb.get_value() is True:
  172. pass
  173. elif self.ui.rectangular_cb.get_value() is True:
  174. pass
  175. else:
  176. continue
  177. elif ap_type == 'O':
  178. if self.ui.oblong_cb.get_value() is False:
  179. continue
  180. elif self.ui.other_cb.get_value() is True:
  181. pass
  182. else:
  183. continue
  184. # Aperture CODE
  185. ap_code_item = QtWidgets.QTableWidgetItem(ap_code)
  186. ap_code_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
  187. # Aperture TYPE
  188. ap_type_item = QtWidgets.QTableWidgetItem(str(ap_type))
  189. ap_type_item.setFlags(QtCore.Qt.ItemIsEnabled)
  190. # Aperture SIZE
  191. try:
  192. if obj.apertures[ap_code]['size'] is not None:
  193. size_val = self.app.dec_format(float(obj.apertures[ap_code]['size']), self.decimals)
  194. ap_size_item = QtWidgets.QTableWidgetItem(str(size_val))
  195. else:
  196. ap_size_item = QtWidgets.QTableWidgetItem('')
  197. except KeyError:
  198. ap_size_item = QtWidgets.QTableWidgetItem('')
  199. ap_size_item.setFlags(QtCore.Qt.ItemIsEnabled)
  200. # Aperture MARK Item
  201. mark_item = FCCheckBox()
  202. mark_item.setLayoutDirection(QtCore.Qt.RightToLeft)
  203. # Empty PLOT ITEM
  204. empty_plot_item = QtWidgets.QTableWidgetItem('')
  205. empty_plot_item.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
  206. empty_plot_item.setFlags(QtCore.Qt.ItemIsEnabled)
  207. self.ui.apertures_table.setItem(row, 0, ap_code_item) # Aperture Code
  208. self.ui.apertures_table.setItem(row, 1, ap_type_item) # Aperture Type
  209. self.ui.apertures_table.setItem(row, 2, ap_size_item) # Aperture Dimensions
  210. self.ui.apertures_table.setItem(row, 3, empty_plot_item)
  211. self.ui.apertures_table.setCellWidget(row, 3, mark_item)
  212. # increment row
  213. row += 1
  214. self.ui.apertures_table.selectColumn(0)
  215. self.ui.apertures_table.resizeColumnsToContents()
  216. self.ui.apertures_table.resizeRowsToContents()
  217. vertical_header = self.ui.apertures_table.verticalHeader()
  218. vertical_header.hide()
  219. # self.ui.apertures_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  220. horizontal_header = self.ui.apertures_table.horizontalHeader()
  221. horizontal_header.setMinimumSectionSize(10)
  222. horizontal_header.setDefaultSectionSize(70)
  223. horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
  224. horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
  225. horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
  226. horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.Fixed)
  227. horizontal_header.resizeSection(3, 17)
  228. self.ui.apertures_table.setColumnWidth(3, 17)
  229. self.ui.apertures_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  230. self.ui.apertures_table.setSortingEnabled(False)
  231. # self.ui.apertures_table.setMinimumHeight(self.ui.apertures_table.getHeight())
  232. # self.ui.apertures_table.setMaximumHeight(self.ui.apertures_table.getHeight())
  233. self.ui_connect()
  234. def on_select_all(self, state):
  235. self.ui_disconnect()
  236. if state:
  237. self.ui.circular_cb.setChecked(True)
  238. self.ui.oblong_cb.setChecked(True)
  239. self.ui.square_cb.setChecked(True)
  240. self.ui.rectangular_cb.setChecked(True)
  241. self.ui.other_cb.setChecked(True)
  242. else:
  243. self.ui.circular_cb.setChecked(False)
  244. self.ui.oblong_cb.setChecked(False)
  245. self.ui.square_cb.setChecked(False)
  246. self.ui.rectangular_cb.setChecked(False)
  247. self.ui.other_cb.setChecked(False)
  248. # get the Gerber file who is the source of the punched Gerber
  249. selection_index = self.ui.gerber_object_combo.currentIndex()
  250. model_index = self.app.collection.index(selection_index, 0, self.ui.gerber_object_combo.rootModelIndex())
  251. try:
  252. grb_obj = model_index.internalPointer().obj
  253. except Exception:
  254. return
  255. grb_obj.clear_plot_apertures()
  256. self.ui_connect()
  257. def on_method(self, val):
  258. self.ui.exc_label.hide()
  259. self.ui.exc_combo.hide()
  260. self.ui.fixed_label.hide()
  261. self.ui.dia_label.hide()
  262. self.ui.dia_entry.hide()
  263. self.ui.ring_frame.hide()
  264. self.ui.prop_label.hide()
  265. self.ui.factor_label.hide()
  266. self.ui.factor_entry.hide()
  267. if val == 'exc':
  268. self.ui.exc_label.show()
  269. self.ui.exc_combo.show()
  270. elif val == 'fixed':
  271. self.ui.fixed_label.show()
  272. self.ui.dia_label.show()
  273. self.ui.dia_entry.show()
  274. elif val == 'ring':
  275. self.ui.ring_frame.show()
  276. elif val == 'prop':
  277. self.ui.prop_label.show()
  278. self.ui.factor_label.show()
  279. self.ui.factor_entry.show()
  280. def ui_connect(self):
  281. self.ui.select_all_cb.stateChanged.connect(self.on_select_all)
  282. # Mark Checkboxes
  283. for row in range(self.ui.apertures_table.rowCount()):
  284. try:
  285. self.ui.apertures_table.cellWidget(row, 3).clicked.disconnect()
  286. except (TypeError, AttributeError):
  287. pass
  288. self.ui.apertures_table.cellWidget(row, 3).clicked.connect(self.on_mark_cb_click_table)
  289. def ui_disconnect(self):
  290. try:
  291. self.ui.select_all_cb.stateChanged.disconnect()
  292. except (AttributeError, TypeError):
  293. pass
  294. # Mark Checkboxes
  295. for row in range(self.ui.apertures_table.rowCount()):
  296. try:
  297. self.ui.apertures_table.cellWidget(row, 3).clicked.disconnect()
  298. except (TypeError, AttributeError):
  299. pass
  300. def on_generate_object(self):
  301. # get the Gerber file who is the source of the punched Gerber
  302. selection_index = self.ui.gerber_object_combo.currentIndex()
  303. model_index = self.app.collection.index(selection_index, 0, self.ui.gerber_object_combo.rootModelIndex())
  304. try:
  305. grb_obj = model_index.internalPointer().obj
  306. except Exception:
  307. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
  308. return
  309. name = grb_obj.options['name'].rpartition('.')[0]
  310. outname = name + "_punched"
  311. punch_method = self.ui.method_punch.get_value()
  312. if punch_method == 'exc':
  313. self.on_excellon_method(grb_obj, outname)
  314. elif punch_method == 'fixed':
  315. self.on_fixed_method(grb_obj, outname)
  316. elif punch_method == 'ring':
  317. self.on_ring_method(grb_obj, outname)
  318. elif punch_method == 'prop':
  319. self.on_proportional_method(grb_obj, outname)
  320. def on_excellon_method(self, grb_obj, outname):
  321. # get the Excellon file whose geometry will create the punch holes
  322. selection_index = self.ui.exc_combo.currentIndex()
  323. model_index = self.app.collection.index(selection_index, 0, self.ui.exc_combo.rootModelIndex())
  324. try:
  325. exc_obj = model_index.internalPointer().obj
  326. except Exception:
  327. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Excellon object loaded ..."))
  328. return
  329. new_options = {}
  330. for opt in grb_obj.options:
  331. new_options[opt] = deepcopy(grb_obj.options[opt])
  332. # selected codes in thre apertures UI table
  333. sel_apid = []
  334. for it in self.ui.apertures_table.selectedItems():
  335. sel_apid.append(it.text())
  336. # this is the punching geometry
  337. exc_solid_geometry = MultiPolygon(exc_obj.solid_geometry)
  338. # this is the target geometry
  339. # if isinstance(grb_obj.solid_geometry, list):
  340. # grb_solid_geometry = MultiPolygon(grb_obj.solid_geometry)
  341. # else:
  342. # grb_solid_geometry = grb_obj.solid_geometry
  343. grb_solid_geometry = []
  344. target_geometry = []
  345. for apid in grb_obj.apertures:
  346. if 'geometry' in grb_obj.apertures[apid]:
  347. for el_geo in grb_obj.apertures[apid]['geometry']:
  348. if 'solid' in el_geo:
  349. if apid in sel_apid:
  350. target_geometry.append(el_geo['solid'])
  351. else:
  352. grb_solid_geometry.append(el_geo['solid'])
  353. target_geometry = MultiPolygon(target_geometry)
  354. # create the punched Gerber solid_geometry
  355. punched_target_geometry = target_geometry.difference(exc_solid_geometry)
  356. # add together the punched geometry and the not affected geometry
  357. punched_solid_geometry = []
  358. try:
  359. for geo in punched_target_geometry.geoms:
  360. punched_solid_geometry.append(geo)
  361. except AttributeError:
  362. punched_solid_geometry.append(punched_target_geometry)
  363. for geo in grb_solid_geometry:
  364. punched_solid_geometry.append(geo)
  365. punched_solid_geometry = unary_union(punched_solid_geometry)
  366. # update the gerber apertures to include the clear geometry so it can be exported successfully
  367. new_apertures = deepcopy(grb_obj.apertures)
  368. new_apertures_items = new_apertures.items()
  369. # find maximum aperture id
  370. new_apid = max([int(x) for x, __ in new_apertures_items])
  371. # store here the clear geometry, the key is the drill size
  372. holes_apertures = {}
  373. for apid, val in new_apertures_items:
  374. if apid in sel_apid:
  375. for elem in val['geometry']:
  376. # make it work only for Gerber Flashes who are Points in 'follow'
  377. if 'solid' in elem and isinstance(elem['follow'], Point):
  378. for tool in exc_obj.tools:
  379. clear_apid_size = exc_obj.tools[tool]['tooldia']
  380. if 'drills' in exc_obj.tools[tool]:
  381. for drill_pt in exc_obj.tools[tool]['drills']:
  382. # since there may be drills that do not drill into a pad we test only for
  383. # drills in a pad
  384. if drill_pt.within(elem['solid']):
  385. geo_elem = {}
  386. geo_elem['clear'] = drill_pt
  387. if clear_apid_size not in holes_apertures:
  388. holes_apertures[clear_apid_size] = {}
  389. holes_apertures[clear_apid_size]['type'] = 'C'
  390. holes_apertures[clear_apid_size]['size'] = clear_apid_size
  391. holes_apertures[clear_apid_size]['geometry'] = []
  392. holes_apertures[clear_apid_size]['geometry'].append(deepcopy(geo_elem))
  393. # add the clear geometry to new apertures; it's easier than to test if there are apertures with the same
  394. # size and add there the clear geometry
  395. for hole_size, ap_val in holes_apertures.items():
  396. new_apid += 1
  397. new_apertures[str(new_apid)] = deepcopy(ap_val)
  398. def init_func(new_obj, app_obj):
  399. new_obj.options.update(new_options)
  400. new_obj.options['name'] = outname
  401. new_obj.fill_color = deepcopy(grb_obj.fill_color)
  402. new_obj.outline_color = deepcopy(grb_obj.outline_color)
  403. new_obj.apertures = deepcopy(new_apertures)
  404. new_obj.solid_geometry = deepcopy(punched_solid_geometry)
  405. new_obj.source_file = self.app.f_handlers.export_gerber(obj_name=outname, filename=None,
  406. local_use=new_obj, use_thread=False)
  407. self.app.app_obj.new_object('gerber', outname, init_func)
  408. def on_fixed_method(self, grb_obj, outname):
  409. punch_size = float(self.ui.dia_entry.get_value())
  410. if punch_size == 0.0:
  411. self.app.inform.emit('[WARNING_NOTCL] %s' % _("The value of the fixed diameter is 0.0. Aborting."))
  412. return 'fail'
  413. fail_msg = _("Could not generate punched hole Gerber because the punch hole size is bigger than"
  414. " some of the apertures in the Gerber object.")
  415. new_options = {}
  416. for opt in grb_obj.options:
  417. new_options[opt] = deepcopy(grb_obj.options[opt])
  418. # selected codes in thre apertures UI table
  419. sel_apid = []
  420. for it in self.ui.apertures_table.selectedItems():
  421. sel_apid.append(it.text())
  422. punching_geo = []
  423. for apid in grb_obj.apertures:
  424. if apid in sel_apid:
  425. if grb_obj.apertures[apid]['type'] == 'C' and self.ui.circular_cb.get_value():
  426. for elem in grb_obj.apertures[apid]['geometry']:
  427. if 'follow' in elem:
  428. if isinstance(elem['follow'], Point):
  429. if punch_size >= float(grb_obj.apertures[apid]['size']):
  430. self.app.inform.emit('[ERROR_NOTCL] %s' % fail_msg)
  431. return 'fail'
  432. punching_geo.append(elem['follow'].buffer(punch_size / 2))
  433. elif grb_obj.apertures[apid]['type'] == 'R':
  434. if round(float(grb_obj.apertures[apid]['width']), self.decimals) == \
  435. round(float(grb_obj.apertures[apid]['height']), self.decimals) and \
  436. self.ui.square_cb.get_value():
  437. for elem in grb_obj.apertures[apid]['geometry']:
  438. if 'follow' in elem:
  439. if isinstance(elem['follow'], Point):
  440. if punch_size >= float(grb_obj.apertures[apid]['width']) or \
  441. punch_size >= float(grb_obj.apertures[apid]['height']):
  442. self.app.inform.emit('[ERROR_NOTCL] %s' % fail_msg)
  443. return 'fail'
  444. punching_geo.append(elem['follow'].buffer(punch_size / 2))
  445. elif round(float(grb_obj.apertures[apid]['width']), self.decimals) != \
  446. round(float(grb_obj.apertures[apid]['height']), self.decimals) and \
  447. self.ui.rectangular_cb.get_value():
  448. for elem in grb_obj.apertures[apid]['geometry']:
  449. if 'follow' in elem:
  450. if isinstance(elem['follow'], Point):
  451. if punch_size >= float(grb_obj.apertures[apid]['width']) or \
  452. punch_size >= float(grb_obj.apertures[apid]['height']):
  453. self.app.inform.emit('[ERROR_NOTCL] %s' % fail_msg)
  454. return 'fail'
  455. punching_geo.append(elem['follow'].buffer(punch_size / 2))
  456. elif grb_obj.apertures[apid]['type'] == 'O' and self.ui.oblong_cb.get_value():
  457. for elem in grb_obj.apertures[apid]['geometry']:
  458. if 'follow' in elem:
  459. if isinstance(elem['follow'], Point):
  460. if punch_size >= float(grb_obj.apertures[apid]['size']):
  461. self.app.inform.emit('[ERROR_NOTCL] %s' % fail_msg)
  462. return 'fail'
  463. punching_geo.append(elem['follow'].buffer(punch_size / 2))
  464. elif grb_obj.apertures[apid]['type'] not in ['C', 'R', 'O'] and self.ui.other_cb.get_value():
  465. for elem in grb_obj.apertures[apid]['geometry']:
  466. if 'follow' in elem:
  467. if isinstance(elem['follow'], Point):
  468. if punch_size >= float(grb_obj.apertures[apid]['size']):
  469. self.app.inform.emit('[ERROR_NOTCL] %s' % fail_msg)
  470. return 'fail'
  471. punching_geo.append(elem['follow'].buffer(punch_size / 2))
  472. punching_geo = MultiPolygon(punching_geo)
  473. if isinstance(grb_obj.solid_geometry, list):
  474. temp_solid_geometry = MultiPolygon(grb_obj.solid_geometry)
  475. else:
  476. temp_solid_geometry = grb_obj.solid_geometry
  477. punched_solid_geometry = temp_solid_geometry.difference(punching_geo)
  478. if punched_solid_geometry == temp_solid_geometry:
  479. self.app.inform.emit('[WARNING_NOTCL] %s' %
  480. _("Could not generate punched hole Gerber because the newly created object "
  481. "geometry is the same as the one in the source object geometry..."))
  482. return 'fail'
  483. # update the gerber apertures to include the clear geometry so it can be exported successfully
  484. new_apertures = deepcopy(grb_obj.apertures)
  485. new_apertures_items = new_apertures.items()
  486. # find maximum aperture id
  487. new_apid = max([int(x) for x, __ in new_apertures_items])
  488. # store here the clear geometry, the key is the drill size
  489. holes_apertures = {}
  490. for apid, val in new_apertures_items:
  491. for elem in val['geometry']:
  492. # make it work only for Gerber Flashes who are Points in 'follow'
  493. if 'solid' in elem and isinstance(elem['follow'], Point):
  494. for geo in punching_geo:
  495. clear_apid_size = punch_size
  496. # since there may be drills that do not drill into a pad we test only for drills in a pad
  497. if geo.within(elem['solid']):
  498. geo_elem = {}
  499. geo_elem['clear'] = geo.centroid
  500. if clear_apid_size not in holes_apertures:
  501. holes_apertures[clear_apid_size] = {}
  502. holes_apertures[clear_apid_size]['type'] = 'C'
  503. holes_apertures[clear_apid_size]['size'] = clear_apid_size
  504. holes_apertures[clear_apid_size]['geometry'] = []
  505. holes_apertures[clear_apid_size]['geometry'].append(deepcopy(geo_elem))
  506. # add the clear geometry to new apertures; it's easier than to test if there are apertures with the same
  507. # size and add there the clear geometry
  508. for hole_size, ap_val in holes_apertures.items():
  509. new_apid += 1
  510. new_apertures[str(new_apid)] = deepcopy(ap_val)
  511. def init_func(new_obj, app_obj):
  512. new_obj.options.update(new_options)
  513. new_obj.options['name'] = outname
  514. new_obj.fill_color = deepcopy(grb_obj.fill_color)
  515. new_obj.outline_color = deepcopy(grb_obj.outline_color)
  516. new_obj.apertures = deepcopy(new_apertures)
  517. new_obj.solid_geometry = deepcopy(punched_solid_geometry)
  518. new_obj.source_file = self.app.f_handlers.export_gerber(obj_name=outname, filename=None,
  519. local_use=new_obj, use_thread=False)
  520. self.app.app_obj.new_object('gerber', outname, init_func)
  521. def on_ring_method(self, grb_obj, outname):
  522. circ_r_val = self.ui.circular_ring_entry.get_value()
  523. oblong_r_val = self.ui.oblong_ring_entry.get_value()
  524. square_r_val = self.ui.square_ring_entry.get_value()
  525. rect_r_val = self.ui.rectangular_ring_entry.get_value()
  526. other_r_val = self.ui.other_ring_entry.get_value()
  527. dia = None
  528. new_options = {}
  529. for opt in grb_obj.options:
  530. new_options[opt] = deepcopy(grb_obj.options[opt])
  531. if isinstance(grb_obj.solid_geometry, list):
  532. temp_solid_geometry = MultiPolygon(grb_obj.solid_geometry)
  533. else:
  534. temp_solid_geometry = grb_obj.solid_geometry
  535. punched_solid_geometry = temp_solid_geometry
  536. new_apertures = deepcopy(grb_obj.apertures)
  537. new_apertures_items = new_apertures.items()
  538. # find maximum aperture id
  539. new_apid = max([int(x) for x, __ in new_apertures_items])
  540. # selected codes in the apertures UI table
  541. sel_apid = []
  542. for it in self.ui.apertures_table.selectedItems():
  543. sel_apid.append(it.text())
  544. # store here the clear geometry, the key is the new aperture size
  545. holes_apertures = {}
  546. for apid, apid_value in grb_obj.apertures.items():
  547. ap_type = apid_value['type']
  548. punching_geo = []
  549. if apid in sel_apid:
  550. if ap_type == 'C' and self.ui.circular_cb.get_value():
  551. dia = float(apid_value['size']) - (2 * circ_r_val)
  552. for elem in apid_value['geometry']:
  553. if 'follow' in elem and isinstance(elem['follow'], Point):
  554. punching_geo.append(elem['follow'].buffer(dia / 2))
  555. elif ap_type == 'O' and self.ui.oblong_cb.get_value():
  556. width = float(apid_value['width'])
  557. height = float(apid_value['height'])
  558. if width > height:
  559. dia = float(apid_value['height']) - (2 * oblong_r_val)
  560. else:
  561. dia = float(apid_value['width']) - (2 * oblong_r_val)
  562. for elem in grb_obj.apertures[apid]['geometry']:
  563. if 'follow' in elem:
  564. if isinstance(elem['follow'], Point):
  565. punching_geo.append(elem['follow'].buffer(dia / 2))
  566. elif ap_type == 'R':
  567. width = float(apid_value['width'])
  568. height = float(apid_value['height'])
  569. # if the height == width (float numbers so the reason for the following)
  570. if round(width, self.decimals) == round(height, self.decimals):
  571. if self.ui.square_cb.get_value():
  572. dia = float(apid_value['height']) - (2 * square_r_val)
  573. for elem in grb_obj.apertures[apid]['geometry']:
  574. if 'follow' in elem:
  575. if isinstance(elem['follow'], Point):
  576. punching_geo.append(elem['follow'].buffer(dia / 2))
  577. elif self.ui.rectangular_cb.get_value():
  578. if width > height:
  579. dia = float(apid_value['height']) - (2 * rect_r_val)
  580. else:
  581. dia = float(apid_value['width']) - (2 * rect_r_val)
  582. for elem in grb_obj.apertures[apid]['geometry']:
  583. if 'follow' in elem:
  584. if isinstance(elem['follow'], Point):
  585. punching_geo.append(elem['follow'].buffer(dia / 2))
  586. elif self.ui.other_cb.get_value():
  587. try:
  588. dia = float(apid_value['size']) - (2 * other_r_val)
  589. except KeyError:
  590. if ap_type == 'AM':
  591. pol = apid_value['geometry'][0]['solid']
  592. x0, y0, x1, y1 = pol.bounds
  593. dx = x1 - x0
  594. dy = y1 - y0
  595. if dx <= dy:
  596. dia = dx - (2 * other_r_val)
  597. else:
  598. dia = dy - (2 * other_r_val)
  599. for elem in grb_obj.apertures[apid]['geometry']:
  600. if 'follow' in elem:
  601. if isinstance(elem['follow'], Point):
  602. punching_geo.append(elem['follow'].buffer(dia / 2))
  603. # if dia is None then none of the above applied so we skip the following
  604. if dia is None:
  605. continue
  606. punching_geo = MultiPolygon(punching_geo)
  607. if punching_geo is None or punching_geo.is_empty:
  608. continue
  609. punched_solid_geometry = punched_solid_geometry.difference(punching_geo)
  610. # update the gerber apertures to include the clear geometry so it can be exported successfully
  611. for elem in apid_value['geometry']:
  612. # make it work only for Gerber Flashes who are Points in 'follow'
  613. if 'solid' in elem and isinstance(elem['follow'], Point):
  614. clear_apid_size = dia
  615. for geo in punching_geo:
  616. # since there may be drills that do not drill into a pad we test only for geos in a pad
  617. if geo.within(elem['solid']):
  618. geo_elem = {}
  619. geo_elem['clear'] = geo.centroid
  620. if clear_apid_size not in holes_apertures:
  621. holes_apertures[clear_apid_size] = {}
  622. holes_apertures[clear_apid_size]['type'] = 'C'
  623. holes_apertures[clear_apid_size]['size'] = clear_apid_size
  624. holes_apertures[clear_apid_size]['geometry'] = []
  625. holes_apertures[clear_apid_size]['geometry'].append(deepcopy(geo_elem))
  626. # add the clear geometry to new apertures; it's easier than to test if there are apertures with the same
  627. # size and add there the clear geometry
  628. for hole_size, ap_val in holes_apertures.items():
  629. new_apid += 1
  630. new_apertures[str(new_apid)] = deepcopy(ap_val)
  631. def init_func(new_obj, app_obj):
  632. new_obj.options.update(new_options)
  633. new_obj.options['name'] = outname
  634. new_obj.fill_color = deepcopy(grb_obj.fill_color)
  635. new_obj.outline_color = deepcopy(grb_obj.outline_color)
  636. new_obj.apertures = deepcopy(new_apertures)
  637. new_obj.solid_geometry = deepcopy(punched_solid_geometry)
  638. new_obj.source_file = self.app.f_handlers.export_gerber(obj_name=outname, filename=None,
  639. local_use=new_obj, use_thread=False)
  640. self.app.app_obj.new_object('gerber', outname, init_func)
  641. def on_proportional_method(self, grb_obj, outname):
  642. prop_factor = self.ui.factor_entry.get_value() / 100.0
  643. dia = None
  644. new_options = {}
  645. for opt in grb_obj.options:
  646. new_options[opt] = deepcopy(grb_obj.options[opt])
  647. if isinstance(grb_obj.solid_geometry, list):
  648. temp_solid_geometry = MultiPolygon(grb_obj.solid_geometry)
  649. else:
  650. temp_solid_geometry = grb_obj.solid_geometry
  651. punched_solid_geometry = temp_solid_geometry
  652. new_apertures = deepcopy(grb_obj.apertures)
  653. new_apertures_items = new_apertures.items()
  654. # find maximum aperture id
  655. new_apid = max([int(x) for x, __ in new_apertures_items])
  656. # selected codes in the apertures UI table
  657. sel_apid = []
  658. for it in self.ui.apertures_table.selectedItems():
  659. sel_apid.append(it.text())
  660. # store here the clear geometry, the key is the new aperture size
  661. holes_apertures = {}
  662. for apid, apid_value in grb_obj.apertures.items():
  663. ap_type = apid_value['type']
  664. punching_geo = []
  665. if apid in sel_apid:
  666. if ap_type == 'C' and self.ui.circular_cb.get_value():
  667. dia = float(apid_value['size']) * prop_factor
  668. for elem in apid_value['geometry']:
  669. if 'follow' in elem and isinstance(elem['follow'], Point):
  670. punching_geo.append(elem['follow'].buffer(dia / 2))
  671. elif ap_type == 'O' and self.ui.oblong_cb.get_value():
  672. width = float(apid_value['width'])
  673. height = float(apid_value['height'])
  674. if width > height:
  675. dia = float(apid_value['height']) * prop_factor
  676. else:
  677. dia = float(apid_value['width']) * prop_factor
  678. for elem in grb_obj.apertures[apid]['geometry']:
  679. if 'follow' in elem:
  680. if isinstance(elem['follow'], Point):
  681. punching_geo.append(elem['follow'].buffer(dia / 2))
  682. elif ap_type == 'R':
  683. width = float(apid_value['width'])
  684. height = float(apid_value['height'])
  685. # if the height == width (float numbers so the reason for the following)
  686. if round(width, self.decimals) == round(height, self.decimals):
  687. if self.ui.square_cb.get_value():
  688. dia = float(apid_value['height']) * prop_factor
  689. for elem in grb_obj.apertures[apid]['geometry']:
  690. if 'follow' in elem:
  691. if isinstance(elem['follow'], Point):
  692. punching_geo.append(elem['follow'].buffer(dia / 2))
  693. elif self.ui.rectangular_cb.get_value():
  694. if width > height:
  695. dia = float(apid_value['height']) * prop_factor
  696. else:
  697. dia = float(apid_value['width']) * prop_factor
  698. for elem in grb_obj.apertures[apid]['geometry']:
  699. if 'follow' in elem:
  700. if isinstance(elem['follow'], Point):
  701. punching_geo.append(elem['follow'].buffer(dia / 2))
  702. elif self.ui.other_cb.get_value():
  703. try:
  704. dia = float(apid_value['size']) * prop_factor
  705. except KeyError:
  706. if ap_type == 'AM':
  707. pol = apid_value['geometry'][0]['solid']
  708. x0, y0, x1, y1 = pol.bounds
  709. dx = x1 - x0
  710. dy = y1 - y0
  711. if dx <= dy:
  712. dia = dx * prop_factor
  713. else:
  714. dia = dy * prop_factor
  715. for elem in grb_obj.apertures[apid]['geometry']:
  716. if 'follow' in elem:
  717. if isinstance(elem['follow'], Point):
  718. punching_geo.append(elem['follow'].buffer(dia / 2))
  719. # if dia is None then none of the above applied so we skip the following
  720. if dia is None:
  721. continue
  722. punching_geo = MultiPolygon(punching_geo)
  723. if punching_geo is None or punching_geo.is_empty:
  724. continue
  725. punched_solid_geometry = punched_solid_geometry.difference(punching_geo)
  726. # update the gerber apertures to include the clear geometry so it can be exported successfully
  727. for elem in apid_value['geometry']:
  728. # make it work only for Gerber Flashes who are Points in 'follow'
  729. if 'solid' in elem and isinstance(elem['follow'], Point):
  730. clear_apid_size = dia
  731. for geo in punching_geo:
  732. # since there may be drills that do not drill into a pad we test only for geos in a pad
  733. if geo.within(elem['solid']):
  734. geo_elem = {}
  735. geo_elem['clear'] = geo.centroid
  736. if clear_apid_size not in holes_apertures:
  737. holes_apertures[clear_apid_size] = {}
  738. holes_apertures[clear_apid_size]['type'] = 'C'
  739. holes_apertures[clear_apid_size]['size'] = clear_apid_size
  740. holes_apertures[clear_apid_size]['geometry'] = []
  741. holes_apertures[clear_apid_size]['geometry'].append(deepcopy(geo_elem))
  742. # add the clear geometry to new apertures; it's easier than to test if there are apertures with the same
  743. # size and add there the clear geometry
  744. for hole_size, ap_val in holes_apertures.items():
  745. new_apid += 1
  746. new_apertures[str(new_apid)] = deepcopy(ap_val)
  747. def init_func(new_obj, app_obj):
  748. new_obj.options.update(new_options)
  749. new_obj.options['name'] = outname
  750. new_obj.fill_color = deepcopy(grb_obj.fill_color)
  751. new_obj.outline_color = deepcopy(grb_obj.outline_color)
  752. new_obj.apertures = deepcopy(new_apertures)
  753. new_obj.solid_geometry = deepcopy(punched_solid_geometry)
  754. new_obj.source_file = self.app.f_handlers.export_gerber(obj_name=outname, filename=None,
  755. local_use=new_obj, use_thread=False)
  756. self.app.app_obj.new_object('gerber', outname, init_func)
  757. def on_mark_cb_click_table(self):
  758. """
  759. Will mark aperture geometries on canvas or delete the markings depending on the checkbox state
  760. :return:
  761. """
  762. try:
  763. cw = self.sender()
  764. cw_index = self.ui.apertures_table.indexAt(cw.pos())
  765. cw_row = cw_index.row()
  766. except AttributeError:
  767. cw_row = 0
  768. except TypeError:
  769. return
  770. try:
  771. aperture = self.ui.apertures_table.item(cw_row, 0).text()
  772. except AttributeError:
  773. return
  774. # get the Gerber file who is the source of the punched Gerber
  775. selection_index = self.ui.gerber_object_combo.currentIndex()
  776. model_index = self.app.collection.index(selection_index, 0, self.ui.gerber_object_combo.rootModelIndex())
  777. try:
  778. grb_obj = model_index.internalPointer().obj
  779. except Exception:
  780. return
  781. if self.ui.apertures_table.cellWidget(cw_row, 3).isChecked():
  782. # self.plot_aperture(color='#2d4606bf', marked_aperture=aperture, visible=True)
  783. grb_obj.plot_aperture(color=self.app.defaults['global_sel_draw_color'] + 'AA',
  784. marked_aperture=aperture, visible=True, run_thread=True)
  785. else:
  786. grb_obj.clear_plot_apertures(aperture=aperture)
  787. def reset_fields(self):
  788. self.ui.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  789. self.ui.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
  790. self.ui_disconnect()
  791. class PunchUI:
  792. toolName = _("Punch Gerber")
  793. def __init__(self, layout, app):
  794. self.app = app
  795. self.decimals = self.app.decimals
  796. self.layout = layout
  797. # ## Title
  798. title_label = QtWidgets.QLabel("%s" % self.toolName)
  799. title_label.setStyleSheet("""
  800. QLabel
  801. {
  802. font-size: 16px;
  803. font-weight: bold;
  804. }
  805. """)
  806. self.layout.addWidget(title_label)
  807. # Punch Drill holes
  808. self.layout.addWidget(QtWidgets.QLabel(""))
  809. # ## Grid Layout
  810. grid_lay = QtWidgets.QGridLayout()
  811. self.layout.addLayout(grid_lay)
  812. grid_lay.setColumnStretch(0, 1)
  813. grid_lay.setColumnStretch(1, 0)
  814. # ## Gerber Object
  815. self.gerber_object_combo = FCComboBox()
  816. self.gerber_object_combo.setModel(self.app.collection)
  817. self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  818. self.gerber_object_combo.is_last = True
  819. self.gerber_object_combo.obj_type = "Gerber"
  820. self.grb_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
  821. self.grb_label.setToolTip('%s.' % _("Gerber into which to punch holes"))
  822. grid_lay.addWidget(self.grb_label, 0, 0, 1, 2)
  823. grid_lay.addWidget(self.gerber_object_combo, 1, 0, 1, 2)
  824. separator_line = QtWidgets.QFrame()
  825. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  826. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  827. grid_lay.addWidget(separator_line, 2, 0, 1, 2)
  828. self.padt_label = QtWidgets.QLabel("<b>%s</b>" % _("Processed Pads Type"))
  829. self.padt_label.setToolTip(
  830. _("The type of pads shape to be processed.\n"
  831. "If the PCB has many SMD pads with rectangular pads,\n"
  832. "disable the Rectangular aperture.")
  833. )
  834. grid_lay.addWidget(self.padt_label, 3, 0, 1, 2)
  835. pad_all_grid = QtWidgets.QGridLayout()
  836. pad_all_grid.setColumnStretch(0, 0)
  837. pad_all_grid.setColumnStretch(1, 1)
  838. grid_lay.addLayout(pad_all_grid, 5, 0, 1, 2)
  839. pad_grid = QtWidgets.QGridLayout()
  840. pad_grid.setColumnStretch(0, 0)
  841. pad_all_grid.addLayout(pad_grid, 0, 0)
  842. # Select all
  843. self.select_all_cb = FCCheckBox('%s' % _("ALL"))
  844. pad_grid.addWidget(self.select_all_cb, 0, 0)
  845. # Circular Aperture Selection
  846. self.circular_cb = FCCheckBox('%s' % _("Circular"))
  847. self.circular_cb.setToolTip(
  848. _("Process Circular Pads.")
  849. )
  850. pad_grid.addWidget(self.circular_cb, 1, 0)
  851. # Oblong Aperture Selection
  852. self.oblong_cb = FCCheckBox('%s' % _("Oblong"))
  853. self.oblong_cb.setToolTip(
  854. _("Process Oblong Pads.")
  855. )
  856. pad_grid.addWidget(self.oblong_cb, 2, 0)
  857. # Square Aperture Selection
  858. self.square_cb = FCCheckBox('%s' % _("Square"))
  859. self.square_cb.setToolTip(
  860. _("Process Square Pads.")
  861. )
  862. pad_grid.addWidget(self.square_cb, 3, 0)
  863. # Rectangular Aperture Selection
  864. self.rectangular_cb = FCCheckBox('%s' % _("Rectangular"))
  865. self.rectangular_cb.setToolTip(
  866. _("Process Rectangular Pads.")
  867. )
  868. pad_grid.addWidget(self.rectangular_cb, 4, 0)
  869. # Others type of Apertures Selection
  870. self.other_cb = FCCheckBox('%s' % _("Others"))
  871. self.other_cb.setToolTip(
  872. _("Process pads not in the categories above.")
  873. )
  874. pad_grid.addWidget(self.other_cb, 5, 0)
  875. # Aperture Table
  876. self.apertures_table = FCTable()
  877. pad_all_grid.addWidget(self.apertures_table, 0, 1)
  878. self.apertures_table.setColumnCount(4)
  879. self.apertures_table.setHorizontalHeaderLabels([_('Code'), _('Type'), _('Size'), 'M'])
  880. self.apertures_table.setSortingEnabled(False)
  881. self.apertures_table.setRowCount(0)
  882. self.apertures_table.resizeColumnsToContents()
  883. self.apertures_table.resizeRowsToContents()
  884. self.apertures_table.horizontalHeaderItem(0).setToolTip(
  885. _("Aperture Code"))
  886. self.apertures_table.horizontalHeaderItem(1).setToolTip(
  887. _("Type of aperture: circular, rectangle, macros etc"))
  888. self.apertures_table.horizontalHeaderItem(2).setToolTip(
  889. _("Aperture Size:"))
  890. self.apertures_table.horizontalHeaderItem(3).setToolTip(
  891. _("Mark the aperture instances on canvas."))
  892. sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred)
  893. self.apertures_table.setSizePolicy(sizePolicy)
  894. self.apertures_table.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
  895. separator_line = QtWidgets.QFrame()
  896. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  897. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  898. grid_lay.addWidget(separator_line, 10, 0, 1, 2)
  899. # Grid Layout
  900. grid0 = QtWidgets.QGridLayout()
  901. self.layout.addLayout(grid0)
  902. grid0.setColumnStretch(0, 0)
  903. grid0.setColumnStretch(1, 1)
  904. self.method_label = QtWidgets.QLabel('<b>%s:</b>' % _("Method"))
  905. self.method_label.setToolTip(
  906. _("The punch hole source can be:\n"
  907. "- Excellon Object-> the Excellon object drills center will serve as reference.\n"
  908. "- Fixed Diameter -> will try to use the pads center as reference adding fixed diameter holes.\n"
  909. "- Fixed Annular Ring -> will try to keep a set annular ring.\n"
  910. "- Proportional -> will make a Gerber punch hole having the diameter a percentage of the pad diameter.")
  911. )
  912. self.method_punch = RadioSet(
  913. [
  914. {'label': _('Excellon'), 'value': 'exc'},
  915. {'label': _("Fixed Diameter"), 'value': 'fixed'},
  916. {'label': _("Proportional"), 'value': 'prop'},
  917. {'label': _("Fixed Annular Ring"), 'value': 'ring'}
  918. ],
  919. orientation='vertical',
  920. stretch=False)
  921. grid0.addWidget(self.method_label, 0, 0, 1, 2)
  922. grid0.addWidget(self.method_punch, 1, 0, 1, 2)
  923. separator_line = QtWidgets.QFrame()
  924. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  925. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  926. grid0.addWidget(separator_line, 2, 0, 1, 2)
  927. self.exc_label = QtWidgets.QLabel('<b>%s</b>' % _("Excellon"))
  928. self.exc_label.setToolTip(
  929. _("Remove the geometry of Excellon from the Gerber to create the holes in pads.")
  930. )
  931. self.exc_combo = FCComboBox()
  932. self.exc_combo.setModel(self.app.collection)
  933. self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
  934. self.exc_combo.is_last = True
  935. self.exc_combo.obj_type = "Excellon"
  936. grid0.addWidget(self.exc_label, 3, 0, 1, 2)
  937. grid0.addWidget(self.exc_combo, 4, 0, 1, 2)
  938. # Fixed Dia
  939. self.fixed_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Diameter"))
  940. grid0.addWidget(self.fixed_label, 6, 0, 1, 2)
  941. # Diameter value
  942. self.dia_entry = FCDoubleSpinner(callback=self.confirmation_message)
  943. self.dia_entry.set_precision(self.decimals)
  944. self.dia_entry.set_range(0.0000, 9999.9999)
  945. self.dia_label = QtWidgets.QLabel('%s:' % _("Value"))
  946. self.dia_label.setToolTip(
  947. _("Fixed hole diameter.")
  948. )
  949. grid0.addWidget(self.dia_label, 8, 0)
  950. grid0.addWidget(self.dia_entry, 8, 1)
  951. # #############################################################################################################
  952. # RING FRAME
  953. # #############################################################################################################
  954. self.ring_frame = QtWidgets.QFrame()
  955. self.ring_frame.setContentsMargins(0, 0, 0, 0)
  956. grid0.addWidget(self.ring_frame, 10, 0, 1, 2)
  957. self.ring_box = QtWidgets.QVBoxLayout()
  958. self.ring_box.setContentsMargins(0, 0, 0, 0)
  959. self.ring_frame.setLayout(self.ring_box)
  960. # Annular Ring value
  961. self.ring_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Annular Ring"))
  962. self.ring_label.setToolTip(
  963. _("The size of annular ring.\n"
  964. "The copper sliver between the hole exterior\n"
  965. "and the margin of the copper pad.")
  966. )
  967. self.ring_box.addWidget(self.ring_label)
  968. # ## Grid Layout
  969. self.grid1 = QtWidgets.QGridLayout()
  970. self.grid1.setColumnStretch(0, 0)
  971. self.grid1.setColumnStretch(1, 1)
  972. self.ring_box.addLayout(self.grid1)
  973. # Circular Annular Ring Value
  974. self.circular_ring_label = QtWidgets.QLabel('%s:' % _("Circular"))
  975. self.circular_ring_label.setToolTip(
  976. _("The size of annular ring for circular pads.")
  977. )
  978. self.circular_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
  979. self.circular_ring_entry.set_precision(self.decimals)
  980. self.circular_ring_entry.set_range(0.0000, 9999.9999)
  981. self.grid1.addWidget(self.circular_ring_label, 3, 0)
  982. self.grid1.addWidget(self.circular_ring_entry, 3, 1)
  983. # Oblong Annular Ring Value
  984. self.oblong_ring_label = QtWidgets.QLabel('%s:' % _("Oblong"))
  985. self.oblong_ring_label.setToolTip(
  986. _("The size of annular ring for oblong pads.")
  987. )
  988. self.oblong_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
  989. self.oblong_ring_entry.set_precision(self.decimals)
  990. self.oblong_ring_entry.set_range(0.0000, 9999.9999)
  991. self.grid1.addWidget(self.oblong_ring_label, 4, 0)
  992. self.grid1.addWidget(self.oblong_ring_entry, 4, 1)
  993. # Square Annular Ring Value
  994. self.square_ring_label = QtWidgets.QLabel('%s:' % _("Square"))
  995. self.square_ring_label.setToolTip(
  996. _("The size of annular ring for square pads.")
  997. )
  998. self.square_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
  999. self.square_ring_entry.set_precision(self.decimals)
  1000. self.square_ring_entry.set_range(0.0000, 9999.9999)
  1001. self.grid1.addWidget(self.square_ring_label, 5, 0)
  1002. self.grid1.addWidget(self.square_ring_entry, 5, 1)
  1003. # Rectangular Annular Ring Value
  1004. self.rectangular_ring_label = QtWidgets.QLabel('%s:' % _("Rectangular"))
  1005. self.rectangular_ring_label.setToolTip(
  1006. _("The size of annular ring for rectangular pads.")
  1007. )
  1008. self.rectangular_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
  1009. self.rectangular_ring_entry.set_precision(self.decimals)
  1010. self.rectangular_ring_entry.set_range(0.0000, 9999.9999)
  1011. self.grid1.addWidget(self.rectangular_ring_label, 6, 0)
  1012. self.grid1.addWidget(self.rectangular_ring_entry, 6, 1)
  1013. # Others Annular Ring Value
  1014. self.other_ring_label = QtWidgets.QLabel('%s:' % _("Others"))
  1015. self.other_ring_label.setToolTip(
  1016. _("The size of annular ring for other pads.")
  1017. )
  1018. self.other_ring_entry = FCDoubleSpinner(callback=self.confirmation_message)
  1019. self.other_ring_entry.set_precision(self.decimals)
  1020. self.other_ring_entry.set_range(0.0000, 9999.9999)
  1021. self.grid1.addWidget(self.other_ring_label, 7, 0)
  1022. self.grid1.addWidget(self.other_ring_entry, 7, 1)
  1023. # #############################################################################################################
  1024. # Proportional value
  1025. self.prop_label = QtWidgets.QLabel('<b>%s</b>' % _("Proportional Diameter"))
  1026. grid0.addWidget(self.prop_label, 12, 0, 1, 2)
  1027. # Diameter value
  1028. self.factor_entry = FCDoubleSpinner(callback=self.confirmation_message, suffix='%')
  1029. self.factor_entry.set_precision(self.decimals)
  1030. self.factor_entry.set_range(0.0000, 100.0000)
  1031. self.factor_entry.setSingleStep(0.1)
  1032. self.factor_label = QtWidgets.QLabel('%s:' % _("Value"))
  1033. self.factor_label.setToolTip(
  1034. _("Proportional Diameter.\n"
  1035. "The hole diameter will be a fraction of the pad size.")
  1036. )
  1037. grid0.addWidget(self.factor_label, 13, 0)
  1038. grid0.addWidget(self.factor_entry, 13, 1)
  1039. separator_line3 = QtWidgets.QFrame()
  1040. separator_line3.setFrameShape(QtWidgets.QFrame.HLine)
  1041. separator_line3.setFrameShadow(QtWidgets.QFrame.Sunken)
  1042. grid0.addWidget(separator_line3, 14, 0, 1, 2)
  1043. # Buttons
  1044. self.punch_object_button = QtWidgets.QPushButton(_("Punch Gerber"))
  1045. self.punch_object_button.setIcon(QtGui.QIcon(self.app.resource_location + '/punch32.png'))
  1046. self.punch_object_button.setToolTip(
  1047. _("Create a Gerber object from the selected object, within\n"
  1048. "the specified box.")
  1049. )
  1050. self.punch_object_button.setStyleSheet("""
  1051. QPushButton
  1052. {
  1053. font-weight: bold;
  1054. }
  1055. """)
  1056. self.layout.addWidget(self.punch_object_button)
  1057. self.layout.addStretch()
  1058. # ## Reset Tool
  1059. self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
  1060. self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
  1061. self.reset_button.setToolTip(
  1062. _("Will reset the tool parameters.")
  1063. )
  1064. self.reset_button.setStyleSheet("""
  1065. QPushButton
  1066. {
  1067. font-weight: bold;
  1068. }
  1069. """)
  1070. self.layout.addWidget(self.reset_button)
  1071. self.circular_ring_entry.setEnabled(False)
  1072. self.oblong_ring_entry.setEnabled(False)
  1073. self.square_ring_entry.setEnabled(False)
  1074. self.rectangular_ring_entry.setEnabled(False)
  1075. self.other_ring_entry.setEnabled(False)
  1076. self.dia_entry.hide()
  1077. self.dia_label.hide()
  1078. self.factor_label.hide()
  1079. self.factor_entry.hide()
  1080. # #################################### FINSIHED GUI ###########################
  1081. # #############################################################################
  1082. def confirmation_message(self, accepted, minval, maxval):
  1083. if accepted is False:
  1084. self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
  1085. self.decimals,
  1086. minval,
  1087. self.decimals,
  1088. maxval), False)
  1089. else:
  1090. self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
  1091. def confirmation_message_int(self, accepted, minval, maxval):
  1092. if accepted is False:
  1093. self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
  1094. (_("Edited value is out of range"), minval, maxval), False)
  1095. else:
  1096. self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)