FlatCAMObj.py 37 KB

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