ToolCutOut.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. from FlatCAMTool import FlatCAMTool
  2. from copy import copy,deepcopy
  3. from ObjectCollection import *
  4. from FlatCAMApp import *
  5. from PyQt5 import QtGui, QtCore, QtWidgets
  6. from GUIElements import IntEntry, RadioSet, LengthEntry, FloatEntry
  7. from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber
  8. class ToolCutOut(FlatCAMTool):
  9. toolName = "Cutout PCB"
  10. def __init__(self, app):
  11. FlatCAMTool.__init__(self, app)
  12. ## Title
  13. title_label = QtWidgets.QLabel("%s" % self.toolName)
  14. title_label.setStyleSheet("""
  15. QLabel
  16. {
  17. font-size: 16px;
  18. font-weight: bold;
  19. }
  20. """)
  21. self.layout.addWidget(title_label)
  22. ## Form Layout
  23. form_layout = QtWidgets.QFormLayout()
  24. self.layout.addLayout(form_layout)
  25. ## Type of object to be cutout
  26. self.type_obj_combo = QtWidgets.QComboBox()
  27. self.type_obj_combo.addItem("Gerber")
  28. self.type_obj_combo.addItem("Excellon")
  29. self.type_obj_combo.addItem("Geometry")
  30. # we get rid of item1 ("Excellon") as it is not suitable for creating film
  31. self.type_obj_combo.view().setRowHidden(1, True)
  32. self.type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
  33. # self.type_obj_combo.setItemIcon(1, QtGui.QIcon("share/drill16.png"))
  34. self.type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
  35. self.type_obj_combo_label = QtWidgets.QLabel("Object Type:")
  36. self.type_obj_combo_label.setToolTip(
  37. "Specify the type of object to be cutout.\n"
  38. "It can be of type: Gerber or Geometry.\n"
  39. "What is selected here will dictate the kind\n"
  40. "of objects that will populate the 'Object' combobox."
  41. )
  42. form_layout.addRow(self.type_obj_combo_label, self.type_obj_combo)
  43. ## Object to be cutout
  44. self.obj_combo = QtWidgets.QComboBox()
  45. self.obj_combo.setModel(self.app.collection)
  46. self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  47. self.obj_combo.setCurrentIndex(1)
  48. self.object_label = QtWidgets.QLabel("Object:")
  49. self.object_label.setToolTip(
  50. "Object to be cutout. "
  51. )
  52. form_layout.addRow(self.object_label, self.obj_combo)
  53. # Tool Diameter
  54. self.dia = FCEntry()
  55. self.dia_label = QtWidgets.QLabel("Tool Dia:")
  56. self.dia_label.setToolTip(
  57. "Diameter of the tool used to cutout\n"
  58. "the PCB shape out of the surrounding material."
  59. )
  60. form_layout.addRow(self.dia_label, self.dia)
  61. # Margin
  62. self.margin = FCEntry()
  63. self.margin_label = QtWidgets.QLabel("Margin:")
  64. self.margin_label.setToolTip(
  65. "Margin over bounds. A positive value here\n"
  66. "will make the cutout of the PCB further from\n"
  67. "the actual PCB border"
  68. )
  69. form_layout.addRow(self.margin_label, self.margin)
  70. # Gapsize
  71. self.gapsize = FCEntry()
  72. self.gapsize_label = QtWidgets.QLabel("Gap size:")
  73. self.gapsize_label.setToolTip(
  74. "The size of the gaps in the cutout\n"
  75. "used to keep the board connected to\n"
  76. "the surrounding material (the one \n"
  77. "from which the PCB is cutout)."
  78. )
  79. form_layout.addRow(self.gapsize_label, self.gapsize)
  80. ## Title2
  81. title_ff_label = QtWidgets.QLabel("<font size=4><b>FreeForm Cutout</b></font>")
  82. self.layout.addWidget(title_ff_label)
  83. ## Form Layout
  84. form_layout_2 = QtWidgets.QFormLayout()
  85. self.layout.addLayout(form_layout_2)
  86. # How gaps wil be rendered:
  87. # lr - left + right
  88. # tb - top + bottom
  89. # 4 - left + right +top + bottom
  90. # 2lr - 2*left + 2*right
  91. # 2tb - 2*top + 2*bottom
  92. # 8 - 2*left + 2*right +2*top + 2*bottom
  93. # Gaps
  94. gaps_ff_label = QtWidgets.QLabel('Gaps FF: ')
  95. gaps_ff_label.setToolTip(
  96. "Number of gaps used for the FreeForm cutout.\n"
  97. "There can be maximum 8 bridges/gaps.\n"
  98. "The choices are:\n"
  99. "- lr - left + right\n"
  100. "- tb - top + bottom\n"
  101. "- 4 - left + right +top + bottom\n"
  102. "- 2lr - 2*left + 2*right\n"
  103. "- 2tb - 2*top + 2*bottom\n"
  104. "- 8 - 2*left + 2*right +2*top + 2*bottom"
  105. )
  106. self.gaps = FCComboBox()
  107. gaps_items = ['LR', 'TB', '4', '2LR', '2TB', '8']
  108. for it in gaps_items:
  109. self.gaps.addItem(it)
  110. self.gaps.setStyleSheet('background-color: rgb(255,255,255)')
  111. form_layout_2.addRow(gaps_ff_label, self.gaps)
  112. ## Buttons
  113. hlay = QtWidgets.QHBoxLayout()
  114. self.layout.addLayout(hlay)
  115. hlay.addStretch()
  116. self.ff_cutout_object_btn = QtWidgets.QPushButton(" FreeForm Cutout Object ")
  117. self.ff_cutout_object_btn.setToolTip(
  118. "Cutout the selected object.\n"
  119. "The cutout shape can be any shape.\n"
  120. "Useful when the PCB has a non-rectangular shape.\n"
  121. "But if the object to be cutout is of Gerber Type,\n"
  122. "it needs to be an outline of the actual board shape."
  123. )
  124. hlay.addWidget(self.ff_cutout_object_btn)
  125. ## Title3
  126. title_rct_label = QtWidgets.QLabel("<font size=4><b>Rectangular Cutout</b></font>")
  127. self.layout.addWidget(title_rct_label)
  128. ## Form Layout
  129. form_layout_3 = QtWidgets.QFormLayout()
  130. self.layout.addLayout(form_layout_3)
  131. gapslabel_rect = QtWidgets.QLabel('Type of gaps:')
  132. gapslabel_rect.setToolTip(
  133. "Where to place the gaps:\n"
  134. "- one gap Top / one gap Bottom\n"
  135. "- one gap Left / one gap Right\n"
  136. "- one gap on each of the 4 sides."
  137. )
  138. self.gaps_rect_radio = RadioSet([{'label': '2(T/B)', 'value': 'TB'},
  139. {'label': '2(L/R)', 'value': 'LR'},
  140. {'label': '4', 'value': '4'}])
  141. form_layout_3.addRow(gapslabel_rect, self.gaps_rect_radio)
  142. hlay2 = QtWidgets.QHBoxLayout()
  143. self.layout.addLayout(hlay2)
  144. hlay2.addStretch()
  145. self.rect_cutout_object_btn = QtWidgets.QPushButton("Rectangular Cutout Object")
  146. self.rect_cutout_object_btn.setToolTip(
  147. "Cutout the selected object.\n"
  148. "The resulting cutout shape is\n"
  149. "always of a rectangle form and it will be\n"
  150. "the bounding box of the Object."
  151. )
  152. hlay2.addWidget(self.rect_cutout_object_btn)
  153. self.layout.addStretch()
  154. ## Init GUI
  155. # self.dia.set_value(1)
  156. # self.margin.set_value(0)
  157. # self.gapsize.set_value(1)
  158. # self.gaps.set_value(4)
  159. # self.gaps_rect_radio.set_value("4")
  160. ## Signals
  161. self.ff_cutout_object_btn.clicked.connect(self.on_freeform_cutout)
  162. self.rect_cutout_object_btn.clicked.connect(self.on_rectangular_cutout)
  163. self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
  164. def on_type_obj_index_changed(self, index):
  165. obj_type = self.type_obj_combo.currentIndex()
  166. self.obj_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
  167. self.obj_combo.setCurrentIndex(0)
  168. def run(self):
  169. self.app.report_usage("ToolCutOut()")
  170. FlatCAMTool.run(self)
  171. self.set_tool_ui()
  172. # if the splitter us hidden, display it
  173. if self.app.ui.splitter.sizes()[0] == 0:
  174. self.app.ui.splitter.setSizes([1, 1])
  175. self.app.ui.notebook.setTabText(2, "Cutout Tool")
  176. def install(self, icon=None, separator=None, **kwargs):
  177. FlatCAMTool.install(self, icon, separator, shortcut='ALT+U', **kwargs)
  178. def set_tool_ui(self):
  179. self.reset_fields()
  180. self.dia.set_value(float(self.app.defaults["tools_cutouttooldia"]))
  181. self.margin.set_value(float(self.app.defaults["tools_cutoutmargin"]))
  182. self.gapsize.set_value(float(self.app.defaults["tools_cutoutgapsize"]))
  183. self.gaps.set_value(4)
  184. self.gaps_rect_radio.set_value(str(self.app.defaults["tools_gaps_rect"]))
  185. def on_freeform_cutout(self):
  186. def subtract_rectangle(obj_, x0, y0, x1, y1):
  187. pts = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
  188. obj_.subtract_polygon(pts)
  189. name = self.obj_combo.currentText()
  190. # Get source object.
  191. try:
  192. cutout_obj = self.app.collection.get_by_name(str(name))
  193. except:
  194. self.app.inform.emit("[ERROR_NOTCL]Could not retrieve object: %s" % name)
  195. return "Could not retrieve object: %s" % name
  196. if cutout_obj is None:
  197. self.app.inform.emit("[ERROR_NOTCL]There is no object selected for Cutout.\nSelect one and try again.")
  198. return
  199. try:
  200. dia = float(self.dia.get_value())
  201. except ValueError:
  202. # try to convert comma to decimal point. if it's still not working error message and return
  203. try:
  204. dia = float(self.dia.get_value().replace(',', '.'))
  205. except ValueError:
  206. self.app.inform.emit("[WARNING_NOTCL] Tool diameter value is missing or wrong format. "
  207. "Add it and retry.")
  208. return
  209. try:
  210. margin = float(self.margin.get_value())
  211. except ValueError:
  212. # try to convert comma to decimal point. if it's still not working error message and return
  213. try:
  214. margin = float(self.margin.get_value().replace(',', '.'))
  215. except ValueError:
  216. self.app.inform.emit("[WARNING_NOTCL] Margin value is missing or wrong format. "
  217. "Add it and retry.")
  218. return
  219. try:
  220. gapsize = float(self.gapsize.get_value())
  221. except ValueError:
  222. # try to convert comma to decimal point. if it's still not working error message and return
  223. try:
  224. gapsize = float(self.gapsize.get_value().replace(',', '.'))
  225. except ValueError:
  226. self.app.inform.emit("[WARNING_NOTCL] Gap size value is missing or wrong format. "
  227. "Add it and retry.")
  228. return
  229. try:
  230. gaps = self.gaps.get_value()
  231. except TypeError:
  232. self.app.inform.emit("[WARNING_NOTCL] Number of gaps value is missing. Add it and retry.")
  233. return
  234. if 0 in {dia}:
  235. self.app.inform.emit("[WARNING_NOTCL]Tool Diameter is zero value. Change it to a positive integer.")
  236. return "Tool Diameter is zero value. Change it to a positive integer."
  237. if gaps not in ['LR', 'TB', '2LR', '2TB', '4', '8']:
  238. self.app.inform.emit("[WARNING_NOTCL] Gaps value can be only one of: 'lr', 'tb', '2lr', '2tb', 4 or 8. "
  239. "Fill in a correct value and retry. ")
  240. return
  241. if cutout_obj.multigeo is True:
  242. self.app.inform.emit("[ERROR]Cutout operation cannot be done on a multi-geo Geometry.\n"
  243. "Optionally, this Multi-geo Geometry can be converted to Single-geo Geometry,\n"
  244. "and after that perform Cutout.")
  245. return
  246. # Get min and max data for each object as we just cut rectangles across X or Y
  247. xmin, ymin, xmax, ymax = cutout_obj.bounds()
  248. px = 0.5 * (xmin + xmax) + margin
  249. py = 0.5 * (ymin + ymax) + margin
  250. lenghtx = (xmax - xmin) + (margin * 2)
  251. lenghty = (ymax - ymin) + (margin * 2)
  252. gapsize = gapsize / 2 + (dia / 2)
  253. if isinstance(cutout_obj,FlatCAMGeometry):
  254. # rename the obj name so it can be identified as cutout
  255. cutout_obj.options["name"] += "_cutout"
  256. else:
  257. cutout_obj.isolate(dia=dia, passes=1, overlap=1, combine=False, outname="_temp")
  258. ext_obj = self.app.collection.get_by_name("_temp")
  259. def geo_init(geo_obj, app_obj):
  260. geo_obj.solid_geometry = obj_exteriors
  261. outname = cutout_obj.options["name"] + "_cutout"
  262. obj_exteriors = ext_obj.get_exteriors()
  263. self.app.new_object('geometry', outname, geo_init)
  264. self.app.collection.set_all_inactive()
  265. self.app.collection.set_active("_temp")
  266. self.app.on_delete()
  267. cutout_obj = self.app.collection.get_by_name(outname)
  268. if gaps == '8' or gaps == '2LR':
  269. subtract_rectangle(cutout_obj,
  270. xmin - gapsize, # botleft_x
  271. py - gapsize + lenghty / 4, # botleft_y
  272. xmax + gapsize, # topright_x
  273. py + gapsize + lenghty / 4) # topright_y
  274. subtract_rectangle(cutout_obj,
  275. xmin - gapsize,
  276. py - gapsize - lenghty / 4,
  277. xmax + gapsize,
  278. py + gapsize - lenghty / 4)
  279. if gaps == '8' or gaps == '2TB':
  280. subtract_rectangle(cutout_obj,
  281. px - gapsize + lenghtx / 4,
  282. ymin - gapsize,
  283. px + gapsize + lenghtx / 4,
  284. ymax + gapsize)
  285. subtract_rectangle(cutout_obj,
  286. px - gapsize - lenghtx / 4,
  287. ymin - gapsize,
  288. px + gapsize - lenghtx / 4,
  289. ymax + gapsize)
  290. if gaps == '4' or gaps == 'LR':
  291. subtract_rectangle(cutout_obj,
  292. xmin - gapsize,
  293. py - gapsize,
  294. xmax + gapsize,
  295. py + gapsize)
  296. if gaps == '4' or gaps == 'TB':
  297. subtract_rectangle(cutout_obj,
  298. px - gapsize,
  299. ymin - gapsize,
  300. px + gapsize,
  301. ymax + gapsize)
  302. cutout_obj.plot()
  303. self.app.inform.emit("[success] Any form CutOut operation finished.")
  304. self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
  305. self.app.should_we_save = True
  306. def on_rectangular_cutout(self):
  307. name = self.obj_combo.currentText()
  308. # Get source object.
  309. try:
  310. cutout_obj = self.app.collection.get_by_name(str(name))
  311. except:
  312. self.app.inform.emit("[ERROR_NOTCL]Could not retrieve object: %s" % name)
  313. return "Could not retrieve object: %s" % name
  314. if cutout_obj is None:
  315. self.app.inform.emit("[ERROR_NOTCL]Object not found: %s" % cutout_obj)
  316. try:
  317. dia = float(self.dia.get_value())
  318. except ValueError:
  319. # try to convert comma to decimal point. if it's still not working error message and return
  320. try:
  321. dia = float(self.dia.get_value().replace(',', '.'))
  322. except ValueError:
  323. self.app.inform.emit("[WARNING_NOTCL] Tool diameter value is missing or wrong format. "
  324. "Add it and retry.")
  325. return
  326. try:
  327. margin = float(self.margin.get_value())
  328. except ValueError:
  329. # try to convert comma to decimal point. if it's still not working error message and return
  330. try:
  331. margin = float(self.margin.get_value().replace(',', '.'))
  332. except ValueError:
  333. self.app.inform.emit("[WARNING_NOTCL] Margin value is missing or wrong format. "
  334. "Add it and retry.")
  335. return
  336. try:
  337. gapsize = float(self.gapsize.get_value())
  338. except ValueError:
  339. # try to convert comma to decimal point. if it's still not working error message and return
  340. try:
  341. gapsize = float(self.gapsize.get_value().replace(',', '.'))
  342. except ValueError:
  343. self.app.inform.emit("[WARNING_NOTCL] Gap size value is missing or wrong format. "
  344. "Add it and retry.")
  345. return
  346. try:
  347. gaps = self.gaps_rect_radio.get_value()
  348. except TypeError:
  349. self.app.inform.emit("[WARNING_NOTCL] Number of gaps value is missing. Add it and retry.")
  350. return
  351. if 0 in {dia}:
  352. self.app.inform.emit("[ERROR_NOTCL]Tool Diameter is zero value. Change it to a positive integer.")
  353. return "Tool Diameter is zero value. Change it to a positive integer."
  354. if cutout_obj.multigeo is True:
  355. self.app.inform.emit("[ERROR]Cutout operation cannot be done on a multi-geo Geometry.\n"
  356. "Optionally, this Multi-geo Geometry can be converted to Single-geo Geometry,\n"
  357. "and after that perform Cutout.")
  358. return
  359. def geo_init(geo_obj, app_obj):
  360. real_margin = margin + (dia / 2)
  361. real_gap_size = gapsize + dia
  362. minx, miny, maxx, maxy = cutout_obj.bounds()
  363. minx -= real_margin
  364. maxx += real_margin
  365. miny -= real_margin
  366. maxy += real_margin
  367. midx = 0.5 * (minx + maxx)
  368. midy = 0.5 * (miny + maxy)
  369. hgap = 0.5 * real_gap_size
  370. pts = [[midx - hgap, maxy],
  371. [minx, maxy],
  372. [minx, midy + hgap],
  373. [minx, midy - hgap],
  374. [minx, miny],
  375. [midx - hgap, miny],
  376. [midx + hgap, miny],
  377. [maxx, miny],
  378. [maxx, midy - hgap],
  379. [maxx, midy + hgap],
  380. [maxx, maxy],
  381. [midx + hgap, maxy]]
  382. cases = {"TB": [[pts[0], pts[1], pts[4], pts[5]],
  383. [pts[6], pts[7], pts[10], pts[11]]],
  384. "LR": [[pts[9], pts[10], pts[1], pts[2]],
  385. [pts[3], pts[4], pts[7], pts[8]]],
  386. "4": [[pts[0], pts[1], pts[2]],
  387. [pts[3], pts[4], pts[5]],
  388. [pts[6], pts[7], pts[8]],
  389. [pts[9], pts[10], pts[11]]]}
  390. cuts = cases[gaps]
  391. geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
  392. # TODO: Check for None
  393. self.app.new_object("geometry", name + "_cutout", geo_init)
  394. self.app.inform.emit("[success] Rectangular CutOut operation finished.")
  395. self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
  396. def reset_fields(self):
  397. self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))