AppObject.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  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. # Modified by Marius Stanciu (2020) #
  8. # ###########################################################
  9. from PyQt5 import QtCore
  10. from appObjects.ObjectCollection import *
  11. from appObjects.FlatCAMCNCJob import CNCJobObject
  12. from appObjects.FlatCAMDocument import DocumentObject
  13. from appObjects.FlatCAMExcellon import ExcellonObject
  14. from appObjects.FlatCAMGeometry import GeometryObject
  15. from appObjects.FlatCAMGerber import GerberObject
  16. from appObjects.FlatCAMScript import ScriptObject
  17. import time
  18. import traceback
  19. # FlatCAM Translation
  20. import gettext
  21. import appTranslation as fcTranslate
  22. import builtins
  23. fcTranslate.apply_language('strings')
  24. if '_' not in builtins.__dict__:
  25. _ = gettext.gettext
  26. class AppObject(QtCore.QObject):
  27. # Emitted by app_obj.new_object() and passes the new object as argument, plot flag.
  28. # on_object_created() adds the object to the collection, plots on appropriate flag
  29. # and emits app_obj.new_object_available.
  30. object_created = QtCore.pyqtSignal(object, bool, bool, object, list)
  31. # Emitted when a object has been changed (like scaled, mirrored)
  32. object_changed = QtCore.pyqtSignal(object)
  33. # Emitted after object has been plotted.
  34. # Calls 'on_zoom_fit' method to fit object in scene view in main thread to prevent drawing glitches.
  35. object_plotted = QtCore.pyqtSignal(object)
  36. plots_updated = QtCore.pyqtSignal()
  37. def __init__(self, app):
  38. super(AppObject, self).__init__()
  39. self.app = app
  40. self.inform = app.inform
  41. # signals that are emitted when object state changes
  42. self.object_created.connect(self.on_object_created)
  43. self.object_changed.connect(self.on_object_changed)
  44. self.object_plotted.connect(self.on_object_plotted)
  45. self.plots_updated.connect(self.app.on_plots_updated)
  46. def new_object(self, kind, name, initialize, plot=True, autoselected=True, callback=None, callback_params=None):
  47. """
  48. Creates a new specialized FlatCAMObj and attaches it to the application,
  49. this is, updates the GUI accordingly, any other records and plots it.
  50. This method is thread-safe.
  51. Notes:
  52. * If the name is in use, the self.collection will modify it
  53. when appending it to the collection. There is no need to handle
  54. name conflicts here.
  55. :param kind: The kind of object to create. One of 'gerber', 'excellon', 'cncjob' and 'geometry'.
  56. :type kind: str
  57. :param name: Name for the object.
  58. :type name: str
  59. :param initialize: Function to run after creation of the object but before it is attached to the
  60. application.
  61. The function is called with 2 parameters: the new object and the App instance.
  62. :type initialize: function
  63. :param plot: If to plot the resulting object
  64. :param autoselected: if the resulting object is autoselected in the Project tab and therefore in the
  65. self.collection
  66. :param callback: a method that is launched after the object is created
  67. :type callback: function
  68. :param callback_params: a list of parameters for the parameter: callback
  69. :type callback_params: list
  70. :return: Either the object or the string 'fail'
  71. :rtype: object
  72. """
  73. if callback_params is None:
  74. callback_params = [None]
  75. log.debug("AppObject.new_object()")
  76. obj_plot = plot
  77. obj_autoselected = autoselected
  78. t0 = time.time() # Debug
  79. # ## Create object
  80. classdict = {
  81. "gerber": GerberObject,
  82. "excellon": ExcellonObject,
  83. "cncjob": CNCJobObject,
  84. "geometry": GeometryObject,
  85. "script": ScriptObject,
  86. "document": DocumentObject
  87. }
  88. log.debug("Calling object constructor...")
  89. # Object creation/instantiation
  90. obj = classdict[kind](name)
  91. # ############################################################################################################
  92. # adding object PROPERTIES
  93. # ############################################################################################################
  94. obj.units = self.app.options["units"]
  95. obj.isHovering = False
  96. obj.notHovering = True
  97. # IMPORTANT
  98. # The key names in defaults and options dictionary's are not random:
  99. # they have to have in name first the type of the object (geometry, excellon, cncjob and gerber) or how it's
  100. # called here, the 'kind' followed by an underline. Above the App default values from self.defaults are
  101. # copied to self.options. After that, below, depending on the type of
  102. # object that is created, it will strip the name of the object and the underline (if the original key was
  103. # let's say "excellon_toolchange", it will strip the excellon_) and to the obj.options the key will become
  104. # "toolchange"
  105. # ############################################################################################################
  106. # this section copies the application defaults related to the object to the object OPTIONS
  107. # ############################################################################################################
  108. for option in self.app.options:
  109. if option.find(kind + "_") == 0:
  110. oname = option[len(kind) + 1:]
  111. obj.options[oname] = self.app.options[option]
  112. # add some of the FlatCAM Tools related properties
  113. if kind == 'excellon':
  114. for option in self.app.options:
  115. if option.find('tools_drill_') == 0 or option.find('tools_mill_') == 0:
  116. obj.options[option] = self.app.options[option]
  117. if kind == 'gerber':
  118. for option in self.app.options:
  119. if option.find('tools_iso_') == 0:
  120. obj.options[option] = self.app.options[option]
  121. if kind == 'geometry':
  122. for option in self.app.options:
  123. if option.find('tools_mill_') == 0:
  124. obj.options[option] = self.app.options[option]
  125. # ############################################################################################################
  126. # ############################################################################################################
  127. # Initialize as per user request
  128. # User must take care to implement initialize
  129. # in a thread-safe way as is is likely that we
  130. # have been invoked in a separate thread.
  131. t1 = time.time()
  132. log.debug("%f seconds before initialize()." % (t1 - t0))
  133. try:
  134. return_value = initialize(obj, self.app)
  135. except Exception as e:
  136. msg = '[ERROR_NOTCL] %s' % _("An internal error has occurred. See shell.\n")
  137. msg += _("Object ({kind}) failed because: {error} \n\n").format(kind=kind, error=str(e))
  138. msg += traceback.format_exc()
  139. self.app.inform.emit(msg)
  140. return "fail"
  141. t2 = time.time()
  142. msg = "%s %s. %f seconds executing initialize()." % (_("New object with name:"), name, (t2 - t1))
  143. log.debug(msg)
  144. self.app.inform_shell.emit(msg)
  145. if return_value == 'fail':
  146. log.debug("Object (%s) parsing and/or geometry creation failed." % kind)
  147. return "fail"
  148. # ############################################################################################################
  149. # Check units and convert if necessary
  150. # This condition CAN be true because initialize() can change obj.units
  151. # ############################################################################################################
  152. if self.app.options["units"].upper() != obj.units.upper():
  153. self.app.inform.emit('%s: %s' % (_("Converting units to "), self.app.options["units"]))
  154. obj.convert_units(self.app.options["units"])
  155. t3 = time.time()
  156. log.debug("%f seconds converting units." % (t3 - t2))
  157. # ############################################################################################################
  158. # Create the bounding box for the object and then add the results to the obj.options
  159. # But not for Scripts or for Documents
  160. # ############################################################################################################
  161. if kind != 'document' and kind != 'script':
  162. try:
  163. xmin, ymin, xmax, ymax = obj.bounds()
  164. obj.options['xmin'] = xmin
  165. obj.options['ymin'] = ymin
  166. obj.options['xmax'] = xmax
  167. obj.options['ymax'] = ymax
  168. except Exception as e:
  169. log.warning("AppObject.new_object() -> The object has no bounds properties. %s" % str(e))
  170. return "fail"
  171. # ############################################################################################################
  172. # update the KeyWords list with the name of the file
  173. # ############################################################################################################
  174. self.app.myKeywords.append(obj.options['name'])
  175. log.debug("Moving new object back to main thread.")
  176. # ############################################################################################################
  177. # Move the object to the main thread and let the app know that it is available.
  178. # ############################################################################################################
  179. obj.moveToThread(self.app.main_thread)
  180. if callback_params is None:
  181. callback_params = []
  182. self.object_created.emit(obj, obj_plot, obj_autoselected, callback, callback_params)
  183. return obj
  184. def new_excellon_object(self):
  185. """
  186. Creates a new, blank Excellon object.
  187. :return: None
  188. """
  189. self.new_object('excellon', 'new_exc', lambda x, y: None, plot=False)
  190. def new_geometry_object(self):
  191. """
  192. Creates a new, blank and single-tool Geometry object.
  193. :return: None
  194. """
  195. outname = 'new_geo'
  196. def initialize(obj, app):
  197. obj.multitool = True
  198. obj.multigeo = True
  199. # store here the default data for Geometry Data
  200. default_data = {}
  201. for opt_key, opt_val in app.options.items():
  202. if opt_key.find('geometry' + "_") == 0:
  203. oname = opt_key[len('geometry') + 1:]
  204. default_data[oname] = self.app.options[opt_key]
  205. if opt_key.find('tools_mill' + "_") == 0:
  206. oname = opt_key[len('tools_mill') + 1:]
  207. default_data[oname] = self.app.options[opt_key]
  208. obj.tools = {}
  209. obj.tools.update({
  210. 1: {
  211. 'tooldia': float(app.defaults["geometry_cnctooldia"]),
  212. 'offset': 'Path',
  213. 'offset_value': 0.0,
  214. 'type': 'Rough',
  215. 'tool_type': 'C1',
  216. 'data': deepcopy(default_data),
  217. 'solid_geometry': []
  218. }
  219. })
  220. obj.tools[1]['data']['name'] = outname
  221. self.new_object('geometry', outname, initialize, plot=False)
  222. def new_gerber_object(self):
  223. """
  224. Creates a new, blank Gerber object.
  225. :return: None
  226. """
  227. def initialize(grb_obj, app):
  228. grb_obj.multitool = False
  229. grb_obj.source_file = []
  230. grb_obj.multigeo = False
  231. grb_obj.follow = False
  232. grb_obj.apertures = {}
  233. grb_obj.solid_geometry = []
  234. try:
  235. grb_obj.options['xmin'] = 0
  236. grb_obj.options['ymin'] = 0
  237. grb_obj.options['xmax'] = 0
  238. grb_obj.options['ymax'] = 0
  239. except KeyError:
  240. pass
  241. self.new_object('gerber', 'new_grb', initialize, plot=False)
  242. def new_script_object(self):
  243. """
  244. Creates a new, blank TCL Script object.
  245. :return: None
  246. """
  247. # commands_list = "# AddCircle, AddPolygon, AddPolyline, AddRectangle, AlignDrill, " \
  248. # "AlignDrillGrid, Bbox, Bounds, ClearShell, CopperClear,\n" \
  249. # "# Cncjob, Cutout, Delete, Drillcncjob, ExportDXF, ExportExcellon, ExportGcode,\n" \
  250. # "# ExportGerber, ExportSVG, Exteriors, Follow, GeoCutout, GeoUnion, GetNames,\n" \
  251. # "# GetSys, ImportSvg, Interiors, Isolate, JoinExcellon, JoinGeometry, " \
  252. # "ListSys, MillDrills,\n" \
  253. # "# MillSlots, Mirror, New, NewExcellon, NewGeometry, NewGerber, Nregions, " \
  254. # "Offset, OpenExcellon, OpenGCode, OpenGerber, OpenProject,\n" \
  255. # "# Options, Paint, Panelize, PlotAl, PlotObjects, SaveProject, " \
  256. # "SaveSys, Scale, SetActive, SetSys, SetOrigin, Skew, SubtractPoly,\n" \
  257. # "# SubtractRectangle, Version, WriteGCode\n"
  258. new_source_file = '# %s\n' % _('CREATE A NEW FLATCAM TCL SCRIPT') + \
  259. '# %s:\n' % _('TCL Tutorial is here') + \
  260. '# https://www.tcl.tk/man/tcl8.5/tutorial/tcltutorial.html\n' + '\n\n' + \
  261. '# %s:\n' % _("FlatCAM commands list")
  262. new_source_file += '# %s\n\n' % _("Type >help< followed by Run Code for a list of FlatCAM Tcl Commands "
  263. "(displayed in Tcl Shell).")
  264. def initialize(obj, app):
  265. obj.source_file = deepcopy(new_source_file)
  266. outname = 'new_script'
  267. self.new_object('script', outname, initialize, plot=False)
  268. def new_document_object(self):
  269. """
  270. Creates a new, blank Document object.
  271. :return: None
  272. """
  273. def initialize(obj, app):
  274. obj.source_file = ""
  275. self.new_object('document', 'new_document', initialize, plot=False)
  276. def on_object_created(self, obj, plot, auto_select, callback, callback_params):
  277. """
  278. Event callback for object creation.
  279. It will add the new object to the collection. After that it will plot the object in a threaded way
  280. :param obj: The newly created FlatCAM object.
  281. :param plot: if the newly create object to be plotted
  282. :param auto_select: if the newly created object to be autoselected after creation
  283. :param callback: a method that is launched after the object is created
  284. :param callback_params: a list of parameters for the parameter: callback
  285. :type callback_params: list
  286. :return: None
  287. """
  288. t0 = time.time() # DEBUG
  289. log.debug("on_object_created()")
  290. # The Collection might change the name if there is a collision
  291. self.app.collection.append(obj)
  292. # after adding the object to the collection always update the list of objects that are in the collection
  293. self.app.all_objects_list = self.app.collection.get_list()
  294. # self.app.inform.emit('[selected] %s created & selected: %s' %
  295. # (str(obj.kind).capitalize(), str(obj.options['name'])))
  296. # #############################################################################################################
  297. # ###################### Set colors for the message in the Status Bar #######################################
  298. # #############################################################################################################
  299. if obj.kind == 'gerber':
  300. self.app.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
  301. kind=obj.kind.capitalize(),
  302. color='green',
  303. name=str(obj.options['name']), tx=_("created/selected"))
  304. )
  305. elif obj.kind == 'excellon':
  306. self.app.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
  307. kind=obj.kind.capitalize(),
  308. color='brown',
  309. name=str(obj.options['name']), tx=_("created/selected"))
  310. )
  311. elif obj.kind == 'cncjob':
  312. self.app.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
  313. kind=obj.kind.capitalize(),
  314. color='blue',
  315. name=str(obj.options['name']), tx=_("created/selected"))
  316. )
  317. elif obj.kind == 'geometry':
  318. self.app.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
  319. kind=obj.kind.capitalize(),
  320. color='red',
  321. name=str(obj.options['name']), tx=_("created/selected"))
  322. )
  323. elif obj.kind == 'script':
  324. self.app.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
  325. kind=obj.kind.capitalize(),
  326. color='orange',
  327. name=str(obj.options['name']), tx=_("created/selected"))
  328. )
  329. elif obj.kind == 'document':
  330. self.app.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
  331. kind=obj.kind.capitalize(),
  332. color='darkCyan',
  333. name=str(obj.options['name']), tx=_("created/selected"))
  334. )
  335. # ############################################################################################################
  336. # Set the colors for the objects that have geometry
  337. # ############################################################################################################
  338. if obj.kind != 'document' and obj.kind != 'script':
  339. try:
  340. if obj.kind == 'excellon':
  341. obj.fill_color = self.app.defaults["excellon_plot_fill"]
  342. obj.outline_color = self.app.defaults["excellon_plot_line"]
  343. if obj.kind == 'gerber':
  344. if self.app.defaults["gerber_store_color_list"] is True:
  345. group = self.app.collection.group_items["gerber"]
  346. index = group.child_count() - 1
  347. # when loading a Gerber object always create a color tuple (line color, fill_color)
  348. # and add it to the self.app.defaults["gerber_color_list"] from where it will be picked and used
  349. try:
  350. colors = self.app.defaults["gerber_color_list"][index]
  351. except IndexError:
  352. obj.outline_color = self.app.defaults["gerber_plot_line"]
  353. obj.fill_color = self.app.defaults["gerber_plot_fill"]
  354. colors = (obj.outline_color, obj.fill_color)
  355. self.app.defaults["gerber_color_list"].append(colors)
  356. new_line_color = colors[0]
  357. new_fill = colors[1]
  358. obj.outline_color = new_line_color
  359. obj.fill_color = new_fill
  360. else:
  361. obj.outline_color = self.app.defaults["gerber_plot_line"]
  362. obj.fill_color = self.app.defaults["gerber_plot_fill"]
  363. except Exception as e:
  364. log.warning("AppObject.new_object() -> setting colors error. %s" % str(e))
  365. # #############################################################################################################
  366. # update the SHELL auto-completer model with the name of the new object
  367. # #############################################################################################################
  368. self.app.shell.command_line().set_model_data(self.app.myKeywords)
  369. if auto_select or self.app.ui.notebook.currentWidget() is self.app.ui.properties_tab:
  370. # select the just opened object but deselect the previous ones
  371. self.app.collection.set_all_inactive()
  372. self.app.collection.set_active(obj.options["name"])
  373. else:
  374. self.app.collection.set_all_inactive()
  375. # here it is done the object plotting
  376. def plotting_task(t_obj):
  377. with self.app.proc_container.new('%s ...' % _("Plotting")):
  378. if t_obj.kind == 'cncjob':
  379. t_obj.plot(kind=self.app.defaults["cncjob_plot_kind"])
  380. if t_obj.kind == 'gerber':
  381. t_obj.plot(color=t_obj.outline_color, face_color=t_obj.fill_color)
  382. else:
  383. t_obj.plot()
  384. t1 = time.time() # DEBUG
  385. msg = "%f seconds adding object and plotting." % (t1 - t0)
  386. log.debug(msg)
  387. self.object_plotted.emit(t_obj)
  388. if t_obj.kind == 'gerber' and self.app.defaults["gerber_buffering"] != 'full' and \
  389. self.app.defaults["gerber_delayed_buffering"]:
  390. t_obj.do_buffer_signal.emit()
  391. # Send to worker
  392. # self.worker.add_task(worker_task, [self])
  393. if plot is True:
  394. self.app.worker_task.emit({'fcn': plotting_task, 'params': [obj]})
  395. if callback is not None:
  396. # callback(*callback_params)
  397. self.app.worker_task.emit({'fcn': callback, 'params': callback_params})
  398. def on_object_changed(self, obj):
  399. """
  400. Called whenever the geometry of the object was changed in some way.
  401. This require the update of it's bounding values so it can be the selected on canvas.
  402. Update the bounding box data from obj.options
  403. :param obj: the object that was changed
  404. :return: None
  405. """
  406. try:
  407. xmin, ymin, xmax, ymax = obj.bounds()
  408. except TypeError:
  409. return
  410. obj.options['xmin'] = xmin
  411. obj.options['ymin'] = ymin
  412. obj.options['xmax'] = xmax
  413. obj.options['ymax'] = ymax
  414. log.debug("Object changed, updating the bounding box data on self.options")
  415. # delete the old selection shape
  416. self.app.delete_selection_shape()
  417. self.app.should_we_save = True
  418. def on_object_plotted(self):
  419. """
  420. Callback called whenever the plotted object needs to be fit into the viewport (canvas)
  421. :return: None
  422. """
  423. self.app.on_zoom_fit()