ToolSub.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # File Author: Marius Adrian Stanciu (c) #
  4. # Date: 4/24/2019 #
  5. # MIT Licence #
  6. # ##########################################################
  7. from PyQt5 import QtWidgets, QtCore, QtGui
  8. from appTool import AppTool
  9. from appGUI.GUIElements import FCCheckBox, FCButton, FCComboBox
  10. from shapely.geometry import Polygon, MultiPolygon, MultiLineString, LineString
  11. from shapely.ops import unary_union
  12. import traceback
  13. from copy import deepcopy
  14. import time
  15. import logging
  16. import gettext
  17. import appTranslation as fcTranslate
  18. import builtins
  19. fcTranslate.apply_language('strings')
  20. if '_' not in builtins.__dict__:
  21. _ = gettext.gettext
  22. log = logging.getLogger('base')
  23. class ToolSub(AppTool):
  24. job_finished = QtCore.pyqtSignal(bool)
  25. # the string param is the outname and the list is a list of tuples each being formed from the new_aperture_geometry
  26. # list and the second element is also a list with possible geometry that needs to be added to the '0' aperture
  27. # meaning geometry that was deformed
  28. aperture_processing_finished = QtCore.pyqtSignal(str, list)
  29. def __init__(self, app):
  30. self.app = app
  31. self.decimals = self.app.decimals
  32. AppTool.__init__(self, app)
  33. # #############################################################################
  34. # ######################### Tool GUI ##########################################
  35. # #############################################################################
  36. self.ui = SubUI(layout=self.layout, app=self.app)
  37. self.toolName = self.ui.toolName
  38. # QTimer for periodic check
  39. self.check_thread = QtCore.QTimer()
  40. # Every time an intersection job is started we add a promise; every time an intersection job is finished
  41. # we remove a promise.
  42. # When empty we start the layer rendering
  43. self.promises = []
  44. self.new_apertures = {}
  45. self.new_tools = {}
  46. self.new_solid_geometry = []
  47. self.sub_solid_union = None
  48. self.sub_follow_union = None
  49. self.sub_clear_union = None
  50. self.sub_grb_obj = None
  51. self.sub_grb_obj_name = None
  52. self.target_grb_obj = None
  53. self.target_grb_obj_name = None
  54. self.sub_geo_obj = None
  55. self.sub_geo_obj_name = None
  56. self.target_geo_obj = None
  57. self.target_geo_obj_name = None
  58. # signal which type of substraction to do: "geo" or "gerber"
  59. self.sub_type = None
  60. # store here the options from target_obj
  61. self.target_options = {}
  62. self.sub_union = []
  63. # multiprocessing
  64. self.pool = self.app.pool
  65. self.results = []
  66. # Signals
  67. self.ui.intersect_btn.clicked.connect(self.on_subtract_gerber_click)
  68. self.ui.intersect_geo_btn.clicked.connect(self.on_subtract_geo_click)
  69. self.ui.reset_button.clicked.connect(self.set_tool_ui)
  70. # Custom Signals
  71. self.job_finished.connect(self.on_job_finished)
  72. self.aperture_processing_finished.connect(self.new_gerber_object)
  73. def install(self, icon=None, separator=None, **kwargs):
  74. AppTool.install(self, icon, separator, shortcut='Alt+W', **kwargs)
  75. def run(self, toggle=True):
  76. self.app.defaults.report_usage("ToolSub()")
  77. if toggle:
  78. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  79. if self.app.ui.splitter.sizes()[0] == 0:
  80. self.app.ui.splitter.setSizes([1, 1])
  81. else:
  82. try:
  83. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  84. # if tab is populated with the tool but it does not have the focus, focus on it
  85. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  86. # focus on Tool Tab
  87. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  88. else:
  89. self.app.ui.splitter.setSizes([0, 1])
  90. except AttributeError:
  91. pass
  92. else:
  93. if self.app.ui.splitter.sizes()[0] == 0:
  94. self.app.ui.splitter.setSizes([1, 1])
  95. AppTool.run(self)
  96. self.set_tool_ui()
  97. self.app.ui.notebook.setTabText(2, _("Sub Tool"))
  98. def set_tool_ui(self):
  99. self.new_apertures.clear()
  100. self.new_tools.clear()
  101. self.new_solid_geometry = []
  102. self.target_options.clear()
  103. self.ui.tools_frame.show()
  104. self.ui.close_paths_cb.setChecked(self.app.defaults["tools_sub_close_paths"])
  105. self.ui.delete_sources_cb.setChecked(self.app.defaults["tools_sub_delete_sources"])
  106. def on_subtract_gerber_click(self):
  107. # reset previous values
  108. self.new_apertures.clear()
  109. self.new_solid_geometry = []
  110. self.sub_union = []
  111. self.sub_type = "gerber"
  112. # --------------------------------
  113. # Get TARGET name
  114. # --------------------------------
  115. self.target_grb_obj_name = self.ui.target_gerber_combo.currentText()
  116. if self.target_grb_obj_name == '':
  117. self.app.inform.emit('[ERROR_NOTCL] %s' % _("No Target object loaded."))
  118. return
  119. self.app.inform.emit('%s' % _("Loading geometry from Gerber objects."))
  120. # --------------------------------
  121. # Get TARGET object.
  122. # --------------------------------
  123. try:
  124. self.target_grb_obj = self.app.collection.get_by_name(self.target_grb_obj_name)
  125. except Exception as e:
  126. log.debug("ToolSub.on_subtract_gerber_click() --> %s" % str(e))
  127. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), self.obj_name))
  128. return "Could not retrieve object: %s" % self.target_grb_obj_name
  129. # --------------------------------
  130. # Get SUBTRACTOR name
  131. # --------------------------------
  132. self.sub_grb_obj_name = self.ui.sub_gerber_combo.currentText()
  133. if self.sub_grb_obj_name == '':
  134. self.app.inform.emit('[ERROR_NOTCL] %s' % _("No Subtractor object loaded."))
  135. return
  136. # --------------------------------
  137. # Get SUBTRACTOR object.
  138. # --------------------------------
  139. try:
  140. self.sub_grb_obj = self.app.collection.get_by_name(self.sub_grb_obj_name)
  141. except Exception as e:
  142. log.debug("ToolSub.on_subtract_gerber_click() --> %s" % str(e))
  143. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), self.obj_name))
  144. return "Could not retrieve object: %s" % self.sub_grb_obj_name
  145. # --------------------------------
  146. # crate the new_apertures
  147. # dict structure from TARGET apertures
  148. # --------------------------------
  149. for apid in self.target_grb_obj.apertures:
  150. self.new_apertures[apid] = {}
  151. for key in self.target_grb_obj.apertures[apid]:
  152. if key == 'geometry':
  153. self.new_apertures[apid]['geometry'] = []
  154. else:
  155. self.new_apertures[apid][key] = self.target_grb_obj.apertures[apid][key]
  156. def worker_job(app_obj):
  157. # SUBTRACTOR geometry (always the same)
  158. sub_geometry = {'solid': [], 'clear': []}
  159. # iterate over SUBTRACTOR geometry and load it in the sub_geometry dict
  160. for s_apid in app_obj.sub_grb_obj.apertures:
  161. for s_el in app_obj.sub_grb_obj.apertures[s_apid]['geometry']:
  162. if "solid" in s_el:
  163. sub_geometry['solid'].append(s_el["solid"])
  164. if "clear" in s_el:
  165. sub_geometry['clear'].append(s_el["clear"])
  166. for ap_id in app_obj.target_grb_obj.apertures:
  167. # TARGET geometry
  168. target_geo = [geo for geo in app_obj.target_grb_obj.apertures[ap_id]['geometry']]
  169. # send the job to the multiprocessing JOB
  170. app_obj.results.append(
  171. app_obj.pool.apply_async(app_obj.aperture_intersection, args=(ap_id, target_geo, sub_geometry))
  172. )
  173. output = []
  174. for p in app_obj.results:
  175. res = p.get()
  176. output.append(res)
  177. app_obj.app.inform.emit('%s: %s...' % (_("Finished parsing geometry for aperture"), str(res[0])))
  178. app_obj.app.inform.emit("%s" % _("Subtraction aperture processing finished."))
  179. outname = app_obj.ui.target_gerber_combo.currentText() + '_sub'
  180. app_obj.aperture_processing_finished.emit(outname, output)
  181. self.app.worker_task.emit({'fcn': worker_job, 'params': [self]})
  182. @staticmethod
  183. def aperture_intersection(apid, target_geo, sub_geometry):
  184. """
  185. :param apid: the aperture id for which we process geometry
  186. :type apid: str
  187. :param target_geo: the geometry list that holds the geometry from which we subtract
  188. :type target_geo: list
  189. :param sub_geometry: the apertures dict that holds all the geometry that is subtracted
  190. :type sub_geometry: dict
  191. :return: (apid, unaffected_geometry lsit, affected_geometry list)
  192. :rtype: tuple
  193. """
  194. unafected_geo = []
  195. affected_geo = []
  196. for target_geo_obj in target_geo:
  197. solid_is_modified = False
  198. destination_geo_obj = {}
  199. if "solid" in target_geo_obj:
  200. diff = []
  201. for sub_solid_geo in sub_geometry["solid"]:
  202. if target_geo_obj["solid"].intersects(sub_solid_geo):
  203. new_geo = target_geo_obj["solid"].difference(sub_solid_geo)
  204. if not new_geo.is_empty:
  205. diff.append(new_geo)
  206. solid_is_modified = True
  207. if solid_is_modified:
  208. target_geo_obj["solid"] = unary_union(diff)
  209. destination_geo_obj["solid"] = deepcopy(target_geo_obj["solid"])
  210. clear_is_modified = False
  211. if "clear" in target_geo_obj:
  212. clear_diff = []
  213. for sub_clear_geo in sub_geometry["clear"]:
  214. if target_geo_obj["clear"].intersects(sub_clear_geo):
  215. new_geo = target_geo_obj["clear"].difference(sub_clear_geo)
  216. if not new_geo.is_empty:
  217. clear_diff.append(new_geo)
  218. clear_is_modified = True
  219. if clear_is_modified:
  220. target_geo_obj["clear"] = unary_union(clear_diff)
  221. destination_geo_obj["clear"] = deepcopy(target_geo_obj["clear"])
  222. if solid_is_modified or clear_is_modified:
  223. affected_geo.append(deepcopy(destination_geo_obj))
  224. else:
  225. unafected_geo.append(deepcopy(destination_geo_obj))
  226. return apid, unafected_geo, affected_geo
  227. def new_gerber_object(self, outname, output):
  228. """
  229. :param outname: name for the new Gerber object
  230. :type outname: str
  231. :param output: a list made of tuples in format:
  232. (aperture id in the target Gerber, unaffected_geometry list, affected_geometry list)
  233. :type output: list
  234. :return:
  235. :rtype:
  236. """
  237. def obj_init(grb_obj, app_obj):
  238. grb_obj.apertures = deepcopy(self.new_apertures)
  239. if '0' not in grb_obj.apertures:
  240. grb_obj.apertures['0'] = {}
  241. grb_obj.apertures['0']['type'] = 'REG'
  242. grb_obj.apertures['0']['size'] = 0.0
  243. grb_obj.apertures['0']['geometry'] = []
  244. for apid in list(grb_obj.apertures.keys()):
  245. # output is a tuple in the format (apid, surviving_geo, modified_geo)
  246. # apid is the aperture id (key in the obj.apertures and string)
  247. # unaffected_geo and affected_geo are lists
  248. for t in output:
  249. new_apid = t[0]
  250. if apid == new_apid:
  251. surving_geo = t[1]
  252. modified_geo = t[2]
  253. if surving_geo:
  254. grb_obj.apertures[apid]['geometry'] += deepcopy(surving_geo)
  255. if modified_geo:
  256. grb_obj.apertures['0']['geometry'] += modified_geo
  257. # if the current aperture does not have geometry then get rid of it
  258. if not grb_obj.apertures[apid]['geometry']:
  259. grb_obj.apertures.pop(apid, None)
  260. # delete the '0' aperture if it has no geometry
  261. if not grb_obj.apertures['0']['geometry']:
  262. grb_obj.apertures.pop('0', None)
  263. poly_buff = []
  264. follow_buff = []
  265. for ap in grb_obj.apertures:
  266. for elem in grb_obj.apertures[ap]['geometry']:
  267. if 'solid' in elem:
  268. solid_geo = elem['solid']
  269. poly_buff.append(solid_geo)
  270. if 'follow' in elem:
  271. follow_buff.append(elem['follow'])
  272. work_poly_buff = MultiPolygon(poly_buff)
  273. try:
  274. poly_buff = work_poly_buff.buffer(0.0000001)
  275. except ValueError:
  276. pass
  277. try:
  278. poly_buff = poly_buff.buffer(-0.0000001)
  279. except ValueError:
  280. pass
  281. grb_obj.solid_geometry = deepcopy(poly_buff)
  282. grb_obj.follow_geometry = deepcopy(follow_buff)
  283. grb_obj.source_file = app_obj.f_handlers.export_gerber(obj_name=outname, filename=None,
  284. local_use=grb_obj, use_thread=False)
  285. with self.app.proc_container.new(_("Generating new object ...")):
  286. ret = self.app.app_obj.new_object('gerber', outname, obj_init, autoselected=False)
  287. if ret == 'fail':
  288. self.app.inform.emit('[ERROR_NOTCL] %s' % _('Generating new object failed.'))
  289. return
  290. # GUI feedback
  291. self.app.inform.emit('[success] %s: %s' % (_("Created"), outname))
  292. # Delete source objects if it was selected
  293. if self.ui.delete_sources_cb.get_value():
  294. self.app.collection.delete_by_name(self.target_grb_obj_name)
  295. self.app.collection.delete_by_name(self.sub_grb_obj_name)
  296. # cleanup
  297. self.new_apertures.clear()
  298. self.new_solid_geometry[:] = []
  299. self.results = []
  300. def on_subtract_geo_click(self):
  301. # reset previous values
  302. self.new_tools.clear()
  303. self.target_options.clear()
  304. self.new_solid_geometry = []
  305. self.sub_union = []
  306. self.sub_type = "geo"
  307. self.target_geo_obj_name = self.ui.target_geo_combo.currentText()
  308. if self.target_geo_obj_name == '':
  309. self.app.inform.emit('[ERROR_NOTCL] %s' % _("No Target object loaded."))
  310. return
  311. # Get target object.
  312. try:
  313. self.target_geo_obj = self.app.collection.get_by_name(self.target_geo_obj_name)
  314. except Exception as e:
  315. log.debug("ToolSub.on_subtract_geo_click() --> %s" % str(e))
  316. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), self.target_geo_obj_name))
  317. return "Could not retrieve object: %s" % self.target_grb_obj_name
  318. self.sub_geo_obj_name = self.ui.sub_geo_combo.currentText()
  319. if self.sub_geo_obj_name == '':
  320. self.app.inform.emit('[ERROR_NOTCL] %s' % _("No Subtractor object loaded."))
  321. return
  322. # Get substractor object.
  323. try:
  324. self.sub_geo_obj = self.app.collection.get_by_name(self.sub_geo_obj_name)
  325. except Exception as e:
  326. log.debug("ToolSub.on_subtract_geo_click() --> %s" % str(e))
  327. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), self.sub_geo_obj_name))
  328. return "Could not retrieve object: %s" % self.sub_geo_obj_name
  329. if self.sub_geo_obj.multigeo:
  330. self.app.inform.emit('[ERROR_NOTCL] %s' %
  331. _("Currently, the Subtractor geometry cannot be of type Multigeo."))
  332. return
  333. # create the target_options obj
  334. # self.target_options = {}
  335. # for k, v in self.target_geo_obj.options.items():
  336. # if k != 'name':
  337. # self.target_options[k] = v
  338. # crate the new_tools dict structure
  339. for tool in self.target_geo_obj.tools:
  340. self.new_tools[tool] = {}
  341. for key, v in self.target_geo_obj.tools[tool]:
  342. self.new_tools[tool][key] = [] if key == 'solid_geometry' else deepcopy(v)
  343. # add the promises
  344. if self.target_geo_obj.multigeo:
  345. for tool in self.target_geo_obj.tools:
  346. self.promises.append(tool)
  347. else:
  348. self.promises.append("single")
  349. self.sub_union = unary_union(self.sub_geo_obj.solid_geometry)
  350. # start the QTimer to check for promises with 0.5 second period check
  351. self.periodic_check(500, reset=True)
  352. if self.target_geo_obj.multigeo:
  353. for tool in self.target_geo_obj.tools:
  354. geo = self.target_geo_obj.tools[tool]['solid_geometry']
  355. self.app.worker_task.emit({'fcn': self.toolgeo_intersection, 'params': [tool, geo]})
  356. else:
  357. geo = self.target_geo_obj.solid_geometry
  358. self.app.worker_task.emit({'fcn': self.toolgeo_intersection, 'params': ["single", geo]})
  359. def toolgeo_intersection(self, tool, geo):
  360. new_geometry = []
  361. log.debug("Working on promise: %s" % str(tool))
  362. if tool == "single":
  363. text = _("Parsing solid_geometry ...")
  364. else:
  365. text = '%s: %s...' % (_("Parsing solid_geometry for tool"), str(tool))
  366. with self.app.proc_container.new(text):
  367. # resulting paths are closed resulting into Polygons
  368. if self.ui.close_paths_cb.isChecked():
  369. new_geo = (unary_union(geo)).difference(self.sub_union)
  370. if new_geo:
  371. if not new_geo.is_empty:
  372. new_geometry.append(new_geo)
  373. # resulting paths are unclosed resulting in a multitude of rings
  374. else:
  375. try:
  376. for geo_elem in geo:
  377. if isinstance(geo_elem, Polygon):
  378. for ring in self.poly2rings(geo_elem):
  379. new_geo = ring.difference(self.sub_union)
  380. if new_geo and not new_geo.is_empty:
  381. new_geometry.append(new_geo)
  382. elif isinstance(geo_elem, MultiPolygon):
  383. for poly in geo_elem:
  384. for ring in self.poly2rings(poly):
  385. new_geo = ring.difference(self.sub_union)
  386. if new_geo and not new_geo.is_empty:
  387. new_geometry.append(new_geo)
  388. elif isinstance(geo_elem, LineString):
  389. new_geo = geo_elem.difference(self.sub_union)
  390. if new_geo:
  391. if not new_geo.is_empty:
  392. new_geometry.append(new_geo)
  393. elif isinstance(geo_elem, MultiLineString):
  394. for line_elem in geo_elem:
  395. new_geo = line_elem.difference(self.sub_union)
  396. if new_geo and not new_geo.is_empty:
  397. new_geometry.append(new_geo)
  398. except TypeError:
  399. if isinstance(geo, Polygon):
  400. for ring in self.poly2rings(geo):
  401. new_geo = ring.difference(self.sub_union)
  402. if new_geo:
  403. if not new_geo.is_empty:
  404. new_geometry.append(new_geo)
  405. elif isinstance(geo, LineString):
  406. new_geo = geo.difference(self.sub_union)
  407. if new_geo and not new_geo.is_empty:
  408. new_geometry.append(new_geo)
  409. elif isinstance(geo, MultiLineString):
  410. for line_elem in geo:
  411. new_geo = line_elem.difference(self.sub_union)
  412. if new_geo and not new_geo.is_empty:
  413. new_geometry.append(new_geo)
  414. if new_geometry:
  415. if tool == "single":
  416. while not self.new_solid_geometry:
  417. self.new_solid_geometry = deepcopy(new_geometry)
  418. time.sleep(0.5)
  419. else:
  420. while not self.new_tools[tool]['solid_geometry']:
  421. self.new_tools[tool]['solid_geometry'] = deepcopy(new_geometry)
  422. time.sleep(0.5)
  423. while True:
  424. # removal from list is done in a multithreaded way therefore not always the removal can be done
  425. # so we keep trying until it's done
  426. if tool not in self.promises:
  427. break
  428. self.promises.remove(tool)
  429. time.sleep(0.5)
  430. log.debug("Promise fulfilled: %s" % str(tool))
  431. def new_geo_object(self, outname):
  432. geo_name = outname
  433. def obj_init(geo_obj, app_obj):
  434. # geo_obj.options = self.target_options
  435. # create the target_options obj
  436. for k, v in self.target_geo_obj.options.items():
  437. geo_obj.options[k] = v
  438. geo_obj.options['name'] = geo_name
  439. if self.target_geo_obj.multigeo:
  440. geo_obj.tools = deepcopy(self.new_tools)
  441. # this turn on the FlatCAMCNCJob plot for multiple tools
  442. geo_obj.multigeo = True
  443. geo_obj.multitool = True
  444. else:
  445. geo_obj.solid_geometry = deepcopy(self.new_solid_geometry)
  446. try:
  447. geo_obj.tools = deepcopy(self.new_tools)
  448. for tool in geo_obj.tools:
  449. geo_obj.tools[tool]['solid_geometry'] = deepcopy(self.new_solid_geometry)
  450. except Exception as e:
  451. app_obj.log.debug("ToolSub.new_geo_object() --> %s" % str(e))
  452. geo_obj.multigeo = False
  453. with self.app.proc_container.new(_("Generating new object ...")):
  454. ret = self.app.app_obj.new_object('geometry', outname, obj_init, autoselected=False)
  455. if ret == 'fail':
  456. self.app.inform.emit('[ERROR_NOTCL] %s' % _('Generating new object failed.'))
  457. return
  458. # Register recent file
  459. self.app.file_opened.emit('geometry', outname)
  460. # GUI feedback
  461. self.app.inform.emit('[success] %s: %s' % (_("Created"), outname))
  462. # Delete source objects if it was selected
  463. if self.ui.delete_sources_cb.get_value():
  464. self.app.collection.delete_by_name(self.target_geo_obj_name)
  465. self.app.collection.delete_by_name(self.sub_geo_obj_name)
  466. # cleanup
  467. self.new_tools.clear()
  468. self.new_solid_geometry[:] = []
  469. self.sub_union = []
  470. def periodic_check(self, check_period, reset=False):
  471. """
  472. This function starts an QTimer and it will periodically check if intersections are done
  473. :param check_period: time at which to check periodically
  474. :param reset: will reset the timer
  475. :return:
  476. """
  477. log.debug("ToolSub --> Periodic Check started.")
  478. try:
  479. self.check_thread.stop()
  480. except (TypeError, AttributeError):
  481. pass
  482. if reset:
  483. self.check_thread.setInterval(check_period)
  484. try:
  485. self.check_thread.timeout.disconnect(self.periodic_check_handler)
  486. except (TypeError, AttributeError):
  487. pass
  488. self.check_thread.timeout.connect(self.periodic_check_handler)
  489. self.check_thread.start(QtCore.QThread.HighPriority)
  490. def periodic_check_handler(self):
  491. """
  492. If the intersections workers finished then start creating the solid_geometry
  493. :return:
  494. """
  495. # log.debug("checking parsing --> %s" % str(self.parsing_promises))
  496. try:
  497. if not self.promises:
  498. self.check_thread.stop()
  499. self.job_finished.emit(True)
  500. # reset the type of substraction for next time
  501. self.sub_type = None
  502. log.debug("ToolSub --> Periodic check finished.")
  503. except Exception as e:
  504. self.job_finished.emit(False)
  505. log.debug("ToolSub().periodic_check_handler() --> %s" % str(e))
  506. traceback.print_exc()
  507. def on_job_finished(self, succcess):
  508. """
  509. :param succcess: boolean, this parameter signal if all the apertures were processed
  510. :return: None
  511. """
  512. if succcess is True:
  513. if self.sub_type == "gerber":
  514. outname = self.ui.target_gerber_combo.currentText() + '_sub'
  515. # intersection jobs finished, start the creation of solid_geometry
  516. self.app.worker_task.emit({'fcn': self.new_gerber_object, 'params': [outname]})
  517. else:
  518. outname = self.ui.target_geo_combo.currentText() + '_sub'
  519. # intersection jobs finished, start the creation of solid_geometry
  520. self.app.worker_task.emit({'fcn': self.new_geo_object, 'params': [outname]})
  521. else:
  522. self.app.inform.emit('[ERROR_NOTCL] %s' % _('Generating new object failed.'))
  523. def reset_fields(self):
  524. self.ui.target_gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  525. self.ui.sub_gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  526. self.ui.target_geo_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
  527. self.ui.sub_geo_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
  528. @staticmethod
  529. def poly2rings(poly):
  530. return [poly.exterior] + [interior for interior in poly.interiors]
  531. class SubUI:
  532. toolName = _("Subtract Tool")
  533. def __init__(self, layout, app):
  534. self.app = app
  535. self.decimals = self.app.decimals
  536. self.layout = layout
  537. # ## Title
  538. title_label = QtWidgets.QLabel("%s" % self.toolName)
  539. title_label.setStyleSheet("""
  540. QLabel
  541. {
  542. font-size: 16px;
  543. font-weight: bold;
  544. }
  545. """)
  546. self.layout.addWidget(title_label)
  547. self.layout.addWidget(QtWidgets.QLabel(""))
  548. self.tools_frame = QtWidgets.QFrame()
  549. self.tools_frame.setContentsMargins(0, 0, 0, 0)
  550. self.layout.addWidget(self.tools_frame)
  551. self.tools_box = QtWidgets.QVBoxLayout()
  552. self.tools_box.setContentsMargins(0, 0, 0, 0)
  553. self.tools_frame.setLayout(self.tools_box)
  554. # Form Layout
  555. grid0 = QtWidgets.QGridLayout()
  556. grid0.setColumnStretch(0, 0)
  557. grid0.setColumnStretch(1, 1)
  558. self.tools_box.addLayout(grid0)
  559. self.delete_sources_cb = FCCheckBox(_("Delete source"))
  560. self.delete_sources_cb.setToolTip(
  561. _("When checked will delete the source objects\n"
  562. "after a successful operation.")
  563. )
  564. grid0.addWidget(self.delete_sources_cb, 0, 0, 1, 2)
  565. separator_line = QtWidgets.QFrame()
  566. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  567. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  568. grid0.addWidget(separator_line, 2, 0, 1, 3)
  569. grid0.addWidget(QtWidgets.QLabel(''), 4, 0, 1, 2)
  570. self.gerber_title = QtWidgets.QLabel("<b>%s</b>" % _("GERBER"))
  571. grid0.addWidget(self.gerber_title, 6, 0, 1, 2)
  572. # Target Gerber Object
  573. self.target_gerber_combo = FCComboBox()
  574. self.target_gerber_combo.setModel(self.app.collection)
  575. self.target_gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  576. # self.target_gerber_combo.setCurrentIndex(1)
  577. self.target_gerber_combo.is_last = True
  578. self.target_gerber_combo.obj_type = "Gerber"
  579. self.target_gerber_label = QtWidgets.QLabel('%s:' % _("Target"))
  580. self.target_gerber_label.setToolTip(
  581. _("Gerber object from which to subtract\n"
  582. "the subtractor Gerber object.")
  583. )
  584. grid0.addWidget(self.target_gerber_label, 8, 0)
  585. grid0.addWidget(self.target_gerber_combo, 8, 1)
  586. # Substractor Gerber Object
  587. self.sub_gerber_combo = FCComboBox()
  588. self.sub_gerber_combo.setModel(self.app.collection)
  589. self.sub_gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  590. self.sub_gerber_combo.is_last = True
  591. self.sub_gerber_combo.obj_type = "Gerber"
  592. self.sub_gerber_label = QtWidgets.QLabel('%s:' % _("Subtractor"))
  593. self.sub_gerber_label.setToolTip(
  594. _("Gerber object that will be subtracted\n"
  595. "from the target Gerber object.")
  596. )
  597. grid0.addWidget(self.sub_gerber_label, 10, 0)
  598. grid0.addWidget(self.sub_gerber_combo, 10, 1)
  599. self.intersect_btn = FCButton(_('Subtract Gerber'))
  600. self.intersect_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/subtract_btn32.png'))
  601. self.intersect_btn.setToolTip(
  602. _("Will remove the area occupied by the subtractor\n"
  603. "Gerber from the Target Gerber.\n"
  604. "Can be used to remove the overlapping silkscreen\n"
  605. "over the soldermask.")
  606. )
  607. self.intersect_btn.setStyleSheet("""
  608. QPushButton
  609. {
  610. font-weight: bold;
  611. }
  612. """)
  613. grid0.addWidget(self.intersect_btn, 12, 0, 1, 2)
  614. grid0.addWidget(QtWidgets.QLabel(''), 14, 0, 1, 2)
  615. self.geo_title = QtWidgets.QLabel("<b>%s</b>" % _("GEOMETRY"))
  616. grid0.addWidget(self.geo_title, 16, 0, 1, 2)
  617. # Target Geometry Object
  618. self.target_geo_combo = FCComboBox()
  619. self.target_geo_combo.setModel(self.app.collection)
  620. self.target_geo_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
  621. # self.target_geo_combo.setCurrentIndex(1)
  622. self.target_geo_combo.is_last = True
  623. self.target_geo_combo.obj_type = "Geometry"
  624. self.target_geo_label = QtWidgets.QLabel('%s:' % _("Target"))
  625. self.target_geo_label.setToolTip(
  626. _("Geometry object from which to subtract\n"
  627. "the subtractor Geometry object.")
  628. )
  629. grid0.addWidget(self.target_geo_label, 18, 0)
  630. grid0.addWidget(self.target_geo_combo, 18, 1)
  631. # Substractor Geometry Object
  632. self.sub_geo_combo = FCComboBox()
  633. self.sub_geo_combo.setModel(self.app.collection)
  634. self.sub_geo_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
  635. self.sub_geo_combo.is_last = True
  636. self.sub_geo_combo.obj_type = "Geometry"
  637. self.sub_geo_label = QtWidgets.QLabel('%s:' % _("Subtractor"))
  638. self.sub_geo_label.setToolTip(
  639. _("Geometry object that will be subtracted\n"
  640. "from the target Geometry object.")
  641. )
  642. grid0.addWidget(self.sub_geo_label, 20, 0)
  643. grid0.addWidget(self.sub_geo_combo, 20, 1)
  644. self.close_paths_cb = FCCheckBox(_("Close paths"))
  645. self.close_paths_cb.setToolTip(_("Checking this will close the paths cut by the Geometry subtractor object."))
  646. grid0.addWidget(self.close_paths_cb, 22, 0, 1, 2)
  647. self.intersect_geo_btn = FCButton(_('Subtract Geometry'))
  648. self.intersect_geo_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/subtract_btn32.png'))
  649. self.intersect_geo_btn.setToolTip(
  650. _("Will remove the area occupied by the subtractor\n"
  651. "Geometry from the Target Geometry.")
  652. )
  653. self.intersect_geo_btn.setStyleSheet("""
  654. QPushButton
  655. {
  656. font-weight: bold;
  657. }
  658. """)
  659. grid0.addWidget(self.intersect_geo_btn, 24, 0, 1, 2)
  660. grid0.addWidget(QtWidgets.QLabel(''), 26, 0, 1, 2)
  661. self.tools_box.addStretch()
  662. # ## Reset Tool
  663. self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
  664. self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
  665. self.reset_button.setToolTip(
  666. _("Will reset the tool parameters.")
  667. )
  668. self.reset_button.setStyleSheet("""
  669. QPushButton
  670. {
  671. font-weight: bold;
  672. }
  673. """)
  674. self.tools_box.addWidget(self.reset_button)
  675. # #################################### FINSIHED GUI ###########################
  676. # #############################################################################
  677. def confirmation_message(self, accepted, minval, maxval):
  678. if accepted is False:
  679. self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
  680. self.decimals,
  681. minval,
  682. self.decimals,
  683. maxval), False)
  684. else:
  685. self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
  686. def confirmation_message_int(self, accepted, minval, maxval):
  687. if accepted is False:
  688. self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
  689. (_("Edited value is out of range"), minval, maxval), False)
  690. else:
  691. self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)