ToolFiducials.py 40 KB

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