ToolTransform.py 41 KB

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