AppObject.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  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)
  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):
  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 application.
  60. The function is called with 2 parameters: the new object and the App instance.
  61. :type initialize: function
  62. :param plot: If to plot the resulting object
  63. :param autoselected: if the resulting object is autoselected in the Project tab and therefore in the
  64. self.collection
  65. :return: None
  66. :rtype: None
  67. """
  68. log.debug("AppObject.new_object()")
  69. obj_plot = plot
  70. obj_autoselected = autoselected
  71. t0 = time.time() # Debug
  72. # ## Create object
  73. classdict = {
  74. "gerber": GerberObject,
  75. "excellon": ExcellonObject,
  76. "cncjob": CNCJobObject,
  77. "geometry": GeometryObject,
  78. "script": ScriptObject,
  79. "document": DocumentObject
  80. }
  81. log.debug("Calling object constructor...")
  82. # Object creation/instantiation
  83. obj = classdict[kind](name)
  84. obj.units = self.app.options["units"]
  85. # IMPORTANT
  86. # The key names in defaults and options dictionary's are not random:
  87. # they have to have in name first the type of the object (geometry, excellon, cncjob and gerber) or how it's
  88. # called here, the 'kind' followed by an underline. Above the App default values from self.defaults are
  89. # copied to self.options. After that, below, depending on the type of
  90. # object that is created, it will strip the name of the object and the underline (if the original key was
  91. # let's say "excellon_toolchange", it will strip the excellon_) and to the obj.options the key will become
  92. # "toolchange"
  93. for option in self.app.options:
  94. if option.find(kind + "_") == 0:
  95. oname = option[len(kind) + 1:]
  96. obj.options[oname] = self.app.options[option]
  97. obj.isHovering = False
  98. obj.notHovering = True
  99. # Initialize as per user request
  100. # User must take care to implement initialize
  101. # in a thread-safe way as is is likely that we
  102. # have been invoked in a separate thread.
  103. t1 = time.time()
  104. log.debug("%f seconds before initialize()." % (t1 - t0))
  105. try:
  106. return_value = initialize(obj, self.app)
  107. except Exception as e:
  108. msg = '[ERROR_NOTCL] %s' % _("An internal error has occurred. See shell.\n")
  109. msg += _("Object ({kind}) failed because: {error} \n\n").format(kind=kind, error=str(e))
  110. msg += traceback.format_exc()
  111. self.app.inform.emit(msg)
  112. return "fail"
  113. t2 = time.time()
  114. msg = "%f seconds executing initialize()." % (t2 - t1)
  115. log.debug(msg)
  116. self.app.shell_message(msg)
  117. if return_value == 'fail':
  118. log.debug("Object (%s) parsing and/or geometry creation failed." % kind)
  119. return "fail"
  120. # Check units and convert if necessary
  121. # This condition CAN be true because initialize() can change obj.units
  122. if self.app.options["units"].upper() != obj.units.upper():
  123. self.app.inform.emit('%s: %s' % (_("Converting units to "), self.app.options["units"]))
  124. obj.convert_units(self.app.options["units"])
  125. t3 = time.time()
  126. log.debug("%f seconds converting units." % (t3 - t2))
  127. # Create the bounding box for the object and then add the results to the obj.options
  128. # But not for Scripts or for Documents
  129. if kind != 'document' and kind != 'script':
  130. try:
  131. xmin, ymin, xmax, ymax = obj.bounds()
  132. obj.options['xmin'] = xmin
  133. obj.options['ymin'] = ymin
  134. obj.options['xmax'] = xmax
  135. obj.options['ymax'] = ymax
  136. except Exception as e:
  137. log.warning("AppObject.new_object() -> The object has no bounds properties. %s" % str(e))
  138. return "fail"
  139. try:
  140. if kind == 'excellon':
  141. obj.fill_color = self.app.defaults["excellon_plot_fill"]
  142. obj.outline_color = self.app.defaults["excellon_plot_line"]
  143. if kind == 'gerber':
  144. obj.fill_color = self.app.defaults["gerber_plot_fill"]
  145. obj.outline_color = self.app.defaults["gerber_plot_line"]
  146. except Exception as e:
  147. log.warning("AppObject.new_object() -> setting colors error. %s" % str(e))
  148. # update the KeyWords list with the name of the file
  149. self.app.myKeywords.append(obj.options['name'])
  150. log.debug("Moving new object back to main thread.")
  151. # Move the object to the main thread and let the app know that it is available.
  152. obj.moveToThread(self.app.main_thread)
  153. self.object_created.emit(obj, obj_plot, obj_autoselected)
  154. return obj
  155. def new_excellon_object(self):
  156. """
  157. Creates a new, blank Excellon object.
  158. :return: None
  159. """
  160. self.new_object('excellon', 'new_exc', lambda x, y: None, plot=False)
  161. def new_geometry_object(self):
  162. """
  163. Creates a new, blank and single-tool Geometry object.
  164. :return: None
  165. """
  166. def initialize(obj, app):
  167. obj.multitool = False
  168. self.new_object('geometry', 'new_geo', initialize, plot=False)
  169. def new_gerber_object(self):
  170. """
  171. Creates a new, blank Gerber object.
  172. :return: None
  173. """
  174. def initialize(grb_obj, app):
  175. grb_obj.multitool = False
  176. grb_obj.source_file = []
  177. grb_obj.multigeo = False
  178. grb_obj.follow = False
  179. grb_obj.apertures = {}
  180. grb_obj.solid_geometry = []
  181. try:
  182. grb_obj.options['xmin'] = 0
  183. grb_obj.options['ymin'] = 0
  184. grb_obj.options['xmax'] = 0
  185. grb_obj.options['ymax'] = 0
  186. except KeyError:
  187. pass
  188. self.new_object('gerber', 'new_grb', initialize, plot=False)
  189. def new_script_object(self):
  190. """
  191. Creates a new, blank TCL Script object.
  192. :return: None
  193. """
  194. # commands_list = "# AddCircle, AddPolygon, AddPolyline, AddRectangle, AlignDrill, " \
  195. # "AlignDrillGrid, Bbox, Bounds, ClearShell, CopperClear,\n" \
  196. # "# Cncjob, Cutout, Delete, Drillcncjob, ExportDXF, ExportExcellon, ExportGcode,\n" \
  197. # "# ExportGerber, ExportSVG, Exteriors, Follow, GeoCutout, GeoUnion, GetNames,\n" \
  198. # "# GetSys, ImportSvg, Interiors, Isolate, JoinExcellon, JoinGeometry, " \
  199. # "ListSys, MillDrills,\n" \
  200. # "# MillSlots, Mirror, New, NewExcellon, NewGeometry, NewGerber, Nregions, " \
  201. # "Offset, OpenExcellon, OpenGCode, OpenGerber, OpenProject,\n" \
  202. # "# Options, Paint, Panelize, PlotAl, PlotObjects, SaveProject, " \
  203. # "SaveSys, Scale, SetActive, SetSys, SetOrigin, Skew, SubtractPoly,\n" \
  204. # "# SubtractRectangle, Version, WriteGCode\n"
  205. new_source_file = '# %s\n' % _('CREATE A NEW FLATCAM TCL SCRIPT') + \
  206. '# %s:\n' % _('TCL Tutorial is here') + \
  207. '# https://www.tcl.tk/man/tcl8.5/tutorial/tcltutorial.html\n' + '\n\n' + \
  208. '# %s:\n' % _("FlatCAM commands list")
  209. new_source_file += '# %s\n\n' % _("Type >help< followed by Run Code for a list of FlatCAM Tcl Commands "
  210. "(displayed in Tcl Shell).")
  211. def initialize(obj, app):
  212. obj.source_file = deepcopy(new_source_file)
  213. outname = 'new_script'
  214. self.new_object('script', outname, initialize, plot=False)
  215. def new_document_object(self):
  216. """
  217. Creates a new, blank Document object.
  218. :return: None
  219. """
  220. def initialize(obj, app):
  221. obj.source_file = ""
  222. self.new_object('document', 'new_document', initialize, plot=False)
  223. def on_object_created(self, obj, plot, auto_select):
  224. """
  225. Event callback for object creation.
  226. It will add the new object to the collection. After that it will plot the object in a threaded way
  227. :param obj: The newly created FlatCAM object.
  228. :param plot: if the newly create object t obe plotted
  229. :param auto_select: if the newly created object to be autoselected after creation
  230. :return: None
  231. """
  232. t0 = time.time() # DEBUG
  233. log.debug("on_object_created()")
  234. # The Collection might change the name if there is a collision
  235. self.app.collection.append(obj)
  236. # after adding the object to the collection always update the list of objects that are in the collection
  237. self.app.all_objects_list = self.app.collection.get_list()
  238. # self.app.inform.emit('[selected] %s created & selected: %s' %
  239. # (str(obj.kind).capitalize(), str(obj.options['name'])))
  240. if obj.kind == 'gerber':
  241. self.app.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
  242. kind=obj.kind.capitalize(),
  243. color='green',
  244. name=str(obj.options['name']), tx=_("created/selected"))
  245. )
  246. elif obj.kind == 'excellon':
  247. self.app.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
  248. kind=obj.kind.capitalize(),
  249. color='brown',
  250. name=str(obj.options['name']), tx=_("created/selected"))
  251. )
  252. elif obj.kind == 'cncjob':
  253. self.app.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
  254. kind=obj.kind.capitalize(),
  255. color='blue',
  256. name=str(obj.options['name']), tx=_("created/selected"))
  257. )
  258. elif obj.kind == 'geometry':
  259. self.app.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
  260. kind=obj.kind.capitalize(),
  261. color='red',
  262. name=str(obj.options['name']), tx=_("created/selected"))
  263. )
  264. elif obj.kind == 'script':
  265. self.app.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
  266. kind=obj.kind.capitalize(),
  267. color='orange',
  268. name=str(obj.options['name']), tx=_("created/selected"))
  269. )
  270. elif obj.kind == 'document':
  271. self.app.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
  272. kind=obj.kind.capitalize(),
  273. color='darkCyan',
  274. name=str(obj.options['name']), tx=_("created/selected"))
  275. )
  276. # update the SHELL auto-completer model with the name of the new object
  277. self.app.shell._edit.set_model_data(self.app.myKeywords)
  278. if auto_select:
  279. # select the just opened object but deselect the previous ones
  280. self.app.collection.set_all_inactive()
  281. self.app.collection.set_active(obj.options["name"])
  282. else:
  283. self.app.collection.set_all_inactive()
  284. # here it is done the object plotting
  285. def task(t_obj):
  286. with self.app.proc_container.new(_("Plotting")):
  287. if t_obj.kind == 'cncjob':
  288. t_obj.plot(kind=self.app.defaults["cncjob_plot_kind"])
  289. else:
  290. t_obj.plot()
  291. t1 = time.time() # DEBUG
  292. msg = "%f seconds adding object and plotting." % (t1 - t0)
  293. log.debug(msg)
  294. self.object_plotted.emit(t_obj)
  295. if t_obj.kind == 'gerber' and self.app.defaults["gerber_buffering"] != 'full' and \
  296. self.app.defaults["gerber_delayed_buffering"]:
  297. t_obj.do_buffer_signal.emit()
  298. # Send to worker
  299. # self.worker.add_task(worker_task, [self])
  300. if plot is True:
  301. self.app.worker_task.emit({'fcn': task, 'params': [obj]})
  302. def on_object_changed(self, obj):
  303. """
  304. Called whenever the geometry of the object was changed in some way.
  305. This require the update of it's bounding values so it can be the selected on canvas.
  306. Update the bounding box data from obj.options
  307. :param obj: the object that was changed
  308. :return: None
  309. """
  310. try:
  311. xmin, ymin, xmax, ymax = obj.bounds()
  312. except TypeError:
  313. return
  314. obj.options['xmin'] = xmin
  315. obj.options['ymin'] = ymin
  316. obj.options['xmax'] = xmax
  317. obj.options['ymax'] = ymax
  318. log.debug("Object changed, updating the bounding box data on self.options")
  319. # delete the old selection shape
  320. self.app.delete_selection_shape()
  321. self.app.should_we_save = True
  322. def on_object_plotted(self):
  323. """
  324. Callback called whenever the plotted object needs to be fit into the viewport (canvas)
  325. :return: None
  326. """
  327. self.app.on_zoom_fit()