ToolPanelize.py 44 KB

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