FlatCAMObj.py 17 KB

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