Merged in test_beta_8.910 (pull request #134)

Test beta 8.910
This commit is contained in:
Marius Stanciu 2019-02-21 21:55:41 +00:00
commit c7a2b69637
35 changed files with 4743 additions and 548 deletions

View File

@ -1,7 +1,8 @@
import sys
import sys, os
from PyQt5 import sip
from PyQt5 import QtGui, QtCore, QtWidgets
from PyQt5.QtCore import QSettings, Qt
from FlatCAMApp import App
from multiprocessing import freeze_support
import VisPyPatches
@ -31,7 +32,31 @@ if __name__ == '__main__':
debug_trace()
VisPyPatches.apply_patches()
# apply High DPI support
settings = QSettings("Open Source", "FlatCAM")
if settings.contains("hdpi"):
hdpi_support = settings.value('hdpi', type=int)
else:
hdpi_support = 0
if hdpi_support == 2:
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
else:
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "0"
app = QtWidgets.QApplication(sys.argv)
# apply style
settings = QSettings("Open Source", "FlatCAM")
if settings.contains("style"):
style = settings.value('style', type=str)
app.setStyle(style)
if hdpi_support == 2:
app.setAttribute(Qt.AA_EnableHighDpiScaling, True)
else:
app.setAttribute(Qt.AA_EnableHighDpiScaling, False)
fc = App()
sys.exit(app.exec_())

View File

@ -14,6 +14,7 @@ import os
import random
import logging
import simplejson as json
import lzma
import re
import os
@ -92,8 +93,8 @@ class App(QtCore.QObject):
log.addHandler(handler)
# Version
version = 8.909
version_date = "2019/02/16"
version = 8.910
version_date = "2019/02/23"
beta = True
# current date now
@ -311,6 +312,9 @@ class App(QtCore.QObject):
"global_send_stats": self.general_defaults_form.general_app_group.send_stats_cb,
"global_project_at_startup": self.general_defaults_form.general_app_group.project_startup_cb,
"global_project_autohide": self.general_defaults_form.general_app_group.project_autohide_cb,
"global_app_level": self.general_defaults_form.general_app_group.app_level_radio,
"global_compression_level": self.general_defaults_form.general_app_group.compress_combo,
"global_save_compressed": self.general_defaults_form.general_app_group.save_type_cb,
"global_gridx": self.general_defaults_form.general_gui_group.gridx_entry,
"global_gridy": self.general_defaults_form.general_gui_group.gridy_entry,
@ -457,6 +461,7 @@ class App(QtCore.QObject):
"tools_panelize_constrain": self.tools_defaults_form.tools_panelize_group.pconstrain_cb,
"tools_panelize_constrainx": self.tools_defaults_form.tools_panelize_group.px_width_entry,
"tools_panelize_constrainy": self.tools_defaults_form.tools_panelize_group.py_height_entry,
"tools_panelize_panel_type": self.tools_defaults_form.tools_panelize_group.panel_type_radio,
"tools_calc_vshape_tip_dia": self.tools_defaults_form.tools_calculators_group.tip_dia_entry,
"tools_calc_vshape_tip_angle": self.tools_defaults_form.tools_calculators_group.tip_angle_entry,
@ -464,16 +469,56 @@ class App(QtCore.QObject):
"tools_calc_electro_length": self.tools_defaults_form.tools_calculators_group.pcblength_entry,
"tools_calc_electro_width": self.tools_defaults_form.tools_calculators_group.pcbwidth_entry,
"tools_calc_electro_cdensity": self.tools_defaults_form.tools_calculators_group.cdensity_entry,
"tools_calc_electro_growth": self.tools_defaults_form.tools_calculators_group.growth_entry
"tools_calc_electro_growth": self.tools_defaults_form.tools_calculators_group.growth_entry,
"tools_transform_rotate": self.tools_defaults_form.tools_transform_group.rotate_entry,
"tools_transform_skew_x": self.tools_defaults_form.tools_transform_group.skewx_entry,
"tools_transform_skew_y": self.tools_defaults_form.tools_transform_group.skewy_entry,
"tools_transform_scale_x": self.tools_defaults_form.tools_transform_group.scalex_entry,
"tools_transform_scale_y": self.tools_defaults_form.tools_transform_group.scaley_entry,
"tools_transform_scale_link": self.tools_defaults_form.tools_transform_group.link_cb,
"tools_transform_scale_reference": self.tools_defaults_form.tools_transform_group.reference_cb,
"tools_transform_offset_x": self.tools_defaults_form.tools_transform_group.offx_entry,
"tools_transform_offset_y": self.tools_defaults_form.tools_transform_group.offy_entry,
"tools_transform_mirror_reference": self.tools_defaults_form.tools_transform_group.mirror_reference_cb,
"tools_transform_mirror_point": self.tools_defaults_form.tools_transform_group.flip_ref_entry,
"tools_solderpaste_tools": self.tools_defaults_form.tools_solderpaste_group.nozzle_tool_dia_entry,
"tools_solderpaste_new": self.tools_defaults_form.tools_solderpaste_group.addtool_entry,
"tools_solderpaste_z_start": self.tools_defaults_form.tools_solderpaste_group.z_start_entry,
"tools_solderpaste_z_dispense": self.tools_defaults_form.tools_solderpaste_group.z_dispense_entry,
"tools_solderpaste_z_stop": self.tools_defaults_form.tools_solderpaste_group.z_stop_entry,
"tools_solderpaste_z_travel": self.tools_defaults_form.tools_solderpaste_group.z_travel_entry,
"tools_solderpaste_z_toolchange": self.tools_defaults_form.tools_solderpaste_group.z_toolchange_entry,
"tools_solderpaste_xy_toolchange": self.tools_defaults_form.tools_solderpaste_group.xy_toolchange_entry,
"tools_solderpaste_frxy": self.tools_defaults_form.tools_solderpaste_group.frxy_entry,
"tools_solderpaste_frz": self.tools_defaults_form.tools_solderpaste_group.frz_entry,
"tools_solderpaste_frz_dispense": self.tools_defaults_form.tools_solderpaste_group.frz_dispense_entry,
"tools_solderpaste_speedfwd": self.tools_defaults_form.tools_solderpaste_group.speedfwd_entry,
"tools_solderpaste_dwellfwd": self.tools_defaults_form.tools_solderpaste_group.dwellfwd_entry,
"tools_solderpaste_speedrev": self.tools_defaults_form.tools_solderpaste_group.speedrev_entry,
"tools_solderpaste_dwellrev": self.tools_defaults_form.tools_solderpaste_group.dwellrev_entry,
"tools_solderpaste_pp": self.tools_defaults_form.tools_solderpaste_group.pp_combo
}
# loads postprocessors
#############################
#### LOAD POSTPROCESSORS ####
#############################
self.postprocessors = load_postprocessors(self)
for name in list(self.postprocessors.keys()):
# 'Paste' postprocessors are to be used only in the Solder Paste Dispensing Tool
if name.partition('_')[0] == 'Paste':
self.tools_defaults_form.tools_solderpaste_group.pp_combo.addItem(name)
continue
self.geometry_defaults_form.geometry_opt_group.pp_geometry_name_cb.addItem(name)
# HPGL postprocessor is only for Geometry objects therefore it should not be in the Excellon Preferences
if name == 'hpgl':
continue
self.excellon_defaults_form.excellon_opt_group.pp_excellon_name_cb.addItem(name)
self.defaults = LoudDict()
@ -486,6 +531,7 @@ class App(QtCore.QObject):
"global_send_stats": True,
"global_project_at_startup": False,
"global_project_autohide": True,
"global_app_level": 'b',
"global_gridx": 1.0,
"global_gridy": 1.0,
@ -531,6 +577,9 @@ class App(QtCore.QObject):
"global_shell_shape": [500, 300], # Shape of the shell in pixels.
"global_shell_at_startup": False, # Show the shell at startup.
"global_recent_limit": 10, # Max. items in recent list.
"global_compression_level": 3,
"global_save_compressed": True,
"fit_key": 'V',
"zoom_out_key": '-',
"zoom_in_key": '=',
@ -666,6 +715,7 @@ class App(QtCore.QObject):
"tools_panelize_constrain": False,
"tools_panelize_constrainx": 0.0,
"tools_panelize_constrainy": 0.0,
"tools_panelize_panel_type": 'gerber',
"tools_calc_vshape_tip_dia": 0.007874,
"tools_calc_vshape_tip_angle": 30,
@ -673,7 +723,36 @@ class App(QtCore.QObject):
"tools_calc_electro_length": 10.0,
"tools_calc_electro_width": 10.0,
"tools_calc_electro_cdensity":13.0,
"tools_calc_electro_growth": 10.0
"tools_calc_electro_growth": 10.0,
"tools_transform_rotate": 90,
"tools_transform_skew_x": 0.0,
"tools_transform_skew_y": 0.0,
"tools_transform_scale_x": 1.0,
"tools_transform_scale_y": 1.0,
"tools_transform_scale_link": True,
"tools_transform_scale_reference": True,
"tools_transform_offset_x": 0.0,
"tools_transform_offset_y": 0.0,
"tools_transform_mirror_reference": False,
"tools_transform_mirror_point": (0, 0),
"tools_solderpaste_tools": "1.0, 0.3",
"tools_solderpaste_new": 0.3,
"tools_solderpaste_z_start": 0.005,
"tools_solderpaste_z_dispense": 0.01,
"tools_solderpaste_z_stop": 0.005,
"tools_solderpaste_z_travel": 0.1,
"tools_solderpaste_z_toolchange": 1.0,
"tools_solderpaste_xy_toolchange": "0.0, 0.0",
"tools_solderpaste_frxy": 3.0,
"tools_solderpaste_frz": 3.0,
"tools_solderpaste_frz_dispense": 1.0,
"tools_solderpaste_speedfwd": 20,
"tools_solderpaste_dwellfwd": 1,
"tools_solderpaste_speedrev": 10,
"tools_solderpaste_dwellrev": 1,
"tools_solderpaste_pp": 'Paste_1'
})
###############################
@ -1069,7 +1148,6 @@ class App(QtCore.QObject):
self.ui.menufilenewexc.triggered.connect(self.new_excellon_object)
self.ui.menufileopengerber.triggered.connect(self.on_fileopengerber)
self.ui.menufileopengerber_follow.triggered.connect(self.on_fileopengerber_follow)
self.ui.menufileopenexcellon.triggered.connect(self.on_fileopenexcellon)
self.ui.menufileopengcode.triggered.connect(self.on_fileopengcode)
self.ui.menufileopenproject.triggered.connect(self.on_file_openproject)
@ -1129,7 +1207,7 @@ class App(QtCore.QObject):
self.ui.menuoptions_transform_flipx.triggered.connect(self.on_flipx)
self.ui.menuoptions_transform_flipy.triggered.connect(self.on_flipy)
self.ui.menuoptions_view_source.triggered.connect(self.on_view_source)
self.ui.menuviewdisableall.triggered.connect(self.disable_all_plots)
self.ui.menuviewdisableother.triggered.connect(self.disable_other_plots)
@ -1156,6 +1234,8 @@ class App(QtCore.QObject):
self.ui.menuprojectenable.triggered.connect(lambda: self.enable_plots(self.collection.get_selected()))
self.ui.menuprojectdisable.triggered.connect(lambda: self.disable_plots(self.collection.get_selected()))
self.ui.menuprojectgeneratecnc.triggered.connect(lambda: self.generate_cnc_job(self.collection.get_selected()))
self.ui.menuprojectviewsource.triggered.connect(self.on_view_source)
self.ui.menuprojectcopy.triggered.connect(self.on_copy_object)
self.ui.menuprojectedit.triggered.connect(self.object2editor)
@ -1262,7 +1342,7 @@ class App(QtCore.QObject):
self.general_defaults_form.general_gui_group.wk_cb.currentIndexChanged.connect(self.on_workspace_modified)
self.general_defaults_form.general_gui_group.workspace_cb.stateChanged.connect(self.on_workspace)
self.general_defaults_form.general_gui_group.layout_combo.activated.connect(self.on_layout)
self.general_defaults_form.general_gui_set_group.layout_combo.activated.connect(self.on_layout)
# Modify G-CODE Plot Area TAB
self.ui.code_editor.textChanged.connect(self.handleTextChanged)
@ -1468,7 +1548,7 @@ class App(QtCore.QObject):
if not factory_defaults:
self.save_factory_defaults(silent=False)
# ONLY AT FIRST STARTUP INIT THE GUI LAYOUT TO 'COMPACT'
self.on_layout(layout='compact')
self.on_layout(index=None, lay='compact')
factory_file.close()
# and then make the factory_defaults.FlatConfig file read_only os it can't be modified after creation.
@ -1544,13 +1624,16 @@ class App(QtCore.QObject):
self.panelize_tool.install(icon=QtGui.QIcon('share/panel16.png'))
self.film_tool = Film(self)
self.film_tool.install(icon=QtGui.QIcon('share/film16.png'), separator=True)
self.film_tool.install(icon=QtGui.QIcon('share/film16.png'))
self.paste_tool = SolderPaste(self)
self.paste_tool.install(icon=QtGui.QIcon('share/solderpastebis32.png'), separator=True)
self.move_tool = ToolMove(self)
self.move_tool.install(icon=QtGui.QIcon('share/move16.png'), pos=self.ui.menuedit,
before=self.ui.menueditorigin)
self.cutout_tool = ToolCutOut(self)
self.cutout_tool = CutOut(self)
self.cutout_tool.install(icon=QtGui.QIcon('share/cut16.png'), pos=self.ui.menutool,
before=self.measurement_tool.menuAction)
@ -1634,6 +1717,10 @@ class App(QtCore.QObject):
# store the Geometry Editor Toolbar visibility before entering in the Editor
self.geo_editor.toolbar_old_state = True if self.ui.geo_edit_toolbar.isVisible() else False
self.geo_editor.edit_fcgeometry(edited_object)
# we set the notebook to hidden
self.ui.splitter.setSizes([0, 1])
# set call source to the Editor we go into
self.call_source = 'geo_editor'
@ -1701,6 +1788,10 @@ class App(QtCore.QObject):
self.inform.emit("[WARNING_NOTCL]Select a Geometry or Excellon Object to update.")
return
# if notebook is hidden we show it
if self.ui.splitter.sizes()[0] == 0:
self.ui.splitter.setSizes([1, 1])
# restore the call_source to app
self.call_source = 'app'
@ -3363,13 +3454,12 @@ class App(QtCore.QObject):
self.general_defaults_form.general_gui_group.workspace_cb.setChecked(True)
self.on_workspace()
def on_layout(self, layout=None):
def on_layout(self, index, lay=None):
self.report_usage("on_layout()")
if layout is None:
current_layout= self.general_defaults_form.general_gui_group.layout_combo.get_value().lower()
if lay:
current_layout = lay
else:
current_layout = layout
current_layout = self.general_defaults_form.general_gui_set_group.layout_combo.get_value().lower()
settings = QSettings("Open Source", "FlatCAM")
settings.setValue('layout', current_layout)
@ -3588,34 +3678,46 @@ class App(QtCore.QObject):
# work only if the notebook tab on focus is the Selected_Tab and only if the object is Geometry
if notebook_widget_name == 'selected_tab':
if str(type(self.collection.get_active())) == "<class 'FlatCAMObj.FlatCAMGeometry'>":
tool_add_popup = FCInputDialog(title="New Tool ...",
text='Enter a Tool Diameter:',
min=0.0000, max=99.9999, decimals=4)
tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png'))
# Tool add works for Geometry only if Advanced is True in Preferences
if self.defaults["global_advanced"] is True:
tool_add_popup = FCInputDialog(title="New Tool ...",
text='Enter a Tool Diameter:',
min=0.0000, max=99.9999, decimals=4)
tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png'))
val, ok = tool_add_popup.get_value()
if ok:
if float(val) == 0:
val, ok = tool_add_popup.get_value()
if ok:
if float(val) == 0:
self.inform.emit(
"[WARNING_NOTCL] Please enter a tool diameter with non-zero value, in Float format.")
return
self.collection.get_active().on_tool_add(dia=float(val))
else:
self.inform.emit(
"[WARNING_NOTCL] Please enter a tool diameter with non-zero value, in Float format.")
return
self.collection.get_active().on_tool_add(dia=float(val))
"[WARNING_NOTCL] Adding Tool cancelled ...")
else:
self.inform.emit(
"[WARNING_NOTCL] Adding Tool cancelled ...")
msgbox = QtWidgets.QMessageBox()
msgbox.setText("Adding Tool works only when Advanced is checked.\n"
"Go to Preferences -> General - Show Advanced Options.")
msgbox.setWindowTitle("Tool adding ...")
msgbox.setWindowIcon(QtGui.QIcon('share/warning.png'))
msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
msgbox.setDefaultButton(QtWidgets.QMessageBox.Ok)
msgbox.exec_()
# work only if the notebook tab on focus is the Tools_Tab
if notebook_widget_name == 'tool_tab':
tool_widget = self.ui.tool_scroll_area.widget().objectName()
tool_add_popup = FCInputDialog(title="New Tool ...",
text='Enter a Tool Diameter:',
min=0.0000, max=99.9999, decimals=4)
tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png'))
val, ok = tool_add_popup.get_value()
# and only if the tool is NCC Tool
if tool_widget == self.ncclear_tool.toolName:
tool_add_popup = FCInputDialog(title="New Tool ...",
text='Enter a Tool Diameter:',
min=0.0000, max=99.9999, decimals=4)
tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png'))
val, ok = tool_add_popup.get_value()
if ok:
if float(val) == 0:
self.inform.emit(
@ -3627,12 +3729,6 @@ class App(QtCore.QObject):
"[WARNING_NOTCL] Adding Tool cancelled ...")
# and only if the tool is Paint Area Tool
elif tool_widget == self.paint_tool.toolName:
tool_add_popup = FCInputDialog(title="New Tool ...",
text='Enter a Tool Diameter:',
min=0.0000, max=99.9999, decimals=4)
tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png'))
val, ok = tool_add_popup.get_value()
if ok:
if float(val) == 0:
self.inform.emit(
@ -3642,6 +3738,18 @@ class App(QtCore.QObject):
else:
self.inform.emit(
"[WARNING_NOTCL] Adding Tool cancelled ...")
# and only if the tool is Solder Paste Dispensing Tool
elif tool_widget == self.paste_tool.toolName:
if ok:
if float(val) == 0:
self.inform.emit(
"[WARNING_NOTCL] Please enter a tool diameter with non-zero value, in Float format.")
return
self.paste_tool.on_tool_add(dia=float(val))
else:
self.inform.emit(
"[WARNING_NOTCL] Adding Tool cancelled ...")
# It's meant to delete tools in tool tables via a 'Delete' shortcut key but only if certain conditions are met
# See description bellow.
@ -3665,6 +3773,9 @@ class App(QtCore.QObject):
elif tool_widget == self.paint_tool.toolName:
self.paint_tool.on_tool_delete()
# and only if the tool is Solder Paste Dispensing Tool
elif tool_widget == self.paste_tool.toolName:
self.paste_tool.on_tool_delete()
else:
self.on_delete()
@ -3934,7 +4045,7 @@ class App(QtCore.QObject):
obj.mirror('X', [px, py])
obj.plot()
self.object_changed.emit(obj)
self.inform.emit("[success] Flip on Y axis done.")
except Exception as e:
self.inform.emit("[ERROR_NOTCL] Due of %s, Flip action was not executed." % str(e))
return
@ -3974,7 +4085,7 @@ class App(QtCore.QObject):
obj.mirror('Y', [px, py])
obj.plot()
self.object_changed.emit(obj)
self.inform.emit("[success] Flip on X axis done.")
except Exception as e:
self.inform.emit("[ERROR_NOTCL] Due of %s, Flip action was not executed." % str(e))
return
@ -3993,7 +4104,8 @@ class App(QtCore.QObject):
else:
if silent is False:
rotatebox = FCInputDialog(title="Transform", text="Enter the Angle value:",
min=-360, max=360, decimals=3)
min=-360, max=360, decimals=4,
init_val=float(self.defaults['tools_transform_rotate']))
num, ok = rotatebox.get_value()
else:
num = preset
@ -4018,9 +4130,10 @@ class App(QtCore.QObject):
py = 0.5 * (yminimal + ymaximal)
for sel_obj in obj_list:
sel_obj.rotate(-num, point=(px, py))
sel_obj.rotate(-float(num), point=(px, py))
sel_obj.plot()
self.object_changed.emit(sel_obj)
self.inform.emit("[success] Rotation done.")
except Exception as e:
self.inform.emit("[ERROR_NOTCL] Due of %s, rotation movement was not executed." % str(e))
return
@ -4036,7 +4149,8 @@ class App(QtCore.QObject):
self.inform.emit("[WARNING_NOTCL] No object selected to Skew/Shear on X axis.")
else:
skewxbox = FCInputDialog(title="Transform", text="Enter the Angle value:",
min=-360, max=360, decimals=3)
min=-360, max=360, decimals=4,
init_val=float(self.defaults['tools_transform_skew_x']))
num, ok = skewxbox.get_value()
if ok:
# first get a bounding box to fit all
@ -4053,6 +4167,7 @@ class App(QtCore.QObject):
obj.skew(num, 0, point=(xminimal, yminimal))
obj.plot()
self.object_changed.emit(obj)
self.inform.emit("[success] Skew on X axis done.")
def on_skewy(self):
self.report_usage("on_skewy()")
@ -4065,7 +4180,8 @@ class App(QtCore.QObject):
self.inform.emit("[WARNING_NOTCL] No object selected to Skew/Shear on Y axis.")
else:
skewybox = FCInputDialog(title="Transform", text="Enter the Angle value:",
min=-360, max=360, decimals=3)
min=-360, max=360, decimals=4,
init_val=float(self.defaults['tools_transform_skew_y']))
num, ok = skewybox.get_value()
if ok:
# first get a bounding box to fit all
@ -4082,6 +4198,7 @@ class App(QtCore.QObject):
obj.skew(0, num, point=(xminimal, yminimal))
obj.plot()
self.object_changed.emit(obj)
self.inform.emit("[success] Skew on Y axis done.")
def delete_first_selected(self):
# Keep this for later
@ -4728,10 +4845,46 @@ class App(QtCore.QObject):
self.on_file_exportexcellon()
elif type(obj) == FlatCAMCNCjob:
obj.on_exportgcode_button_click()
elif type(obj) == FlatCAMGerber:
self.on_file_exportgerber()
def on_view_source(self):
try:
obj = self.collection.get_active()
except:
self.inform.emit("[WARNING_NOTCL] Select an Gerber or Excellon file to view it's source.")
# add the tab if it was closed
self.ui.plot_tab_area.addTab(self.ui.cncjob_tab, "Code Editor")
# first clear previous text in text editor (if any)
self.ui.code_editor.clear()
# Switch plot_area to CNCJob tab
self.ui.plot_tab_area.setCurrentWidget(self.ui.cncjob_tab)
# then append the text from GCode to the text editor
file = StringIO(obj.source_file)
try:
for line in file:
proc_line = str(line).strip('\n')
self.ui.code_editor.append(proc_line)
except Exception as e:
log.debug('App.on_view_source() -->%s' % str(e))
self.inform.emit('[ERROR]App.on_view_source() -->%s' % str(e))
return
self.ui.code_editor.moveCursor(QtGui.QTextCursor.Start)
self.handleTextChanged()
self.ui.show()
# if type(obj) == FlatCAMGerber:
# self.on_file_exportdxf()
# elif type(obj) == FlatCAMExcellon:
# self.on_file_exportexcellon()
def obj_move(self):
self.report_usage("obj_move()")
self.move_tool.run()
def on_fileopengerber(self):
@ -4770,42 +4923,6 @@ class App(QtCore.QObject):
self.worker_task.emit({'fcn': self.open_gerber,
'params': [filename]})
def on_fileopengerber_follow(self):
"""
File menu callback for opening a Gerber.
:return: None
"""
self.report_usage("on_fileopengerber_follow")
App.log.debug("on_fileopengerber_follow()")
_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);;" \
"Protel Files (*.gtl *.gbl *.gts *.gbs *.gto *.gbo *.gtp *.gbp *.gml *.gm1 *.gm3 *.gko);;" \
"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 (*.*)"
try:
filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Open Gerber with Follow",
directory=self.get_last_folder(), filter=_filter_)
except TypeError:
filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Open Gerber with Follow", filter=_filter_)
# The Qt methods above will return a QString which can cause problems later.
# So far json.dump() will fail to serialize it.
# TODO: Improve the serialization methods and remove this fix.
filename = str(filename)
follow = True
if filename == "":
self.inform.emit("[WARNING_NOTCL]Open Gerber-Follow cancelled.")
else:
self.worker_task.emit({'fcn': self.open_gerber,
'params': [filename, follow]})
def on_fileopenexcellon(self):
"""
File menu callback for opening an Excellon file.
@ -4975,6 +5092,45 @@ class App(QtCore.QObject):
write_png(filename, data)
self.file_saved.emit("png", filename)
def on_file_exportgerber(self):
"""
Callback for menu item File->Export SVG.
:return: None
"""
self.report_usage("on_file_exportgerber")
App.log.debug("on_file_exportgerber()")
obj = self.collection.get_active()
if obj is None:
self.inform.emit("[WARNING_NOTCL] No object selected. Please Select an Gerber object to export.")
return
# Check for more compatible types and add as required
if not isinstance(obj, FlatCAMGerber):
self.inform.emit("[ERROR_NOTCL] Failed. Only Gerber objects can be saved as Gerber files...")
return
name = self.collection.get_active().options["name"]
filter = "Gerber File (*.GBR);;Gerber File (*.GRB);;All Files (*.*)"
try:
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
caption="Export Gerber",
directory=self.get_last_save_folder() + '/' + name,
filter=filter)
except TypeError:
filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export Gerber", filter=filter)
filename = str(filename)
if filename == "":
self.inform.emit("[WARNING_NOTCL]Export Gerber cancelled.")
return
else:
self.export_gerber(name, filename)
self.file_saved.emit("Gerber", filename)
def on_file_exportexcellon(self):
"""
Callback for menu item File->Export SVG.
@ -5213,15 +5369,15 @@ class App(QtCore.QObject):
except IOError:
exists = False
msg = "Project file exists. Overwrite?"
if exists:
msgbox = QtWidgets.QMessageBox()
msgbox.setInformativeText(msg)
msgbox.setStandardButtons(QtWidgets.QMessageBox.Cancel |QtWidgets.QMessageBox.Ok)
msgbox.setDefaultButton(QtWidgets.QMessageBox.Cancel)
result = msgbox.exec_()
if result ==QtWidgets.QMessageBox.Cancel:
return
# msg = "Project file exists. Overwrite?"
# if exists:
# msgbox = QtWidgets.QMessageBox()
# msgbox.setInformativeText(msg)
# msgbox.setStandardButtons(QtWidgets.QMessageBox.Cancel |QtWidgets.QMessageBox.Ok)
# msgbox.setDefaultButton(QtWidgets.QMessageBox.Cancel)
# result = msgbox.exec_()
# if result ==QtWidgets.QMessageBox.Cancel:
# return
if thread is True:
self.worker_task.emit({'fcn': self.save_project,
@ -5527,9 +5683,38 @@ class App(QtCore.QObject):
else:
make_black_film()
def export_gerber(self, obj_name, filename, use_thread=True):
"""
Exports a Gerber Object to an Gerber file.
:param filename: Path to the Gerber file to save to.
:return:
"""
self.report_usage("export_gerber()")
if filename is None:
filename = self.defaults["global_last_save_folder"]
self.log.debug("export_gerber()")
obj = self.collection.get_by_name(obj_name)
file_string = StringIO(obj.source_file)
time_string = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
with open(filename, 'w') as file:
file.writelines('G04*\n')
file.writelines('G04 GERBER (RE)GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s*\n' %
(str(self.version), str(self.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)
def export_excellon(self, obj_name, filename, use_thread=True):
"""
Exports a Geometry Object to an Excellon file.
Exports a Excellon Object to an Excellon file.
:param filename: Path to the Excellon file to save to.
:return:
@ -5834,7 +6019,7 @@ class App(QtCore.QObject):
self.inform.emit("[success] Opened: " + filename)
self.progress.emit(100)
def open_gerber(self, filename, follow=False, outname=None):
def open_gerber(self, filename, outname=None):
"""
Opens a Gerber file, parses it and creates a new object for
it in the program. Thread-safe.
@ -5858,7 +6043,7 @@ class App(QtCore.QObject):
# Opening the file happens here
self.progress.emit(30)
try:
gerber_obj.parse_file(filename, follow=follow)
gerber_obj.parse_file(filename)
except IOError:
app_obj.inform.emit("[ERROR_NOTCL] Failed to open file: " + filename)
app_obj.progress.emit(0)
@ -5886,10 +6071,7 @@ class App(QtCore.QObject):
# Further parsing
self.progress.emit(70) # TODO: Note the mixture of self and app_obj used here
if follow is False:
App.log.debug("open_gerber()")
else:
App.log.debug("open_gerber() with 'follow' attribute")
App.log.debug("open_gerber()")
with self.proc_container.new("Opening Gerber") as proc:
@ -6063,7 +6245,7 @@ class App(QtCore.QObject):
"""
App.log.debug("Opening project: " + filename)
# Open and parse
# Open and parse an uncompressed Project file
try:
f = open(filename, 'r')
except IOError:
@ -6077,7 +6259,16 @@ class App(QtCore.QObject):
App.log.error("Failed to parse project file: %s" % filename)
self.inform.emit("[ERROR_NOTCL] Failed to parse project file: %s" % filename)
f.close()
return
# Open and parse a compressed Project file
try:
with lzma.open(filename) as f:
file_content = f.read().decode('utf-8')
d = json.loads(file_content, object_hook=dict2obj)
except IOError:
App.log.error("Failed to open project file: %s" % filename)
self.inform.emit("[ERROR_NOTCL] Failed to open project file: %s" % filename)
return
self.file_opened.emit("project", filename)
@ -6101,7 +6292,6 @@ class App(QtCore.QObject):
obj_inst.from_dict(obj)
App.log.debug(obj['kind'] + ": " + obj['options']['name'])
self.new_object(obj['kind'], obj['options']['name'], obj_init, active=False, fit=False, plot=True)
self.plot_all()
self.inform.emit("[success] Project loaded from: " + filename)
@ -6160,6 +6350,12 @@ class App(QtCore.QObject):
self.defaults["global_def_win_w"],
self.defaults["global_def_win_h"])
self.ui.splitter.setSizes([self.defaults["def_notebook_width"], 0])
settings = QSettings("Open Source", "FlatCAM")
if settings.contains("maximized_gui"):
maximized_ui = settings.value('maximized_gui', type=bool)
if maximized_ui is True:
self.ui.showMaximized()
except KeyError:
pass
@ -6174,7 +6370,7 @@ class App(QtCore.QObject):
for obj in self.collection.get_list():
def worker_task(obj):
with self.proc_container.new("Plotting"):
obj.plot()
obj.plot(kind=self.defaults["cncjob_plot_kind"])
if zoom:
self.object_plotted.emit(obj)
@ -6793,37 +6989,45 @@ The normal flow when working in FlatCAM is the following:</span></p>
"options": self.options,
"version": self.version}
# Open file
try:
f = open(filename, 'w')
except IOError:
App.log.error("[ERROR] Failed to open file for saving: %s", filename)
return
# Write
json.dump(d, f, default=to_dict, indent=2, sort_keys=True)
f.close()
# verification of the saved project
# Open and parse
try:
saved_f = open(filename, 'r')
except IOError:
self.inform.emit("[ERROR_NOTCL] Failed to verify project file: %s. Retry to save it." % filename)
return
try:
saved_d = json.load(saved_f, object_hook=dict2obj)
except:
self.inform.emit("[ERROR_NOTCL] Failed to parse saved project file: %s. Retry to save it." % filename)
f.close()
return
saved_f.close()
if 'version' in saved_d:
if self.defaults["global_save_compressed"] is True:
with lzma.open(filename, "w", preset=int(self.defaults['global_compression_level'])) as f:
g = json.dumps(d, default=to_dict, indent=2, sort_keys=True).encode('utf-8')
# # Write
f.write(g)
self.inform.emit("[success] Project saved to: %s" % filename)
else:
self.inform.emit("[ERROR_NOTCL] Failed to save project file: %s. Retry to save it." % filename)
# Open file
try:
f = open(filename, 'w')
except IOError:
App.log.error("[ERROR] Failed to open file for saving: %s", filename)
return
# Write
json.dump(d, f, default=to_dict, indent=2, sort_keys=True)
f.close()
# verification of the saved project
# Open and parse
try:
saved_f = open(filename, 'r')
except IOError:
self.inform.emit("[ERROR_NOTCL] Failed to verify project file: %s. Retry to save it." % filename)
return
try:
saved_d = json.load(saved_f, object_hook=dict2obj)
except:
self.inform.emit(
"[ERROR_NOTCL] Failed to parse saved project file: %s. Retry to save it." % filename)
f.close()
return
saved_f.close()
if 'version' in saved_d:
self.inform.emit("[success] Project saved to: %s" % filename)
else:
self.inform.emit("[ERROR_NOTCL] Failed to save project file: %s. Retry to save it." % filename)
def on_options_app2project(self):
"""

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -106,7 +106,12 @@ class FlatCAMObj(QtCore.QObject):
if attr == 'options':
self.options.update(d[attr])
else:
setattr(self, attr, d[attr])
try:
setattr(self, attr, d[attr])
except KeyError:
log.debug("FlatCAMObj.from_dict() --> KeyError: %s. Means that we are loading an old project that don't"
"have all attributes in the latest FlatCAM." % str(attr))
pass
def on_options_change(self, key):
# Update form on programmatically options change
@ -366,9 +371,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
if grb_final.solid_geometry is None:
grb_final.solid_geometry = []
grb_final.follow_geometry = []
if type(grb_final.solid_geometry) is not list:
grb_final.solid_geometry = [grb_final.solid_geometry]
grb_final.follow_geometry = [grb_final.follow_geometry]
for grb in grb_list:
for option in grb.options:
@ -384,8 +391,10 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
else: # If not list, just append
for geos in grb.solid_geometry:
grb_final.solid_geometry.append(geos)
grb_final.follow_geometry.append(geos)
grb_final.solid_geometry = MultiPolygon(grb_final.solid_geometry)
grb_final.follow_geometry = MultiPolygon(grb_final.follow_geometry)
def __init__(self, name):
Gerber.__init__(self, steps_per_circle=int(self.app.defaults["gerber_circle_steps"]))
@ -413,15 +422,15 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
# type of isolation: 0 = exteriors, 1 = interiors, 2 = complete isolation (both interiors and exteriors)
self.iso_type = 2
# Attributes to be included in serialization
# Always append to it because it carries contents
# from predecessors.
self.ser_attrs += ['options', 'kind']
self.multigeo = False
self.follow = False
self.apertures_row = 0
# store the source file here
self.source_file = ""
# assert isinstance(self.ui, GerberObjectUI)
# self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
# self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
@ -431,6 +440,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
# self.ui.generate_bb_button.clicked.connect(self.on_generatebb_button_click)
# self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click)
# Attributes to be included in serialization
# Always append to it because it carries contents
# from predecessors.
self.ser_attrs += ['options', 'kind']
def set_ui(self, ui):
"""
Maps options with GUI inputs.
@ -474,6 +488,20 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
self.ui.generate_bb_button.clicked.connect(self.on_generatebb_button_click)
self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click)
self.ui.aperture_table_visibility_cb.stateChanged.connect(self.on_aperture_table_visibility_change)
self.ui.follow_cb.stateChanged.connect(self.on_follow_cb_click)
# Show/Hide Advanced Options
if self.app.defaults["global_app_level"] == 'b':
self.ui.level.setText('<span style="color:green;"><b>Basic</b></span>')
self.ui.apertures_table_label.hide()
self.ui.aperture_table_visibility_cb.hide()
self.ui.milling_type_label.hide()
self.ui.milling_type_radio.hide()
self.ui.generate_ext_iso_button.hide()
self.ui.generate_int_iso_button.hide()
else:
self.ui.level.setText('<span style="color:red;"><b>Advanced</b></span>')
self.build_ui()
@ -654,7 +682,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
def on_int_iso_button_click(self, *args):
if self.ui.follow_cb.get_value() == True:
if self.ui.follow_cb.get_value() is True:
obj = self.app.collection.get_active()
obj.follow()
# in the end toggle the visibility of the origin object so we can see the generated Geometry
@ -666,9 +694,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
def on_iso_button_click(self, *args):
if self.ui.follow_cb.get_value() == True:
if self.ui.follow_cb.get_value() is True:
obj = self.app.collection.get_active()
obj.follow()
obj.follow_geo()
# in the end toggle the visibility of the origin object so we can see the generated Geometry
obj.ui.plot_cb.toggle()
else:
@ -676,7 +704,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
self.read_form()
self.isolate()
def follow(self, outname=None):
def follow_geo(self, outname=None):
"""
Creates a geometry object "following" the gerber paths.
@ -694,7 +722,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
def follow_init(follow_obj, app):
# Propagate options
follow_obj.options["cnctooldia"] = float(self.options["isotooldia"])
follow_obj.solid_geometry = self.solid_geometry
follow_obj.solid_geometry = self.follow_geometry
# TODO: Do something if this is None. Offer changing name?
try:
@ -703,7 +731,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
return "Operation failed: %s" % str(e)
def isolate(self, iso_type=None, dia=None, passes=None, overlap=None,
outname=None, combine=None, milling_type=None):
outname=None, combine=None, milling_type=None, follow=None):
"""
Creates an isolation routing geometry object in the project.
@ -714,6 +742,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
:param outname: Base name of the output object
:return: None
"""
if dia is None:
dia = float(self.options["isotooldia"])
if passes is None:
@ -734,7 +764,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
base_name = self.options["name"] + "_iso"
base_name = outname or base_name
def generate_envelope(offset, invert, envelope_iso_type=2):
def generate_envelope(offset, invert, envelope_iso_type=2, follow=None):
# isolation_geometry produces an envelope that is going on the left of the geometry
# (the copper features). To leave the least amount of burrs on the features
# the tool needs to travel on the right side of the features (this is called conventional milling)
@ -742,7 +772,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
# the other passes overlap preceding ones and cut the left over copper. It is better for them
# to cut on the right side of the left over copper i.e on the left side of the features.
try:
geom = self.isolation_geometry(offset, iso_type=envelope_iso_type)
geom = self.isolation_geometry(offset, iso_type=envelope_iso_type, follow=follow)
except Exception as e:
log.debug(str(e))
return 'fail'
@ -782,9 +812,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
# if milling type is climb then the move is counter-clockwise around features
if milling_type == 'cl':
# geom = generate_envelope (offset, i == 0)
geom = generate_envelope(iso_offset, 1, envelope_iso_type=self.iso_type)
geom = generate_envelope(iso_offset, 1, envelope_iso_type=self.iso_type, follow=follow)
else:
geom = generate_envelope(iso_offset, 0, envelope_iso_type=self.iso_type)
geom = generate_envelope(iso_offset, 0, envelope_iso_type=self.iso_type, follow=follow)
geo_obj.solid_geometry.append(geom)
# detect if solid_geometry is empty and this require list flattening which is "heavy"
@ -834,9 +864,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
# if milling type is climb then the move is counter-clockwise around features
if milling_type == 'cl':
# geo_obj.solid_geometry = generate_envelope(offset, i == 0)
geo_obj.solid_geometry = generate_envelope(offset, 1, envelope_iso_type=self.iso_type)
geo_obj.solid_geometry = generate_envelope(offset, 1, envelope_iso_type=self.iso_type,
follow=follow)
else:
geo_obj.solid_geometry = generate_envelope(offset, 0, envelope_iso_type=self.iso_type)
geo_obj.solid_geometry = generate_envelope(offset, 0, envelope_iso_type=self.iso_type,
follow=follow)
# detect if solid_geometry is empty and this require list flattening which is "heavy"
# or just looking in the lists (they are one level depth) and if any is not empty
@ -876,6 +908,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
self.read_form_item('multicolored')
self.plot()
def on_follow_cb_click(self):
if self.muted_ui:
return
self.plot()
def on_aperture_table_visibility_change(self):
if self.ui.aperture_table_visibility_cb.isChecked():
self.ui.apertures_table.setVisible(True)
@ -921,7 +958,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
else:
face_color = self.app.defaults['global_plot_fill']
geometry = self.solid_geometry
# if the Follow Geometry checkbox is checked then plot only the follow geometry
if self.ui.follow_cb.get_value():
geometry = self.follow_geometry
else:
geometry = self.solid_geometry
# Make sure geometry is iterable.
try:
@ -941,6 +982,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
self.add_shape(shape=g, color=color,
face_color=random_color() if self.options['multicolored']
else face_color, visible=self.options['plot'])
elif type(g) == Point:
pass
else:
for el in g:
self.add_shape(shape=el, color=color,
@ -951,6 +994,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
if type(g) == Polygon or type(g) == LineString:
self.add_shape(shape=g, color=random_color() if self.options['multicolored'] else 'black',
visible=self.options['plot'])
elif type(g) == Point:
pass
else:
for el in g:
self.add_shape(shape=el, color=random_color() if self.options['multicolored'] else 'black',
@ -1076,11 +1121,6 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
# dict to hold the tool number as key and tool offset as value
self.tool_offset ={}
# Attributes to be included in serialization
# Always append to it because it carries contents
# from predecessors.
self.ser_attrs += ['options', 'kind']
# variable to store the total amount of drills per job
self.tot_drill_cnt = 0
self.tool_row = 0
@ -1092,8 +1132,16 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
# variable to store the distance travelled
self.travel_distance = 0.0
# store the source file here
self.source_file = ""
self.multigeo = False
# Attributes to be included in serialization
# Always append to it because it carries contents
# from predecessors.
self.ser_attrs += ['options', 'kind']
@staticmethod
def merge(exc_list, exc_final):
"""
@ -1522,6 +1570,24 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
dia = float('%.3f' % float(value['C']))
self.tool_offset[dia] = t_default_offset
# Show/Hide Advanced Options
if self.app.defaults["global_app_level"] == 'b':
self.ui.level.setText('<span style="color:green;"><b>Basic</b></span>')
self.ui.tools_table.setColumnHidden(4, True)
self.ui.estartz_label.hide()
self.ui.estartz_entry.hide()
self.ui.eendz_label.hide()
self.ui.eendz_entry.hide()
self.ui.feedrate_rapid_label.hide()
self.ui.feedrate_rapid_entry.hide()
self.ui.pdepth_label.hide()
self.ui.pdepth_entry.hide()
self.ui.feedrate_probe_label.hide()
self.ui.feedrate_probe_entry.hide()
else:
self.ui.level.setText('<span style="color:red;"><b>Advanced</b></span>')
assert isinstance(self.ui, ExcellonObjectUI), \
"Expected a ExcellonObjectUI, got %s" % type(self.ui)
self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
@ -1989,8 +2055,14 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
tools = self.get_selected_tools_list()
if len(tools) == 0:
self.app.inform.emit("[ERROR_NOTCL]Please select one or more tools from the list and try again.")
return
# if there is a single tool in the table (remember that the last 2 rows are for totals and do not count in
# tool number) it means that there are 3 rows (1 tool and 2 totals).
# in this case regardless of the selection status of that tool, use it.
if self.ui.tools_table.rowCount() == 3:
tools.append(self.ui.tools_table.item(0, 0).text())
else:
self.app.inform.emit("[ERROR_NOTCL]Please select one or more tools from the list and try again.")
return
xmin = self.options['xmin']
ymin = self.options['ymin']
@ -2462,8 +2534,14 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
# flag to store if the V-Shape tool is selected in self.ui.geo_tools_table
self.v_tool_type = None
# flag to store if the Geometry is type 'multi-geometry' meaning that each tool has it's own geometry
# the default value is False
self.multigeo = False
# flag to store if the geometry is part of a special group of geometries that can't be processed by the default
# engine of FlatCAM. Most likely are generated by some of tools and are special cases of geometries.
self. special_group = None
# Attributes to be included in serialization
# Always append to it because it carries contents
# from predecessors.
@ -2732,6 +2810,30 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
self.ui.geo_tools_table.addContextMenu(
"Delete", lambda: self.on_tool_delete(all=None), icon=QtGui.QIcon("share/delete32.png"))
# Show/Hide Advanced Options
if self.app.defaults["global_app_level"] == 'b':
self.ui.level.setText('<span style="color:green;"><b>Basic</b></span>')
self.ui.geo_tools_table.setColumnHidden(2, True)
self.ui.geo_tools_table.setColumnHidden(3, True)
self.ui.geo_tools_table.setColumnHidden(4, True)
self.ui.addtool_entry_lbl.hide()
self.ui.addtool_entry.hide()
self.ui.addtool_btn.hide()
self.ui.copytool_btn.hide()
self.ui.deltool_btn.hide()
self.ui.endzlabel.hide()
self.ui.gendz_entry.hide()
self.ui.fr_rapidlabel.hide()
self.ui.cncfeedrate_rapid_entry.hide()
self.ui.extracut_cb.hide()
self.ui.pdepth_label.hide()
self.ui.pdepth_entry.hide()
self.ui.feedrate_probe_label.hide()
self.ui.feedrate_probe_entry.hide()
else:
self.ui.level.setText('<span style="color:red;"><b>Advanced</b></span>')
self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click)
self.ui.paint_tool_button.clicked.connect(self.app.paint_tool.run)
@ -2832,8 +2934,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
self.ui.grid3.itemAt(i).widget().currentIndexChanged.disconnect()
if isinstance(self.ui.grid3.itemAt(i).widget(), LengthEntry) or \
isinstance(self.ui.grid3.itemAt(i), IntEntry) or \
isinstance(self.ui.grid3.itemAt(i), FCEntry):
isinstance(self.ui.grid3.itemAt(i).widget(), IntEntry) or \
isinstance(self.ui.grid3.itemAt(i).widget(), FCEntry):
self.ui.grid3.itemAt(i).widget().editingFinished.disconnect()
except:
pass
@ -3544,6 +3646,17 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
self.app.report_usage("geometry_on_generatecnc_button")
self.read_form()
self.sel_tools = {}
try:
if self.special_group:
self.app.inform.emit("[WARNING_NOTCL]This Geometry can't be processed because it is %s geometry." %
str(self.special_group))
return
except AttributeError:
pass
# test to see if we have tools available in the tool table
if self.ui.geo_tools_table.selectedItems():
for x in self.ui.geo_tools_table.selectedItems():
@ -3565,8 +3678,19 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
tooluid: copy.deepcopy(tooluid_value)
})
self.mtool_gen_cncjob()
self.ui.geo_tools_table.clearSelection()
elif self.ui.geo_tools_table.rowCount() == 1:
tooluid = int(self.ui.geo_tools_table.item(0, 5).text())
for tooluid_key, tooluid_value in self.tools.items():
if int(tooluid_key) == tooluid:
self.sel_tools.update({
tooluid: copy.deepcopy(tooluid_value)
})
self.mtool_gen_cncjob()
self.ui.geo_tools_table.clearSelection()
else:
self.app.inform.emit("[ERROR_NOTCL] Failed. No tool selected in the tool table ...")
@ -3855,6 +3979,16 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
'[ERROR_NOTCL]Wrong value format for self.defaults["feedrate_probe"] '
'or self.options["feedrate_probe"]')
# make sure that trying to make a CNCJob from an empty file is not creating an app crash
if not self.solid_geometry:
a = 0
for tooluid_key in self.tools:
if self.tools[tooluid_key]['solid_geometry'] is None:
a += 1
if a == len(self.tools):
self.app.inform.emit('[ERROR_NOTCL]Cancelled. Empty file, it has no geometry...')
return 'fail'
for tooluid_key in self.sel_tools:
tool_cnt += 1
app_obj.progress.emit(20)
@ -4561,6 +4695,9 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
'''
self.exc_cnc_tools = {}
# flag to store if the CNCJob is part of a special group of CNCJob objects that can't be processed by the
# default engine of FlatCAM. They generated by some of tools and are special cases of CNCJob objects.
self. special_group = None
# for now it show if the plot will be done for multi-tool CNCJob (True) or for single tool
# (like the one in the TCL Command), False
@ -4736,6 +4873,13 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
# set the kind of geometries are plotted by default with plot2() from camlib.CNCJob
self.ui.cncplot_method_combo.set_value(self.app.defaults["cncjob_plot_kind"])
# Show/Hide Advanced Options
if self.app.defaults["global_app_level"] == 'b':
self.ui.level.setText('<span style="color:green;"><b>Basic</b></span>')
else:
self.ui.level.setText('<span style="color:red;"><b>Advanced</b></span>')
self.ui.updateplot_button.clicked.connect(self.on_updateplot_button_click)
self.ui.export_gcode_button.clicked.connect(self.on_exportgcode_button_click)
self.ui.modify_gcode_button.clicked.connect(self.on_modifygcode_button_click)
@ -4803,13 +4947,24 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
preamble = str(self.ui.prepend_text.get_value())
postamble = str(self.ui.append_text.get_value())
self.export_gcode(filename, preamble=preamble, postamble=postamble)
gc = self.export_gcode(filename, preamble=preamble, postamble=postamble)
if gc == 'fail':
return
self.app.file_saved.emit("gcode", filename)
self.app.inform.emit("[success] Machine Code file saved to: %s" % filename)
def on_modifygcode_button_click(self, *args):
preamble = str(self.ui.prepend_text.get_value())
postamble = str(self.ui.append_text.get_value())
gc = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True)
if gc == 'fail':
return
else:
self.app.gcode_edited = gc
# add the tab if it was closed
self.app.ui.plot_tab_area.addTab(self.app.ui.cncjob_tab, "CNC Code Editor")
self.app.ui.plot_tab_area.addTab(self.app.ui.cncjob_tab, "Code Editor")
# delete the absolute and relative position and messages in the infobar
self.app.ui.position_label.setText("")
@ -4818,10 +4973,6 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
# Switch plot_area to CNCJob tab
self.app.ui.plot_tab_area.setCurrentWidget(self.app.ui.cncjob_tab)
preamble = str(self.ui.prepend_text.get_value())
postamble = str(self.ui.append_text.get_value())
self.app.gcode_edited = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True)
# print(self.app.gcode_edited)
# first clear previous text in text editor (if any)
self.app.ui.code_editor.clear()
@ -4935,6 +5086,14 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
roland = False
hpgl = False
try:
if self.special_group:
self.app.inform.emit("[WARNING_NOTCL]This CNCJob object can't be processed because "
"it is a %s CNCJob object." % str(self.special_group))
return 'fail'
except AttributeError:
pass
# detect if using Roland postprocessor
try:
for key in self.cnc_tools:

View File

@ -20,6 +20,7 @@ class ABCPostProcRegister(ABCMeta):
postprocessors[newclass.__name__] = newclass() # here is your register function
return newclass
class FlatCAMPostProc(object, metaclass=ABCPostProcRegister):
@abstractmethod
def start_code(self, p):
@ -65,6 +66,77 @@ class FlatCAMPostProc(object, metaclass=ABCPostProcRegister):
def spindle_stop_code(self,p):
pass
class FlatCAMPostProc_Tools(object, metaclass=ABCPostProcRegister):
@abstractmethod
def start_code(self, p):
pass
@abstractmethod
def lift_code(self, p):
pass
@abstractmethod
def down_z_start_code(self, p):
pass
@abstractmethod
def lift_z_dispense_code(self, p):
pass
@abstractmethod
def down_z_stop_code(self, p):
pass
@abstractmethod
def toolchange_code(self, p):
pass
@abstractmethod
def rapid_code(self, p):
pass
@abstractmethod
def linear_code(self, p):
pass
@abstractmethod
def end_code(self, p):
pass
@abstractmethod
def feedrate_xy_code(self, p):
pass
@abstractmethod
def feedrate_z_code(self, p):
pass
@abstractmethod
def feedrate_z_dispense_code(self,p):
pass
@abstractmethod
def spindle_fwd_code(self,p):
pass
@abstractmethod
def spindle_rev_code(self,p):
pass
@abstractmethod
def spindle_off_code(self,p):
pass
@abstractmethod
def dwell_fwd_code(self,p):
pass
@abstractmethod
def dwell_rev_code(self,p):
pass
def load_postprocessors(app):
postprocessors_path_search = [os.path.join(app.data_path,'postprocessors','*.py'),
os.path.join('postprocessors', '*.py')]

View File

@ -490,7 +490,8 @@ class FCComboBox(QtWidgets.QComboBox):
class FCInputDialog(QtWidgets.QInputDialog):
def __init__(self, parent=None, ok=False, val=None, title=None, text=None, min=None, max=None, decimals=None):
def __init__(self, parent=None, ok=False, val=None, title=None, text=None, min=None, max=None, decimals=None,
init_val=None):
super(FCInputDialog, self).__init__(parent)
self.allow_empty = ok
self.empty_val = val
@ -498,6 +499,8 @@ class FCInputDialog(QtWidgets.QInputDialog):
self.val = 0.0
self.ok = ''
self.init_value = init_val if init_val else 0.0
if title is None:
self.title = 'title'
else:
@ -521,7 +524,7 @@ class FCInputDialog(QtWidgets.QInputDialog):
def get_value(self):
self.val, self.ok = self.getDouble(self, self.title, self.text, min=self.min,
max=self.max, decimals=self.decimals)
max=self.max, decimals=self.decimals, value=self.init_value)
return [self.val, self.ok]
# "Transform", "Enter the Angle value:"

View File

@ -504,6 +504,8 @@ class ObjectCollection(QtCore.QAbstractItemModel):
sel = len(self.view.selectedIndexes()) > 0
self.app.ui.menuprojectenable.setEnabled(sel)
self.app.ui.menuprojectdisable.setEnabled(sel)
self.app.ui.menuprojectviewsource.setEnabled(sel)
self.app.ui.menuprojectcopy.setEnabled(sel)
self.app.ui.menuprojectedit.setEnabled(sel)
self.app.ui.menuprojectdelete.setEnabled(sel)
@ -514,14 +516,15 @@ class ObjectCollection(QtCore.QAbstractItemModel):
self.app.ui.menuprojectgeneratecnc.setVisible(True)
self.app.ui.menuprojectedit.setVisible(True)
self.app.ui.menuprojectsave.setVisible(True)
self.app.ui.menuprojectviewsource.setVisible(True)
for obj in self.get_selected():
if type(obj) != FlatCAMGeometry:
self.app.ui.menuprojectgeneratecnc.setVisible(False)
if type(obj) != FlatCAMGeometry and type(obj) != FlatCAMExcellon:
self.app.ui.menuprojectedit.setVisible(False)
if type(obj) != FlatCAMGeometry and type(obj) != FlatCAMExcellon and type(obj) != FlatCAMCNCjob:
self.app.ui.menuprojectsave.setVisible(False)
if type(obj) != FlatCAMGerber and type(obj) != FlatCAMExcellon:
self.app.ui.menuprojectviewsource.setVisible(False)
else:
self.app.ui.menuprojectgeneratecnc.setVisible(False)
@ -918,7 +921,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
except IndexError:
FlatCAMApp.App.log.debug("on_list_selection_change(): Index Error (Nothing selected?)")
self.app.inform.emit('')
try:
self.app.ui.selected_scroll_area.takeWidget()
except:

View File

@ -32,6 +32,19 @@ class ObjectUI(QtWidgets.QWidget):
self.title_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
self.title_box.addWidget(self.title_label, stretch=1)
## App Level label
self.level = QtWidgets.QLabel("")
self.level.setToolTip(
"BASIC is suitable for a beginner. Many parameters\n"
"are hidden from the user in this mode.\n"
"ADVANCED mode will make available all parameters.\n\n"
"To change the application LEVEL, go to:\n"
"Edit -> Preferences -> General and check:\n"
"'APP. LEVEL' radio button."
)
self.level.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.title_box.addWidget(self.level)
## Box box for custom widgets
# This gets populated in offspring implementations.
self.custom_box = QtWidgets.QVBoxLayout()
@ -237,13 +250,13 @@ class GerberObjectUI(ObjectUI):
grid1.addWidget(self.iso_overlap_entry, 2, 1)
# Milling Type Radio Button
milling_type_label = QtWidgets.QLabel('Milling Type:')
milling_type_label.setToolTip(
self.milling_type_label = QtWidgets.QLabel('Milling Type:')
self.milling_type_label.setToolTip(
"Milling type:\n"
"- climb / best for precision milling and to reduce tool usage\n"
"- conventional / useful when there is no backlash compensation"
)
grid1.addWidget(milling_type_label, 3, 0)
grid1.addWidget(self.milling_type_label, 3, 0)
self.milling_type_radio = RadioSet([{'label': 'Climb', 'value': 'cl'},
{'label': 'Conv.', 'value': 'cv'}])
grid1.addWidget(self.milling_type_radio, 3, 1)
@ -256,13 +269,12 @@ class GerberObjectUI(ObjectUI):
grid1.addWidget(self.combine_passes_cb, 4, 0)
# generate follow
self.follow_cb = FCCheckBox(label='"Follow" Geo')
self.follow_cb = FCCheckBox(label='"Follow"')
self.follow_cb.setToolTip(
"Generate a 'Follow' geometry.\n"
"This means that it will cut through\n"
"the middle of the trace.\n"
"Requires that the Gerber file to be\n"
"loaded with 'follow' parameter."
"the middle of the trace."
)
grid1.addWidget(self.follow_cb, 4, 1)
@ -380,16 +392,19 @@ class GerberObjectUI(ObjectUI):
# Rounded corners
self.noncopper_rounded_cb = FCCheckBox(label="Rounded corners")
self.noncopper_rounded_cb.setToolTip(
"Creates a Geometry objects with polygons\n"
"covering the copper-free areas of the PCB."
"Resulting geometry will have rounded corners."
)
grid4.addWidget(self.noncopper_rounded_cb, 1, 0, 1, 2)
grid4.addWidget(self.noncopper_rounded_cb, 1, 0)
self.generate_noncopper_button = QtWidgets.QPushButton('Generate Geometry')
self.custom_box.addWidget(self.generate_noncopper_button)
self.generate_noncopper_button = QtWidgets.QPushButton('Generate Geo')
grid4.addWidget(self.generate_noncopper_button, 1, 1)
## Bounding box
self.boundingbox_label = QtWidgets.QLabel('<b>Bounding Box:</b>')
self.boundingbox_label.setToolTip(
"Create a geometry surrounding the Gerber object.\n"
"Square shape."
)
self.custom_box.addWidget(self.boundingbox_label)
grid5 = QtWidgets.QGridLayout()
@ -411,13 +426,13 @@ class GerberObjectUI(ObjectUI):
"their radius is equal to\n"
"the margin."
)
grid5.addWidget(self.bbrounded_cb, 1, 0, 1, 2)
grid5.addWidget(self.bbrounded_cb, 1, 0)
self.generate_bb_button = QtWidgets.QPushButton('Generate Geometry')
self.generate_bb_button = QtWidgets.QPushButton('Generate Geo')
self.generate_bb_button.setToolTip(
"Genrate the Geometry object."
"Generate the Geometry object."
)
self.custom_box.addWidget(self.generate_bb_button)
grid5.addWidget(self.generate_bb_button, 1, 1)
class ExcellonObjectUI(ObjectUI):
@ -485,7 +500,7 @@ class ExcellonObjectUI(ObjectUI):
self.tools_box.addWidget(self.tools_table)
self.tools_table.setColumnCount(6)
self.tools_table.setHorizontalHeaderLabels(['#', 'Diameter', 'Drills', 'Slots', 'Offset', 'P'])
self.tools_table.setHorizontalHeaderLabels(['#', 'Diameter', 'Drills', 'Slots', 'Offset Z', 'P'])
self.tools_table.setSortingEnabled(False)
self.tools_table.horizontalHeaderItem(0).setToolTip(
@ -562,22 +577,22 @@ class ExcellonObjectUI(ObjectUI):
self.ois_tcz_e = OptionalInputSection(self.toolchange_cb, [self.toolchangez_entry])
# Start move Z:
startzlabel = QtWidgets.QLabel("Start move Z:")
startzlabel.setToolTip(
self.estartz_label = QtWidgets.QLabel("Start move Z:")
self.estartz_label.setToolTip(
"Tool height just before starting the work.\n"
"Delete the value if you don't need this feature."
)
grid1.addWidget(startzlabel, 4, 0)
grid1.addWidget(self.estartz_label, 4, 0)
self.estartz_entry = FloatEntry()
grid1.addWidget(self.estartz_entry, 4, 1)
# End move Z:
endzlabel = QtWidgets.QLabel("End move Z:")
endzlabel.setToolTip(
self.eendz_label = QtWidgets.QLabel("End move Z:")
self.eendz_label.setToolTip(
"Z-axis position (height) for\n"
"the last move."
)
grid1.addWidget(endzlabel, 5, 0)
grid1.addWidget(self.eendz_label, 5, 0)
self.eendz_entry = LengthEntry()
grid1.addWidget(self.eendz_entry, 5, 1)
@ -593,13 +608,13 @@ class ExcellonObjectUI(ObjectUI):
grid1.addWidget(self.feedrate_entry, 6, 1)
# Excellon Rapid Feedrate
fr_rapid_label = QtWidgets.QLabel('Feedrate Rapids:')
fr_rapid_label.setToolTip(
self.feedrate_rapid_label = QtWidgets.QLabel('Feedrate Rapids:')
self.feedrate_rapid_label.setToolTip(
"Tool speed while drilling\n"
"(in units per minute).\n"
"This is for the rapid move G00."
)
grid1.addWidget(fr_rapid_label, 7, 0)
grid1.addWidget(self.feedrate_rapid_label, 7, 0)
self.feedrate_rapid_entry = LengthEntry()
grid1.addWidget(self.feedrate_rapid_entry, 7, 1)
@ -976,7 +991,6 @@ class GeometryObjectUI(ObjectUI):
)
self.grid3.addWidget(self.mpass_cb, 4, 0)
self.maxdepth_entry = LengthEntry()
self.maxdepth_entry.setToolTip(
"Depth of each pass (positive)."
@ -1026,12 +1040,12 @@ class GeometryObjectUI(ObjectUI):
# self.grid3.addWidget(self.gstartz_entry, 8, 1)
# The Z value for the end move
endzlabel = QtWidgets.QLabel('End move Z:')
endzlabel.setToolTip(
self.endzlabel = QtWidgets.QLabel('End move Z:')
self.endzlabel.setToolTip(
"This is the height (Z) at which the CNC\n"
"will go as the last move."
)
self.grid3.addWidget(endzlabel, 9, 0)
self.grid3.addWidget(self.endzlabel, 9, 0)
self.gendz_entry = LengthEntry()
self.grid3.addWidget(self.gendz_entry, 9, 1)
@ -1056,13 +1070,13 @@ class GeometryObjectUI(ObjectUI):
self.grid3.addWidget(self.cncplunge_entry, 11, 1)
# Feedrate rapids
fr_rapidlabel = QtWidgets.QLabel('Feed Rate Rapids:')
fr_rapidlabel.setToolTip(
self.fr_rapidlabel = QtWidgets.QLabel('Feed Rate Rapids:')
self.fr_rapidlabel.setToolTip(
"Cutting speed in the XY\n"
"plane in units per minute\n"
"for the rapid movements"
)
self.grid3.addWidget(fr_rapidlabel, 12, 0)
self.grid3.addWidget(self.fr_rapidlabel, 12, 0)
self.cncfeedrate_rapid_entry = LengthEntry()
self.grid3.addWidget(self.cncfeedrate_rapid_entry, 12, 1)
@ -1338,9 +1352,9 @@ class CNCObjectUI(ObjectUI):
self.custom_box.addLayout(h_lay)
# Edit GCode Button
self.modify_gcode_button = QtWidgets.QPushButton('Edit CNC Code')
self.modify_gcode_button = QtWidgets.QPushButton('View CNC Code')
self.modify_gcode_button.setToolTip(
"Opens TAB to modify/print G-Code\n"
"Opens TAB to view/modify/print G-Code\n"
"file."
)

View File

@ -198,6 +198,13 @@ class PlotCanvas(QtCore.QObject):
except TypeError:
pass
# adjust the view camera to be slightly bigger than the bounds so the shape colleaction can be seen clearly
# otherwise the shape collection boundary will have no border
rect.left *= 0.96
rect.bottom *= 0.96
rect.right *= 1.01
rect.top *= 1.01
self.vispy_canvas.view.camera.rect = rect
self.shape_collection.unlock_updates()

View File

@ -9,6 +9,74 @@ CAD program, and create G-Code for Isolation routing.
=================================================
21.02.2019
- added protection against creating CNCJob from an empty Geometry object (with no geometry inside)
- changed the shortcut key for YouTube channel from F2 to key F4
- changed the way APP LEVEL is showed both in Edit -> Preferences -> General tab and in each Selected Tab. Changed the ToolTips content for this.
- added the functions for GCode View and GCode Save in Tool SolderPaste
- some work in the Gcode generation function in Tool SolderPaste
- added protection against trying to create a CNCJob from a solder_paste dispenser geometry. This one is different than the default Geometry and can be handled only by SolderPaste Tool.
- ToolSolderPaste tools (nozzles) now have each it's own settings
- creating the camlib functions for the ToolSolderPaste gcode generation functions
- finished work in ToolSolderPaste
20.02.2019
- finished added a Tool Table for Tool SolderPaste
- working on multi tool solder paste dispensing
- finished the Edit -> Preferences defaults section
- finished the UI, created the postprocessor file template
- finished the multi-tool solder paste dispensing: it will start using the biggest nozzle, fill the pads it can, and then go to the next smaller nozzle until there are no pads without solder.
19.02.2019
- added the ability to compress the FlatCAM project on save with LZMA compression. There is a setting in Edit -> Preferences -> Compression Level between 0 and 9. 9 level yields best compression at the price of RAM usage and time spent.
- made FlatCAM able to load old type (uncompressed) FlatCAM projects
- fixed issue with not loading old projects that do not have certain information's required by the new versions of FlatCAM
- compacted a bit more the GUI for Gerber Object
- removed the Open Gerber with 'follow' menu entry and also the open_gerber Tcl Command attribute 'follow'. This is no longer required because now the follow_geometry is stored by default in a Gerber object attribute gerber_obj.follow_geometry
- added a new parameter for the Tcl CommandIsolate, named: 'follow'. When follow = 1 (True) the resulting geometry will follow the Gerber paths.
- added a new setting in Edit -> Preferences -> General that allow to select the type of saving for the FlatCAM project: either compressed or uncompressed. Compression introduce an time overhead to the saving/restoring of a FlatCAM project.
- started to work on Solder Paste Dispensing Tool
- fixed a bug in rotate from shortcut function
- finished generating the solder paste dispense geometry
18.02.2019
- added protections again wrong values for the Buffer and Paint Tool in Geometry Editor
- the Paint Tool in Geometry Editor will load the default values from Tool Paint in Preferences
- when the Tools in Geometry Editor are activated, the notebook with the Tool Tab will be unhidden. After execution the notebook will hide again for the Buffer Tool.
- changed the font in Tool names
- added in Geometry Editor a new Tool: Transformation Tool.
- in Geometry Editor by selecting a shape with a selection shape, that object was added multiple times (one per each selection) to the selected list, which is not intended. Bug fixed.
- finished adding Transform Tool in Geometry Editor - everything is working as intended
- fixed a bug in Tool Transform that made the user to not be able to capture the click coordinates with SHIFT + LMB click combo
- added the ability to choose an App QStyle out of the offered choices (different for each OS) to be applied at the next app start (Preferences -> General -> Gui Pref -> Style Combobox)
- added support for FlatCAM usage with High DPI monitors (4k). It is applied on the next app startup after change in Preferences -> General -> Gui Settings -> HDPI Support Checkbox
- made the app not remember the window size if the app is maximized and remember in QSettings if it was maximized. This way we can restore the maximized state but restore the windows size unmaximized
- added a button to clear the GUI preferences in Preferences -> General -> Gui Settings -> Clear GUI Settings
- added key shortcuts for the shape transformations within Geometry Editor: X, Y keys for Flip(mirror), SHIFT+X, SHIFT+Y combo keys for Skew and ALT+X, ALT+Y combo keys for Offset
- adjusted the plotcanvas.zomm_fit() function so the objects are better fit into view (with a border around)
- modified the GUI in Objects Selected Tab to accommodate 2 different modes: basic and Advanced. In Basic mode, some of the functionality's are hidden from the user.
- added Tool Transform preferences in Edit -> Preferences and used them through out the app
- made the output of Panelization Tool a choice out of Gerber and Geometry type of objects. Useful for those who want to engrave multiple copies of the same design.
17.02.2019
- changed some status bar messages
- New feature: added the capability to view the source code of the Gerber/Excellon file that was loaded into the app. The file is also stored as an object attribute for later use. The view option is in the project context menu and in Menu -> Options -> View Source
- Serialized the source_file of the Objects so it is saved in the FlatCAM project and restored.
- if there is a single tool in the tool list (Geometry , Excellon) and the user click the Generate GCode, use that tool even if it is not selected
- fixed issue where after loading a project, if the default kind of CNCjob view is only 'cuts' the plot will revert to the 'all' type
- in Editors, if the modifier key set in Preferences (CTRL or SHIFT key) is pressed at the end of one tool operation it will automatically continue to that action until the modifier is no longer pressed when Select tool will be automatically selected.
- in Geometry Editor, on entry the notebook is automatically hidden and restored on Geometry Editor exit.
- when pressing Escape in Geometry Editor it will automatically deselect any shape not only the currently selected tool.
- when deselecting an object in Project menu the status bar selection message is deleted
- added ability to save the Gerber file content that is stored in FlatCAM on Gerber file loading. It's useful to recover from saved FlatCAM projects when the source files are no longer available.
- fixed an issue where the function handler that changed the layout had a parameter changed accidentally by an index value passed by the 'activate' signal to which was connected
- fixed bug in paint function in Geometry Editor that didn't allow painting due of overlap value
16.02.2019
- added the 'Save' menu entry to the Project context menu, for CNCJob: it will export the GCode.

383
camlib.py
View File

@ -92,8 +92,11 @@ class Geometry(object):
# Final geometry: MultiPolygon or list (of geometry constructs)
self.solid_geometry = None
# Final geometry: MultiLineString or list (of LineString or Points)
self.follow_geometry = None
# Attributes to be included in serialization
self.ser_attrs = ["units", 'solid_geometry']
self.ser_attrs = ["units", 'solid_geometry', 'follow_geometry']
# Flattened geometry (list of paths only)
self.flat_geometry = []
@ -500,7 +503,7 @@ class Geometry(object):
#
# return self.flat_geometry, self.flat_geometry_rtree
def isolation_geometry(self, offset, iso_type=2, corner=None):
def isolation_geometry(self, offset, iso_type=2, corner=None, follow=None):
"""
Creates contours around geometry at a given
offset distance.
@ -542,16 +545,24 @@ class Geometry(object):
# the previously commented block is replaced with this block - regression - to solve the bug with multiple
# isolation passes cutting from the copper features
if offset == 0:
geo_iso = self.solid_geometry
else:
if corner is None:
geo_iso = self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4))
if follow:
geo_iso = self.follow_geometry
else:
geo_iso = self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4), join_style=corner)
geo_iso = self.solid_geometry
else:
if follow:
geo_iso = self.follow_geometry
else:
if corner is None:
geo_iso = self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4))
else:
geo_iso = self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4),
join_style=corner)
# end of replaced block
if iso_type == 2:
if follow:
return geo_iso
elif iso_type == 2:
return geo_iso
elif iso_type == 0:
return self.get_exteriors(geo_iso)
@ -1375,8 +1386,6 @@ class Geometry(object):
except AttributeError:
self.app.inform.emit("[ERROR_NOTCL] Failed to mirror. No object selected")
def rotate(self, angle, point):
"""
Rotate an object by an angle (in degrees) around the provided coordinates.
@ -1891,8 +1900,12 @@ class Gerber (Geometry):
# Initialize parent
Geometry.__init__(self, geo_steps_per_circle=int(steps_per_circle))
# will store the Gerber geometry's as solids
self.solid_geometry = Polygon()
# will store the Gerber geometry's as paths
self.follow_geometry = []
# Number format
self.int_digits = 3
"""Number of integer digits in Gerber numbers. Used during parsing."""
@ -1913,11 +1926,13 @@ class Gerber (Geometry):
# Aperture Macros
self.aperture_macros = {}
self.source_file = ''
# Attributes to be included in serialization
# Always append to it because it carries contents
# from Geometry.
self.ser_attrs += ['int_digits', 'frac_digits', 'apertures',
'aperture_macros', 'solid_geometry']
'aperture_macros', 'solid_geometry', 'source_file']
#### Parser patterns ####
# FS - Format Specification
@ -2113,10 +2128,10 @@ class Gerber (Geometry):
yield line
break
self.parse_lines(line_generator(), follow=follow)
self.parse_lines(line_generator())
#@profile
def parse_lines(self, glines, follow=False):
def parse_lines(self, glines):
"""
Main Gerber parser. Reads Gerber and populates ``self.paths``, ``self.apertures``,
``self.flashes``, ``self.regions`` and ``self.units``.
@ -2143,6 +2158,9 @@ class Gerber (Geometry):
# applying a union for every new polygon.
poly_buffer = []
# store here the follow geometry
follow_buffer = []
last_path_aperture = None
current_aperture = None
@ -2185,6 +2203,8 @@ class Gerber (Geometry):
for gline in glines:
line_num += 1
self.source_file += gline + '\n'
### Cleanup
gline = gline.strip(' \r\n')
# log.debug("Line=%3s %s" % (line_num, gline))
@ -2206,10 +2226,11 @@ class Gerber (Geometry):
# --- Buffered ----
width = self.apertures[last_path_aperture]["size"]
if follow:
geo = LineString(path)
else:
geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
geo = LineString(path)
if not geo.is_empty:
follow_buffer.append(geo)
geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
if not geo.is_empty:
poly_buffer.append(geo)
@ -2220,9 +2241,14 @@ class Gerber (Geometry):
# TODO: Remove when bug fixed
if len(poly_buffer) > 0:
if current_polarity == 'D':
self.follow_geometry = self.solid_geometry.union(cascaded_union(follow_buffer))
self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer))
else:
self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer))
self.follow_geometry = self.solid_geometry.difference(cascaded_union(follow_buffer))
self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer))
follow_buffer = []
poly_buffer = []
current_polarity = match.group(1)
@ -2366,8 +2392,6 @@ class Gerber (Geometry):
log.debug("Bare op-code %d." % current_operation_code)
# flash = Gerber.create_flash_geometry(Point(path[-1]),
# self.apertures[current_aperture])
if follow:
continue
flash = Gerber.create_flash_geometry(
Point(current_x, current_y), self.apertures[current_aperture],
int(self.steps_per_circle))
@ -2403,10 +2427,11 @@ class Gerber (Geometry):
# --- Buffered ----
width = self.apertures[last_path_aperture]["size"]
if follow:
geo = LineString(path)
else:
geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
geo = LineString(path)
if not geo.is_empty:
follow_buffer.append(geo)
geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
if not geo.is_empty:
poly_buffer.append(geo)
@ -2422,10 +2447,11 @@ class Gerber (Geometry):
## --- Buffered ---
width = self.apertures[last_path_aperture]["size"]
if follow:
geo = LineString(path)
else:
geo = LineString(path).buffer(width/1.999, int(self.steps_per_circle / 4))
geo = LineString(path)
if not geo.is_empty:
follow_buffer.append(geo)
geo = LineString(path).buffer(width/1.999, int(self.steps_per_circle / 4))
if not geo.is_empty:
poly_buffer.append(geo)
@ -2443,6 +2469,7 @@ class Gerber (Geometry):
if current_operation_code == 2:
if geo:
if not geo.is_empty:
follow_buffer.append(geo)
poly_buffer.append(geo)
continue
@ -2462,15 +2489,14 @@ class Gerber (Geometry):
# "aperture": last_path_aperture})
# --- Buffered ---
if follow:
region = Polygon()
else:
region = Polygon(path)
region = Polygon()
if not region.is_empty:
follow_buffer.append(region)
region = Polygon(path)
if not region.is_valid:
if not follow:
region = region.buffer(0, int(self.steps_per_circle / 4))
region = region.buffer(0, int(self.steps_per_circle / 4))
if not region.is_empty:
poly_buffer.append(region)
@ -2527,7 +2553,7 @@ class Gerber (Geometry):
if path[-1] != [linear_x, linear_y]:
path.append([linear_x, linear_y])
if follow == 0 and making_region is False:
if making_region is False:
# if the aperture is rectangle then add a rectangular shape having as parameters the
# coordinates of the start and end point and also the width and height
# of the 'R' aperture
@ -2553,29 +2579,35 @@ class Gerber (Geometry):
geo = None
## --- BUFFERED ---
# this treats the case when we are storing geometry as paths only
if making_region:
if follow:
geo = Polygon()
else:
elem = [linear_x, linear_y]
if elem != path[-1]:
path.append([linear_x, linear_y])
try:
geo = Polygon(path)
except ValueError:
log.warning("Problem %s %s" % (gline, line_num))
self.app.inform.emit("[ERROR] Region does not have enough points. "
"File will be processed but there are parser errors. "
"Line number: %s" % str(line_num))
geo = Polygon()
else:
geo = LineString(path)
try:
if self.apertures[last_path_aperture]["type"] != 'R':
if not geo.is_empty:
follow_buffer.append(geo)
except:
follow_buffer.append(geo)
# this treats the case when we are storing geometry as solids
if making_region:
elem = [linear_x, linear_y]
if elem != path[-1]:
path.append([linear_x, linear_y])
try:
geo = Polygon(path)
except ValueError:
log.warning("Problem %s %s" % (gline, line_num))
self.app.inform.emit("[ERROR] Region does not have enough points. "
"File will be processed but there are parser errors. "
"Line number: %s" % str(line_num))
else:
if last_path_aperture is None:
log.warning("No aperture defined for curent path. (%d)" % line_num)
width = self.apertures[last_path_aperture]["size"] # TODO: WARNING this should fail!
#log.debug("Line %d: Setting aperture to %s before buffering." % (line_num, last_path_aperture))
if follow:
geo = LineString(path)
else:
geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
try:
if self.apertures[last_path_aperture]["type"] != 'R':
@ -2598,19 +2630,23 @@ class Gerber (Geometry):
# Create path draw so far.
if len(path) > 1:
# --- Buffered ----
# this treats the case when we are storing geometry as paths
geo = LineString(path)
if not geo.is_empty:
try:
if self.apertures[current_aperture]["type"] != 'R':
follow_buffer.append(geo)
except:
follow_buffer.append(geo)
# this treats the case when we are storing geometry as solids
width = self.apertures[last_path_aperture]["size"]
if follow:
geo = LineString(path)
else:
geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
if not geo.is_empty:
try:
if self.apertures[current_aperture]["type"] != 'R':
poly_buffer.append(geo)
else:
pass
except:
poly_buffer.append(geo)
@ -2619,11 +2655,12 @@ class Gerber (Geometry):
# --- BUFFERED ---
# Draw the flash
if follow:
continue
# this treats the case when we are storing geometry as paths
follow_buffer.append(Point([linear_x, linear_y]))
# this treats the case when we are storing geometry as solids
flash = Gerber.create_flash_geometry(
Point(
[linear_x, linear_y]),
Point( [linear_x, linear_y]),
self.apertures[current_aperture],
int(self.steps_per_circle)
)
@ -2709,10 +2746,13 @@ class Gerber (Geometry):
# --- BUFFERED ---
width = self.apertures[last_path_aperture]["size"]
if follow:
buffered = LineString(path)
else:
buffered = LineString(path).buffer(width / 1.999, int(self.steps_per_circle))
# this treats the case when we are storing geometry as paths
geo = LineString(path)
if not geo.is_empty:
follow_buffer.append(geo)
# this treats the case when we are storing geometry as solids
buffered = LineString(path).buffer(width / 1.999, int(self.steps_per_circle))
if not buffered.is_empty:
poly_buffer.append(buffered)
@ -2831,19 +2871,24 @@ class Gerber (Geometry):
else:
# EOF, create shapely LineString if something still in path
## --- Buffered ---
# this treats the case when we are storing geometry as paths
geo = LineString(path)
if not geo.is_empty:
follow_buffer.append(geo)
# this treats the case when we are storing geometry as solids
width = self.apertures[last_path_aperture]["size"]
if follow:
geo = LineString(path)
else:
geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
if not geo.is_empty:
poly_buffer.append(geo)
# --- Apply buffer ---
if follow:
self.solid_geometry = poly_buffer
return
# this treats the case when we are storing geometry as paths
self.follow_geometry = follow_buffer
# this treats the case when we are storing geometry as solids
log.warning("Joining %d polygons." % len(poly_buffer))
if len(poly_buffer) == 0:
@ -3293,6 +3338,8 @@ class Excellon(Geometry):
# self.slots (list) to store the slots; each is a dictionary
self.slots = []
self.source_file = ''
# it serve to flag if a start routing or a stop routing was encountered
# if a stop is encounter and this flag is still 0 (so there is no stop for a previous start) issue error
self.routing_flag = 1
@ -3323,7 +3370,8 @@ class Excellon(Geometry):
# Always append to it because it carries contents
# from Geometry.
self.ser_attrs += ['tools', 'drills', 'zeros', 'excellon_format_upper_mm', 'excellon_format_lower_mm',
'excellon_format_upper_in', 'excellon_format_lower_in', 'excellon_units', 'slots']
'excellon_format_upper_in', 'excellon_format_lower_in', 'excellon_units', 'slots',
'source_file']
#### Patterns ####
# Regex basics:
@ -3469,6 +3517,8 @@ class Excellon(Geometry):
line_num += 1
# log.debug("%3d %s" % (line_num, str(eline)))
self.source_file += eline
# Cleanup lines
eline = eline.strip(' \r\n')
@ -3819,7 +3869,7 @@ class Excellon(Geometry):
self.drills.append({'point': Point((coordx, coordy)), 'tool': current_tool})
repeat -= 1
repeating_x = repeating_y = 0
log.debug("{:15} {:8} {:8}".format(eline, x, y))
# log.debug("{:15} {:8} {:8}".format(eline, x, y))
continue
## Coordinates with period: Use literally. ##
@ -3901,7 +3951,7 @@ class Excellon(Geometry):
self.drills.append({'point': Point((coordx, coordy)), 'tool': current_tool})
repeat -= 1
repeating_x = repeating_y = 0
log.debug("{:15} {:8} {:8}".format(eline, x, y))
# log.debug("{:15} {:8} {:8}".format(eline, x, y))
continue
#### Header ####
@ -4004,7 +4054,6 @@ class Excellon(Geometry):
# is finished since the tools definitions are spread in the Excellon body. We use as units the value
# from self.defaults['excellon_units']
log.info("Zeros: %s, Units %s." % (self.zeros, self.units))
except Exception as e:
log.error("Excellon PARSING FAILED. Line %d: %s" % (line_num, eline))
msg = "[ERROR_NOTCL] An internal error has ocurred. See shell.\n"
@ -4454,6 +4503,9 @@ class CNCjob(Geometry):
self.pp_excellon_name = pp_excellon_name
self.pp_excellon = self.app.postprocessors[self.pp_excellon_name]
self.pp_solderpaste_name = None
# Controls if the move from Z_Toolchange to Z_Move is done fast with G0 or normally with G1
self.f_plunge = None
@ -4478,6 +4530,8 @@ class CNCjob(Geometry):
self.oldx = None
self.oldy = None
self.tool = 0.0
# Attributes to be included in serialization
# Always append to it because it carries contents
# from Geometry.
@ -4977,7 +5031,7 @@ class CNCjob(Geometry):
:param depthpercut: Maximum depth in each pass.
:param extracut: Adds (or not) an extra cut at the end of each path
overlapping the first point in path to ensure complete copper removal
:return: None
:return: GCode - string
"""
log.debug("Generate_from_multitool_geometry()")
@ -5103,7 +5157,9 @@ class CNCjob(Geometry):
log.debug("Starting G-Code...")
path_count = 0
current_pt = (0, 0)
pt, geo = storage.nearest(current_pt)
try:
while True:
path_count += 1
@ -5392,13 +5448,162 @@ class CNCjob(Geometry):
return self.gcode
def generate_gcode_from_solderpaste_geo(self, **kwargs):
"""
Algorithm to generate from multitool Geometry.
Algorithm description:
----------------------
Uses RTree to find the nearest path to follow.
:return: Gcode string
"""
log.debug("Generate_from_solderpaste_geometry()")
## Index first and last points in paths
# What points to index.
def get_pts(o):
return [o.coords[0], o.coords[-1]]
self.gcode = ""
if not kwargs:
log.debug("camlib.generate_from_solderpaste_geo() --> No tool in the solderpaste geometry.")
self.app.inform.emit("[ERROR_NOTCL] There is no tool data in the SolderPaste geometry.")
# this is the tool diameter, it is used as such to accommodate the postprocessor who need the tool diameter
# given under the name 'toolC'
self.postdata['z_start'] = kwargs['data']['tools_solderpaste_z_start']
self.postdata['z_dispense'] = kwargs['data']['tools_solderpaste_z_dispense']
self.postdata['z_stop'] = kwargs['data']['tools_solderpaste_z_stop']
self.postdata['z_travel'] = kwargs['data']['tools_solderpaste_z_travel']
self.postdata['z_toolchange'] = kwargs['data']['tools_solderpaste_z_toolchange']
self.postdata['xy_toolchange'] = kwargs['data']['tools_solderpaste_xy_toolchange']
self.postdata['frxy'] = kwargs['data']['tools_solderpaste_frxy']
self.postdata['frz'] = kwargs['data']['tools_solderpaste_frz']
self.postdata['frz_dispense'] = kwargs['data']['tools_solderpaste_frz_dispense']
self.postdata['speedfwd'] = kwargs['data']['tools_solderpaste_speedfwd']
self.postdata['dwellfwd'] = kwargs['data']['tools_solderpaste_dwellfwd']
self.postdata['speedrev'] = kwargs['data']['tools_solderpaste_speedrev']
self.postdata['dwellrev'] = kwargs['data']['tools_solderpaste_dwellrev']
self.postdata['pp_solderpaste_name'] = kwargs['data']['tools_solderpaste_pp']
self.postdata['toolC'] = kwargs['tooldia']
self.pp_solderpaste_name = kwargs['data']['tools_solderpaste_pp'] if kwargs['data']['tools_solderpaste_pp'] \
else self.app.defaults['tools_solderpaste_pp']
p = self.app.postprocessors[self.pp_solderpaste_name]
## Flatten the geometry. Only linear elements (no polygons) remain.
flat_geometry = self.flatten(kwargs['solid_geometry'], pathonly=True)
log.debug("%d paths" % len(flat_geometry))
# Create the indexed storage.
storage = FlatCAMRTreeStorage()
storage.get_points = get_pts
# Store the geometry
log.debug("Indexing geometry before generating G-Code...")
for shape in flat_geometry:
if shape is not None:
storage.insert(shape)
# Initial G-Code
self.gcode = self.doformat(p.start_code)
self.gcode += self.doformat(p.spindle_off_code)
self.gcode += self.doformat(p.toolchange_code)
## Iterate over geometry paths getting the nearest each time.
log.debug("Starting SolderPaste G-Code...")
path_count = 0
current_pt = (0, 0)
pt, geo = storage.nearest(current_pt)
try:
while True:
path_count += 1
# Remove before modifying, otherwise deletion will fail.
storage.remove(geo)
# If last point in geometry is the nearest but prefer the first one if last point == first point
# then reverse coordinates.
if pt != geo.coords[0] and pt == geo.coords[-1]:
geo.coords = list(geo.coords)[::-1]
self.gcode += self.create_soldepaste_gcode(geo, p=p)
current_pt = geo.coords[-1]
pt, geo = storage.nearest(current_pt) # Next
except StopIteration: # Nothing found in storage.
pass
log.debug("Finishing SolderPste G-Code... %s paths traced." % path_count)
# Finish
self.gcode += self.doformat(p.lift_code)
self.gcode += self.doformat(p.end_code)
return self.gcode
def create_soldepaste_gcode(self, geometry, p):
gcode = ''
path = geometry.coords
if type(geometry) == LineString or type(geometry) == LinearRing:
# Move fast to 1st point
gcode += self.doformat(p.rapid_code, x=path[0][0], y=path[0][1]) # Move to first point
# Move down to cutting depth
gcode += self.doformat(p.feedrate_z_code)
gcode += self.doformat(p.down_z_start_code)
gcode += self.doformat(p.spindle_fwd_code) # Start dispensing
gcode += self.doformat(p.dwell_fwd_code)
gcode += self.doformat(p.feedrate_z_dispense_code)
gcode += self.doformat(p.lift_z_dispense_code)
gcode += self.doformat(p.feedrate_xy_code)
# Cutting...
for pt in path[1:]:
gcode += self.doformat(p.linear_code, x=pt[0], y=pt[1]) # Linear motion to point
# Up to travelling height.
gcode += self.doformat(p.spindle_off_code) # Stop dispensing
gcode += self.doformat(p.spindle_rev_code)
gcode += self.doformat(p.down_z_stop_code)
gcode += self.doformat(p.spindle_off_code)
gcode += self.doformat(p.dwell_rev_code)
gcode += self.doformat(p.feedrate_z_code)
gcode += self.doformat(p.lift_code)
elif type(geometry) == Point:
gcode += self.doformat(p.linear_code, x=path[0][0], y=path[0][1]) # Move to first point
gcode += self.doformat(p.feedrate_z_dispense_code)
gcode += self.doformat(p.down_z_start_code)
gcode += self.doformat(p.spindle_fwd_code) # Start dispensing
gcode += self.doformat(p.dwell_fwd_code)
gcode += self.doformat(p.lift_z_dispense_code)
gcode += self.doformat(p.spindle_off_code) # Stop dispensing
gcode += self.doformat(p.spindle_rev_code)
gcode += self.doformat(p.spindle_off_code)
gcode += self.doformat(p.down_z_stop_code)
gcode += self.doformat(p.dwell_rev_code)
gcode += self.doformat(p.feedrate_z_code)
gcode += self.doformat(p.lift_code)
return gcode
def create_gcode_single_pass(self, geometry, extracut, tolerance):
# G-code. Note: self.linear2gcode() and self.point2gcode() will lower and raise the tool every time.
gcode_single_pass = ''
if type(geometry) == LineString or type(geometry) == LinearRing:
if extracut is False:
gcode_single_pass = self.linear2gcode(geometry, tolerance=tolerance, )
gcode_single_pass = self.linear2gcode(geometry, tolerance=tolerance)
else:
if geometry.is_ring:
gcode_single_pass = self.linear2gcode_extra(geometry, tolerance=tolerance)
@ -5503,7 +5708,8 @@ class CNCjob(Geometry):
else:
command['Z'] = 0
elif 'grbl_laser' in self.pp_excellon_name or 'grbl_laser' in self.pp_geometry_name:
elif 'grbl_laser' in self.pp_excellon_name or 'grbl_laser' in self.pp_geometry_name or \
(self.pp_solderpaste_name is not None and 'Paste' in self.pp_solderpaste_name):
match_lsr = re.search(r"X([\+-]?\d+.[\+-]?\d+)\s*Y([\+-]?\d+.[\+-]?\d+)", gline)
if match_lsr:
command['X'] = float(match_lsr.group(1).replace(" ", ""))
@ -5517,7 +5723,12 @@ class CNCjob(Geometry):
command['Z'] = 1
else:
command['Z'] = 0
elif self.pp_solderpaste is not None:
if 'Paste' in self.pp_solderpaste:
match_paste = re.search(r"X([\+-]?\d+.[\+-]?\d+)\s*Y([\+-]?\d+.[\+-]?\d+)", gline)
if match_paste:
command['X'] = float(match_paste.group(1).replace(" ", ""))
command['Y'] = float(match_paste.group(2).replace(" ", ""))
else:
match = re.search(r'^\s*([A-Z])\s*([\+\-\.\d\s]+)', gline)
while match:

View File

@ -18,7 +18,14 @@ class ToolCalculator(FlatCAMTool):
self.app = app
## Title
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
title_label = QtWidgets.QLabel("%s" % self.toolName)
title_label.setStyleSheet("""
QLabel
{
font-size: 16px;
font-weight: bold;
}
""")
self.layout.addWidget(title_label)
######################

View File

@ -1,14 +1,9 @@
from FlatCAMTool import FlatCAMTool
from copy import copy,deepcopy
from ObjectCollection import *
from FlatCAMApp import *
from PyQt5 import QtGui, QtCore, QtWidgets
from GUIElements import IntEntry, RadioSet, LengthEntry
from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber
class ToolCutOut(FlatCAMTool):
class CutOut(FlatCAMTool):
toolName = "Cutout PCB"
@ -16,7 +11,14 @@ class ToolCutOut(FlatCAMTool):
FlatCAMTool.__init__(self, app)
## Title
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
title_label = QtWidgets.QLabel("%s" % self.toolName)
title_label.setStyleSheet("""
QLabel
{
font-size: 16px;
font-weight: bold;
}
""")
self.layout.addWidget(title_label)
## Form Layout

View File

@ -15,7 +15,14 @@ class DblSidedTool(FlatCAMTool):
FlatCAMTool.__init__(self, app)
## Title
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
title_label = QtWidgets.QLabel("%s" % self.toolName)
title_label.setStyleSheet("""
QLabel
{
font-size: 16px;
font-weight: bold;
}
""")
self.layout.addWidget(title_label)
self.empty_lb = QtWidgets.QLabel("")

View File

@ -12,7 +12,14 @@ class Film(FlatCAMTool):
FlatCAMTool.__init__(self, app)
# Title
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
title_label = QtWidgets.QLabel("%s" % self.toolName)
title_label.setStyleSheet("""
QLabel
{
font-size: 16px;
font-weight: bold;
}
""")
self.layout.addWidget(title_label)
# Form Layout

View File

@ -12,7 +12,14 @@ class ToolImage(FlatCAMTool):
FlatCAMTool.__init__(self, app)
# Title
title_label = QtWidgets.QLabel("<font size=4><b>IMAGE to PCB</b></font>")
title_label = QtWidgets.QLabel("%s" % 'Image to PCB')
title_label.setStyleSheet("""
QLabel
{
font-size: 16px;
font-weight: bold;
}
""")
self.layout.addWidget(title_label)
# Form Layout

View File

@ -1,7 +1,5 @@
from FlatCAMTool import FlatCAMTool
from copy import copy,deepcopy
# from GUIElements import IntEntry, RadioSet, FCEntry
# from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber
from ObjectCollection import *
import time
@ -24,7 +22,14 @@ class NonCopperClear(FlatCAMTool, Gerber):
self.tools_frame.setLayout(self.tools_box)
## Title
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
title_label = QtWidgets.QLabel("%s" % self.toolName)
title_label.setStyleSheet("""
QLabel
{
font-size: 16px;
font-weight: bold;
}
""")
self.tools_box.addWidget(title_label)
## Form Layout

View File

@ -14,7 +14,14 @@ class ToolPaint(FlatCAMTool, Gerber):
Geometry.__init__(self, geo_steps_per_circle=self.app.defaults["geometry_circle_steps"])
## Title
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
title_label = QtWidgets.QLabel("%s" % self.toolName)
title_label.setStyleSheet("""
QLabel
{
font-size: 16px;
font-weight: bold;
}
""")
self.layout.addWidget(title_label)
self.tools_frame = QtWidgets.QFrame()

View File

@ -13,7 +13,14 @@ class Panelize(FlatCAMTool):
self.app = app
## Title
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
title_label = QtWidgets.QLabel("%s" % self.toolName)
title_label.setStyleSheet("""
QLabel
{
font-size: 16px;
font-weight: bold;
}
""")
self.layout.addWidget(title_label)
## Form Layout
@ -119,6 +126,18 @@ class Panelize(FlatCAMTool):
)
form_layout.addRow(self.rows_label, self.rows)
## Type of resulting Panel object
self.panel_type_radio = RadioSet([{'label': 'Gerber', 'value': 'gerber'},
{'label': 'Geometry', 'value': 'geometry'}])
self.panel_type_label = QtWidgets.QLabel("Panel Type:")
self.panel_type_label.setToolTip(
"Choose the type of object for the panel object:\n"
"- Geometry\n"
"- Gerber"
)
form_layout.addRow(self.panel_type_label)
form_layout.addRow(self.panel_type_radio)
## Constrains
self.constrain_cb = FCCheckBox("Constrain panel within:")
self.constrain_cb.setToolTip(
@ -149,7 +168,6 @@ class Panelize(FlatCAMTool):
self.constrain_sel = OptionalInputSection(
self.constrain_cb, [self.x_width_lbl, self.x_width_entry, self.y_height_lbl, self.y_height_entry])
## Buttons
hlay_2 = QtWidgets.QHBoxLayout()
self.layout.addLayout(hlay_2)
@ -225,6 +243,10 @@ class Panelize(FlatCAMTool):
self.app.defaults["tools_panelize_constrainy"] else 0.0
self.y_height_entry.set_value(float(y_w))
panel_type = self.app.defaults["tools_panelize_panel_type"] if \
self.app.defaults["tools_panelize_panel_type"] else 'gerber'
self.panel_type_radio.set_value(panel_type)
def on_type_obj_index_changed(self):
obj_type = self.type_obj_combo.currentIndex()
self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
@ -337,6 +359,9 @@ class Panelize(FlatCAMTool):
"use a number.")
return
panel_type = str(self.panel_type_radio.get_value())
if 0 in {columns, rows}:
self.app.inform.emit("[ERROR_NOTCL]Columns or Rows are zero value. Change them to a positive integer.")
return "Columns or Rows are zero value. Change them to a positive integer."
@ -541,7 +566,8 @@ class Panelize(FlatCAMTool):
self.app.new_object("excellon", self.outname, job_init_excellon, plot=True, autoselected=True)
else:
self.app.progress.emit(50)
self.app.new_object("geometry", self.outname, job_init_geometry, plot=True, autoselected=True)
self.app.new_object(panel_type, self.outname, job_init_geometry,
plot=True, autoselected=True)
if self.constrain_flag is False:
self.app.inform.emit("[success]Panel done...")

View File

@ -22,7 +22,14 @@ class Properties(FlatCAMTool):
self.properties_frame.setLayout(self.properties_box)
## Title
title_label = QtWidgets.QLabel("<font size=4><b>&nbsp;%s</b></font>" % self.toolName)
title_label = QtWidgets.QLabel("%s" % self.toolName)
title_label.setStyleSheet("""
QLabel
{
font-size: 16px;
font-weight: bold;
}
""")
self.properties_box.addWidget(title_label)
# self.layout.setMargin(0) # PyQt4

File diff suppressed because it is too large Load Diff

View File

@ -20,7 +20,14 @@ class ToolTransform(FlatCAMTool):
self.transform_lay = QtWidgets.QVBoxLayout()
self.layout.addLayout(self.transform_lay)
## Title
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
title_label = QtWidgets.QLabel("%s" % self.toolName)
title_label.setStyleSheet("""
QLabel
{
font-size: 16px;
font-weight: bold;
}
""")
self.transform_lay.addWidget(title_label)
self.empty_label = QtWidgets.QLabel("")
@ -368,18 +375,64 @@ class ToolTransform(FlatCAMTool):
self.app.ui.notebook.setTabText(2, "Transform Tool")
def install(self, icon=None, separator=None, **kwargs):
FlatCAMTool.install(self, icon, separator, shortcut='ALT+R', **kwargs)
FlatCAMTool.install(self, icon, separator, shortcut='ALT+T', **kwargs)
def set_tool_ui(self):
## Initialize form
self.rotate_entry.set_value('0')
self.skewx_entry.set_value('0')
self.skewy_entry.set_value('0')
self.scalex_entry.set_value('1')
self.scaley_entry.set_value('1')
self.offx_entry.set_value('0')
self.offy_entry.set_value('0')
self.flip_ref_cb.setChecked(False)
if self.app.defaults["tools_transform_rotate"]:
self.rotate_entry.set_value(self.app.defaults["tools_transform_rotate"])
else:
self.rotate_entry.set_value(0.0)
if self.app.defaults["tools_transform_skew_x"]:
self.skewx_entry.set_value(self.app.defaults["tools_transform_skew_x"])
else:
self.skewx_entry.set_value(0.0)
if self.app.defaults["tools_transform_skew_y"]:
self.skewy_entry.set_value(self.app.defaults["tools_transform_skew_y"])
else:
self.skewy_entry.set_value(0.0)
if self.app.defaults["tools_transform_scale_x"]:
self.scalex_entry.set_value(self.app.defaults["tools_transform_scale_x"])
else:
self.scalex_entry.set_value(1.0)
if self.app.defaults["tools_transform_scale_y"]:
self.scaley_entry.set_value(self.app.defaults["tools_transform_scale_y"])
else:
self.scaley_entry.set_value(1.0)
if self.app.defaults["tools_transform_scale_link"]:
self.scale_link_cb.set_value(self.app.defaults["tools_transform_scale_link"])
else:
self.scale_link_cb.set_value(True)
if self.app.defaults["tools_transform_scale_reference"]:
self.scale_zero_ref_cb.set_value(self.app.defaults["tools_transform_scale_reference"])
else:
self.scale_zero_ref_cb.set_value(True)
if self.app.defaults["tools_transform_offset_x"]:
self.offx_entry.set_value(self.app.defaults["tools_transform_offset_x"])
else:
self.offx_entry.set_value(0.0)
if self.app.defaults["tools_transform_offset_y"]:
self.offy_entry.set_value(self.app.defaults["tools_transform_offset_y"])
else:
self.offy_entry.set_value(0.0)
if self.app.defaults["tools_transform_mirror_reference"]:
self.flip_ref_cb.set_value(self.app.defaults["tools_transform_mirror_reference"])
else:
self.flip_ref_cb.set_value(False)
if self.app.defaults["tools_transform_mirror_point"]:
self.flip_ref_entry.set_value(self.app.defaults["tools_transform_mirror_point"])
else:
self.flip_ref_entry.set_value((0,0))
def on_rotate(self):
try:
@ -412,7 +465,7 @@ class ToolTransform(FlatCAMTool):
return
def on_flip_add_coords(self):
val = self.app.defaults["global_point_clipboard_format"] % (self.app.pos[0], self.app.pos[1])
val = self.app.clipboard.text()
self.flip_ref_entry.set_value(val)
def on_skewx(self):
@ -595,7 +648,7 @@ class ToolTransform(FlatCAMTool):
# add information to the object that it was changed and how much
sel_obj.options['rotate'] = num
self.app.inform.emit('Object(s) were rotated ...')
self.app.inform.emit('[success]Rotate done ...')
self.app.progress.emit(100)
except Exception as e:
@ -656,7 +709,7 @@ class ToolTransform(FlatCAMTool):
else:
obj.options['mirror_y'] = True
obj.plot()
self.app.inform.emit('Flipped on the Y axis ...')
self.app.inform.emit('[success]Flip on the Y axis done ...')
elif axis is 'Y':
obj.mirror('Y', (px, py))
# add information to the object that it was changed and how much
@ -666,9 +719,8 @@ class ToolTransform(FlatCAMTool):
else:
obj.options['mirror_x'] = True
obj.plot()
self.app.inform.emit('Flipped on the X axis ...')
self.app.inform.emit('[success]Flip on the X axis done ...')
self.app.object_changed.emit(obj)
self.app.progress.emit(100)
except Exception as e:
@ -715,7 +767,7 @@ class ToolTransform(FlatCAMTool):
obj.options['skew_y'] = num
obj.plot()
self.app.object_changed.emit(obj)
self.app.inform.emit('Object(s) were skewed on %s axis ...' % str(axis))
self.app.inform.emit('[success]Skew on the %s axis done ...' % str(axis))
self.app.progress.emit(100)
except Exception as e:
@ -771,7 +823,7 @@ class ToolTransform(FlatCAMTool):
obj.options['scale_y'] = yfactor
obj.plot()
self.app.object_changed.emit(obj)
self.app.inform.emit('Object(s) were scaled on %s axis ...' % str(axis))
self.app.inform.emit('[success]Scale on the %s axis done ...' % str(axis))
self.app.progress.emit(100)
except Exception as e:
self.app.inform.emit("[ERROR_NOTCL] Due of %s, Scale action was not executed." % str(e))
@ -816,7 +868,7 @@ class ToolTransform(FlatCAMTool):
obj.options['offset_y'] = num
obj.plot()
self.app.object_changed.emit(obj)
self.app.inform.emit('Object(s) were offseted on %s axis ...' % str(axis))
self.app.inform.emit('[success]Offset on the %s axis done ...' % str(axis))
self.app.progress.emit(100)
except Exception as e:

View File

@ -6,12 +6,13 @@ from flatcamTools.ToolFilm import Film
from flatcamTools.ToolMove import ToolMove
from flatcamTools.ToolDblSided import DblSidedTool
from flatcamTools.ToolCutOut import ToolCutOut
from flatcamTools.ToolCutOut import CutOut
from flatcamTools.ToolCalculators import ToolCalculator
from flatcamTools.ToolProperties import Properties
from flatcamTools.ToolImage import ToolImage
from flatcamTools.ToolPaint import ToolPaint
from flatcamTools.ToolNonCopperClear import NonCopperClear
from flatcamTools.ToolTransform import ToolTransform
from flatcamTools.ToolSolderPaste import SolderPaste
from flatcamTools.ToolShell import FCShell

View File

@ -73,7 +73,7 @@ else:
excludes=['scipy', 'pytz'],
# packages=['OpenGL','numpy','vispy','ortools','google']
# packages=['numpy', 'rasterio'] # works for Python 3.7
packages = ['opengl', 'numpy', 'google', 'rasterio'] # works for Python 3.6.5 and Python 3.7.1
packages = ['opengl', 'numpy', 'rasterio'] # works for Python 3.6.5 and Python 3.7.1
)

151
postprocessors/Paste_1.py Normal file
View File

@ -0,0 +1,151 @@
from FlatCAMPostProc import *
class Paste_1(FlatCAMPostProc_Tools):
coordinate_format = "%.*f"
feedrate_format = '%.*f'
def start_code(self, p):
units = ' ' + str(p['units']).lower()
coords_xy = [float(eval(a)) for a in p['xy_toolchange'].split(",")]
gcode = ''
xmin = '%.*f' % (p.coords_decimals, p['options']['xmin'])
xmax = '%.*f' % (p.coords_decimals, p['options']['xmax'])
ymin = '%.*f' % (p.coords_decimals, p['options']['ymin'])
ymax = '%.*f' % (p.coords_decimals, p['options']['ymax'])
gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n'
gcode += '(Feedrate_XY: ' + str(p['frxy']) + units + '/min' + ')\n'
gcode += '(Feedrate_Z: ' + str(p['frz']) + units + '/min' + ')\n'
gcode += '(Feedrate_Z_Dispense: ' + str(p['frz_dispense']) + units + '/min' + ')\n'
gcode += '(Z_Dispense_Start: ' + str(p['z_start']) + units + ')\n'
gcode += '(Z_Dispense: ' + str(p['z_dispense']) + units + ')\n'
gcode += '(Z_Dispense_Stop: ' + str(p['z_stop']) + units + ')\n'
gcode += '(Z_Travel: ' + str(p['z_travel']) + units + ')\n'
gcode += '(Z Toolchange: ' + str(p['z_toolchange']) + units + ')\n'
gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
if 'Paste' in p.pp_solderpaste_name:
gcode += '(Postprocessor SolderPaste Dispensing Geometry: ' + str(p.pp_solderpaste_name) + ')\n' + '\n'
gcode += '(X range: ' + '{: >9s}'.format(xmin) + ' ... ' + '{: >9s}'.format(xmax) + ' ' + units + ')\n'
gcode += '(Y range: ' + '{: >9s}'.format(ymin) + ' ... ' + '{: >9s}'.format(ymax) + ' ' + units + ')\n\n'
gcode += '(Spindle Speed FWD: %s RPM)\n' % str(p['speedfwd'])
gcode += '(Spindle Speed REV: %s RPM)\n' % str(p['speedrev'])
gcode += '(Dwell FWD: %s RPM)\n' % str(p['dwellfwd'])
gcode += '(Dwell REV: %s RPM)\n' % str(p['dwellrev'])
gcode += ('G20\n' if p.units.upper() == 'IN' else 'G21\n')
gcode += 'G90\n'
gcode += 'G94\n'
return gcode
def lift_code(self, p):
return 'G00 Z' + self.coordinate_format%(p.coords_decimals, float(p['z_travel']))
def down_z_start_code(self, p):
return 'G01 Z' + self.coordinate_format%(p.coords_decimals, float(p['z_start']))
def lift_z_dispense_code(self, p):
return 'G01 Z' + self.coordinate_format%(p.coords_decimals, float(p['z_dispense']))
def down_z_stop_code(self, p):
return 'G01 Z' + self.coordinate_format%(p.coords_decimals, float(p['z_stop']))
def toolchange_code(self, p):
toolchangez = float(p['z_toolchange'])
toolchangexy = [float(eval(a)) for a in p['xy_toolchange'].split(",")]
gcode = ''
if toolchangexy is not None:
toolchangex = toolchangexy[0]
toolchangey = toolchangexy[1]
if p.units.upper() == 'MM':
toolC_formatted = format(float(p['toolC']), '.2f')
else:
toolC_formatted = format(float(p['toolC']), '.4f')
if toolchangexy is not None:
gcode = """
G00 Z{toolchangez}
G00 X{toolchangex} Y{toolchangey}
T{tool}
M6
(MSG, Change to Tool with Nozzle Dia = {toolC})
M0
""".format(toolchangex=self.coordinate_format % (p.coords_decimals, toolchangex),
toolchangey=self.coordinate_format % (p.coords_decimals, toolchangey),
toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez),
tool=int(int(p.tool)),
toolC=toolC_formatted)
else:
gcode = """
G00 Z{toolchangez}
T{tool}
M6
(MSG, Change to Tool with Nozzle Dia = {toolC})
M0
""".format(toolchangez=self.coordinate_format % (p.coords_decimals, toolchangez),
tool=int(int(p.tool)),
toolC=toolC_formatted)
return gcode
def position_code(self, p):
return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \
(p.coords_decimals, p.x, p.coords_decimals, p.y)
def rapid_code(self, p):
return ('G00 ' + self.position_code(p)).format(**p) + '\nG00 Z' + \
self.coordinate_format%(p.coords_decimals, float(p['z_travel']))
def linear_code(self, p):
return ('G01 ' + self.position_code(p)).format(**p)
def end_code(self, p):
coords_xy = [float(eval(a)) for a in p['xy_toolchange'].split(",")]
gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, float(p['z_toolchange'])) + "\n")
if coords_xy is not None:
gcode += 'G00 X{x} Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n"
return gcode
def feedrate_xy_code(self, p):
return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, float(p['frxy'])))
def feedrate_z_code(self, p):
return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, float(p['frz'])))
def feedrate_z_dispense_code(self, p):
return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, float(p['frz_dispense'])))
def spindle_fwd_code(self, p):
if p.spindlespeed:
return 'M03 S' + str(float(p['speedfwd']))
else:
return 'M03'
def spindle_rev_code(self, p):
if p.spindlespeed:
return 'M04 S' + str(float(p['speedrev']))
else:
return 'M04'
def spindle_off_code(self,p):
return 'M05'
def dwell_fwd_code(self, p):
if p.dwelltime:
return 'G4 P' + str(float(p['dwellfwd']))
def dwell_rev_code(self, p):
if p.dwelltime:
return 'G4 P' + str(float(p['dwellrev']))

View File

@ -14,7 +14,6 @@ apt-get install python3-tk
apt-get install libspatialindex-dev
apt-get install python3-gdal
apt-get install python3-lxml
apt-get install python3-ezdxf
easy_install3 -U distribute
pip3 install --upgrade dill
pip3 install --upgrade Shapely

BIN
share/offsetx32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

BIN
share/offsety32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

BIN
share/solderpaste32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 732 B

BIN
share/solderpastebis32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 B

BIN
share/source32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -225,7 +225,7 @@ class TclCommandGeoCutout(TclCommandSignaled):
def geo_init(geo_obj, app_obj):
try:
geo = cutout_obj.isolation_geometry((dia / 2), iso_type=0, corner=2)
geo = cutout_obj.isolation_geometry((dia / 2), iso_type=0, corner=2, follow=None)
except Exception as e:
log.debug("TclCommandGeoCutout.execute() --> %s" % str(e))
return 'fail'

View File

@ -28,7 +28,9 @@ class TclCommandIsolate(TclCommandSignaled):
('passes', int),
('overlap', float),
('combine', int),
('outname', str)
('outname', str),
('follow', str)
])
# array of mandatory options for current Tcl command: required = {'name','outname'}
@ -43,7 +45,8 @@ class TclCommandIsolate(TclCommandSignaled):
('passes', 'Passes of tool width.'),
('overlap', 'Fraction of tool diameter to overlap passes.'),
('combine', 'Combine all passes into one geometry.'),
('outname', 'Name of the resulting Geometry object.')
('outname', 'Name of the resulting Geometry object.'),
('follow', 'Create a Geometry that follows the Gerber path.')
]),
'examples': []
}
@ -68,6 +71,9 @@ class TclCommandIsolate(TclCommandSignaled):
else:
timeout = 10000
if 'follow' not in args:
args['follow'] = None
obj = self.app.collection.get_by_name(name)
if obj is None:
self.raise_tcl_error("Object not found: %s" % name)

View File

@ -17,7 +17,6 @@ class TclCommandOpenGerber(TclCommandSignaled):
# dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
option_types = collections.OrderedDict([
('follow', str),
('outname', str)
])
@ -29,7 +28,6 @@ class TclCommandOpenGerber(TclCommandSignaled):
'main': "Opens a Gerber file.",
'args': collections.OrderedDict([
('filename', 'Path to file to open.'),
('follow', 'N If 1, does not create polygons, just follows the gerber path.'),
('outname', 'Name of the resulting Gerber object.')
]),
'examples': []
@ -54,7 +52,7 @@ class TclCommandOpenGerber(TclCommandSignaled):
# Opening the file happens here
self.app.progress.emit(30)
try:
gerber_obj.parse_file(filename, follow=follow)
gerber_obj.parse_file(filename)
except IOError:
app_obj.inform.emit("[ERROR_NOTCL] Failed to open file: %s " % filename)
@ -77,9 +75,8 @@ class TclCommandOpenGerber(TclCommandSignaled):
else:
outname = filename.split('/')[-1].split('\\')[-1]
follow = None
if 'follow' in args:
follow = args['follow']
self.raise_tcl_error("The 'follow' parameter is obsolete. To create 'follow' geometry use the 'follow' parameter for the Tcl Command isolate()")
with self.app.proc_container.new("Opening Gerber"):