ToolExtractDrills.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. from PyQt5 import QtWidgets, QtCore
  2. from FlatCAMTool import FlatCAMTool
  3. from flatcamGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox
  4. from shapely.geometry import Point
  5. import logging
  6. import gettext
  7. import FlatCAMTranslation as fcTranslate
  8. import builtins
  9. fcTranslate.apply_language('strings')
  10. if '_' not in builtins.__dict__:
  11. _ = gettext.gettext
  12. log = logging.getLogger('base')
  13. class ToolExtractDrills(FlatCAMTool):
  14. toolName = _("Extract Drills")
  15. def __init__(self, app):
  16. FlatCAMTool.__init__(self, app)
  17. self.decimals = self.app.decimals
  18. # ## Title
  19. title_label = QtWidgets.QLabel("%s" % self.toolName)
  20. title_label.setStyleSheet("""
  21. QLabel
  22. {
  23. font-size: 16px;
  24. font-weight: bold;
  25. }
  26. """)
  27. self.layout.addWidget(title_label)
  28. self.empty_lb = QtWidgets.QLabel("")
  29. self.layout.addWidget(self.empty_lb)
  30. # ## Grid Layout
  31. grid_lay = QtWidgets.QGridLayout()
  32. self.layout.addLayout(grid_lay)
  33. grid_lay.setColumnStretch(0, 1)
  34. grid_lay.setColumnStretch(1, 0)
  35. # ## Gerber Object
  36. self.gerber_object_combo = QtWidgets.QComboBox()
  37. self.gerber_object_combo.setModel(self.app.collection)
  38. self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  39. self.gerber_object_combo.setCurrentIndex(1)
  40. self.grb_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
  41. self.grb_label.setToolTip('%s.' % _("Gerber from which to extract drill holes"))
  42. # grid_lay.addRow("Bottom Layer:", self.object_combo)
  43. grid_lay.addWidget(self.grb_label, 0, 0, 1, 2)
  44. grid_lay.addWidget(self.gerber_object_combo, 1, 0, 1, 2)
  45. self.padt_label = QtWidgets.QLabel("<b>%s</b>" % _("Processed Pads Type"))
  46. self.padt_label.setToolTip(
  47. _("The type of pads shape to be processed.\n"
  48. "If the PCB has many SMD pads with rectangular pads,\n"
  49. "disable the Rectangular aperture.")
  50. )
  51. grid_lay.addWidget(self.padt_label, 2, 0, 1, 2)
  52. # Circular Aperture Selection
  53. self.circular_cb = FCCheckBox('%s' % _("Circular"))
  54. self.circular_cb.setToolTip(
  55. _("Create drills from circular pads.")
  56. )
  57. grid_lay.addWidget(self.circular_cb, 3, 0, 1, 2)
  58. # Oblong Aperture Selection
  59. self.oblong_cb = FCCheckBox('%s' % _("Oblong"))
  60. self.oblong_cb.setToolTip(
  61. _("Create drills from oblong pads.")
  62. )
  63. grid_lay.addWidget(self.oblong_cb, 4, 0, 1, 2)
  64. # Square Aperture Selection
  65. self.square_cb = FCCheckBox('%s' % _("Square"))
  66. self.square_cb.setToolTip(
  67. _("Create drills from square pads.")
  68. )
  69. grid_lay.addWidget(self.square_cb, 5, 0, 1, 2)
  70. # Rectangular Aperture Selection
  71. self.rectangular_cb = FCCheckBox('%s' % _("Rectangular"))
  72. self.rectangular_cb.setToolTip(
  73. _("Create drills from rectangular pads.")
  74. )
  75. grid_lay.addWidget(self.rectangular_cb, 6, 0, 1, 2)
  76. # Others type of Apertures Selection
  77. self.other_cb = FCCheckBox('%s' % _("Others"))
  78. self.other_cb.setToolTip(
  79. _("Create drills from other types of pad shape.")
  80. )
  81. grid_lay.addWidget(self.other_cb, 7, 0, 1, 2)
  82. separator_line = QtWidgets.QFrame()
  83. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  84. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  85. grid_lay.addWidget(separator_line, 8, 0, 1, 2)
  86. # ## Grid Layout
  87. grid1 = QtWidgets.QGridLayout()
  88. self.layout.addLayout(grid1)
  89. grid1.setColumnStretch(0, 0)
  90. grid1.setColumnStretch(1, 1)
  91. # ## Axis
  92. self.hole_size_radio = RadioSet([{'label': _("Fixed"), 'value': 'fixed'},
  93. {'label': _("Proportional"), 'value': 'prop'}])
  94. self.hole_size_label = QtWidgets.QLabel('%s:' % _("Hole Size"))
  95. self.hole_size_label.setToolTip(
  96. _("The type of hole size. Can be:\n"
  97. "- Fixed -> all holes will have a set size\n"
  98. "- Proprotional -> each hole will havea a variable size\n"
  99. "such as to preserve a set annular ring"))
  100. grid1.addWidget(self.hole_size_label, 3, 0)
  101. grid1.addWidget(self.hole_size_radio, 3, 1)
  102. # grid_lay1.addWidget(QtWidgets.QLabel(''))
  103. separator_line = QtWidgets.QFrame()
  104. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  105. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  106. grid1.addWidget(separator_line, 5, 0, 1, 2)
  107. # Diameter value
  108. self.dia_entry = FCDoubleSpinner()
  109. self.dia_entry.set_precision(self.decimals)
  110. self.dia_entry.set_range(0.0000, 9999.9999)
  111. self.dia_label = QtWidgets.QLabel('%s:' % _("Diameter"))
  112. self.dia_label.setToolTip(
  113. _("Fixed hole diameter.")
  114. )
  115. grid1.addWidget(self.dia_label, 7, 0)
  116. grid1.addWidget(self.dia_entry, 7, 1)
  117. self.ring_frame = QtWidgets.QFrame()
  118. self.ring_frame.setContentsMargins(0, 0, 0, 0)
  119. self.layout.addWidget(self.ring_frame)
  120. self.ring_box = QtWidgets.QVBoxLayout()
  121. self.ring_box.setContentsMargins(0, 0, 0, 0)
  122. self.ring_frame.setLayout(self.ring_box)
  123. # ## Grid Layout
  124. grid2 = QtWidgets.QGridLayout()
  125. grid2.setColumnStretch(0, 0)
  126. grid2.setColumnStretch(1, 1)
  127. self.ring_box.addLayout(grid2)
  128. # Annular Ring value
  129. self.ring_label = QtWidgets.QLabel('<b>%s</b>' % _("Annular Ring"))
  130. self.ring_label.setToolTip(
  131. _("The size of annular ring.\n"
  132. "The copper sliver between the drill hole exterior\n"
  133. "and the margin of the copper pad.")
  134. )
  135. grid2.addWidget(self.ring_label, 0, 0, 1, 2)
  136. # Circular Annular Ring Value
  137. self.circular_ring_label = QtWidgets.QLabel('%s:' % _("Circular"))
  138. self.circular_ring_label.setToolTip(
  139. _("The size of annular ring for circular pads.")
  140. )
  141. self.circular_ring_entry = FCDoubleSpinner()
  142. self.circular_ring_entry.set_precision(self.decimals)
  143. self.circular_ring_entry.set_range(0.0000, 9999.9999)
  144. grid2.addWidget(self.circular_ring_label, 1, 0)
  145. grid2.addWidget(self.circular_ring_entry, 1, 1)
  146. # Oblong Annular Ring Value
  147. self.oblong_ring_label = QtWidgets.QLabel('%s:' % _("Oblong"))
  148. self.oblong_ring_label.setToolTip(
  149. _("The size of annular ring for oblong pads.")
  150. )
  151. self.oblong_ring_entry = FCDoubleSpinner()
  152. self.oblong_ring_entry.set_precision(self.decimals)
  153. self.oblong_ring_entry.set_range(0.0000, 9999.9999)
  154. grid2.addWidget(self.oblong_ring_label, 2, 0)
  155. grid2.addWidget(self.oblong_ring_entry, 2, 1)
  156. # Square Annular Ring Value
  157. self.square_ring_label = QtWidgets.QLabel('%s:' % _("Square"))
  158. self.square_ring_label.setToolTip(
  159. _("The size of annular ring for square pads.")
  160. )
  161. self.square_ring_entry = FCDoubleSpinner()
  162. self.square_ring_entry.set_precision(self.decimals)
  163. self.square_ring_entry.set_range(0.0000, 9999.9999)
  164. grid2.addWidget(self.square_ring_label, 3, 0)
  165. grid2.addWidget(self.square_ring_entry, 3, 1)
  166. # Rectangular Annular Ring Value
  167. self.rectangular_ring_label = QtWidgets.QLabel('%s:' % _("Rectangular"))
  168. self.rectangular_ring_label.setToolTip(
  169. _("The size of annular ring for rectangular pads.")
  170. )
  171. self.rectangular_ring_entry = FCDoubleSpinner()
  172. self.rectangular_ring_entry.set_precision(self.decimals)
  173. self.rectangular_ring_entry.set_range(0.0000, 9999.9999)
  174. grid2.addWidget(self.rectangular_ring_label, 4, 0)
  175. grid2.addWidget(self.rectangular_ring_entry, 4, 1)
  176. # Others Annular Ring Value
  177. self.other_ring_label = QtWidgets.QLabel('%s:' % _("Others"))
  178. self.other_ring_label.setToolTip(
  179. _("The size of annular ring for other pads.")
  180. )
  181. self.other_ring_entry = FCDoubleSpinner()
  182. self.other_ring_entry.set_precision(self.decimals)
  183. self.other_ring_entry.set_range(0.0000, 9999.9999)
  184. grid2.addWidget(self.other_ring_label, 5, 0)
  185. grid2.addWidget(self.other_ring_entry, 5, 1)
  186. # Extract drills from Gerber apertures flashes (pads)
  187. self.e_drills_button = QtWidgets.QPushButton(_("Extract Drills"))
  188. self.e_drills_button.setToolTip(
  189. _("Extract drills from a given Gerber file.")
  190. )
  191. self.e_drills_button.setStyleSheet("""
  192. QPushButton
  193. {
  194. font-weight: bold;
  195. }
  196. """)
  197. self.layout.addWidget(self.e_drills_button)
  198. self.layout.addStretch()
  199. # ## Reset Tool
  200. self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
  201. self.reset_button.setToolTip(
  202. _("Will reset the tool parameters.")
  203. )
  204. self.reset_button.setStyleSheet("""
  205. QPushButton
  206. {
  207. font-weight: bold;
  208. }
  209. """)
  210. self.layout.addWidget(self.reset_button)
  211. self.circular_ring_entry.setEnabled(False)
  212. self.oblong_ring_entry.setEnabled(False)
  213. self.square_ring_entry.setEnabled(False)
  214. self.rectangular_ring_entry.setEnabled(False)
  215. self.other_ring_entry.setEnabled(False)
  216. # ## Signals
  217. self.hole_size_radio.activated_custom.connect(self.on_hole_size_toggle)
  218. self.e_drills_button.clicked.connect(self.on_extract_drills_click)
  219. self.reset_button.clicked.connect(self.set_tool_ui)
  220. self.circular_cb.stateChanged.connect(
  221. lambda state:
  222. self.circular_ring_entry.setDisabled(False) if state else self.circular_ring_entry.setDisabled(True)
  223. )
  224. self.oblong_cb.stateChanged.connect(
  225. lambda state:
  226. self.oblong_ring_entry.setDisabled(False) if state else self.oblong_ring_entry.setDisabled(True)
  227. )
  228. self.square_cb.stateChanged.connect(
  229. lambda state:
  230. self.square_ring_entry.setDisabled(False) if state else self.square_ring_entry.setDisabled(True)
  231. )
  232. self.rectangular_cb.stateChanged.connect(
  233. lambda state:
  234. self.rectangular_ring_entry.setDisabled(False) if state else self.rectangular_ring_entry.setDisabled(True)
  235. )
  236. self.other_cb.stateChanged.connect(
  237. lambda state:
  238. self.other_ring_entry.setDisabled(False) if state else self.other_ring_entry.setDisabled(True)
  239. )
  240. def install(self, icon=None, separator=None, **kwargs):
  241. FlatCAMTool.install(self, icon, separator, shortcut='ALT+I', **kwargs)
  242. def run(self, toggle=True):
  243. self.app.report_usage("Extract Drills()")
  244. if toggle:
  245. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  246. if self.app.ui.splitter.sizes()[0] == 0:
  247. self.app.ui.splitter.setSizes([1, 1])
  248. else:
  249. try:
  250. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  251. # if tab is populated with the tool but it does not have the focus, focus on it
  252. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  253. # focus on Tool Tab
  254. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  255. else:
  256. self.app.ui.splitter.setSizes([0, 1])
  257. except AttributeError:
  258. pass
  259. else:
  260. if self.app.ui.splitter.sizes()[0] == 0:
  261. self.app.ui.splitter.setSizes([1, 1])
  262. FlatCAMTool.run(self)
  263. self.set_tool_ui()
  264. self.app.ui.notebook.setTabText(2, _("Extract Drills Tool"))
  265. def set_tool_ui(self):
  266. self.reset_fields()
  267. self.hole_size_radio.set_value(self.app.defaults["tools_edrills_hole_type"])
  268. self.dia_entry.set_value(float(self.app.defaults["tools_edrills_hole_fixed_dia"]))
  269. self.circular_ring_entry.set_value(float(self.app.defaults["tools_edrills_circular_ring"]))
  270. self.oblong_ring_entry.set_value(float(self.app.defaults["tools_edrills_oblong_ring"]))
  271. self.square_ring_entry.set_value(float(self.app.defaults["tools_edrills_square_ring"]))
  272. self.rectangular_ring_entry.set_value(float(self.app.defaults["tools_edrills_rectangular_ring"]))
  273. self.other_ring_entry.set_value(float(self.app.defaults["tools_edrills_others_ring"]))
  274. self.circular_cb.set_value(self.app.defaults["tools_edrills_circular"])
  275. self.oblong_cb.set_value(self.app.defaults["tools_edrills_oblong"])
  276. self.square_cb.set_value(self.app.defaults["tools_edrills_square"])
  277. self.rectangular_cb.set_value(self.app.defaults["tools_edrills_rectangular"])
  278. self.other_cb.set_value(self.app.defaults["tools_edrills_others"])
  279. def on_extract_drills_click(self):
  280. drill_dia = self.dia_entry.get_value()
  281. circ_r_val = self.circular_ring_entry.get_value()
  282. oblong_r_val = self.oblong_ring_entry.get_value()
  283. square_r_val = self.square_ring_entry.get_value()
  284. rect_r_val = self.rectangular_ring_entry.get_value()
  285. other_r_val = self.other_ring_entry.get_value()
  286. drills = list()
  287. tools = dict()
  288. selection_index = self.gerber_object_combo.currentIndex()
  289. model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
  290. try:
  291. fcobj = model_index.internalPointer().obj
  292. except Exception as e:
  293. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
  294. return
  295. outname = fcobj.options['name'].rpartition('.')[0]
  296. mode = self.hole_size_radio.get_value()
  297. if mode == 'fixed':
  298. tools = {"1": {"C": drill_dia}}
  299. for apid, apid_value in fcobj.apertures.items():
  300. ap_type = apid_value['type']
  301. if ap_type == 'C':
  302. if self.circular_cb.get_value() is False:
  303. continue
  304. elif ap_type == 'O':
  305. if self.oblong_cb.get_value() is False:
  306. continue
  307. elif ap_type == 'R':
  308. width = float(apid_value['width'])
  309. height = float(apid_value['height'])
  310. # if the height == width (float numbers so the reason for the following)
  311. if round(width, self.decimals) == round(height, self.decimals):
  312. if self.square_cb.get_value() is False:
  313. continue
  314. else:
  315. if self.rectangular_cb.get_value() is False:
  316. continue
  317. else:
  318. if self.other_cb.get_value() is False:
  319. continue
  320. for geo_el in apid_value['geometry']:
  321. if 'follow' in geo_el and isinstance(geo_el['follow'], Point):
  322. drills.append({"point": geo_el['follow'], "tool": "1"})
  323. if 'solid_geometry' not in tools["1"]:
  324. tools["1"]['solid_geometry'] = list()
  325. else:
  326. tools["1"]['solid_geometry'].append(geo_el['follow'])
  327. if 'solid_geometry' not in tools["1"] or not tools["1"]['solid_geometry']:
  328. self.app.inform.emit('[WARNING_NOTCL] %s' % _("No drills extracted. Try different parameters."))
  329. return
  330. else:
  331. drills_found = set()
  332. for apid, apid_value in fcobj.apertures.items():
  333. ap_type = apid_value['type']
  334. dia = None
  335. if ap_type == 'C':
  336. if self.circular_cb.get_value():
  337. dia = float(apid_value['size']) - (2 * circ_r_val)
  338. elif ap_type == 'O':
  339. width = float(apid_value['width'])
  340. height = float(apid_value['height'])
  341. if self.oblong_cb.get_value():
  342. if width > height:
  343. dia = float(apid_value['height']) - (2 * rect_r_val)
  344. else:
  345. dia = float(apid_value['width']) - (2 * rect_r_val)
  346. elif ap_type == 'R':
  347. width = float(apid_value['width'])
  348. height = float(apid_value['height'])
  349. # if the height == width (float numbers so the reason for the following)
  350. if abs(float('%.*f' % (self.decimals, width)) - float('%.*f' % (self.decimals, height))) < \
  351. (10 ** -self.decimals):
  352. if self.square_cb.get_value():
  353. dia = float(apid_value['height']) - (2 * square_r_val)
  354. else:
  355. if self.rectangular_cb.get_value():
  356. if width > height:
  357. dia = float(apid_value['height']) - (2 * rect_r_val)
  358. else:
  359. dia = float(apid_value['width']) - (2 * rect_r_val)
  360. else:
  361. if self.other_cb.get_value():
  362. try:
  363. dia = float(apid_value['size']) - (2 * other_r_val)
  364. except KeyError:
  365. if ap_type == 'AM':
  366. pol = apid_value['geometry'][0]['solid']
  367. x0, y0, x1, y1 = pol.bounds
  368. dx = x1 - x0
  369. dy = y1 - y0
  370. if dx <= dy:
  371. dia = dx - (2 * other_r_val)
  372. else:
  373. dia = dy - (2 * other_r_val)
  374. # if dia is None then none of the above applied so we skip the following
  375. if dia is None:
  376. continue
  377. tool_in_drills = False
  378. for tool, tool_val in tools.items():
  379. if abs(float('%.*f' % (self.decimals, tool_val["C"])) - float('%.*f' % (self.decimals, dia))) < \
  380. (10 ** -self.decimals):
  381. tool_in_drills = tool
  382. if tool_in_drills is False:
  383. if tools:
  384. new_tool = max([int(t) for t in tools]) + 1
  385. tool_in_drills = str(new_tool)
  386. else:
  387. tool_in_drills = "1"
  388. for geo_el in apid_value['geometry']:
  389. if 'follow' in geo_el and isinstance(geo_el['follow'], Point):
  390. if tool_in_drills not in tools:
  391. tools[tool_in_drills] = {"C": dia}
  392. drills.append({"point": geo_el['follow'], "tool": tool_in_drills})
  393. if 'solid_geometry' not in tools[tool_in_drills]:
  394. tools[tool_in_drills]['solid_geometry'] = list()
  395. else:
  396. tools[tool_in_drills]['solid_geometry'].append(geo_el['follow'])
  397. if tool_in_drills in tools:
  398. if 'solid_geometry' not in tools[tool_in_drills] or not tools[tool_in_drills]['solid_geometry']:
  399. drills_found.add(False)
  400. else:
  401. drills_found.add(True)
  402. if True not in drills_found:
  403. self.app.inform.emit('[WARNING_NOTCL] %s' % _("No drills extracted. Try different parameters."))
  404. return
  405. def obj_init(obj_inst, app_inst):
  406. obj_inst.tools = tools
  407. obj_inst.drills = drills
  408. obj_inst.create_geometry()
  409. obj_inst.source_file = self.app.export_excellon(obj_name=outname, local_use=obj_inst, filename=None,
  410. use_thread=False)
  411. self.app.new_object("excellon", outname, obj_init)
  412. def on_hole_size_toggle(self, val):
  413. if val == "fixed":
  414. self.dia_entry.setDisabled(False)
  415. self.dia_label.setDisabled(False)
  416. self.ring_frame.setDisabled(True)
  417. else:
  418. self.dia_entry.setDisabled(True)
  419. self.dia_label.setDisabled(True)
  420. self.ring_frame.setDisabled(False)
  421. def reset_fields(self):
  422. self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  423. self.gerber_object_combo.setCurrentIndex(0)