ToolPanelize.py 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # File Author: Marius Adrian Stanciu (c) #
  4. # Date: 3/10/2019 #
  5. # MIT Licence #
  6. # ##########################################################
  7. from PyQt5 import QtWidgets, QtGui, QtCore
  8. from appTool import AppTool
  9. from appGUI.GUIElements import FCSpinner, FCDoubleSpinner, RadioSet, FCCheckBox, OptionalInputSection, FCComboBox
  10. from camlib import grace
  11. from copy import deepcopy
  12. import numpy as np
  13. import shapely.affinity as affinity
  14. from shapely.ops import unary_union
  15. from shapely.geometry import LineString
  16. import gettext
  17. import appTranslation as fcTranslate
  18. import builtins
  19. import logging
  20. fcTranslate.apply_language('strings')
  21. if '_' not in builtins.__dict__:
  22. _ = gettext.gettext
  23. log = logging.getLogger('base')
  24. class Panelize(AppTool):
  25. toolName = _("Panelize PCB")
  26. def __init__(self, app):
  27. AppTool.__init__(self, app)
  28. self.decimals = app.decimals
  29. self.app = app
  30. # #############################################################################
  31. # ######################### Tool GUI ##########################################
  32. # #############################################################################
  33. self.ui = PanelizeUI(layout=self.layout, app=self.app)
  34. self.toolName = self.ui.toolName
  35. # Signals
  36. self.ui.reference_radio.activated_custom.connect(self.on_reference_radio_changed)
  37. self.ui.panelize_object_button.clicked.connect(self.on_panelize)
  38. self.ui.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
  39. self.ui.type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed)
  40. self.ui.reset_button.clicked.connect(self.set_tool_ui)
  41. # list to hold the temporary objects
  42. self.objs = []
  43. # final name for the panel object
  44. self.outname = ""
  45. # flag to signal the constrain was activated
  46. self.constrain_flag = False
  47. def run(self, toggle=True):
  48. self.app.defaults.report_usage("ToolPanelize()")
  49. if toggle:
  50. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  51. if self.app.ui.splitter.sizes()[0] == 0:
  52. self.app.ui.splitter.setSizes([1, 1])
  53. else:
  54. try:
  55. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  56. # if tab is populated with the tool but it does not have the focus, focus on it
  57. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  58. # focus on Tool Tab
  59. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  60. else:
  61. self.app.ui.splitter.setSizes([0, 1])
  62. except AttributeError:
  63. pass
  64. else:
  65. if self.app.ui.splitter.sizes()[0] == 0:
  66. self.app.ui.splitter.setSizes([1, 1])
  67. AppTool.run(self)
  68. self.set_tool_ui()
  69. self.app.ui.notebook.setTabText(2, _("Panel. Tool"))
  70. def install(self, icon=None, separator=None, **kwargs):
  71. AppTool.install(self, icon, separator, shortcut='Alt+Z', **kwargs)
  72. def set_tool_ui(self):
  73. self.reset_fields()
  74. self.reference_radio.set_value('bbox')
  75. sp_c = self.app.defaults["tools_panelize_spacing_columns"] if \
  76. self.app.defaults["tools_panelize_spacing_columns"] else 0.0
  77. self.ui.spacing_columns.set_value(float(sp_c))
  78. sp_r = self.app.defaults["tools_panelize_spacing_rows"] if \
  79. self.app.defaults["tools_panelize_spacing_rows"] else 0.0
  80. self.ui.spacing_rows.set_value(float(sp_r))
  81. rr = self.app.defaults["tools_panelize_rows"] if \
  82. self.app.defaults["tools_panelize_rows"] else 0.0
  83. self.ui.rows.set_value(int(rr))
  84. cc = self.app.defaults["tools_panelize_columns"] if \
  85. self.app.defaults["tools_panelize_columns"] else 0.0
  86. self.ui.columns.set_value(int(cc))
  87. c_cb = self.app.defaults["tools_panelize_constrain"] if \
  88. self.app.defaults["tools_panelize_constrain"] else False
  89. self.ui.constrain_cb.set_value(c_cb)
  90. x_w = self.app.defaults["tools_panelize_constrainx"] if \
  91. self.app.defaults["tools_panelize_constrainx"] else 0.0
  92. self.ui.x_width_entry.set_value(float(x_w))
  93. y_w = self.app.defaults["tools_panelize_constrainy"] if \
  94. self.app.defaults["tools_panelize_constrainy"] else 0.0
  95. self.ui.y_height_entry.set_value(float(y_w))
  96. panel_type = self.app.defaults["tools_panelize_panel_type"] if \
  97. self.app.defaults["tools_panelize_panel_type"] else 'gerber'
  98. self.ui.panel_type_radio.set_value(panel_type)
  99. # run once the following so the obj_type attribute is updated in the FCComboBoxes
  100. # such that the last loaded object is populated in the combo boxes
  101. self.on_type_obj_index_changed()
  102. self.on_type_box_index_changed()
  103. def on_type_obj_index_changed(self):
  104. obj_type = self.ui.type_obj_combo.currentIndex()
  105. self.ui.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
  106. self.ui.object_combo.setCurrentIndex(0)
  107. self.ui.object_combo.obj_type = {
  108. _("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry"
  109. }[self.ui.type_obj_combo.get_value()]
  110. # hide the panel type for Excellons, the panel can be only of type Geometry
  111. if self.ui.type_obj_combo.currentText() != 'Excellon':
  112. self.ui.panel_type_label.setDisabled(False)
  113. self.ui.panel_type_radio.setDisabled(False)
  114. else:
  115. self.ui.panel_type_label.setDisabled(True)
  116. self.ui.panel_type_radio.setDisabled(True)
  117. self.ui.panel_type_radio.set_value('geometry')
  118. def on_type_box_index_changed(self):
  119. obj_type = self.ui.type_box_combo.currentIndex()
  120. obj_type = 2 if obj_type == 1 else obj_type
  121. self.ui.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
  122. self.ui.box_combo.setCurrentIndex(0)
  123. self.ui.box_combo.obj_type = {
  124. _("Gerber"): "Gerber", _("Geometry"): "Geometry"
  125. }[self.ui.type_box_combo.get_value()]
  126. def on_reference_radio_changed(self, current_val):
  127. if current_val == 'object':
  128. self.ui.type_box_combo.setDisabled(False)
  129. self.ui.type_box_combo_label.setDisabled(False)
  130. self.ui.box_combo.setDisabled(False)
  131. else:
  132. self.ui.type_box_combo.setDisabled(True)
  133. self.ui.type_box_combo_label.setDisabled(True)
  134. self.ui.box_combo.setDisabled(True)
  135. def on_panelize(self):
  136. name = self.ui.object_combo.currentText()
  137. # Get source object to be panelized.
  138. try:
  139. panel_source_obj = self.app.collection.get_by_name(str(name))
  140. except Exception as e:
  141. log.debug("Panelize.on_panelize() --> %s" % str(e))
  142. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), name))
  143. return
  144. if panel_source_obj is None:
  145. self.app.inform.emit('[ERROR_NOTCL] %s: %s' %
  146. (_("Object not found"), panel_source_obj))
  147. return
  148. boxname = self.ui.box_combo.currentText()
  149. try:
  150. box = self.app.collection.get_by_name(boxname)
  151. except Exception as e:
  152. log.debug("Panelize.on_panelize() --> %s" % str(e))
  153. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), boxname))
  154. return
  155. if box is None:
  156. self.app.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), panel_source_obj))
  157. self.ui.reference_radio.set_value('bbox')
  158. if self.ui.reference_radio.get_value() == 'bbox':
  159. box = panel_source_obj
  160. self.outname = name + '_panelized'
  161. spacing_columns = float(self.ui.spacing_columns.get_value())
  162. spacing_columns = spacing_columns if spacing_columns is not None else 0
  163. spacing_rows = float(self.ui.spacing_rows.get_value())
  164. spacing_rows = spacing_rows if spacing_rows is not None else 0
  165. rows = int(self.ui.rows.get_value())
  166. rows = rows if rows is not None else 1
  167. columns = int(self.ui.columns.get_value())
  168. columns = columns if columns is not None else 1
  169. constrain_dx = float(self.ui.x_width_entry.get_value())
  170. constrain_dy = float(self.ui.y_height_entry.get_value())
  171. panel_type = str(self.ui.panel_type_radio.get_value())
  172. if 0 in {columns, rows}:
  173. self.app.inform.emit('[ERROR_NOTCL] %s' %
  174. _("Columns or Rows are zero value. Change them to a positive integer."))
  175. return
  176. xmin, ymin, xmax, ymax = box.bounds()
  177. lenghtx = xmax - xmin + spacing_columns
  178. lenghty = ymax - ymin + spacing_rows
  179. # check if constrain within an area is desired
  180. if self.ui.constrain_cb.isChecked():
  181. panel_lengthx = ((xmax - xmin) * columns) + (spacing_columns * (columns - 1))
  182. panel_lengthy = ((ymax - ymin) * rows) + (spacing_rows * (rows - 1))
  183. # adjust the number of columns and/or rows so the panel will fit within the panel constraint area
  184. if (panel_lengthx > constrain_dx) or (panel_lengthy > constrain_dy):
  185. self.constrain_flag = True
  186. while panel_lengthx > constrain_dx:
  187. columns -= 1
  188. panel_lengthx = ((xmax - xmin) * columns) + (spacing_columns * (columns - 1))
  189. while panel_lengthy > constrain_dy:
  190. rows -= 1
  191. panel_lengthy = ((ymax - ymin) * rows) + (spacing_rows * (rows - 1))
  192. if panel_source_obj.kind == 'excellon' or panel_source_obj.kind == 'geometry':
  193. # make a copy of the panelized Excellon or Geometry tools
  194. copied_tools = {}
  195. for tt, tt_val in list(panel_source_obj.tools.items()):
  196. copied_tools[tt] = deepcopy(tt_val)
  197. if panel_source_obj.kind == 'gerber':
  198. # make a copy of the panelized Gerber apertures
  199. copied_apertures = {}
  200. for tt, tt_val in list(panel_source_obj.apertures.items()):
  201. copied_apertures[tt] = deepcopy(tt_val)
  202. def panelize_worker():
  203. if panel_source_obj is not None:
  204. self.app.inform.emit(_("Generating panel ... "))
  205. def job_init_excellon(obj_fin, app_obj):
  206. currenty = 0.0
  207. obj_fin.tools = copied_tools
  208. obj_fin.drills = []
  209. obj_fin.slots = []
  210. obj_fin.solid_geometry = []
  211. for option in panel_source_obj.options:
  212. if option != 'name':
  213. try:
  214. obj_fin.options[option] = panel_source_obj.options[option]
  215. except KeyError:
  216. log.warning("Failed to copy option. %s" % str(option))
  217. geo_len_drills = len(panel_source_obj.drills) if panel_source_obj.drills else 0
  218. geo_len_slots = len(panel_source_obj.slots) if panel_source_obj.slots else 0
  219. element = 0
  220. for row in range(rows):
  221. currentx = 0.0
  222. for col in range(columns):
  223. element += 1
  224. old_disp_number = 0
  225. if panel_source_obj.drills:
  226. drill_nr = 0
  227. for tool_dict in panel_source_obj.drills:
  228. if self.app.abort_flag:
  229. # graceful abort requested by the user
  230. raise grace
  231. point_offseted = affinity.translate(tool_dict['point'], currentx, currenty)
  232. obj_fin.drills.append(
  233. {
  234. "point": point_offseted,
  235. "tool": tool_dict['tool']
  236. }
  237. )
  238. drill_nr += 1
  239. disp_number = int(np.interp(drill_nr, [0, geo_len_drills], [0, 100]))
  240. if old_disp_number < disp_number <= 100:
  241. self.app.proc_container.update_view_text(' %s: %d D:%d%%' %
  242. (_("Copy"),
  243. int(element),
  244. disp_number))
  245. old_disp_number = disp_number
  246. if panel_source_obj.slots:
  247. slot_nr = 0
  248. for tool_dict in panel_source_obj.slots:
  249. if self.app.abort_flag:
  250. # graceful abort requested by the user
  251. raise grace
  252. start_offseted = affinity.translate(tool_dict['start'], currentx, currenty)
  253. stop_offseted = affinity.translate(tool_dict['stop'], currentx, currenty)
  254. obj_fin.slots.append(
  255. {
  256. "start": start_offseted,
  257. "stop": stop_offseted,
  258. "tool": tool_dict['tool']
  259. }
  260. )
  261. slot_nr += 1
  262. disp_number = int(np.interp(slot_nr, [0, geo_len_slots], [0, 100]))
  263. if old_disp_number < disp_number <= 100:
  264. self.app.proc_container.update_view_text(' %s: %d S:%d%%' %
  265. (_("Copy"),
  266. int(element),
  267. disp_number))
  268. old_disp_number = disp_number
  269. currentx += lenghtx
  270. currenty += lenghty
  271. obj_fin.create_geometry()
  272. obj_fin.zeros = panel_source_obj.zeros
  273. obj_fin.units = panel_source_obj.units
  274. app_obj.proc_container.update_view_text('')
  275. def job_init_geometry(obj_fin, app_obj):
  276. currentx = 0.0
  277. currenty = 0.0
  278. def translate_recursion(geom):
  279. if type(geom) == list:
  280. geoms = []
  281. for local_geom in geom:
  282. res_geo = translate_recursion(local_geom)
  283. try:
  284. geoms += res_geo
  285. except TypeError:
  286. geoms.append(res_geo)
  287. return geoms
  288. else:
  289. return affinity.translate(geom, xoff=currentx, yoff=currenty)
  290. obj_fin.solid_geometry = []
  291. # create the initial structure on which to create the panel
  292. if panel_source_obj.kind == 'geometry':
  293. obj_fin.multigeo = panel_source_obj.multigeo
  294. obj_fin.tools = copied_tools
  295. if panel_source_obj.multigeo is True:
  296. for tool in panel_source_obj.tools:
  297. obj_fin.tools[tool]['solid_geometry'][:] = []
  298. elif panel_source_obj.kind == 'gerber':
  299. obj_fin.apertures = copied_apertures
  300. for ap in obj_fin.apertures:
  301. obj_fin.apertures[ap]['geometry'] = []
  302. # find the number of polygons in the source solid_geometry
  303. geo_len = 0
  304. if panel_source_obj.kind == 'geometry':
  305. if panel_source_obj.multigeo is True:
  306. for tool in panel_source_obj.tools:
  307. try:
  308. geo_len += len(panel_source_obj.tools[tool]['solid_geometry'])
  309. except TypeError:
  310. geo_len += 1
  311. # else:
  312. # try:
  313. # geo_len = len(panel_source_obj.solid_geometry)
  314. # except TypeError:
  315. # geo_len = 1
  316. elif panel_source_obj.kind == 'gerber':
  317. for ap in panel_source_obj.apertures:
  318. if 'geometry' in panel_source_obj.apertures[ap]:
  319. try:
  320. geo_len += len(panel_source_obj.apertures[ap]['geometry'])
  321. except TypeError:
  322. geo_len += 1
  323. element = 0
  324. for row in range(rows):
  325. currentx = 0.0
  326. for col in range(columns):
  327. element += 1
  328. old_disp_number = 0
  329. # Will panelize a Geometry Object
  330. if panel_source_obj.kind == 'geometry':
  331. if panel_source_obj.multigeo is True:
  332. for tool in panel_source_obj.tools:
  333. if app_obj.abort_flag:
  334. # graceful abort requested by the user
  335. raise grace
  336. # calculate the number of polygons
  337. geo_len = len(panel_source_obj.tools[tool]['solid_geometry'])
  338. pol_nr = 0
  339. for geo_el in panel_source_obj.tools[tool]['solid_geometry']:
  340. trans_geo = translate_recursion(geo_el)
  341. obj_fin.tools[tool]['solid_geometry'].append(trans_geo)
  342. pol_nr += 1
  343. disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
  344. if old_disp_number < disp_number <= 100:
  345. app_obj.proc_container.update_view_text(
  346. ' %s: %d %d%%' % (_("Copy"), int(element), disp_number))
  347. old_disp_number = disp_number
  348. else:
  349. if app_obj.abort_flag:
  350. # graceful abort requested by the user
  351. raise grace
  352. try:
  353. # calculate the number of polygons
  354. geo_len = len(panel_source_obj.solid_geometry)
  355. except TypeError:
  356. geo_len = 1
  357. pol_nr = 0
  358. try:
  359. for geo_el in panel_source_obj.solid_geometry:
  360. if app_obj.abort_flag:
  361. # graceful abort requested by the user
  362. raise grace
  363. trans_geo = translate_recursion(geo_el)
  364. obj_fin.solid_geometry.append(trans_geo)
  365. pol_nr += 1
  366. disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
  367. if old_disp_number < disp_number <= 100:
  368. app_obj.proc_container.update_view_text(
  369. ' %s: %d %d%%' % (_("Copy"), int(element), disp_number))
  370. old_disp_number = disp_number
  371. except TypeError:
  372. trans_geo = translate_recursion(panel_source_obj.solid_geometry)
  373. obj_fin.solid_geometry.append(trans_geo)
  374. # Will panelize a Gerber Object
  375. else:
  376. if self.app.abort_flag:
  377. # graceful abort requested by the user
  378. raise grace
  379. try:
  380. for geo_el in panel_source_obj.solid_geometry:
  381. if app_obj.abort_flag:
  382. # graceful abort requested by the user
  383. raise grace
  384. trans_geo = translate_recursion(geo_el)
  385. obj_fin.solid_geometry.append(trans_geo)
  386. except TypeError:
  387. trans_geo = translate_recursion(panel_source_obj.solid_geometry)
  388. obj_fin.solid_geometry.append(trans_geo)
  389. for apid in panel_source_obj.apertures:
  390. if app_obj.abort_flag:
  391. # graceful abort requested by the user
  392. raise grace
  393. if 'geometry' in panel_source_obj.apertures[apid]:
  394. try:
  395. # calculate the number of polygons
  396. geo_len = len(panel_source_obj.apertures[apid]['geometry'])
  397. except TypeError:
  398. geo_len = 1
  399. pol_nr = 0
  400. for el in panel_source_obj.apertures[apid]['geometry']:
  401. if app_obj.abort_flag:
  402. # graceful abort requested by the user
  403. raise grace
  404. new_el = {}
  405. if 'solid' in el:
  406. geo_aper = translate_recursion(el['solid'])
  407. new_el['solid'] = geo_aper
  408. if 'clear' in el:
  409. geo_aper = translate_recursion(el['clear'])
  410. new_el['clear'] = geo_aper
  411. if 'follow' in el:
  412. geo_aper = translate_recursion(el['follow'])
  413. new_el['follow'] = geo_aper
  414. obj_fin.apertures[apid]['geometry'].append(deepcopy(new_el))
  415. pol_nr += 1
  416. disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
  417. if old_disp_number < disp_number <= 100:
  418. app_obj.proc_container.update_view_text(
  419. ' %s: %d %d%%' % (_("Copy"), int(element), disp_number))
  420. old_disp_number = disp_number
  421. currentx += lenghtx
  422. currenty += lenghty
  423. if panel_source_obj.kind == 'geometry' and panel_source_obj.multigeo is True:
  424. # I'm going to do this only here as a fix for panelizing cutouts
  425. # I'm going to separate linestrings out of the solid geometry from other
  426. # possible type of elements and apply unary_union on them to fuse them
  427. for tool in obj_fin.tools:
  428. lines = []
  429. other_geo = []
  430. for geo in obj_fin.tools[tool]['solid_geometry']:
  431. if isinstance(geo, LineString):
  432. lines.append(geo)
  433. else:
  434. other_geo.append(geo)
  435. fused_lines = list(unary_union(lines))
  436. obj_fin.tools[tool]['solid_geometry'] = fused_lines + other_geo
  437. if panel_type == 'gerber':
  438. app_obj.inform.emit('%s' % _("Generating panel ... Adding the Gerber code."))
  439. obj_fin.source_file = self.app.export_gerber(obj_name=self.outname, filename=None,
  440. local_use=obj_fin, use_thread=False)
  441. # obj_fin.solid_geometry = cascaded_union(obj_fin.solid_geometry)
  442. # app_obj.log.debug("Finished creating a cascaded union for the panel.")
  443. app_obj.proc_container.update_view_text('')
  444. self.app.inform.emit('%s: %d' % (_("Generating panel... Spawning copies"), (int(rows * columns))))
  445. if panel_source_obj.kind == 'excellon':
  446. self.app.app_obj.new_object(
  447. "excellon", self.outname, job_init_excellon, plot=True, autoselected=True)
  448. else:
  449. self.app.app_obj.new_object(
  450. panel_type, self.outname, job_init_geometry, plot=True, autoselected=True)
  451. if self.constrain_flag is False:
  452. self.app.inform.emit('[success] %s' % _("Panel done..."))
  453. else:
  454. self.constrain_flag = False
  455. self.app.inform.emit(_("{text} Too big for the constrain area. "
  456. "Final panel has {col} columns and {row} rows").format(
  457. text='[WARNING] ', col=columns, row=rows))
  458. proc = self.app.proc_container.new(_("Working..."))
  459. def job_thread(app_obj):
  460. try:
  461. panelize_worker()
  462. app_obj.inform.emit('[success] %s' % _("Panel created successfully."))
  463. except Exception as ee:
  464. proc.done()
  465. log.debug(str(ee))
  466. return
  467. proc.done()
  468. self.app.collection.promise(self.outname)
  469. self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
  470. def reset_fields(self):
  471. self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  472. self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  473. class PanelizeUI:
  474. toolName = _("Panelize PCB")
  475. def __init__(self, layout, app):
  476. self.app = app
  477. self.decimals = self.app.decimals
  478. self.layout = layout
  479. # ## Title
  480. title_label = QtWidgets.QLabel("%s" % self.toolName)
  481. title_label.setStyleSheet("""
  482. QLabel
  483. {
  484. font-size: 16px;
  485. font-weight: bold;
  486. }
  487. """)
  488. self.layout.addWidget(title_label)
  489. self.object_label = QtWidgets.QLabel('<b>%s:</b>' % _("Source Object"))
  490. self.object_label.setToolTip(
  491. _("Specify the type of object to be panelized\n"
  492. "It can be of type: Gerber, Excellon or Geometry.\n"
  493. "The selection here decide the type of objects that will be\n"
  494. "in the Object combobox.")
  495. )
  496. self.layout.addWidget(self.object_label)
  497. # Form Layout
  498. form_layout_0 = QtWidgets.QFormLayout()
  499. self.layout.addLayout(form_layout_0)
  500. # Type of object to be panelized
  501. self.type_obj_combo = FCComboBox()
  502. self.type_obj_combo.addItem("Gerber")
  503. self.type_obj_combo.addItem("Excellon")
  504. self.type_obj_combo.addItem("Geometry")
  505. self.type_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
  506. self.type_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
  507. self.type_obj_combo.setItemIcon(2, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
  508. self.type_object_label = QtWidgets.QLabel('%s:' % _("Object Type"))
  509. form_layout_0.addRow(self.type_object_label, self.type_obj_combo)
  510. # Object to be panelized
  511. self.object_combo = FCComboBox()
  512. self.object_combo.setModel(self.app.collection)
  513. self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  514. self.object_combo.is_last = True
  515. self.object_combo.setToolTip(
  516. _("Object to be panelized. This means that it will\n"
  517. "be duplicated in an array of rows and columns.")
  518. )
  519. form_layout_0.addRow(self.object_combo)
  520. # Form Layout
  521. form_layout = QtWidgets.QFormLayout()
  522. self.layout.addLayout(form_layout)
  523. # Type of box Panel object
  524. self.reference_radio = RadioSet([{'label': _('Object'), 'value': 'object'},
  525. {'label': _('Bounding Box'), 'value': 'bbox'}])
  526. self.box_label = QtWidgets.QLabel("<b>%s:</b>" % _("Penelization Reference"))
  527. self.box_label.setToolTip(
  528. _("Choose the reference for panelization:\n"
  529. "- Object = the bounding box of a different object\n"
  530. "- Bounding Box = the bounding box of the object to be panelized\n"
  531. "\n"
  532. "The reference is useful when doing panelization for more than one\n"
  533. "object. The spacings (really offsets) will be applied in reference\n"
  534. "to this reference object therefore maintaining the panelized\n"
  535. "objects in sync.")
  536. )
  537. form_layout.addRow(self.box_label)
  538. form_layout.addRow(self.reference_radio)
  539. # Type of Box Object to be used as an envelope for panelization
  540. self.type_box_combo = FCComboBox()
  541. self.type_box_combo.addItems([_("Gerber"), _("Geometry")])
  542. # we get rid of item1 ("Excellon") as it is not suitable for use as a "box" for panelizing
  543. # self.type_box_combo.view().setRowHidden(1, True)
  544. self.type_box_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
  545. self.type_box_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
  546. self.type_box_combo_label = QtWidgets.QLabel('%s:' % _("Box Type"))
  547. self.type_box_combo_label.setToolTip(
  548. _("Specify the type of object to be used as an container for\n"
  549. "panelization. It can be: Gerber or Geometry type.\n"
  550. "The selection here decide the type of objects that will be\n"
  551. "in the Box Object combobox.")
  552. )
  553. form_layout.addRow(self.type_box_combo_label, self.type_box_combo)
  554. # Box
  555. self.box_combo = FCComboBox()
  556. self.box_combo.setModel(self.app.collection)
  557. self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  558. self.box_combo.is_last = True
  559. self.box_combo.setToolTip(
  560. _("The actual object that is used as container for the\n "
  561. "selected object that is to be panelized.")
  562. )
  563. form_layout.addRow(self.box_combo)
  564. separator_line = QtWidgets.QFrame()
  565. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  566. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  567. form_layout.addRow(separator_line)
  568. panel_data_label = QtWidgets.QLabel("<b>%s:</b>" % _("Panel Data"))
  569. panel_data_label.setToolTip(
  570. _("This informations will shape the resulting panel.\n"
  571. "The number of rows and columns will set how many\n"
  572. "duplicates of the original geometry will be generated.\n"
  573. "\n"
  574. "The spacings will set the distance between any two\n"
  575. "elements of the panel array.")
  576. )
  577. form_layout.addRow(panel_data_label)
  578. # Spacing Columns
  579. self.spacing_columns = FCDoubleSpinner(callback=self.confirmation_message)
  580. self.spacing_columns.set_range(0, 9999)
  581. self.spacing_columns.set_precision(4)
  582. self.spacing_columns_label = QtWidgets.QLabel('%s:' % _("Spacing cols"))
  583. self.spacing_columns_label.setToolTip(
  584. _("Spacing between columns of the desired panel.\n"
  585. "In current units.")
  586. )
  587. form_layout.addRow(self.spacing_columns_label, self.spacing_columns)
  588. # Spacing Rows
  589. self.spacing_rows = FCDoubleSpinner(callback=self.confirmation_message)
  590. self.spacing_rows.set_range(0, 9999)
  591. self.spacing_rows.set_precision(4)
  592. self.spacing_rows_label = QtWidgets.QLabel('%s:' % _("Spacing rows"))
  593. self.spacing_rows_label.setToolTip(
  594. _("Spacing between rows of the desired panel.\n"
  595. "In current units.")
  596. )
  597. form_layout.addRow(self.spacing_rows_label, self.spacing_rows)
  598. # Columns
  599. self.columns = FCSpinner(callback=self.confirmation_message_int)
  600. self.columns.set_range(0, 9999)
  601. self.columns_label = QtWidgets.QLabel('%s:' % _("Columns"))
  602. self.columns_label.setToolTip(
  603. _("Number of columns of the desired panel")
  604. )
  605. form_layout.addRow(self.columns_label, self.columns)
  606. # Rows
  607. self.rows = FCSpinner(callback=self.confirmation_message_int)
  608. self.rows.set_range(0, 9999)
  609. self.rows_label = QtWidgets.QLabel('%s:' % _("Rows"))
  610. self.rows_label.setToolTip(
  611. _("Number of rows of the desired panel")
  612. )
  613. form_layout.addRow(self.rows_label, self.rows)
  614. separator_line = QtWidgets.QFrame()
  615. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  616. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  617. form_layout.addRow(separator_line)
  618. # Type of resulting Panel object
  619. self.panel_type_radio = RadioSet([{'label': _('Gerber'), 'value': 'gerber'},
  620. {'label': _('Geo'), 'value': 'geometry'}])
  621. self.panel_type_label = QtWidgets.QLabel("<b>%s:</b>" % _("Panel Type"))
  622. self.panel_type_label.setToolTip(
  623. _("Choose the type of object for the panel object:\n"
  624. "- Geometry\n"
  625. "- Gerber")
  626. )
  627. form_layout.addRow(self.panel_type_label)
  628. form_layout.addRow(self.panel_type_radio)
  629. # Constrains
  630. self.constrain_cb = FCCheckBox('%s:' % _("Constrain panel within"))
  631. self.constrain_cb.setToolTip(
  632. _("Area define by DX and DY within to constrain the panel.\n"
  633. "DX and DY values are in current units.\n"
  634. "Regardless of how many columns and rows are desired,\n"
  635. "the final panel will have as many columns and rows as\n"
  636. "they fit completely within selected area.")
  637. )
  638. form_layout.addRow(self.constrain_cb)
  639. self.x_width_entry = FCDoubleSpinner(callback=self.confirmation_message)
  640. self.x_width_entry.set_precision(4)
  641. self.x_width_entry.set_range(0, 9999)
  642. self.x_width_lbl = QtWidgets.QLabel('%s:' % _("Width (DX)"))
  643. self.x_width_lbl.setToolTip(
  644. _("The width (DX) within which the panel must fit.\n"
  645. "In current units.")
  646. )
  647. form_layout.addRow(self.x_width_lbl, self.x_width_entry)
  648. self.y_height_entry = FCDoubleSpinner(callback=self.confirmation_message)
  649. self.y_height_entry.set_range(0, 9999)
  650. self.y_height_entry.set_precision(4)
  651. self.y_height_lbl = QtWidgets.QLabel('%s:' % _("Height (DY)"))
  652. self.y_height_lbl.setToolTip(
  653. _("The height (DY)within which the panel must fit.\n"
  654. "In current units.")
  655. )
  656. form_layout.addRow(self.y_height_lbl, self.y_height_entry)
  657. self.constrain_sel = OptionalInputSection(
  658. self.constrain_cb, [self.x_width_lbl, self.x_width_entry, self.y_height_lbl, self.y_height_entry])
  659. separator_line = QtWidgets.QFrame()
  660. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  661. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  662. form_layout.addRow(separator_line)
  663. # Buttons
  664. self.panelize_object_button = QtWidgets.QPushButton(_("Panelize Object"))
  665. self.panelize_object_button.setToolTip(
  666. _("Panelize the specified object around the specified box.\n"
  667. "In other words it creates multiple copies of the source object,\n"
  668. "arranged in a 2D array of rows and columns.")
  669. )
  670. self.panelize_object_button.setStyleSheet("""
  671. QPushButton
  672. {
  673. font-weight: bold;
  674. }
  675. """)
  676. self.layout.addWidget(self.panelize_object_button)
  677. self.layout.addStretch()
  678. # ## Reset Tool
  679. self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
  680. self.reset_button.setToolTip(
  681. _("Will reset the tool parameters.")
  682. )
  683. self.reset_button.setStyleSheet("""
  684. QPushButton
  685. {
  686. font-weight: bold;
  687. }
  688. """)
  689. self.layout.addWidget(self.reset_button)
  690. # #################################### FINSIHED GUI ###########################
  691. # #############################################################################
  692. def confirmation_message(self, accepted, minval, maxval):
  693. if accepted is False:
  694. self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
  695. self.decimals,
  696. minval,
  697. self.decimals,
  698. maxval), False)
  699. else:
  700. self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
  701. def confirmation_message_int(self, accepted, minval, maxval):
  702. if accepted is False:
  703. self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
  704. (_("Edited value is out of range"), minval, maxval), False)
  705. else:
  706. self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)