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. 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, %s.' %
  246. (_("Due of"), str(e), _("action was not executed.")))
  247. return
  248. def on_flip(self, axis, point):
  249. obj_list = self.app.collection.get_selected()
  250. if not obj_list:
  251. self.app.inform.emit('[WARNING_NOTCL] %s!' % _("No object selected. Please Select an object to flip"))
  252. return
  253. else:
  254. with self.app.proc_container.new(_("Applying Flip")):
  255. try:
  256. px, py = point
  257. # execute mirroring
  258. for sel_obj in obj_list:
  259. if sel_obj.kind == 'cncjob':
  260. self.app.inform.emit(_("CNCJob objects can't be mirrored/flipped."))
  261. else:
  262. if axis == 'X':
  263. sel_obj.mirror('X', (px, py))
  264. # add information to the object that it was changed and how much
  265. # the axis is reversed because of the reference
  266. if 'mirror_y' in sel_obj.options:
  267. sel_obj.options['mirror_y'] = not sel_obj.options['mirror_y']
  268. else:
  269. sel_obj.options['mirror_y'] = True
  270. self.app.inform.emit('[success] %s...' % _('Flip on the Y axis done'))
  271. elif axis == 'Y':
  272. sel_obj.mirror('Y', (px, py))
  273. # add information to the object that it was changed and how much
  274. # the axis is reversed because of the reference
  275. if 'mirror_x' in sel_obj.options:
  276. sel_obj.options['mirror_x'] = not sel_obj.options['mirror_x']
  277. else:
  278. sel_obj.options['mirror_x'] = True
  279. self.app.inform.emit('[success] %s...' % _('Flip on the X axis done'))
  280. self.app.app_obj.object_changed.emit(sel_obj)
  281. sel_obj.plot()
  282. except Exception as e:
  283. self.app.inform.emit('[ERROR_NOTCL] %s %s, %s.' %
  284. (_("Due of"), str(e), _("action was not executed.")))
  285. return
  286. def on_skew(self, axis, xvalue, yvalue, point):
  287. obj_list = self.app.collection.get_selected()
  288. if xvalue in [90, 180] or yvalue in [90, 180] or xvalue == yvalue == 0:
  289. self.app.inform.emit('[WARNING_NOTCL] %s' %
  290. _("Skew transformation can not be done for 0, 90 and 180 degrees."))
  291. return
  292. if not obj_list:
  293. self.app.inform.emit('[WARNING_NOTCL] %s' %
  294. _("No object selected. Please Select an object to shear/skew!"))
  295. return
  296. else:
  297. with self.app.proc_container.new(_("Applying Skew")):
  298. try:
  299. px, py = point
  300. for sel_obj in obj_list:
  301. if sel_obj.kind == 'cncjob':
  302. self.app.inform.emit(_("CNCJob objects can't be skewed."))
  303. else:
  304. sel_obj.skew(xvalue, yvalue, point=(px, py))
  305. # add information to the object that it was changed and how much
  306. sel_obj.options['skew_x'] = xvalue
  307. sel_obj.options['skew_y'] = yvalue
  308. self.app.app_obj.object_changed.emit(sel_obj)
  309. sel_obj.plot()
  310. self.app.inform.emit('[success] %s %s %s...' % (_('Skew on the'), str(axis), _("axis done")))
  311. except Exception as e:
  312. self.app.inform.emit('[ERROR_NOTCL] %s %s, %s.' %
  313. (_("Due of"), str(e), _("action was not executed.")))
  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 selected. Please Select an object to scale!"))
  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, %s.' %
  337. (_("Due of"), str(e), _("action was not executed.")))
  338. return
  339. def on_offset(self, axis, num):
  340. obj_list = self.app.collection.get_selected()
  341. if not obj_list:
  342. self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object selected. Please Select an object to offset!"))
  343. return
  344. else:
  345. with self.app.proc_container.new(_("Applying Offset")):
  346. try:
  347. for sel_obj in obj_list:
  348. if sel_obj.kind == 'cncjob':
  349. self.app.inform.emit(_("CNCJob objects can't be offset."))
  350. else:
  351. if axis == 'X':
  352. sel_obj.offset((num, 0))
  353. # add information to the object that it was changed and how much
  354. sel_obj.options['offset_x'] = num
  355. elif axis == 'Y':
  356. sel_obj.offset((0, num))
  357. # add information to the object that it was changed and how much
  358. sel_obj.options['offset_y'] = num
  359. self.app.app_obj.object_changed.emit(sel_obj)
  360. sel_obj.plot()
  361. self.app.inform.emit('[success] %s %s %s...' % (_('Offset on the'), str(axis), _('axis done')))
  362. except Exception as e:
  363. self.app.inform.emit('[ERROR_NOTCL] %s: %s.' % (_("Action was not executed, due of"), str(e)))
  364. return
  365. def on_buffer_action(self, value, join, factor=None):
  366. obj_list = self.app.collection.get_selected()
  367. if not obj_list:
  368. self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object selected. Please Select an object to buffer!"))
  369. return
  370. else:
  371. with self.app.proc_container.new(_("Applying Buffer")):
  372. try:
  373. for sel_obj in obj_list:
  374. if sel_obj.kind == 'cncjob':
  375. self.app.inform.emit(_("CNCJob objects can't be buffered."))
  376. elif sel_obj.kind.lower() == 'gerber':
  377. sel_obj.buffer(value, join, factor)
  378. sel_obj.source_file = self.app.f_handlers.export_gerber(obj_name=sel_obj.options['name'],
  379. filename=None, local_use=sel_obj,
  380. use_thread=False)
  381. elif sel_obj.kind.lower() == 'excellon':
  382. sel_obj.buffer(value, join, factor)
  383. sel_obj.source_file = self.app.f_handlers.export_excellon(obj_name=sel_obj.options['name'],
  384. filename=None, local_use=sel_obj,
  385. use_thread=False)
  386. elif sel_obj.kind.lower() == 'geometry':
  387. sel_obj.buffer(value, join, factor)
  388. self.app.app_obj.object_changed.emit(sel_obj)
  389. sel_obj.plot()
  390. self.app.inform.emit('[success] %s...' % _('Buffer done'))
  391. except Exception as e:
  392. self.app.log.debug("ToolTransform.on_buffer_action() --> %s" % str(e))
  393. self.app.inform.emit('[ERROR_NOTCL] %s: %s.' % (_("Action was not executed, due of"), str(e)))
  394. return
  395. @staticmethod
  396. def alt_bounds(obj_list):
  397. """
  398. Returns coordinates of rectangular bounds
  399. of an object with geometry: (xmin, ymin, xmax, ymax).
  400. """
  401. def bounds_rec(lst):
  402. minx = np.Inf
  403. miny = np.Inf
  404. maxx = -np.Inf
  405. maxy = -np.Inf
  406. try:
  407. for obj in lst:
  408. if obj.kind != 'cncjob':
  409. minx_, miny_, maxx_, maxy_ = bounds_rec(obj)
  410. minx = min(minx, minx_)
  411. miny = min(miny, miny_)
  412. maxx = max(maxx, maxx_)
  413. maxy = max(maxy, maxy_)
  414. return minx, miny, maxx, maxy
  415. except TypeError:
  416. # it's an object, return it's bounds
  417. return lst.bounds()
  418. return bounds_rec(obj_list)
  419. class TransformUI:
  420. toolName = _("Object Transform")
  421. rotateName = _("Rotate")
  422. skewName = _("Skew/Shear")
  423. scaleName = _("Scale")
  424. flipName = _("Mirror (Flip)")
  425. offsetName = _("Offset")
  426. bufferName = _("Buffer")
  427. def __init__(self, layout, app):
  428. self.app = app
  429. self.decimals = self.app.decimals
  430. self.layout = layout
  431. # ## Title
  432. title_label = FCLabel("%s" % self.toolName)
  433. title_label.setStyleSheet("""
  434. QLabel
  435. {
  436. font-size: 16px;
  437. font-weight: bold;
  438. }
  439. """)
  440. self.layout.addWidget(title_label)
  441. self.layout.addWidget(FCLabel(""))
  442. # ## Layout
  443. grid0 = QtWidgets.QGridLayout()
  444. self.layout.addLayout(grid0)
  445. grid0.setColumnStretch(0, 0)
  446. grid0.setColumnStretch(1, 1)
  447. grid0.setColumnStretch(2, 0)
  448. grid0.addWidget(FCLabel(''))
  449. # Reference
  450. ref_label = FCLabel('%s:' % _("Reference"))
  451. ref_label.setToolTip(
  452. _("The reference point for Rotate, Skew, Scale, Mirror.\n"
  453. "Can be:\n"
  454. "- Origin -> it is the 0, 0 point\n"
  455. "- Selection -> the center of the bounding box of the selected objects\n"
  456. "- Point -> a custom point defined by X,Y coordinates\n"
  457. "- Object -> the center of the bounding box of a specific object")
  458. )
  459. self.ref_combo = FCComboBox()
  460. self.ref_items = [_("Origin"), _("Selection"), _("Point"), _("Object")]
  461. self.ref_combo.addItems(self.ref_items)
  462. grid0.addWidget(ref_label, 0, 0)
  463. grid0.addWidget(self.ref_combo, 0, 1, 1, 2)
  464. self.point_label = FCLabel('%s:' % _("Value"))
  465. self.point_label.setToolTip(
  466. _("A point of reference in format X,Y.")
  467. )
  468. self.point_entry = NumericalEvalTupleEntry()
  469. grid0.addWidget(self.point_label, 1, 0)
  470. grid0.addWidget(self.point_entry, 1, 1, 1, 2)
  471. self.point_button = FCButton(_("Add"))
  472. self.point_button.setToolTip(
  473. _("Add point coordinates from clipboard.")
  474. )
  475. grid0.addWidget(self.point_button, 2, 0, 1, 3)
  476. # Type of object to be used as reference
  477. self.type_object_label = FCLabel('%s:' % _("Type"))
  478. self.type_object_label.setToolTip(
  479. _("The type of object used as reference.")
  480. )
  481. self.type_obj_combo = FCComboBox()
  482. self.type_obj_combo.addItem(_("Gerber"))
  483. self.type_obj_combo.addItem(_("Excellon"))
  484. self.type_obj_combo.addItem(_("Geometry"))
  485. self.type_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
  486. self.type_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
  487. self.type_obj_combo.setItemIcon(2, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
  488. grid0.addWidget(self.type_object_label, 3, 0)
  489. grid0.addWidget(self.type_obj_combo, 3, 1, 1, 2)
  490. # Object to be used as reference
  491. self.object_combo = FCComboBox()
  492. self.object_combo.setModel(self.app.collection)
  493. self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  494. self.object_combo.is_last = True
  495. self.object_combo.setToolTip(
  496. _("The object used as reference.\n"
  497. "The used point is the center of it's bounding box.")
  498. )
  499. grid0.addWidget(self.object_combo, 4, 0, 1, 3)
  500. separator_line = QtWidgets.QFrame()
  501. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  502. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  503. grid0.addWidget(separator_line, 5, 0, 1, 3)
  504. # ## Rotate Title
  505. rotate_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.rotateName)
  506. grid0.addWidget(rotate_title_label, 6, 0, 1, 3)
  507. self.rotate_label = FCLabel('%s:' % _("Angle"))
  508. self.rotate_label.setToolTip(
  509. _("Angle for Rotation action, in degrees.\n"
  510. "Float number between -360 and 359.\n"
  511. "Positive numbers for CW motion.\n"
  512. "Negative numbers for CCW motion.")
  513. )
  514. self.rotate_entry = FCDoubleSpinner(callback=self.confirmation_message)
  515. self.rotate_entry.set_precision(self.decimals)
  516. self.rotate_entry.setSingleStep(45)
  517. self.rotate_entry.setWrapping(True)
  518. self.rotate_entry.set_range(-360, 360)
  519. # self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
  520. self.rotate_button = FCButton(_("Rotate"))
  521. self.rotate_button.setToolTip(
  522. _("Rotate the selected object(s).\n"
  523. "The point of reference is the middle of\n"
  524. "the bounding box for all selected objects.")
  525. )
  526. self.rotate_button.setMinimumWidth(90)
  527. grid0.addWidget(self.rotate_label, 7, 0)
  528. grid0.addWidget(self.rotate_entry, 7, 1)
  529. grid0.addWidget(self.rotate_button, 7, 2)
  530. separator_line = QtWidgets.QFrame()
  531. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  532. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  533. grid0.addWidget(separator_line, 8, 0, 1, 3)
  534. # ## Skew Title
  535. skew_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.skewName)
  536. grid0.addWidget(skew_title_label, 9, 0, 1, 2)
  537. self.skew_link_cb = FCCheckBox()
  538. self.skew_link_cb.setText(_("Link"))
  539. self.skew_link_cb.setToolTip(
  540. _("Link the Y entry to X entry and copy its content.")
  541. )
  542. grid0.addWidget(self.skew_link_cb, 9, 2)
  543. self.skewx_label = FCLabel('%s:' % _("X angle"))
  544. self.skewx_label.setToolTip(
  545. _("Angle for Skew action, in degrees.\n"
  546. "Float number between -360 and 360.")
  547. )
  548. self.skewx_entry = FCDoubleSpinner(callback=self.confirmation_message)
  549. # self.skewx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
  550. self.skewx_entry.set_precision(self.decimals)
  551. self.skewx_entry.set_range(-360, 360)
  552. self.skewx_button = FCButton(_("Skew X"))
  553. self.skewx_button.setToolTip(
  554. _("Skew/shear the selected object(s).\n"
  555. "The point of reference is the middle of\n"
  556. "the bounding box for all selected objects."))
  557. self.skewx_button.setMinimumWidth(90)
  558. grid0.addWidget(self.skewx_label, 10, 0)
  559. grid0.addWidget(self.skewx_entry, 10, 1)
  560. grid0.addWidget(self.skewx_button, 10, 2)
  561. self.skewy_label = FCLabel('%s:' % _("Y angle"))
  562. self.skewy_label.setToolTip(
  563. _("Angle for Skew action, in degrees.\n"
  564. "Float number between -360 and 360.")
  565. )
  566. self.skewy_entry = FCDoubleSpinner(callback=self.confirmation_message)
  567. # self.skewy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
  568. self.skewy_entry.set_precision(self.decimals)
  569. self.skewy_entry.set_range(-360, 360)
  570. self.skewy_button = FCButton(_("Skew Y"))
  571. self.skewy_button.setToolTip(
  572. _("Skew/shear the selected object(s).\n"
  573. "The point of reference is the middle of\n"
  574. "the bounding box for all selected objects."))
  575. self.skewy_button.setMinimumWidth(90)
  576. grid0.addWidget(self.skewy_label, 12, 0)
  577. grid0.addWidget(self.skewy_entry, 12, 1)
  578. grid0.addWidget(self.skewy_button, 12, 2)
  579. self.ois_sk = OptionalInputSection(self.skew_link_cb, [self.skewy_label, self.skewy_entry, self.skewy_button],
  580. logic=False)
  581. separator_line = QtWidgets.QFrame()
  582. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  583. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  584. grid0.addWidget(separator_line, 14, 0, 1, 3)
  585. # ## Scale Title
  586. scale_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.scaleName)
  587. grid0.addWidget(scale_title_label, 15, 0, 1, 2)
  588. self.scale_link_cb = FCCheckBox()
  589. self.scale_link_cb.setText(_("Link"))
  590. self.scale_link_cb.setToolTip(
  591. _("Link the Y entry to X entry and copy its content.")
  592. )
  593. grid0.addWidget(self.scale_link_cb, 15, 2)
  594. self.scalex_label = FCLabel('%s:' % _("X factor"))
  595. self.scalex_label.setToolTip(
  596. _("Factor for scaling on X axis.")
  597. )
  598. self.scalex_entry = FCDoubleSpinner(callback=self.confirmation_message)
  599. # self.scalex_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
  600. self.scalex_entry.set_precision(self.decimals)
  601. self.scalex_entry.setMinimum(-1e6)
  602. self.scalex_button = FCButton(_("Scale X"))
  603. self.scalex_button.setToolTip(
  604. _("Scale the selected object(s).\n"
  605. "The point of reference depends on \n"
  606. "the Scale reference checkbox state."))
  607. self.scalex_button.setMinimumWidth(90)
  608. grid0.addWidget(self.scalex_label, 17, 0)
  609. grid0.addWidget(self.scalex_entry, 17, 1)
  610. grid0.addWidget(self.scalex_button, 17, 2)
  611. self.scaley_label = FCLabel('%s:' % _("Y factor"))
  612. self.scaley_label.setToolTip(
  613. _("Factor for scaling on Y axis.")
  614. )
  615. self.scaley_entry = FCDoubleSpinner(callback=self.confirmation_message)
  616. # self.scaley_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
  617. self.scaley_entry.set_precision(self.decimals)
  618. self.scaley_entry.setMinimum(-1e6)
  619. self.scaley_button = FCButton(_("Scale Y"))
  620. self.scaley_button.setToolTip(
  621. _("Scale the selected object(s).\n"
  622. "The point of reference depends on \n"
  623. "the Scale reference checkbox state."))
  624. self.scaley_button.setMinimumWidth(90)
  625. grid0.addWidget(self.scaley_label, 19, 0)
  626. grid0.addWidget(self.scaley_entry, 19, 1)
  627. grid0.addWidget(self.scaley_button, 19, 2)
  628. self.ois_s = OptionalInputSection(self.scale_link_cb,
  629. [
  630. self.scaley_label,
  631. self.scaley_entry,
  632. self.scaley_button
  633. ], logic=False)
  634. separator_line = QtWidgets.QFrame()
  635. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  636. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  637. grid0.addWidget(separator_line, 21, 0, 1, 3)
  638. # ## Flip Title
  639. flip_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.flipName)
  640. grid0.addWidget(flip_title_label, 23, 0, 1, 3)
  641. self.flipx_button = FCButton(_("Flip on X"))
  642. self.flipx_button.setToolTip(
  643. _("Flip the selected object(s) over the X axis.")
  644. )
  645. self.flipy_button = FCButton(_("Flip on Y"))
  646. self.flipy_button.setToolTip(
  647. _("Flip the selected object(s) over the X axis.")
  648. )
  649. hlay0 = QtWidgets.QHBoxLayout()
  650. grid0.addLayout(hlay0, 25, 0, 1, 3)
  651. hlay0.addWidget(self.flipx_button)
  652. hlay0.addWidget(self.flipy_button)
  653. separator_line = QtWidgets.QFrame()
  654. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  655. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  656. grid0.addWidget(separator_line, 27, 0, 1, 3)
  657. # ## Offset Title
  658. offset_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.offsetName)
  659. grid0.addWidget(offset_title_label, 29, 0, 1, 3)
  660. self.offx_label = FCLabel('%s:' % _("X val"))
  661. self.offx_label.setToolTip(
  662. _("Distance to offset on X axis. In current units.")
  663. )
  664. self.offx_entry = FCDoubleSpinner(callback=self.confirmation_message)
  665. # self.offx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
  666. self.offx_entry.set_precision(self.decimals)
  667. self.offx_entry.setMinimum(-1e6)
  668. self.offx_button = FCButton(_("Offset X"))
  669. self.offx_button.setToolTip(
  670. _("Offset the selected object(s).\n"
  671. "The point of reference is the middle of\n"
  672. "the bounding box for all selected objects.\n"))
  673. self.offx_button.setMinimumWidth(90)
  674. grid0.addWidget(self.offx_label, 31, 0)
  675. grid0.addWidget(self.offx_entry, 31, 1)
  676. grid0.addWidget(self.offx_button, 31, 2)
  677. self.offy_label = FCLabel('%s:' % _("Y val"))
  678. self.offy_label.setToolTip(
  679. _("Distance to offset on Y axis. In current units.")
  680. )
  681. self.offy_entry = FCDoubleSpinner(callback=self.confirmation_message)
  682. # self.offy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
  683. self.offy_entry.set_precision(self.decimals)
  684. self.offy_entry.setMinimum(-1e6)
  685. self.offy_button = FCButton(_("Offset Y"))
  686. self.offy_button.setToolTip(
  687. _("Offset the selected object(s).\n"
  688. "The point of reference is the middle of\n"
  689. "the bounding box for all selected objects.\n"))
  690. self.offy_button.setMinimumWidth(90)
  691. grid0.addWidget(self.offy_label, 32, 0)
  692. grid0.addWidget(self.offy_entry, 32, 1)
  693. grid0.addWidget(self.offy_button, 32, 2)
  694. separator_line = QtWidgets.QFrame()
  695. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  696. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  697. grid0.addWidget(separator_line, 34, 0, 1, 3)
  698. # ## Buffer Title
  699. buffer_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.bufferName)
  700. grid0.addWidget(buffer_title_label, 35, 0, 1, 2)
  701. self.buffer_rounded_cb = FCCheckBox('%s' % _("Rounded"))
  702. self.buffer_rounded_cb.setToolTip(
  703. _("If checked then the buffer will surround the buffered shape,\n"
  704. "every corner will be rounded.\n"
  705. "If not checked then the buffer will follow the exact geometry\n"
  706. "of the buffered shape.")
  707. )
  708. grid0.addWidget(self.buffer_rounded_cb, 35, 2)
  709. self.buffer_label = FCLabel('%s:' % _("Distance"))
  710. self.buffer_label.setToolTip(
  711. _("A positive value will create the effect of dilation,\n"
  712. "while a negative value will create the effect of erosion.\n"
  713. "Each geometry element of the object will be increased\n"
  714. "or decreased with the 'distance'.")
  715. )
  716. self.buffer_entry = FCDoubleSpinner(callback=self.confirmation_message)
  717. self.buffer_entry.set_precision(self.decimals)
  718. self.buffer_entry.setSingleStep(0.1)
  719. self.buffer_entry.setWrapping(True)
  720. self.buffer_entry.set_range(-9999.9999, 9999.9999)
  721. self.buffer_button = FCButton(_("Buffer D"))
  722. self.buffer_button.setToolTip(
  723. _("Create the buffer effect on each geometry,\n"
  724. "element from the selected object, using the distance.")
  725. )
  726. self.buffer_button.setMinimumWidth(90)
  727. grid0.addWidget(self.buffer_label, 37, 0)
  728. grid0.addWidget(self.buffer_entry, 37, 1)
  729. grid0.addWidget(self.buffer_button, 37, 2)
  730. self.buffer_factor_label = FCLabel('%s:' % _("Value"))
  731. self.buffer_factor_label.setToolTip(
  732. _("A positive value will create the effect of dilation,\n"
  733. "while a negative value will create the effect of erosion.\n"
  734. "Each geometry element of the object will be increased\n"
  735. "or decreased to fit the 'Value'. Value is a percentage\n"
  736. "of the initial dimension.")
  737. )
  738. self.buffer_factor_entry = FCDoubleSpinner(callback=self.confirmation_message, suffix='%')
  739. self.buffer_factor_entry.set_range(-100.0000, 1000.0000)
  740. self.buffer_factor_entry.set_precision(self.decimals)
  741. self.buffer_factor_entry.setWrapping(True)
  742. self.buffer_factor_entry.setSingleStep(1)
  743. self.buffer_factor_button = FCButton(_("Buffer F"))
  744. self.buffer_factor_button.setToolTip(
  745. _("Create the buffer effect on each geometry,\n"
  746. "element from the selected object, using the factor.")
  747. )
  748. self.buffer_factor_button.setMinimumWidth(90)
  749. grid0.addWidget(self.buffer_factor_label, 38, 0)
  750. grid0.addWidget(self.buffer_factor_entry, 38, 1)
  751. grid0.addWidget(self.buffer_factor_button, 38, 2)
  752. grid0.addWidget(FCLabel(''), 42, 0, 1, 3)
  753. self.layout.addStretch()
  754. # ## Reset Tool
  755. self.reset_button = FCButton(_("Reset Tool"))
  756. self.reset_button.setIcon(QtGui.QIcon(self.app.resource_location + '/reset32.png'))
  757. self.reset_button.setToolTip(
  758. _("Will reset the tool parameters.")
  759. )
  760. self.reset_button.setStyleSheet("""
  761. QPushButton
  762. {
  763. font-weight: bold;
  764. }
  765. """)
  766. self.layout.addWidget(self.reset_button)
  767. # #################################### FINSIHED GUI ###########################
  768. # #############################################################################
  769. def on_reference_changed(self, index):
  770. if index == 0 or index == 1: # "Origin" or "Selection" reference
  771. self.point_label.hide()
  772. self.point_entry.hide()
  773. self.point_button.hide()
  774. self.type_object_label.hide()
  775. self.type_obj_combo.hide()
  776. self.object_combo.hide()
  777. elif index == 2: # "Point" reference
  778. self.point_label.show()
  779. self.point_entry.show()
  780. self.point_button.show()
  781. self.type_object_label.hide()
  782. self.type_obj_combo.hide()
  783. self.object_combo.hide()
  784. else: # "Object" reference
  785. self.point_label.hide()
  786. self.point_entry.hide()
  787. self.point_button.hide()
  788. self.type_object_label.show()
  789. self.type_obj_combo.show()
  790. self.object_combo.show()
  791. def confirmation_message(self, accepted, minval, maxval):
  792. if accepted is False:
  793. self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
  794. self.decimals,
  795. minval,
  796. self.decimals,
  797. maxval), False)
  798. else:
  799. self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
  800. def confirmation_message_int(self, accepted, minval, maxval):
  801. if accepted is False:
  802. self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
  803. (_("Edited value is out of range"), minval, maxval), False)
  804. else:
  805. self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)