ToolAlignObjects.py 19 KB

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