ToolPanelize.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. ############################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://flatcam.org #
  4. # File Author: Marius Adrian Stanciu (c) #
  5. # Date: 3/10/2019 #
  6. # MIT Licence #
  7. ############################################################
  8. from FlatCAMTool import FlatCAMTool
  9. from copy import copy, deepcopy
  10. from ObjectCollection import *
  11. import time
  12. import gettext
  13. import FlatCAMTranslation as fcTranslate
  14. fcTranslate.apply_language('strings')
  15. import builtins
  16. if '_' not in builtins.__dict__:
  17. _ = gettext.gettext
  18. class Panelize(FlatCAMTool):
  19. toolName = _("Panelize PCB")
  20. def __init__(self, app):
  21. super(Panelize, self).__init__(self)
  22. self.app = app
  23. ## Title
  24. title_label = QtWidgets.QLabel("%s" % self.toolName)
  25. title_label.setStyleSheet("""
  26. QLabel
  27. {
  28. font-size: 16px;
  29. font-weight: bold;
  30. }
  31. """)
  32. self.layout.addWidget(title_label)
  33. ## Form Layout
  34. form_layout = QtWidgets.QFormLayout()
  35. self.layout.addLayout(form_layout)
  36. ## Type of object to be panelized
  37. self.type_obj_combo = QtWidgets.QComboBox()
  38. self.type_obj_combo.addItem("Gerber")
  39. self.type_obj_combo.addItem("Excellon")
  40. self.type_obj_combo.addItem("Geometry")
  41. self.type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
  42. self.type_obj_combo.setItemIcon(1, QtGui.QIcon("share/drill16.png"))
  43. self.type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
  44. self.type_obj_combo_label = QtWidgets.QLabel(_("Object Type:"))
  45. self.type_obj_combo_label.setToolTip(
  46. _("Specify the type of object to be panelized\n"
  47. "It can be of type: Gerber, Excellon or Geometry.\n"
  48. "The selection here decide the type of objects that will be\n"
  49. "in the Object combobox.")
  50. )
  51. form_layout.addRow(self.type_obj_combo_label, self.type_obj_combo)
  52. ## Object to be panelized
  53. self.object_combo = QtWidgets.QComboBox()
  54. self.object_combo.setModel(self.app.collection)
  55. self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  56. self.object_combo.setCurrentIndex(1)
  57. self.object_label = QtWidgets.QLabel(_("Object:"))
  58. self.object_label.setToolTip(
  59. _("Object to be panelized. This means that it will\n"
  60. "be duplicated in an array of rows and columns.")
  61. )
  62. form_layout.addRow(self.object_label, self.object_combo)
  63. ## Type of Box Object to be used as an envelope for panelization
  64. self.type_box_combo = QtWidgets.QComboBox()
  65. self.type_box_combo.addItem("Gerber")
  66. self.type_box_combo.addItem("Excellon")
  67. self.type_box_combo.addItem("Geometry")
  68. # we get rid of item1 ("Excellon") as it is not suitable for use as a "box" for panelizing
  69. self.type_box_combo.view().setRowHidden(1, True)
  70. self.type_box_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
  71. self.type_box_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
  72. self.type_box_combo_label = QtWidgets.QLabel(_("Box Type:"))
  73. self.type_box_combo_label.setToolTip(
  74. _("Specify the type of object to be used as an container for\n"
  75. "panelization. It can be: Gerber or Geometry type.\n"
  76. "The selection here decide the type of objects that will be\n"
  77. "in the Box Object combobox.")
  78. )
  79. form_layout.addRow(self.type_box_combo_label, self.type_box_combo)
  80. ## Box
  81. self.box_combo = QtWidgets.QComboBox()
  82. self.box_combo.setModel(self.app.collection)
  83. self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  84. self.box_combo.setCurrentIndex(1)
  85. self.box_combo_label = QtWidgets.QLabel(_("Box Object:"))
  86. self.box_combo_label.setToolTip(
  87. _("The actual object that is used a container for the\n "
  88. "selected object that is to be panelized.")
  89. )
  90. form_layout.addRow(self.box_combo_label, self.box_combo)
  91. ## Spacing Columns
  92. self.spacing_columns = FCEntry()
  93. self.spacing_columns_label = QtWidgets.QLabel(_("Spacing cols:"))
  94. self.spacing_columns_label.setToolTip(
  95. _("Spacing between columns of the desired panel.\n"
  96. "In current units.")
  97. )
  98. form_layout.addRow(self.spacing_columns_label, self.spacing_columns)
  99. ## Spacing Rows
  100. self.spacing_rows = FCEntry()
  101. self.spacing_rows_label = QtWidgets.QLabel(_("Spacing rows:"))
  102. self.spacing_rows_label.setToolTip(
  103. _("Spacing between rows of the desired panel.\n"
  104. "In current units.")
  105. )
  106. form_layout.addRow(self.spacing_rows_label, self.spacing_rows)
  107. ## Columns
  108. self.columns = FCEntry()
  109. self.columns_label = QtWidgets.QLabel(_("Columns:"))
  110. self.columns_label.setToolTip(
  111. _("Number of columns of the desired panel")
  112. )
  113. form_layout.addRow(self.columns_label, self.columns)
  114. ## Rows
  115. self.rows = FCEntry()
  116. self.rows_label = QtWidgets.QLabel(_("Rows:"))
  117. self.rows_label.setToolTip(
  118. _("Number of rows of the desired panel")
  119. )
  120. form_layout.addRow(self.rows_label, self.rows)
  121. ## Type of resulting Panel object
  122. self.panel_type_radio = RadioSet([{'label': 'Gerber', 'value': 'gerber'},
  123. {'label': 'Geometry', 'value': 'geometry'}])
  124. self.panel_type_label = QtWidgets.QLabel(_("Panel Type:"))
  125. self.panel_type_label.setToolTip(
  126. _("Choose the type of object for the panel object:\n"
  127. "- Geometry\n"
  128. "- Gerber")
  129. )
  130. form_layout.addRow(self.panel_type_label)
  131. form_layout.addRow(self.panel_type_radio)
  132. ## Constrains
  133. self.constrain_cb = FCCheckBox(_("Constrain panel within:"))
  134. self.constrain_cb.setToolTip(
  135. _("Area define by DX and DY within to constrain the panel.\n"
  136. "DX and DY values are in current units.\n"
  137. "Regardless of how many columns and rows are desired,\n"
  138. "the final panel will have as many columns and rows as\n"
  139. "they fit completely within selected area.")
  140. )
  141. form_layout.addRow(self.constrain_cb)
  142. self.x_width_entry = FCEntry()
  143. self.x_width_lbl = QtWidgets.QLabel(_("Width (DX):"))
  144. self.x_width_lbl.setToolTip(
  145. _("The width (DX) within which the panel must fit.\n"
  146. "In current units.")
  147. )
  148. form_layout.addRow(self.x_width_lbl, self.x_width_entry)
  149. self.y_height_entry = FCEntry()
  150. self.y_height_lbl = QtWidgets.QLabel(_("Height (DY):"))
  151. self.y_height_lbl.setToolTip(
  152. _("The height (DY)within which the panel must fit.\n"
  153. "In current units.")
  154. )
  155. form_layout.addRow(self.y_height_lbl, self.y_height_entry)
  156. self.constrain_sel = OptionalInputSection(
  157. self.constrain_cb, [self.x_width_lbl, self.x_width_entry, self.y_height_lbl, self.y_height_entry])
  158. ## Buttons
  159. hlay_2 = QtWidgets.QHBoxLayout()
  160. self.layout.addLayout(hlay_2)
  161. hlay_2.addStretch()
  162. self.panelize_object_button = QtWidgets.QPushButton(_("Panelize Object"))
  163. self.panelize_object_button.setToolTip(
  164. _("Panelize the specified object around the specified box.\n"
  165. "In other words it creates multiple copies of the source object,\n"
  166. "arranged in a 2D array of rows and columns.")
  167. )
  168. hlay_2.addWidget(self.panelize_object_button)
  169. self.layout.addStretch()
  170. ## Signals
  171. self.panelize_object_button.clicked.connect(self.on_panelize)
  172. self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
  173. self.type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed)
  174. # list to hold the temporary objects
  175. self.objs = []
  176. # final name for the panel object
  177. self.outname = ""
  178. # flag to signal the constrain was activated
  179. self.constrain_flag = False
  180. def run(self, toggle=True):
  181. self.app.report_usage("ToolPanelize()")
  182. if toggle:
  183. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  184. if self.app.ui.splitter.sizes()[0] == 0:
  185. self.app.ui.splitter.setSizes([1, 1])
  186. else:
  187. try:
  188. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  189. self.app.ui.splitter.setSizes([0, 1])
  190. except AttributeError:
  191. pass
  192. else:
  193. if self.app.ui.splitter.sizes()[0] == 0:
  194. self.app.ui.splitter.setSizes([1, 1])
  195. FlatCAMTool.run(self)
  196. self.set_tool_ui()
  197. self.app.ui.notebook.setTabText(2, "Panel. Tool")
  198. def install(self, icon=None, separator=None, **kwargs):
  199. FlatCAMTool.install(self, icon, separator, shortcut='ALT+Z', **kwargs)
  200. def set_tool_ui(self):
  201. self.reset_fields()
  202. sp_c = self.app.defaults["tools_panelize_spacing_columns"] if \
  203. self.app.defaults["tools_panelize_spacing_columns"] else 0.0
  204. self.spacing_columns.set_value(float(sp_c))
  205. sp_r = self.app.defaults["tools_panelize_spacing_rows"] if \
  206. self.app.defaults["tools_panelize_spacing_rows"] else 0.0
  207. self.spacing_rows.set_value(float(sp_r))
  208. rr = self.app.defaults["tools_panelize_rows"] if \
  209. self.app.defaults["tools_panelize_rows"] else 0.0
  210. self.rows.set_value(int(rr))
  211. cc = self.app.defaults["tools_panelize_columns"] if \
  212. self.app.defaults["tools_panelize_columns"] else 0.0
  213. self.columns.set_value(int(cc))
  214. c_cb = self.app.defaults["tools_panelize_constrain"] if \
  215. self.app.defaults["tools_panelize_constrain"] else False
  216. self.constrain_cb.set_value(c_cb)
  217. x_w = self.app.defaults["tools_panelize_constrainx"] if \
  218. self.app.defaults["tools_panelize_constrainx"] else 0.0
  219. self.x_width_entry.set_value(float(x_w))
  220. y_w = self.app.defaults["tools_panelize_constrainy"] if \
  221. self.app.defaults["tools_panelize_constrainy"] else 0.0
  222. self.y_height_entry.set_value(float(y_w))
  223. panel_type = self.app.defaults["tools_panelize_panel_type"] if \
  224. self.app.defaults["tools_panelize_panel_type"] else 'gerber'
  225. self.panel_type_radio.set_value(panel_type)
  226. def on_type_obj_index_changed(self):
  227. obj_type = self.type_obj_combo.currentIndex()
  228. self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
  229. self.object_combo.setCurrentIndex(0)
  230. def on_type_box_index_changed(self):
  231. obj_type = self.type_box_combo.currentIndex()
  232. self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
  233. self.box_combo.setCurrentIndex(0)
  234. def on_panelize(self):
  235. name = self.object_combo.currentText()
  236. # Get source object.
  237. try:
  238. obj = self.app.collection.get_by_name(str(name))
  239. except:
  240. self.app.inform.emit(_("[ERROR_NOTCL] Could not retrieve object: %s") % name)
  241. return "Could not retrieve object: %s" % name
  242. panel_obj = obj
  243. if panel_obj is None:
  244. self.app.inform.emit(_("[ERROR_NOTCL] Object not found: %s") % panel_obj)
  245. return "Object not found: %s" % panel_obj
  246. boxname = self.box_combo.currentText()
  247. try:
  248. box = self.app.collection.get_by_name(boxname)
  249. except:
  250. self.app.inform.emit(_("[ERROR_NOTCL] Could not retrieve object: %s") % boxname)
  251. return "Could not retrieve object: %s" % boxname
  252. if box is None:
  253. self.app.inform.emit(_("[WARNING]No object Box. Using instead %s") % panel_obj)
  254. box = panel_obj
  255. self.outname = name + '_panelized'
  256. try:
  257. spacing_columns = float(self.spacing_columns.get_value())
  258. except ValueError:
  259. # try to convert comma to decimal point. if it's still not working error message and return
  260. try:
  261. spacing_columns = float(self.spacing_columns.get_value().replace(',', '.'))
  262. except ValueError:
  263. self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
  264. "use a number."))
  265. return
  266. spacing_columns = spacing_columns if spacing_columns is not None else 0
  267. try:
  268. spacing_rows = float(self.spacing_rows.get_value())
  269. except ValueError:
  270. # try to convert comma to decimal point. if it's still not working error message and return
  271. try:
  272. spacing_rows = float(self.spacing_rows.get_value().replace(',', '.'))
  273. except ValueError:
  274. self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
  275. "use a number."))
  276. return
  277. spacing_rows = spacing_rows if spacing_rows is not None else 0
  278. try:
  279. rows = int(self.rows.get_value())
  280. except ValueError:
  281. # try to convert comma to decimal point. if it's still not working error message and return
  282. try:
  283. rows = float(self.rows.get_value().replace(',', '.'))
  284. rows = int(rows)
  285. except ValueError:
  286. self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
  287. "use a number."))
  288. return
  289. rows = rows if rows is not None else 1
  290. try:
  291. columns = int(self.columns.get_value())
  292. except ValueError:
  293. # try to convert comma to decimal point. if it's still not working error message and return
  294. try:
  295. columns = float(self.columns.get_value().replace(',', '.'))
  296. columns = int(columns)
  297. except ValueError:
  298. self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
  299. "use a number."))
  300. return
  301. columns = columns if columns is not None else 1
  302. try:
  303. constrain_dx = float(self.x_width_entry.get_value())
  304. except ValueError:
  305. # try to convert comma to decimal point. if it's still not working error message and return
  306. try:
  307. constrain_dx = float(self.x_width_entry.get_value().replace(',', '.'))
  308. except ValueError:
  309. self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
  310. "use a number."))
  311. return
  312. try:
  313. constrain_dy = float(self.y_height_entry.get_value())
  314. except ValueError:
  315. # try to convert comma to decimal point. if it's still not working error message and return
  316. try:
  317. constrain_dy = float(self.y_height_entry.get_value().replace(',', '.'))
  318. except ValueError:
  319. self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
  320. "use a number."))
  321. return
  322. panel_type = str(self.panel_type_radio.get_value())
  323. if 0 in {columns, rows}:
  324. self.app.inform.emit(_("[ERROR_NOTCL] Columns or Rows are zero value. Change them to a positive integer."))
  325. return "Columns or Rows are zero value. Change them to a positive integer."
  326. xmin, ymin, xmax, ymax = box.bounds()
  327. lenghtx = xmax - xmin + spacing_columns
  328. lenghty = ymax - ymin + spacing_rows
  329. # check if constrain within an area is desired
  330. if self.constrain_cb.isChecked():
  331. panel_lengthx = ((xmax - xmin) * columns) + (spacing_columns * (columns - 1))
  332. panel_lengthy = ((ymax - ymin) * rows) + (spacing_rows * (rows - 1))
  333. # adjust the number of columns and/or rows so the panel will fit within the panel constraint area
  334. if (panel_lengthx > constrain_dx) or (panel_lengthy > constrain_dy):
  335. self.constrain_flag = True
  336. while panel_lengthx > constrain_dx:
  337. columns -= 1
  338. panel_lengthx = ((xmax - xmin) * columns) + (spacing_columns * (columns - 1))
  339. while panel_lengthy > constrain_dy:
  340. rows -= 1
  341. panel_lengthy = ((ymax - ymin) * rows) + (spacing_rows * (rows - 1))
  342. def panelize_2():
  343. if panel_obj is not None:
  344. self.app.inform.emit(_("Generating panel ... Please wait."))
  345. self.app.progress.emit(0)
  346. def job_init_excellon(obj_fin, app_obj):
  347. currenty = 0.0
  348. self.app.progress.emit(10)
  349. obj_fin.tools = panel_obj.tools.copy()
  350. obj_fin.drills = []
  351. obj_fin.slots = []
  352. obj_fin.solid_geometry = []
  353. for option in panel_obj.options:
  354. if option is not 'name':
  355. try:
  356. obj_fin.options[option] = panel_obj.options[option]
  357. except:
  358. log.warning("Failed to copy option.", option)
  359. for row in range(rows):
  360. currentx = 0.0
  361. for col in range(columns):
  362. if panel_obj.drills:
  363. for tool_dict in panel_obj.drills:
  364. point_offseted = affinity.translate(tool_dict['point'], currentx, currenty)
  365. obj_fin.drills.append(
  366. {
  367. "point": point_offseted,
  368. "tool": tool_dict['tool']
  369. }
  370. )
  371. if panel_obj.slots:
  372. for tool_dict in panel_obj.slots:
  373. start_offseted = affinity.translate(tool_dict['start'], currentx, currenty)
  374. stop_offseted = affinity.translate(tool_dict['stop'], currentx, currenty)
  375. obj_fin.slots.append(
  376. {
  377. "start": start_offseted,
  378. "stop": stop_offseted,
  379. "tool": tool_dict['tool']
  380. }
  381. )
  382. currentx += lenghtx
  383. currenty += lenghty
  384. obj_fin.create_geometry()
  385. obj_fin.zeros = panel_obj.zeros
  386. obj_fin.units = panel_obj.units
  387. def job_init_geometry(obj_fin, app_obj):
  388. currentx = 0.0
  389. currenty = 0.0
  390. def translate_recursion(geom):
  391. if type(geom) == list:
  392. geoms = list()
  393. for local_geom in geom:
  394. geoms.append(translate_recursion(local_geom))
  395. return geoms
  396. else:
  397. return affinity.translate(geom, xoff=currentx, yoff=currenty)
  398. obj_fin.solid_geometry = []
  399. if isinstance(panel_obj, FlatCAMGeometry):
  400. obj_fin.multigeo = panel_obj.multigeo
  401. obj_fin.tools = deepcopy(panel_obj.tools)
  402. if panel_obj.multigeo is True:
  403. for tool in panel_obj.tools:
  404. obj_fin.tools[tool]['solid_geometry'][:] = []
  405. self.app.progress.emit(0)
  406. for row in range(rows):
  407. currentx = 0.0
  408. for col in range(columns):
  409. if isinstance(panel_obj, FlatCAMGeometry):
  410. if panel_obj.multigeo is True:
  411. for tool in panel_obj.tools:
  412. obj_fin.tools[tool]['solid_geometry'].append(translate_recursion(
  413. panel_obj.tools[tool]['solid_geometry'])
  414. )
  415. else:
  416. obj_fin.solid_geometry.append(
  417. translate_recursion(panel_obj.solid_geometry)
  418. )
  419. else:
  420. obj_fin.solid_geometry.append(
  421. translate_recursion(panel_obj.solid_geometry)
  422. )
  423. currentx += lenghtx
  424. currenty += lenghty
  425. if isinstance(panel_obj, FlatCAMExcellon):
  426. self.app.progress.emit(50)
  427. self.app.new_object("excellon", self.outname, job_init_excellon, plot=True, autoselected=True)
  428. else:
  429. self.app.progress.emit(50)
  430. self.app.new_object(panel_type, self.outname, job_init_geometry,
  431. plot=True, autoselected=True)
  432. if self.constrain_flag is False:
  433. self.app.inform.emit(_("[success] Panel done..."))
  434. else:
  435. self.constrain_flag = False
  436. self.app.inform.emit(_("[WARNING] Too big for the constrain area. Final panel has {col} columns and {row} rows").format(
  437. col=columns, row=rows))
  438. proc = self.app.proc_container.new(_("Generating panel ... Please wait."))
  439. def job_thread(app_obj):
  440. try:
  441. panelize_2()
  442. self.app.inform.emit(_("[success] Panel created successfully."))
  443. except Exception as e:
  444. proc.done()
  445. log.debug(str(e))
  446. return
  447. proc.done()
  448. self.app.collection.promise(self.outname)
  449. self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
  450. def reset_fields(self):
  451. self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  452. self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))