FlatCAMObj.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  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 flatcamGUI.ObjectUI import *
  13. from FlatCAMCommon import LoudDict
  14. from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
  15. import sys
  16. import gettext
  17. import FlatCAMTranslation 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 GUI, 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 FlatCAM." % 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. # self.ui.skew_button.clicked.connect(self.on_skew_button_click)
  135. def build_ui(self):
  136. """
  137. Sets up the UI/form for this object. Show the UI
  138. in the App.
  139. :return: None
  140. :rtype: None
  141. """
  142. self.muted_ui = True
  143. log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.build_ui()")
  144. try:
  145. # HACK: disconnect the scale entry signal since on focus out event will trigger an undesired scale()
  146. # it seems that the takewidget() does generate a focus out event for the QDoubleSpinbox ...
  147. # and reconnect after the takeWidget() is done
  148. # self.ui.scale_entry.returnPressed.disconnect(self.on_scale_button_click)
  149. self.app.ui.selected_scroll_area.takeWidget()
  150. # self.ui.scale_entry.returnPressed.connect(self.on_scale_button_click)
  151. except Exception as e:
  152. self.app.log.debug("FlatCAMObj.build_ui() --> Nothing to remove: %s" % str(e))
  153. self.app.ui.selected_scroll_area.setWidget(self.ui)
  154. # self.ui.setMinimumWidth(100)
  155. # self.ui.setMaximumWidth(self.app.ui.selected_tab.sizeHint().width())
  156. self.muted_ui = False
  157. def on_name_activate(self, silent=None):
  158. old_name = copy(self.options["name"])
  159. new_name = self.ui.name_entry.get_value()
  160. if new_name != old_name:
  161. # update the SHELL auto-completer model data
  162. try:
  163. self.app.myKeywords.remove(old_name)
  164. self.app.myKeywords.append(new_name)
  165. self.app.shell._edit.set_model_data(self.app.myKeywords)
  166. self.app.ui.code_editor.set_model_data(self.app.myKeywords)
  167. except Exception:
  168. log.debug("on_name_activate() --> Could not remove the old object name from auto-completer model list")
  169. self.options["name"] = self.ui.name_entry.get_value()
  170. self.default_data["name"] = self.ui.name_entry.get_value()
  171. self.app.collection.update_view()
  172. if silent:
  173. self.app.inform.emit('[success] %s: %s %s: %s' % (
  174. _("Name changed from"), str(old_name), _("to"), str(new_name)
  175. )
  176. )
  177. def on_offset_button_click(self):
  178. self.app.report_usage("obj_on_offset_button")
  179. self.read_form()
  180. vector_val = self.ui.offsetvector_entry.get_value()
  181. def worker_task():
  182. with self.app.proc_container.new(_("Offsetting...")):
  183. self.offset(vector_val)
  184. self.app.proc_container.update_view_text('')
  185. with self.app.proc_container.new('%s...' % _("Plotting")):
  186. self.plot()
  187. self.app.object_changed.emit(self)
  188. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  189. def on_scale_button_click(self):
  190. self.read_form()
  191. try:
  192. factor = float(eval(self.ui.scale_entry.get_value()))
  193. except Exception as e:
  194. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Scaling could not be executed."))
  195. log.debug("FlatCAMObj.on_scale_button_click() -- %s" % str(e))
  196. return
  197. if type(factor) != float:
  198. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Scaling could not be executed."))
  199. # if factor is 1.0 do nothing, there is no point in scaling with a factor of 1.0
  200. if factor == 1.0:
  201. self.app.inform.emit('[success] %s' % _("Scale done."))
  202. return
  203. log.debug("FlatCAMObj.on_scale_button_click()")
  204. def worker_task():
  205. with self.app.proc_container.new(_("Scaling...")):
  206. self.scale(factor)
  207. self.app.inform.emit('[success] %s' % _("Scale done."))
  208. self.app.proc_container.update_view_text('')
  209. with self.app.proc_container.new('%s...' % _("Plotting")):
  210. self.plot()
  211. self.app.object_changed.emit(self)
  212. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  213. def on_skew_button_click(self):
  214. self.app.report_usage("obj_on_skew_button")
  215. self.read_form()
  216. x_angle = self.ui.xangle_entry.get_value()
  217. y_angle = self.ui.yangle_entry.get_value()
  218. def worker_task():
  219. with self.app.proc_container.new(_("Skewing...")):
  220. self.skew(x_angle, y_angle)
  221. self.app.proc_container.update_view_text('')
  222. with self.app.proc_container.new('%s...' % _("Plotting")):
  223. self.plot()
  224. self.app.object_changed.emit(self)
  225. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  226. def to_form(self):
  227. """
  228. Copies options to the UI form.
  229. :return: None
  230. """
  231. log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMObj.to_form()")
  232. for option in self.options:
  233. try:
  234. self.set_form_item(option)
  235. except Exception:
  236. self.app.log.warning("Unexpected error:", sys.exc_info())
  237. def read_form(self):
  238. """
  239. Reads form into ``self.options``.
  240. :return: None
  241. :rtype: None
  242. """
  243. log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.read_form()")
  244. for option in self.options:
  245. try:
  246. self.read_form_item(option)
  247. except Exception:
  248. self.app.log.warning("Unexpected error:", sys.exc_info())
  249. def set_form_item(self, option):
  250. """
  251. Copies the specified option to the UI form.
  252. :param option: Name of the option (Key in ``self.options``).
  253. :type option: str
  254. :return: None
  255. """
  256. try:
  257. self.form_fields[option].set_value(self.options[option])
  258. except KeyError:
  259. # self.app.log.warn("Tried to set an option or field that does not exist: %s" % option)
  260. pass
  261. def read_form_item(self, option):
  262. """
  263. Reads the specified option from the UI form into ``self.options``.
  264. :param option: Name of the option.
  265. :type option: str
  266. :return: None
  267. """
  268. try:
  269. self.options[option] = self.form_fields[option].get_value()
  270. except KeyError:
  271. pass
  272. # self.app.log.warning("Failed to read option from field: %s" % option)
  273. def plot(self, kind=None):
  274. """
  275. Plot this object (Extend this method to implement the actual plotting).
  276. Call this in descendants before doing the plotting.
  277. :param kind: Used by only some of the FlatCAM objects
  278. :return: Whether to continue plotting or not depending on the "plot" option. Boolean
  279. """
  280. log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMObj.plot()")
  281. if self.deleted:
  282. return False
  283. self.clear()
  284. return True
  285. def single_object_plot(self):
  286. def plot_task():
  287. with self.app.proc_container.new('%s...' % _("Plotting")):
  288. self.plot()
  289. self.app.object_changed.emit(self)
  290. self.app.worker_task.emit({'fcn': plot_task, 'params': []})
  291. def serialize(self):
  292. """
  293. Returns a representation of the object as a dictionary so
  294. it can be later exported as JSON. Override this method.
  295. :return: Dictionary representing the object
  296. :rtype: dict
  297. """
  298. return
  299. def deserialize(self, obj_dict):
  300. """
  301. Re-builds an object from its serialized version.
  302. :param obj_dict: Dictionary representing a FlatCAMObj
  303. :type obj_dict: dict
  304. :return: None
  305. """
  306. return
  307. def add_shape(self, **kwargs):
  308. if self.deleted:
  309. raise ObjectDeleted()
  310. else:
  311. key = self.shapes.add(tolerance=self.drawing_tolerance, **kwargs)
  312. return key
  313. def add_mark_shape(self, apid, **kwargs):
  314. if self.deleted:
  315. raise ObjectDeleted()
  316. else:
  317. key = self.mark_shapes[apid].add(tolerance=self.drawing_tolerance, layer=0, **kwargs)
  318. return key
  319. def update_filters(self, last_ext, filter_string):
  320. """
  321. Will modify the filter string that is used when saving a file (a list of file extensions) to have the last
  322. used file extension as the first one in the special string
  323. :param last_ext: The file extension that was last used to save a file
  324. :param filter_string: A key in self.app.defaults that holds a string with the filter from QFileDialog
  325. used when saving a file
  326. :return: None
  327. """
  328. filters = copy(self.app.defaults[filter_string])
  329. filter_list = filters.split(';;')
  330. filter_list_enum_1 = enumerate(filter_list)
  331. # search for the last element in the filters which should always be "All Files (*.*)"
  332. last_elem = ''
  333. for elem in list(filter_list_enum_1):
  334. if '(*.*)' in elem[1]:
  335. last_elem = filter_list.pop(elem[0])
  336. filter_list_enum = enumerate(filter_list)
  337. for elem in list(filter_list_enum):
  338. if '.' + last_ext in elem[1]:
  339. used_ext = filter_list.pop(elem[0])
  340. # sort the extensions back
  341. filter_list.sort(key=lambda x: x.rpartition('.')[2])
  342. # add as a first element the last used extension
  343. filter_list.insert(0, used_ext)
  344. # add back the element that should always be the last (All Files)
  345. filter_list.append(last_elem)
  346. self.app.defaults[filter_string] = ';;'.join(filter_list)
  347. return
  348. @staticmethod
  349. def poly2rings(poly):
  350. return [poly.exterior] + [interior for interior in poly.interiors]
  351. @property
  352. def visible(self):
  353. return self.shapes.visible
  354. @visible.setter
  355. def visible(self, value, threaded=True):
  356. log.debug("FlatCAMObj.visible()")
  357. def worker_task(app_obj):
  358. self.shapes.visible = value
  359. if self.app.is_legacy is False:
  360. # Not all object types has annotations
  361. try:
  362. self.annotation.visible = value
  363. except Exception:
  364. pass
  365. if threaded is False:
  366. worker_task(app_obj=self.app)
  367. else:
  368. self.app.worker_task.emit({'fcn': worker_task, 'params': [self]})
  369. @property
  370. def drawing_tolerance(self):
  371. self.units = self.app.defaults['units'].upper()
  372. tol = self._drawing_tolerance if self.units == 'MM' or not self.units else self._drawing_tolerance / 25.4
  373. return tol
  374. @drawing_tolerance.setter
  375. def drawing_tolerance(self, value):
  376. self.units = self.app.defaults['units'].upper()
  377. self._drawing_tolerance = value if self.units == 'MM' or not self.units else value / 25.4
  378. def clear(self, update=False):
  379. self.shapes.clear(update)
  380. # Not all object types has annotations
  381. try:
  382. self.annotation.clear(update)
  383. except AttributeError:
  384. pass
  385. def delete(self):
  386. # Free resources
  387. del self.ui
  388. del self.options
  389. # Set flag
  390. self.deleted = True