ToolPunchGerber.py 54 KB

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