ToolPcbWizard.py 20 KB

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