AppObject.py 16 KB

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