FlatCAMObj.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977
  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. from shapely.ops import unary_union
  17. from shapely.geometry import Polygon, MultiPolygon
  18. from copy import deepcopy
  19. import sys
  20. import math
  21. import gettext
  22. import appTranslation as fcTranslate
  23. import builtins
  24. fcTranslate.apply_language('strings')
  25. if '_' not in builtins.__dict__:
  26. _ = gettext.gettext
  27. # Interrupts plotting process if FlatCAMObj has been deleted
  28. class ObjectDeleted(Exception):
  29. pass
  30. class ValidationError(Exception):
  31. def __init__(self, message, errors):
  32. super().__init__(message)
  33. self.errors = errors
  34. class FlatCAMObj(QtCore.QObject):
  35. """
  36. Base type of objects handled in FlatCAM. These become interactive
  37. in the appGUI, can be plotted, and their options can be modified
  38. by the user in their respective forms.
  39. """
  40. # Instance of the application to which these are related.
  41. # The app should set this value.
  42. app = None
  43. # signal to plot a single object
  44. plot_single_object = QtCore.pyqtSignal()
  45. # signal for Properties
  46. calculations_finished = QtCore.pyqtSignal(float, float, float, float, float, object)
  47. def __init__(self, name):
  48. """
  49. Constructor.
  50. :param name: Name of the object given by the user.
  51. :return: FlatCAMObj
  52. """
  53. QtCore.QObject.__init__(self)
  54. # View
  55. self.ui = None
  56. # set True by the collection.append() when the object load is complete
  57. self.load_complete = None
  58. self.options = LoudDict(name=name)
  59. self.options.set_change_callback(self.on_options_change)
  60. self.form_fields = {}
  61. # store here the default data for Geometry Data
  62. self.default_data = {}
  63. # 2D mode
  64. # Axes must exist and be attached to canvas.
  65. self.axes = None
  66. self.kind = None # Override with proper name
  67. if self.app.is_legacy is False:
  68. self.shapes = self.app.plotcanvas.new_shape_group()
  69. self.mark_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1)
  70. # self.shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, pool=self.app.pool, layers=2)
  71. else:
  72. self.shapes = ShapeCollectionLegacy(obj=self, app=self.app, name=name)
  73. self.mark_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name=name + "_mark_shapes")
  74. self.item = None # Link with project view item
  75. self.muted_ui = False
  76. self.deleted = False
  77. try:
  78. self._drawing_tolerance = float(self.app.defaults["global_tolerance"]) if \
  79. self.app.defaults["global_tolerance"] else 0.01
  80. except ValueError:
  81. self._drawing_tolerance = 0.01
  82. self.isHovering = False
  83. self.notHovering = True
  84. # Flag to show if a selection shape is drawn
  85. self.selection_shape_drawn = False
  86. # self.units = 'IN'
  87. self.units = self.app.defaults['units']
  88. # this is the treeWidget from the UI; it is updated when the add_properties_items() method is called
  89. self.treeWidget = None
  90. self.plot_single_object.connect(self.single_object_plot)
  91. def __del__(self):
  92. pass
  93. def __str__(self):
  94. return "<FlatCAMObj({:12s}): {:20s}>".format(self.kind, self.options["name"])
  95. def from_dict(self, d):
  96. """
  97. This supersedes ``from_dict`` in derived classes. Derived classes
  98. must inherit from FlatCAMObj first, then from derivatives of Geometry.
  99. ``self.options`` is only updated, not overwritten. This ensures that
  100. options set by the app do not vanish when reading the objects
  101. from a project file.
  102. :param d: Dictionary with attributes to set.
  103. :return: None
  104. """
  105. for attr in self.ser_attrs:
  106. if attr == 'options':
  107. self.options.update(d[attr])
  108. elif attr == 'tools':
  109. #FIXME: JSON stringifies all keys however tools datastructure is indexed by integer not by string
  110. if(d[attr] != None):
  111. d[attr] = {int(k):v for k,v in d[attr].items()}
  112. setattr(self, attr, d[attr])
  113. else:
  114. try:
  115. setattr(self, attr, d[attr])
  116. except KeyError:
  117. log.debug("FlatCAMObj.from_dict() --> KeyError: %s. "
  118. "Means that we are loading an old project that don't"
  119. "have all attributes in the latest application version." % str(attr))
  120. pass
  121. def on_options_change(self, key):
  122. # Update form on programmatically options change
  123. self.set_form_item(key)
  124. # Set object visibility
  125. if key == 'plot':
  126. self.visible = self.options['plot']
  127. self.optionChanged.emit(key)
  128. def set_ui(self, ui):
  129. self.ui = ui
  130. self.form_fields = {"name": self.ui.name_entry}
  131. assert isinstance(self.ui, ObjectUI)
  132. self.ui.name_entry.returnPressed.connect(self.on_name_activate)
  133. try:
  134. # it will raise an exception for those FlatCAM objects that do not build UI with the common elements
  135. self.ui.offset_button.clicked.connect(self.on_offset_button_click)
  136. except (TypeError, AttributeError):
  137. pass
  138. try:
  139. self.ui.scale_button.clicked.connect(self.on_scale_button_click)
  140. except (TypeError, AttributeError):
  141. pass
  142. try:
  143. self.ui.offsetvector_entry.returnPressed.connect(self.on_offset_button_click)
  144. except (TypeError, AttributeError):
  145. pass
  146. # Creates problems on focusOut
  147. try:
  148. self.ui.scale_entry.returnPressed.connect(self.on_scale_button_click)
  149. except (TypeError, AttributeError):
  150. pass
  151. try:
  152. self.ui.transformations_button.clicked.connect(self.app.transform_tool.run)
  153. except (TypeError, AttributeError):
  154. pass
  155. # self.ui.skew_button.clicked.connect(self.on_skew_button_click)
  156. def build_ui(self):
  157. """
  158. Sets up the UI/form for this object. Show the UI in the App.
  159. :return: None
  160. """
  161. self.muted_ui = True
  162. log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.build_ui()")
  163. try:
  164. # HACK: disconnect the scale entry signal since on focus out event will trigger an undesired scale()
  165. # it seems that the takewidget() does generate a focus out event for the QDoubleSpinbox ...
  166. # and reconnect after the takeWidget() is done
  167. # self.ui.scale_entry.returnPressed.disconnect(self.on_scale_button_click)
  168. self.app.ui.properties_scroll_area.takeWidget()
  169. # self.ui.scale_entry.returnPressed.connect(self.on_scale_button_click)
  170. except Exception as e:
  171. self.app.log.debug("FlatCAMObj.build_ui() --> Nothing to remove: %s" % str(e))
  172. self.app.ui.properties_scroll_area.setWidget(self.ui)
  173. # self.ui.setMinimumWidth(100)
  174. # self.ui.setMaximumWidth(self.app.ui.properties_tab.sizeHint().width())
  175. self.muted_ui = False
  176. def on_name_activate(self, silent=None):
  177. old_name = copy(self.options["name"])
  178. new_name = self.ui.name_entry.get_value()
  179. if new_name != old_name:
  180. # update the SHELL auto-completer model data
  181. try:
  182. self.app.myKeywords.remove(old_name)
  183. self.app.myKeywords.append(new_name)
  184. self.app.shell._edit.set_model_data(self.app.myKeywords)
  185. self.app.ui.code_editor.set_model_data(self.app.myKeywords)
  186. except Exception:
  187. log.debug("on_name_activate() --> Could not remove the old object name from auto-completer model list")
  188. self.options["name"] = self.ui.name_entry.get_value()
  189. self.default_data["name"] = self.ui.name_entry.get_value()
  190. self.app.collection.update_view()
  191. if silent:
  192. self.app.inform.emit('[success] %s: %s %s: %s' % (
  193. _("Name changed from"), str(old_name), _("to"), str(new_name)
  194. )
  195. )
  196. def on_offset_button_click(self):
  197. self.app.defaults.report_usage("obj_on_offset_button")
  198. self.read_form()
  199. vector_val = self.ui.offsetvector_entry.get_value()
  200. def worker_task():
  201. with self.app.proc_container.new(_("Offsetting...")):
  202. self.offset(vector_val)
  203. self.app.proc_container.update_view_text('')
  204. with self.app.proc_container.new('%s ...' % _("Plotting")):
  205. self.plot()
  206. self.app.app_obj.object_changed.emit(self)
  207. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  208. def on_scale_button_click(self):
  209. self.read_form()
  210. try:
  211. factor = float(self.ui.scale_entry.get_value())
  212. except Exception as e:
  213. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Scaling could not be executed."))
  214. log.debug("FlatCAMObj.on_scale_button_click() -- %s" % str(e))
  215. return
  216. if type(factor) != float:
  217. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Scaling could not be executed."))
  218. # if factor is 1.0 do nothing, there is no point in scaling with a factor of 1.0
  219. if factor == 1.0:
  220. self.app.inform.emit('[success] %s' % _("Scale done."))
  221. return
  222. log.debug("FlatCAMObj.on_scale_button_click()")
  223. def worker_task():
  224. with self.app.proc_container.new(_("Scaling...")):
  225. self.scale(factor)
  226. self.app.inform.emit('[success] %s' % _("Scale done."))
  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 on_skew_button_click(self):
  233. self.app.defaults.report_usage("obj_on_skew_button")
  234. self.read_form()
  235. x_angle = self.ui.xangle_entry.get_value()
  236. y_angle = self.ui.yangle_entry.get_value()
  237. def worker_task():
  238. with self.app.proc_container.new(_("Skewing...")):
  239. self.skew(x_angle, y_angle)
  240. self.app.proc_container.update_view_text('')
  241. with self.app.proc_container.new('%s ...' % _("Plotting")):
  242. self.plot()
  243. self.app.app_obj.object_changed.emit(self)
  244. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  245. def to_form(self):
  246. """
  247. Copies options to the UI form.
  248. :return: None
  249. """
  250. log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMObj.to_form()")
  251. for option in self.options:
  252. try:
  253. self.set_form_item(option)
  254. except Exception as err:
  255. self.app.log.warning("Unexpected error: %s" % str(sys.exc_info()), str(err))
  256. def read_form(self):
  257. """
  258. Reads form into ``self.options``.
  259. :return: None
  260. :rtype: None
  261. """
  262. log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.read_form()")
  263. for option in self.options:
  264. try:
  265. self.read_form_item(option)
  266. except Exception:
  267. self.app.log.warning("Unexpected error: %s" % str(sys.exc_info()))
  268. def set_form_item(self, option):
  269. """
  270. Copies the specified option to the UI form.
  271. :param option: Name of the option (Key in ``self.options``).
  272. :type option: str
  273. :return: None
  274. """
  275. try:
  276. self.form_fields[option].set_value(self.options[option])
  277. except KeyError:
  278. # self.app.log.warn("Tried to set an option or field that does not exist: %s" % option)
  279. pass
  280. def read_form_item(self, option):
  281. """
  282. Reads the specified option from the UI form into ``self.options``.
  283. :param option: Name of the option.
  284. :type option: str
  285. :return: None
  286. """
  287. try:
  288. self.options[option] = self.form_fields[option].get_value()
  289. except KeyError:
  290. pass
  291. # self.app.log.warning("Failed to read option from field: %s" % option)
  292. def plot(self, kind=None):
  293. """
  294. Plot this object (Extend this method to implement the actual plotting).
  295. Call this in descendants before doing the plotting.
  296. :param kind: Used by only some of the FlatCAM objects
  297. :return: Whether to continue plotting or not depending on the "plot" option. Boolean
  298. """
  299. log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMObj.plot()")
  300. if self.deleted:
  301. return False
  302. self.clear()
  303. return True
  304. def single_object_plot(self):
  305. def plot_task():
  306. with self.app.proc_container.new('%s ...' % _("Plotting")):
  307. self.plot()
  308. self.app.app_obj.object_changed.emit(self)
  309. self.app.worker_task.emit({'fcn': plot_task, 'params': []})
  310. def serialize(self):
  311. """
  312. Returns a representation of the object as a dictionary so
  313. it can be later exported as JSON. Override this method.
  314. :return: Dictionary representing the object
  315. :rtype: dict
  316. """
  317. return
  318. def deserialize(self, obj_dict):
  319. """
  320. Re-builds an object from its serialized version.
  321. :param obj_dict: Dictionary representing a FlatCAMObj
  322. :type obj_dict: dict
  323. :return: None
  324. """
  325. return
  326. def add_shape(self, **kwargs):
  327. if self.deleted:
  328. raise ObjectDeleted()
  329. else:
  330. key = self.shapes.add(tolerance=self.drawing_tolerance, **kwargs)
  331. return key
  332. def add_mark_shape(self, **kwargs):
  333. if self.deleted:
  334. raise ObjectDeleted()
  335. else:
  336. key = self.mark_shapes.add(tolerance=self.drawing_tolerance, layer=0, **kwargs)
  337. return key
  338. def update_filters(self, last_ext, filter_string):
  339. """
  340. Will modify the filter string that is used when saving a file (a list of file extensions) to have the last
  341. used file extension as the first one in the special string
  342. :param last_ext: The file extension that was last used to save a file
  343. :param filter_string: A key in self.app.defaults that holds a string with the filter from QFileDialog
  344. used when saving a file
  345. :return: None
  346. """
  347. filters = copy(self.app.defaults[filter_string])
  348. filter_list = filters.split(';;')
  349. filter_list_enum_1 = enumerate(filter_list)
  350. # search for the last element in the filters which should always be "All Files (*.*)"
  351. last_elem = ''
  352. for elem in list(filter_list_enum_1):
  353. if '(*.*)' in elem[1]:
  354. last_elem = filter_list.pop(elem[0])
  355. filter_list_enum = enumerate(filter_list)
  356. for elem in list(filter_list_enum):
  357. if '.' + last_ext in elem[1]:
  358. used_ext = filter_list.pop(elem[0])
  359. # sort the extensions back
  360. filter_list.sort(key=lambda x: x.rpartition('.')[2])
  361. # add as a first element the last used extension
  362. filter_list.insert(0, used_ext)
  363. # add back the element that should always be the last (All Files)
  364. filter_list.append(last_elem)
  365. self.app.defaults[filter_string] = ';;'.join(filter_list)
  366. return
  367. def add_properties_items(self, obj, treeWidget):
  368. self.treeWidget = treeWidget
  369. parent = self.treeWidget.invisibleRootItem()
  370. apertures = ''
  371. tools = ''
  372. drills = ''
  373. slots = ''
  374. others = ''
  375. font = QtGui.QFont()
  376. font.setBold(True)
  377. p_color = QtGui.QColor("#000000") if self.app.defaults['global_gray_icons'] is False \
  378. else QtGui.QColor("#FFFFFF")
  379. # main Items categories
  380. dims = self.treeWidget.addParent(
  381. parent, _('Dimensions'), expanded=True, color=p_color, font=font)
  382. options = self.treeWidget.addParent(parent, _('Options'), color=p_color, font=font)
  383. if obj.kind.lower() == 'gerber':
  384. apertures = self.treeWidget.addParent(
  385. parent, _('Apertures'), expanded=True, color=p_color, font=font)
  386. else:
  387. tools = self.treeWidget.addParent(
  388. parent, _('Tools'), expanded=True, color=p_color, font=font)
  389. if obj.kind.lower() == 'excellon':
  390. drills = self.treeWidget.addParent(
  391. parent, _('Drills'), expanded=True, color=p_color, font=font)
  392. slots = self.treeWidget.addParent(
  393. parent, _('Slots'), expanded=True, color=p_color, font=font)
  394. if obj.kind.lower() == 'cncjob':
  395. others = self.treeWidget.addParent(
  396. parent, _('Others'), expanded=True, color=p_color, font=font)
  397. # separator = self.treeWidget.addParent(parent, '')
  398. def job_thread(obj_prop):
  399. self.app.proc_container.new(_("Calculating dimensions ... Please wait."))
  400. length = 0.0
  401. width = 0.0
  402. area = 0.0
  403. copper_area = 0.0
  404. geo = obj_prop.solid_geometry
  405. if geo:
  406. # calculate physical dimensions
  407. try:
  408. xmin, ymin, xmax, ymax = obj_prop.bounds()
  409. length = abs(xmax - xmin)
  410. width = abs(ymax - ymin)
  411. except Exception as ee:
  412. log.debug("FlatCAMObj.add_properties_items() -> calculate dimensions --> %s" % str(ee))
  413. # calculate box area
  414. if self.app.defaults['units'].lower() == 'mm':
  415. area = (length * width) / 100
  416. else:
  417. area = length * width
  418. if obj_prop.kind.lower() == 'gerber' and geo:
  419. # calculate copper area
  420. try:
  421. for geo_el in geo:
  422. copper_area += geo_el.area
  423. except TypeError:
  424. copper_area += geo.area
  425. copper_area /= 100
  426. else:
  427. xmin = []
  428. ymin = []
  429. xmax = []
  430. ymax = []
  431. if obj_prop.kind.lower() == 'cncjob':
  432. try:
  433. for tool_k in obj_prop.exc_cnc_tools:
  434. x0, y0, x1, y1 = unary_union(obj_prop.exc_cnc_tools[tool_k]['solid_geometry']).bounds
  435. xmin.append(x0)
  436. ymin.append(y0)
  437. xmax.append(x1)
  438. ymax.append(y1)
  439. except Exception as ee:
  440. log.debug("FlatCAMObj.add_properties_items() cncjob --> %s" % str(ee))
  441. try:
  442. for tool_k in obj_prop.cnc_tools:
  443. x0, y0, x1, y1 = unary_union(obj_prop.cnc_tools[tool_k]['solid_geometry']).bounds
  444. xmin.append(x0)
  445. ymin.append(y0)
  446. xmax.append(x1)
  447. ymax.append(y1)
  448. except Exception as ee:
  449. log.debug("FlatCAMObj.add_properties_items() cncjob --> %s" % str(ee))
  450. else:
  451. try:
  452. if obj_prop.tools:
  453. for tool_k in obj_prop.tools:
  454. t_geo = obj_prop.tools[tool_k]['solid_geometry']
  455. try:
  456. x0, y0, x1, y1 = unary_union(t_geo).bounds
  457. except Exception:
  458. continue
  459. xmin.append(x0)
  460. ymin.append(y0)
  461. xmax.append(x1)
  462. ymax.append(y1)
  463. except Exception as ee:
  464. log.debug("FlatCAMObj.add_properties_items() not cncjob tools --> %s" % str(ee))
  465. if xmin and ymin and xmax and ymax:
  466. xmin = min(xmin)
  467. ymin = min(ymin)
  468. xmax = max(xmax)
  469. ymax = max(ymax)
  470. length = abs(xmax - xmin)
  471. width = abs(ymax - ymin)
  472. # calculate box area
  473. if self.app.defaults['units'].lower() == 'mm':
  474. area = (length * width) / 100
  475. else:
  476. area = length * width
  477. if obj_prop.kind.lower() == 'gerber' and obj_prop.tools:
  478. # calculate copper area
  479. # create a complete solid_geometry from the tools
  480. geo_tools = []
  481. for tool_k in obj_prop.tools:
  482. if 'solid_geometry' in obj_prop.tools[tool_k]:
  483. for geo_el in obj_prop.tools[tool_k]['solid_geometry']:
  484. geo_tools.append(geo_el)
  485. for geo_el in geo_tools:
  486. copper_area += geo_el.area
  487. # in cm2
  488. copper_area /= 100
  489. area_chull = 0.0
  490. if obj_prop.kind.lower() != 'cncjob':
  491. # calculate and add convex hull area
  492. if geo:
  493. if isinstance(geo, list) and geo[0] is not None:
  494. if isinstance(geo, MultiPolygon):
  495. env_obj = geo.convex_hull
  496. elif (isinstance(geo, MultiPolygon) and len(geo) == 1) or \
  497. (isinstance(geo, list) and len(geo) == 1) and isinstance(geo[0], Polygon):
  498. env_obj = unary_union(geo)
  499. env_obj = env_obj.convex_hull
  500. else:
  501. env_obj = unary_union(geo)
  502. env_obj = env_obj.convex_hull
  503. area_chull = env_obj.area
  504. else:
  505. area_chull = 0
  506. else:
  507. try:
  508. area_chull = None
  509. if obj_prop.tools:
  510. area_chull_list = []
  511. for tool_k in obj_prop.tools:
  512. area_el = unary_union(obj_prop.tools[tool_k]['solid_geometry']).convex_hull
  513. area_chull_list.append(area_el.area)
  514. area_chull = max(area_chull_list)
  515. except Exception as er:
  516. area_chull = None
  517. log.debug("FlatCAMObj.add_properties_items() area chull--> %s" % str(er))
  518. if self.app.defaults['units'].lower() == 'mm' and area_chull:
  519. area_chull = area_chull / 100
  520. if area_chull is None:
  521. area_chull = 0
  522. self.calculations_finished.emit(area, length, width, area_chull, copper_area, dims)
  523. self.app.worker_task.emit({'fcn': job_thread, 'params': [obj]})
  524. # Options items
  525. for option in obj.options:
  526. if option == 'name':
  527. continue
  528. self.treeWidget.addChild(options, [str(option), str(obj.options[option])], True)
  529. # Items that depend on the object type
  530. if obj.kind.lower() == 'gerber' and obj.apertures:
  531. temp_ap = {}
  532. for ap in obj.apertures:
  533. temp_ap.clear()
  534. temp_ap = deepcopy(obj.apertures[ap])
  535. temp_ap.pop('geometry', None)
  536. solid_nr = 0
  537. follow_nr = 0
  538. clear_nr = 0
  539. if 'geometry' in obj.apertures[ap]:
  540. if obj.apertures[ap]['geometry']:
  541. font.setBold(True)
  542. for el in obj.apertures[ap]['geometry']:
  543. if 'solid' in el:
  544. solid_nr += 1
  545. if 'follow' in el:
  546. follow_nr += 1
  547. if 'clear' in el:
  548. clear_nr += 1
  549. else:
  550. font.setBold(False)
  551. temp_ap['Solid_Geo'] = '%s Polygons' % str(solid_nr)
  552. temp_ap['Follow_Geo'] = '%s LineStrings' % str(follow_nr)
  553. temp_ap['Clear_Geo'] = '%s Polygons' % str(clear_nr)
  554. apid = self.treeWidget.addParent(
  555. apertures, str(ap), expanded=False, color=p_color, font=font)
  556. for key in temp_ap:
  557. self.treeWidget.addChild(apid, [str(key), str(temp_ap[key])], True)
  558. elif obj.kind.lower() == 'excellon':
  559. tot_drill_cnt = 0
  560. tot_slot_cnt = 0
  561. for tool, value in obj.tools.items():
  562. toolid = self.treeWidget.addParent(
  563. tools, str(tool), expanded=False, color=p_color, font=font)
  564. drill_cnt = 0 # variable to store the nr of drills per tool
  565. slot_cnt = 0 # variable to store the nr of slots per tool
  566. # Find no of drills for the current tool
  567. if 'drills' in value and value['drills']:
  568. drill_cnt = len(value['drills'])
  569. tot_drill_cnt += drill_cnt
  570. # Find no of slots for the current tool
  571. if 'slots' in value and value['slots']:
  572. slot_cnt = len(value['slots'])
  573. tot_slot_cnt += slot_cnt
  574. self.treeWidget.addChild(
  575. toolid,
  576. [
  577. _('Diameter'),
  578. '%.*f %s' % (self.decimals, value['tooldia'], self.app.defaults['units'].lower())
  579. ],
  580. True
  581. )
  582. self.treeWidget.addChild(toolid, [_('Drills number'), str(drill_cnt)], True)
  583. self.treeWidget.addChild(toolid, [_('Slots number'), str(slot_cnt)], True)
  584. self.treeWidget.addChild(drills, [_('Drills total number:'), str(tot_drill_cnt)], True)
  585. self.treeWidget.addChild(slots, [_('Slots total number:'), str(tot_slot_cnt)], True)
  586. elif obj.kind.lower() == 'geometry':
  587. for tool, value in obj.tools.items():
  588. geo_tool = self.treeWidget.addParent(
  589. tools, str(tool), expanded=False, color=p_color, font=font)
  590. for k, v in value.items():
  591. if k == 'solid_geometry':
  592. # printed_value = _('Present') if v else _('None')
  593. try:
  594. printed_value = str(len(v))
  595. except (TypeError, AttributeError):
  596. printed_value = '1'
  597. self.treeWidget.addChild(geo_tool, [str(k), printed_value], True)
  598. elif k == 'data':
  599. tool_data = self.treeWidget.addParent(
  600. geo_tool, str(k).capitalize(), color=p_color, font=font)
  601. for data_k, data_v in v.items():
  602. self.treeWidget.addChild(tool_data, [str(data_k), str(data_v)], True)
  603. else:
  604. self.treeWidget.addChild(geo_tool, [str(k), str(v)], True)
  605. elif obj.kind.lower() == 'cncjob':
  606. # for cncjob objects made from gerber or geometry
  607. for tool, value in obj.cnc_tools.items():
  608. geo_tool = self.treeWidget.addParent(
  609. tools, str(tool), expanded=False, color=p_color, font=font)
  610. for k, v in value.items():
  611. if k == 'solid_geometry':
  612. printed_value = _('Present') if v else _('None')
  613. self.treeWidget.addChild(geo_tool, [_("Solid Geometry"), printed_value], True)
  614. elif k == 'gcode':
  615. printed_value = _('Present') if v != '' else _('None')
  616. self.treeWidget.addChild(geo_tool, [_("GCode Text"), printed_value], True)
  617. elif k == 'gcode_parsed':
  618. printed_value = _('Present') if v else _('None')
  619. self.treeWidget.addChild(geo_tool, [_("GCode Geometry"), printed_value], True)
  620. elif k == 'data':
  621. pass
  622. else:
  623. self.treeWidget.addChild(geo_tool, [str(k), str(v)], True)
  624. v = value['data']
  625. tool_data = self.treeWidget.addParent(
  626. geo_tool, _("Tool Data"), color=p_color, font=font)
  627. for data_k, data_v in v.items():
  628. self.treeWidget.addChild(tool_data, [str(data_k).capitalize(), str(data_v)], True)
  629. # for cncjob objects made from excellon
  630. for tool_dia, value in obj.exc_cnc_tools.items():
  631. exc_tool = self.treeWidget.addParent(
  632. tools, str(value['tool']), expanded=False, color=p_color, font=font
  633. )
  634. self.treeWidget.addChild(
  635. exc_tool,
  636. [
  637. _('Diameter'),
  638. '%.*f %s' % (self.decimals, float(tool_dia), self.app.defaults['units'].lower())
  639. ],
  640. True
  641. )
  642. for k, v in value.items():
  643. if k == 'solid_geometry':
  644. printed_value = _('Present') if v else _('None')
  645. self.treeWidget.addChild(exc_tool, [_("Solid Geometry"), printed_value], True)
  646. elif k == 'nr_drills':
  647. self.treeWidget.addChild(exc_tool, [_("Drills number"), str(v)], True)
  648. elif k == 'nr_slots':
  649. self.treeWidget.addChild(exc_tool, [_("Slots number"), str(v)], True)
  650. elif k == 'gcode':
  651. printed_value = _('Present') if v != '' else _('None')
  652. self.treeWidget.addChild(exc_tool, [_("GCode Text"), printed_value], True)
  653. elif k == 'gcode_parsed':
  654. printed_value = _('Present') if v else _('None')
  655. self.treeWidget.addChild(exc_tool, [_("GCode Geometry"), printed_value], True)
  656. else:
  657. pass
  658. self.treeWidget.addChild(
  659. exc_tool,
  660. [
  661. _("Depth of Cut"),
  662. '%.*f %s' % (
  663. self.decimals,
  664. (obj.z_cut - abs(value['data']['tools_drill_offset'])),
  665. self.app.defaults['units'].lower()
  666. )
  667. ],
  668. True
  669. )
  670. self.treeWidget.addChild(
  671. exc_tool,
  672. [
  673. _("Clearance Height"),
  674. '%.*f %s' % (
  675. self.decimals,
  676. obj.z_move,
  677. self.app.defaults['units'].lower()
  678. )
  679. ],
  680. True
  681. )
  682. self.treeWidget.addChild(
  683. exc_tool,
  684. [
  685. _("Feedrate"),
  686. '%.*f %s/min' % (
  687. self.decimals,
  688. obj.feedrate,
  689. self.app.defaults['units'].lower()
  690. )
  691. ],
  692. True
  693. )
  694. v = value['data']
  695. tool_data = self.treeWidget.addParent(
  696. exc_tool, _("Tool Data"), color=p_color, font=font)
  697. for data_k, data_v in v.items():
  698. self.treeWidget.addChild(tool_data, [str(data_k).capitalize(), str(data_v)], True)
  699. r_time = obj.routing_time
  700. if r_time > 1:
  701. units_lbl = 'min'
  702. else:
  703. r_time *= 60
  704. units_lbl = 'sec'
  705. r_time = math.ceil(float(r_time))
  706. self.treeWidget.addChild(
  707. others,
  708. [
  709. '%s:' % _('Routing time'),
  710. '%.*f %s' % (self.decimals, r_time, units_lbl)],
  711. True
  712. )
  713. self.treeWidget.addChild(
  714. others,
  715. [
  716. '%s:' % _('Travelled distance'),
  717. '%.*f %s' % (self.decimals, obj.travel_distance, self.app.defaults['units'].lower())
  718. ],
  719. True
  720. )
  721. # treeWidget.addChild(separator, [''])
  722. def update_area_chull(self, area, length, width, chull_area, copper_area, location):
  723. # add dimensions
  724. self.treeWidget.addChild(
  725. location,
  726. ['%s:' % _('Length'), '%.*f %s' % (self.decimals, length, self.app.defaults['units'].lower())],
  727. True
  728. )
  729. self.treeWidget.addChild(
  730. location,
  731. ['%s:' % _('Width'), '%.*f %s' % (self.decimals, width, self.app.defaults['units'].lower())],
  732. True
  733. )
  734. # add box area
  735. if self.app.defaults['units'].lower() == 'mm':
  736. self.treeWidget.addChild(location, ['%s:' % _('Box Area'), '%.*f %s' % (self.decimals, area, 'cm2')], True)
  737. self.treeWidget.addChild(
  738. location,
  739. ['%s:' % _('Convex_Hull Area'), '%.*f %s' % (self.decimals, chull_area, 'cm2')],
  740. True
  741. )
  742. else:
  743. self.treeWidget.addChild(location, ['%s:' % _('Box Area'), '%.*f %s' % (self.decimals, area, 'in2')], True)
  744. self.treeWidget.addChild(
  745. location,
  746. ['%s:' % _('Convex_Hull Area'), '%.*f %s' % (self.decimals, chull_area, 'in2')],
  747. True
  748. )
  749. # add copper area
  750. if self.app.defaults['units'].lower() == 'mm':
  751. self.treeWidget.addChild(
  752. location, ['%s:' % _('Copper Area'), '%.*f %s' % (self.decimals, copper_area, 'cm2')], True)
  753. else:
  754. self.treeWidget.addChild(
  755. location, ['%s:' % _('Copper Area'), '%.*f %s' % (self.decimals, copper_area, 'in2')], True)
  756. @staticmethod
  757. def poly2rings(poly):
  758. return [poly.exterior] + [interior for interior in poly.interiors]
  759. @property
  760. def visible(self):
  761. return self.shapes.visible
  762. @visible.setter
  763. def visible(self, value, threaded=True):
  764. log.debug("FlatCAMObj.visible()")
  765. current_visibility = self.shapes.visible
  766. # self.shapes.visible = value # maybe this is slower in VisPy? use enabled property?
  767. def task(visibility):
  768. if visibility is True:
  769. if value is False:
  770. self.shapes.visible = False
  771. else:
  772. if value is True:
  773. self.shapes.visible = True
  774. if self.app.is_legacy is False:
  775. # Not all object types has annotations
  776. try:
  777. self.annotation.visible = value
  778. except Exception:
  779. pass
  780. if threaded:
  781. self.app.worker_task.emit({'fcn': task, 'params': [current_visibility]})
  782. else:
  783. task(current_visibility)
  784. @property
  785. def drawing_tolerance(self):
  786. self.units = self.app.defaults['units'].upper()
  787. tol = self._drawing_tolerance if self.units == 'MM' or not self.units else self._drawing_tolerance / 25.4
  788. return tol
  789. @drawing_tolerance.setter
  790. def drawing_tolerance(self, value):
  791. self.units = self.app.defaults['units'].upper()
  792. self._drawing_tolerance = value if self.units == 'MM' or not self.units else value / 25.4
  793. def clear(self, update=False):
  794. self.shapes.clear(update)
  795. # Not all object types has annotations
  796. try:
  797. self.annotation.clear(update)
  798. except AttributeError:
  799. pass
  800. def delete(self):
  801. # Free resources
  802. del self.ui
  803. del self.options
  804. # Set flag
  805. self.deleted = True