ToolPcbWizard.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. ############################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://flatcam.org #
  4. # File Author: Marius Adrian Stanciu (c) #
  5. # Date: 4/15/2019 #
  6. # MIT Licence #
  7. ############################################################
  8. from FlatCAMTool import FlatCAMTool
  9. from flatcamGUI.GUIElements import RadioSet, FCComboBox, FCSpinner, FCButton, FCTable
  10. from PyQt5 import QtGui, QtWidgets, QtCore
  11. from PyQt5.QtCore import pyqtSignal
  12. import re
  13. import os
  14. import gettext
  15. import FlatCAMTranslation as fcTranslate
  16. fcTranslate.apply_language('strings')
  17. import builtins
  18. if '_' not in builtins.__dict__:
  19. _ = gettext.gettext
  20. class PcbWizard(FlatCAMTool):
  21. file_loaded = pyqtSignal(str, str)
  22. toolName = _("PcbWizard Import Tool")
  23. def __init__(self, app):
  24. FlatCAMTool.__init__(self, app)
  25. self.app = app
  26. # Title
  27. title_label = QtWidgets.QLabel("%s" % _('Import 2-file Excellon'))
  28. title_label.setStyleSheet("""
  29. QLabel
  30. {
  31. font-size: 16px;
  32. font-weight: bold;
  33. }
  34. """)
  35. self.layout.addWidget(title_label)
  36. self.layout.addWidget(QtWidgets.QLabel(""))
  37. self.layout.addWidget(QtWidgets.QLabel("<b>Load files:</b>"))
  38. # Form Layout
  39. form_layout = QtWidgets.QFormLayout()
  40. self.layout.addLayout(form_layout)
  41. self.excellon_label = QtWidgets.QLabel(_("Excellon file:"))
  42. self.excellon_label.setToolTip(
  43. _( "Load the Excellon file.\n"
  44. "Usually it has a .DRL extension")
  45. )
  46. self.excellon_brn = FCButton(_("Open"))
  47. form_layout.addRow(self.excellon_label, self.excellon_brn)
  48. self.inf_label = QtWidgets.QLabel(_("INF file:"))
  49. self.inf_label.setToolTip(
  50. _("Load the INF file.")
  51. )
  52. self.inf_btn = FCButton(_("Open"))
  53. form_layout.addRow(self.inf_label, self.inf_btn)
  54. self.tools_table = FCTable()
  55. self.layout.addWidget(self.tools_table)
  56. self.tools_table.setColumnCount(2)
  57. self.tools_table.setHorizontalHeaderLabels(['#Tool', _('Diameter')])
  58. self.tools_table.horizontalHeaderItem(0).setToolTip(
  59. _("Tool Number"))
  60. self.tools_table.horizontalHeaderItem(1).setToolTip(
  61. _("Tool diameter in file units."))
  62. # start with apertures table hidden
  63. self.tools_table.setVisible(False)
  64. self.layout.addWidget(QtWidgets.QLabel(""))
  65. self.layout.addWidget(QtWidgets.QLabel("<b>Excellon format:</b>"))
  66. # Form Layout
  67. form_layout1 = QtWidgets.QFormLayout()
  68. self.layout.addLayout(form_layout1)
  69. # Integral part of the coordinates
  70. self.int_entry = FCSpinner()
  71. self.int_entry.set_range(1, 10)
  72. self.int_label = QtWidgets.QLabel(_("Int. digits:"))
  73. self.int_label.setToolTip(
  74. _( "The number of digits for the integral part of the coordinates.")
  75. )
  76. form_layout1.addRow(self.int_label, self.int_entry)
  77. # Fractional part of the coordinates
  78. self.frac_entry = FCSpinner()
  79. self.frac_entry.set_range(1, 10)
  80. self.frac_label = QtWidgets.QLabel(_("Frac. digits:"))
  81. self.frac_label.setToolTip(
  82. _("The number of digits for the fractional part of the coordinates.")
  83. )
  84. form_layout1.addRow(self.frac_label, self.frac_entry)
  85. # Zeros suppression for coordinates
  86. self.zeros_radio = RadioSet([{'label': 'LZ', 'value': 'L'},
  87. {'label': 'TZ', 'value': 'T'},
  88. {'label': 'No Suppression', 'value': 'D'}])
  89. self.zeros_label = QtWidgets.QLabel(_("Zeros supp.:"))
  90. self.zeros_label.setToolTip(
  91. _("The type of zeros suppression used.\n"
  92. "Can be of type:\n"
  93. "- LZ = leading zeros are kept\n"
  94. "- TZ = trailing zeros are kept\n"
  95. "- No Suppression = no zero suppression")
  96. )
  97. form_layout1.addRow(self.zeros_label, self.zeros_radio)
  98. # Units type
  99. self.units_radio = RadioSet([{'label': 'INCH', 'value': 'INCH'},
  100. {'label': 'MM', 'value': 'METRIC'}])
  101. self.units_label = QtWidgets.QLabel("<b>%s:</b>" % _('Units'))
  102. self.units_label.setToolTip(
  103. _("The type of units that the coordinates and tool\n"
  104. "diameters are using. Can be INCH or MM.")
  105. )
  106. form_layout1.addRow(self.units_label, self.units_radio)
  107. # Buttons
  108. self.import_button = QtWidgets.QPushButton(_("Import Excellon"))
  109. self.import_button.setToolTip(
  110. _("Import in FlatCAM an Excellon file\n"
  111. "that store it's information's in 2 files.\n"
  112. "One usually has .DRL extension while\n"
  113. "the other has .INF extension.")
  114. )
  115. self.layout.addWidget(self.import_button)
  116. self.layout.addStretch()
  117. self.excellon_loaded = False
  118. self.inf_loaded = False
  119. self.process_finished = False
  120. ## Signals
  121. self.excellon_brn.clicked.connect(self.on_load_excellon_click)
  122. self.inf_btn.clicked.connect(self.on_load_inf_click)
  123. self.import_button.clicked.connect(self.on_import_excellon)
  124. self.file_loaded.connect(self.on_file_loaded)
  125. self.units_radio.activated_custom.connect(self.on_units_change)
  126. self.units = 'INCH'
  127. self.zeros = 'L'
  128. self.integral = 2
  129. self.fractional = 4
  130. self.outname = 'file'
  131. self.exc_file_content = None
  132. self.tools_from_inf = {}
  133. def run(self, toggle=False):
  134. self.app.report_usage("PcbWizard Tool()")
  135. if toggle:
  136. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  137. if self.app.ui.splitter.sizes()[0] == 0:
  138. self.app.ui.splitter.setSizes([1, 1])
  139. else:
  140. try:
  141. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  142. self.app.ui.splitter.setSizes([0, 1])
  143. except AttributeError:
  144. pass
  145. else:
  146. if self.app.ui.splitter.sizes()[0] == 0:
  147. self.app.ui.splitter.setSizes([1, 1])
  148. FlatCAMTool.run(self)
  149. self.set_tool_ui()
  150. self.app.ui.notebook.setTabText(2, _("PCBWizard Tool"))
  151. def install(self, icon=None, separator=None, **kwargs):
  152. FlatCAMTool.install(self, icon, separator, **kwargs)
  153. def set_tool_ui(self):
  154. ## Initialize form
  155. self.int_entry.set_value(self.integral)
  156. self.frac_entry.set_value(self.fractional)
  157. self.zeros_radio.set_value(self.zeros)
  158. self.units_radio.set_value(self.units)
  159. self.excellon_loaded = False
  160. self.inf_loaded = False
  161. self.process_finished = False
  162. self.build_ui()
  163. def build_ui(self):
  164. sorted_tools = []
  165. if not self.tools_from_inf:
  166. self.tools_table.setRowCount(1)
  167. else:
  168. sort = []
  169. for k, v in list(self.tools_from_inf.items()):
  170. sort.append(int(k))
  171. sorted_tools = sorted(sort)
  172. n = len(sorted_tools)
  173. self.tools_table.setRowCount(n)
  174. tool_row = 0
  175. for tool in sorted_tools:
  176. tool_id_item = QtWidgets.QTableWidgetItem('%d' % int(tool))
  177. tool_id_item.setFlags(QtCore.Qt.ItemIsEnabled)
  178. self.tools_table.setItem(tool_row, 0, tool_id_item) # Tool name/id
  179. tool_dia_item = QtWidgets.QTableWidgetItem(str(self.tools_from_inf[tool]))
  180. tool_dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
  181. self.tools_table.setItem(tool_row, 1, tool_dia_item)
  182. tool_row += 1
  183. self.tools_table.resizeColumnsToContents()
  184. self.tools_table.resizeRowsToContents()
  185. vertical_header = self.tools_table.verticalHeader()
  186. vertical_header.hide()
  187. self.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  188. horizontal_header = self.tools_table.horizontalHeader()
  189. # horizontal_header.setMinimumSectionSize(10)
  190. # horizontal_header.setDefaultSectionSize(70)
  191. horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
  192. horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
  193. self.tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  194. self.tools_table.setSortingEnabled(False)
  195. self.tools_table.setMinimumHeight(self.tools_table.getHeight())
  196. self.tools_table.setMaximumHeight(self.tools_table.getHeight())
  197. def update_params(self):
  198. self.units = self.units_radio.get_value()
  199. self.zeros = self.zeros_radio.get_value()
  200. self.integral = self.int_entry.get_value()
  201. self.fractional = self.frac_entry.get_value()
  202. def on_units_change(self, val):
  203. if val == 'INCH':
  204. self.int_entry.set_value(2)
  205. self.frac_entry.set_value(4)
  206. else:
  207. self.int_entry.set_value(3)
  208. self.frac_entry.set_value(3)
  209. def on_load_excellon_click(self):
  210. """
  211. :return: None
  212. """
  213. self.app.log.debug("on_load_excellon_click()")
  214. filter = "Excellon Files(*.DRL *.DRD *.TXT);;All Files (*.*)"
  215. try:
  216. filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Load PcbWizard Excellon file"),
  217. directory=self.app.get_last_folder(),
  218. filter=filter)
  219. except TypeError:
  220. filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Load PcbWizard Excellon file"),
  221. filter=filter)
  222. filename = str(filename)
  223. if filename == "":
  224. self.app.inform.emit(_("Open cancelled."))
  225. else:
  226. self.app.worker_task.emit({'fcn': self.load_excellon,
  227. 'params': [self, filename]})
  228. def on_load_inf_click(self):
  229. """
  230. :return: None
  231. """
  232. self.app.log.debug("on_load_inf_click()")
  233. filter = "INF Files(*.INF);;All Files (*.*)"
  234. try:
  235. filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Load PcbWizard INF file"),
  236. directory=self.app.get_last_folder(),
  237. filter=filter)
  238. except TypeError:
  239. filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Load PcbWizard INF file"),
  240. filter=filter)
  241. filename = str(filename)
  242. if filename == "":
  243. self.app.inform.emit(_("Open cancelled."))
  244. else:
  245. self.app.worker_task.emit({'fcn': self.load_inf, 'params': [filename]})
  246. def load_inf(self, filename):
  247. self.app.log.debug("ToolPcbWizard.load_inf()")
  248. with open(filename, 'r') as inf_f:
  249. inf_file_content = inf_f.readlines()
  250. tool_re = re.compile(r'^T(\d+)\s+(\d*\.?\d+)$')
  251. for eline in inf_file_content:
  252. # Cleanup lines
  253. eline = eline.strip(' \r\n')
  254. match = tool_re.search(eline)
  255. if match:
  256. tool =int( match.group(1))
  257. dia = float(match.group(2))
  258. if dia < 0.1:
  259. # most likely the file is in INCH
  260. self.units_radio.set_value('INCH')
  261. self.tools_from_inf[tool] = dia
  262. if not self.tools_from_inf:
  263. self.app.inform.emit(_("[ERROR] The INF file does not contain the tool table.\n"
  264. "Try to open the Excellon file from File -> Open -> Excellon\n"
  265. "and edit the drill diameters manually."))
  266. return "fail"
  267. self.tools_table.setVisible(True)
  268. self.file_loaded.emit('inf', filename)
  269. def load_excellon(self, filename):
  270. with open(filename, 'r') as exc_f:
  271. self.exc_file_content = exc_f.readlines()
  272. self.file_loaded.emit("excellon", filename)
  273. def on_file_loaded(self, signal, filename):
  274. self.build_ui()
  275. if signal == 'inf':
  276. self.inf_loaded = True
  277. elif signal == 'excellon':
  278. self.excellon_loaded = True
  279. if self.excellon_loaded and self.inf_loaded:
  280. pass
  281. # Register recent file
  282. self.app.defaults["global_last_folder"] = os.path.split(str(filename))[0]
  283. def on_import_excellon(self, signal, excellon_fileobj):
  284. self.app.log.debug("import_2files_excellon()")
  285. # How the object should be initialized
  286. def obj_init(excellon_obj, app_obj):
  287. # self.progress.emit(20)
  288. try:
  289. ret = excellon_obj.parse_file(file_obj=excellon_fileobj)
  290. if ret == "fail":
  291. app_obj.log.debug("Excellon parsing failed.")
  292. app_obj.inform.emit(_("[ERROR_NOTCL] This is not Excellon file."))
  293. return "fail"
  294. except IOError:
  295. app_obj.inform.emit(_("[ERROR_NOTCL] Cannot parse file: %s") % self.outname)
  296. app_obj.log.debug("Could not import Excellon object.")
  297. app_obj.progress.emit(0)
  298. return "fail"
  299. except:
  300. msg = _("[ERROR_NOTCL] An internal error has occurred. See shell.\n")
  301. msg += app_obj.traceback.format_exc()
  302. app_obj.inform.emit(msg)
  303. return "fail"
  304. ret = excellon_obj.create_geometry()
  305. if ret == 'fail':
  306. app_obj.log.debug("Could not create geometry for Excellon object.")
  307. return "fail"
  308. app_obj.progress.emit(100)
  309. for tool in excellon_obj.tools:
  310. if excellon_obj.tools[tool]['solid_geometry']:
  311. return
  312. app_obj.inform.emit(_("[ERROR_NOTCL] No geometry found in file: %s") % name)
  313. return "fail"
  314. if self.process_finished:
  315. with self.app.proc_container.new(_("Importing Excellon.")):
  316. # Object name
  317. name = self.outname
  318. ret = self.app.new_object("excellon", name, obj_init, autoselected=False)
  319. if ret == 'fail':
  320. self.app.inform.emit(_('[ERROR_NOTCL] Import Excellon file failed.'))
  321. return
  322. # Register recent file
  323. self.app.file_opened.emit("excellon", name)
  324. # GUI feedback
  325. self.app.inform.emit(_("[success] Opened: %s") % name)
  326. else:
  327. self.app.inform.emit(_('[WARNING_NOTCL] Excellon merging is in progress. Please wait...'))