- added a new Tcl Command named SetPath which will set a path to be used by the Tcl commands. Once set will serve as a fallback path in case that the files fail to be opened first time. It will be persistent, saved in preferences. - added the GUI for the new Open Example in the FIle -> Scripting menu. - I am modifying all the open ... handlers to add a parameter that will flag if the method was launched from Tcl Shell. This way if the method will fail to open the filename (which include the path) it will try to open from a set fallback path. - fixed issue #406, bug introduced recently (leftover changes). - modified the ImportSVG Tcl command name to OpenSVG (open_svg alias) - added a new Tcl command named OpenDXF (open_dxf alias) - fixed some errors in Scripting features - added a new Tcl command named GetPath as a convenient way to get the current default path stored in App.defaults['global_tcl_path']
1222 lines
52 KiB
Python
1222 lines
52 KiB
Python
# ##########################################################
|
|
# FlatCAM: 2D Post-processing for Manufacturing #
|
|
# http://flatcam.org #
|
|
# Author: Juan Pablo Caram (c) #
|
|
# Date: 2/5/2014 #
|
|
# MIT Licence #
|
|
# ##########################################################
|
|
|
|
# ##########################################################
|
|
# File modified by: Marius Stanciu #
|
|
# ##########################################################
|
|
|
|
from copy import deepcopy
|
|
from io import StringIO
|
|
from datetime import datetime
|
|
|
|
from flatcamEditors.FlatCAMTextEditor import TextEditor
|
|
from flatcamObjects.FlatCAMObj import *
|
|
|
|
from camlib import CNCjob
|
|
|
|
import os
|
|
import sys
|
|
import math
|
|
|
|
import gettext
|
|
import FlatCAMTranslation as fcTranslate
|
|
import builtins
|
|
|
|
fcTranslate.apply_language('strings')
|
|
if '_' not in builtins.__dict__:
|
|
_ = gettext.gettext
|
|
|
|
|
|
class CNCJobObject(FlatCAMObj, CNCjob):
|
|
"""
|
|
Represents G-Code.
|
|
"""
|
|
optionChanged = QtCore.pyqtSignal(str)
|
|
ui_type = CNCObjectUI
|
|
|
|
def __init__(self, name, units="in", kind="generic", z_move=0.1,
|
|
feedrate=3.0, feedrate_rapid=3.0, z_cut=-0.002, tooldia=0.0,
|
|
spindlespeed=None):
|
|
|
|
log.debug("Creating CNCJob object...")
|
|
|
|
self.decimals = self.app.decimals
|
|
|
|
CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
|
|
feedrate=feedrate, feedrate_rapid=feedrate_rapid, z_cut=z_cut, tooldia=tooldia,
|
|
spindlespeed=spindlespeed, steps_per_circle=int(self.app.defaults["cncjob_steps_per_circle"]))
|
|
|
|
FlatCAMObj.__init__(self, name)
|
|
|
|
self.kind = "cncjob"
|
|
|
|
self.options.update({
|
|
"plot": True,
|
|
"tooldia": 0.03937, # 0.4mm in inches
|
|
"append": "",
|
|
"prepend": "",
|
|
"dwell": False,
|
|
"dwelltime": 1,
|
|
"type": 'Geometry',
|
|
"toolchange_macro": '',
|
|
"toolchange_macro_enable": False
|
|
})
|
|
|
|
'''
|
|
This is a dict of dictionaries. Each dict is associated with a tool present in the file. The key is the
|
|
diameter of the tools and the value is another dict that will hold the data under the following form:
|
|
{tooldia: {
|
|
'tooluid': 1,
|
|
'offset': 'Path',
|
|
'type_item': 'Rough',
|
|
'tool_type': 'C1',
|
|
'data': {} # a dict to hold the parameters
|
|
'gcode': "" # a string with the actual GCODE
|
|
'gcode_parsed': {} # dictionary holding the CNCJob geometry and type of geometry
|
|
(cut or move)
|
|
'solid_geometry': []
|
|
},
|
|
...
|
|
}
|
|
It is populated in the GeometryObject.mtool_gen_cncjob()
|
|
BEWARE: I rely on the ordered nature of the Python 3.7 dictionary. Things might change ...
|
|
'''
|
|
self.cnc_tools = {}
|
|
|
|
'''
|
|
This is a dict of dictionaries. Each dict is associated with a tool present in the file. The key is the
|
|
diameter of the tools and the value is another dict that will hold the data under the following form:
|
|
{tooldia: {
|
|
'tool': int,
|
|
'nr_drills': int,
|
|
'nr_slots': int,
|
|
'offset': float,
|
|
'data': {} # a dict to hold the parameters
|
|
'gcode': "" # a string with the actual GCODE
|
|
'gcode_parsed': {} # dictionary holding the CNCJob geometry and type of geometry (cut or move)
|
|
'solid_geometry': []
|
|
},
|
|
...
|
|
}
|
|
It is populated in the ExcellonObject.on_create_cncjob_click() but actually
|
|
it's done in camlib.CNCJob.generate_from_excellon_by_tool()
|
|
BEWARE: I rely on the ordered nature of the Python 3.7 dictionary. Things might change ...
|
|
'''
|
|
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
|
|
self.multitool = False
|
|
|
|
# determine if the GCode was generated out of a Excellon object or a Geometry object
|
|
self.origin_kind = None
|
|
|
|
# used for parsing the GCode lines to adjust the GCode when the GCode is offseted or scaled
|
|
gcodex_re_string = r'(?=.*(X[-\+]?\d*\.\d*))'
|
|
self.g_x_re = re.compile(gcodex_re_string)
|
|
gcodey_re_string = r'(?=.*(Y[-\+]?\d*\.\d*))'
|
|
self.g_y_re = re.compile(gcodey_re_string)
|
|
gcodez_re_string = r'(?=.*(Z[-\+]?\d*\.\d*))'
|
|
self.g_z_re = re.compile(gcodez_re_string)
|
|
|
|
gcodef_re_string = r'(?=.*(F[-\+]?\d*\.\d*))'
|
|
self.g_f_re = re.compile(gcodef_re_string)
|
|
gcodet_re_string = r'(?=.*(\=\s*[-\+]?\d*\.\d*))'
|
|
self.g_t_re = re.compile(gcodet_re_string)
|
|
|
|
gcodenr_re_string = r'([+-]?\d*\.\d+)'
|
|
self.g_nr_re = re.compile(gcodenr_re_string)
|
|
|
|
# Attributes to be included in serialization
|
|
# Always append to it because it carries contents
|
|
# from predecessors.
|
|
self.ser_attrs += ['options', 'kind', 'origin_kind', 'cnc_tools', 'exc_cnc_tools', 'multitool']
|
|
|
|
if self.app.is_legacy is False:
|
|
self.text_col = self.app.plotcanvas.new_text_collection()
|
|
self.text_col.enabled = True
|
|
self.annotation = self.app.plotcanvas.new_text_group(collection=self.text_col)
|
|
|
|
self.gcode_editor_tab = None
|
|
|
|
self.units_found = self.app.defaults['units']
|
|
|
|
def build_ui(self):
|
|
self.ui_disconnect()
|
|
|
|
FlatCAMObj.build_ui(self)
|
|
self.units = self.app.defaults['units'].upper()
|
|
|
|
# if the FlatCAM object is Excellon don't build the CNC Tools Table but hide it
|
|
self.ui.cnc_tools_table.hide()
|
|
if self.cnc_tools:
|
|
self.ui.cnc_tools_table.show()
|
|
self.build_cnc_tools_table()
|
|
|
|
self.ui.exc_cnc_tools_table.hide()
|
|
if self.exc_cnc_tools:
|
|
self.ui.exc_cnc_tools_table.show()
|
|
self.build_excellon_cnc_tools()
|
|
#
|
|
self.ui_connect()
|
|
|
|
def build_cnc_tools_table(self):
|
|
tool_idx = 0
|
|
|
|
n = len(self.cnc_tools)
|
|
self.ui.cnc_tools_table.setRowCount(n)
|
|
|
|
for dia_key, dia_value in self.cnc_tools.items():
|
|
|
|
tool_idx += 1
|
|
row_no = tool_idx - 1
|
|
|
|
t_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
|
|
# id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
|
self.ui.cnc_tools_table.setItem(row_no, 0, t_id) # Tool name/id
|
|
|
|
# Make sure that the tool diameter when in MM is with no more than 2 decimals.
|
|
# There are no tool bits in MM with more than 2 decimals diameter.
|
|
# For INCH the decimals should be no more than 4. There are no tools under 10mils.
|
|
|
|
dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(dia_value['tooldia'])))
|
|
|
|
offset_txt = list(str(dia_value['offset']))
|
|
offset_txt[0] = offset_txt[0].upper()
|
|
offset_item = QtWidgets.QTableWidgetItem(''.join(offset_txt))
|
|
type_item = QtWidgets.QTableWidgetItem(str(dia_value['type']))
|
|
tool_type_item = QtWidgets.QTableWidgetItem(str(dia_value['tool_type']))
|
|
|
|
t_id.setFlags(QtCore.Qt.ItemIsEnabled)
|
|
dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
|
|
offset_item.setFlags(QtCore.Qt.ItemIsEnabled)
|
|
type_item.setFlags(QtCore.Qt.ItemIsEnabled)
|
|
tool_type_item.setFlags(QtCore.Qt.ItemIsEnabled)
|
|
|
|
# hack so the checkbox stay centered in the table cell
|
|
# used this:
|
|
# https://stackoverflow.com/questions/32458111/pyqt-allign-checkbox-and-put-it-in-every-row
|
|
# plot_item = QtWidgets.QWidget()
|
|
# checkbox = FCCheckBox()
|
|
# checkbox.setCheckState(QtCore.Qt.Checked)
|
|
# qhboxlayout = QtWidgets.QHBoxLayout(plot_item)
|
|
# qhboxlayout.addWidget(checkbox)
|
|
# qhboxlayout.setAlignment(QtCore.Qt.AlignCenter)
|
|
# qhboxlayout.setContentsMargins(0, 0, 0, 0)
|
|
plot_item = FCCheckBox()
|
|
plot_item.setLayoutDirection(QtCore.Qt.RightToLeft)
|
|
tool_uid_item = QtWidgets.QTableWidgetItem(str(dia_key))
|
|
if self.ui.plot_cb.isChecked():
|
|
plot_item.setChecked(True)
|
|
|
|
self.ui.cnc_tools_table.setItem(row_no, 1, dia_item) # Diameter
|
|
self.ui.cnc_tools_table.setItem(row_no, 2, offset_item) # Offset
|
|
self.ui.cnc_tools_table.setItem(row_no, 3, type_item) # Toolpath Type
|
|
self.ui.cnc_tools_table.setItem(row_no, 4, tool_type_item) # Tool Type
|
|
|
|
# ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY # ##
|
|
self.ui.cnc_tools_table.setItem(row_no, 5, tool_uid_item) # Tool unique ID)
|
|
self.ui.cnc_tools_table.setCellWidget(row_no, 6, plot_item)
|
|
|
|
# make the diameter column editable
|
|
# for row in range(tool_idx):
|
|
# self.ui.cnc_tools_table.item(row, 1).setFlags(QtCore.Qt.ItemIsSelectable |
|
|
# QtCore.Qt.ItemIsEnabled)
|
|
|
|
for row in range(tool_idx):
|
|
self.ui.cnc_tools_table.item(row, 0).setFlags(
|
|
self.ui.cnc_tools_table.item(row, 0).flags() ^ QtCore.Qt.ItemIsSelectable)
|
|
|
|
self.ui.cnc_tools_table.resizeColumnsToContents()
|
|
self.ui.cnc_tools_table.resizeRowsToContents()
|
|
|
|
vertical_header = self.ui.cnc_tools_table.verticalHeader()
|
|
# vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
|
|
vertical_header.hide()
|
|
self.ui.cnc_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
|
|
|
horizontal_header = self.ui.cnc_tools_table.horizontalHeader()
|
|
horizontal_header.setMinimumSectionSize(10)
|
|
horizontal_header.setDefaultSectionSize(70)
|
|
horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
|
|
horizontal_header.resizeSection(0, 20)
|
|
horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
|
|
horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
|
|
horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed)
|
|
horizontal_header.resizeSection(4, 40)
|
|
horizontal_header.setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed)
|
|
horizontal_header.resizeSection(4, 17)
|
|
# horizontal_header.setStretchLastSection(True)
|
|
self.ui.cnc_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
|
|
|
self.ui.cnc_tools_table.setColumnWidth(0, 20)
|
|
self.ui.cnc_tools_table.setColumnWidth(4, 40)
|
|
self.ui.cnc_tools_table.setColumnWidth(6, 17)
|
|
|
|
# self.ui.geo_tools_table.setSortingEnabled(True)
|
|
|
|
self.ui.cnc_tools_table.setMinimumHeight(self.ui.cnc_tools_table.getHeight())
|
|
self.ui.cnc_tools_table.setMaximumHeight(self.ui.cnc_tools_table.getHeight())
|
|
|
|
def build_excellon_cnc_tools(self):
|
|
tool_idx = 0
|
|
|
|
n = len(self.exc_cnc_tools)
|
|
self.ui.exc_cnc_tools_table.setRowCount(n)
|
|
|
|
for tooldia_key, dia_value in self.exc_cnc_tools.items():
|
|
|
|
tool_idx += 1
|
|
row_no = tool_idx - 1
|
|
|
|
t_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
|
|
dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(tooldia_key)))
|
|
nr_drills_item = QtWidgets.QTableWidgetItem('%d' % int(dia_value['nr_drills']))
|
|
nr_slots_item = QtWidgets.QTableWidgetItem('%d' % int(dia_value['nr_slots']))
|
|
cutz_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(dia_value['offset_z']) + self.z_cut))
|
|
|
|
t_id.setFlags(QtCore.Qt.ItemIsEnabled)
|
|
dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
|
|
nr_drills_item.setFlags(QtCore.Qt.ItemIsEnabled)
|
|
nr_slots_item.setFlags(QtCore.Qt.ItemIsEnabled)
|
|
cutz_item.setFlags(QtCore.Qt.ItemIsEnabled)
|
|
|
|
# hack so the checkbox stay centered in the table cell
|
|
# used this:
|
|
# https://stackoverflow.com/questions/32458111/pyqt-allign-checkbox-and-put-it-in-every-row
|
|
# plot_item = QtWidgets.QWidget()
|
|
# checkbox = FCCheckBox()
|
|
# checkbox.setCheckState(QtCore.Qt.Checked)
|
|
# qhboxlayout = QtWidgets.QHBoxLayout(plot_item)
|
|
# qhboxlayout.addWidget(checkbox)
|
|
# qhboxlayout.setAlignment(QtCore.Qt.AlignCenter)
|
|
# qhboxlayout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
plot_item = FCCheckBox()
|
|
plot_item.setLayoutDirection(QtCore.Qt.RightToLeft)
|
|
tool_uid_item = QtWidgets.QTableWidgetItem(str(dia_value['tool']))
|
|
if self.ui.plot_cb.isChecked():
|
|
plot_item.setChecked(True)
|
|
|
|
# TODO until the feature of individual plot for an Excellon tool is implemented
|
|
plot_item.setDisabled(True)
|
|
|
|
self.ui.exc_cnc_tools_table.setItem(row_no, 0, t_id) # Tool name/id
|
|
self.ui.exc_cnc_tools_table.setItem(row_no, 1, dia_item) # Diameter
|
|
self.ui.exc_cnc_tools_table.setItem(row_no, 2, nr_drills_item) # Nr of drills
|
|
self.ui.exc_cnc_tools_table.setItem(row_no, 3, nr_slots_item) # Nr of slots
|
|
|
|
# ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY # ##
|
|
self.ui.exc_cnc_tools_table.setItem(row_no, 4, tool_uid_item) # Tool unique ID)
|
|
self.ui.exc_cnc_tools_table.setItem(row_no, 5, cutz_item)
|
|
self.ui.exc_cnc_tools_table.setCellWidget(row_no, 6, plot_item)
|
|
|
|
for row in range(tool_idx):
|
|
self.ui.exc_cnc_tools_table.item(row, 0).setFlags(
|
|
self.ui.exc_cnc_tools_table.item(row, 0).flags() ^ QtCore.Qt.ItemIsSelectable)
|
|
|
|
self.ui.exc_cnc_tools_table.resizeColumnsToContents()
|
|
self.ui.exc_cnc_tools_table.resizeRowsToContents()
|
|
|
|
vertical_header = self.ui.exc_cnc_tools_table.verticalHeader()
|
|
vertical_header.hide()
|
|
self.ui.exc_cnc_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
|
|
|
horizontal_header = self.ui.exc_cnc_tools_table.horizontalHeader()
|
|
horizontal_header.setMinimumSectionSize(10)
|
|
horizontal_header.setDefaultSectionSize(70)
|
|
horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
|
|
horizontal_header.resizeSection(0, 20)
|
|
horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
|
|
horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
|
|
horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
|
|
horizontal_header.setSectionResizeMode(5, QtWidgets.QHeaderView.ResizeToContents)
|
|
|
|
horizontal_header.setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed)
|
|
|
|
# horizontal_header.setStretchLastSection(True)
|
|
self.ui.exc_cnc_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
|
|
|
self.ui.exc_cnc_tools_table.setColumnWidth(0, 20)
|
|
self.ui.exc_cnc_tools_table.setColumnWidth(6, 17)
|
|
|
|
self.ui.exc_cnc_tools_table.setMinimumHeight(self.ui.exc_cnc_tools_table.getHeight())
|
|
self.ui.exc_cnc_tools_table.setMaximumHeight(self.ui.exc_cnc_tools_table.getHeight())
|
|
|
|
def set_ui(self, ui):
|
|
FlatCAMObj.set_ui(self, ui)
|
|
|
|
log.debug("FlatCAMCNCJob.set_ui()")
|
|
|
|
assert isinstance(self.ui, CNCObjectUI), \
|
|
"Expected a CNCObjectUI, got %s" % type(self.ui)
|
|
|
|
self.units = self.app.defaults['units'].upper()
|
|
self.units_found = self.app.defaults['units']
|
|
|
|
# this signal has to be connected to it's slot before the defaults are populated
|
|
# the decision done in the slot has to override the default value set bellow
|
|
self.ui.toolchange_cb.toggled.connect(self.on_toolchange_custom_clicked)
|
|
|
|
self.form_fields.update({
|
|
"plot": self.ui.plot_cb,
|
|
"tooldia": self.ui.tooldia_entry,
|
|
"append": self.ui.append_text,
|
|
"prepend": self.ui.prepend_text,
|
|
"toolchange_macro": self.ui.toolchange_text,
|
|
"toolchange_macro_enable": self.ui.toolchange_cb
|
|
})
|
|
|
|
# Fill form fields only on object create
|
|
self.to_form()
|
|
|
|
# this means that the object that created this CNCJob was an Excellon or Geometry
|
|
try:
|
|
if self.travel_distance:
|
|
self.ui.t_distance_label.show()
|
|
self.ui.t_distance_entry.setVisible(True)
|
|
self.ui.t_distance_entry.setDisabled(True)
|
|
self.ui.t_distance_entry.set_value('%.*f' % (self.decimals, float(self.travel_distance)))
|
|
self.ui.units_label.setText(str(self.units).lower())
|
|
self.ui.units_label.setDisabled(True)
|
|
|
|
self.ui.t_time_label.show()
|
|
self.ui.t_time_entry.setVisible(True)
|
|
self.ui.t_time_entry.setDisabled(True)
|
|
# if time is more than 1 then we have minutes, else we have seconds
|
|
if self.routing_time > 1:
|
|
self.ui.t_time_entry.set_value('%.*f' % (self.decimals, math.ceil(float(self.routing_time))))
|
|
self.ui.units_time_label.setText('min')
|
|
else:
|
|
time_r = self.routing_time * 60
|
|
self.ui.t_time_entry.set_value('%.*f' % (self.decimals, math.ceil(float(time_r))))
|
|
self.ui.units_time_label.setText('sec')
|
|
self.ui.units_time_label.setDisabled(True)
|
|
except AttributeError:
|
|
pass
|
|
|
|
if self.multitool is False:
|
|
self.ui.tooldia_entry.show()
|
|
self.ui.updateplot_button.show()
|
|
else:
|
|
self.ui.tooldia_entry.hide()
|
|
self.ui.updateplot_button.hide()
|
|
|
|
# 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"])
|
|
|
|
try:
|
|
self.ui.annotation_cb.stateChanged.disconnect(self.on_annotation_change)
|
|
except (TypeError, AttributeError):
|
|
pass
|
|
self.ui.annotation_cb.stateChanged.connect(self.on_annotation_change)
|
|
|
|
# set if to display text annotations
|
|
self.ui.annotation_cb.set_value(self.app.defaults["cncjob_annotation"])
|
|
|
|
# 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.cnc_frame.hide()
|
|
else:
|
|
self.ui.level.setText(_(
|
|
'<span style="color:red;"><b>Advanced</b></span>'
|
|
))
|
|
self.ui.cnc_frame.show()
|
|
|
|
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_edit_code_click)
|
|
|
|
self.ui.tc_variable_combo.currentIndexChanged[str].connect(self.on_cnc_custom_parameters)
|
|
|
|
self.ui.cncplot_method_combo.activated_custom.connect(self.on_plot_kind_change)
|
|
|
|
def on_cnc_custom_parameters(self, signal_text):
|
|
if signal_text == 'Parameters':
|
|
return
|
|
else:
|
|
self.ui.toolchange_text.insertPlainText('%%%s%%' % signal_text)
|
|
|
|
def ui_connect(self):
|
|
for row in range(self.ui.cnc_tools_table.rowCount()):
|
|
self.ui.cnc_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table)
|
|
self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
|
|
|
|
def ui_disconnect(self):
|
|
for row in range(self.ui.cnc_tools_table.rowCount()):
|
|
self.ui.cnc_tools_table.cellWidget(row, 6).clicked.disconnect(self.on_plot_cb_click_table)
|
|
try:
|
|
self.ui.plot_cb.stateChanged.disconnect(self.on_plot_cb_click)
|
|
except (TypeError, AttributeError):
|
|
pass
|
|
|
|
def on_updateplot_button_click(self, *args):
|
|
"""
|
|
Callback for the "Updata Plot" button. Reads the form for updates
|
|
and plots the object.
|
|
"""
|
|
self.read_form()
|
|
self.on_plot_kind_change()
|
|
|
|
def on_plot_kind_change(self):
|
|
kind = self.ui.cncplot_method_combo.get_value()
|
|
|
|
def worker_task():
|
|
with self.app.proc_container.new(_("Plotting...")):
|
|
self.plot(kind=kind)
|
|
|
|
self.app.worker_task.emit({'fcn': worker_task, 'params': []})
|
|
|
|
def on_exportgcode_button_click(self, *args):
|
|
"""
|
|
Handler activated by a button clicked when exporting GCode.
|
|
|
|
:param args:
|
|
:return:
|
|
"""
|
|
self.app.report_usage("cncjob_on_exportgcode_button")
|
|
|
|
self.read_form()
|
|
name = self.app.collection.get_active().options['name']
|
|
save_gcode = False
|
|
|
|
if 'Roland' in self.pp_excellon_name or 'Roland' in self.pp_geometry_name:
|
|
_filter_ = "RML1 Files .rol (*.rol);;All Files (*.*)"
|
|
elif 'hpgl' in self.pp_geometry_name:
|
|
_filter_ = "HPGL Files .plt (*.plt);;All Files (*.*)"
|
|
else:
|
|
save_gcode = True
|
|
_filter_ = self.app.defaults['cncjob_save_filters']
|
|
|
|
try:
|
|
dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name)
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(
|
|
caption=_("Export Machine Code ..."),
|
|
directory=dir_file_to_save,
|
|
filter=_filter_
|
|
)
|
|
except TypeError:
|
|
filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export Machine Code ..."), filter=_filter_)
|
|
|
|
filename = str(filename)
|
|
|
|
if filename == '':
|
|
self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export Machine Code cancelled ..."))
|
|
return
|
|
else:
|
|
if save_gcode is True:
|
|
used_extension = filename.rpartition('.')[2]
|
|
self.update_filters(last_ext=used_extension, filter_string='cncjob_save_filters')
|
|
|
|
new_name = os.path.split(str(filename))[1].rpartition('.')[0]
|
|
self.ui.name_entry.set_value(new_name)
|
|
self.on_name_activate(silent=True)
|
|
|
|
preamble = str(self.ui.prepend_text.get_value())
|
|
postamble = str(self.ui.append_text.get_value())
|
|
|
|
gc = self.export_gcode(filename, preamble=preamble, postamble=postamble)
|
|
if gc == 'fail':
|
|
return
|
|
|
|
if self.app.defaults["global_open_style"] is False:
|
|
self.app.file_opened.emit("gcode", filename)
|
|
self.app.file_saved.emit("gcode", filename)
|
|
self.app.inform.emit('[success] %s: %s' % (_("Machine Code file saved to"), filename))
|
|
|
|
def on_edit_code_click(self, *args):
|
|
"""
|
|
Handler activated by a button clicked when editing GCode.
|
|
|
|
:param args:
|
|
:return:
|
|
"""
|
|
|
|
self.app.proc_container.view.set_busy(_("Loading..."))
|
|
|
|
preamble = str(self.ui.prepend_text.get_value())
|
|
postamble = str(self.ui.append_text.get_value())
|
|
|
|
gco = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True)
|
|
if gco == 'fail':
|
|
return
|
|
else:
|
|
self.app.gcode_edited = gco
|
|
|
|
self.gcode_editor_tab = TextEditor(app=self.app, plain_text=True)
|
|
|
|
# add the tab if it was closed
|
|
self.app.ui.plot_tab_area.addTab(self.gcode_editor_tab, '%s' % _("Code Editor"))
|
|
self.gcode_editor_tab.setObjectName('code_editor_tab')
|
|
|
|
# delete the absolute and relative position and messages in the infobar
|
|
self.app.ui.position_label.setText("")
|
|
self.app.ui.rel_position_label.setText("")
|
|
|
|
# first clear previous text in text editor (if any)
|
|
self.gcode_editor_tab.code_editor.clear()
|
|
self.gcode_editor_tab.code_editor.setReadOnly(False)
|
|
|
|
self.gcode_editor_tab.code_editor.completer_enable = False
|
|
self.gcode_editor_tab.buttonRun.hide()
|
|
|
|
# Switch plot_area to CNCJob tab
|
|
self.app.ui.plot_tab_area.setCurrentWidget(self.gcode_editor_tab)
|
|
|
|
self.gcode_editor_tab.t_frame.hide()
|
|
# then append the text from GCode to the text editor
|
|
try:
|
|
self.gcode_editor_tab.code_editor.setPlainText(self.app.gcode_edited.getvalue())
|
|
# for line in self.app.gcode_edited:
|
|
# QtWidgets.QApplication.processEvents()
|
|
#
|
|
# proc_line = str(line).strip('\n')
|
|
# self.gcode_editor_tab.code_editor.append(proc_line)
|
|
except Exception as e:
|
|
log.debug('FlatCAMCNNJob.on_edit_code_click() -->%s' % str(e))
|
|
self.app.inform.emit('[ERROR] %s %s' % ('FlatCAMCNNJob.on_edit_code_click() -->', str(e)))
|
|
return
|
|
|
|
self.gcode_editor_tab.code_editor.moveCursor(QtGui.QTextCursor.Start)
|
|
|
|
self.gcode_editor_tab.handleTextChanged()
|
|
self.gcode_editor_tab.t_frame.show()
|
|
self.app.proc_container.view.set_idle()
|
|
|
|
self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Editor'))
|
|
|
|
def gcode_header(self, comment_start_symbol=None, comment_stop_symbol=None):
|
|
"""
|
|
Will create a header to be added to all GCode files generated by FlatCAM
|
|
|
|
:param comment_start_symbol: A symbol to be used as the first symbol in a comment
|
|
:param comment_stop_symbol: A symbol to be used as the last symbol in a comment
|
|
:return: A string with a GCode header
|
|
"""
|
|
|
|
log.debug("FlatCAMCNCJob.gcode_header()")
|
|
time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
|
|
marlin = False
|
|
hpgl = False
|
|
probe_pp = False
|
|
|
|
start_comment = comment_start_symbol if comment_start_symbol is not None else '('
|
|
stop_comment = comment_stop_symbol if comment_stop_symbol is not None else ')'
|
|
|
|
try:
|
|
for key in self.cnc_tools:
|
|
ppg = self.cnc_tools[key]['data']['ppname_g']
|
|
if 'marlin' in ppg.lower() or 'repetier' in ppg.lower():
|
|
marlin = True
|
|
break
|
|
if ppg == 'hpgl':
|
|
hpgl = True
|
|
break
|
|
if "toolchange_probe" in ppg.lower():
|
|
probe_pp = True
|
|
break
|
|
except KeyError:
|
|
# log.debug("FlatCAMCNCJob.gcode_header() error: --> %s" % str(e))
|
|
pass
|
|
|
|
try:
|
|
if 'marlin' in self.options['ppname_e'].lower() or 'repetier' in self.options['ppname_e'].lower():
|
|
marlin = True
|
|
except KeyError:
|
|
# log.debug("FlatCAMCNCJob.gcode_header(): --> There is no such self.option: %s" % str(e))
|
|
pass
|
|
|
|
try:
|
|
if "toolchange_probe" in self.options['ppname_e'].lower():
|
|
probe_pp = True
|
|
except KeyError:
|
|
# log.debug("FlatCAMCNCJob.gcode_header(): --> There is no such self.option: %s" % str(e))
|
|
pass
|
|
|
|
if marlin is True:
|
|
gcode = ';Marlin(Repetier) G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s\n' % \
|
|
(str(self.app.version), str(self.app.version_date)) + '\n'
|
|
|
|
gcode += ';Name: ' + str(self.options['name']) + '\n'
|
|
gcode += ';Type: ' + "G-code from " + str(self.options['type']) + '\n'
|
|
|
|
# if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
|
|
# gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
|
|
|
|
gcode += ';Units: ' + self.units.upper() + '\n' + "\n"
|
|
gcode += ';Created on ' + time_str + '\n' + '\n'
|
|
elif hpgl is True:
|
|
gcode = 'CO "HPGL CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s' % \
|
|
(str(self.app.version), str(self.app.version_date)) + '";\n'
|
|
|
|
gcode += 'CO "Name: ' + str(self.options['name']) + '";\n'
|
|
gcode += 'CO "Type: ' + "HPGL code from " + str(self.options['type']) + '";\n'
|
|
|
|
# if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
|
|
# gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
|
|
|
|
gcode += 'CO "Units: ' + self.units.upper() + '";\n'
|
|
gcode += 'CO "Created on ' + time_str + '";\n'
|
|
elif probe_pp is True:
|
|
gcode = '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \
|
|
(str(self.app.version), str(self.app.version_date)) + '\n'
|
|
|
|
gcode += '(This GCode tool change is done by using a Probe.)\n' \
|
|
'(Make sure that before you start the job you first do a rough zero for Z axis.)\n' \
|
|
'(This means that you need to zero the CNC axis and then jog to the toolchange X, Y location,)\n' \
|
|
'(mount the probe and adjust the Z so more or less the probe tip touch the plate. ' \
|
|
'Then zero the Z axis.)\n' + '\n'
|
|
|
|
gcode += '(Name: ' + str(self.options['name']) + ')\n'
|
|
gcode += '(Type: ' + "G-code from " + str(self.options['type']) + ')\n'
|
|
|
|
# if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
|
|
# gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
|
|
|
|
gcode += '(Units: ' + self.units.upper() + ')\n' + "\n"
|
|
gcode += '(Created on ' + time_str + ')\n' + '\n'
|
|
else:
|
|
gcode = '%sG-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s%s\n' % \
|
|
(start_comment, str(self.app.version), str(self.app.version_date), stop_comment) + '\n'
|
|
|
|
gcode += '%sName: ' % start_comment + str(self.options['name']) + '%s\n' % stop_comment
|
|
gcode += '%sType: ' % start_comment + "G-code from " + str(self.options['type']) + '%s\n' % stop_comment
|
|
|
|
# if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
|
|
# gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
|
|
|
|
gcode += '%sUnits: ' % start_comment + self.units.upper() + '%s\n' % stop_comment + "\n"
|
|
gcode += '%sCreated on ' % start_comment + time_str + '%s\n' % stop_comment + '\n'
|
|
|
|
return gcode
|
|
|
|
@staticmethod
|
|
def gcode_footer(end_command=None):
|
|
"""
|
|
Will add the M02 to the end of GCode, if requested.
|
|
|
|
:param end_command: 'M02' or 'M30' - String
|
|
:return:
|
|
"""
|
|
if end_command:
|
|
return end_command
|
|
else:
|
|
return 'M02'
|
|
|
|
def export_gcode(self, filename=None, preamble='', postamble='', to_file=False, from_tcl=False):
|
|
"""
|
|
This will save the GCode from the Gcode object to a file on the OS filesystem
|
|
|
|
:param filename: filename for the GCode file
|
|
:param preamble: a custom Gcode block to be added at the beginning of the Gcode file
|
|
:param postamble: a custom Gcode block to be added at the end of the Gcode file
|
|
:param to_file: if False then no actual file is saved but the app will know that a file was created
|
|
:param from_tcl: True if run from Tcl Shell
|
|
:return: None
|
|
"""
|
|
# gcode = ''
|
|
# roland = False
|
|
# hpgl = False
|
|
# isel_icp = False
|
|
|
|
include_header = True
|
|
|
|
try:
|
|
if self.special_group:
|
|
self.app.inform.emit('[WARNING_NOTCL] %s %s %s.' %
|
|
(_("This CNCJob object can't be processed because it is a"),
|
|
str(self.special_group),
|
|
_("CNCJob object")))
|
|
return 'fail'
|
|
except AttributeError:
|
|
pass
|
|
|
|
# if this dict is not empty then the object is a Geometry object
|
|
if self.cnc_tools:
|
|
first_key = next(iter(self.cnc_tools))
|
|
include_header = self.app.preprocessors[self.cnc_tools[first_key]['data']['ppname_g']].include_header
|
|
|
|
# if this dict is not empty then the object is an Excellon object
|
|
if self.exc_cnc_tools:
|
|
first_key = next(iter(self.exc_cnc_tools))
|
|
include_header = self.app.preprocessors[self.exc_cnc_tools[first_key]['data']['ppname_e']].include_header
|
|
|
|
# # detect if using Roland preprocessor
|
|
# try:
|
|
# for key in self.cnc_tools:
|
|
# if self.cnc_tools[key]['data']['ppname_g'] == 'Roland_MDX_20':
|
|
# roland = True
|
|
# break
|
|
# except Exception:
|
|
# try:
|
|
# for key in self.cnc_tools:
|
|
# if self.cnc_tools[key]['data']['ppname_e'] == 'Roland_MDX_20':
|
|
# roland = True
|
|
# break
|
|
# except Exception:
|
|
# pass
|
|
#
|
|
# # detect if using HPGL preprocessor
|
|
# try:
|
|
# for key in self.cnc_tools:
|
|
# if self.cnc_tools[key]['data']['ppname_g'] == 'hpgl':
|
|
# hpgl = True
|
|
# break
|
|
# except Exception:
|
|
# try:
|
|
# for key in self.cnc_tools:
|
|
# if self.cnc_tools[key]['data']['ppname_e'] == 'hpgl':
|
|
# hpgl = True
|
|
# break
|
|
# except Exception:
|
|
# pass
|
|
#
|
|
# # detect if using ISEL_ICP_CNC preprocessor
|
|
# try:
|
|
# for key in self.cnc_tools:
|
|
# if 'ISEL_ICP' in self.cnc_tools[key]['data']['ppname_g'].upper():
|
|
# isel_icp = True
|
|
# break
|
|
# except Exception:
|
|
# try:
|
|
# for key in self.cnc_tools:
|
|
# if 'ISEL_ICP' in self.cnc_tools[key]['data']['ppname_e'].upper():
|
|
# isel_icp = True
|
|
# break
|
|
# except Exception:
|
|
# pass
|
|
|
|
# do not add gcode_header when using the Roland preprocessor, add it for every other preprocessor
|
|
# if roland is False and hpgl is False and isel_icp is False:
|
|
# gcode = self.gcode_header()
|
|
|
|
# do not add gcode_header when using the Roland, HPGL or ISEP_ICP_CNC preprocessor (or any other preprocessor
|
|
# that has the include_header attribute set as False, add it for every other preprocessor
|
|
# if include_header:
|
|
# gcode = self.gcode_header()
|
|
# else:
|
|
# gcode = ''
|
|
|
|
# # detect if using multi-tool and make the Gcode summation correctly for each case
|
|
# if self.multitool is True:
|
|
# for tooluid_key in self.cnc_tools:
|
|
# for key, value in self.cnc_tools[tooluid_key].items():
|
|
# if key == 'gcode':
|
|
# gcode += value
|
|
# break
|
|
# else:
|
|
# gcode += self.gcode
|
|
|
|
# if roland is True:
|
|
# g = preamble + gcode + postamble
|
|
# elif hpgl is True:
|
|
# g = self.gcode_header() + preamble + gcode + postamble
|
|
# else:
|
|
# # fix so the preamble gets inserted in between the comments header and the actual start of GCODE
|
|
# g_idx = gcode.rfind('G20')
|
|
#
|
|
# # if it did not find 'G20' then search for 'G21'
|
|
# if g_idx == -1:
|
|
# g_idx = gcode.rfind('G21')
|
|
#
|
|
# # if it did not find 'G20' and it did not find 'G21' then there is an error and return
|
|
# # but only when the preprocessor is not ISEL_ICP who is allowed not to have the G20/G21 command
|
|
# if g_idx == -1 and isel_icp is False:
|
|
# self.app.inform.emit('[ERROR_NOTCL] %s' % _("G-code does not have a units code: either G20 or G21"))
|
|
# return
|
|
#
|
|
# footer = self.app.defaults['cncjob_footer']
|
|
# end_gcode = self.gcode_footer() if footer is True else ''
|
|
# g = gcode[:g_idx] + preamble + '\n' + gcode[g_idx:] + postamble + end_gcode
|
|
|
|
gcode = ''
|
|
if include_header is False:
|
|
g = preamble
|
|
# detect if using multi-tool and make the Gcode summation correctly for each case
|
|
if self.multitool is True:
|
|
for tooluid_key in self.cnc_tools:
|
|
for key, value in self.cnc_tools[tooluid_key].items():
|
|
if key == 'gcode':
|
|
gcode += value
|
|
break
|
|
else:
|
|
gcode += self.gcode
|
|
|
|
g = g + gcode + postamble
|
|
else:
|
|
# search for the GCode beginning which is usually a G20 or G21
|
|
# fix so the preamble gets inserted in between the comments header and the actual start of GCODE
|
|
# g_idx = gcode.rfind('G20')
|
|
#
|
|
# # if it did not find 'G20' then search for 'G21'
|
|
# if g_idx == -1:
|
|
# g_idx = gcode.rfind('G21')
|
|
#
|
|
# # if it did not find 'G20' and it did not find 'G21' then there is an error and return
|
|
# if g_idx == -1:
|
|
# self.app.inform.emit('[ERROR_NOTCL] %s' % _("G-code does not have a units code: either G20 or G21"))
|
|
# return
|
|
|
|
# detect if using multi-tool and make the Gcode summation correctly for each case
|
|
if self.multitool is True:
|
|
for tooluid_key in self.cnc_tools:
|
|
for key, value in self.cnc_tools[tooluid_key].items():
|
|
if key == 'gcode':
|
|
gcode += value
|
|
break
|
|
else:
|
|
gcode += self.gcode
|
|
|
|
end_gcode = self.gcode_footer() if self.app.defaults['cncjob_footer'] is True else ''
|
|
|
|
# detect if using a HPGL preprocessor
|
|
hpgl = False
|
|
if self.cnc_tools:
|
|
for key in self.cnc_tools:
|
|
if 'ppname_g' in self.cnc_tools[key]['data']:
|
|
if 'hpgl' in self.cnc_tools[key]['data']['ppname_g']:
|
|
hpgl = True
|
|
break
|
|
elif self.exc_cnc_tools:
|
|
for key in self.cnc_tools:
|
|
if 'ppname_e' in self.cnc_tools[key]['data']:
|
|
if 'hpgl' in self.cnc_tools[key]['data']['ppname_e']:
|
|
hpgl = True
|
|
break
|
|
|
|
if hpgl:
|
|
processed_gcode = ''
|
|
pa_re = re.compile(r"^PA\s*(-?\d+\.\d*),?\s*(-?\d+\.\d*)*;?$")
|
|
for gline in gcode.splitlines():
|
|
match = pa_re.search(gline)
|
|
if match:
|
|
x_int = int(float(match.group(1)))
|
|
y_int = int(float(match.group(2)))
|
|
new_line = 'PA%d,%d;\n' % (x_int, y_int)
|
|
processed_gcode += new_line
|
|
else:
|
|
processed_gcode += gline + '\n'
|
|
|
|
gcode = processed_gcode
|
|
g = self.gcode_header() + '\n' + preamble + '\n' + gcode + postamble + end_gcode
|
|
else:
|
|
try:
|
|
g_idx = gcode.index('G94')
|
|
g = self.gcode_header() + gcode[:g_idx + 3] + '\n\n' + preamble + '\n' + \
|
|
gcode[(g_idx + 3):] + postamble + end_gcode
|
|
except ValueError:
|
|
self.app.inform.emit('[ERROR_NOTCL] %s' %
|
|
_("G-code does not have a G94 code and we will not include the code in the "
|
|
"'Prepend to GCode' text box"))
|
|
g = self.gcode_header() + '\n' + gcode + postamble + end_gcode
|
|
|
|
# if toolchange custom is used, replace M6 code with the code from the Toolchange Custom Text box
|
|
if self.ui.toolchange_cb.get_value() is True:
|
|
# match = self.re_toolchange.search(g)
|
|
if 'M6' in g:
|
|
m6_code = self.parse_custom_toolchange_code(self.ui.toolchange_text.get_value())
|
|
if m6_code is None or m6_code == '':
|
|
self.app.inform.emit(
|
|
'[ERROR_NOTCL] %s' % _("Cancelled. The Toolchange Custom code is enabled but it's empty.")
|
|
)
|
|
return 'fail'
|
|
|
|
g = g.replace('M6', m6_code)
|
|
self.app.inform.emit('[success] %s' % _("Toolchange G-code was replaced by a custom code."))
|
|
|
|
lines = StringIO(g)
|
|
|
|
# Write
|
|
if filename is not None:
|
|
try:
|
|
force_windows_line_endings = self.app.defaults['cncjob_line_ending']
|
|
if force_windows_line_endings and sys.platform != 'win32':
|
|
with open(filename, 'w', newline='\r\n') as f:
|
|
for line in lines:
|
|
f.write(line)
|
|
else:
|
|
with open(filename, 'w') as f:
|
|
for line in lines:
|
|
f.write(line)
|
|
except FileNotFoundError:
|
|
self.app.inform.emit('[WARNING_NOTCL] %s' % _("No such file or directory"))
|
|
return
|
|
except PermissionError:
|
|
self.app.inform.emit(
|
|
'[WARNING] %s' % _("Permission denied, saving not possible.\n"
|
|
"Most likely another app is holding the file open and not accessible.")
|
|
)
|
|
return 'fail'
|
|
elif to_file is False:
|
|
# Just for adding it to the recent files list.
|
|
if self.app.defaults["global_open_style"] is False:
|
|
self.app.file_opened.emit("cncjob", filename)
|
|
self.app.file_saved.emit("cncjob", filename)
|
|
|
|
self.app.inform.emit('[success] %s: %s' % (_("Saved to"), filename))
|
|
else:
|
|
return lines
|
|
|
|
def on_toolchange_custom_clicked(self, signal):
|
|
"""
|
|
Handler for clicking toolchange custom.
|
|
|
|
:param signal:
|
|
:return:
|
|
"""
|
|
|
|
try:
|
|
if 'toolchange_custom' not in str(self.options['ppname_e']).lower():
|
|
if self.ui.toolchange_cb.get_value():
|
|
self.ui.toolchange_cb.set_value(False)
|
|
self.app.inform.emit('[WARNING_NOTCL] %s' %
|
|
_("The used preprocessor file has to have in it's name: 'toolchange_custom'"))
|
|
except KeyError:
|
|
try:
|
|
for key in self.cnc_tools:
|
|
ppg = self.cnc_tools[key]['data']['ppname_g']
|
|
if 'toolchange_custom' not in str(ppg).lower():
|
|
print(ppg)
|
|
if self.ui.toolchange_cb.get_value():
|
|
self.ui.toolchange_cb.set_value(False)
|
|
self.app.inform.emit('[WARNING_NOTCL] %s' %
|
|
_("The used preprocessor file has to have in it's name: "
|
|
"'toolchange_custom'"))
|
|
except KeyError:
|
|
self.app.inform.emit('[ERROR] %s' % _("There is no preprocessor file."))
|
|
|
|
def get_gcode(self, preamble='', postamble=''):
|
|
"""
|
|
We need this to be able to get_gcode separately for shell command export_gcode
|
|
|
|
:param preamble: Extra GCode added to the beginning of the GCode
|
|
:param postamble: Extra GCode added at the end of the GCode
|
|
:return: The modified GCode
|
|
"""
|
|
return preamble + '\n' + self.gcode + "\n" + postamble
|
|
|
|
def get_svg(self):
|
|
# we need this to be able get_svg separately for shell command export_svg
|
|
pass
|
|
|
|
def on_plot_cb_click(self, *args):
|
|
"""
|
|
Handler for clicking on the Plot checkbox.
|
|
|
|
:param args:
|
|
:return:
|
|
"""
|
|
if self.muted_ui:
|
|
return
|
|
kind = self.ui.cncplot_method_combo.get_value()
|
|
self.plot(kind=kind)
|
|
self.read_form_item('plot')
|
|
|
|
self.ui_disconnect()
|
|
cb_flag = self.ui.plot_cb.isChecked()
|
|
for row in range(self.ui.cnc_tools_table.rowCount()):
|
|
table_cb = self.ui.cnc_tools_table.cellWidget(row, 6)
|
|
if cb_flag:
|
|
table_cb.setChecked(True)
|
|
else:
|
|
table_cb.setChecked(False)
|
|
self.ui_connect()
|
|
|
|
def on_plot_cb_click_table(self):
|
|
"""
|
|
Handler for clicking the plot checkboxes added into a Table on each row. Purpose: toggle visibility for the
|
|
tool/aperture found on that row.
|
|
:return:
|
|
"""
|
|
|
|
# self.ui.cnc_tools_table.cellWidget(row, 2).widget().setCheckState(QtCore.Qt.Unchecked)
|
|
self.ui_disconnect()
|
|
# cw = self.sender()
|
|
# cw_index = self.ui.cnc_tools_table.indexAt(cw.pos())
|
|
# cw_row = cw_index.row()
|
|
|
|
kind = self.ui.cncplot_method_combo.get_value()
|
|
|
|
self.shapes.clear(update=True)
|
|
|
|
for tooluid_key in self.cnc_tools:
|
|
tooldia = float('%.*f' % (self.decimals, float(self.cnc_tools[tooluid_key]['tooldia'])))
|
|
gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
|
|
# tool_uid = int(self.ui.cnc_tools_table.item(cw_row, 3).text())
|
|
|
|
for r in range(self.ui.cnc_tools_table.rowCount()):
|
|
if int(self.ui.cnc_tools_table.item(r, 5).text()) == int(tooluid_key):
|
|
if self.ui.cnc_tools_table.cellWidget(r, 6).isChecked():
|
|
self.plot2(tooldia=tooldia, obj=self, visible=True, gcode_parsed=gcode_parsed, kind=kind)
|
|
|
|
self.shapes.redraw()
|
|
|
|
# make sure that the general plot is disabled if one of the row plot's are disabled and
|
|
# if all the row plot's are enabled also enable the general plot checkbox
|
|
cb_cnt = 0
|
|
total_row = self.ui.cnc_tools_table.rowCount()
|
|
for row in range(total_row):
|
|
if self.ui.cnc_tools_table.cellWidget(row, 6).isChecked():
|
|
cb_cnt += 1
|
|
else:
|
|
cb_cnt -= 1
|
|
if cb_cnt < total_row:
|
|
self.ui.plot_cb.setChecked(False)
|
|
else:
|
|
self.ui.plot_cb.setChecked(True)
|
|
self.ui_connect()
|
|
|
|
def plot(self, visible=None, kind='all'):
|
|
"""
|
|
# Does all the required setup and returns False
|
|
# if the 'ptint' option is set to False.
|
|
|
|
:param visible: Boolean to decide if the object will be plotted as visible or disabled on canvas
|
|
:param kind: String. Can be "all" or "travel" or "cut". For CNCJob plotting
|
|
:return: None
|
|
"""
|
|
if not FlatCAMObj.plot(self):
|
|
return
|
|
|
|
visible = visible if visible else self.options['plot']
|
|
|
|
if self.app.is_legacy is False:
|
|
if self.ui.annotation_cb.get_value() and self.ui.plot_cb.get_value():
|
|
self.text_col.enabled = True
|
|
else:
|
|
self.text_col.enabled = False
|
|
self.annotation.redraw()
|
|
|
|
try:
|
|
if self.multitool is False: # single tool usage
|
|
try:
|
|
dia_plot = float(self.options["tooldia"])
|
|
except ValueError:
|
|
# we may have a tuple with only one element and a comma
|
|
dia_plot = [float(el) for el in self.options["tooldia"].split(',') if el != ''][0]
|
|
self.plot2(dia_plot, obj=self, visible=visible, kind=kind)
|
|
else:
|
|
# multiple tools usage
|
|
if self.cnc_tools:
|
|
for tooluid_key in self.cnc_tools:
|
|
tooldia = float('%.*f' % (self.decimals, float(self.cnc_tools[tooluid_key]['tooldia'])))
|
|
gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
|
|
self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
|
|
|
|
# TODO: until the gcode parsed will be stored on each Excellon tool this will not get executed
|
|
if self.exc_cnc_tools:
|
|
for tooldia_key in self.exc_cnc_tools:
|
|
tooldia = float('%.*f' % (self.decimals, float(tooldia_key)))
|
|
# gcode_parsed = self.cnc_tools[tooldia_key]['gcode_parsed']
|
|
gcode_parsed = self.gcode_parsed
|
|
self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
|
|
|
|
self.shapes.redraw()
|
|
except (ObjectDeleted, AttributeError):
|
|
self.shapes.clear(update=True)
|
|
if self.app.is_legacy is False:
|
|
self.annotation.clear(update=True)
|
|
|
|
def on_annotation_change(self):
|
|
"""
|
|
Handler for toggling the annotation display by clicking a checkbox.
|
|
:return:
|
|
"""
|
|
|
|
if self.app.is_legacy is False:
|
|
if self.ui.annotation_cb.get_value():
|
|
self.text_col.enabled = True
|
|
else:
|
|
self.text_col.enabled = False
|
|
# kind = self.ui.cncplot_method_combo.get_value()
|
|
# self.plot(kind=kind)
|
|
self.annotation.redraw()
|
|
else:
|
|
kind = self.ui.cncplot_method_combo.get_value()
|
|
self.plot(kind=kind)
|
|
|
|
def convert_units(self, units):
|
|
"""
|
|
Units conversion used by the CNCJob objects.
|
|
|
|
:param units: Can be "MM" or "IN"
|
|
:return:
|
|
"""
|
|
|
|
log.debug("FlatCAMObj.FlatCAMECNCjob.convert_units()")
|
|
|
|
factor = CNCjob.convert_units(self, units)
|
|
self.options["tooldia"] = float(self.options["tooldia"]) * factor
|
|
|
|
param_list = ['cutz', 'depthperpass', 'travelz', 'feedrate', 'feedrate_z', 'feedrate_rapid',
|
|
'endz', 'toolchangez']
|
|
|
|
temp_tools_dict = {}
|
|
tool_dia_copy = {}
|
|
data_copy = {}
|
|
|
|
for tooluid_key, tooluid_value in self.cnc_tools.items():
|
|
for dia_key, dia_value in tooluid_value.items():
|
|
if dia_key == 'tooldia':
|
|
dia_value *= factor
|
|
dia_value = float('%.*f' % (self.decimals, dia_value))
|
|
tool_dia_copy[dia_key] = dia_value
|
|
if dia_key == 'offset':
|
|
tool_dia_copy[dia_key] = dia_value
|
|
if dia_key == 'offset_value':
|
|
dia_value *= factor
|
|
tool_dia_copy[dia_key] = dia_value
|
|
|
|
if dia_key == 'type':
|
|
tool_dia_copy[dia_key] = dia_value
|
|
if dia_key == 'tool_type':
|
|
tool_dia_copy[dia_key] = dia_value
|
|
if dia_key == 'data':
|
|
for data_key, data_value in dia_value.items():
|
|
# convert the form fields that are convertible
|
|
for param in param_list:
|
|
if data_key == param and data_value is not None:
|
|
data_copy[data_key] = data_value * factor
|
|
# copy the other dict entries that are not convertible
|
|
if data_key not in param_list:
|
|
data_copy[data_key] = data_value
|
|
tool_dia_copy[dia_key] = deepcopy(data_copy)
|
|
data_copy.clear()
|
|
|
|
if dia_key == 'gcode':
|
|
tool_dia_copy[dia_key] = dia_value
|
|
if dia_key == 'gcode_parsed':
|
|
tool_dia_copy[dia_key] = dia_value
|
|
if dia_key == 'solid_geometry':
|
|
tool_dia_copy[dia_key] = dia_value
|
|
|
|
# if dia_key == 'solid_geometry':
|
|
# tool_dia_copy[dia_key] = affinity.scale(dia_value, xfact=factor, origin=(0, 0))
|
|
# if dia_key == 'gcode_parsed':
|
|
# for g in dia_value:
|
|
# g['geom'] = affinity.scale(g['geom'], factor, factor, origin=(0, 0))
|
|
#
|
|
# tool_dia_copy['gcode_parsed'] = deepcopy(dia_value)
|
|
# tool_dia_copy['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_value])
|
|
|
|
temp_tools_dict.update({
|
|
tooluid_key: deepcopy(tool_dia_copy)
|
|
})
|
|
tool_dia_copy.clear()
|
|
|
|
self.cnc_tools.clear()
|
|
self.cnc_tools = deepcopy(temp_tools_dict)
|