ToolCutout.py 16 KB

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