ToolPunchGerber.py 58 KB

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