ToolOptimal.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # File Author: Marius Adrian Stanciu (c) #
  4. # Date: 09/27/2019 #
  5. # MIT Licence #
  6. # ##########################################################
  7. from PyQt5 import QtWidgets, QtCore, QtGui
  8. from FlatCAMTool import FlatCAMTool
  9. from flatcamGUI.GUIElements import OptionalHideInputSection, FCTextArea, FCEntry, FCSpinner, FCCheckBox
  10. from FlatCAMObj import FlatCAMGerber
  11. import FlatCAMApp
  12. from shapely.geometry import MultiPolygon
  13. from shapely.ops import nearest_points
  14. import numpy as np
  15. import logging
  16. import gettext
  17. import FlatCAMTranslation as fcTranslate
  18. import builtins
  19. fcTranslate.apply_language('strings')
  20. if '_' not in builtins.__dict__:
  21. _ = gettext.gettext
  22. log = logging.getLogger('base')
  23. class ToolOptimal(FlatCAMTool):
  24. toolName = _("Optimal Tool")
  25. update_text = QtCore.pyqtSignal(list)
  26. update_sec_distances = QtCore.pyqtSignal(dict)
  27. def __init__(self, app):
  28. FlatCAMTool.__init__(self, app)
  29. self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
  30. self.decimals = 4
  31. # ############################################################################
  32. # ############################ GUI creation ##################################
  33. # ## Title
  34. title_label = QtWidgets.QLabel("%s" % self.toolName)
  35. title_label.setStyleSheet(
  36. """
  37. QLabel
  38. {
  39. font-size: 16px;
  40. font-weight: bold;
  41. }
  42. """)
  43. self.layout.addWidget(title_label)
  44. # ## Form Layout
  45. form_lay = QtWidgets.QFormLayout()
  46. self.layout.addLayout(form_lay)
  47. form_lay.addRow(QtWidgets.QLabel(""))
  48. # ## Gerber Object to mirror
  49. self.gerber_object_combo = QtWidgets.QComboBox()
  50. self.gerber_object_combo.setModel(self.app.collection)
  51. self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  52. self.gerber_object_combo.setCurrentIndex(1)
  53. self.gerber_object_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
  54. self.gerber_object_label.setToolTip(
  55. "Gerber object for which to find the minimum distance between copper features."
  56. )
  57. form_lay.addRow(self.gerber_object_label, self.gerber_object_combo)
  58. # Precision = nr of decimals
  59. self.precision_label = QtWidgets.QLabel('%s:' % _("Precision"))
  60. self.precision_label.setToolTip(_("Number of decimals kept for found distances."))
  61. self.precision_spinner = FCSpinner()
  62. self.precision_spinner.set_range(2, 10)
  63. self.precision_spinner.setWrapping(True)
  64. form_lay.addRow(self.precision_label, self.precision_spinner)
  65. # Results Title
  66. self.title_res_label = QtWidgets.QLabel('<b>%s:</b>' % _("Minimum distance"))
  67. self.title_res_label.setToolTip(_("Display minimum distance between copper features."))
  68. form_lay.addRow(self.title_res_label)
  69. # Result value
  70. self.result_label = QtWidgets.QLabel('%s:' % _("Determined"))
  71. self.result_entry = FCEntry()
  72. self.result_entry.setReadOnly(True)
  73. self.units_lbl = QtWidgets.QLabel(self.units.lower())
  74. self.units_lbl.setDisabled(True)
  75. hlay = QtWidgets.QHBoxLayout()
  76. hlay.addWidget(self.result_entry)
  77. hlay.addWidget(self.units_lbl)
  78. form_lay.addRow(self.result_label, hlay)
  79. # Frequency of minimum encounter
  80. self.freq_label = QtWidgets.QLabel('%s:' % _("Occurring"))
  81. self.freq_label.setToolTip(_("How many times this minimum is found."))
  82. self.freq_entry = FCEntry()
  83. self.freq_entry.setReadOnly(True)
  84. form_lay.addRow(self.freq_label, self.freq_entry)
  85. # Control if to display the locations of where the minimum was found
  86. self.locations_cb = FCCheckBox(_("Minimum points coordinates"))
  87. self.locations_cb.setToolTip(_("Coordinates for points where minimum distance was found."))
  88. form_lay.addRow(self.locations_cb)
  89. # Locations where minimum was found
  90. self.locations_textb = FCTextArea(parent=self)
  91. self.locations_textb.setReadOnly(True)
  92. stylesheet = """
  93. QTextEdit { selection-background-color:blue;
  94. selection-color:white;
  95. }
  96. """
  97. self.locations_textb.setStyleSheet(stylesheet)
  98. form_lay.addRow(self.locations_textb)
  99. # Jump button
  100. self.locate_button = QtWidgets.QPushButton(_("Jump to selected position"))
  101. self.locate_button.setToolTip(
  102. _("Select a position in the Locations text box and then\n"
  103. "click this button.")
  104. )
  105. self.locate_button.setMinimumWidth(60)
  106. self.locate_button.setDisabled(True)
  107. form_lay.addRow(self.locate_button)
  108. # Other distances in Gerber
  109. self.title_second_res_label = QtWidgets.QLabel('<b>%s:</b>' % _("Other distances"))
  110. self.title_second_res_label.setToolTip(_("Will display other distances in the Gerber file ordered from\n"
  111. "the minimum to the maximum, not including the absolute minimum."))
  112. form_lay.addRow(self.title_second_res_label)
  113. # Control if to display the locations of where the minimum was found
  114. self.sec_locations_cb = FCCheckBox(_("Other distances points coordinates"))
  115. self.sec_locations_cb.setToolTip(_("Other distances and the coordinates for points\n"
  116. "where the distance was found."))
  117. form_lay.addRow(self.sec_locations_cb)
  118. # this way I can hide/show the frame
  119. self.sec_locations_frame = QtWidgets.QFrame()
  120. self.sec_locations_frame.setContentsMargins(0, 0, 0, 0)
  121. self.layout.addWidget(self.sec_locations_frame)
  122. self.distances_box = QtWidgets.QVBoxLayout()
  123. self.distances_box.setContentsMargins(0, 0, 0, 0)
  124. self.sec_locations_frame.setLayout(self.distances_box)
  125. # Other Distances label
  126. self.distances_label = QtWidgets.QLabel('%s' % _("Gerber distances"))
  127. self.distances_label.setToolTip(_("Other distances and the coordinates for points\n"
  128. "where the distance was found."))
  129. self.distances_box.addWidget(self.distances_label)
  130. # Other distances
  131. self.distances_textb = FCTextArea(parent=self)
  132. self.distances_textb.setReadOnly(True)
  133. stylesheet = """
  134. QTextEdit { selection-background-color:blue;
  135. selection-color:white;
  136. }
  137. """
  138. self.distances_textb.setStyleSheet(stylesheet)
  139. self.distances_box.addWidget(self.distances_textb)
  140. self.distances_box.addWidget(QtWidgets.QLabel(''))
  141. # Other Locations label
  142. self.locations_label = QtWidgets.QLabel('%s' % _("Points coordinates"))
  143. self.locations_label.setToolTip(_("Other distances and the coordinates for points\n"
  144. "where the distance was found."))
  145. self.distances_box.addWidget(self.locations_label)
  146. # Locations where minimum was found
  147. self.locations_sec_textb = FCTextArea(parent=self)
  148. self.locations_sec_textb.setReadOnly(True)
  149. stylesheet = """
  150. QTextEdit { selection-background-color:blue;
  151. selection-color:white;
  152. }
  153. """
  154. self.locations_sec_textb.setStyleSheet(stylesheet)
  155. self.distances_box.addWidget(self.locations_sec_textb)
  156. # Jump button
  157. self.locate_sec_button = QtWidgets.QPushButton(_("Jump to selected position"))
  158. self.locate_sec_button.setToolTip(
  159. _("Select a position in the Locations text box and then\n"
  160. "click this button.")
  161. )
  162. self.locate_sec_button.setMinimumWidth(60)
  163. self.locate_sec_button.setDisabled(True)
  164. self.distances_box.addWidget(self.locate_sec_button)
  165. # GO button
  166. self.calculate_button = QtWidgets.QPushButton(_("Find Minimum"))
  167. self.calculate_button.setToolTip(
  168. _("Calculate the minimum distance between copper features,\n"
  169. "this will allow the determination of the right tool to\n"
  170. "use for isolation or copper clearing.")
  171. )
  172. self.calculate_button.setMinimumWidth(60)
  173. self.layout.addWidget(self.calculate_button)
  174. self.loc_ois = OptionalHideInputSection(self.locations_cb, [self.locations_textb, self.locate_button])
  175. self.sec_loc_ois = OptionalHideInputSection(self.sec_locations_cb, [self.sec_locations_frame])
  176. # ################## Finished GUI creation ###################################
  177. # ############################################################################
  178. # this is the line selected in the textbox with the locations of the minimum
  179. self.selected_text = ''
  180. # this is the line selected in the textbox with the locations of the other distances found in the Gerber object
  181. self.selected_locations_text = ''
  182. # dict to hold the distances between every two elements in Gerber as keys and the actual locations where that
  183. # distances happen as values
  184. self.min_dict = dict()
  185. # ############################################################################
  186. # ############################ Signals #######################################
  187. # ############################################################################
  188. self.calculate_button.clicked.connect(self.find_minimum_distance)
  189. self.locate_button.clicked.connect(self.on_locate_position)
  190. self.update_text.connect(self.on_update_text)
  191. self.locations_textb.cursorPositionChanged.connect(self.on_textbox_clicked)
  192. self.locate_sec_button.clicked.connect(self.on_locate_sec_position)
  193. self.update_sec_distances.connect(self.on_update_sec_distances_txt)
  194. self.distances_textb.cursorPositionChanged.connect(self.on_distances_textb_clicked)
  195. self.locations_sec_textb.cursorPositionChanged.connect(self.on_locations_sec_clicked)
  196. self.layout.addStretch()
  197. def install(self, icon=None, separator=None, **kwargs):
  198. FlatCAMTool.install(self, icon, separator, shortcut='ALT+O', **kwargs)
  199. def run(self, toggle=True):
  200. self.app.report_usage("ToolOptimal()")
  201. self.result_entry.set_value(0.0)
  202. self.freq_entry.set_value('0')
  203. if toggle:
  204. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  205. if self.app.ui.splitter.sizes()[0] == 0:
  206. self.app.ui.splitter.setSizes([1, 1])
  207. else:
  208. try:
  209. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  210. # if tab is populated with the tool but it does not have the focus, focus on it
  211. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  212. # focus on Tool Tab
  213. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  214. else:
  215. self.app.ui.splitter.setSizes([0, 1])
  216. except AttributeError:
  217. pass
  218. else:
  219. if self.app.ui.splitter.sizes()[0] == 0:
  220. self.app.ui.splitter.setSizes([1, 1])
  221. FlatCAMTool.run(self)
  222. self.set_tool_ui()
  223. self.app.ui.notebook.setTabText(2, _("Optimal Tool"))
  224. def set_tool_ui(self):
  225. self.precision_spinner.set_value(int(self.app.defaults["tools_opt_precision"]))
  226. self.locations_textb.clear()
  227. # new cursor - select all document
  228. cursor = self.locations_textb.textCursor()
  229. cursor.select(QtGui.QTextCursor.Document)
  230. # clear previous selection highlight
  231. tmp = cursor.blockFormat()
  232. tmp.clearBackground()
  233. cursor.setBlockFormat(tmp)
  234. self.locations_textb.setVisible(False)
  235. self.locate_button.setVisible(False)
  236. self.result_entry.set_value(0.0)
  237. self.freq_entry.set_value('0')
  238. self.reset_fields()
  239. def find_minimum_distance(self):
  240. self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
  241. self.decimals = int(self.precision_spinner.get_value())
  242. selection_index = self.gerber_object_combo.currentIndex()
  243. model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
  244. try:
  245. fcobj = model_index.internalPointer().obj
  246. except Exception as e:
  247. log.debug("ToolOptimal.find_minimum_distance() --> %s" % str(e))
  248. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
  249. return
  250. if not isinstance(fcobj, FlatCAMGerber):
  251. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Gerber objects can be evaluated."))
  252. return
  253. proc = self.app.proc_container.new(_("Working..."))
  254. def job_thread(app_obj):
  255. app_obj.inform.emit(_("Optimal Tool. Started to search for the minimum distance between copper features."))
  256. try:
  257. old_disp_number = 0
  258. pol_nr = 0
  259. app_obj.proc_container.update_view_text(' %d%%' % 0)
  260. total_geo = list()
  261. for ap in list(fcobj.apertures.keys()):
  262. if 'geometry' in fcobj.apertures[ap]:
  263. app_obj.inform.emit(
  264. '%s: %s' % (_("Optimal Tool. Parsing geometry for aperture"), str(ap)))
  265. for geo_el in fcobj.apertures[ap]['geometry']:
  266. if self.app.abort_flag:
  267. # graceful abort requested by the user
  268. raise FlatCAMApp.GracefulException
  269. if 'solid' in geo_el and geo_el['solid'] is not None and geo_el['solid'].is_valid:
  270. total_geo.append(geo_el['solid'])
  271. app_obj.inform.emit(
  272. _("Optimal Tool. Creating a buffer for the object geometry."))
  273. total_geo = MultiPolygon(total_geo)
  274. total_geo = total_geo.buffer(0)
  275. try:
  276. __ = iter(total_geo)
  277. geo_len = len(total_geo)
  278. geo_len = (geo_len * (geo_len - 1)) / 2
  279. except TypeError:
  280. app_obj.inform.emit('[ERROR_NOTCL] %s' %
  281. _("The Gerber object has one Polygon as geometry.\n"
  282. "There are no distances between geometry elements to be found."))
  283. return 'fail'
  284. app_obj.inform.emit(
  285. '%s: %s' % (_("Optimal Tool. Finding the distances between each two elements. Iterations"),
  286. str(geo_len)))
  287. self.min_dict = dict()
  288. idx = 1
  289. for geo in total_geo:
  290. for s_geo in total_geo[idx:]:
  291. if self.app.abort_flag:
  292. # graceful abort requested by the user
  293. raise FlatCAMApp.GracefulException
  294. # minimize the number of distances by not taking into considerations those that are too small
  295. dist = geo.distance(s_geo)
  296. dist = float('%.*f' % (self.decimals, dist))
  297. loc_1, loc_2 = nearest_points(geo, s_geo)
  298. proc_loc = (
  299. (float('%.*f' % (self.decimals, loc_1.x)), float('%.*f' % (self.decimals, loc_1.y))),
  300. (float('%.*f' % (self.decimals, loc_2.x)), float('%.*f' % (self.decimals, loc_2.y)))
  301. )
  302. if dist in self.min_dict:
  303. self.min_dict[dist].append(proc_loc)
  304. else:
  305. self.min_dict[dist] = [proc_loc]
  306. pol_nr += 1
  307. disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
  308. if old_disp_number < disp_number <= 100:
  309. app_obj.proc_container.update_view_text(' %d%%' % disp_number)
  310. old_disp_number = disp_number
  311. idx += 1
  312. app_obj.inform.emit(
  313. _("Optimal Tool. Finding the minimum distance."))
  314. min_list = list(self.min_dict.keys())
  315. min_dist = min(min_list)
  316. min_dist_string = '%.*f' % (self.decimals, float(min_dist))
  317. self.result_entry.set_value(min_dist_string)
  318. freq = len(self.min_dict[min_dist])
  319. freq = '%d' % int(freq)
  320. self.freq_entry.set_value(freq)
  321. min_locations = self.min_dict.pop(min_dist)
  322. self.update_text.emit(min_locations)
  323. self.update_sec_distances.emit(self.min_dict)
  324. app_obj.inform.emit('[success] %s' % _("Optimal Tool. Finished successfully."))
  325. except Exception as ee:
  326. proc.done()
  327. log.debug(str(ee))
  328. return
  329. proc.done()
  330. self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
  331. def on_locate_position(self):
  332. # cursor = self.locations_textb.textCursor()
  333. # self.selected_text = cursor.selectedText()
  334. try:
  335. if self.selected_text != '':
  336. loc = eval(self.selected_text)
  337. else:
  338. return 'fail'
  339. except Exception as e:
  340. log.debug("ToolOptimal.on_locate_position() --> first try %s" % str(e))
  341. self.app.inform.emit("[ERROR_NOTCL] The selected text is no valid location in the format "
  342. "((x0, y0), (x1, y1)).")
  343. return 'fail'
  344. try:
  345. loc_1 = loc[0]
  346. loc_2 = loc[1]
  347. dx = loc_1[0] - loc_2[0]
  348. dy = loc_1[1] - loc_2[1]
  349. loc = (float('%.*f' % (self.decimals, (min(loc_1[0], loc_2[0]) + (abs(dx) / 2)))),
  350. float('%.*f' % (self.decimals, (min(loc_1[1], loc_2[1]) + (abs(dy) / 2)))))
  351. self.app.on_jump_to(custom_location=loc)
  352. except Exception as e:
  353. log.debug("ToolOptimal.on_locate_position() --> sec try %s" % str(e))
  354. return 'fail'
  355. def on_update_text(self, data):
  356. txt = ''
  357. for loc in data:
  358. if loc:
  359. txt += '%s, %s\n' % (str(loc[0]), str(loc[1]))
  360. self.locations_textb.setPlainText(txt)
  361. self.locate_button.setDisabled(False)
  362. def on_textbox_clicked(self):
  363. # new cursor - select all document
  364. cursor = self.locations_textb.textCursor()
  365. cursor.select(QtGui.QTextCursor.Document)
  366. # clear previous selection highlight
  367. tmp = cursor.blockFormat()
  368. tmp.clearBackground()
  369. cursor.setBlockFormat(tmp)
  370. # new cursor - select the current line
  371. cursor = self.locations_textb.textCursor()
  372. cursor.select(QtGui.QTextCursor.LineUnderCursor)
  373. # highlight the current selected line
  374. tmp = cursor.blockFormat()
  375. tmp.setBackground(QtGui.QBrush(QtCore.Qt.yellow))
  376. cursor.setBlockFormat(tmp)
  377. self.selected_text = cursor.selectedText()
  378. def on_update_sec_distances_txt(self, data):
  379. distance_list = sorted(list(data.keys()))
  380. txt = ''
  381. for loc in distance_list:
  382. txt += '%s\n' % str(loc)
  383. self.distances_textb.setPlainText(txt)
  384. self.locate_sec_button.setDisabled(False)
  385. def on_distances_textb_clicked(self):
  386. # new cursor - select all document
  387. cursor = self.distances_textb.textCursor()
  388. cursor.select(QtGui.QTextCursor.Document)
  389. # clear previous selection highlight
  390. tmp = cursor.blockFormat()
  391. tmp.clearBackground()
  392. cursor.setBlockFormat(tmp)
  393. # new cursor - select the current line
  394. cursor = self.distances_textb.textCursor()
  395. cursor.select(QtGui.QTextCursor.LineUnderCursor)
  396. # highlight the current selected line
  397. tmp = cursor.blockFormat()
  398. tmp.setBackground(QtGui.QBrush(QtCore.Qt.yellow))
  399. cursor.setBlockFormat(tmp)
  400. distance_text = cursor.selectedText()
  401. key_in_min_dict = eval(distance_text)
  402. self.on_update_locations_text(dist=key_in_min_dict)
  403. def on_update_locations_text(self, dist):
  404. distance_list = self.min_dict[dist]
  405. txt = ''
  406. for loc in distance_list:
  407. if loc:
  408. txt += '%s, %s\n' % (str(loc[0]), str(loc[1]))
  409. self.locations_sec_textb.setPlainText(txt)
  410. def on_locations_sec_clicked(self):
  411. # new cursor - select all document
  412. cursor = self.locations_sec_textb.textCursor()
  413. cursor.select(QtGui.QTextCursor.Document)
  414. # clear previous selection highlight
  415. tmp = cursor.blockFormat()
  416. tmp.clearBackground()
  417. cursor.setBlockFormat(tmp)
  418. # new cursor - select the current line
  419. cursor = self.locations_sec_textb.textCursor()
  420. cursor.select(QtGui.QTextCursor.LineUnderCursor)
  421. # highlight the current selected line
  422. tmp = cursor.blockFormat()
  423. tmp.setBackground(QtGui.QBrush(QtCore.Qt.yellow))
  424. cursor.setBlockFormat(tmp)
  425. self.selected_locations_text = cursor.selectedText()
  426. def on_locate_sec_position(self):
  427. try:
  428. if self.selected_locations_text != '':
  429. loc = eval(self.selected_locations_text)
  430. else:
  431. return 'fail'
  432. except Exception as e:
  433. log.debug("ToolOptimal.on_locate_sec_position() --> first try %s" % str(e))
  434. self.app.inform.emit("[ERROR_NOTCL] The selected text is no valid location in the format "
  435. "((x0, y0), (x1, y1)).")
  436. return 'fail'
  437. try:
  438. loc_1 = loc[0]
  439. loc_2 = loc[1]
  440. dx = loc_1[0] - loc_2[0]
  441. dy = loc_1[1] - loc_2[1]
  442. loc = (float('%.*f' % (self.decimals, (min(loc_1[0], loc_2[0]) + (abs(dx) / 2)))),
  443. float('%.*f' % (self.decimals, (min(loc_1[1], loc_2[1]) + (abs(dy) / 2)))))
  444. self.app.on_jump_to(custom_location=loc)
  445. except Exception as e:
  446. log.debug("ToolOptimal.on_locate_sec_position() --> sec try %s" % str(e))
  447. return 'fail'
  448. def reset_fields(self):
  449. self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  450. self.gerber_object_combo.setCurrentIndex(0)