ToolTransform.py 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977
  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. # ##########################################################
  8. # File Modified : Marcos Dumay de Medeiros #
  9. # Modifications under GPLv3 #
  10. # ##########################################################
  11. from PyQt5 import QtWidgets, QtGui, QtCore
  12. from appTool import AppTool
  13. from appGUI.GUIElements import FCDoubleSpinner, FCCheckBox, FCButton, OptionalInputSection, FCComboBox, \
  14. NumericalEvalTupleEntry, FCLabel
  15. import numpy as np
  16. import gettext
  17. import appTranslation as fcTranslate
  18. import builtins
  19. fcTranslate.apply_language('strings')
  20. if '_' not in builtins.__dict__:
  21. _ = gettext.gettext
  22. class ToolTransform(AppTool):
  23. def __init__(self, app):
  24. AppTool.__init__(self, app)
  25. self.decimals = self.app.decimals
  26. # #############################################################################
  27. # ######################### Tool GUI ##########################################
  28. # #############################################################################
  29. self.ui = TransformUI(layout=self.layout, app=self.app)
  30. self.toolName = self.ui.toolName
  31. # ## Signals
  32. self.ui.ref_combo.currentIndexChanged.connect(self.ui.on_reference_changed)
  33. self.ui.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
  34. self.ui.point_button.clicked.connect(self.on_add_coords)
  35. self.ui.rotate_button.clicked.connect(self.on_rotate)
  36. self.ui.skewx_button.clicked.connect(self.on_skewx)
  37. self.ui.skewy_button.clicked.connect(self.on_skewy)
  38. self.ui.scalex_button.clicked.connect(self.on_scalex)
  39. self.ui.scaley_button.clicked.connect(self.on_scaley)
  40. self.ui.offx_button.clicked.connect(self.on_offx)
  41. self.ui.offy_button.clicked.connect(self.on_offy)
  42. self.ui.flipx_button.clicked.connect(self.on_flipx)
  43. self.ui.flipy_button.clicked.connect(self.on_flipy)
  44. self.ui.buffer_button.clicked.connect(self.on_buffer_by_distance)
  45. self.ui.buffer_factor_button.clicked.connect(self.on_buffer_by_factor)
  46. self.ui.reset_button.clicked.connect(self.set_tool_ui)
  47. def run(self, toggle=True):
  48. self.app.defaults.report_usage("ToolTransform()")
  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, _("Transform Tool"))
  70. def install(self, icon=None, separator=None, **kwargs):
  71. AppTool.install(self, icon, separator, shortcut='Alt+T', **kwargs)
  72. def set_tool_ui(self):
  73. # ## Initialize form
  74. self.ui.ref_combo.set_value(self.app.defaults["tools_transform_reference"])
  75. self.ui.type_obj_combo.set_value(self.app.defaults["tools_transform_ref_object"])
  76. self.ui.point_entry.set_value(self.app.defaults["tools_transform_ref_point"])
  77. self.ui.rotate_entry.set_value(self.app.defaults["tools_transform_rotate"])
  78. self.ui.skewx_entry.set_value(self.app.defaults["tools_transform_skew_x"])
  79. self.ui.skewy_entry.set_value(self.app.defaults["tools_transform_skew_y"])
  80. self.ui.skew_link_cb.set_value(self.app.defaults["tools_transform_skew_link"])
  81. self.ui.scalex_entry.set_value(self.app.defaults["tools_transform_scale_x"])
  82. self.ui.scaley_entry.set_value(self.app.defaults["tools_transform_scale_y"])
  83. self.ui.scale_link_cb.set_value(self.app.defaults["tools_transform_scale_link"])
  84. self.ui.offx_entry.set_value(self.app.defaults["tools_transform_offset_x"])
  85. self.ui.offy_entry.set_value(self.app.defaults["tools_transform_offset_y"])
  86. self.ui.buffer_entry.set_value(self.app.defaults["tools_transform_buffer_dis"])
  87. self.ui.buffer_factor_entry.set_value(self.app.defaults["tools_transform_buffer_factor"])
  88. self.ui.buffer_rounded_cb.set_value(self.app.defaults["tools_transform_buffer_corner"])
  89. # initial state is hidden
  90. self.ui.point_label.hide()
  91. self.ui.point_entry.hide()
  92. self.ui.point_button.hide()
  93. self.ui.type_object_label.hide()
  94. self.ui.type_obj_combo.hide()
  95. self.ui.object_combo.hide()
  96. def on_type_obj_index_changed(self, index):
  97. self.ui.object_combo.setRootModelIndex(self.app.collection.index(index, 0, QtCore.QModelIndex()))
  98. self.ui.object_combo.setCurrentIndex(0)
  99. self.ui.object_combo.obj_type = {
  100. _("Gerber"): "Gerber", _("Excellon"): "Excellon", _("Geometry"): "Geometry"
  101. }[self.ui.type_obj_combo.get_value()]
  102. def on_calculate_reference(self):
  103. ref_val = self.ui.ref_combo.currentIndex()
  104. if ref_val == 0: # "Origin" reference
  105. return 0, 0
  106. elif ref_val == 1: # "Selection" reference
  107. sel_list = self.app.collection.get_selected()
  108. if sel_list:
  109. xmin, ymin, xmax, ymax = self.alt_bounds(obj_list=sel_list)
  110. px = (xmax + xmin) * 0.5
  111. py = (ymax + ymin) * 0.5
  112. return px, py
  113. else:
  114. self.app.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
  115. return "fail"
  116. elif ref_val == 2: # "Point" reference
  117. point_val = self.uipoint_entry.get_value()
  118. try:
  119. px, py = eval('{}'.format(point_val))
  120. return px, py
  121. except Exception:
  122. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Incorrect format for Point value. Needs format X,Y"))
  123. return "fail"
  124. else: # "Object" reference
  125. obj_name = self.ui.object_combo.get_value()
  126. ref_obj = self.app.collection.get_by_name(obj_name)
  127. xmin, ymin, xmax, ymax = ref_obj.bounds()
  128. px = (xmax + xmin) * 0.5
  129. py = (ymax + ymin) * 0.5
  130. return px, py
  131. def on_add_coords(self):
  132. val = self.app.clipboard.text()
  133. self.ui.point_entry.set_value(val)
  134. def on_rotate(self):
  135. value = float(self.ui.rotate_entry.get_value())
  136. if value == 0:
  137. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Rotate transformation can not be done for a value of 0."))
  138. return
  139. point = self.on_calculate_reference()
  140. if point == 'fail':
  141. return
  142. self.app.worker_task.emit({'fcn': self.on_rotate_action, 'params': [value, point]})
  143. def on_flipx(self):
  144. axis = 'Y'
  145. point = self.on_calculate_reference()
  146. if point == 'fail':
  147. return
  148. self.app.worker_task.emit({'fcn': self.on_flip, 'params': [axis, point]})
  149. def on_flipy(self):
  150. axis = 'X'
  151. point = self.on_calculate_reference()
  152. if point == 'fail':
  153. return
  154. self.app.worker_task.emit({'fcn': self.on_flip, 'params': [axis, point]})
  155. def on_skewx(self):
  156. xvalue = float(self.ui.skewx_entry.get_value())
  157. if xvalue == 0:
  158. return
  159. if self.ui.skew_link_cb.get_value():
  160. yvalue = xvalue
  161. else:
  162. yvalue = 0
  163. axis = 'X'
  164. point = self.on_calculate_reference()
  165. if point == 'fail':
  166. return
  167. self.app.worker_task.emit({'fcn': self.on_skew, 'params': [axis, xvalue, yvalue, point]})
  168. def on_skewy(self):
  169. xvalue = 0
  170. yvalue = float(self.ui.skewy_entry.get_value())
  171. if yvalue == 0:
  172. return
  173. axis = 'Y'
  174. point = self.on_calculate_reference()
  175. if point == 'fail':
  176. return
  177. self.app.worker_task.emit({'fcn': self.on_skew, 'params': [axis, xvalue, yvalue, point]})
  178. def on_scalex(self):
  179. xvalue = float(self.ui.scalex_entry.get_value())
  180. if xvalue == 0 or xvalue == 1:
  181. self.app.inform.emit('[WARNING_NOTCL] %s' %
  182. _("Scale transformation can not be done for a factor of 0 or 1."))
  183. return
  184. if self.ui.scale_link_cb.get_value():
  185. yvalue = xvalue
  186. else:
  187. yvalue = 1
  188. axis = 'X'
  189. point = self.on_calculate_reference()
  190. if point == 'fail':
  191. return
  192. self.app.worker_task.emit({'fcn': self.on_scale, 'params': [axis, xvalue, yvalue, point]})
  193. def on_scaley(self):
  194. xvalue = 1
  195. yvalue = float(self.ui.scaley_entry.get_value())
  196. if yvalue == 0 or yvalue == 1:
  197. self.app.inform.emit('[WARNING_NOTCL] %s' %
  198. _("Scale transformation can not be done for a factor of 0 or 1."))
  199. return
  200. axis = 'Y'
  201. point = self.on_calculate_reference()
  202. if point == 'fail':
  203. return
  204. self.app.worker_task.emit({'fcn': self.on_scale, 'params': [axis, xvalue, yvalue, point]})
  205. def on_offx(self):
  206. value = float(self.ui.offx_entry.get_value())
  207. if value == 0:
  208. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Offset transformation can not be done for a value of 0."))
  209. return
  210. axis = 'X'
  211. self.app.worker_task.emit({'fcn': self.on_offset, 'params': [axis, value]})
  212. def on_offy(self):
  213. value = float(self.ui.offy_entry.get_value())
  214. if value == 0:
  215. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Offset transformation can not be done for a value of 0."))
  216. return
  217. axis = 'Y'
  218. self.app.worker_task.emit({'fcn': self.on_offset, 'params': [axis, value]})
  219. def on_buffer_by_distance(self):
  220. value = self.ui.buffer_entry.get_value()
  221. join = 1 if self.ui.buffer_rounded_cb.get_value() else 2
  222. self.app.worker_task.emit({'fcn': self.on_buffer_action, 'params': [value, join]})
  223. def on_buffer_by_factor(self):
  224. value = 1 + self.ui.buffer_factor_entry.get_value() / 100.0
  225. join = 1 if self.ui.buffer_rounded_cb.get_value() else 2
  226. # tell the buffer method to use the factor
  227. factor = True
  228. self.app.worker_task.emit({'fcn': self.on_buffer_action, 'params': [value, join, factor]})
  229. def on_rotate_action(self, num, point):
  230. obj_list = self.app.collection.get_selected()
  231. if not obj_list:
  232. self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object is selected."))
  233. return
  234. else:
  235. with self.app.proc_container.new(_("Appying Rotate")):
  236. try:
  237. px, py = point
  238. for sel_obj in obj_list:
  239. if sel_obj.kind == 'cncjob':
  240. self.app.inform.emit(_("CNCJob objects can't be rotated."))
  241. else:
  242. sel_obj.rotate(-num, point=(px, py))
  243. self.app.app_obj.object_changed.emit(sel_obj)
  244. # add information to the object that it was changed and how much
  245. sel_obj.options['rotate'] = num
  246. sel_obj.plot()
  247. self.app.inform.emit('[success] %s...' % _('Rotate done'))
  248. except Exception as e:
  249. self.app.inform.emit('[ERROR_NOTCL] %s: %s.' % (_("Action was not executed"), str(e)))
  250. return
  251. def on_flip(self, axis, point):
  252. obj_list = self.app.collection.get_selected()
  253. if not obj_list:
  254. self.app.inform.emit('[WARNING_NOTCL] %s!' % _("No object is selected."))
  255. return
  256. else:
  257. with self.app.proc_container.new(_("Applying Flip")):
  258. try:
  259. px, py = point
  260. # execute mirroring
  261. for sel_obj in obj_list:
  262. if sel_obj.kind == 'cncjob':
  263. self.app.inform.emit(_("CNCJob objects can't be mirrored/flipped."))
  264. else:
  265. if axis == 'X':
  266. sel_obj.mirror('X', (px, py))
  267. # add information to the object that it was changed and how much
  268. # the axis is reversed because of the reference
  269. if 'mirror_y' in sel_obj.options:
  270. sel_obj.options['mirror_y'] = not sel_obj.options['mirror_y']
  271. else:
  272. sel_obj.options['mirror_y'] = True
  273. self.app.inform.emit('[success] %s...' % _('Flip on Y axis done'))
  274. elif axis == 'Y':
  275. sel_obj.mirror('Y', (px, py))
  276. # add information to the object that it was changed and how much
  277. # the axis is reversed because of the reference
  278. if 'mirror_x' in sel_obj.options:
  279. sel_obj.options['mirror_x'] = not sel_obj.options['mirror_x']
  280. else:
  281. sel_obj.options['mirror_x'] = True
  282. self.app.inform.emit('[success] %s...' % _('Flip on X axis done'))
  283. self.app.app_obj.object_changed.emit(sel_obj)
  284. sel_obj.plot()
  285. except Exception as e:
  286. self.app.inform.emit('[ERROR_NOTCL] %s: %s.' % (_("Action was not executed"), str(e)))
  287. return
  288. def on_skew(self, axis, xvalue, yvalue, point):
  289. obj_list = self.app.collection.get_selected()
  290. if xvalue in [90, 180] or yvalue in [90, 180] or xvalue == yvalue == 0:
  291. self.app.inform.emit('[WARNING_NOTCL] %s' %
  292. _("Skew transformation can not be done for 0, 90 and 180 degrees."))
  293. return
  294. if not obj_list:
  295. self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object is selected."))
  296. return
  297. else:
  298. with self.app.proc_container.new(_("Applying Skew")):
  299. try:
  300. px, py = point
  301. for sel_obj in obj_list:
  302. if sel_obj.kind == 'cncjob':
  303. self.app.inform.emit(_("CNCJob objects can't be skewed."))
  304. else:
  305. sel_obj.skew(xvalue, yvalue, point=(px, py))
  306. # add information to the object that it was changed and how much
  307. sel_obj.options['skew_x'] = xvalue
  308. sel_obj.options['skew_y'] = yvalue
  309. self.app.app_obj.object_changed.emit(sel_obj)
  310. sel_obj.plot()
  311. self.app.inform.emit('[success] %s %s %s...' % (_('Skew on the'), str(axis), _("axis done")))
  312. except Exception as e:
  313. self.app.inform.emit('[ERROR_NOTCL] %s: %s.' % (_("Action was not executed"), str(e)))
  314. return
  315. def on_scale(self, axis, xfactor, yfactor, point=None):
  316. obj_list = self.app.collection.get_selected()
  317. if not obj_list:
  318. self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object is selected."))
  319. return
  320. else:
  321. with self.app.proc_container.new(_("Applying Scale")):
  322. try:
  323. px, py = point
  324. for sel_obj in obj_list:
  325. if sel_obj.kind == 'cncjob':
  326. self.app.inform.emit(_("CNCJob objects can't be scaled."))
  327. else:
  328. sel_obj.scale(xfactor, yfactor, point=(px, py))
  329. # add information to the object that it was changed and how much
  330. sel_obj.options['scale_x'] = xfactor
  331. sel_obj.options['scale_y'] = yfactor
  332. self.app.app_obj.object_changed.emit(sel_obj)
  333. sel_obj.plot()
  334. self.app.inform.emit('[success] %s %s %s...' % (_('Scale on the'), str(axis), _('axis done')))
  335. except Exception as e:
  336. self.app.inform.emit('[ERROR_NOTCL] %s: %s.' % (_("Action was not executed"), str(e)))
  337. return
  338. def on_offset(self, axis, num):
  339. obj_list = self.app.collection.get_selected()
  340. if not obj_list:
  341. self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object is selected."))
  342. return
  343. else:
  344. with self.app.proc_container.new(_("Applying Offset")):
  345. try:
  346. for sel_obj in obj_list:
  347. if sel_obj.kind == 'cncjob':
  348. self.app.inform.emit(_("CNCJob objects can't be offset."))
  349. else:
  350. if axis == 'X':
  351. sel_obj.offset((num, 0))
  352. # add information to the object that it was changed and how much
  353. sel_obj.options['offset_x'] = num
  354. elif axis == 'Y':
  355. sel_obj.offset((0, num))
  356. # add information to the object that it was changed and how much
  357. sel_obj.options['offset_y'] = num
  358. self.app.app_obj.object_changed.emit(sel_obj)
  359. sel_obj.plot()
  360. self.app.inform.emit('[success] %s %s %s...' % (_('Offset on the'), str(axis), _('axis done')))
  361. except Exception as e:
  362. self.app.inform.emit('[ERROR_NOTCL] %s: %s.' % (_("Action was not executed"), str(e)))
  363. return
  364. def on_buffer_action(self, value, join, factor=None):
  365. obj_list = self.app.collection.get_selected()
  366. if not obj_list:
  367. self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object is selected."))
  368. return
  369. else:
  370. with self.app.proc_container.new(_("Applying Buffer")):
  371. try:
  372. for sel_obj in obj_list:
  373. if sel_obj.kind == 'cncjob':
  374. self.app.inform.emit(_("CNCJob objects can't be buffered."))
  375. elif sel_obj.kind.lower() == 'gerber':
  376. sel_obj.buffer(value, join, factor)
  377. sel_obj.source_file = self.app.f_handlers.export_gerber(obj_name=sel_obj.options['name'],
  378. filename=None, local_use=sel_obj,
  379. use_thread=False)
  380. elif sel_obj.kind.lower() == 'excellon':
  381. sel_obj.buffer(value, join, factor)
  382. sel_obj.source_file = self.app.f_handlers.export_excellon(obj_name=sel_obj.options['name'],
  383. filename=None, local_use=sel_obj,
  384. use_thread=False)
  385. elif sel_obj.kind.lower() == 'geometry':
  386. sel_obj.buffer(value, join, factor)
  387. self.app.app_obj.object_changed.emit(sel_obj)
  388. sel_obj.plot()
  389. self.app.inform.emit('[success] %s...' % _('Buffer done'))
  390. except Exception as e:
  391. self.app.log.debug("ToolTransform.on_buffer_action() --> %s" % str(e))
  392. self.app.inform.emit('[ERROR_NOTCL] %s: %s.' % (_("Action was not executed"), str(e)))
  393. return
  394. @staticmethod
  395. def alt_bounds(obj_list):
  396. """
  397. Returns coordinates of rectangular bounds
  398. of an object with geometry: (xmin, ymin, xmax, ymax).
  399. """
  400. def bounds_rec(lst):
  401. minx = np.inf
  402. miny = np.inf
  403. maxx = -np.inf
  404. maxy = -np.inf
  405. try:
  406. for obj in lst:
  407. if obj.kind != 'cncjob':
  408. minx_, miny_, maxx_, maxy_ = bounds_rec(obj)
  409. minx = min(minx, minx_)
  410. miny = min(miny, miny_)
  411. maxx = max(maxx, maxx_)
  412. maxy = max(maxy, maxy_)
  413. return minx, miny, maxx, maxy
  414. except TypeError:
  415. # it's an object, return it's bounds
  416. return lst.bounds()
  417. return bounds_rec(obj_list)
  418. class TransformUI:
  419. toolName = _("Object Transform")
  420. rotateName = _("Rotate")
  421. skewName = _("Skew/Shear")
  422. scaleName = _("Scale")
  423. flipName = _("Mirror (Flip)")
  424. offsetName = _("Offset")
  425. bufferName = _("Buffer")
  426. def __init__(self, layout, app):
  427. self.app = app
  428. self.decimals = self.app.decimals
  429. self.layout = layout
  430. # ## Title
  431. title_label = FCLabel("%s" % self.toolName)
  432. title_label.setStyleSheet("""
  433. QLabel
  434. {
  435. font-size: 16px;
  436. font-weight: bold;
  437. }
  438. """)
  439. self.layout.addWidget(title_label)
  440. self.layout.addWidget(FCLabel(""))
  441. # ## Layout
  442. grid0 = QtWidgets.QGridLayout()
  443. self.layout.addLayout(grid0)
  444. grid0.setColumnStretch(0, 0)
  445. grid0.setColumnStretch(1, 1)
  446. grid0.setColumnStretch(2, 0)
  447. grid0.addWidget(FCLabel(''))
  448. # Reference
  449. ref_label = FCLabel('%s:' % _("Reference"))
  450. ref_label.setToolTip(
  451. _("The reference point for Rotate, Skew, Scale, Mirror.\n"
  452. "Can be:\n"
  453. "- Origin -> it is the 0, 0 point\n"
  454. "- Selection -> the center of the bounding box of the selected objects\n"
  455. "- Point -> a custom point defined by X,Y coordinates\n"
  456. "- Object -> the center of the bounding box of a specific object")
  457. )
  458. self.ref_combo = FCComboBox()
  459. self.ref_items = [_("Origin"), _("Selection"), _("Point"), _("Object")]
  460. self.ref_combo.addItems(self.ref_items)
  461. grid0.addWidget(ref_label, 0, 0)
  462. grid0.addWidget(self.ref_combo, 0, 1, 1, 2)
  463. self.point_label = FCLabel('%s:' % _("Value"))
  464. self.point_label.setToolTip(
  465. _("A point of reference in format X,Y.")
  466. )
  467. self.point_entry = NumericalEvalTupleEntry()
  468. grid0.addWidget(self.point_label, 1, 0)
  469. grid0.addWidget(self.point_entry, 1, 1, 1, 2)
  470. self.point_button = FCButton(_("Add"))
  471. self.point_button.setToolTip(
  472. _("Add point coordinates from clipboard.")
  473. )
  474. grid0.addWidget(self.point_button, 2, 0, 1, 3)
  475. # Type of object to be used as reference
  476. self.type_object_label = FCLabel('%s:' % _("Type"))
  477. self.type_object_label.setToolTip(
  478. _("The type of object used as reference.")
  479. )
  480. self.type_obj_combo = FCComboBox()
  481. self.type_obj_combo.addItem(_("Gerber"))
  482. self.type_obj_combo.addItem(_("Excellon"))
  483. self.type_obj_combo.addItem(_("Geometry"))
  484. self.type_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
  485. self.type_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
  486. self.type_obj_combo.setItemIcon(2, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
  487. grid0.addWidget(self.type_object_label, 3, 0)
  488. grid0.addWidget(self.type_obj_combo, 3, 1, 1, 2)
  489. # Object to be used as reference
  490. self.object_combo = FCComboBox()
  491. self.object_combo.setModel(self.app.collection)
  492. self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  493. self.object_combo.is_last = True
  494. self.object_combo.setToolTip(
  495. _("The object used as reference.\n"
  496. "The used point is the center of it's bounding box.")
  497. )
  498. grid0.addWidget(self.object_combo, 4, 0, 1, 3)
  499. separator_line = QtWidgets.QFrame()
  500. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  501. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  502. grid0.addWidget(separator_line, 5, 0, 1, 3)
  503. # ## Rotate Title
  504. rotate_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.rotateName)
  505. grid0.addWidget(rotate_title_label, 6, 0, 1, 3)
  506. self.rotate_label = FCLabel('%s:' % _("Angle"))
  507. self.rotate_label.setToolTip(
  508. _("Angle, in degrees.\n"
  509. "Float number between -360 and 359.\n"
  510. "Positive numbers for CW motion.\n"
  511. "Negative numbers for CCW motion.")
  512. )
  513. self.rotate_entry = FCDoubleSpinner(callback=self.confirmation_message)
  514. self.rotate_entry.set_precision(self.decimals)
  515. self.rotate_entry.setSingleStep(45)
  516. self.rotate_entry.setWrapping(True)
  517. self.rotate_entry.set_range(-360, 360)
  518. # self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
  519. self.rotate_button = FCButton(_("Rotate"))
  520. self.rotate_button.setToolTip(
  521. _("Rotate the selected object(s).\n"
  522. "The point of reference is the middle of\n"
  523. "the bounding box for all selected objects.")
  524. )
  525. self.rotate_button.setMinimumWidth(90)
  526. grid0.addWidget(self.rotate_label, 7, 0)
  527. grid0.addWidget(self.rotate_entry, 7, 1)
  528. grid0.addWidget(self.rotate_button, 7, 2)
  529. separator_line = QtWidgets.QFrame()
  530. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  531. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  532. grid0.addWidget(separator_line, 8, 0, 1, 3)
  533. # ## Skew Title
  534. skew_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.skewName)
  535. grid0.addWidget(skew_title_label, 9, 0, 1, 2)
  536. self.skew_link_cb = FCCheckBox()
  537. self.skew_link_cb.setText(_("Link"))
  538. self.skew_link_cb.setToolTip(
  539. _("Link the Y entry to X entry and copy its content.")
  540. )
  541. grid0.addWidget(self.skew_link_cb, 9, 2)
  542. self.skewx_label = FCLabel('%s:' % _("X angle"))
  543. self.skewx_label.setToolTip(
  544. _("Angle for Skew action, in degrees.\n"
  545. "Float number between -360 and 360.")
  546. )
  547. self.skewx_entry = FCDoubleSpinner(callback=self.confirmation_message)
  548. # self.skewx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
  549. self.skewx_entry.set_precision(self.decimals)
  550. self.skewx_entry.set_range(-360, 360)
  551. self.skewx_button = FCButton(_("Skew X"))
  552. self.skewx_button.setToolTip(
  553. _("Skew/shear the selected object(s).\n"
  554. "The point of reference is the middle of\n"
  555. "the bounding box for all selected objects."))
  556. self.skewx_button.setMinimumWidth(90)
  557. grid0.addWidget(self.skewx_label, 10, 0)
  558. grid0.addWidget(self.skewx_entry, 10, 1)
  559. grid0.addWidget(self.skewx_button, 10, 2)
  560. self.skewy_label = FCLabel('%s:' % _("Y angle"))
  561. self.skewy_label.setToolTip(
  562. _("Angle for Skew action, in degrees.\n"
  563. "Float number between -360 and 360.")
  564. )
  565. self.skewy_entry = FCDoubleSpinner(callback=self.confirmation_message)
  566. # self.skewy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
  567. self.skewy_entry.set_precision(self.decimals)
  568. self.skewy_entry.set_range(-360, 360)
  569. self.skewy_button = FCButton(_("Skew Y"))
  570. self.skewy_button.setToolTip(
  571. _("Skew/shear the selected object(s).\n"
  572. "The point of reference is the middle of\n"
  573. "the bounding box for all selected objects."))
  574. self.skewy_button.setMinimumWidth(90)
  575. grid0.addWidget(self.skewy_label, 12, 0)
  576. grid0.addWidget(self.skewy_entry, 12, 1)
  577. grid0.addWidget(self.skewy_button, 12, 2)
  578. self.ois_sk = OptionalInputSection(self.skew_link_cb, [self.skewy_label, self.skewy_entry, self.skewy_button],
  579. logic=False)
  580. separator_line = QtWidgets.QFrame()
  581. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  582. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  583. grid0.addWidget(separator_line, 14, 0, 1, 3)
  584. # ## Scale Title
  585. scale_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.scaleName)
  586. grid0.addWidget(scale_title_label, 15, 0, 1, 2)
  587. self.scale_link_cb = FCCheckBox()
  588. self.scale_link_cb.setText(_("Link"))
  589. self.scale_link_cb.setToolTip(
  590. _("Link the Y entry to X entry and copy its content.")
  591. )
  592. grid0.addWidget(self.scale_link_cb, 15, 2)
  593. self.scalex_label = FCLabel('%s:' % _("X factor"))
  594. self.scalex_label.setToolTip(
  595. _("Factor for scaling on X axis.")
  596. )
  597. self.scalex_entry = FCDoubleSpinner(callback=self.confirmation_message)
  598. # self.scalex_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
  599. self.scalex_entry.set_precision(self.decimals)
  600. self.scalex_entry.setMinimum(-1e6)
  601. self.scalex_button = FCButton(_("Scale X"))
  602. self.scalex_button.setToolTip(
  603. _("Scale the selected object(s).\n"
  604. "The point of reference depends on \n"
  605. "the Scale reference checkbox state."))
  606. self.scalex_button.setMinimumWidth(90)
  607. grid0.addWidget(self.scalex_label, 17, 0)
  608. grid0.addWidget(self.scalex_entry, 17, 1)
  609. grid0.addWidget(self.scalex_button, 17, 2)
  610. self.scaley_label = FCLabel('%s:' % _("Y factor"))
  611. self.scaley_label.setToolTip(
  612. _("Factor for scaling on Y axis.")
  613. )
  614. self.scaley_entry = FCDoubleSpinner(callback=self.confirmation_message)
  615. # self.scaley_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
  616. self.scaley_entry.set_precision(self.decimals)
  617. self.scaley_entry.setMinimum(-1e6)
  618. self.scaley_button = FCButton(_("Scale Y"))
  619. self.scaley_button.setToolTip(
  620. _("Scale the selected object(s).\n"
  621. "The point of reference depends on \n"
  622. "the Scale reference checkbox state."))
  623. self.scaley_button.setMinimumWidth(90)
  624. grid0.addWidget(self.scaley_label, 19, 0)
  625. grid0.addWidget(self.scaley_entry, 19, 1)
  626. grid0.addWidget(self.scaley_button, 19, 2)
  627. self.ois_s = OptionalInputSection(self.scale_link_cb,
  628. [
  629. self.scaley_label,
  630. self.scaley_entry,
  631. self.scaley_button
  632. ], logic=False)
  633. separator_line = QtWidgets.QFrame()
  634. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  635. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  636. grid0.addWidget(separator_line, 21, 0, 1, 3)
  637. # ## Flip Title
  638. flip_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.flipName)
  639. grid0.addWidget(flip_title_label, 23, 0, 1, 3)
  640. self.flipx_button = FCButton(_("Flip on X"))
  641. self.flipx_button.setToolTip(
  642. _("Flip the selected object(s) over the X axis.")
  643. )
  644. self.flipy_button = FCButton(_("Flip on Y"))
  645. self.flipy_button.setToolTip(
  646. _("Flip the selected object(s) over the X axis.")
  647. )
  648. hlay0 = QtWidgets.QHBoxLayout()
  649. grid0.addLayout(hlay0, 25, 0, 1, 3)
  650. hlay0.addWidget(self.flipx_button)
  651. hlay0.addWidget(self.flipy_button)
  652. separator_line = QtWidgets.QFrame()
  653. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  654. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  655. grid0.addWidget(separator_line, 27, 0, 1, 3)
  656. # ## Offset Title
  657. offset_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.offsetName)
  658. grid0.addWidget(offset_title_label, 29, 0, 1, 3)
  659. self.offx_label = FCLabel('%s:' % _("X val"))
  660. self.offx_label.setToolTip(
  661. _("Distance to offset on X axis. In current units.")
  662. )
  663. self.offx_entry = FCDoubleSpinner(callback=self.confirmation_message)
  664. # self.offx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
  665. self.offx_entry.set_precision(self.decimals)
  666. self.offx_entry.setMinimum(-1e6)
  667. self.offx_button = FCButton(_("Offset X"))
  668. self.offx_button.setToolTip(
  669. _("Offset the selected object(s).\n"
  670. "The point of reference is the middle of\n"
  671. "the bounding box for all selected objects.\n"))
  672. self.offx_button.setMinimumWidth(90)
  673. grid0.addWidget(self.offx_label, 31, 0)
  674. grid0.addWidget(self.offx_entry, 31, 1)
  675. grid0.addWidget(self.offx_button, 31, 2)
  676. self.offy_label = FCLabel('%s:' % _("Y val"))
  677. self.offy_label.setToolTip(
  678. _("Distance to offset on Y axis. In current units.")
  679. )
  680. self.offy_entry = FCDoubleSpinner(callback=self.confirmation_message)
  681. # self.offy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
  682. self.offy_entry.set_precision(self.decimals)
  683. self.offy_entry.setMinimum(-1e6)
  684. self.offy_button = FCButton(_("Offset Y"))
  685. self.offy_button.setToolTip(
  686. _("Offset the selected object(s).\n"
  687. "The point of reference is the middle of\n"
  688. "the bounding box for all selected objects.\n"))
  689. self.offy_button.setMinimumWidth(90)
  690. grid0.addWidget(self.offy_label, 32, 0)
  691. grid0.addWidget(self.offy_entry, 32, 1)
  692. grid0.addWidget(self.offy_button, 32, 2)
  693. separator_line = QtWidgets.QFrame()
  694. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  695. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  696. grid0.addWidget(separator_line, 34, 0, 1, 3)
  697. # ## Buffer Title
  698. buffer_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.bufferName)
  699. grid0.addWidget(buffer_title_label, 35, 0, 1, 2)
  700. self.buffer_rounded_cb = FCCheckBox('%s' % _("Rounded"))
  701. self.buffer_rounded_cb.setToolTip(
  702. _("If checked then the buffer will surround the buffered shape,\n"
  703. "every corner will be rounded.\n"
  704. "If not checked then the buffer will follow the exact geometry\n"
  705. "of the buffered shape.")
  706. )
  707. grid0.addWidget(self.buffer_rounded_cb, 35, 2)
  708. self.buffer_label = FCLabel('%s:' % _("Distance"))
  709. self.buffer_label.setToolTip(
  710. _("A positive value will create the effect of dilation,\n"
  711. "while a negative value will create the effect of erosion.\n"
  712. "Each geometry element of the object will be increased\n"
  713. "or decreased with the 'distance'.")
  714. )
  715. self.buffer_entry = FCDoubleSpinner(callback=self.confirmation_message)
  716. self.buffer_entry.set_precision(self.decimals)
  717. self.buffer_entry.setSingleStep(0.1)
  718. self.buffer_entry.setWrapping(True)
  719. self.buffer_entry.set_range(-10000.0000, 10000.0000)
  720. self.buffer_button = FCButton(_("Buffer D"))
  721. self.buffer_button.setToolTip(
  722. _("Create the buffer effect on each geometry,\n"
  723. "element from the selected object, using the distance.")
  724. )
  725. self.buffer_button.setMinimumWidth(90)
  726. grid0.addWidget(self.buffer_label, 37, 0)
  727. grid0.addWidget(self.buffer_entry, 37, 1)
  728. grid0.addWidget(self.buffer_button, 37, 2)
  729. self.buffer_factor_label = FCLabel('%s:' % _("Value"))
  730. self.buffer_factor_label.setToolTip(
  731. _("A positive value will create the effect of dilation,\n"
  732. "while a negative value will create the effect of erosion.\n"
  733. "Each geometry element of the object will be increased\n"
  734. "or decreased to fit the 'Value'. Value is a percentage\n"
  735. "of the initial dimension.")
  736. )
  737. self.buffer_factor_entry = FCDoubleSpinner(callback=self.confirmation_message, suffix='%')
  738. self.buffer_factor_entry.set_range(-100.0000, 1000.0000)
  739. self.buffer_factor_entry.set_precision(self.decimals)
  740. self.buffer_factor_entry.setWrapping(True)
  741. self.buffer_factor_entry.setSingleStep(1)
  742. self.buffer_factor_button = FCButton(_("Buffer F"))
  743. self.buffer_factor_button.setToolTip(
  744. _("Create the buffer effect on each geometry,\n"
  745. "element from the selected object, using the factor.")
  746. )
  747. self.buffer_factor_button.setMinimumWidth(90)
  748. grid0.addWidget(self.buffer_factor_label, 38, 0)
  749. grid0.addWidget(self.buffer_factor_entry, 38, 1)
  750. grid0.addWidget(self.buffer_factor_button, 38, 2)
  751. grid0.addWidget(FCLabel(''), 42, 0, 1, 3)
  752. self.layout.addStretch()
  753. # ## Reset Tool
  754. self.reset_button = FCButton(_("Reset Tool"))
  755. self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
  756. self.reset_button.setToolTip(
  757. _("Will reset the tool parameters.")
  758. )
  759. self.reset_button.setStyleSheet("""
  760. QPushButton
  761. {
  762. font-weight: bold;
  763. }
  764. """)
  765. self.layout.addWidget(self.reset_button)
  766. # #################################### FINSIHED GUI ###########################
  767. # #############################################################################
  768. def on_reference_changed(self, index):
  769. if index == 0 or index == 1: # "Origin" or "Selection" reference
  770. self.point_label.hide()
  771. self.point_entry.hide()
  772. self.point_button.hide()
  773. self.type_object_label.hide()
  774. self.type_obj_combo.hide()
  775. self.object_combo.hide()
  776. elif index == 2: # "Point" reference
  777. self.point_label.show()
  778. self.point_entry.show()
  779. self.point_button.show()
  780. self.type_object_label.hide()
  781. self.type_obj_combo.hide()
  782. self.object_combo.hide()
  783. else: # "Object" reference
  784. self.point_label.hide()
  785. self.point_entry.hide()
  786. self.point_button.hide()
  787. self.type_object_label.show()
  788. self.type_obj_combo.show()
  789. self.object_combo.show()
  790. def confirmation_message(self, accepted, minval, maxval):
  791. if accepted is False:
  792. self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
  793. self.decimals,
  794. minval,
  795. self.decimals,
  796. maxval), False)
  797. else:
  798. self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
  799. def confirmation_message_int(self, accepted, minval, maxval):
  800. if accepted is False:
  801. self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
  802. (_("Edited value is out of range"), minval, maxval), False)
  803. else:
  804. self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)