ToolAlignObjects.py 21 KB

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