FlatCAMObj.py 17 KB

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