ToolFiducials.py 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # File Author: Marius Adrian Stanciu (c) #
  4. # Date: 11/21/2019 #
  5. # MIT Licence #
  6. # ##########################################################
  7. from PyQt5 import QtWidgets, QtCore, QtGui
  8. from appTool import AppTool
  9. from appGUI.GUIElements import FCDoubleSpinner, RadioSet, EvalEntry, FCTable, FCComboBox
  10. from shapely.geometry import Point, Polygon, MultiPolygon, LineString
  11. from shapely.geometry import box as box
  12. from shapely.ops import unary_union
  13. import math
  14. import logging
  15. from copy import deepcopy
  16. import gettext
  17. import appTranslation as fcTranslate
  18. import builtins
  19. fcTranslate.apply_language('strings')
  20. if '_' not in builtins.__dict__:
  21. _ = gettext.gettext
  22. log = logging.getLogger('base')
  23. class ToolFiducials(AppTool):
  24. def __init__(self, app):
  25. AppTool.__init__(self, app)
  26. self.app = app
  27. self.canvas = self.app.plotcanvas
  28. self.decimals = self.app.decimals
  29. self.units = ''
  30. # #############################################################################
  31. # ######################### Tool GUI ##########################################
  32. # #############################################################################
  33. self.ui = FidoUI(layout=self.layout, app=self.app)
  34. self.toolName = self.ui.toolName
  35. # Objects involved in Copper thieving
  36. self.grb_object = None
  37. self.sm_object = None
  38. self.copper_obj_set = set()
  39. self.sm_obj_set = set()
  40. # store the flattened geometry here:
  41. self.flat_geometry = []
  42. # Events ID
  43. self.mr = None
  44. self.mm = None
  45. # Mouse cursor positions
  46. self.cursor_pos = (0, 0)
  47. self.first_click = False
  48. self.mode_method = False
  49. # Tool properties
  50. self.fid_dia = None
  51. self.sm_opening_dia = None
  52. self.margin_val = None
  53. self.sec_position = None
  54. self.grb_steps_per_circle = self.app.defaults["gerber_circle_steps"]
  55. self.click_points = []
  56. self.handlers_connected = False
  57. # SIGNALS
  58. self.ui.add_cfid_button.clicked.connect(self.add_fiducials)
  59. self.ui.add_sm_opening_button.clicked.connect(self.add_soldermask_opening)
  60. self.ui.fid_type_radio.activated_custom.connect(self.on_fiducial_type)
  61. self.ui.pos_radio.activated_custom.connect(self.on_second_point)
  62. self.ui.mode_radio.activated_custom.connect(self.on_method_change)
  63. self.ui.reset_button.clicked.connect(self.set_tool_ui)
  64. def run(self, toggle=True):
  65. self.app.defaults.report_usage("ToolFiducials()")
  66. if toggle:
  67. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  68. if self.app.ui.splitter.sizes()[0] == 0:
  69. self.app.ui.splitter.setSizes([1, 1])
  70. else:
  71. try:
  72. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  73. # if tab is populated with the tool but it does not have the focus, focus on it
  74. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  75. # focus on Tool Tab
  76. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  77. else:
  78. self.app.ui.splitter.setSizes([0, 1])
  79. except AttributeError:
  80. pass
  81. else:
  82. if self.app.ui.splitter.sizes()[0] == 0:
  83. self.app.ui.splitter.setSizes([1, 1])
  84. AppTool.run(self)
  85. self.set_tool_ui()
  86. self.app.ui.notebook.setTabText(2, _("Fiducials Tool"))
  87. def install(self, icon=None, separator=None, **kwargs):
  88. AppTool.install(self, icon, separator, shortcut='Alt+F', **kwargs)
  89. def set_tool_ui(self):
  90. self.units = self.app.defaults['units']
  91. self.ui.fid_size_entry.set_value(self.app.defaults["tools_fiducials_dia"])
  92. self.ui.margin_entry.set_value(float(self.app.defaults["tools_fiducials_margin"]))
  93. self.ui.mode_radio.set_value(self.app.defaults["tools_fiducials_mode"])
  94. self.ui.pos_radio.set_value(self.app.defaults["tools_fiducials_second_pos"])
  95. self.ui.fid_type_radio.set_value(self.app.defaults["tools_fiducials_type"])
  96. self.ui.line_thickness_entry.set_value(float(self.app.defaults["tools_fiducials_line_thickness"]))
  97. self.click_points = []
  98. self.ui.bottom_left_coords_entry.set_value('')
  99. self.ui.top_right_coords_entry.set_value('')
  100. self.ui.sec_points_coords_entry.set_value('')
  101. self.copper_obj_set = set()
  102. self.sm_obj_set = set()
  103. def on_second_point(self, val):
  104. if val == 'no':
  105. self.ui.id_item_3.setFlags(QtCore.Qt.NoItemFlags)
  106. self.ui.sec_point_coords_lbl.setFlags(QtCore.Qt.NoItemFlags)
  107. self.ui.sec_points_coords_entry.setDisabled(True)
  108. else:
  109. self.ui.id_item_3.setFlags(QtCore.Qt.ItemIsEnabled)
  110. self.ui.sec_point_coords_lbl.setFlags(QtCore.Qt.ItemIsEnabled)
  111. self.ui.sec_points_coords_entry.setDisabled(False)
  112. def on_method_change(self, val):
  113. """
  114. Make sure that on method change we disconnect the event handlers and reset the points storage
  115. :param val: value of the Radio button which trigger this method
  116. :return: None
  117. """
  118. self.click_points = []
  119. if val == 'auto':
  120. try:
  121. self.disconnect_event_handlers()
  122. except TypeError:
  123. pass
  124. def on_fiducial_type(self, val):
  125. if val == 'cross':
  126. self.ui.line_thickness_label.setDisabled(False)
  127. self.ui.line_thickness_entry.setDisabled(False)
  128. else:
  129. self.ui.line_thickness_label.setDisabled(True)
  130. self.ui.line_thickness_entry.setDisabled(True)
  131. def add_fiducials(self):
  132. self.app.call_source = "fiducials_tool"
  133. self.mode_method = self.ui.mode_radio.get_value()
  134. self.margin_val = self.ui.margin_entry.get_value()
  135. self.sec_position = self.ui.pos_radio.get_value()
  136. fid_type = self.ui.fid_type_radio.get_value()
  137. self.click_points = []
  138. # get the Gerber object on which the Fiducial will be inserted
  139. selection_index = self.ui.grb_object_combo.currentIndex()
  140. model_index = self.app.collection.index(selection_index, 0, self.ui.grb_object_combo.rootModelIndex())
  141. try:
  142. self.grb_object = model_index.internalPointer().obj
  143. except Exception as e:
  144. log.debug("ToolFiducials.execute() --> %s" % str(e))
  145. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
  146. return
  147. self.copper_obj_set.add(self.grb_object.options['name'])
  148. if self.mode_method == 'auto':
  149. xmin, ymin, xmax, ymax = self.grb_object.bounds()
  150. bbox = box(xmin, ymin, xmax, ymax)
  151. buf_bbox = bbox.buffer(self.margin_val, self.grb_steps_per_circle, join_style=2)
  152. x0, y0, x1, y1 = buf_bbox.bounds
  153. self.click_points.append(
  154. (
  155. float('%.*f' % (self.decimals, x0)),
  156. float('%.*f' % (self.decimals, y0))
  157. )
  158. )
  159. self.ui.bottom_left_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x0, self.decimals, y0))
  160. self.click_points.append(
  161. (
  162. float('%.*f' % (self.decimals, x1)),
  163. float('%.*f' % (self.decimals, y1))
  164. )
  165. )
  166. self.ui.top_right_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x1, self.decimals, y1))
  167. if self.sec_position == 'up':
  168. self.click_points.append(
  169. (
  170. float('%.*f' % (self.decimals, x0)),
  171. float('%.*f' % (self.decimals, y1))
  172. )
  173. )
  174. self.ui.sec_points_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x0, self.decimals, y1))
  175. elif self.sec_position == 'down':
  176. self.click_points.append(
  177. (
  178. float('%.*f' % (self.decimals, x1)),
  179. float('%.*f' % (self.decimals, y0))
  180. )
  181. )
  182. self.ui.sec_points_coords_entry.set_value('(%.*f, %.*f)' % (self.decimals, x1, self.decimals, y0))
  183. ret_val = self.add_fiducials_geo(self.click_points, g_obj=self.grb_object, fid_type=fid_type)
  184. self.app.call_source = "app"
  185. if ret_val == 'fail':
  186. self.app.call_source = "app"
  187. self.disconnect_event_handlers()
  188. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
  189. return
  190. self.on_exit()
  191. else:
  192. self.app.inform.emit(_("Click to add first Fiducial. Bottom Left..."))
  193. self.ui.bottom_left_coords_entry.set_value('')
  194. self.ui.top_right_coords_entry.set_value('')
  195. self.ui.sec_points_coords_entry.set_value('')
  196. self.connect_event_handlers()
  197. # To be called after clicking on the plot.
  198. def add_fiducials_geo(self, points_list, g_obj, fid_size=None, fid_type=None, line_size=None):
  199. """
  200. Add geometry to the solid_geometry of the copper Gerber object
  201. :param points_list: list of coordinates for the fiducials
  202. :param g_obj: the Gerber object where to add the geometry
  203. :param fid_size: the overall size of the fiducial or fiducial opening depending on the g_obj type
  204. :param fid_type: the type of fiducial: circular or cross
  205. :param line_size: the line thickenss when the fiducial type is cross
  206. :return:
  207. """
  208. fid_size = self.ui.fid_size_entry.get_value() if fid_size is None else fid_size
  209. fid_type = 'circular' if fid_type is None else fid_type
  210. line_thickness = self.ui.line_thickness_entry.get_value() if line_size is None else line_size
  211. radius = fid_size / 2.0
  212. new_apertures = deepcopy(g_obj.apertures)
  213. if fid_type == 'circular':
  214. geo_list = [Point(pt).buffer(radius, self.grb_steps_per_circle) for pt in points_list]
  215. aperture_found = None
  216. for ap_id, ap_val in g_obj.apertures.items():
  217. if ap_val['type'] == 'C' and ap_val['size'] == fid_size:
  218. aperture_found = ap_id
  219. break
  220. if aperture_found:
  221. for geo in geo_list:
  222. dict_el = {'follow': geo.centroid, 'solid': geo}
  223. new_apertures[aperture_found]['geometry'].append(deepcopy(dict_el))
  224. else:
  225. ap_keys = list(g_obj.apertures.keys())
  226. if ap_keys:
  227. new_apid = str(int(max(ap_keys)) + 1)
  228. else:
  229. new_apid = '10'
  230. new_apertures[new_apid] = {
  231. 'type': 'C',
  232. 'size': fid_size,
  233. 'geometry': []
  234. }
  235. for geo in geo_list:
  236. dict_el = {'follow': geo.centroid, 'solid': geo}
  237. new_apertures[new_apid]['geometry'].append(deepcopy(dict_el))
  238. s_list = []
  239. if g_obj.solid_geometry:
  240. try:
  241. for poly in g_obj.solid_geometry:
  242. s_list.append(poly)
  243. except TypeError:
  244. s_list.append(g_obj.solid_geometry)
  245. s_list += geo_list
  246. elif fid_type == 'cross':
  247. geo_list = []
  248. for pt in points_list:
  249. x = pt[0]
  250. y = pt[1]
  251. line_geo_hor = LineString([
  252. (x - radius + (line_thickness / 2.0), y), (x + radius - (line_thickness / 2.0), y)
  253. ])
  254. line_geo_vert = LineString([
  255. (x, y - radius + (line_thickness / 2.0)), (x, y + radius - (line_thickness / 2.0))
  256. ])
  257. geo_list.append([line_geo_hor, line_geo_vert])
  258. aperture_found = None
  259. for ap_id, ap_val in g_obj.apertures.items():
  260. if ap_val['type'] == 'C' and ap_val['size'] == line_thickness:
  261. aperture_found = ap_id
  262. break
  263. geo_buff_list = []
  264. if aperture_found:
  265. for geo in geo_list:
  266. geo_buff_h = geo[0].buffer(line_thickness / 2.0, self.grb_steps_per_circle)
  267. geo_buff_v = geo[1].buffer(line_thickness / 2.0, self.grb_steps_per_circle)
  268. geo_buff_list.append(geo_buff_h)
  269. geo_buff_list.append(geo_buff_v)
  270. dict_el = {'follow': geo_buff_h.centroid, 'solid': geo_buff_h}
  271. new_apertures[aperture_found]['geometry'].append(deepcopy(dict_el))
  272. dict_el = {'follow': geo_buff_v.centroid, 'solid': geo_buff_v}
  273. new_apertures[aperture_found]['geometry'].append(deepcopy(dict_el))
  274. else:
  275. ap_keys = list(g_obj.apertures.keys())
  276. if ap_keys:
  277. new_apid = str(int(max(ap_keys)) + 1)
  278. else:
  279. new_apid = '10'
  280. new_apertures[new_apid] = {
  281. 'type': 'C',
  282. 'size': line_thickness,
  283. 'geometry': []
  284. }
  285. for geo in geo_list:
  286. geo_buff_h = geo[0].buffer(line_thickness / 2.0, self.grb_steps_per_circle)
  287. geo_buff_v = geo[1].buffer(line_thickness / 2.0, self.grb_steps_per_circle)
  288. geo_buff_list.append(geo_buff_h)
  289. geo_buff_list.append(geo_buff_v)
  290. dict_el = {'follow': geo_buff_h.centroid, 'solid': geo_buff_h}
  291. new_apertures[new_apid]['geometry'].append(deepcopy(dict_el))
  292. dict_el = {'follow': geo_buff_v.centroid, 'solid': geo_buff_v}
  293. new_apertures[new_apid]['geometry'].append(deepcopy(dict_el))
  294. s_list = []
  295. if g_obj.solid_geometry:
  296. try:
  297. for poly in g_obj.solid_geometry:
  298. s_list.append(poly)
  299. except TypeError:
  300. s_list.append(g_obj.solid_geometry)
  301. geo_buff_list = MultiPolygon(geo_buff_list)
  302. geo_buff_list = geo_buff_list.buffer(0)
  303. for poly in geo_buff_list:
  304. s_list.append(poly)
  305. else:
  306. # chess pattern fiducial type
  307. geo_list = []
  308. def make_square_poly(center_pt, side_size):
  309. half_s = side_size / 2
  310. x_center = center_pt[0]
  311. y_center = center_pt[1]
  312. pt1 = (x_center - half_s, y_center - half_s)
  313. pt2 = (x_center + half_s, y_center - half_s)
  314. pt3 = (x_center + half_s, y_center + half_s)
  315. pt4 = (x_center - half_s, y_center + half_s)
  316. return Polygon([pt1, pt2, pt3, pt4, pt1])
  317. for pt in points_list:
  318. x = pt[0]
  319. y = pt[1]
  320. first_square = make_square_poly(center_pt=(x-fid_size/4, y+fid_size/4), side_size=fid_size/2)
  321. second_square = make_square_poly(center_pt=(x+fid_size/4, y-fid_size/4), side_size=fid_size/2)
  322. geo_list += [first_square, second_square]
  323. aperture_found = None
  324. new_ap_size = math.sqrt(fid_size**2 + fid_size**2)
  325. for ap_id, ap_val in g_obj.apertures.items():
  326. if ap_val['type'] == 'R' and \
  327. round(ap_val['size'], ndigits=self.decimals) == round(new_ap_size, ndigits=self.decimals):
  328. aperture_found = ap_id
  329. break
  330. geo_buff_list = []
  331. if aperture_found:
  332. for geo in geo_list:
  333. geo_buff_list.append(geo)
  334. dict_el = {'follow': geo.centroid, 'solid': geo}
  335. new_apertures[aperture_found]['geometry'].append(deepcopy(dict_el))
  336. else:
  337. ap_keys = list(g_obj.apertures.keys())
  338. if ap_keys:
  339. new_apid = str(int(max(ap_keys)) + 1)
  340. else:
  341. new_apid = '10'
  342. new_apertures[new_apid] = {
  343. 'type': 'R',
  344. 'size': new_ap_size,
  345. 'width': fid_size,
  346. 'height': fid_size,
  347. 'geometry': []
  348. }
  349. for geo in geo_list:
  350. geo_buff_list.append(geo)
  351. dict_el = {'follow': geo.centroid, 'solid': geo}
  352. new_apertures[new_apid]['geometry'].append(deepcopy(dict_el))
  353. s_list = []
  354. if g_obj.solid_geometry:
  355. try:
  356. for poly in g_obj.solid_geometry:
  357. s_list.append(poly)
  358. except TypeError:
  359. s_list.append(g_obj.solid_geometry)
  360. for poly in geo_buff_list:
  361. s_list.append(poly)
  362. outname = '%s_%s' % (str(g_obj.options['name']), 'fid')
  363. def initialize(grb_obj, app_obj):
  364. grb_obj.options = {}
  365. for opt in g_obj.options:
  366. if opt != 'name':
  367. grb_obj.options[opt] = deepcopy(g_obj.options[opt])
  368. grb_obj.options['name'] = outname
  369. grb_obj.multitool = False
  370. grb_obj.multigeo = False
  371. grb_obj.follow = deepcopy(g_obj.follow)
  372. grb_obj.apertures = new_apertures
  373. grb_obj.solid_geometry = unary_union(s_list)
  374. grb_obj.follow_geometry = deepcopy(g_obj.follow_geometry) + geo_list
  375. grb_obj.source_file = app_obj.f_handlers.export_gerber(obj_name=outname, filename=None, local_use=grb_obj,
  376. use_thread=False)
  377. ret = self.app.app_obj.new_object('gerber', outname, initialize, plot=True)
  378. return ret
  379. def add_soldermask_opening(self):
  380. sm_opening_dia = self.ui.fid_size_entry.get_value() * 2.0
  381. # get the Gerber object on which the Fiducial will be inserted
  382. selection_index = self.ui.sm_object_combo.currentIndex()
  383. model_index = self.app.collection.index(selection_index, 0, self.ui.sm_object_combo.rootModelIndex())
  384. try:
  385. self.sm_object = model_index.internalPointer().obj
  386. except Exception as e:
  387. log.debug("ToolFiducials.add_soldermask_opening() --> %s" % str(e))
  388. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
  389. return
  390. self.sm_obj_set.add(self.sm_object.options['name'])
  391. ret_val = self.add_fiducials_geo(
  392. self.click_points, g_obj=self.sm_object, fid_size=sm_opening_dia, fid_type='circular')
  393. self.app.call_source = "app"
  394. if ret_val == 'fail':
  395. self.app.call_source = "app"
  396. self.disconnect_event_handlers()
  397. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
  398. return
  399. self.on_exit()
  400. def on_mouse_release(self, event):
  401. if event.button == 1:
  402. if self.app.is_legacy is False:
  403. event_pos = event.pos
  404. else:
  405. event_pos = (event.xdata, event.ydata)
  406. pos_canvas = self.canvas.translate_coords(event_pos)
  407. if self.app.grid_status():
  408. pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
  409. else:
  410. pos = (pos_canvas[0], pos_canvas[1])
  411. click_pt = Point([pos[0], pos[1]])
  412. self.click_points.append(
  413. (
  414. float('%.*f' % (self.decimals, click_pt.x)),
  415. float('%.*f' % (self.decimals, click_pt.y))
  416. )
  417. )
  418. self.check_points()
  419. def check_points(self):
  420. fid_type = self.ui.fid_type_radio.get_value()
  421. if len(self.click_points) == 1:
  422. self.ui.bottom_left_coords_entry.set_value(self.click_points[0])
  423. self.app.inform.emit(_("Click to add the last fiducial. Top Right..."))
  424. if self.sec_position != 'no':
  425. if len(self.click_points) == 2:
  426. self.ui.top_right_coords_entry.set_value(self.click_points[1])
  427. self.app.inform.emit(_("Click to add the second fiducial. Top Left or Bottom Right..."))
  428. elif len(self.click_points) == 3:
  429. self.ui.sec_points_coords_entry.set_value(self.click_points[2])
  430. self.app.inform.emit('[success] %s' % _("Done."))
  431. ret_val = self.add_fiducials_geo(self.click_points, g_obj=self.grb_object, fid_type=fid_type)
  432. self.app.call_source = "app"
  433. if ret_val == 'fail':
  434. self.app.call_source = "app"
  435. self.disconnect_event_handlers()
  436. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
  437. return
  438. self.on_exit()
  439. else:
  440. if len(self.click_points) == 2:
  441. self.ui.top_right_coords_entry.set_value(self.click_points[1])
  442. self.app.inform.emit('[success] %s' % _("Done."))
  443. ret_val = self.add_fiducials_geo(self.click_points, g_obj=self.grb_object, fid_type=fid_type)
  444. self.app.call_source = "app"
  445. if ret_val == 'fail':
  446. self.app.call_source = "app"
  447. self.disconnect_event_handlers()
  448. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
  449. return
  450. self.on_exit()
  451. def on_mouse_move(self, event):
  452. pass
  453. def replot(self, obj, run_thread=True):
  454. def worker_task():
  455. with self.app.proc_container.new('%s ...' % _("Plotting")):
  456. obj.plot()
  457. self.app.app_obj.object_plotted.emit(obj)
  458. if run_thread:
  459. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  460. else:
  461. worker_task()
  462. def on_exit(self):
  463. # plot the object
  464. for ob_name in self.copper_obj_set:
  465. try:
  466. copper_obj = self.app.collection.get_by_name(name=ob_name)
  467. if len(self.copper_obj_set) > 1:
  468. self.replot(obj=copper_obj, run_thread=False)
  469. else:
  470. self.replot(obj=copper_obj)
  471. except (AttributeError, TypeError):
  472. continue
  473. # update the bounding box values
  474. try:
  475. a, b, c, d = copper_obj.bounds()
  476. copper_obj.options['xmin'] = a
  477. copper_obj.options['ymin'] = b
  478. copper_obj.options['xmax'] = c
  479. copper_obj.options['ymax'] = d
  480. except Exception as e:
  481. log.debug("ToolFiducials.on_exit() copper_obj bounds error --> %s" % str(e))
  482. for ob_name in self.sm_obj_set:
  483. try:
  484. sm_obj = self.app.collection.get_by_name(name=ob_name)
  485. if len(self.sm_obj_set) > 1:
  486. self.replot(obj=sm_obj, run_thread=False)
  487. else:
  488. self.replot(obj=sm_obj)
  489. except (AttributeError, TypeError):
  490. continue
  491. # update the bounding box values
  492. try:
  493. a, b, c, d = sm_obj.bounds()
  494. sm_obj.options['xmin'] = a
  495. sm_obj.options['ymin'] = b
  496. sm_obj.options['xmax'] = c
  497. sm_obj.options['ymax'] = d
  498. except Exception as e:
  499. log.debug("ToolFiducials.on_exit() sm_obj bounds error --> %s" % str(e))
  500. # Events ID
  501. self.mr = None
  502. # self.mm = None
  503. # Mouse cursor positions
  504. self.cursor_pos = (0, 0)
  505. self.first_click = False
  506. self.disconnect_event_handlers()
  507. self.app.call_source = "app"
  508. self.app.inform.emit('[success] %s' % _("Fiducials Tool exit."))
  509. def connect_event_handlers(self):
  510. if self.handlers_connected is False:
  511. if self.app.is_legacy is False:
  512. self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
  513. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
  514. else:
  515. self.app.plotcanvas.graph_event_disconnect(self.app.mp)
  516. self.app.plotcanvas.graph_event_disconnect(self.app.mr)
  517. self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
  518. self.handlers_connected = True
  519. def disconnect_event_handlers(self):
  520. if self.handlers_connected is True:
  521. if self.app.is_legacy is False:
  522. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
  523. else:
  524. self.app.plotcanvas.graph_event_disconnect(self.mr)
  525. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
  526. self.app.on_mouse_click_over_plot)
  527. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  528. self.app.on_mouse_click_release_over_plot)
  529. self.handlers_connected = False
  530. def flatten(self, geometry):
  531. """
  532. Creates a list of non-iterable linear geometry objects.
  533. :param geometry: Shapely type or list or list of list of such.
  534. Results are placed in self.flat_geometry
  535. """
  536. # ## If iterable, expand recursively.
  537. try:
  538. for geo in geometry:
  539. if geo is not None:
  540. self.flatten(geometry=geo)
  541. # ## Not iterable, do the actual indexing and add.
  542. except TypeError:
  543. self.flat_geometry.append(geometry)
  544. return self.flat_geometry
  545. class FidoUI:
  546. toolName = _("Fiducials Tool")
  547. def __init__(self, layout, app):
  548. self.app = app
  549. self.decimals = self.app.decimals
  550. self.layout = layout
  551. # ## Title
  552. title_label = QtWidgets.QLabel("%s" % self.toolName)
  553. title_label.setStyleSheet("""
  554. QLabel
  555. {
  556. font-size: 16px;
  557. font-weight: bold;
  558. }
  559. """)
  560. self.layout.addWidget(title_label)
  561. self.layout.addWidget(QtWidgets.QLabel(""))
  562. self.points_label = QtWidgets.QLabel('<b>%s:</b>' % _('Fiducials Coordinates'))
  563. self.points_label.setToolTip(
  564. _("A table with the fiducial points coordinates,\n"
  565. "in the format (x, y).")
  566. )
  567. self.layout.addWidget(self.points_label)
  568. self.points_table = FCTable()
  569. self.points_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
  570. self.points_table.setColumnCount(3)
  571. self.points_table.setHorizontalHeaderLabels(
  572. [
  573. '#',
  574. _("Name"),
  575. _("Coordinates"),
  576. ]
  577. )
  578. self.points_table.setRowCount(3)
  579. row = 0
  580. flags = QtCore.Qt.ItemIsEnabled
  581. # BOTTOM LEFT
  582. id_item_1 = QtWidgets.QTableWidgetItem('%d' % 1)
  583. id_item_1.setFlags(flags)
  584. self.points_table.setItem(row, 0, id_item_1) # Tool name/id
  585. self.bottom_left_coords_lbl = QtWidgets.QTableWidgetItem('%s' % _('Bottom Left'))
  586. self.bottom_left_coords_lbl.setFlags(flags)
  587. self.points_table.setItem(row, 1, self.bottom_left_coords_lbl)
  588. self.bottom_left_coords_entry = EvalEntry()
  589. self.points_table.setCellWidget(row, 2, self.bottom_left_coords_entry)
  590. row += 1
  591. # TOP RIGHT
  592. id_item_2 = QtWidgets.QTableWidgetItem('%d' % 2)
  593. id_item_2.setFlags(flags)
  594. self.points_table.setItem(row, 0, id_item_2) # Tool name/id
  595. self.top_right_coords_lbl = QtWidgets.QTableWidgetItem('%s' % _('Top Right'))
  596. self.top_right_coords_lbl.setFlags(flags)
  597. self.points_table.setItem(row, 1, self.top_right_coords_lbl)
  598. self.top_right_coords_entry = EvalEntry()
  599. self.points_table.setCellWidget(row, 2, self.top_right_coords_entry)
  600. row += 1
  601. # Second Point
  602. self.id_item_3 = QtWidgets.QTableWidgetItem('%d' % 3)
  603. self.id_item_3.setFlags(flags)
  604. self.points_table.setItem(row, 0, self.id_item_3) # Tool name/id
  605. self.sec_point_coords_lbl = QtWidgets.QTableWidgetItem('%s' % _('Second Point'))
  606. self.sec_point_coords_lbl.setFlags(flags)
  607. self.points_table.setItem(row, 1, self.sec_point_coords_lbl)
  608. self.sec_points_coords_entry = EvalEntry()
  609. self.points_table.setCellWidget(row, 2, self.sec_points_coords_entry)
  610. vertical_header = self.points_table.verticalHeader()
  611. vertical_header.hide()
  612. self.points_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  613. horizontal_header = self.points_table.horizontalHeader()
  614. horizontal_header.setMinimumSectionSize(10)
  615. horizontal_header.setDefaultSectionSize(70)
  616. self.points_table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
  617. # for x in range(4):
  618. # self.points_table.resizeColumnToContents(x)
  619. self.points_table.resizeColumnsToContents()
  620. self.points_table.resizeRowsToContents()
  621. horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
  622. horizontal_header.resizeSection(0, 20)
  623. horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
  624. horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
  625. self.points_table.setMinimumHeight(self.points_table.getHeight() + 2)
  626. self.points_table.setMaximumHeight(self.points_table.getHeight() + 2)
  627. # remove the frame on the QLineEdit childrens of the table
  628. for row in range(self.points_table.rowCount()):
  629. self.points_table.cellWidget(row, 2).setFrame(False)
  630. self.layout.addWidget(self.points_table)
  631. separator_line = QtWidgets.QFrame()
  632. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  633. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  634. self.layout.addWidget(separator_line)
  635. # ## Grid Layout
  636. grid_lay = QtWidgets.QGridLayout()
  637. self.layout.addLayout(grid_lay)
  638. grid_lay.setColumnStretch(0, 0)
  639. grid_lay.setColumnStretch(1, 1)
  640. self.param_label = QtWidgets.QLabel('<b>%s:</b>' % _('Parameters'))
  641. self.param_label.setToolTip(
  642. _("Parameters used for this tool.")
  643. )
  644. grid_lay.addWidget(self.param_label, 0, 0, 1, 2)
  645. # DIAMETER #
  646. self.size_label = QtWidgets.QLabel('%s:' % _("Size"))
  647. self.size_label.setToolTip(
  648. _("This set the fiducial diameter if fiducial type is circular,\n"
  649. "otherwise is the size of the fiducial.\n"
  650. "The soldermask opening is double than that.")
  651. )
  652. self.fid_size_entry = FCDoubleSpinner(callback=self.confirmation_message)
  653. self.fid_size_entry.set_range(1.0000, 3.0000)
  654. self.fid_size_entry.set_precision(self.decimals)
  655. self.fid_size_entry.setWrapping(True)
  656. self.fid_size_entry.setSingleStep(0.1)
  657. grid_lay.addWidget(self.size_label, 1, 0)
  658. grid_lay.addWidget(self.fid_size_entry, 1, 1)
  659. # MARGIN #
  660. self.margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
  661. self.margin_label.setToolTip(
  662. _("Bounding box margin.")
  663. )
  664. self.margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
  665. self.margin_entry.set_range(-10000.0000, 10000.0000)
  666. self.margin_entry.set_precision(self.decimals)
  667. self.margin_entry.setSingleStep(0.1)
  668. grid_lay.addWidget(self.margin_label, 2, 0)
  669. grid_lay.addWidget(self.margin_entry, 2, 1)
  670. # Mode #
  671. self.mode_radio = RadioSet([
  672. {'label': _('Auto'), 'value': 'auto'},
  673. {"label": _("Manual"), "value": "manual"}
  674. ], stretch=False)
  675. self.mode_label = QtWidgets.QLabel(_("Mode:"))
  676. self.mode_label.setToolTip(
  677. _("- 'Auto' - automatic placement of fiducials in the corners of the bounding box.\n"
  678. "- 'Manual' - manual placement of fiducials.")
  679. )
  680. grid_lay.addWidget(self.mode_label, 3, 0)
  681. grid_lay.addWidget(self.mode_radio, 3, 1)
  682. # Position for second fiducial #
  683. self.pos_radio = RadioSet([
  684. {'label': _('Up'), 'value': 'up'},
  685. {"label": _("Down"), "value": "down"},
  686. {"label": _("None"), "value": "no"}
  687. ], stretch=False)
  688. self.pos_label = QtWidgets.QLabel('%s:' % _("Second fiducial"))
  689. self.pos_label.setToolTip(
  690. _("The position for the second fiducial.\n"
  691. "- 'Up' - the order is: bottom-left, top-left, top-right.\n"
  692. "- 'Down' - the order is: bottom-left, bottom-right, top-right.\n"
  693. "- 'None' - there is no second fiducial. The order is: bottom-left, top-right.")
  694. )
  695. grid_lay.addWidget(self.pos_label, 4, 0)
  696. grid_lay.addWidget(self.pos_radio, 4, 1)
  697. separator_line = QtWidgets.QFrame()
  698. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  699. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  700. grid_lay.addWidget(separator_line, 5, 0, 1, 2)
  701. # Fiducial type #
  702. self.fid_type_radio = RadioSet([
  703. {'label': _('Circular'), 'value': 'circular'},
  704. {"label": _("Cross"), "value": "cross"},
  705. {"label": _("Chess"), "value": "chess"}
  706. ], stretch=False)
  707. self.fid_type_label = QtWidgets.QLabel('%s:' % _("Fiducial Type"))
  708. self.fid_type_label.setToolTip(
  709. _("The type of fiducial.\n"
  710. "- 'Circular' - this is the regular fiducial.\n"
  711. "- 'Cross' - cross lines fiducial.\n"
  712. "- 'Chess' - chess pattern fiducial.")
  713. )
  714. grid_lay.addWidget(self.fid_type_label, 6, 0)
  715. grid_lay.addWidget(self.fid_type_radio, 6, 1)
  716. # Line Thickness #
  717. self.line_thickness_label = QtWidgets.QLabel('%s:' % _("Line thickness"))
  718. self.line_thickness_label.setToolTip(
  719. _("Thickness of the line that makes the fiducial.")
  720. )
  721. self.line_thickness_entry = FCDoubleSpinner(callback=self.confirmation_message)
  722. self.line_thickness_entry.set_range(0.00001, 10000.0000)
  723. self.line_thickness_entry.set_precision(self.decimals)
  724. self.line_thickness_entry.setSingleStep(0.1)
  725. grid_lay.addWidget(self.line_thickness_label, 7, 0)
  726. grid_lay.addWidget(self.line_thickness_entry, 7, 1)
  727. separator_line_1 = QtWidgets.QFrame()
  728. separator_line_1.setFrameShape(QtWidgets.QFrame.HLine)
  729. separator_line_1.setFrameShadow(QtWidgets.QFrame.Sunken)
  730. grid_lay.addWidget(separator_line_1, 8, 0, 1, 2)
  731. # Copper Gerber object
  732. self.grb_object_combo = FCComboBox()
  733. self.grb_object_combo.setModel(self.app.collection)
  734. self.grb_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  735. self.grb_object_combo.is_last = True
  736. self.grb_object_combo.obj_type = "Gerber"
  737. self.grbobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
  738. self.grbobj_label.setToolTip(
  739. _("Gerber Object to which will be added a copper thieving.")
  740. )
  741. grid_lay.addWidget(self.grbobj_label, 9, 0, 1, 2)
  742. grid_lay.addWidget(self.grb_object_combo, 10, 0, 1, 2)
  743. # ## Insert Copper Fiducial
  744. self.add_cfid_button = QtWidgets.QPushButton(_("Add Fiducial"))
  745. self.add_cfid_button.setIcon(QtGui.QIcon(self.app.resource_location + '/fiducials_32.png'))
  746. self.add_cfid_button.setToolTip(
  747. _("Will add a polygon on the copper layer to serve as fiducial.")
  748. )
  749. self.add_cfid_button.setStyleSheet("""
  750. QPushButton
  751. {
  752. font-weight: bold;
  753. }
  754. """)
  755. grid_lay.addWidget(self.add_cfid_button, 11, 0, 1, 2)
  756. separator_line_2 = QtWidgets.QFrame()
  757. separator_line_2.setFrameShape(QtWidgets.QFrame.HLine)
  758. separator_line_2.setFrameShadow(QtWidgets.QFrame.Sunken)
  759. grid_lay.addWidget(separator_line_2, 12, 0, 1, 2)
  760. # Soldermask Gerber object #
  761. self.sm_object_label = QtWidgets.QLabel('<b>%s:</b>' % _("Soldermask Gerber"))
  762. self.sm_object_label.setToolTip(
  763. _("The Soldermask Gerber object.")
  764. )
  765. self.sm_object_combo = FCComboBox()
  766. self.sm_object_combo.setModel(self.app.collection)
  767. self.sm_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  768. self.sm_object_combo.is_last = True
  769. self.sm_object_combo.obj_type = "Gerber"
  770. grid_lay.addWidget(self.sm_object_label, 13, 0, 1, 2)
  771. grid_lay.addWidget(self.sm_object_combo, 14, 0, 1, 2)
  772. # ## Insert Soldermask opening for Fiducial
  773. self.add_sm_opening_button = QtWidgets.QPushButton(_("Add Soldermask Opening"))
  774. self.add_sm_opening_button.setToolTip(
  775. _("Will add a polygon on the soldermask layer\n"
  776. "to serve as fiducial opening.\n"
  777. "The diameter is always double of the diameter\n"
  778. "for the copper fiducial.")
  779. )
  780. self.add_sm_opening_button.setStyleSheet("""
  781. QPushButton
  782. {
  783. font-weight: bold;
  784. }
  785. """)
  786. grid_lay.addWidget(self.add_sm_opening_button, 15, 0, 1, 2)
  787. self.layout.addStretch()
  788. # ## Reset Tool
  789. self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
  790. self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
  791. self.reset_button.setToolTip(
  792. _("Will reset the tool parameters.")
  793. )
  794. self.reset_button.setStyleSheet("""
  795. QPushButton
  796. {
  797. font-weight: bold;
  798. }
  799. """)
  800. self.layout.addWidget(self.reset_button)
  801. # #################################### FINSIHED GUI ###########################
  802. # #############################################################################
  803. def confirmation_message(self, accepted, minval, maxval):
  804. if accepted is False:
  805. self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
  806. self.decimals,
  807. minval,
  808. self.decimals,
  809. maxval), False)
  810. else:
  811. self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
  812. def confirmation_message_int(self, accepted, minval, maxval):
  813. if accepted is False:
  814. self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
  815. (_("Edited value is out of range"), minval, maxval), False)
  816. else:
  817. self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)