ToolOptimal.py 23 KB

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