ToolTransform.py 40 KB

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