ToolAlignObjects.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # File Author: Marius Adrian Stanciu (c) #
  4. # Date: 1/13/2020 #
  5. # MIT Licence #
  6. # ##########################################################
  7. from PyQt5 import QtWidgets, QtGui, QtCore
  8. from FlatCAMTool import FlatCAMTool
  9. from flatcamGUI.GUIElements import FCComboBox
  10. from copy import deepcopy
  11. import numpy as np
  12. from shapely.geometry import Point
  13. import gettext
  14. import FlatCAMTranslation as fcTranslate
  15. import builtins
  16. import logging
  17. fcTranslate.apply_language('strings')
  18. if '_' not in builtins.__dict__:
  19. _ = gettext.gettext
  20. log = logging.getLogger('base')
  21. class AlignObjects(FlatCAMTool):
  22. toolName = _("Align Objects")
  23. def __init__(self, app):
  24. FlatCAMTool.__init__(self, app)
  25. self.app = app
  26. self.decimals = app.decimals
  27. self.canvas = self.app.plotcanvas
  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. grid0 = QtWidgets.QGridLayout()
  40. grid0.setColumnStretch(0, 0)
  41. grid0.setColumnStretch(1, 1)
  42. self.layout.addLayout(grid0)
  43. self.aligned_label = QtWidgets.QLabel('<b>%s</b>' % _("Selection of the aligned object"))
  44. grid0.addWidget(self.aligned_label, 0, 0, 1, 2)
  45. # Type of object to be aligned
  46. self.type_obj_combo = QtWidgets.QComboBox()
  47. self.type_obj_combo.addItem("Gerber")
  48. self.type_obj_combo.addItem("Excellon")
  49. self.type_obj_combo.addItem("Geometry")
  50. self.type_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
  51. self.type_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
  52. self.type_obj_combo.setItemIcon(2, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
  53. self.type_obj_combo_label = QtWidgets.QLabel('%s:' % _("Object Type"))
  54. self.type_obj_combo_label.setToolTip(
  55. _("Specify the type of object to be aligned.\n"
  56. "It can be of type: Gerber, Excellon or Geometry.\n"
  57. "The selection here decide the type of objects that will be\n"
  58. "in the Object combobox.")
  59. )
  60. grid0.addWidget(self.type_obj_combo_label, 2, 0)
  61. grid0.addWidget(self.type_obj_combo, 2, 1)
  62. # Object to be aligned
  63. self.object_combo = QtWidgets.QComboBox()
  64. self.object_combo.setModel(self.app.collection)
  65. self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  66. self.object_combo.setCurrentIndex(1)
  67. self.object_label = QtWidgets.QLabel('%s:' % _("Object"))
  68. self.object_label.setToolTip(
  69. _("Object to be aligned.")
  70. )
  71. grid0.addWidget(self.object_label, 3, 0)
  72. grid0.addWidget(self.object_combo, 3, 1)
  73. separator_line = QtWidgets.QFrame()
  74. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  75. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  76. grid0.addWidget(separator_line, 4, 0, 1, 2)
  77. self.aligned_label = QtWidgets.QLabel('<b>%s</b>' % _("Selection of the aligner object"))
  78. self.aligned_label.setToolTip(
  79. _("Object to which the other objects will be aligned to (moved).")
  80. )
  81. grid0.addWidget(self.aligned_label, 6, 0, 1, 2)
  82. # Type of object to be aligned to = aligner
  83. self.type_aligner_obj_combo = QtWidgets.QComboBox()
  84. self.type_aligner_obj_combo.addItem("Gerber")
  85. self.type_aligner_obj_combo.addItem("Excellon")
  86. self.type_aligner_obj_combo.addItem("Geometry")
  87. self.type_aligner_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
  88. self.type_aligner_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
  89. self.type_aligner_obj_combo.setItemIcon(2, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
  90. self.type_aligner_obj_combo_label = QtWidgets.QLabel('%s:' % _("Object Type"))
  91. self.type_aligner_obj_combo_label.setToolTip(
  92. _("Specify the type of object to be aligned to.\n"
  93. "It can be of type: Gerber, Excellon or Geometry.\n"
  94. "The selection here decide the type of objects that will be\n"
  95. "in the Object combobox.")
  96. )
  97. grid0.addWidget(self.type_aligner_obj_combo_label, 7, 0)
  98. grid0.addWidget(self.type_aligner_obj_combo, 7, 1)
  99. # Object to be aligned to = aligner
  100. self.aligner_object_combo = QtWidgets.QComboBox()
  101. self.aligner_object_combo.setModel(self.app.collection)
  102. self.aligner_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  103. self.aligner_object_combo.setCurrentIndex(1)
  104. self.aligner_object_label = QtWidgets.QLabel('%s:' % _("Object"))
  105. self.aligner_object_label.setToolTip(
  106. _("Object to be aligned to. Aligner.")
  107. )
  108. grid0.addWidget(self.aligner_object_label, 8, 0)
  109. grid0.addWidget(self.aligner_object_combo, 8, 1)
  110. separator_line = QtWidgets.QFrame()
  111. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  112. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  113. grid0.addWidget(separator_line, 9, 0, 1, 2)
  114. # Buttons
  115. self.align_object_button = QtWidgets.QPushButton(_("Align Object"))
  116. self.align_object_button.setToolTip(
  117. _("Align the specified object to the aligner object.\n"
  118. "If only one point is used then it assumes translation.\n"
  119. "If tho points are used it assume translation and rotation.")
  120. )
  121. self.align_object_button.setStyleSheet("""
  122. QPushButton
  123. {
  124. font-weight: bold;
  125. }
  126. """)
  127. self.layout.addWidget(self.align_object_button)
  128. self.layout.addStretch()
  129. # ## Reset Tool
  130. self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
  131. self.reset_button.setToolTip(
  132. _("Will reset the tool parameters.")
  133. )
  134. self.reset_button.setStyleSheet("""
  135. QPushButton
  136. {
  137. font-weight: bold;
  138. }
  139. """)
  140. self.layout.addWidget(self.reset_button)
  141. # Signals
  142. self.align_object_button.clicked.connect(self.on_align)
  143. self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
  144. self.type_aligner_obj_combo.currentIndexChanged.connect(self.on_type_aligner_index_changed)
  145. self.reset_button.clicked.connect(self.set_tool_ui)
  146. self.mr = None
  147. # if the mouse events are connected to a local method set this True
  148. self.local_connected = False
  149. # store the status of the grid
  150. self.grid_status_memory = None
  151. self.aligned_obj = None
  152. self.aligner_obj = None
  153. # here store the alignment points for the aligned object
  154. self.aligned_clicked_points = list()
  155. # here store the alignment points for the aligner object
  156. self.aligner_clicked_points = list()
  157. # counter for the clicks
  158. self.click_cnt = 0
  159. def run(self, toggle=True):
  160. self.app.report_usage("ToolAlignObjects()")
  161. if toggle:
  162. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  163. if self.app.ui.splitter.sizes()[0] == 0:
  164. self.app.ui.splitter.setSizes([1, 1])
  165. else:
  166. try:
  167. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  168. # if tab is populated with the tool but it does not have the focus, focus on it
  169. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  170. # focus on Tool Tab
  171. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  172. else:
  173. self.app.ui.splitter.setSizes([0, 1])
  174. except AttributeError:
  175. pass
  176. else:
  177. if self.app.ui.splitter.sizes()[0] == 0:
  178. self.app.ui.splitter.setSizes([1, 1])
  179. FlatCAMTool.run(self)
  180. self.set_tool_ui()
  181. self.app.ui.notebook.setTabText(2, _("Align Tool"))
  182. def install(self, icon=None, separator=None, **kwargs):
  183. FlatCAMTool.install(self, icon, separator, shortcut='ALT+A', **kwargs)
  184. def set_tool_ui(self):
  185. self.reset_fields()
  186. self.click_cnt = 0
  187. if self.local_connected is True:
  188. self.disconnect_cal_events()
  189. def on_type_obj_index_changed(self):
  190. obj_type = self.type_obj_combo.currentIndex()
  191. self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
  192. self.object_combo.setCurrentIndex(0)
  193. def on_type_aligner_index_changed(self):
  194. obj_type = self.type_aligner_obj_combo.currentIndex()
  195. self.aligner_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
  196. self.aligner_object_combo.setCurrentIndex(0)
  197. def on_align(self):
  198. obj_sel_index = self.object_combo.currentIndex()
  199. obj_model_index = self.app.collection.index(obj_sel_index, 0, self.object_combo.rootModelIndex())
  200. try:
  201. self.aligned_obj = obj_model_index.internalPointer().obj
  202. except AttributeError:
  203. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no aligned FlatCAM object selected..."))
  204. return
  205. aligner_obj_sel_index = self.object_combo.currentIndex()
  206. aligner_obj_model_index = self.app.collection.index(
  207. aligner_obj_sel_index, 0, self.object_combo.rootModelIndex())
  208. try:
  209. self.aligner_obj = aligner_obj_model_index.internalPointer().obj
  210. except AttributeError:
  211. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no aligner FlatCAM object selected..."))
  212. return
  213. # disengage the grid snapping since it will be hard to find the drills or pads on grid
  214. if self.app.ui.grid_snap_btn.isChecked():
  215. self.grid_status_memory = True
  216. self.app.ui.grid_snap_btn.trigger()
  217. else:
  218. self.grid_status_memory = False
  219. self.mr = self.canvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
  220. if self.app.is_legacy is False:
  221. self.canvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
  222. else:
  223. self.canvas.graph_event_disconnect(self.app.mr)
  224. self.local_connected = True
  225. self.app.inform.emit(_("Get First alignment point on the aligned object."))
  226. def on_mouse_click_release(self, event):
  227. if self.app.is_legacy is False:
  228. event_pos = event.pos
  229. right_button = 2
  230. self.app.event_is_dragging = self.app.event_is_dragging
  231. else:
  232. event_pos = (event.xdata, event.ydata)
  233. right_button = 3
  234. self.app.event_is_dragging = self.app.ui.popMenu.mouse_is_panning
  235. pos_canvas = self.canvas.translate_coords(event_pos)
  236. if event.button == 1:
  237. click_pt = Point([pos_canvas[0], pos_canvas[1]])
  238. if self.app.selection_type is not None:
  239. # delete previous selection shape
  240. self.app.delete_selection_shape()
  241. self.app.selection_type = None
  242. else:
  243. if self.target_obj.kind.lower() == 'excellon':
  244. for tool, tool_dict in self.target_obj.tools.items():
  245. for geo in tool_dict['solid_geometry']:
  246. if click_pt.within(geo):
  247. center_pt = geo.centroid
  248. self.click_points.append(
  249. [
  250. float('%.*f' % (self.decimals, center_pt.x)),
  251. float('%.*f' % (self.decimals, center_pt.y))
  252. ]
  253. )
  254. self.check_points()
  255. elif self.target_obj.kind.lower() == 'gerber':
  256. for apid, apid_val in self.target_obj.apertures.items():
  257. for geo_el in apid_val['geometry']:
  258. if 'solid' in geo_el:
  259. if click_pt.within(geo_el['solid']):
  260. if isinstance(geo_el['follow'], Point):
  261. center_pt = geo_el['solid'].centroid
  262. self.click_points.append(
  263. [
  264. float('%.*f' % (self.decimals, center_pt.x)),
  265. float('%.*f' % (self.decimals, center_pt.y))
  266. ]
  267. )
  268. self.check_points()
  269. elif event.button == right_button and self.app.event_is_dragging is False:
  270. if not len(self.click_points):
  271. self.reset_calibration_points()
  272. self.disconnect_cal_events()
  273. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled by user request."))
  274. def check_points(self):
  275. if len(self.aligned_click_points) == 1:
  276. self.app.inform.emit(_("Get Second alignment point on aligned object. "
  277. "Or right click to get First alignment point on the aligner object."))
  278. if len(self.aligned_click_points) == 2:
  279. self.app.inform.emit(_("Get First alignment point on the aligner object."))
  280. if len(self.aligner_click_points) == 1:
  281. self.app.inform.emit(_("Get Second alignment point on the aligner object. Or right click to finish."))
  282. self.align_translate()
  283. self.align_rotate()
  284. self.disconnect_cal_events()
  285. def align_translate(self):
  286. pass
  287. def align_rotate(self):
  288. pass
  289. def execute(self):
  290. aligned_name = self.object_combo.currentText()
  291. # Get source object.
  292. try:
  293. aligned_obj = self.app.collection.get_by_name(str(aligned_name))
  294. except Exception as e:
  295. log.debug("AlignObjects.on_align() --> %s" % str(e))
  296. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), aligned_name))
  297. return "Could not retrieve object: %s" % aligned_name
  298. if aligned_obj is None:
  299. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), aligned_obj))
  300. return "Object not found: %s" % aligned_obj
  301. aligner_name = self.box_combo.currentText()
  302. try:
  303. aligner_obj = self.app.collection.get_by_name(aligner_name)
  304. except Exception as e:
  305. log.debug("AlignObjects.on_align() --> %s" % str(e))
  306. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), aligner_name))
  307. return "Could not retrieve object: %s" % aligner_name
  308. if aligner_obj is None:
  309. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), aligner_name))
  310. def align_job():
  311. pass
  312. proc = self.app.proc_container.new(_("Working..."))
  313. def job_thread(app_obj):
  314. try:
  315. align_job()
  316. app_obj.inform.emit('[success] %s' % _("Panel created successfully."))
  317. except Exception as ee:
  318. proc.done()
  319. log.debug(str(ee))
  320. return
  321. proc.done()
  322. self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
  323. def disconnect_cal_events(self):
  324. # restore the Grid snapping if it was active before
  325. if self.grid_status_memory is True:
  326. self.app.ui.grid_snap_btn.trigger()
  327. self.app.mr = self.canvas.graph_event_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
  328. if self.app.is_legacy is False:
  329. self.canvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
  330. else:
  331. self.canvas.graph_event_disconnect(self.mr)
  332. self.local_connected = False
  333. self.click_cnt = 0
  334. def reset_fields(self):
  335. self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  336. self.aligner_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))