FlatCAMObj.py 17 KB

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