2910 lines
123 KiB
Python
2910 lines
123 KiB
Python
|
|
from PyQt6 import QtCore, QtGui, QtWidgets
|
|
from PyQt6.QtCore import Qt, QSettings
|
|
|
|
from appEditors.AppExcEditor import AppExcEditor
|
|
from appEditors.AppGeoEditor import AppGeoEditor
|
|
from appEditors.AppGerberEditor import AppGerberEditor
|
|
|
|
from appGUI.GUIElements import FCFileSaveDialog, FCMessageBox
|
|
from camlib import to_dict, dict2obj, ET, ParseError
|
|
from appParsers.ParseHPGL2 import HPGL2
|
|
|
|
from appObjects.ObjectCollection import GerberObject, ExcellonObject, GeometryObject, ScriptObject, CNCJobObject
|
|
|
|
from reportlab.graphics import renderPDF
|
|
from reportlab.pdfgen import canvas
|
|
from reportlab.lib.units import inch, mm
|
|
from reportlab.lib.pagesizes import landscape, portrait
|
|
from svglib.svglib import svg2rlg
|
|
from xml.dom.minidom import parseString as parse_xml_string
|
|
|
|
import time
|
|
import sys
|
|
import os
|
|
from copy import deepcopy
|
|
import re
|
|
|
|
import numpy as np
|
|
from numpy import Inf
|
|
|
|
from datetime import datetime
|
|
import simplejson as json
|
|
|
|
from appCommon.Common import LoudDict
|
|
|
|
from vispy.gloo.util import _screenshot
|
|
from vispy.io import write_png
|
|
|
|
import traceback
|
|
import lzma
|
|
from io import StringIO
|
|
|
|
# App Translation
|
|
import gettext
|
|
import appTranslation as fcTranslate
|
|
import builtins
|
|
|
|
import typing
|
|
|
|
if typing.TYPE_CHECKING:
|
|
import appMain
|
|
|
|
fcTranslate.apply_language('strings')
|
|
if '_' not in builtins.__dict__:
|
|
_ = gettext.gettext
|
|
|
|
|
|
class AppIO(QtCore.QObject):
|
|
def __init__(self, app):
|
|
"""
|
|
A class that holds all the menu -> file handlers
|
|
"""
|
|
super().__init__()
|
|
|
|
self.app = app
|
|
self.log = self.app.log
|
|
self.inform = self.app.inform
|
|
self.splash = self.app.splash
|
|
self.worker_task = self.app.worker_task
|
|
self.options = self.app.options
|
|
self.app_units = self.app.app_units
|
|
self.pagesize = {}
|
|
|
|
self.app.new_project_signal.connect(self.on_new_project_house_keeping)
|
|
|
|
def on_fileopengerber(self, name=None):
|
|
"""
|
|
File menu callback for opening a Gerber.
|
|
|
|
:param name:
|
|
:return: None
|
|
"""
|
|
|
|
self.log.debug("on_fileopengerber()")
|
|
|
|
_filter_ = "Gerber Files (*.gbr *.ger *.gtl *.gbl *.gts *.gbs *.gtp *.gbp *.gto *.gbo *.gm1 *.gml *.gm3 " \
|
|
"*.gko *.cmp *.sol *.stc *.sts *.plc *.pls *.crc *.crs *.tsm *.bsm *.ly2 *.ly15 *.dim *.mil *.grb " \
|
|
"*.top *.bot *.smt *.smb *.sst *.ssb *.spt *.spb *.pho *.gdo *.art *.gbd *.outline);;" \
|
|
"Protel Files (*.gtl *.gbl *.gts *.gbs *.gto *.gbo *.gtp *.gbp *.gml *.gm1 *.gm3 *.gko " \
|
|
"*.outline);;" \
|
|
"Eagle Files (*.cmp *.sol *.stc *.sts *.plc *.pls *.crc *.crs *.tsm *.bsm *.ly2 *.ly15 *.dim " \
|
|
"*.mil);;" \
|
|
"OrCAD Files (*.top *.bot *.smt *.smb *.sst *.ssb *.spt *.spb);;" \
|
|
"Allegro Files (*.art);;" \
|
|
"Mentor Files (*.pho *.gdo);;" \
|
|
"All Files (*.*)"
|
|
|
|
if name is None:
|
|
try:
|
|
filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Gerber"),
|
|
directory=self.app.get_last_folder(),
|
|
filter=_filter_,
|
|
initialFilter=self.app.last_op_gerber_filter)
|
|
except TypeError:
|
|
filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Gerber"), filter=_filter_)
|
|
|
|
filenames = [str(filename) for filename in filenames]
|
|
self.app.last_op_gerber_filter = _f
|
|
else:
|
|
filenames = [name]
|
|
self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
|
|
"Canvas initialization finished in"),
|
|
'%.2f' % self.app.used_time,
|
|
_("Opening Gerber file.")),
|
|
alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
|
|
color=QtGui.QColor("lightgray"))
|
|
|
|
if len(filenames) == 0:
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
else:
|
|
for filename in filenames:
|
|
if filename != '':
|
|
self.worker_task.emit({'fcn': self.open_gerber, 'params': [filename]})
|
|
|
|
def on_fileopenexcellon(self, name=None):
|
|
"""
|
|
File menu callback for opening an Excellon file.
|
|
|
|
:param name:
|
|
:return: None
|
|
"""
|
|
|
|
self.log.debug("on_fileopenexcellon()")
|
|
|
|
_filter_ = "Excellon Files (*.drl *.txt *.xln *.drd *.tap *.exc *.ncd);;" \
|
|
"All Files (*.*)"
|
|
if name is None:
|
|
try:
|
|
filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Excellon"),
|
|
directory=self.app.get_last_folder(),
|
|
filter=_filter_,
|
|
initialFilter=self.app.last_op_excellon_filter)
|
|
except TypeError:
|
|
filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Excellon"), filter=_filter_)
|
|
filenames = [str(filename) for filename in filenames]
|
|
self.app.last_op_excellon_filter = _f
|
|
else:
|
|
filenames = [str(name)]
|
|
self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
|
|
"Canvas initialization finished in"),
|
|
'%.2f' % self.app.used_time,
|
|
_("Opening Excellon file.")),
|
|
alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
|
|
color=QtGui.QColor("lightgray"))
|
|
|
|
if len(filenames) == 0:
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
else:
|
|
for filename in filenames:
|
|
if filename != '':
|
|
self.worker_task.emit({'fcn': self.open_excellon, 'params': [filename]})
|
|
|
|
def on_fileopengcode(self, name=None):
|
|
"""
|
|
|
|
File menu call back for opening gcode.
|
|
|
|
:param name:
|
|
:return:
|
|
"""
|
|
|
|
self.log.debug("on_fileopengcode()")
|
|
|
|
# https://bobcadsupport.com/helpdesk/index.php?/Knowledgebase/Article/View/13/5/known-g-code-file-extensions
|
|
_filter_ = "G-Code Files (*.txt *.nc *.ncc *.tap *.gcode *.cnc *.ecs *.fnc *.dnc *.ncg *.gc *.fan *.fgc" \
|
|
" *.din *.xpi *.hnc *.h *.i *.ncp *.min *.gcd *.rol *.knc *.mpr *.ply *.out *.eia *.sbp *.mpf);;" \
|
|
"All Files (*.*)"
|
|
|
|
if name is None:
|
|
try:
|
|
filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open G-Code"),
|
|
directory=self.app.get_last_folder(),
|
|
filter=_filter_,
|
|
initialFilter=self.app.last_op_gcode_filter)
|
|
except TypeError:
|
|
filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open G-Code"), filter=_filter_)
|
|
|
|
filenames = [str(filename) for filename in filenames]
|
|
self.app.last_op_gcode_filter = _f
|
|
else:
|
|
filenames = [name]
|
|
self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
|
|
"Canvas initialization finished in"),
|
|
'%.2f' % self.app.used_time,
|
|
_("Opening G-Code file.")),
|
|
alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
|
|
color=QtGui.QColor("lightgray"))
|
|
|
|
if len(filenames) == 0:
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
else:
|
|
for filename in filenames:
|
|
if filename != '':
|
|
self.worker_task.emit({'fcn': self.open_gcode, 'params': [filename, None, True]})
|
|
|
|
def on_file_openproject(self):
|
|
"""
|
|
File menu callback for opening a project.
|
|
|
|
:return: None
|
|
"""
|
|
|
|
self.log.debug("on_file_openproject()")
|
|
|
|
_filter_ = "FlatCAM Project (*.FlatPrj);;All Files (*.*)"
|
|
try:
|
|
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Project"),
|
|
directory=self.app.get_last_folder(), filter=_filter_)
|
|
except TypeError:
|
|
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Project"), filter=_filter_)
|
|
|
|
filename = str(filename)
|
|
|
|
if filename == "":
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
else:
|
|
# self.worker_task.emit({'fcn': self.open_project,
|
|
# 'params': [filename]})
|
|
# The above was failing because open_project() is not
|
|
# thread safe. The new_project()
|
|
self.open_project(filename)
|
|
|
|
def on_fileopenhpgl2(self, name=None):
|
|
"""
|
|
File menu callback for opening a HPGL2.
|
|
|
|
:param name:
|
|
:return: None
|
|
"""
|
|
self.log.debug("on_fileopenhpgl2()")
|
|
|
|
_filter_ = "HPGL2 Files (*.plt);;" \
|
|
"All Files (*.*)"
|
|
|
|
if name is None:
|
|
try:
|
|
filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open HPGL2"),
|
|
directory=self.app.get_last_folder(),
|
|
filter=_filter_)
|
|
except TypeError:
|
|
filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open HPGL2"), filter=_filter_)
|
|
|
|
filenames = [str(filename) for filename in filenames]
|
|
else:
|
|
filenames = [name]
|
|
self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
|
|
"Canvas initialization finished in"),
|
|
'%.2f' % self.app.used_time,
|
|
_("Opening HPGL2 file.")),
|
|
alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
|
|
color=QtGui.QColor("lightgray"))
|
|
|
|
if len(filenames) == 0:
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
else:
|
|
for filename in filenames:
|
|
if filename != '':
|
|
self.worker_task.emit({'fcn': self.open_hpgl2, 'params': [filename]})
|
|
|
|
def on_file_openconfig(self):
|
|
"""
|
|
File menu callback for opening a config file.
|
|
|
|
:return: None
|
|
"""
|
|
|
|
self.log.debug("on_file_openconfig()")
|
|
|
|
_filter_ = "FlatCAM Config (*.FlatConfig);;FlatCAM Config (*.json);;All Files (*.*)"
|
|
try:
|
|
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Configuration File"),
|
|
directory=self.app.data_path, filter=_filter_)
|
|
except TypeError:
|
|
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Configuration File"),
|
|
filter=_filter_)
|
|
|
|
if filename == "":
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
else:
|
|
self.open_config_file(filename)
|
|
|
|
def on_file_exportsvg(self):
|
|
"""
|
|
Callback for menu item File->Export SVG.
|
|
|
|
:return: None
|
|
"""
|
|
self.log.debug("on_file_exportsvg()")
|
|
|
|
obj = self.app.collection.get_active()
|
|
if obj is None:
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
|
|
return
|
|
|
|
# Check for more compatible types and add as required
|
|
if (not isinstance(obj, GeometryObject)
|
|
and not isinstance(obj, GerberObject)
|
|
and not isinstance(obj, CNCJobObject)
|
|
and not isinstance(obj, ExcellonObject)):
|
|
msg = _("Only Geometry, Gerber and CNCJob objects can be used.")
|
|
msgbox = FCMessageBox(parent=self.app.ui)
|
|
msgbox.setWindowTitle(msg) # taskbar still shows it
|
|
msgbox.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/app128.png'))
|
|
|
|
msgbox.setInformativeText(msg)
|
|
msgbox.setIconPixmap(QtGui.QPixmap(self.app.resource_location + '/waning.png'))
|
|
|
|
bt_ok = msgbox.addButton(_('Ok'), QtWidgets.QMessageBox.ButtonRole.AcceptRole)
|
|
msgbox.setDefaultButton(bt_ok)
|
|
msgbox.exec()
|
|
return
|
|
|
|
name = obj.obj_options["name"]
|
|
|
|
_filter = "SVG File (*.svg);;All Files (*.*)"
|
|
try:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Export SVG"),
|
|
directory=self.app.get_last_save_folder() + '/' + str(name) + '_svg',
|
|
ext_filter=_filter)
|
|
except TypeError:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Export SVG"),
|
|
ext_filter=_filter)
|
|
|
|
filename = str(filename)
|
|
|
|
if filename == "":
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
return
|
|
else:
|
|
self.export_svg(name, filename)
|
|
if self.options["global_open_style"] is False:
|
|
self.app.file_opened.emit("SVG", filename)
|
|
self.app.file_saved.emit("SVG", filename)
|
|
|
|
def on_file_exportpng(self):
|
|
|
|
self.log.debug("on_file_exportpng()")
|
|
|
|
date = str(datetime.today()).rpartition('.')[0]
|
|
date = ''.join(c for c in date if c not in ':-')
|
|
date = date.replace(' ', '_')
|
|
|
|
data = None
|
|
if self.app.use_3d_engine:
|
|
image = _screenshot(alpha=False)
|
|
data = np.asarray(image)
|
|
if not data.ndim == 3 and data.shape[-1] in (3, 4):
|
|
self.inform.emit('[[WARNING_NOTCL]] %s' % _('Data must be a 3D array with last dimension 3 or 4'))
|
|
return
|
|
|
|
filter_ = "PNG File (*.png);;All Files (*.*)"
|
|
try:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Export PNG Image"),
|
|
directory=self.app.get_last_save_folder() + '/png_' + date,
|
|
ext_filter=filter_)
|
|
except TypeError:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Export PNG Image"),
|
|
ext_filter=filter_)
|
|
|
|
filename = str(filename)
|
|
|
|
if filename == "":
|
|
self.inform.emit(_("Cancelled."))
|
|
return
|
|
else:
|
|
if self.app.use_3d_engine:
|
|
write_png(filename, data)
|
|
else:
|
|
self.app.plotcanvas.figure.savefig(filename)
|
|
|
|
if self.options["global_open_style"] is False:
|
|
self.app.file_opened.emit("png", filename)
|
|
self.app.file_saved.emit("png", filename)
|
|
|
|
def on_file_savegerber(self):
|
|
"""
|
|
Callback for menu item in Project context menu.
|
|
|
|
:return: None
|
|
"""
|
|
self.log.debug("on_file_savegerber()")
|
|
|
|
obj = self.app.collection.get_active()
|
|
if obj is None:
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
|
|
return
|
|
|
|
# Check for more compatible types and add as required
|
|
if not isinstance(obj, GerberObject):
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Only Gerber objects can be saved as Gerber files..."))
|
|
return
|
|
|
|
name = self.app.collection.get_active().obj_options["name"]
|
|
|
|
_filter = "Gerber File (*.GBR);;Gerber File (*.GRB);;All Files (*.*)"
|
|
try:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption="Save Gerber source file",
|
|
directory=self.app.get_last_save_folder() + '/' + name,
|
|
ext_filter=_filter)
|
|
except TypeError:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Save Gerber source file"),
|
|
ext_filter=_filter)
|
|
|
|
filename = str(filename)
|
|
|
|
if filename == "":
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
return
|
|
else:
|
|
self.save_source_file(name, filename)
|
|
if self.options["global_open_style"] is False:
|
|
self.app.file_opened.emit("Gerber", filename)
|
|
self.app.file_saved.emit("Gerber", filename)
|
|
|
|
def on_file_savescript(self):
|
|
"""
|
|
Callback for menu item in Project context menu.
|
|
|
|
:return: None
|
|
"""
|
|
self.log.debug("on_file_savescript()")
|
|
|
|
obj = self.app.collection.get_active()
|
|
if obj is None:
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
|
|
return
|
|
|
|
# Check for more compatible types and add as required
|
|
if not isinstance(obj, ScriptObject):
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Only Script objects can be saved as TCL Script files..."))
|
|
return
|
|
|
|
name = self.app.collection.get_active().obj_options["name"]
|
|
|
|
_filter = "FlatCAM Scripts (*.FlatScript);;All Files (*.*)"
|
|
try:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption="Save Script source file",
|
|
directory=self.app.get_last_save_folder() + '/' + name,
|
|
ext_filter=_filter)
|
|
except TypeError:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Save Script source file"),
|
|
ext_filter=_filter)
|
|
|
|
filename = str(filename)
|
|
|
|
if filename == "":
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
return
|
|
else:
|
|
self.save_source_file(name, filename)
|
|
if self.options["global_open_style"] is False:
|
|
self.app.file_opened.emit("Script", filename)
|
|
self.app.file_saved.emit("Script", filename)
|
|
|
|
def on_file_savedocument(self):
|
|
"""
|
|
Callback for menu item in Project context menu.
|
|
|
|
:return: None
|
|
"""
|
|
self.log.debug("on_file_savedocument()")
|
|
|
|
obj = self.app.collection.get_active()
|
|
if obj is None:
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
|
|
return
|
|
|
|
# Check for more compatible types and add as required
|
|
if not isinstance(obj, ScriptObject):
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Only Document objects can be saved as Document files..."))
|
|
return
|
|
|
|
name = self.app.collection.get_active().obj_options["name"]
|
|
|
|
_filter = "FlatCAM Documents (*.FlatDoc);;All Files (*.*)"
|
|
try:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption="Save Document source file",
|
|
directory=self.app.get_last_save_folder() + '/' + name,
|
|
ext_filter=_filter)
|
|
except TypeError:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Save Document source file"),
|
|
ext_filter=_filter)
|
|
|
|
filename = str(filename)
|
|
|
|
if filename == "":
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
return
|
|
else:
|
|
self.save_source_file(name, filename)
|
|
if self.options["global_open_style"] is False:
|
|
self.app.file_opened.emit("Document", filename)
|
|
self.app.file_saved.emit("Document", filename)
|
|
|
|
def on_file_saveexcellon(self):
|
|
"""
|
|
Callback for menu item in project context menu.
|
|
|
|
:return: None
|
|
"""
|
|
self.log.debug("on_file_saveexcellon()")
|
|
|
|
obj = self.app.collection.get_active()
|
|
if obj is None:
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
|
|
return
|
|
|
|
# Check for more compatible types and add as required
|
|
if not isinstance(obj, ExcellonObject):
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Only Excellon objects can be saved as Excellon files..."))
|
|
return
|
|
|
|
name = self.app.collection.get_active().obj_options["name"]
|
|
|
|
_filter = "Excellon File (*.DRL);;Excellon File (*.TXT);;All Files (*.*)"
|
|
try:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Save Excellon source file"),
|
|
directory=self.app.get_last_save_folder() + '/' + name,
|
|
ext_filter=_filter)
|
|
except TypeError:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Save Excellon source file"), ext_filter=_filter)
|
|
|
|
filename = str(filename)
|
|
|
|
if filename == "":
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
return
|
|
else:
|
|
self.save_source_file(name, filename)
|
|
if self.options["global_open_style"] is False:
|
|
self.app.file_opened.emit("Excellon", filename)
|
|
self.app.file_saved.emit("Excellon", filename)
|
|
|
|
def on_file_exportexcellon(self):
|
|
"""
|
|
Callback for menu item File->Export->Excellon.
|
|
|
|
:return: None
|
|
"""
|
|
self.log.debug("on_file_exportexcellon()")
|
|
|
|
obj = self.app.collection.get_active()
|
|
if obj is None:
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
|
|
return
|
|
|
|
# Check for more compatible types and add as required
|
|
if not isinstance(obj, ExcellonObject):
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Only Excellon objects can be saved as Excellon files..."))
|
|
return
|
|
|
|
name = self.app.collection.get_active().obj_options["name"]
|
|
|
|
_filter = self.options["excellon_save_filters"]
|
|
try:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Export Excellon"),
|
|
directory=self.app.get_last_save_folder() + '/' + name,
|
|
ext_filter=_filter)
|
|
except TypeError:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Export Excellon"),
|
|
ext_filter=_filter)
|
|
|
|
filename = str(filename)
|
|
|
|
if filename == "":
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
return
|
|
else:
|
|
used_extension = filename.rpartition('.')[2]
|
|
obj.update_filters(last_ext=used_extension, filter_string='excellon_save_filters')
|
|
|
|
self.export_excellon(name, filename)
|
|
if self.options["global_open_style"] is False:
|
|
self.app.file_opened.emit("Excellon", filename)
|
|
self.app.file_saved.emit("Excellon", filename)
|
|
|
|
def on_file_exportgerber(self):
|
|
"""
|
|
Callback for menu item File->Export->Gerber.
|
|
|
|
:return: None
|
|
"""
|
|
self.log.debug("on_file_exportgerber()")
|
|
|
|
obj = self.app.collection.get_active()
|
|
if obj is None:
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
|
|
return
|
|
|
|
# Check for more compatible types and add as required
|
|
if not isinstance(obj, GerberObject):
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Only Gerber objects can be saved as Gerber files..."))
|
|
return
|
|
|
|
name = self.app.collection.get_active().obj_options["name"]
|
|
|
|
_filter_ = self.options['gerber_save_filters']
|
|
try:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Export Gerber"),
|
|
directory=self.app.get_last_save_folder() + '/' + name,
|
|
ext_filter=_filter_)
|
|
except TypeError:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Export Gerber"),
|
|
ext_filter=_filter_)
|
|
|
|
filename = str(filename)
|
|
|
|
if filename == "":
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
return
|
|
else:
|
|
used_extension = filename.rpartition('.')[2]
|
|
obj.update_filters(last_ext=used_extension, filter_string='gerber_save_filters')
|
|
|
|
self.export_gerber(name, filename)
|
|
if self.options["global_open_style"] is False:
|
|
self.app.file_opened.emit("Gerber", filename)
|
|
self.app.file_saved.emit("Gerber", filename)
|
|
|
|
def on_file_exportdxf(self):
|
|
"""
|
|
Callback for menu item File->Export DXF.
|
|
|
|
:return: None
|
|
"""
|
|
self.log.debug("on_file_exportdxf()")
|
|
|
|
obj = self.app.collection.get_active()
|
|
if obj is None:
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("No object is selected."))
|
|
return
|
|
|
|
# Check for more compatible types and add as required
|
|
if obj.kind != 'geometry':
|
|
msg = _("Only Geometry objects can be used.")
|
|
msgbox = FCMessageBox(parent=self.app.ui)
|
|
msgbox.setWindowTitle(msg) # taskbar still shows it
|
|
msgbox.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/app128.png'))
|
|
|
|
msgbox.setInformativeText(msg)
|
|
msgbox.setIconPixmap(QtGui.QPixmap(self.app.resource_location + '/waning.png'))
|
|
|
|
msgbox.setIcon(QtWidgets.QMessageBox.Icon.Warning)
|
|
|
|
msgbox.setInformativeText(msg)
|
|
bt_ok = msgbox.addButton(_('Ok'), QtWidgets.QMessageBox.ButtonRole.AcceptRole)
|
|
msgbox.setDefaultButton(bt_ok)
|
|
msgbox.exec()
|
|
return
|
|
|
|
name = self.app.collection.get_active().obj_options["name"]
|
|
|
|
_filter_ = "DXF File .dxf (*.DXF);;All Files (*.*)"
|
|
try:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Export DXF"),
|
|
directory=self.app.get_last_save_folder() + '/' + name,
|
|
ext_filter=_filter_)
|
|
except TypeError:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Export DXF"),
|
|
ext_filter=_filter_)
|
|
|
|
filename = str(filename)
|
|
|
|
if filename == "":
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
return
|
|
else:
|
|
self.export_dxf(name, filename)
|
|
if self.options["global_open_style"] is False:
|
|
self.app.file_opened.emit("DXF", filename)
|
|
self.app.file_saved.emit("DXF", filename)
|
|
|
|
def on_file_importsvg(self, type_of_obj):
|
|
"""
|
|
Callback for menu item File->Import SVG.
|
|
:param type_of_obj: to import the SVG as Geometry or as Gerber
|
|
:type type_of_obj: str
|
|
:return: None
|
|
"""
|
|
self.log.debug("on_file_importsvg()")
|
|
|
|
_filter_ = "SVG File .svg (*.svg);;All Files (*.*)"
|
|
try:
|
|
filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Import SVG"),
|
|
directory=self.app.get_last_folder(),
|
|
filter=_filter_)
|
|
except TypeError:
|
|
filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Import SVG"),
|
|
filter=_filter_)
|
|
|
|
if type_of_obj != "geometry" and type_of_obj != "gerber":
|
|
type_of_obj = "geometry"
|
|
|
|
filenames = [str(filename) for filename in filenames]
|
|
|
|
if len(filenames) == 0:
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
else:
|
|
for filename in filenames:
|
|
if filename != '':
|
|
self.worker_task.emit({'fcn': self.import_svg, 'params': [filename, type_of_obj]})
|
|
|
|
def on_file_importdxf(self, type_of_obj):
|
|
"""
|
|
Callback for menu item File->Import DXF.
|
|
:param type_of_obj: to import the DXF as Geometry or as Gerber
|
|
:type type_of_obj: str
|
|
:return: None
|
|
"""
|
|
self.log.debug("on_file_importdxf()")
|
|
|
|
_filter_ = "DXF File .dxf (*.DXF);;All Files (*.*)"
|
|
try:
|
|
filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Import DXF"),
|
|
directory=self.app.get_last_folder(),
|
|
filter=_filter_)
|
|
except TypeError:
|
|
filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Import DXF"),
|
|
filter=_filter_)
|
|
|
|
if type_of_obj != "geometry" and type_of_obj != "gerber":
|
|
type_of_obj = "geometry"
|
|
|
|
filenames = [str(filename) for filename in filenames]
|
|
|
|
if len(filenames) == 0:
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
else:
|
|
for filename in filenames:
|
|
if filename != '':
|
|
self.worker_task.emit({'fcn': self.import_dxf, 'params': [filename, type_of_obj]})
|
|
|
|
def on_file_new_click(self):
|
|
"""
|
|
Callback for menu item File -> New.
|
|
Executed on clicking the Menu -> File -> New Project
|
|
|
|
:return:
|
|
"""
|
|
self.log.debug("on_file_new_click()")
|
|
|
|
if self.app.collection.get_list() and self.app.should_we_save:
|
|
msgbox = FCMessageBox(parent=self.app.ui)
|
|
title = _("Save changes")
|
|
txt = _("There are files/objects opened.\n"
|
|
"Creating a New project will delete them.\n"
|
|
"Do you want to Save the project?")
|
|
msgbox.setWindowTitle(title) # taskbar still shows it
|
|
msgbox.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/app128.png'))
|
|
msgbox.setText('<b>%s</b>' % title)
|
|
msgbox.setInformativeText(txt)
|
|
msgbox.setIconPixmap(QtGui.QPixmap(self.app.resource_location + '/save_as.png'))
|
|
|
|
bt_yes = msgbox.addButton(_('Yes'), QtWidgets.QMessageBox.ButtonRole.YesRole)
|
|
bt_no = msgbox.addButton(_('No'), QtWidgets.QMessageBox.ButtonRole.NoRole)
|
|
bt_cancel = msgbox.addButton(_('Cancel'), QtWidgets.QMessageBox.ButtonRole.RejectRole)
|
|
|
|
msgbox.setDefaultButton(bt_yes)
|
|
msgbox.exec()
|
|
response = msgbox.clickedButton()
|
|
|
|
if response == bt_yes:
|
|
self.on_file_saveprojectas(use_thread=True)
|
|
elif response == bt_cancel:
|
|
return
|
|
elif response == bt_no:
|
|
self.on_file_new_project(use_thread=True, silenced=True)
|
|
else:
|
|
self.on_file_new_project(use_thread=True, silenced=True)
|
|
|
|
def on_file_new_project(self, cli=None, reset_tcl=True, use_thread=None, silenced=None, keep_scripts=True):
|
|
"""
|
|
Returns the application to its startup state. This method is thread-safe.
|
|
|
|
:param cli: Boolean. If True this method was run from command line
|
|
:param reset_tcl: Boolean. If False, on new project creation the Tcl instance is not recreated, therefore it
|
|
will remember all the previous variables. If True then the Tcl is re-instantiated.
|
|
:param use_thread: Bool. If True some part of the initialization are done threaded
|
|
:param silenced: Bool or None. If True then the app will not ask to save the current parameters.
|
|
:param keep_scripts: Bool. If True the Script objects are not deleted when creating a new project
|
|
:return: None
|
|
"""
|
|
|
|
self.log.debug("on_file_new_project()")
|
|
|
|
t_start_proj = time.time()
|
|
|
|
# close any editor that might be open
|
|
if self.app.call_source != 'app':
|
|
self.app.editor2object(cleanup=True)
|
|
# ## EDITOR section
|
|
self.app.geo_editor = AppGeoEditor(self.app)
|
|
self.app.exc_editor = AppExcEditor(self.app)
|
|
self.app.grb_editor = AppGerberEditor(self.app)
|
|
|
|
for obj in self.app.collection.get_list():
|
|
# delete shapes left drawn from mark shape_collections, if any
|
|
if isinstance(obj, GerberObject):
|
|
try:
|
|
obj.mark_shapes_storage.clear()
|
|
obj.mark_shapes.clear(update=True)
|
|
obj.mark_shapes.enabled = False
|
|
except AttributeError:
|
|
pass
|
|
|
|
# also delete annotation shapes, if any
|
|
elif isinstance(obj, CNCJobObject):
|
|
try:
|
|
obj.text_col.enabled = False
|
|
del obj.text_col
|
|
obj.annotation.clear(update=True)
|
|
del obj.annotation
|
|
except AttributeError:
|
|
pass
|
|
|
|
# delete the exclusion areas
|
|
self.app.exc_areas.clear_shapes()
|
|
|
|
# delete any selection shape on canvas
|
|
self.app.delete_selection_shape()
|
|
|
|
# delete all App objects
|
|
if keep_scripts is True:
|
|
for prj_obj in self.app.collection.get_list():
|
|
if prj_obj.kind != 'script':
|
|
self.app.collection.delete_by_name(prj_obj.obj_options['name'], select_project=False)
|
|
else:
|
|
self.app.collection.delete_all()
|
|
|
|
self.log.debug('%s: %s %s.' %
|
|
("Deleted all the application objects", str(time.time() - t_start_proj), _("seconds")))
|
|
|
|
# add in Selected tab an initial text that describe the flow of work in FlatCAm
|
|
self.app.setup_default_properties_tab()
|
|
|
|
# Clear project filename
|
|
self.app.project_filename = None
|
|
|
|
default_file = self.app.defaults_path()
|
|
# Load the application options
|
|
self.options.load(filename=default_file, inform=self.inform)
|
|
|
|
# Re-fresh project options
|
|
self.app.on_defaults2options()
|
|
|
|
if use_thread is True:
|
|
self.app.new_project_signal.emit()
|
|
else:
|
|
t0 = time.time()
|
|
# Clear pool
|
|
self.app.clear_pool()
|
|
|
|
# Init FlatCAMTools
|
|
if reset_tcl is True:
|
|
self.app.init_tools(init_tcl=True)
|
|
else:
|
|
self.app.init_tools(init_tcl=False)
|
|
self.log.debug(
|
|
'%s: %s %s.' % ("Initiated the MP pool and plugins in: ", str(time.time() - t0), _("seconds")))
|
|
|
|
# tcl needs to be reinitialized, otherwise old shell variables etc remains
|
|
# self.app.shell.init_tcl()
|
|
|
|
# Try to close all tabs in the PlotArea but only if the appGUI is active (CLI is None)
|
|
if cli is None:
|
|
# we need to go in reverse because once we remove a tab then the index changes
|
|
# meaning that removing the first tab (idx = 0) then the tab at former idx = 1 will assume idx = 0
|
|
# and so on. Therefore the deletion should be done in reverse
|
|
wdg_count = self.app.ui.plot_tab_area.tabBar.count() - 1
|
|
for index in range(wdg_count, -1, -1):
|
|
try:
|
|
self.app.ui.plot_tab_area.closeTab(index)
|
|
except Exception as e:
|
|
self.log.error("App.on_file_new_project() --> %s" % str(e))
|
|
|
|
# # And then add again the Plot Area
|
|
self.app.ui.plot_tab_area.insertTab(0, self.app.ui.plot_tab, _("Plot Area"))
|
|
self.app.ui.plot_tab_area.protectTab(0)
|
|
|
|
# take the focus of the Notebook on Project Tab.
|
|
self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
|
|
|
|
self.log.debug('%s: %s %s.' % (_("Project created in"), str(time.time() - t_start_proj), _("seconds")))
|
|
self.app.ui.set_ui_title(name=_("New Project - Not saved"))
|
|
|
|
self.inform.emit('[success] %s...' % _("New Project created"))
|
|
|
|
def on_new_project_house_keeping(self):
|
|
"""
|
|
Do dome of the new project initialization in a threaded way
|
|
|
|
:return:
|
|
:rtype:
|
|
"""
|
|
t0 = time.time()
|
|
|
|
# Clear pool
|
|
self.log.debug("New Project: cleaning multiprocessing pool.")
|
|
self.app.clear_pool()
|
|
|
|
# Init FlatCAMTools
|
|
self.log.debug("New Project: initializing the Tools and Tcl Shell.")
|
|
self.app.init_tools(init_tcl=True)
|
|
self.log.debug('%s: %s %s.' % ("Initiated the MP pool and plugins in: ", str(time.time() - t0), _("seconds")))
|
|
|
|
def on_filenewscript(self, silent=False):
|
|
"""
|
|
Will create a new script file and open it in the Code Editor
|
|
|
|
:param silent: if True will not display status messages
|
|
:return: None
|
|
"""
|
|
self.log.debug("on_filenewscript()")
|
|
|
|
if silent is False:
|
|
self.inform.emit('[success] %s' % _("New TCL script file created in Code Editor."))
|
|
|
|
# hide coordinates toolbars in the infobar while in DB
|
|
self.app.ui.coords_toolbar.hide()
|
|
self.app.ui.delta_coords_toolbar.hide()
|
|
|
|
self.app.app_obj.new_script_object()
|
|
|
|
def on_fileopenscript(self, name=None, silent=False):
|
|
"""
|
|
Will open a Tcl script file into the Code Editor
|
|
|
|
:param silent: if True will not display status messages
|
|
:param name: name of a Tcl script file to open
|
|
:return: None
|
|
"""
|
|
|
|
self.log.debug("on_fileopenscript()")
|
|
|
|
_filter_ = "TCL script .FlatScript (*.FlatScript);;TCL script .tcl (*.TCL);;TCL script .txt (*.TXT);;" \
|
|
"All Files (*.*)"
|
|
|
|
if name:
|
|
filenames = [name]
|
|
else:
|
|
try:
|
|
filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(
|
|
caption=_("Open TCL script"), directory=self.app.get_last_folder(), filter=_filter_)
|
|
except TypeError:
|
|
filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open TCL script"), filter=_filter_)
|
|
|
|
if len(filenames) == 0:
|
|
if silent is False:
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
else:
|
|
for filename in filenames:
|
|
if filename != '':
|
|
self.worker_task.emit({'fcn': self.open_script, 'params': [filename]})
|
|
|
|
def on_fileopenscript_example(self, name=None, silent=False):
|
|
"""
|
|
Will open a Tcl script file into the Code Editor
|
|
|
|
:param silent: if True will not display status messages
|
|
:param name: name of a Tcl script file to open
|
|
:return:
|
|
"""
|
|
|
|
self.log.debug("on_fileopenscript_example()")
|
|
|
|
_filter_ = "TCL script .FlatScript (*.FlatScript);;TCL script .tcl (*.TCL);;TCL script .txt (*.TXT);;" \
|
|
"All Files (*.*)"
|
|
|
|
# test if the app was frozen and choose the path for the configuration file
|
|
if getattr(sys, "frozen", False) is True:
|
|
example_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + '\\assets\\examples'
|
|
else:
|
|
example_path = os.path.dirname(os.path.realpath(__file__)) + '\\assets\\examples'
|
|
|
|
if name:
|
|
filenames = [name]
|
|
else:
|
|
try:
|
|
filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(
|
|
caption=_("Open TCL script"), directory=example_path, filter=_filter_)
|
|
except TypeError:
|
|
filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open TCL script"), filter=_filter_)
|
|
|
|
if len(filenames) == 0:
|
|
if silent is False:
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
else:
|
|
for filename in filenames:
|
|
if filename != '':
|
|
self.worker_task.emit({'fcn': self.open_script, 'params': [filename]})
|
|
|
|
def on_filerunscript(self, name=None, silent=False):
|
|
"""
|
|
File menu callback for loading and running a TCL script.
|
|
|
|
:param silent: if True will not display status messages
|
|
:param name: name of a Tcl script file to be run by FlatCAM
|
|
:return: None
|
|
"""
|
|
|
|
self.log.debug("on_file_runscript()")
|
|
|
|
if name:
|
|
filename = name
|
|
if self.app.cmd_line_headless != 1:
|
|
self.splash.showMessage('%s: %ssec\n%s' %
|
|
(_("Canvas initialization started.\n"
|
|
"Canvas initialization finished in"), '%.2f' % self.app.used_time,
|
|
_("Executing ScriptObject file.")
|
|
),
|
|
alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
|
|
color=QtGui.QColor("lightgray"))
|
|
else:
|
|
_filter_ = "TCL script .FlatScript (*.FlatScript);;TCL script .tcl (*.TCL);;TCL script .txt (*.TXT);;" \
|
|
"All Files (*.*)"
|
|
try:
|
|
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Run TCL script"),
|
|
directory=self.app.get_last_folder(),
|
|
filter=_filter_)
|
|
except TypeError:
|
|
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Run TCL script"), filter=_filter_)
|
|
|
|
# The Qt methods above will return a QString which can cause problems later.
|
|
# So far json.dump() will fail to serialize it.
|
|
filename = str(filename)
|
|
|
|
if filename == "":
|
|
if silent is False:
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
else:
|
|
if self.app.cmd_line_headless != 1:
|
|
if self.app.ui.shell_dock.isHidden():
|
|
self.app.ui.shell_dock.show()
|
|
|
|
try:
|
|
with open(filename, "r") as tcl_script:
|
|
cmd_line_shellfile_content = tcl_script.read()
|
|
if self.app.cmd_line_headless != 1:
|
|
self.app.shell.exec_command(cmd_line_shellfile_content)
|
|
else:
|
|
self.app.shell.exec_command(cmd_line_shellfile_content, no_echo=True)
|
|
|
|
if silent is False:
|
|
self.inform.emit('[success] %s' % _("TCL script file opened in Code Editor and executed."))
|
|
except Exception as e:
|
|
self.app.error("App.on_filerunscript() -> %s" % str(e))
|
|
sys.exit(2)
|
|
|
|
def on_file_saveproject(self, silent=False):
|
|
"""
|
|
Callback for menu item File->Save Project. Saves the project to
|
|
``self.project_filename`` or calls ``self.on_file_saveprojectas()``
|
|
if set to None. The project is saved by calling ``self.save_project()``.
|
|
|
|
:param silent: if True will not display status messages
|
|
:return: None
|
|
"""
|
|
self.log.debug("on_file_saveproject()")
|
|
|
|
if self.app.project_filename is None:
|
|
self.on_file_saveprojectas()
|
|
else:
|
|
self.worker_task.emit({'fcn': self.save_project, 'params': [self.app.project_filename, silent]})
|
|
if self.options["global_open_style"] is False:
|
|
self.app.file_opened.emit("project", self.app.project_filename)
|
|
self.app.file_saved.emit("project", self.app.project_filename)
|
|
|
|
self.app.ui.set_ui_title(name=self.app.project_filename)
|
|
|
|
self.app.should_we_save = False
|
|
|
|
def on_file_saveprojectas(self, make_copy=False, use_thread=True, quit_action=False):
|
|
"""
|
|
Callback for menu item File->Save Project As... Opens a file
|
|
chooser and saves the project to the given file via
|
|
``self.save_project()``.
|
|
|
|
:param make_copy if to be create a copy of the project; boolean
|
|
:param use_thread: if to be run in a separate thread; boolean
|
|
:param quit_action: if to be followed by quiting the application; boolean
|
|
:return: None
|
|
"""
|
|
self.log.debug("on_file_saveprojectas()")
|
|
|
|
date = str(datetime.today()).rpartition('.')[0]
|
|
date = ''.join(c for c in date if c not in ':-')
|
|
date = date.replace(' ', '_')
|
|
|
|
filter_ = "FlatCAM Project .FlatPrj (*.FlatPrj);; All Files (*.*)"
|
|
try:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Save Project As ..."),
|
|
directory='{l_save}/{proj}_{date}'.format(l_save=str(self.app.get_last_save_folder()), date=date,
|
|
proj=_("Project")),
|
|
ext_filter=filter_
|
|
)
|
|
except TypeError:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Save Project As ..."),
|
|
ext_filter=filter_)
|
|
|
|
filename = str(filename)
|
|
|
|
if filename == '':
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
return
|
|
|
|
if use_thread is True:
|
|
self.worker_task.emit({'fcn': self.save_project, 'params': [filename, quit_action]})
|
|
else:
|
|
self.save_project(filename, quit_action)
|
|
|
|
# self.save_project(filename)
|
|
if self.options["global_open_style"] is False:
|
|
self.app.file_opened.emit("project", filename)
|
|
self.app.file_saved.emit("project", filename)
|
|
|
|
if not make_copy:
|
|
self.app.project_filename = filename
|
|
|
|
self.app.ui.set_ui_title(name=self.app.project_filename)
|
|
self.app.should_we_save = False
|
|
|
|
def on_file_save_objects_pdf(self, use_thread=True):
|
|
self.log.debug("on_file_save_objects_pdf()")
|
|
|
|
date = str(datetime.today()).rpartition('.')[0]
|
|
date = ''.join(c for c in date if c not in ':-')
|
|
date = date.replace(' ', '_')
|
|
|
|
try:
|
|
obj_selection = self.app.collection.get_selected()
|
|
if len(obj_selection) == 1:
|
|
obj_name = str(obj_selection[0].obj_options['name'])
|
|
else:
|
|
obj_name = _("General_print")
|
|
except AttributeError as att_err:
|
|
self.log.debug("App.on_file_save_object_pdf() --> %s" % str(att_err))
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
|
|
return
|
|
|
|
if not obj_selection:
|
|
self.inform.emit(
|
|
'[WARNING_NOTCL] %s %s' % (_("No object is selected."), _("Print everything in the workspace.")))
|
|
obj_selection = self.app.collection.get_list()
|
|
|
|
filter_ = "PDF File .pdf (*.PDF);; All Files (*.*)"
|
|
try:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Save Object as PDF ..."),
|
|
directory='{l_save}/{obj_name}_{date}'.format(l_save=str(self.app.get_last_save_folder()),
|
|
obj_name=obj_name,
|
|
date=date),
|
|
ext_filter=filter_
|
|
)
|
|
except TypeError:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Save Object as PDF ..."),
|
|
ext_filter=filter_)
|
|
|
|
filename = str(filename)
|
|
|
|
if filename == '':
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
return
|
|
|
|
if use_thread is True:
|
|
self.app.proc_container.new(_("Printing PDF ..."))
|
|
self.worker_task.emit({'fcn': self.save_pdf, 'params': [filename, obj_selection]})
|
|
else:
|
|
self.save_pdf(filename, obj_selection)
|
|
|
|
# self.save_project(filename)
|
|
if self.options["global_open_style"] is False:
|
|
self.app.file_opened.emit("pdf", filename)
|
|
self.app.file_saved.emit("pdf", filename)
|
|
|
|
def save_pdf(self, file_name, obj_selection):
|
|
self.log.debug("save_pdf()")
|
|
|
|
p_size = self.options['global_workspaceT']
|
|
orientation = self.options['global_workspace_orientation']
|
|
color = 'black'
|
|
transparency_level = 1.0
|
|
|
|
self.pagesize.update(
|
|
{
|
|
'Bounds': None,
|
|
'A0': (841 * mm, 1189 * mm),
|
|
'A1': (594 * mm, 841 * mm),
|
|
'A2': (420 * mm, 594 * mm),
|
|
'A3': (297 * mm, 420 * mm),
|
|
'A4': (210 * mm, 297 * mm),
|
|
'A5': (148 * mm, 210 * mm),
|
|
'A6': (105 * mm, 148 * mm),
|
|
'A7': (74 * mm, 105 * mm),
|
|
'A8': (52 * mm, 74 * mm),
|
|
'A9': (37 * mm, 52 * mm),
|
|
'A10': (26 * mm, 37 * mm),
|
|
|
|
'B0': (1000 * mm, 1414 * mm),
|
|
'B1': (707 * mm, 1000 * mm),
|
|
'B2': (500 * mm, 707 * mm),
|
|
'B3': (353 * mm, 500 * mm),
|
|
'B4': (250 * mm, 353 * mm),
|
|
'B5': (176 * mm, 250 * mm),
|
|
'B6': (125 * mm, 176 * mm),
|
|
'B7': (88 * mm, 125 * mm),
|
|
'B8': (62 * mm, 88 * mm),
|
|
'B9': (44 * mm, 62 * mm),
|
|
'B10': (31 * mm, 44 * mm),
|
|
|
|
'C0': (917 * mm, 1297 * mm),
|
|
'C1': (648 * mm, 917 * mm),
|
|
'C2': (458 * mm, 648 * mm),
|
|
'C3': (324 * mm, 458 * mm),
|
|
'C4': (229 * mm, 324 * mm),
|
|
'C5': (162 * mm, 229 * mm),
|
|
'C6': (114 * mm, 162 * mm),
|
|
'C7': (81 * mm, 114 * mm),
|
|
'C8': (57 * mm, 81 * mm),
|
|
'C9': (40 * mm, 57 * mm),
|
|
'C10': (28 * mm, 40 * mm),
|
|
|
|
# American paper sizes
|
|
'LETTER': (8.5 * inch, 11 * inch),
|
|
'LEGAL': (8.5 * inch, 14 * inch),
|
|
'ELEVENSEVENTEEN': (11 * inch, 17 * inch),
|
|
|
|
# From https://en.wikipedia.org/wiki/Paper_size
|
|
'JUNIOR_LEGAL': (5 * inch, 8 * inch),
|
|
'HALF_LETTER': (5.5 * inch, 8 * inch),
|
|
'GOV_LETTER': (8 * inch, 10.5 * inch),
|
|
'GOV_LEGAL': (8.5 * inch, 13 * inch),
|
|
'LEDGER': (17 * inch, 11 * inch),
|
|
}
|
|
)
|
|
|
|
# make sure that the Excellon objeacts are drawn on top of everything
|
|
excellon_objs = [obj for obj in obj_selection if obj.kind == 'excellon']
|
|
cncjob_objs = [obj for obj in obj_selection if obj.kind == 'cncjob']
|
|
# reverse the object order such that the first selected is on top
|
|
rest_objs = [obj for obj in obj_selection if obj.kind != 'excellon' and obj.kind != 'cncjob'][::-1]
|
|
obj_selection = rest_objs + cncjob_objs + excellon_objs
|
|
|
|
# generate the SVG files from the application objects
|
|
exported_svg = []
|
|
for obj in obj_selection:
|
|
svg_obj = obj.export_svg(scale_stroke_factor=0.0)
|
|
|
|
if obj.kind.lower() == 'gerber' or obj.kind.lower() == 'excellon':
|
|
color = obj.fill_color[:-2]
|
|
transparency_level = obj.fill_color[-2:]
|
|
elif obj.kind.lower() == 'geometry':
|
|
color = self.options["global_draw_color"]
|
|
|
|
# Change the attributes of the exported SVG
|
|
# We don't need stroke-width
|
|
# We set opacity to maximum
|
|
# We set the colour to WHITE
|
|
|
|
try:
|
|
root = ET.fromstring(svg_obj)
|
|
except Exception as e:
|
|
self.log.debug("AppIO.save_pdf() -> Missing root node -> %s" % str(e))
|
|
self.app.inform.emit("[ERROR_NOTCL] %s" % _("Failed."))
|
|
return
|
|
|
|
for child in root:
|
|
child.set('fill', str(color))
|
|
child.set('opacity', str(transparency_level))
|
|
child.set('stroke', str(color))
|
|
|
|
exported_svg.append(ET.tostring(root))
|
|
|
|
xmin = Inf
|
|
ymin = Inf
|
|
xmax = -Inf
|
|
ymax = -Inf
|
|
|
|
for obj in obj_selection:
|
|
try:
|
|
gxmin, gymin, gxmax, gymax = obj.bounds()
|
|
xmin = min([xmin, gxmin])
|
|
ymin = min([ymin, gymin])
|
|
xmax = max([xmax, gxmax])
|
|
ymax = max([ymax, gymax])
|
|
except Exception as e:
|
|
self.log.error("Tried to get bounds of empty geometry in App.save_pdf(). %s" % str(e))
|
|
|
|
# Determine bounding area for svg export
|
|
bounds = [xmin, ymin, xmax, ymax]
|
|
size = bounds[2] - bounds[0], bounds[3] - bounds[1]
|
|
|
|
# This contain the measure units
|
|
uom = obj_selection[0].units.lower()
|
|
|
|
# Define a boundary around SVG of about 1.0mm (~39mils)
|
|
if uom in "mm":
|
|
boundary = 1.0
|
|
else:
|
|
boundary = 0.0393701
|
|
|
|
# Convert everything to strings for use in the xml doc
|
|
svgwidth = str(size[0] + (2 * boundary))
|
|
svgheight = str(size[1] + (2 * boundary))
|
|
minx = str(bounds[0] - boundary)
|
|
miny = str(bounds[1] + boundary + size[1])
|
|
|
|
# Add a SVG Header and footer to the svg output from shapely
|
|
# The transform flips the Y Axis so that everything renders
|
|
# properly within svg apps such as inkscape
|
|
svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
|
|
'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
|
|
svg_header += 'width="' + svgwidth + uom + '" '
|
|
svg_header += 'height="' + svgheight + uom + '" '
|
|
svg_header += 'viewBox="' + minx + ' -' + miny + ' ' + svgwidth + ' ' + svgheight + '" '
|
|
svg_header += '>'
|
|
svg_header += '<g transform="scale(1,-1)">'
|
|
svg_footer = '</g> </svg>'
|
|
|
|
svg_elem = str(svg_header)
|
|
for svg_item in exported_svg:
|
|
svg_elem += str(svg_item)
|
|
svg_elem += str(svg_footer)
|
|
|
|
# Parse the xml through a xml parser just to add line feeds
|
|
# and to make it look more pretty for the output
|
|
doc = parse_xml_string(svg_elem)
|
|
doc_final = doc.toprettyxml()
|
|
|
|
try:
|
|
if self.app_units.upper() == 'IN':
|
|
unit = inch
|
|
else:
|
|
unit = mm
|
|
|
|
doc_final = StringIO(doc_final)
|
|
drawing = svg2rlg(doc_final)
|
|
|
|
if p_size == 'Bounds':
|
|
renderPDF.drawToFile(drawing, file_name)
|
|
else:
|
|
if orientation == 'p':
|
|
page_size = portrait(self.pagesize[p_size])
|
|
else:
|
|
page_size = landscape(self.pagesize[p_size])
|
|
|
|
my_canvas = canvas.Canvas(file_name, pagesize=page_size)
|
|
my_canvas.translate(bounds[0] * unit, bounds[1] * unit)
|
|
renderPDF.draw(drawing, my_canvas, 0, 0)
|
|
my_canvas.save()
|
|
except Exception as e:
|
|
self.log.error("AppIO.save_pdf() --> PDF output --> %s" % str(e))
|
|
return 'fail'
|
|
|
|
self.inform.emit('[success] %s: %s' % (_("PDF file saved to"), file_name))
|
|
|
|
def export_svg(self, obj_name, filename, scale_stroke_factor=0.00):
|
|
"""
|
|
Exports a Geometry Object to an SVG file.
|
|
|
|
:param obj_name: the name of the FlatCAM object to be saved as SVG
|
|
:param filename: Path to the SVG file to save to.
|
|
:param scale_stroke_factor: factor by which to change/scale the thickness of the features
|
|
:return:
|
|
"""
|
|
if filename is None:
|
|
filename = self.app.options["global_last_save_folder"] if \
|
|
self.app.options["global_last_save_folder"] is not None else self.app.options["global_last_folder"]
|
|
|
|
self.log.debug("export_svg()")
|
|
|
|
try:
|
|
obj = self.app.collection.get_by_name(str(obj_name))
|
|
except Exception:
|
|
return 'fail'
|
|
|
|
with self.app.proc_container.new(_("Exporting ...")):
|
|
exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor)
|
|
|
|
# Determine bounding area for svg export
|
|
bounds = obj.bounds()
|
|
size = obj.size()
|
|
|
|
# Convert everything to strings for use in the xml doc
|
|
svgwidth = str(size[0])
|
|
svgheight = str(size[1])
|
|
minx = str(bounds[0])
|
|
miny = str(bounds[1] - size[1])
|
|
uom = obj.units.lower()
|
|
|
|
# Add a SVG Header and footer to the svg output from shapely
|
|
# The transform flips the Y Axis so that everything renders
|
|
# properly within svg apps such as inkscape
|
|
svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
|
|
'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
|
|
svg_header += 'width="' + svgwidth + uom + '" '
|
|
svg_header += 'height="' + svgheight + uom + '" '
|
|
svg_header += 'viewBox="' + minx + ' ' + miny + ' ' + svgwidth + ' ' + svgheight + '">'
|
|
svg_header += '<g transform="scale(1,-1)">'
|
|
svg_footer = '</g> </svg>'
|
|
svg_elem = svg_header + exported_svg + svg_footer
|
|
|
|
# Parse the xml through a xml parser just to add line feeds
|
|
# and to make it look more pretty for the output
|
|
svgcode = parse_xml_string(svg_elem)
|
|
svgcode = svgcode.toprettyxml()
|
|
|
|
try:
|
|
with open(filename, 'w') as fp:
|
|
fp.write(svgcode)
|
|
except PermissionError:
|
|
self.inform.emit('[WARNING] %s' %
|
|
_("Permission denied, saving not possible.\n"
|
|
"Most likely another app is holding the file open and not accessible."))
|
|
return 'fail'
|
|
|
|
if self.options["global_open_style"] is False:
|
|
self.app.file_opened.emit("SVG", filename)
|
|
self.app.file_saved.emit("SVG", filename)
|
|
self.inform.emit('[success] %s: %s' % (_("SVG file exported to"), filename))
|
|
|
|
def on_import_preferences(self):
|
|
"""
|
|
Loads the application default settings from a saved file into
|
|
``self.options`` dictionary.
|
|
|
|
:return: None
|
|
"""
|
|
|
|
self.log.debug("App.on_import_preferences()")
|
|
|
|
# Show file chooser
|
|
filter_ = "Config File (*.FlatConfig);;All Files (*.*)"
|
|
try:
|
|
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import FlatCAM Preferences"),
|
|
directory=self.app.data_path,
|
|
filter=filter_)
|
|
except TypeError:
|
|
filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import FlatCAM Preferences"),
|
|
filter=filter_)
|
|
filename = str(filename)
|
|
if filename == "":
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
return
|
|
|
|
# Load in the options from the chosen file
|
|
self.options.load(filename=filename, inform=self.inform)
|
|
|
|
self.app.preferencesUiManager.on_preferences_edited()
|
|
self.inform.emit('[success] %s: %s' % (_("Imported Defaults from"), filename))
|
|
|
|
def on_export_preferences(self):
|
|
"""
|
|
Save the options dictionary to a file.
|
|
|
|
:return: None
|
|
"""
|
|
self.log.debug("on_export_preferences()")
|
|
|
|
# defaults_file_content = None
|
|
|
|
# Show file chooser
|
|
date = str(datetime.today()).rpartition('.')[0]
|
|
date = ''.join(c for c in date if c not in ':-')
|
|
date = date.replace(' ', '_')
|
|
filter__ = "Config File .FlatConfig (*.FlatConfig);;All Files (*.*)"
|
|
try:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Export FlatCAM Preferences"),
|
|
directory=os.path.join(self.app.data_path, 'preferences_%s' % date),
|
|
ext_filter=filter__
|
|
)
|
|
except TypeError:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Export FlatCAM Preferences"), ext_filter=filter__)
|
|
filename = str(filename)
|
|
if filename == "":
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
|
|
return 'fail'
|
|
|
|
# Update options
|
|
self.app.preferencesUiManager.defaults_read_form()
|
|
self.options.propagate_defaults()
|
|
|
|
# Save update options
|
|
try:
|
|
self.options.write(filename=filename)
|
|
except Exception:
|
|
self.inform.emit('[ERROR_NOTCL] %s %s' % (_("Failed to write defaults to file."), str(filename)))
|
|
return
|
|
|
|
if self.options["global_open_style"] is False:
|
|
self.app.file_opened.emit("preferences", filename)
|
|
self.app.file_saved.emit("preferences", filename)
|
|
self.inform.emit('[success] %s: %s' % (_("Exported preferences to"), filename))
|
|
|
|
def export_excellon(self, obj_name, filename, local_use=None, use_thread=True):
|
|
"""
|
|
Exports a Excellon Object to an Excellon file.
|
|
|
|
:param obj_name: the name of the FlatCAM object to be saved as Excellon
|
|
:param filename: Path to the Excellon file to save to.
|
|
:param local_use:
|
|
:param use_thread: if to be run in a separate thread
|
|
:return:
|
|
"""
|
|
|
|
if filename is None:
|
|
if self.app.options["global_last_save_folder"]:
|
|
filename = self.app.options["global_last_save_folder"] + '/' + 'exported_excellon'
|
|
else:
|
|
filename = self.app.options["global_last_folder"] + '/' + 'exported_excellon'
|
|
|
|
self.log.debug("export_excellon()")
|
|
|
|
format_exc = ';FILE_FORMAT=%d:%d\n' % (self.options["excellon_exp_integer"],
|
|
self.options["excellon_exp_decimals"]
|
|
)
|
|
|
|
if local_use is None:
|
|
try:
|
|
obj = self.app.collection.get_by_name(str(obj_name))
|
|
except Exception:
|
|
return "Could not retrieve object: %s" % obj_name
|
|
else:
|
|
obj = local_use
|
|
|
|
if not isinstance(obj, ExcellonObject):
|
|
self.inform.emit('[ERROR_NOTCL] %s' %
|
|
_("Failed. Only Excellon objects can be saved as Excellon files..."))
|
|
return
|
|
|
|
# updated units
|
|
eunits = self.options["excellon_exp_units"]
|
|
ewhole = self.options["excellon_exp_integer"]
|
|
efract = self.options["excellon_exp_decimals"]
|
|
ezeros = self.options["excellon_exp_zeros"]
|
|
eformat = self.options["excellon_exp_format"]
|
|
slot_type = self.options["excellon_exp_slot_type"]
|
|
|
|
fc_units = self.app_units.upper()
|
|
if fc_units == 'MM':
|
|
factor = 1 if eunits == 'METRIC' else 0.03937
|
|
else:
|
|
factor = 25.4 if eunits == 'METRIC' else 1
|
|
|
|
def make_excellon():
|
|
try:
|
|
time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
|
|
|
|
header = 'M48\n'
|
|
header += ';EXCELLON GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s\n' % \
|
|
(str(self.app.version), str(self.app.version_date))
|
|
|
|
header += ';Filename: %s' % str(obj_name) + '\n'
|
|
header += ';Created on : %s' % time_str + '\n'
|
|
|
|
if eformat == 'dec':
|
|
has_slots, excellon_code = obj.export_excellon(ewhole, efract, factor=factor, slot_type=slot_type)
|
|
header += eunits + '\n'
|
|
|
|
for tool in obj.tools:
|
|
if eunits == 'METRIC':
|
|
header += "T{tool}F00S00C{:.{dec}f}\n".format(float(obj.tools[tool]['tooldia']) * factor,
|
|
tool=str(tool),
|
|
dec=2)
|
|
else:
|
|
header += "T{tool}F00S00C{:.{dec}f}\n".format(float(obj.tools[tool]['tooldia']) * factor,
|
|
tool=str(tool),
|
|
dec=4)
|
|
else:
|
|
if ezeros == 'LZ':
|
|
has_slots, excellon_code = obj.export_excellon(ewhole, efract,
|
|
form='ndec', e_zeros='LZ', factor=factor,
|
|
slot_type=slot_type)
|
|
header += '%s,%s\n' % (eunits, 'LZ')
|
|
header += format_exc
|
|
|
|
for tool in obj.tools:
|
|
if eunits == 'METRIC':
|
|
header += "T{tool}F00S00C{:.{dec}f}\n".format(
|
|
float(obj.tools[tool]['tooldia']) * factor,
|
|
tool=str(tool),
|
|
dec=2)
|
|
else:
|
|
header += "T{tool}F00S00C{:.{dec}f}\n".format(
|
|
float(obj.tools[tool]['tooldia']) * factor,
|
|
tool=str(tool),
|
|
dec=4)
|
|
else:
|
|
has_slots, excellon_code = obj.export_excellon(ewhole, efract,
|
|
form='ndec', e_zeros='TZ', factor=factor,
|
|
slot_type=slot_type)
|
|
header += '%s,%s\n' % (eunits, 'TZ')
|
|
header += format_exc
|
|
|
|
for tool in obj.tools:
|
|
if eunits == 'METRIC':
|
|
header += "T{tool}F00S00C{:.{dec}f}\n".format(
|
|
float(obj.tools[tool]['tooldia']) * factor,
|
|
tool=str(tool),
|
|
dec=2)
|
|
else:
|
|
header += "T{tool}F00S00C{:.{dec}f}\n".format(
|
|
float(obj.tools[tool]['tooldia']) * factor,
|
|
tool=str(tool),
|
|
dec=4)
|
|
header += '%\n'
|
|
footer = 'M30\n'
|
|
|
|
exported_excellon = header
|
|
exported_excellon += excellon_code
|
|
exported_excellon += footer
|
|
|
|
if local_use is None:
|
|
try:
|
|
with open(filename, 'w') as fp:
|
|
fp.write(exported_excellon)
|
|
except PermissionError:
|
|
self.inform.emit('[WARNING] %s' %
|
|
_("Permission denied, saving not possible.\n"
|
|
"Most likely another app is holding the file open and not accessible."))
|
|
return 'fail'
|
|
|
|
if self.options["global_open_style"] is False:
|
|
self.app.file_opened.emit("Excellon", filename)
|
|
self.app.file_saved.emit("Excellon", filename)
|
|
self.inform.emit('[success] %s: %s' % (_("Excellon file exported to"), filename))
|
|
else:
|
|
return exported_excellon
|
|
except Exception as e:
|
|
self.log.error("App.export_excellon.make_excellon() --> %s" % str(e))
|
|
return 'fail'
|
|
|
|
if use_thread is True:
|
|
|
|
with self.app.proc_container.new(_("Exporting ...")):
|
|
|
|
def job_thread_exc(app_obj):
|
|
ret = make_excellon()
|
|
if ret == 'fail':
|
|
app_obj.inform.emit('[ERROR_NOTCL] %s' % _('Could not export.'))
|
|
return
|
|
|
|
self.worker_task.emit({'fcn': job_thread_exc, 'params': [self]})
|
|
else:
|
|
eret = make_excellon()
|
|
if eret == 'fail':
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _('Could not export.'))
|
|
return 'fail'
|
|
if local_use is not None:
|
|
return eret
|
|
|
|
def export_gerber(self, obj_name, filename, local_use=None, use_thread=True):
|
|
"""
|
|
Exports a Gerber Object to an Gerber file.
|
|
|
|
:param obj_name: the name of the FlatCAM object to be saved as Gerber
|
|
:param filename: Path to the Gerber file to save to.
|
|
:param local_use: if the Gerber code is to be saved to a file (None) or used within FlatCAM.
|
|
When not None, the value will be the actual Gerber object for which to create
|
|
the Gerber code
|
|
:param use_thread: if to be run in a separate thread
|
|
:return:
|
|
"""
|
|
if filename is None:
|
|
filename = self.app.options["global_last_save_folder"] if \
|
|
self.app.options["global_last_save_folder"] is not None else self.app.options["global_last_folder"]
|
|
|
|
self.log.debug("export_gerber()")
|
|
|
|
if local_use is None:
|
|
try:
|
|
obj = self.app.collection.get_by_name(str(obj_name))
|
|
except Exception:
|
|
return 'fail'
|
|
else:
|
|
obj = local_use
|
|
|
|
# updated units
|
|
gunits = self.options["gerber_exp_units"]
|
|
gwhole = self.options["gerber_exp_integer"]
|
|
gfract = self.options["gerber_exp_decimals"]
|
|
gzeros = self.options["gerber_exp_zeros"]
|
|
|
|
fc_units = self.app_units.upper()
|
|
if fc_units == 'MM':
|
|
factor = 1 if gunits == 'MM' else 0.03937
|
|
else:
|
|
factor = 25.4 if gunits == 'MM' else 1
|
|
|
|
def make_gerber():
|
|
try:
|
|
time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
|
|
|
|
header = 'G04*\n'
|
|
header += 'G04 RS-274X GERBER GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s*\n' % \
|
|
(str(self.app.version), str(self.app.version_date))
|
|
|
|
header += 'G04 Filename: %s*' % str(obj_name) + '\n'
|
|
header += 'G04 Created on : %s*' % time_str + '\n'
|
|
header += '%%FS%sAX%s%sY%s%s*%%\n' % (gzeros, gwhole, gfract, gwhole, gfract)
|
|
header += "%MO{units}*%\n".format(units=gunits)
|
|
|
|
for apid in obj.tools:
|
|
if obj.tools[apid]['type'] == 'C':
|
|
header += "%ADD{apid}{type},{size}*%\n".format(
|
|
apid=str(apid),
|
|
type='C',
|
|
size=(factor * obj.tools[apid]['size'])
|
|
)
|
|
elif obj.tools[apid]['type'] == 'R':
|
|
header += "%ADD{apid}{type},{width}X{height}*%\n".format(
|
|
apid=str(apid),
|
|
type='R',
|
|
width=(factor * obj.tools[apid]['width']),
|
|
height=(factor * obj.tools[apid]['height'])
|
|
)
|
|
elif obj.tools[apid]['type'] == 'O':
|
|
header += "%ADD{apid}{type},{width}X{height}*%\n".format(
|
|
apid=str(apid),
|
|
type='O',
|
|
width=(factor * obj.tools[apid]['width']),
|
|
height=(factor * obj.tools[apid]['height'])
|
|
)
|
|
|
|
header += '\n'
|
|
|
|
# obsolete units but some software may need it
|
|
if gunits == 'IN':
|
|
header += 'G70*\n'
|
|
else:
|
|
header += 'G71*\n'
|
|
|
|
# Absolute Mode
|
|
header += 'G90*\n'
|
|
|
|
header += 'G01*\n'
|
|
# positive polarity
|
|
header += '%LPD*%\n'
|
|
|
|
footer = 'M02*\n'
|
|
|
|
gerber_code = obj.export_gerber(gwhole, gfract, g_zeros=gzeros, factor=factor)
|
|
|
|
exported_gerber = header
|
|
exported_gerber += gerber_code
|
|
exported_gerber += footer
|
|
|
|
if local_use is None:
|
|
try:
|
|
with open(filename, 'w') as fp:
|
|
fp.write(exported_gerber)
|
|
except PermissionError:
|
|
self.inform.emit('[WARNING] %s' %
|
|
_("Permission denied, saving not possible.\n"
|
|
"Most likely another app is holding the file open and not accessible."))
|
|
return 'fail'
|
|
|
|
if self.options["global_open_style"] is False:
|
|
self.app.file_opened.emit("Gerber", filename)
|
|
self.app.file_saved.emit("Gerber", filename)
|
|
self.inform.emit('[success] %s: %s' % (_("Gerber file exported to"), filename))
|
|
else:
|
|
return exported_gerber
|
|
except Exception as e:
|
|
self.log.error("App.export_gerber.make_gerber() --> %s" % str(e))
|
|
return 'fail'
|
|
|
|
if use_thread is True:
|
|
with self.app.proc_container.new(_("Exporting ...")):
|
|
|
|
def job_thread_grb(app_obj):
|
|
ret = make_gerber()
|
|
if ret == 'fail':
|
|
app_obj.inform.emit('[ERROR_NOTCL] %s' % _('Could not export.'))
|
|
return 'fail'
|
|
|
|
self.worker_task.emit({'fcn': job_thread_grb, 'params': [self]})
|
|
else:
|
|
gret = make_gerber()
|
|
if gret == 'fail':
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _('Could not export.'))
|
|
return 'fail'
|
|
if local_use is not None:
|
|
return gret
|
|
|
|
def export_dxf(self, obj_name, filename, local_use=None, use_thread=True):
|
|
"""
|
|
Exports a Geometry Object to an DXF file.
|
|
|
|
:param obj_name: the name of the FlatCAM object to be saved as DXF
|
|
:param filename: Path to the DXF file to save to.
|
|
:param local_use: if the Gerber code is to be saved to a file (None) or used within FlatCAM.
|
|
When not None, the value will be the actual Geometry object for which to create
|
|
the Geometry/DXF code
|
|
:param use_thread: if to be run in a separate thread
|
|
:return:
|
|
"""
|
|
if filename is None:
|
|
filename = self.app.options["global_last_save_folder"] if \
|
|
self.app.options["global_last_save_folder"] is not None else self.app.options["global_last_folder"]
|
|
|
|
self.log.debug("export_dxf()")
|
|
|
|
if local_use is None:
|
|
try:
|
|
obj = self.app.collection.get_by_name(str(obj_name))
|
|
except Exception:
|
|
return 'fail'
|
|
else:
|
|
obj = local_use
|
|
|
|
def make_dxf():
|
|
try:
|
|
dxf_code = obj.export_dxf()
|
|
if local_use is None:
|
|
try:
|
|
dxf_code.saveas(filename)
|
|
except PermissionError:
|
|
self.inform.emit('[WARNING] %s' %
|
|
_("Permission denied, saving not possible.\n"
|
|
"Most likely another app is holding the file open and not accessible."))
|
|
return 'fail'
|
|
|
|
if self.options["global_open_style"] is False:
|
|
self.app.file_opened.emit("DXF", filename)
|
|
self.app.file_saved.emit("DXF", filename)
|
|
self.inform.emit('[success] %s: %s' % (_("DXF file exported to"), filename))
|
|
else:
|
|
return dxf_code
|
|
except Exception as e:
|
|
self.log.error("App.export_dxf.make_dxf() --> %s" % str(e))
|
|
return 'fail'
|
|
|
|
if use_thread is True:
|
|
|
|
with self.app.proc_container.new(_("Exporting ...")):
|
|
|
|
def job_thread_exc(app_obj):
|
|
ret_dxf_val = make_dxf()
|
|
if ret_dxf_val == 'fail':
|
|
app_obj.inform.emit('[WARNING_NOTCL] %s' % _('Could not export.'))
|
|
return
|
|
|
|
self.worker_task.emit({'fcn': job_thread_exc, 'params': [self]})
|
|
else:
|
|
ret = make_dxf()
|
|
if ret == 'fail':
|
|
self.inform.emit('[WARNING_NOTCL] %s' % _('Could not export.'))
|
|
return
|
|
if local_use is not None:
|
|
return ret
|
|
|
|
def import_svg(self, filename, geo_type='geometry', outname=None, plot=True):
|
|
"""
|
|
Adds a new Geometry Object to the projects and populates
|
|
it with shapes extracted from the SVG file.
|
|
|
|
:param plot: If True then the resulting object will be plotted on canvas
|
|
:param filename: Path to the SVG file.
|
|
:param geo_type: Type of FlatCAM object that will be created from SVG
|
|
:param outname: The name given to the resulting FlatCAM object
|
|
:return:
|
|
"""
|
|
self.log.debug("App.import_svg()")
|
|
if not os.path.exists(filename):
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("File no longer available."))
|
|
return
|
|
|
|
obj_type = ""
|
|
if geo_type is None or geo_type == "geometry":
|
|
obj_type = "geometry"
|
|
elif geo_type == "gerber":
|
|
obj_type = "gerber"
|
|
else:
|
|
self.inform.emit('[ERROR_NOTCL] %s' %
|
|
_("Not supported type is picked as parameter. Only Geometry and Gerber are supported"))
|
|
return
|
|
|
|
units = self.app_units.upper()
|
|
|
|
def obj_init(geo_obj, app_obj):
|
|
res = geo_obj.import_svg(filename, obj_type, units=units)
|
|
if res == 'fail':
|
|
return 'fail'
|
|
|
|
geo_obj.multigeo = True
|
|
|
|
with open(filename) as f:
|
|
file_content = f.read()
|
|
geo_obj.source_file = file_content
|
|
|
|
# appGUI feedback
|
|
app_obj.inform.emit('[success] %s: %s' % (_("Opened"), filename))
|
|
|
|
with self.app.proc_container.new('%s ...' % _("Importing")):
|
|
|
|
# Object name
|
|
name = outname or filename.split('/')[-1].split('\\')[-1]
|
|
|
|
ret = self.app.app_obj.new_object(obj_type, name, obj_init, autoselected=False, plot=plot)
|
|
|
|
if ret == 'fail':
|
|
self.inform.emit('[ERROR_NOTCL]%s' % _('Import failed.'))
|
|
return 'fail'
|
|
|
|
# Register recent file
|
|
self.app.file_opened.emit("svg", filename)
|
|
|
|
def import_dxf(self, filename, geo_type='geometry', outname=None, plot=True):
|
|
"""
|
|
Adds a new Geometry Object to the projects and populates
|
|
it with shapes extracted from the DXF file.
|
|
|
|
:param filename: Path to the DXF file.
|
|
:param geo_type: Type of FlatCAM object that will be created from DXF
|
|
:param outname: Name for the imported Geometry
|
|
:param plot: If True then the resulting object will be plotted on canvas
|
|
:return:
|
|
"""
|
|
self.log.debug(" ********* Importing DXF as: %s ********* " % geo_type.capitalize())
|
|
if not os.path.exists(filename):
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("File no longer available."))
|
|
return
|
|
|
|
obj_type = ""
|
|
if geo_type is None or geo_type == "geometry":
|
|
obj_type = "geometry"
|
|
elif geo_type == "gerber":
|
|
obj_type = geo_type
|
|
else:
|
|
self.inform.emit('[ERROR_NOTCL] %s' %
|
|
_("Not supported type is picked as parameter. Only Geometry and Gerber are supported"))
|
|
return
|
|
|
|
units = self.app_units.upper()
|
|
|
|
def obj_init(geo_obj, app_obj):
|
|
if obj_type == "geometry":
|
|
geo_obj.import_dxf_as_geo(filename, units=units)
|
|
elif obj_type == "gerber":
|
|
geo_obj.import_dxf_as_gerber(filename, units=units)
|
|
else:
|
|
return "fail"
|
|
|
|
with open(filename) as f:
|
|
file_content = f.read()
|
|
geo_obj.source_file = file_content
|
|
|
|
# appGUI feedback
|
|
app_obj.inform.emit('[success] %s: %s' % (_("Opened"), filename))
|
|
|
|
with self.app.proc_container.new('%s ...' % _("Importing")):
|
|
|
|
# Object name
|
|
name = outname or filename.split('/')[-1].split('\\')[-1]
|
|
|
|
ret = self.app.app_obj.new_object(obj_type, name, obj_init, autoselected=False, plot=plot)
|
|
|
|
if ret == 'fail':
|
|
self.inform.emit('[ERROR_NOTCL]%s' % _('Import failed.'))
|
|
return 'fail'
|
|
|
|
# Register recent file
|
|
self.app.file_opened.emit("dxf", filename)
|
|
|
|
def import_pdf(self, filename):
|
|
self.app.pdf_tool.periodic_check(1000)
|
|
self.worker_task.emit({'fcn': self.app.pdf_tool.open_pdf, 'params': [filename]})
|
|
|
|
def open_gerber(self, filename, outname=None, plot=True, from_tcl=False):
|
|
"""
|
|
Opens a Gerber file, parses it and creates a new object for
|
|
it in the program. Thread-safe.
|
|
|
|
:param outname: Name of the resulting object. None causes the
|
|
name to be that of the file. Str.
|
|
:param filename: Gerber file filename
|
|
:type filename: str
|
|
:param plot: boolean, to plot or not the resulting object
|
|
:param from_tcl: True if run from Tcl Shell
|
|
:return: None
|
|
"""
|
|
|
|
# How the object should be initialized
|
|
def obj_init(gerber_obj, app_obj):
|
|
|
|
assert isinstance(gerber_obj, GerberObject), \
|
|
"Expected to initialize a GerberObject but got %s" % type(gerber_obj)
|
|
|
|
# Opening the file happens here
|
|
try:
|
|
parse_ret_val = gerber_obj.parse_file(filename)
|
|
except IOError:
|
|
app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open file"), filename))
|
|
return "fail"
|
|
except ParseError as parse_err:
|
|
app_obj.inform.emit('[ERROR_NOTCL] %s: %s. %s' % (_("Failed to parse file"), filename, str(parse_err)))
|
|
app_obj.log.error(str(parse_err))
|
|
return "fail"
|
|
except Exception as e:
|
|
app_obj.log.error("App.open_gerber() --> %s" % str(e))
|
|
msg = '[ERROR] %s' % _("An internal error has occurred. See shell.\n")
|
|
msg += traceback.format_exc()
|
|
app_obj.inform.emit(msg)
|
|
return "fail"
|
|
|
|
if gerber_obj.is_empty():
|
|
app_obj.inform.emit('[ERROR_NOTCL] %s' %
|
|
_("Object is not Gerber file or empty. Aborting object creation."))
|
|
return "fail"
|
|
|
|
if parse_ret_val:
|
|
return parse_ret_val
|
|
|
|
self.log.debug("open_gerber()")
|
|
if not os.path.exists(filename):
|
|
self.inform.emit('[ERROR_NOTCL] %s. %s' % (filename, _("File no longer available.")))
|
|
return
|
|
|
|
with self.app.proc_container.new('%s...' % _("Opening")):
|
|
# Object name
|
|
name = outname or filename.split('/')[-1].split('\\')[-1]
|
|
|
|
# # ## Object creation # ##
|
|
ret_val = self.app.app_obj.new_object("gerber", name, obj_init, autoselected=False, plot=plot)
|
|
if ret_val == 'fail':
|
|
if from_tcl:
|
|
filename = self.options['global_tcl_path'] + '/' + name
|
|
ret_val = self.app.app_obj.new_object("gerber", name, obj_init, autoselected=False, plot=plot)
|
|
if ret_val == 'fail':
|
|
self.inform.emit('[ERROR_NOTCL]%s' % _('Open Gerber failed. Probable not a Gerber file.'))
|
|
return 'fail'
|
|
|
|
# Register recent file
|
|
self.app.file_opened.emit("gerber", filename)
|
|
|
|
# appGUI feedback
|
|
self.app.inform.emit('[success] %s: %s' % (_("Opened"), filename))
|
|
|
|
def open_excellon(self, filename, outname=None, plot=True, from_tcl=False):
|
|
"""
|
|
Opens an Excellon file, parses it and creates a new object for
|
|
it in the program. Thread-safe.
|
|
|
|
:param outname: Name of the resulting object. None causes the name to be that of the file.
|
|
:param filename: Excellon file filename
|
|
:type filename: str
|
|
:param plot: boolean, to plot or not the resulting object
|
|
:param from_tcl: True if run from Tcl Shell
|
|
:return: None
|
|
"""
|
|
|
|
self.log.debug("open_excellon()")
|
|
|
|
if not os.path.exists(filename):
|
|
self.inform.emit('[ERROR_NOTCL] %s. %s' % (filename, _("File no longer available.")))
|
|
return
|
|
|
|
# How the object should be initialized
|
|
def obj_init(excellon_obj, app_obj):
|
|
# populate excellon_obj.tools dict
|
|
try:
|
|
ret = excellon_obj.parse_file(filename=filename)
|
|
if ret == "fail":
|
|
app_obj.log.debug("Excellon parsing failed.")
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("This is not Excellon file."))
|
|
return "fail"
|
|
except IOError:
|
|
app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Cannot open file"), filename))
|
|
app_obj.log.debug("Could not open Excellon object.")
|
|
return "fail"
|
|
except Exception:
|
|
msg = '[ERROR_NOTCL] %s' % _("An internal error has occurred. See shell.\n")
|
|
msg += traceback.format_exc()
|
|
app_obj.inform.emit(msg)
|
|
return "fail"
|
|
|
|
# populate excellon_obj.solid_geometry list
|
|
ret = excellon_obj.create_geometry()
|
|
if ret == 'fail':
|
|
app_obj.log.debug("Could not create geometry for Excellon object.")
|
|
return "fail"
|
|
|
|
for tool in excellon_obj.tools:
|
|
if excellon_obj.tools[tool]['solid_geometry']:
|
|
return
|
|
app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("No geometry found in file"), filename))
|
|
return "fail"
|
|
|
|
with self.app.proc_container.new('%s...' % _("Opening")):
|
|
# Object name
|
|
name = outname or filename.split('/')[-1].split('\\')[-1]
|
|
ret_val = self.app.app_obj.new_object("excellon", name, obj_init, autoselected=False, plot=plot)
|
|
if ret_val == 'fail':
|
|
if from_tcl:
|
|
filename = self.options['global_tcl_path'] + '/' + name
|
|
ret_val = self.app.app_obj.new_object("excellon", name, obj_init, autoselected=False, plot=plot)
|
|
if ret_val == 'fail':
|
|
self.inform.emit('[ERROR_NOTCL] %s' %
|
|
_('Open Excellon file failed. Probable not an Excellon file.'))
|
|
return
|
|
|
|
# Register recent file
|
|
self.app.file_opened.emit("excellon", filename)
|
|
|
|
# appGUI feedback
|
|
self.inform.emit('[success] %s: %s' % (_("Opened"), filename))
|
|
|
|
def open_gcode(self, filename, outname=None, force_parsing=None, plot=True, from_tcl=False):
|
|
"""
|
|
Opens a G-gcode file, parses it and creates a new object for
|
|
it in the program. Thread-safe.
|
|
|
|
:param filename: G-code file filename
|
|
:param outname: Name of the resulting object. None causes the name to be that of the file.
|
|
:param force_parsing:
|
|
:param plot: If True plot the object on canvas
|
|
:param from_tcl: True if run from Tcl Shell
|
|
:return: None
|
|
"""
|
|
self.log.debug("open_gcode()")
|
|
|
|
if not os.path.exists(filename):
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("File no longer available."))
|
|
return
|
|
|
|
# How the object should be initialized
|
|
def obj_init(job_obj, app_obj_: "appMain.App"):
|
|
"""
|
|
:param job_obj: the resulting object
|
|
:type app_obj_: App
|
|
"""
|
|
|
|
app_obj_.inform.emit('%s...' % _("Reading GCode file")) # noqa
|
|
try:
|
|
f = open(filename)
|
|
gcode = f.read()
|
|
f.close()
|
|
except IOError:
|
|
app_obj_.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open"), filename)) # noqa
|
|
return "fail"
|
|
|
|
# try to find from what kind of object this GCode was created
|
|
gcode_origin = 'Geometry'
|
|
match = re.search(r'^.*Type:\s*.*(\bGeometry\b|\bExcellon\b)', gcode, re.MULTILINE)
|
|
if match:
|
|
gcode_origin = match.group(1)
|
|
job_obj.obj_options['type'] = gcode_origin
|
|
# add at least one default tool
|
|
if 'excellon' in gcode_origin.lower():
|
|
job_obj.tools = {1: {'data': {'tools_drill_ppname_e': 'default'}}}
|
|
if 'geometry' in gcode_origin.lower():
|
|
job_obj.tools = {1: {'data': {'tools_mill_ppname_g': 'default'}}}
|
|
|
|
# try to find from what kind of object this GCode was created
|
|
match = re.search(r'^.*Preprocessor:\s*.*\bGeometry\b|\bExcellon\b:\s(\b.*\b)', gcode, re.MULTILINE)
|
|
detected_preprocessor = 'default'
|
|
if match:
|
|
detected_preprocessor = match.group(1)
|
|
# determine if there is any tool data
|
|
match = re.findall(r'^.*Tool:\s*(\d*)\s*->\s*Dia:\s*(\d*\.?\d*)', gcode, re.MULTILINE)
|
|
if match:
|
|
job_obj.tools = {}
|
|
for m in match:
|
|
if 'excellon' in gcode_origin.lower():
|
|
job_obj.tools[int(m[0])] = {
|
|
'tooldia': float(m[1]),
|
|
'nr_drills': 0,
|
|
'nr_slots': 0,
|
|
'offset_z': 0,
|
|
'data': {'tools_drill_ppname_e': detected_preprocessor}
|
|
}
|
|
# if 'geometry' in gcode_origin.lower():
|
|
# job_obj.tools[int(m[0])] = {
|
|
# 'tooldia': float(m[1]),
|
|
# 'data': {
|
|
# 'tools_mill_ppname_g': detected_preprocessor,
|
|
# 'tools_mill_offset_value': 0.0,
|
|
# 'tools_mill_job_type': _('Roughing'),
|
|
# 'tools_mill_tool_shape': "C1"
|
|
#
|
|
# }
|
|
# }
|
|
job_obj.used_tools = list(job_obj.tools.keys())
|
|
# determine if there is any Cut Z data
|
|
match = re.findall(r'^.*Tool:\s*(\d*)\s*->\s*Z_Cut:\s*([\-|+]?\d*\.?\d*)', gcode, re.MULTILINE)
|
|
if match:
|
|
for m in match:
|
|
if 'excellon' in gcode_origin.lower():
|
|
if int(m[0]) in job_obj.tools:
|
|
job_obj.tools[int(m[0])]['offset_z'] = 0.0
|
|
job_obj.tools[int(m[0])]['data']['tools_drill_cutz'] = float(m[1])
|
|
# if 'geometry' in gcode_origin.lower():
|
|
# if int(m[0]) in job_obj.tools:
|
|
# job_obj.tools[int(m[0])]['data']['tools_mill_cutz'] = float(m[1])
|
|
|
|
job_obj.gcode = gcode
|
|
|
|
gcode_ret = job_obj.gcode_parse(force_parsing=force_parsing)
|
|
if gcode_ret == "fail":
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("This is not GCODE"))
|
|
return "fail"
|
|
|
|
for k in job_obj.tools:
|
|
job_obj.tools[k]['gcode'] = gcode
|
|
job_obj.tools[k]['gcode_parsed'] = []
|
|
|
|
for k in job_obj.tools:
|
|
print(k, job_obj.tools[k])
|
|
job_obj.create_geometry()
|
|
|
|
with self.app.proc_container.new('%s...' % _("Opening")):
|
|
|
|
# Object name
|
|
name = outname or filename.split('/')[-1].split('\\')[-1]
|
|
|
|
# New object creation and file processing
|
|
ret_val = self.app.app_obj.new_object("cncjob", name, obj_init, autoselected=False, plot=plot)
|
|
if ret_val == 'fail':
|
|
if from_tcl:
|
|
filename = self.options['global_tcl_path'] + '/' + name
|
|
ret_val = self.app.app_obj.new_object("cncjob", name, obj_init, autoselected=False, plot=plot)
|
|
if ret_val == 'fail':
|
|
self.inform.emit('[ERROR_NOTCL] %s' %
|
|
_("Failed to create CNCJob Object. Probable not a GCode file. "
|
|
"Try to load it from File menu.\n "
|
|
"Attempting to create a FlatCAM CNCJob Object from "
|
|
"G-Code file failed during processing"))
|
|
return "fail"
|
|
|
|
# Register recent file
|
|
self.app.file_opened.emit("cncjob", filename)
|
|
|
|
# appGUI feedback
|
|
self.inform.emit('[success] %s: %s' % (_("Opened"), filename))
|
|
|
|
def open_hpgl2(self, filename, outname=None):
|
|
"""
|
|
Opens a HPGL2 file, parses it and creates a new object for
|
|
it in the program. Thread-safe.
|
|
|
|
:param outname: Name of the resulting object. None causes the name to be that of the file.
|
|
:param filename: HPGL2 file filename
|
|
:return: None
|
|
"""
|
|
filename = filename
|
|
|
|
# How the object should be initialized
|
|
def obj_init(geo_obj, app_obj):
|
|
|
|
assert isinstance(geo_obj, GeometryObject), \
|
|
"Expected to initialize a GeometryObject but got %s" % type(geo_obj)
|
|
|
|
# Opening the file happens here
|
|
obj = HPGL2(self.app)
|
|
try:
|
|
HPGL2.parse_file(obj, filename)
|
|
except IOError:
|
|
app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open file"), filename))
|
|
return "fail"
|
|
except ParseError as parse_err:
|
|
app_obj.inform.emit('[ERROR_NOTCL] %s: %s. %s' % (_("Failed to parse file"), filename, str(parse_err)))
|
|
app_obj.log.error(str(parse_err))
|
|
return "fail"
|
|
except Exception as e:
|
|
app_obj.log.error("App.open_hpgl2() --> %s" % str(e))
|
|
msg = '[ERROR] %s' % _("An internal error has occurred. See shell.\n")
|
|
msg += traceback.format_exc()
|
|
app_obj.inform.emit(msg)
|
|
return "fail"
|
|
|
|
geo_obj.multigeo = True
|
|
geo_obj.solid_geometry = deepcopy(obj.solid_geometry)
|
|
geo_obj.tools = deepcopy(obj.tools)
|
|
geo_obj.source_file = deepcopy(obj.source_file)
|
|
|
|
del obj
|
|
|
|
if not geo_obj.solid_geometry:
|
|
app_obj.inform.emit('[ERROR_NOTCL] %s' %
|
|
_("Object is not HPGL2 file or empty. Aborting object creation."))
|
|
return "fail"
|
|
|
|
self.log.debug("open_hpgl2()")
|
|
|
|
with self.app.proc_container.new('%s...' % _("Opening")):
|
|
# Object name
|
|
name = outname or filename.split('/')[-1].split('\\')[-1]
|
|
|
|
# # ## Object creation # ##
|
|
ret = self.app.app_obj.new_object("geometry", name, obj_init, autoselected=False)
|
|
if ret == 'fail':
|
|
self.inform.emit('[ERROR_NOTCL]%s' % _('Failed. Probable not a HPGL2 file.'))
|
|
return 'fail'
|
|
|
|
# Register recent file
|
|
self.app.file_opened.emit("geometry", filename)
|
|
|
|
# appGUI feedback
|
|
self.inform.emit('[success] %s: %s' % (_("Opened"), filename))
|
|
|
|
def open_script(self, filename, outname=None, silent=False):
|
|
"""
|
|
Opens a Script file, parses it and creates a new object for
|
|
it in the program. Thread-safe.
|
|
|
|
:param outname: Name of the resulting object. None causes the name to be that of the file.
|
|
:param filename: Script file filename
|
|
:param silent: If True there will be no messages printed to StatusBar
|
|
:return: None
|
|
"""
|
|
|
|
def obj_init(script_obj, app_obj):
|
|
|
|
assert isinstance(script_obj, ScriptObject), \
|
|
"Expected to initialize a ScriptObject but got %s" % type(script_obj)
|
|
|
|
if silent is False:
|
|
app_obj.inform.emit('[success] %s' % _("TCL script file opened in Code Editor."))
|
|
|
|
try:
|
|
script_obj.parse_file(filename)
|
|
except IOError:
|
|
app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open file"), filename))
|
|
return "fail"
|
|
except ParseError as parse_err:
|
|
app_obj.inform.emit('[ERROR_NOTCL] %s: %s. %s' % (_("Failed to parse file"), filename, str(parse_err)))
|
|
app_obj.log.error(str(parse_err))
|
|
return "fail"
|
|
except Exception as e:
|
|
app_obj.log.error("App.open_script() -> %s" % str(e))
|
|
msg = '[ERROR] %s' % _("An internal error has occurred. See shell.\n")
|
|
msg += traceback.format_exc()
|
|
app_obj.inform.emit(msg)
|
|
return "fail"
|
|
|
|
self.log.debug("open_script()")
|
|
if not os.path.exists(filename):
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("File no longer available."))
|
|
return
|
|
|
|
with self.app.proc_container.new('%s...' % _("Opening")):
|
|
|
|
# Object name
|
|
script_name = outname or filename.split('/')[-1].split('\\')[-1]
|
|
|
|
# Object creation
|
|
ret_val = self.app.app_obj.new_object("script", script_name, obj_init, autoselected=False, plot=False)
|
|
if ret_val == 'fail':
|
|
filename = self.options['global_tcl_path'] + '/' + script_name
|
|
ret_val = self.app.app_obj.new_object("script", script_name, obj_init, autoselected=False, plot=False)
|
|
if ret_val == 'fail':
|
|
self.inform.emit('[ERROR_NOTCL]%s' % _('Failed to open TCL Script.'))
|
|
return 'fail'
|
|
|
|
# Register recent file
|
|
self.app.file_opened.emit("script", filename)
|
|
|
|
# appGUI feedback
|
|
self.inform.emit('[success] %s: %s' % (_("Opened"), filename))
|
|
|
|
def open_config_file(self, filename, run_from_arg=None):
|
|
"""
|
|
Loads a config file from the specified file.
|
|
|
|
:param filename: Name of the file from which to load.
|
|
:param run_from_arg: if True the FlatConfig file will be open as an command line argument
|
|
:return: None
|
|
"""
|
|
self.log.debug("Opening config file: " + filename)
|
|
|
|
if run_from_arg:
|
|
self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
|
|
"Canvas initialization finished in"),
|
|
'%.2f' % self.app.used_time,
|
|
_("Opening FlatCAM Config file.")),
|
|
alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
|
|
color=QtGui.QColor("lightgray"))
|
|
# # add the tab if it was closed
|
|
# self.ui.plot_tab_area.addTab(self.ui.text_editor_tab, _("Code Editor"))
|
|
# # first clear previous text in text editor (if any)
|
|
# self.ui.text_editor_tab.code_editor.clear()
|
|
#
|
|
# # Switch plot_area to CNCJob tab
|
|
# self.ui.plot_tab_area.setCurrentWidget(self.ui.text_editor_tab)
|
|
|
|
# close the Code editor if already open
|
|
if self.app.toggle_codeeditor:
|
|
self.app.on_toggle_code_editor()
|
|
|
|
self.app.on_toggle_code_editor()
|
|
|
|
try:
|
|
if filename:
|
|
f = QtCore.QFile(filename)
|
|
if f.open(QtCore.QIODevice.OpenModeFlag.ReadOnly):
|
|
stream = QtCore.QTextStream(f)
|
|
code_edited = stream.readAll()
|
|
self.app.text_editor_tab.load_text(code_edited, clear_text=True, move_to_start=True)
|
|
f.close()
|
|
except IOError:
|
|
self.log.error("Failed to open config file: %s" % filename)
|
|
self.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed."), filename))
|
|
return
|
|
|
|
def open_project(self, filename, run_from_arg=False, plot=True, cli=False, from_tcl=False):
|
|
"""
|
|
Loads a project from the specified file.
|
|
|
|
1) Loads and parses file
|
|
2) Registers the file as recently opened.
|
|
3) Calls on_file_new_project()
|
|
4) Updates options
|
|
5) Calls app_obj.new_object() with the object's from_dict() as init method.
|
|
6) Calls plot_all() if plot=True
|
|
|
|
:param filename: Name of the file from which to load.
|
|
:param run_from_arg: True if run for arguments
|
|
:param plot: If True plot all objects in the project
|
|
:param cli: Run from command line
|
|
:param from_tcl: True if run from Tcl Sehll
|
|
:return: None
|
|
"""
|
|
|
|
project_filename = filename
|
|
|
|
self.log.debug("Opening project: " + project_filename)
|
|
if not os.path.exists(project_filename):
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("File no longer available."))
|
|
return
|
|
|
|
# block autosaving while a project is loaded
|
|
self.app.block_autosave = True
|
|
|
|
# for some reason, setting ui_title does not work when this method is called from Tcl Shell
|
|
# it's because the TclCommand is run in another thread (it inherit TclCommandSignaled)
|
|
if cli is None:
|
|
self.app.ui.set_ui_title(name=_("Loading Project ... Please Wait ..."))
|
|
|
|
if run_from_arg:
|
|
self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
|
|
"Canvas initialization finished in"),
|
|
'%.2f' % self.app.used_time,
|
|
_("Opening FlatCAM Project file.")),
|
|
alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignLeft,
|
|
color=QtGui.QColor("lightgray"))
|
|
|
|
def parse_worker(prj_filename):
|
|
with self.app.proc_container.new('%s' % _("Parsing...")):
|
|
# Open and parse an uncompressed Project file
|
|
try:
|
|
f = open(prj_filename, 'r')
|
|
except IOError:
|
|
if from_tcl:
|
|
name = prj_filename.split('/')[-1].split('\\')[-1]
|
|
prj_filename = os.path.join(self.options['global_tcl_path'], name)
|
|
try:
|
|
f = open(prj_filename, 'r')
|
|
except IOError:
|
|
self.log.error("Failed to open project file: %s" % prj_filename)
|
|
self.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open project file"), prj_filename))
|
|
return
|
|
else:
|
|
self.log.error("Failed to open project file: %s" % prj_filename)
|
|
self.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open project file"), prj_filename))
|
|
return
|
|
|
|
try:
|
|
d = json.load(f, object_hook=dict2obj)
|
|
except Exception as e:
|
|
self.log.debug(
|
|
"Failed to parse project file, trying to see if it loads as an LZMA archive: %s because %s" %
|
|
(prj_filename, str(e)))
|
|
f.close()
|
|
|
|
# Open and parse a compressed Project file
|
|
try:
|
|
with lzma.open(prj_filename) as f:
|
|
file_content = f.read().decode('utf-8')
|
|
d = json.loads(file_content, object_hook=dict2obj)
|
|
except Exception as e:
|
|
self.log.error("Failed to open project file: %s with error: %s" % (prj_filename, str(e)))
|
|
self.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Failed to open project file"), prj_filename))
|
|
return
|
|
|
|
# Check for older projects
|
|
found_older_project = False
|
|
for obj in d['objs']:
|
|
if 'cnc_tools' in obj or 'exc_cnc_tools' in obj or 'apertures' in obj:
|
|
self.app.log.error(
|
|
'AppIO.open_project() --> %s %s. %s' %
|
|
("Failed to open the CNCJob file:", str(obj['options']['name']),
|
|
"Maybe it is an old project."))
|
|
found_older_project = True
|
|
|
|
if found_older_project:
|
|
if not run_from_arg or not cli or from_tcl is False:
|
|
msgbox = FCMessageBox(parent=self.app.ui)
|
|
title = _("Legacy Project")
|
|
txt = _("The project was made with an older app version.\n"
|
|
"It may not load correctly.\n\n"
|
|
"Do you want to continue?")
|
|
msgbox.setWindowTitle(title) # taskbar still shows it
|
|
msgbox.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/app128.png'))
|
|
msgbox.setText('<b>%s</b>' % title)
|
|
msgbox.setInformativeText(txt)
|
|
msgbox.setIcon(QtWidgets.QMessageBox.Icon.Question)
|
|
|
|
bt_ok = msgbox.addButton(_('Ok'), QtWidgets.QMessageBox.ButtonRole.AcceptRole)
|
|
bt_cancel = msgbox.addButton(_('Cancel'), QtWidgets.QMessageBox.ButtonRole.RejectRole)
|
|
|
|
msgbox.setDefaultButton(bt_ok)
|
|
msgbox.exec()
|
|
response = msgbox.clickedButton()
|
|
|
|
if response == bt_cancel:
|
|
return
|
|
else:
|
|
self.app.log.error("Legacy Project. Loading not supported.")
|
|
return
|
|
|
|
self.app.restore_project.emit(d, prj_filename, run_from_arg, from_tcl, cli, plot)
|
|
|
|
self.app.worker_task.emit({'fcn': parse_worker, 'params': [project_filename]})
|
|
|
|
def restore_project_handler(self, proj_dict, filename, run_from_arg, from_tcl, cli, plot):
|
|
# Clear the current project
|
|
# # NOT THREAD SAFE # ##
|
|
if run_from_arg is True:
|
|
pass
|
|
elif cli is True:
|
|
self.app.delete_selection_shape()
|
|
else:
|
|
self.on_file_new_project()
|
|
|
|
if not run_from_arg or not cli or from_tcl is False:
|
|
msgbox = FCMessageBox(parent=self.app.ui)
|
|
title = _("Import Settings")
|
|
txt = _("Do you want to import the loaded project settings?")
|
|
msgbox.setWindowTitle(title) # taskbar still shows it
|
|
msgbox.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/app128.png'))
|
|
msgbox.setText('<b>%s</b>' % title)
|
|
msgbox.setInformativeText(txt)
|
|
msgbox.setIconPixmap(QtGui.QPixmap(self.app.resource_location + '/import.png'))
|
|
|
|
bt_yes = msgbox.addButton(_('Yes'), QtWidgets.QMessageBox.ButtonRole.YesRole)
|
|
bt_no = msgbox.addButton(_('No'), QtWidgets.QMessageBox.ButtonRole.NoRole)
|
|
# bt_cancel = msgbox.addButton(_('Cancel'), QtWidgets.QMessageBox.ButtonRole.RejectRole)
|
|
|
|
msgbox.setDefaultButton(bt_yes)
|
|
msgbox.exec()
|
|
response = msgbox.clickedButton()
|
|
|
|
if response == bt_yes:
|
|
# self.app.defaults.update(self.app.options)
|
|
# self.app.preferencesUiManager.save_defaults()
|
|
# Project options
|
|
self.app.options.update(proj_dict['options'])
|
|
if response == bt_no:
|
|
pass
|
|
else:
|
|
# Load by default new options when not using GUI
|
|
# Project options
|
|
self.app.options.update(proj_dict['options'])
|
|
|
|
self.app.project_filename = filename
|
|
|
|
# for some reason, setting ui_title does not work when this method is called from Tcl Shell
|
|
# it's because the TclCommand is run in another thread (it inherits TclCommandSignaled)
|
|
|
|
self.app.restore_project_objects_sig.emit(proj_dict, filename, cli, plot)
|
|
|
|
def restore_project_objects(self, proj_dict, filename, cli, plot):
|
|
|
|
def worker_task():
|
|
with self.app.proc_container.new('%s' % _("Loading...")):
|
|
# Re create objects
|
|
self.log.debug(" **************** Started PROEJCT loading... **************** ")
|
|
for obj in proj_dict['objs']:
|
|
try:
|
|
msg = "Recreating from opened project an %s object: %s" % \
|
|
(obj['kind'].capitalize(), obj['obj_options']['name'])
|
|
except KeyError:
|
|
# allowance for older projects
|
|
msg = "Recreating from opened project an %s object: %s" % \
|
|
(obj['kind'].capitalize(), obj['options']['name'])
|
|
self.app.log.debug(msg)
|
|
|
|
def obj_init(new_obj, app_inst):
|
|
try:
|
|
new_obj.from_dict(obj)
|
|
except Exception as erro:
|
|
app_inst.log.error('AppIO.open_project() --> ' + str(erro))
|
|
return 'fail'
|
|
|
|
# make the 'obj_options' dict a LoudDict
|
|
try:
|
|
new_obj_options = LoudDict()
|
|
new_obj_options.update(new_obj.obj_options)
|
|
new_obj.obj_options = new_obj_options
|
|
except AttributeError:
|
|
new_obj_options = LoudDict()
|
|
new_obj_options.update(new_obj.options)
|
|
new_obj.obj_options = new_obj_options
|
|
except Exception as erro:
|
|
app_inst.log.error('AppIO.open_project() make a LoudDict--> ' + str(erro))
|
|
return 'fail'
|
|
|
|
# #############################################################################################
|
|
# for older projects loading try to convert the 'apertures' or 'cnc_tools' or 'exc_cnc_tools'
|
|
# attributes, if found, to 'tools'
|
|
# #############################################################################################
|
|
# for older loaded projects
|
|
if 'apertures' in obj:
|
|
new_obj.tools = obj['apertures']
|
|
if 'cnc_tools' in obj and obj['cnc_tools']:
|
|
new_obj.tools = obj['cnc_tools']
|
|
# new_obj.used_tools = [int(k) for k in new_obj.tools.keys()]
|
|
# first_key = list(obj['cnc_tools'].keys())[0]
|
|
# used_preprocessor = obj['cnc_tools'][first_key]['data']['ppname_g']
|
|
# new_obj.gc_start = new_obj.doformat(self.app.preprocessors[used_preprocessor].start_code)
|
|
if 'exc_cnc_tools' in obj and obj['exc_cnc_tools']:
|
|
new_obj.tools = obj['exc_cnc_tools']
|
|
# add the used_tools (all of them will be used)
|
|
new_obj.used_tools = [float(k) for k in new_obj.tools.keys()]
|
|
# add a missing key, 'tooldia' used for plotting CNCJob objects
|
|
for td in new_obj.tools:
|
|
new_obj.tools[td]['tooldia'] = float(td)
|
|
# #############################################################################################
|
|
# #############################################################################################
|
|
|
|
# try to make the keys in the tools dictionary to be integers
|
|
# JSON serialization makes them strings
|
|
# not all FlatCAM objects have the 'tools' dictionary attribute
|
|
try:
|
|
new_obj.tools = {
|
|
int(tool): tool_dict for tool, tool_dict in list(new_obj.tools.items())
|
|
}
|
|
except ValueError:
|
|
# for older loaded projects
|
|
new_obj.tools = {
|
|
float(tool): tool_dict for tool, tool_dict in list(new_obj.tools.items())
|
|
}
|
|
except Exception as erro:
|
|
app_inst.log.error('AppIO.open_project() keys to int--> ' + str(erro))
|
|
return 'fail'
|
|
|
|
# #############################################################################################
|
|
# for older loaded projects
|
|
# ony older CNCJob objects hold those
|
|
if 'cnc_tools' in obj:
|
|
new_obj.obj_options['type'] = 'Geometry'
|
|
if 'exc_cnc_tools' in obj:
|
|
new_obj.obj_options['type'] = 'Excellon'
|
|
# #############################################################################################
|
|
|
|
if new_obj.kind == 'cncjob':
|
|
# some attributes are serialized so we need t otake this into consideration in
|
|
# CNCJob.set_ui()
|
|
new_obj.is_loaded_from_project = True
|
|
|
|
# for some reason, setting ui_title does not work when this method is called from Tcl Shell
|
|
# it's because the TclCommand is run in another thread (it inherits TclCommandSignaled)
|
|
try:
|
|
if cli is None:
|
|
self.app.ui.set_ui_title(name="{} {}: {}".format(
|
|
_("Loading Project ... restoring"), obj['kind'].upper(), obj['obj_options']['name']))
|
|
|
|
ret = self.app.app_obj.new_object(obj['kind'], obj['obj_options']['name'], obj_init, plot=plot)
|
|
except KeyError:
|
|
# allowance for older projects
|
|
if cli is None:
|
|
self.app.ui.set_ui_title(name="{} {}: {}".format(
|
|
_("Loading Project ... restoring"), obj['kind'].upper(), obj['options']['name']))
|
|
try:
|
|
ret = self.app.app_obj.new_object(obj['kind'], obj['options']['name'], obj_init, plot=plot)
|
|
except Exception:
|
|
continue
|
|
if ret == 'fail':
|
|
continue
|
|
|
|
self.inform.emit('[success] %s: %s' % (_("Project loaded from"), filename))
|
|
|
|
self.app.should_we_save = False
|
|
self.app.file_opened.emit("project", filename)
|
|
|
|
# restore autosaving after a project was loaded
|
|
self.app.block_autosave = False
|
|
|
|
# for some reason, setting ui_title does not work when this method is called from Tcl Shell
|
|
# it's because the TclCommand is run in another thread (it inherit TclCommandSignaled)
|
|
if cli is None:
|
|
self.app.ui.set_ui_title(name=self.app.project_filename)
|
|
|
|
self.log.debug(" **************** Finished PROJECT loading... **************** ")
|
|
|
|
self.app.worker_task.emit({'fcn': worker_task, 'params': []})
|
|
|
|
def save_project(self, filename, quit_action=False, silent=False, from_tcl=False):
|
|
"""
|
|
Saves the current project to the specified file.
|
|
|
|
:param filename: Name of the file in which to save.
|
|
:type filename: str
|
|
:param quit_action: if the project saving will be followed by an app quit; boolean
|
|
:param silent: if True will not display status messages
|
|
:param from_tcl True is run from Tcl Shell
|
|
:return: None
|
|
"""
|
|
self.log.debug("save_project() -> Saving Project")
|
|
self.app.save_in_progress = True
|
|
|
|
if from_tcl:
|
|
self.log.debug("AppIO.save_project() -> Project saved from TCL command.")
|
|
|
|
with self.app.proc_container.new(_("Saving Project ...")):
|
|
# Capture the latest changes
|
|
# Current object
|
|
try:
|
|
current_object = self.app.collection.get_active()
|
|
if current_object:
|
|
current_object.read_form()
|
|
except Exception as e:
|
|
self.log.error("save_project() --> There was no active object. Skipping read_form. %s" % str(e))
|
|
|
|
app_options = {k: v for k, v in self.app.options.items()}
|
|
d = {
|
|
"objs": [obj.to_dict() for obj in self.app.collection.get_list()],
|
|
"options": app_options,
|
|
"version": self.app.version
|
|
}
|
|
|
|
if self.options["global_save_compressed"] is True:
|
|
try:
|
|
project_as_json = json.dumps(d, default=to_dict, indent=2, sort_keys=True).encode('utf-8')
|
|
except Exception as e:
|
|
self.log.error(
|
|
"Failed to serialize file before compression: %s because: %s" % (str(filename), str(e)))
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
|
|
return
|
|
|
|
try:
|
|
# with lzma.open(filename, "w", preset=int(self.options['global_compression_level'])) as f:
|
|
# # # Write
|
|
# f.write(project_as_json)
|
|
|
|
compressor_obj = lzma.LZMACompressor(preset=int(self.options['global_compression_level']))
|
|
out1 = compressor_obj.compress(project_as_json)
|
|
out2 = compressor_obj.flush()
|
|
project_zipped = b"".join([out1, out2])
|
|
except Exception as errrr:
|
|
self.log.error("Failed to save compressed file: %s because: %s" % (str(filename), str(errrr)))
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
|
|
return
|
|
|
|
if project_zipped != b'':
|
|
with open(filename, "wb") as f_to_write:
|
|
f_to_write.write(project_zipped)
|
|
|
|
self.inform.emit('[success] %s: %s' % (_("Project saved to"), str(filename)))
|
|
else:
|
|
self.log.error("Failed to save file: %s. Empty binary file.", str(filename))
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
|
|
return
|
|
else:
|
|
# Open file
|
|
try:
|
|
f = open(filename, 'w')
|
|
except IOError:
|
|
self.log.error("Failed to open file for saving: %s", str(filename))
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("The object is used by another application."))
|
|
return
|
|
|
|
# Write
|
|
try:
|
|
json.dump(d, f, default=to_dict, indent=2, sort_keys=True)
|
|
except Exception as e:
|
|
self.log.error(
|
|
"Failed to serialize file: %s because: %s" % (str(filename), str(e)))
|
|
self.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
|
|
return
|
|
f.close()
|
|
|
|
# verification of the saved project
|
|
# Open and parse
|
|
try:
|
|
saved_f = open(filename, 'r')
|
|
except IOError:
|
|
if silent is False:
|
|
self.inform.emit('[ERROR_NOTCL] %s: %s %s' %
|
|
(_("Failed to verify project file"), str(filename), _("Retry to save it.")))
|
|
return
|
|
|
|
try:
|
|
saved_d = json.load(saved_f, object_hook=dict2obj)
|
|
if not saved_d:
|
|
self.inform.emit('[ERROR_NOTCL] %s: %s %s' %
|
|
(_("Failed to parse saved project file"),
|
|
str(filename),
|
|
_("Retry to save it."))) # noqa
|
|
f.close()
|
|
return
|
|
except Exception:
|
|
if silent is False:
|
|
self.inform.emit('[ERROR_NOTCL] %s: %s %s' %
|
|
(_("Failed to parse saved project file"),
|
|
str(filename),
|
|
_("Retry to save it."))) # noqa
|
|
f.close()
|
|
return
|
|
|
|
saved_f.close()
|
|
|
|
if silent is False:
|
|
if 'version' in saved_d:
|
|
self.inform.emit('[success] %s: %s' % (_("Project saved to"), str(filename)))
|
|
else:
|
|
self.inform.emit('[ERROR_NOTCL] %s: %s %s' %
|
|
(_("Failed to parse saved project file"),
|
|
str(filename),
|
|
_("Retry to save it."))) # noqa
|
|
|
|
# if quit:
|
|
# t = threading.Thread(target=lambda: self.check_project_file_size(1, filename=filename))
|
|
# t.start()
|
|
self.app.start_delayed_quit(delay=500, filename=filename, should_quit=quit_action)
|
|
|
|
def save_source_file(self, obj_name, filename):
|
|
"""
|
|
Exports a FlatCAM Object to an Gerber/Excellon file.
|
|
|
|
:param obj_name: the name of the FlatCAM object for which to save it's embedded source file
|
|
:param filename: Path to the Gerber file to save to.
|
|
:return:
|
|
"""
|
|
|
|
if filename is None:
|
|
filename = self.app.options["global_last_save_folder"] if \
|
|
self.app.options["global_last_save_folder"] is not None else self.app.options["global_last_folder"]
|
|
|
|
self.log.debug("save_source_file()")
|
|
|
|
obj = self.app.collection.get_by_name(obj_name)
|
|
|
|
file_string = StringIO(obj.source_file)
|
|
time_string = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
|
|
|
|
if file_string.getvalue() == '':
|
|
msg = _("Save cancelled because source file is empty. Try to export the file.")
|
|
self.inform.emit('[ERROR_NOTCL] %s' % msg) # noqa
|
|
return 'fail'
|
|
|
|
try:
|
|
with open(filename, 'w') as file:
|
|
file.writelines('G04*\n')
|
|
file.writelines('G04 %s (RE)GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s*\n' %
|
|
(obj.kind.upper(), str(self.app.version), str(self.app.version_date)))
|
|
file.writelines('G04 Filename: %s*\n' % str(obj_name))
|
|
file.writelines('G04 Created on : %s*\n' % time_string)
|
|
|
|
for line in file_string:
|
|
file.writelines(line)
|
|
except PermissionError:
|
|
self.inform.emit('[WARNING] %s' %
|
|
_("Permission denied, saving not possible.\n"
|
|
"Most likely another app is holding the file open and not accessible.")) # noqa
|
|
return 'fail'
|
|
|
|
def on_file_savedefaults(self):
|
|
"""
|
|
Callback for menu item File->Save Defaults. Saves application default options
|
|
``self.options`` to current_defaults.FlatConfig.
|
|
|
|
:return: None
|
|
"""
|
|
self.app.defaults.update(self.app.options)
|
|
self.app.preferencesUiManager.save_defaults()
|