ToolTransform.py 39 KB

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