ToolFiducials.py 36 KB

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