ToolPunchGerber.py 47 KB

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