ToolAlignObjects.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  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, RadioSet
  10. import math
  11. from shapely.geometry import Point
  12. from shapely.affinity import translate
  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 WORKING object"))
  44. grid0.addWidget(self.aligned_label, 0, 0, 1, 2)
  45. # Type of object to be aligned
  46. self.type_obj_combo = FCComboBox()
  47. self.type_obj_combo.addItem("Gerber")
  48. self.type_obj_combo.addItem("Excellon")
  49. self.type_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
  50. self.type_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
  51. self.type_obj_combo_label = QtWidgets.QLabel('%s:' % _("Object Type"))
  52. self.type_obj_combo_label.setToolTip(
  53. _("Specify the type of object to be aligned.\n"
  54. "It can be of type: Gerber or Excellon.\n"
  55. "The selection here decide the type of objects that will be\n"
  56. "in the Object combobox.")
  57. )
  58. grid0.addWidget(self.type_obj_combo_label, 2, 0)
  59. grid0.addWidget(self.type_obj_combo, 2, 1)
  60. # Object to be aligned
  61. self.object_combo = FCComboBox()
  62. self.object_combo.setModel(self.app.collection)
  63. self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  64. self.object_combo.setCurrentIndex(1)
  65. self.object_label = QtWidgets.QLabel('%s:' % _("Object"))
  66. self.object_label.setToolTip(
  67. _("Object to be aligned.")
  68. )
  69. grid0.addWidget(self.object_label, 3, 0)
  70. grid0.addWidget(self.object_combo, 3, 1)
  71. separator_line = QtWidgets.QFrame()
  72. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  73. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  74. grid0.addWidget(separator_line, 4, 0, 1, 2)
  75. self.aligned_label = QtWidgets.QLabel('<b>%s</b>' % _("Selection of the TARGET object"))
  76. self.aligned_label.setToolTip(
  77. _("Object to which the other objects will be aligned to (moved to).")
  78. )
  79. grid0.addWidget(self.aligned_label, 6, 0, 1, 2)
  80. # Type of object to be aligned to = aligner
  81. self.type_aligner_obj_combo = FCComboBox()
  82. self.type_aligner_obj_combo.addItem("Gerber")
  83. self.type_aligner_obj_combo.addItem("Excellon")
  84. self.type_aligner_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
  85. self.type_aligner_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
  86. self.type_aligner_obj_combo_label = QtWidgets.QLabel('%s:' % _("Object Type"))
  87. self.type_aligner_obj_combo_label.setToolTip(
  88. _("Specify the type of object to be aligned to.\n"
  89. "It can be of type: Gerber or Excellon.\n"
  90. "The selection here decide the type of objects that will be\n"
  91. "in the Object combobox.")
  92. )
  93. grid0.addWidget(self.type_aligner_obj_combo_label, 7, 0)
  94. grid0.addWidget(self.type_aligner_obj_combo, 7, 1)
  95. # Object to be aligned to = aligner
  96. self.aligner_object_combo = FCComboBox()
  97. self.aligner_object_combo.setModel(self.app.collection)
  98. self.aligner_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  99. self.aligner_object_combo.setCurrentIndex(1)
  100. self.aligner_object_label = QtWidgets.QLabel('%s:' % _("Object"))
  101. self.aligner_object_label.setToolTip(
  102. _("Object to be aligned to. Aligner.")
  103. )
  104. grid0.addWidget(self.aligner_object_label, 8, 0)
  105. grid0.addWidget(self.aligner_object_combo, 8, 1)
  106. separator_line = QtWidgets.QFrame()
  107. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  108. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  109. grid0.addWidget(separator_line, 9, 0, 1, 2)
  110. # Alignment Type
  111. self.a_type_lbl = QtWidgets.QLabel('<b>%s:</b>' % _("Alignment Type"))
  112. self.a_type_lbl.setToolTip(
  113. _("The type of alignment can be:\n"
  114. "- Single Point -> it require a single point of sync, the action will be a translation\n"
  115. "- Dual Point -> it require two points of sync, the action will be translation followed by rotation")
  116. )
  117. self.a_type_radio = RadioSet(
  118. [
  119. {'label': _('Single Point'), 'value': 'sp'},
  120. {'label': _('Dual Point'), 'value': 'dp'}
  121. ],
  122. orientation='horizontal',
  123. stretch=False
  124. )
  125. grid0.addWidget(self.a_type_lbl, 10, 0, 1, 2)
  126. grid0.addWidget(self.a_type_radio, 11, 0, 1, 2)
  127. separator_line = QtWidgets.QFrame()
  128. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  129. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  130. grid0.addWidget(separator_line, 12, 0, 1, 2)
  131. # Buttons
  132. self.align_object_button = QtWidgets.QPushButton(_("Align Object"))
  133. self.align_object_button.setToolTip(
  134. _("Align the specified object to the aligner object.\n"
  135. "If only one point is used then it assumes translation.\n"
  136. "If tho points are used it assume translation and rotation.")
  137. )
  138. self.align_object_button.setStyleSheet("""
  139. QPushButton
  140. {
  141. font-weight: bold;
  142. }
  143. """)
  144. self.layout.addWidget(self.align_object_button)
  145. self.layout.addStretch()
  146. # ## Reset Tool
  147. self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
  148. self.reset_button.setToolTip(
  149. _("Will reset the tool parameters.")
  150. )
  151. self.reset_button.setStyleSheet("""
  152. QPushButton
  153. {
  154. font-weight: bold;
  155. }
  156. """)
  157. self.layout.addWidget(self.reset_button)
  158. # Signals
  159. self.align_object_button.clicked.connect(self.on_align)
  160. self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
  161. self.type_aligner_obj_combo.currentIndexChanged.connect(self.on_type_aligner_index_changed)
  162. self.reset_button.clicked.connect(self.set_tool_ui)
  163. self.mr = None
  164. # if the mouse events are connected to a local method set this True
  165. self.local_connected = False
  166. # store the status of the grid
  167. self.grid_status_memory = None
  168. self.aligned_obj = None
  169. self.aligner_obj = None
  170. # this is one of the objects: self.aligned_obj or self.aligner_obj
  171. self.target_obj = None
  172. # here store the alignment points
  173. self.clicked_points = list()
  174. self.align_type = None
  175. # old colors of objects involved in the alignment
  176. self.aligner_old_fill_color = None
  177. self.aligner_old_line_color = None
  178. self.aligned_old_fill_color = None
  179. self.aligned_old_line_color = None
  180. def run(self, toggle=True):
  181. self.app.report_usage("ToolAlignObjects()")
  182. if toggle:
  183. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  184. if self.app.ui.splitter.sizes()[0] == 0:
  185. self.app.ui.splitter.setSizes([1, 1])
  186. else:
  187. try:
  188. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  189. # if tab is populated with the tool but it does not have the focus, focus on it
  190. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  191. # focus on Tool Tab
  192. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  193. else:
  194. self.app.ui.splitter.setSizes([0, 1])
  195. except AttributeError:
  196. pass
  197. else:
  198. if self.app.ui.splitter.sizes()[0] == 0:
  199. self.app.ui.splitter.setSizes([1, 1])
  200. FlatCAMTool.run(self)
  201. self.set_tool_ui()
  202. self.app.ui.notebook.setTabText(2, _("Align Tool"))
  203. def install(self, icon=None, separator=None, **kwargs):
  204. FlatCAMTool.install(self, icon, separator, shortcut='ALT+A', **kwargs)
  205. def set_tool_ui(self):
  206. self.reset_fields()
  207. self.clicked_points = list()
  208. self.target_obj = None
  209. self.aligned_obj = None
  210. self.aligner_obj = None
  211. self.aligner_old_fill_color = None
  212. self.aligner_old_line_color = None
  213. self.aligned_old_fill_color = None
  214. self.aligned_old_line_color = None
  215. self.a_type_radio.set_value(self.app.defaults["tools_align_objects_align_type"])
  216. if self.local_connected is True:
  217. self.disconnect_cal_events()
  218. def on_type_obj_index_changed(self):
  219. obj_type = self.type_obj_combo.currentIndex()
  220. self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
  221. self.object_combo.setCurrentIndex(0)
  222. def on_type_aligner_index_changed(self):
  223. obj_type = self.type_aligner_obj_combo.currentIndex()
  224. self.aligner_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
  225. self.aligner_object_combo.setCurrentIndex(0)
  226. def on_align(self):
  227. self.app.delete_selection_shape()
  228. obj_sel_index = self.object_combo.currentIndex()
  229. obj_model_index = self.app.collection.index(obj_sel_index, 0, self.object_combo.rootModelIndex())
  230. try:
  231. self.aligned_obj = obj_model_index.internalPointer().obj
  232. except AttributeError:
  233. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no aligned FlatCAM object selected..."))
  234. return
  235. aligner_obj_sel_index = self.aligner_object_combo.currentIndex()
  236. aligner_obj_model_index = self.app.collection.index(
  237. aligner_obj_sel_index, 0, self.aligner_object_combo.rootModelIndex())
  238. try:
  239. self.aligner_obj = aligner_obj_model_index.internalPointer().obj
  240. except AttributeError:
  241. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no aligner FlatCAM object selected..."))
  242. return
  243. self.align_type = self.a_type_radio.get_value()
  244. # disengage the grid snapping since it will be hard to find the drills or pads on grid
  245. if self.app.ui.grid_snap_btn.isChecked():
  246. self.grid_status_memory = True
  247. self.app.ui.grid_snap_btn.trigger()
  248. else:
  249. self.grid_status_memory = False
  250. self.mr = self.canvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
  251. if self.app.is_legacy is False:
  252. self.canvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
  253. else:
  254. self.canvas.graph_event_disconnect(self.app.mr)
  255. self.local_connected = True
  256. self.aligner_old_fill_color = self.aligner_obj.fill_color
  257. self.aligner_old_line_color = self.aligner_obj.outline_color
  258. self.aligned_old_fill_color = self.aligned_obj.fill_color
  259. self.aligned_old_line_color = self.aligned_obj.outline_color
  260. self.app.inform.emit('%s: %s' % (_("First Point"), _("Click on the START point.")))
  261. self.target_obj = self.aligned_obj
  262. self.set_color()
  263. def on_mouse_click_release(self, event):
  264. if self.app.is_legacy is False:
  265. event_pos = event.pos
  266. right_button = 2
  267. self.app.event_is_dragging = self.app.event_is_dragging
  268. else:
  269. event_pos = (event.xdata, event.ydata)
  270. right_button = 3
  271. self.app.event_is_dragging = self.app.ui.popMenu.mouse_is_panning
  272. pos_canvas = self.canvas.translate_coords(event_pos)
  273. if event.button == 1:
  274. click_pt = Point([pos_canvas[0], pos_canvas[1]])
  275. if self.app.selection_type is not None:
  276. # delete previous selection shape
  277. self.app.delete_selection_shape()
  278. self.app.selection_type = None
  279. else:
  280. if self.target_obj.kind.lower() == 'excellon':
  281. for tool, tool_dict in self.target_obj.tools.items():
  282. for geo in tool_dict['solid_geometry']:
  283. if click_pt.within(geo):
  284. center_pt = geo.centroid
  285. self.clicked_points.append(
  286. [
  287. float('%.*f' % (self.decimals, center_pt.x)),
  288. float('%.*f' % (self.decimals, center_pt.y))
  289. ]
  290. )
  291. self.check_points()
  292. elif self.target_obj.kind.lower() == 'gerber':
  293. for apid, apid_val in self.target_obj.apertures.items():
  294. for geo_el in apid_val['geometry']:
  295. if 'solid' in geo_el:
  296. if click_pt.within(geo_el['solid']):
  297. if isinstance(geo_el['follow'], Point):
  298. center_pt = geo_el['solid'].centroid
  299. self.clicked_points.append(
  300. [
  301. float('%.*f' % (self.decimals, center_pt.x)),
  302. float('%.*f' % (self.decimals, center_pt.y))
  303. ]
  304. )
  305. self.check_points()
  306. elif event.button == right_button and self.app.event_is_dragging is False:
  307. self.reset_color()
  308. self.clicked_points = list()
  309. self.disconnect_cal_events()
  310. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled by user request."))
  311. def check_points(self):
  312. if len(self.clicked_points) == 1:
  313. self.app.inform.emit('%s: %s. %s' % (
  314. _("First Point"), _("Click on the DESTINATION point."), _(" Or right click to cancel.")))
  315. self.target_obj = self.aligner_obj
  316. self.reset_color()
  317. self.set_color()
  318. if len(self.clicked_points) == 2:
  319. if self.align_type == 'sp':
  320. self.align_translate()
  321. self.app.inform.emit('[success] %s' % _("Done."))
  322. self.app.plot_all()
  323. self.disconnect_cal_events()
  324. return
  325. else:
  326. self.app.inform.emit('%s: %s. %s' % (
  327. _("Second Point"), _("Click on the START point."), _(" Or right click to cancel.")))
  328. self.target_obj = self.aligned_obj
  329. self.reset_color()
  330. self.set_color()
  331. if len(self.clicked_points) == 3:
  332. self.app.inform.emit('%s: %s. %s' % (
  333. _("Second Point"), _("Click on the DESTINATION point."), _(" Or right click to cancel.")))
  334. self.target_obj = self.aligner_obj
  335. self.reset_color()
  336. self.set_color()
  337. if len(self.clicked_points) == 4:
  338. self.align_translate()
  339. self.align_rotate()
  340. self.app.inform.emit('[success] %s' % _("Done."))
  341. self.disconnect_cal_events()
  342. self.app.plot_all()
  343. def align_translate(self):
  344. dx = self.clicked_points[1][0] - self.clicked_points[0][0]
  345. dy = self.clicked_points[1][1] - self.clicked_points[0][1]
  346. self.aligned_obj.offset((dx, dy))
  347. # Update the object bounding box options
  348. a, b, c, d = self.aligned_obj.bounds()
  349. self.aligned_obj.options['xmin'] = a
  350. self.aligned_obj.options['ymin'] = b
  351. self.aligned_obj.options['xmax'] = c
  352. self.aligned_obj.options['ymax'] = d
  353. def align_rotate(self):
  354. dx = self.clicked_points[1][0] - self.clicked_points[0][0]
  355. dy = self.clicked_points[1][1] - self.clicked_points[0][1]
  356. test_rotation_pt = translate(Point(self.clicked_points[2]), xoff=dx, yoff=dy)
  357. new_start = (test_rotation_pt.x, test_rotation_pt.y)
  358. new_dest = self.clicked_points[3]
  359. origin_pt = self.clicked_points[1]
  360. dxd = new_dest[0] - origin_pt[0]
  361. dyd = new_dest[1] - origin_pt[1]
  362. dxs = new_start[0] - origin_pt[0]
  363. dys = new_start[1] - origin_pt[1]
  364. rotation_not_needed = (abs(new_start[0] - new_dest[0]) <= (10 ** -self.decimals)) or \
  365. (abs(new_start[1] - new_dest[1]) <= (10 ** -self.decimals))
  366. if rotation_not_needed is False:
  367. # calculate rotation angle
  368. angle_dest = math.degrees(math.atan(dyd / dxd))
  369. angle_start = math.degrees(math.atan(dys / dxs))
  370. angle = angle_dest - angle_start
  371. self.aligned_obj.rotate(angle=angle, point=origin_pt)
  372. def disconnect_cal_events(self):
  373. # restore the Grid snapping if it was active before
  374. if self.grid_status_memory is True:
  375. self.app.ui.grid_snap_btn.trigger()
  376. self.app.mr = self.canvas.graph_event_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
  377. if self.app.is_legacy is False:
  378. self.canvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
  379. else:
  380. self.canvas.graph_event_disconnect(self.mr)
  381. self.local_connected = False
  382. self.aligner_old_fill_color = None
  383. self.aligner_old_line_color = None
  384. self.aligned_old_fill_color = None
  385. self.aligned_old_line_color = None
  386. def set_color(self):
  387. new_color = "#15678abf"
  388. new_line_color = new_color
  389. self.target_obj.shapes.redraw(
  390. update_colors=(new_color, new_line_color)
  391. )
  392. def reset_color(self):
  393. self.aligned_obj.shapes.redraw(
  394. update_colors=(self.aligned_old_fill_color, self.aligned_old_line_color)
  395. )
  396. self.aligner_obj.shapes.redraw(
  397. update_colors=(self.aligner_old_fill_color, self.aligner_old_line_color)
  398. )
  399. def reset_fields(self):
  400. self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  401. self.aligner_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))