ToolPunchGerber.py 53 KB

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