ToolFiducials.py 32 KB

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