ToolOptimal.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  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_text = 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. # ## Title
  29. title_label = QtWidgets.QLabel("%s" % self.toolName)
  30. title_label.setStyleSheet("""
  31. QLabel
  32. {
  33. font-size: 16px;
  34. font-weight: bold;
  35. }
  36. """)
  37. self.layout.addWidget(title_label)
  38. # ## Form Layout
  39. form_lay = QtWidgets.QFormLayout()
  40. self.layout.addLayout(form_lay)
  41. form_lay.addRow(QtWidgets.QLabel(""))
  42. # ## Gerber Object to mirror
  43. self.gerber_object_combo = QtWidgets.QComboBox()
  44. self.gerber_object_combo.setModel(self.app.collection)
  45. self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  46. self.gerber_object_combo.setCurrentIndex(1)
  47. self.gerber_object_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
  48. self.gerber_object_label.setToolTip(
  49. "Gerber object for which to find the minimum distance between copper features."
  50. )
  51. form_lay.addRow(self.gerber_object_label, self.gerber_object_combo)
  52. # Precision = nr of decimals
  53. self.precision_label = QtWidgets.QLabel('%s:' % _("Precision"))
  54. self.precision_label.setToolTip(_("Number of decimals kept for found distances."))
  55. self.precision_spinner = FCSpinner()
  56. self.precision_spinner.set_range(2, 10)
  57. self.precision_spinner.setWrapping(True)
  58. form_lay.addRow(self.precision_label, self.precision_spinner)
  59. # Results Title
  60. self.title_res_label = QtWidgets.QLabel('<b>%s:</b>' % _("Minimum distance"))
  61. self.title_res_label.setToolTip(_("Display minimum distance between copper features."))
  62. form_lay.addRow(self.title_res_label)
  63. # Result value
  64. self.result_label = QtWidgets.QLabel('%s:' % _("Determined"))
  65. self.result_entry = FCEntry()
  66. self.result_entry.setReadOnly(True)
  67. self.units_lbl = QtWidgets.QLabel(self.units.lower())
  68. self.units_lbl.setDisabled(True)
  69. hlay = QtWidgets.QHBoxLayout()
  70. hlay.addWidget(self.result_entry)
  71. hlay.addWidget(self.units_lbl)
  72. form_lay.addRow(self.result_label, hlay)
  73. # Frequency of minimum encounter
  74. self.freq_label = QtWidgets.QLabel('%s:' % _("Occurring"))
  75. self.freq_label.setToolTip(_("How many times this minimum is found."))
  76. self.freq_entry = FCEntry()
  77. self.freq_entry.setReadOnly(True)
  78. form_lay.addRow(self.freq_label, self.freq_entry)
  79. # Control if to display the locations of where the minimum was found
  80. self.locations_cb = FCCheckBox(_("Minimum points coordinates"))
  81. self.locations_cb.setToolTip(_("Coordinates for points where minimum distance was found."))
  82. form_lay.addRow(self.locations_cb)
  83. # Locations where minimum was found
  84. self.locations_textb = FCTextArea(parent=self)
  85. self.locations_textb.setReadOnly(True)
  86. self.locations_textb.setToolTip(_("Coordinates for points where minimum distance was found."))
  87. stylesheet = """
  88. QTextEdit { selection-background-color:yellow;
  89. selection-color:black;
  90. }
  91. """
  92. self.locations_textb.setStyleSheet(stylesheet)
  93. form_lay.addRow(self.locations_textb)
  94. # Jump button
  95. self.locate_button = QtWidgets.QPushButton(_("Jump to selected position"))
  96. self.locate_button.setToolTip(
  97. _("Select a position in the Locations text box and then\n"
  98. "click this button.")
  99. )
  100. self.locate_button.setMinimumWidth(60)
  101. self.locate_button.setDisabled(True)
  102. form_lay.addRow(self.locate_button)
  103. # Other distances in Gerber
  104. self.title_second_res_label = QtWidgets.QLabel('<b>%s:</b>' % _("Other distances"))
  105. self.title_second_res_label.setToolTip(_("Will display other distances in the Gerber file ordered from\n"
  106. "the minimum to the maximum, not including the absolute minimum."))
  107. form_lay.addRow(self.title_second_res_label)
  108. # Control if to display the locations of where the minimum was found
  109. self.sec_locations_cb = FCCheckBox(_("Other distances points coordinates"))
  110. self.sec_locations_cb.setToolTip(_("Other distances and the coordinates for points\n"
  111. "where the distance was found."))
  112. form_lay.addRow(self.sec_locations_cb)
  113. # this way I can hide/show the frame
  114. self.sec_locations_frame = QtWidgets.QFrame()
  115. self.sec_locations_frame.setContentsMargins(0, 0, 0, 0)
  116. self.layout.addWidget(self.sec_locations_frame)
  117. self.distances_box = QtWidgets.QVBoxLayout()
  118. self.distances_box.setContentsMargins(0, 0, 0, 0)
  119. self.sec_locations_frame.setLayout(self.distances_box)
  120. # Other distances
  121. self.distances_textb = FCTextArea(parent=self)
  122. self.distances_textb.setReadOnly(True)
  123. self.distances_textb.setToolTip(_("Gerber distances."))
  124. stylesheet = """
  125. QTextEdit { selection-background-color:yellow;
  126. selection-color:black;
  127. }
  128. """
  129. self.distances_textb.setStyleSheet(stylesheet)
  130. self.distances_box.addWidget(self.distances_textb)
  131. # Locations where minimum was found
  132. self.locations_sec_textb = FCTextArea(parent=self)
  133. self.locations_sec_textb.setReadOnly(True)
  134. self.locations_sec_textb.setToolTip(_("Coordinates for points where the selected distance was found."))
  135. stylesheet = """
  136. QTextEdit { selection-background-color:yellow;
  137. selection-color:black;
  138. }
  139. """
  140. self.locations_sec_textb.setStyleSheet(stylesheet)
  141. self.distances_box.addWidget(self.locations_sec_textb)
  142. # Jump button
  143. self.locate_sec_button = QtWidgets.QPushButton(_("Jump to selected position"))
  144. self.locate_sec_button.setToolTip(
  145. _("Select a position in the Locations text box and then\n"
  146. "click this button.")
  147. )
  148. self.locate_sec_button.setMinimumWidth(60)
  149. self.locate_sec_button.setDisabled(True)
  150. self.distances_box.addWidget(self.locate_sec_button)
  151. # GO button
  152. self.calculate_button = QtWidgets.QPushButton(_("Find Minimum"))
  153. self.calculate_button.setToolTip(
  154. _("Calculate the minimum distance between copper features,\n"
  155. "this will allow the determination of the right tool to\n"
  156. "use for isolation or copper clearing.")
  157. )
  158. self.calculate_button.setMinimumWidth(60)
  159. self.layout.addWidget(self.calculate_button)
  160. self.loc_ois = OptionalHideInputSection(self.locations_cb, [self.locations_textb, self.locate_button])
  161. self.sec_loc_ois = OptionalHideInputSection(self.sec_locations_cb, [self.sec_locations_frame])
  162. self.selected_text = ''
  163. # ## Signals
  164. self.calculate_button.clicked.connect(self.find_minimum_distance)
  165. self.locate_button.clicked.connect(self.on_locate_position)
  166. self.update_text.connect(self.on_update_text)
  167. self.locations_textb.cursorPositionChanged.connect(self.on_textbox_clicked)
  168. self.locate_sec_button.clicked.connect(self.on_locate_sec_position)
  169. self.update_sec_text.connect(self.on_update_sec_text)
  170. self.locations_sec_textb.cursorPositionChanged.connect(self.on_textbox_sec_clicked)
  171. self.layout.addStretch()
  172. def install(self, icon=None, separator=None, **kwargs):
  173. FlatCAMTool.install(self, icon, separator, shortcut='ALT+O', **kwargs)
  174. def run(self, toggle=True):
  175. self.app.report_usage("ToolOptimal()")
  176. self.result_entry.set_value(0.0)
  177. self.freq_entry.set_value('0')
  178. if toggle:
  179. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  180. if self.app.ui.splitter.sizes()[0] == 0:
  181. self.app.ui.splitter.setSizes([1, 1])
  182. else:
  183. try:
  184. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  185. # if tab is populated with the tool but it does not have the focus, focus on it
  186. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  187. # focus on Tool Tab
  188. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  189. else:
  190. self.app.ui.splitter.setSizes([0, 1])
  191. except AttributeError:
  192. pass
  193. else:
  194. if self.app.ui.splitter.sizes()[0] == 0:
  195. self.app.ui.splitter.setSizes([1, 1])
  196. FlatCAMTool.run(self)
  197. self.set_tool_ui()
  198. self.app.ui.notebook.setTabText(2, _("Optimal Tool"))
  199. def set_tool_ui(self):
  200. self.precision_spinner.set_value(int(self.decimals))
  201. self.locations_textb.clear()
  202. # new cursor - select all document
  203. cursor = self.locations_textb.textCursor()
  204. cursor.select(QtGui.QTextCursor.Document)
  205. # clear previous selection highlight
  206. tmp = cursor.blockFormat()
  207. tmp.clearBackground()
  208. cursor.setBlockFormat(tmp)
  209. self.locations_textb.setVisible(False)
  210. self.locate_button.setVisible(False)
  211. self.result_entry.set_value(0.0)
  212. self.freq_entry.set_value('0')
  213. self.reset_fields()
  214. def find_minimum_distance(self):
  215. self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
  216. self.decimals = int(self.precision_spinner.get_value())
  217. selection_index = self.gerber_object_combo.currentIndex()
  218. model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
  219. try:
  220. fcobj = model_index.internalPointer().obj
  221. except Exception as e:
  222. log.debug("ToolOptimal.find_minimum_distance() --> %s" % str(e))
  223. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
  224. return
  225. if not isinstance(fcobj, FlatCAMGerber):
  226. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Only Gerber objects can be evaluated."))
  227. return
  228. proc = self.app.proc_container.new(_("Working..."))
  229. def job_thread(app_obj):
  230. app_obj.inform.emit(_("Optimal Tool. Started to search for the minimum distance between copper features."))
  231. try:
  232. old_disp_number = 0
  233. pol_nr = 0
  234. app_obj.proc_container.update_view_text(' %d%%' % 0)
  235. total_geo = list()
  236. for ap in list(fcobj.apertures.keys()):
  237. if 'geometry' in fcobj.apertures[ap]:
  238. app_obj.inform.emit(
  239. '%s: %s' % (_("Optimal Tool. Parsing geometry for aperture"), str(ap)))
  240. for geo_el in fcobj.apertures[ap]['geometry']:
  241. if self.app.abort_flag:
  242. # graceful abort requested by the user
  243. raise FlatCAMApp.GracefulException
  244. if 'solid' in geo_el and geo_el['solid'] is not None and geo_el['solid'].is_valid:
  245. total_geo.append(geo_el['solid'])
  246. app_obj.inform.emit(
  247. _("Optimal Tool. Creating a buffer for the object geometry."))
  248. total_geo = MultiPolygon(total_geo)
  249. total_geo = total_geo.buffer(0)
  250. geo_len = len(total_geo)
  251. geo_len = (geo_len * (geo_len - 1)) / 2
  252. app_obj.inform.emit(
  253. '%s: %s' % (_("Optimal Tool. Finding the distances between each two elements. Iterations"),
  254. str(geo_len)))
  255. min_dict = dict()
  256. idx = 1
  257. for geo in total_geo:
  258. for s_geo in total_geo[idx:]:
  259. if self.app.abort_flag:
  260. # graceful abort requested by the user
  261. raise FlatCAMApp.GracefulException
  262. # minimize the number of distances by not taking into considerations those that are too small
  263. dist = geo.distance(s_geo)
  264. dist = float('%.*f' % (self.decimals, dist))
  265. loc_1, loc_2 = nearest_points(geo, s_geo)
  266. dx = loc_1.x - loc_2.x
  267. dy = loc_1.y - loc_2.y
  268. loc = (float('%.*f' % (self.decimals, (min(loc_1.x, loc_2.x) + (abs(dx) / 2)))),
  269. float('%.*f' % (self.decimals, (min(loc_1.y, loc_2.y) + (abs(dy) / 2)))))
  270. if dist in min_dict:
  271. min_dict[dist].append(loc)
  272. else:
  273. min_dict[dist] = [loc]
  274. pol_nr += 1
  275. disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
  276. if old_disp_number < disp_number <= 100:
  277. app_obj.proc_container.update_view_text(' %d%%' % disp_number)
  278. old_disp_number = disp_number
  279. idx += 1
  280. app_obj.inform.emit(
  281. _("Optimal Tool. Finding the minimum distance."))
  282. min_list = list(min_dict.keys())
  283. min_dist = min(min_list)
  284. min_dist_string = '%.*f' % (self.decimals, float(min_dist))
  285. self.result_entry.set_value(min_dist_string)
  286. freq = len(min_dict[min_dist])
  287. freq = '%d' % int(freq)
  288. self.freq_entry.set_value(freq)
  289. min_locations = min_dict.pop(min_dist)
  290. self.update_text.emit(min_locations)
  291. self.update_sec_text.emit(min_dict)
  292. app_obj.inform.emit('[success] %s' % _("Optimal Tool. Finished successfully."))
  293. except Exception as ee:
  294. proc.done()
  295. log.debug(str(ee))
  296. return
  297. proc.done()
  298. self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
  299. def on_locate_position(self):
  300. # cursor = self.locations_textb.textCursor()
  301. # self.selected_text = cursor.selectedText()
  302. try:
  303. loc = eval(self.selected_text)
  304. self.app.on_jump_to(custom_location=loc)
  305. except Exception as e:
  306. self.app.inform.emit("[ERROR_NOTCL] The selected text is no valid location in the format (x, y).")
  307. def on_locate_sec_position(self):
  308. pass
  309. def on_update_text(self, data):
  310. txt = ''
  311. for loc in data:
  312. txt += '%s\n' % str(loc)
  313. self.locations_textb.setPlainText(txt)
  314. self.locate_button.setDisabled(False)
  315. def on_update_sec_text(self, data):
  316. pass
  317. def on_textbox_clicked(self):
  318. # new cursor - select all document
  319. cursor = self.locations_textb.textCursor()
  320. cursor.select(QtGui.QTextCursor.Document)
  321. # clear previous selection highlight
  322. tmp = cursor.blockFormat()
  323. tmp.clearBackground()
  324. cursor.setBlockFormat(tmp)
  325. # new cursor - select the current line
  326. cursor = self.locations_textb.textCursor()
  327. cursor.select(QtGui.QTextCursor.LineUnderCursor)
  328. # highlight the current selected line
  329. tmp = cursor.blockFormat()
  330. tmp.setBackground(QtGui.QBrush(QtCore.Qt.yellow))
  331. cursor.setBlockFormat(tmp)
  332. self.selected_text = cursor.selectedText()
  333. def on_textbox_sec_clicked(self):
  334. pass
  335. def reset_fields(self):
  336. self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  337. self.gerber_object_combo.setCurrentIndex(0)