FlatCAMObj.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://flatcam.org #
  4. # Author: Juan Pablo Caram (c) #
  5. # Date: 2/5/2014 #
  6. # MIT Licence #
  7. # ##########################################################
  8. # ##########################################################
  9. # File modified by: Marius Stanciu #
  10. # ##########################################################
  11. import inspect # TODO: For debugging only.
  12. from AppGUI.ObjectUI import *
  13. from Common import LoudDict
  14. from AppGUI.PlotCanvasLegacy import ShapeCollectionLegacy
  15. import sys
  16. import gettext
  17. import AppTranslation as fcTranslate
  18. import builtins
  19. fcTranslate.apply_language('strings')
  20. if '_' not in builtins.__dict__:
  21. _ = gettext.gettext
  22. # Interrupts plotting process if FlatCAMObj has been deleted
  23. class ObjectDeleted(Exception):
  24. pass
  25. class ValidationError(Exception):
  26. def __init__(self, message, errors):
  27. super().__init__(message)
  28. self.errors = errors
  29. class FlatCAMObj(QtCore.QObject):
  30. """
  31. Base type of objects handled in FlatCAM. These become interactive
  32. in the AppGUI, can be plotted, and their options can be modified
  33. by the user in their respective forms.
  34. """
  35. # Instance of the application to which these are related.
  36. # The app should set this value.
  37. app = None
  38. # signal to plot a single object
  39. plot_single_object = QtCore.pyqtSignal()
  40. def __init__(self, name):
  41. """
  42. Constructor.
  43. :param name: Name of the object given by the user.
  44. :return: FlatCAMObj
  45. """
  46. QtCore.QObject.__init__(self)
  47. # View
  48. self.ui = None
  49. self.options = LoudDict(name=name)
  50. self.options.set_change_callback(self.on_options_change)
  51. self.form_fields = {}
  52. # store here the default data for Geometry Data
  53. self.default_data = {}
  54. # 2D mode
  55. # Axes must exist and be attached to canvas.
  56. self.axes = None
  57. self.kind = None # Override with proper name
  58. if self.app.is_legacy is False:
  59. self.shapes = self.app.plotcanvas.new_shape_group()
  60. # self.shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, pool=self.app.pool, layers=2)
  61. else:
  62. self.shapes = ShapeCollectionLegacy(obj=self, app=self.app, name=name)
  63. self.mark_shapes = {}
  64. self.item = None # Link with project view item
  65. self.muted_ui = False
  66. self.deleted = False
  67. try:
  68. self._drawing_tolerance = float(self.app.defaults["global_tolerance"]) if \
  69. self.app.defaults["global_tolerance"] else 0.01
  70. except ValueError:
  71. self._drawing_tolerance = 0.01
  72. self.isHovering = False
  73. self.notHovering = True
  74. # Flag to show if a selection shape is drawn
  75. self.selection_shape_drawn = False
  76. # self.units = 'IN'
  77. self.units = self.app.defaults['units']
  78. self.plot_single_object.connect(self.single_object_plot)
  79. def __del__(self):
  80. pass
  81. def __str__(self):
  82. return "<FlatCAMObj({:12s}): {:20s}>".format(self.kind, self.options["name"])
  83. def from_dict(self, d):
  84. """
  85. This supersedes ``from_dict`` in derived classes. Derived classes
  86. must inherit from FlatCAMObj first, then from derivatives of Geometry.
  87. ``self.options`` is only updated, not overwritten. This ensures that
  88. options set by the app do not vanish when reading the objects
  89. from a project file.
  90. :param d: Dictionary with attributes to set.
  91. :return: None
  92. """
  93. for attr in self.ser_attrs:
  94. if attr == 'options':
  95. self.options.update(d[attr])
  96. else:
  97. try:
  98. setattr(self, attr, d[attr])
  99. except KeyError:
  100. log.debug("FlatCAMObj.from_dict() --> KeyError: %s. "
  101. "Means that we are loading an old project that don't"
  102. "have all attributes in the latest application version." % str(attr))
  103. pass
  104. def on_options_change(self, key):
  105. # Update form on programmatically options change
  106. self.set_form_item(key)
  107. # Set object visibility
  108. if key == 'plot':
  109. self.visible = self.options['plot']
  110. self.optionChanged.emit(key)
  111. def set_ui(self, ui):
  112. self.ui = ui
  113. self.form_fields = {"name": self.ui.name_entry}
  114. assert isinstance(self.ui, ObjectUI)
  115. self.ui.name_entry.returnPressed.connect(self.on_name_activate)
  116. try:
  117. # it will raise an exception for those FlatCAM objects that do not build UI with the common elements
  118. self.ui.offset_button.clicked.connect(self.on_offset_button_click)
  119. except (TypeError, AttributeError):
  120. pass
  121. try:
  122. self.ui.scale_button.clicked.connect(self.on_scale_button_click)
  123. except (TypeError, AttributeError):
  124. pass
  125. try:
  126. self.ui.offsetvector_entry.returnPressed.connect(self.on_offset_button_click)
  127. except (TypeError, AttributeError):
  128. pass
  129. # Creates problems on focusOut
  130. try:
  131. self.ui.scale_entry.returnPressed.connect(self.on_scale_button_click)
  132. except (TypeError, AttributeError):
  133. pass
  134. try:
  135. self.ui.transformations_button.clicked.connect(self.app.transform_tool.run)
  136. except (TypeError, AttributeError):
  137. pass
  138. # self.ui.skew_button.clicked.connect(self.on_skew_button_click)
  139. def build_ui(self):
  140. """
  141. Sets up the UI/form for this object. Show the UI in the App.
  142. :return: None
  143. """
  144. self.muted_ui = True
  145. log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.build_ui()")
  146. try:
  147. # HACK: disconnect the scale entry signal since on focus out event will trigger an undesired scale()
  148. # it seems that the takewidget() does generate a focus out event for the QDoubleSpinbox ...
  149. # and reconnect after the takeWidget() is done
  150. # self.ui.scale_entry.returnPressed.disconnect(self.on_scale_button_click)
  151. self.app.ui.selected_scroll_area.takeWidget()
  152. # self.ui.scale_entry.returnPressed.connect(self.on_scale_button_click)
  153. except Exception as e:
  154. self.app.log.debug("FlatCAMObj.build_ui() --> Nothing to remove: %s" % str(e))
  155. self.app.ui.selected_scroll_area.setWidget(self.ui)
  156. # self.ui.setMinimumWidth(100)
  157. # self.ui.setMaximumWidth(self.app.ui.selected_tab.sizeHint().width())
  158. self.muted_ui = False
  159. def on_name_activate(self, silent=None):
  160. old_name = copy(self.options["name"])
  161. new_name = self.ui.name_entry.get_value()
  162. if new_name != old_name:
  163. # update the SHELL auto-completer model data
  164. try:
  165. self.app.myKeywords.remove(old_name)
  166. self.app.myKeywords.append(new_name)
  167. self.app.shell._edit.set_model_data(self.app.myKeywords)
  168. self.app.ui.code_editor.set_model_data(self.app.myKeywords)
  169. except Exception:
  170. log.debug("on_name_activate() --> Could not remove the old object name from auto-completer model list")
  171. self.options["name"] = self.ui.name_entry.get_value()
  172. self.default_data["name"] = self.ui.name_entry.get_value()
  173. self.app.collection.update_view()
  174. if silent:
  175. self.app.inform.emit('[success] %s: %s %s: %s' % (
  176. _("Name changed from"), str(old_name), _("to"), str(new_name)
  177. )
  178. )
  179. def on_offset_button_click(self):
  180. self.app.defaults.report_usage("obj_on_offset_button")
  181. self.read_form()
  182. vector_val = self.ui.offsetvector_entry.get_value()
  183. def worker_task():
  184. with self.app.proc_container.new(_("Offsetting...")):
  185. self.offset(vector_val)
  186. self.app.proc_container.update_view_text('')
  187. with self.app.proc_container.new('%s...' % _("Plotting")):
  188. self.plot()
  189. self.app.app_obj.object_changed.emit(self)
  190. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  191. def on_scale_button_click(self):
  192. self.read_form()
  193. try:
  194. factor = float(self.ui.scale_entry.get_value())
  195. except Exception as e:
  196. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Scaling could not be executed."))
  197. log.debug("FlatCAMObj.on_scale_button_click() -- %s" % str(e))
  198. return
  199. if type(factor) != float:
  200. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Scaling could not be executed."))
  201. # if factor is 1.0 do nothing, there is no point in scaling with a factor of 1.0
  202. if factor == 1.0:
  203. self.app.inform.emit('[success] %s' % _("Scale done."))
  204. return
  205. log.debug("FlatCAMObj.on_scale_button_click()")
  206. def worker_task():
  207. with self.app.proc_container.new(_("Scaling...")):
  208. self.scale(factor)
  209. self.app.inform.emit('[success] %s' % _("Scale done."))
  210. self.app.proc_container.update_view_text('')
  211. with self.app.proc_container.new('%s...' % _("Plotting")):
  212. self.plot()
  213. self.app.app_obj.object_changed.emit(self)
  214. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  215. def on_skew_button_click(self):
  216. self.app.defaults.report_usage("obj_on_skew_button")
  217. self.read_form()
  218. x_angle = self.ui.xangle_entry.get_value()
  219. y_angle = self.ui.yangle_entry.get_value()
  220. def worker_task():
  221. with self.app.proc_container.new(_("Skewing...")):
  222. self.skew(x_angle, y_angle)
  223. self.app.proc_container.update_view_text('')
  224. with self.app.proc_container.new('%s...' % _("Plotting")):
  225. self.plot()
  226. self.app.app_obj.object_changed.emit(self)
  227. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  228. def to_form(self):
  229. """
  230. Copies options to the UI form.
  231. :return: None
  232. """
  233. log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMObj.to_form()")
  234. for option in self.options:
  235. try:
  236. self.set_form_item(option)
  237. except Exception as err:
  238. self.app.log.warning("Unexpected error: %s" % str(sys.exc_info()), str(err))
  239. def read_form(self):
  240. """
  241. Reads form into ``self.options``.
  242. :return: None
  243. :rtype: None
  244. """
  245. log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.read_form()")
  246. for option in self.options:
  247. try:
  248. self.read_form_item(option)
  249. except Exception:
  250. self.app.log.warning("Unexpected error: %s" % str(sys.exc_info()))
  251. def set_form_item(self, option):
  252. """
  253. Copies the specified option to the UI form.
  254. :param option: Name of the option (Key in ``self.options``).
  255. :type option: str
  256. :return: None
  257. """
  258. try:
  259. self.form_fields[option].set_value(self.options[option])
  260. except KeyError:
  261. # self.app.log.warn("Tried to set an option or field that does not exist: %s" % option)
  262. pass
  263. def read_form_item(self, option):
  264. """
  265. Reads the specified option from the UI form into ``self.options``.
  266. :param option: Name of the option.
  267. :type option: str
  268. :return: None
  269. """
  270. try:
  271. self.options[option] = self.form_fields[option].get_value()
  272. except KeyError:
  273. pass
  274. # self.app.log.warning("Failed to read option from field: %s" % option)
  275. def plot(self, kind=None):
  276. """
  277. Plot this object (Extend this method to implement the actual plotting).
  278. Call this in descendants before doing the plotting.
  279. :param kind: Used by only some of the FlatCAM objects
  280. :return: Whether to continue plotting or not depending on the "plot" option. Boolean
  281. """
  282. log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMObj.plot()")
  283. if self.deleted:
  284. return False
  285. self.clear()
  286. return True
  287. def single_object_plot(self):
  288. def plot_task():
  289. with self.app.proc_container.new('%s...' % _("Plotting")):
  290. self.plot()
  291. self.app.app_obj.object_changed.emit(self)
  292. self.app.worker_task.emit({'fcn': plot_task, 'params': []})
  293. def serialize(self):
  294. """
  295. Returns a representation of the object as a dictionary so
  296. it can be later exported as JSON. Override this method.
  297. :return: Dictionary representing the object
  298. :rtype: dict
  299. """
  300. return
  301. def deserialize(self, obj_dict):
  302. """
  303. Re-builds an object from its serialized version.
  304. :param obj_dict: Dictionary representing a FlatCAMObj
  305. :type obj_dict: dict
  306. :return: None
  307. """
  308. return
  309. def add_shape(self, **kwargs):
  310. if self.deleted:
  311. raise ObjectDeleted()
  312. else:
  313. key = self.shapes.add(tolerance=self.drawing_tolerance, **kwargs)
  314. return key
  315. def add_mark_shape(self, apid, **kwargs):
  316. if self.deleted:
  317. raise ObjectDeleted()
  318. else:
  319. key = self.mark_shapes[apid].add(tolerance=self.drawing_tolerance, layer=0, **kwargs)
  320. return key
  321. def update_filters(self, last_ext, filter_string):
  322. """
  323. Will modify the filter string that is used when saving a file (a list of file extensions) to have the last
  324. used file extension as the first one in the special string
  325. :param last_ext: The file extension that was last used to save a file
  326. :param filter_string: A key in self.app.defaults that holds a string with the filter from QFileDialog
  327. used when saving a file
  328. :return: None
  329. """
  330. filters = copy(self.app.defaults[filter_string])
  331. filter_list = filters.split(';;')
  332. filter_list_enum_1 = enumerate(filter_list)
  333. # search for the last element in the filters which should always be "All Files (*.*)"
  334. last_elem = ''
  335. for elem in list(filter_list_enum_1):
  336. if '(*.*)' in elem[1]:
  337. last_elem = filter_list.pop(elem[0])
  338. filter_list_enum = enumerate(filter_list)
  339. for elem in list(filter_list_enum):
  340. if '.' + last_ext in elem[1]:
  341. used_ext = filter_list.pop(elem[0])
  342. # sort the extensions back
  343. filter_list.sort(key=lambda x: x.rpartition('.')[2])
  344. # add as a first element the last used extension
  345. filter_list.insert(0, used_ext)
  346. # add back the element that should always be the last (All Files)
  347. filter_list.append(last_elem)
  348. self.app.defaults[filter_string] = ';;'.join(filter_list)
  349. return
  350. @staticmethod
  351. def poly2rings(poly):
  352. return [poly.exterior] + [interior for interior in poly.interiors]
  353. @property
  354. def visible(self):
  355. return self.shapes.visible
  356. @visible.setter
  357. def visible(self, value, threaded=True):
  358. log.debug("FlatCAMObj.visible()")
  359. current_visibility = self.shapes.visible
  360. # self.shapes.visible = value # maybe this is slower in VisPy? use enabled property?
  361. def task(current_visibility):
  362. if current_visibility is True:
  363. if value is False:
  364. self.shapes.visible = False
  365. else:
  366. if value is True:
  367. self.shapes.visible = True
  368. if self.app.is_legacy is False:
  369. # Not all object types has annotations
  370. try:
  371. self.annotation.visible = value
  372. except Exception:
  373. pass
  374. if threaded:
  375. self.app.worker_task.emit({'fcn': task, 'params': [current_visibility]})
  376. else:
  377. task(current_visibility)
  378. @property
  379. def drawing_tolerance(self):
  380. self.units = self.app.defaults['units'].upper()
  381. tol = self._drawing_tolerance if self.units == 'MM' or not self.units else self._drawing_tolerance / 25.4
  382. return tol
  383. @drawing_tolerance.setter
  384. def drawing_tolerance(self, value):
  385. self.units = self.app.defaults['units'].upper()
  386. self._drawing_tolerance = value if self.units == 'MM' or not self.units else value / 25.4
  387. def clear(self, update=False):
  388. self.shapes.clear(update)
  389. # Not all object types has annotations
  390. try:
  391. self.annotation.clear(update)
  392. except AttributeError:
  393. pass
  394. def delete(self):
  395. # Free resources
  396. del self.ui
  397. del self.options
  398. # Set flag
  399. self.deleted = True