ToolCutOut.py 19 KB

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